diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index e338fea06a06..08ee25bd1f0d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -186,6 +186,12 @@ protected Response() { } public static implicit operator T (Azure.Response response) { throw null; } public override string ToString() { throw null; } } + public partial class SyncAsyncEventArgs : System.EventArgs + { + public SyncAsyncEventArgs(bool runSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public System.Threading.CancellationToken CancellationToken { get { throw null; } } + public bool RunSynchronously { get { throw null; } } + } } namespace Azure.Core { @@ -410,6 +416,7 @@ internal RetryOptions() { } public Azure.Core.RetryMode Mode { get { throw null; } set { } } public System.TimeSpan NetworkTimeout { get { throw null; } set { } } } + public delegate System.Threading.Tasks.Task SyncAsyncEventHandler(T e) where T : Azure.SyncAsyncEventArgs; public abstract partial class TokenCredential { protected TokenCredential() { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs index 2522846ea306..76cca4e18084 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -186,6 +186,12 @@ protected Response() { } public static implicit operator T (Azure.Response response) { throw null; } public override string ToString() { throw null; } } + public partial class SyncAsyncEventArgs : System.EventArgs + { + public SyncAsyncEventArgs(bool runSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public System.Threading.CancellationToken CancellationToken { get { throw null; } } + public bool RunSynchronously { get { throw null; } } + } } namespace Azure.Core { @@ -410,6 +416,7 @@ internal RetryOptions() { } public Azure.Core.RetryMode Mode { get { throw null; } set { } } public System.TimeSpan NetworkTimeout { get { throw null; } set { } } } + public delegate System.Threading.Tasks.Task SyncAsyncEventHandler(T e) where T : Azure.SyncAsyncEventArgs; public abstract partial class TokenCredential { protected TokenCredential() { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index e338fea06a06..08ee25bd1f0d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -186,6 +186,12 @@ protected Response() { } public static implicit operator T (Azure.Response response) { throw null; } public override string ToString() { throw null; } } + public partial class SyncAsyncEventArgs : System.EventArgs + { + public SyncAsyncEventArgs(bool runSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public System.Threading.CancellationToken CancellationToken { get { throw null; } } + public bool RunSynchronously { get { throw null; } } + } } namespace Azure.Core { @@ -410,6 +416,7 @@ internal RetryOptions() { } public Azure.Core.RetryMode Mode { get { throw null; } set { } } public System.TimeSpan NetworkTimeout { get { throw null; } set { } } } + public delegate System.Threading.Tasks.Task SyncAsyncEventHandler(T e) where T : Azure.SyncAsyncEventArgs; public abstract partial class TokenCredential { protected TokenCredential() { } diff --git a/sdk/core/Azure.Core/samples/Events.md b/sdk/core/Azure.Core/samples/Events.md new file mode 100644 index 000000000000..76b020e8354f --- /dev/null +++ b/sdk/core/Azure.Core/samples/Events.md @@ -0,0 +1,199 @@ +# Azure.Core Event samples + +**NOTE:** Samples in this file only apply to packages following the +[Azure SDK Design Guidelines](https://azure.github.io/azure-sdk/dotnet_introduction.html). +The names of these packages usually start with `Azure`. + +Most Azure client libraries for .NET offer both synchronous and asynchronous +methods for calling Azure services. You can distinguish the asynchronous +methods by their `Async` suffix. For example, `BlobClient.Download` and +`BlobClient.DownloadAsync` make the same underlying REST call and only differ in +whether they block. We recommend using our async methods for new applications, +but there are perfectly valid cases for using sync methods as well. These dual +method invocation semantics allow for flexibility, but require a little extra +care when writing event handlers. + +The `SyncAsyncEventHandler` is a delegate used by events in Azure client +libraries to represent an event handler that can be invoked from either sync or +async code paths. It takes event arguments deriving from `SyncAsyncEventArgs` +that contain important information for writing your event handler. + +- `SyncAsyncEventArgs.CancellationToken` is a cancellation token related to the + original operation that raised the event. It's important for your handler to + pass this token along to any asynchronous or long-running synchronous + operations that take a token so cancellation (via something like + `new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token`, for example) + will correctly propagate. + +- There is a `SyncAsyncEventArgs.RunSynchronously` flag indicating whether your + handler was invoked synchronously or asynchronously. In general, + + - If you're calling sync methods on your client, you should use sync methods + to implement your event handler. You can return `Task.CompletedTask`. + - If you're calling async methods on your client, you should use async + methods where possible to implement your event handler. + - If you're not in control of how the client will be used or want to write + safer code, you should check the `RunSynchronously` property and call + either sync or async methods as directed. + + There are code examples of all three situations below to compare. Please also + see the note at the very end discussing the dangers of sync-over-async to + understand the risks of not using the `RunSynchronously` flag. + +- Most events will customize the event data by deriving from `SyncAsyncEventArgs` + and including details about what triggered the event or providing options to + react. Many times this will include a reference to the client that raised the + event in case you need it for additional processing. + +When an event using `SyncAsyncEventHandler` is raised, the handlers will be +executed sequentially to avoid introducing any unintended parallelism. The +event handlers will finish before returning control to the code path raising the +event. This means blocking for events raised synchronously and waiting for the +returned `Task` to complete for events raised asynchronously. + +Any exceptions thrown from a handler will be wrapped in a single +`AggregateException`. If one handler throws an exception, it will not prevent +other handlers from running. This is also relevant for cancellation because all +handlers are still raised if cancellation occurs. You should both pass +`SyncAsyncEventArgs.CancellationToken` to asynchronous or long-running +synchronous operations and consider calling `CancellationToken.ThrowIfCancellationRequested` +in compute heavy handlers. + +A [distributed tracing span](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Diagnostics.md#distributed-tracing) +is wrapped around your handlers using the event name so you can see how long +your handlers took to run, whether they made other calls to Azure services, and +details about any exceptions that were thrown. + +The rest of the code samples are using a fictitious `AlarmClient` to demonstrate +how to handle `SyncAsyncEventHandler` events. There are `Snooze` and +`SnoozeAsync` methods that both raise a `Ring` event. + +## Adding a synchronous event handler + +If you're using the synchronous, blocking methods of a client (i.e., methods +without an `Async` suffix), they will raise events that require handlers to +execute synchronously as well. Even though the signature of your handler +returns a `Task`, you should write regular sync code that blocks and return +`Task.CompletedTask` when finished. + +```C# Snippet:Azure_Core_Samples_EventSamples_SyncHandler +var client = new AlarmClient(); +client.Ring += (SyncAsyncEventArgs e) => +{ + Console.WriteLine("Wake up!"); + return Task.CompletedTask; +}; + +client.Snooze(); +``` + +If you need to call an async method from a synchronous event handler, you have +two options: + +- You can use [`Task.Run`](https://docs.microsoft.com/dotnet/api/system.threading.tasks.task.run) + to queue a task for execution on the ThreadPool without waiting on it to + complete. This "fire and forget" approach may not run before your handler + finishes executing. Be sure to understand + [exception handling in the Task Parallel Library](https://docs.microsoft.com/dotnet/standard/parallel-programming/exception-handling-task-parallel-library) + to avoid unhandled exceptions tearing down your process. +- If you absolutely need the async method to execute before returning from your + handler, you can call `myAsyncTask.GetAwaiter().GetResult()`. Please be aware + this may cause ThreadPool starvation. See the sync-over-async note below for + more details. + +## Adding an asynchronous event handler + +If you're using the asynchronous, non-blocking methods of a client (i.e., +methods with an `Async` suffix), they will raise events that expect handlers to +execute asynchronously. + +```C# Snippet:Azure_Core_Samples_EventSamples_AsyncHandler +var client = new AlarmClient(); +client.Ring += async (SyncAsyncEventArgs e) => +{ + await Console.Out.WriteLineAsync("Wake up!"); +}; + +await client.SnoozeAsync(); +``` + +## Handlers that can be called sync or async + +The same event can be raised from both synchronous and asynchronous code paths +depending on whether you're calling sync or async methods on a client. If you +write an async handler but raise it from a sync method, the handler will be +doing sync-over-async and may cause ThreadPool starvation. See the note at the +bottom for more details. + +You should use the `SyncAsyncEventArgs.RunSynchronously` property to check how +the event is being raised and implement your handler accordingly. Here's an +example handler that's safe to invoke from both sync and async code paths. + +```C# Snippet:Azure_Core_Samples_EventSamples_CombinedHandler +var client = new AlarmClient(); +client.Ring += async (SyncAsyncEventArgs e) => +{ + if (e.RunSynchronously) + { + Console.WriteLine("Wake up!"); + } + else + { + await Console.Out.WriteLineAsync("Wake up!"); + } +}; + +client.Snooze(); // sync call that blocks +await client.SnoozeAsync(); // async call that doesn't block +``` + +## Handling exceptions + +Any exceptions thrown by an event handler will be wrapped in a single +[`AggregateException`](https://docs.microsoft.com/dotnet/api/system.aggregateexception) and thrown from the code that raised the event. You can check the +[`AggregateException.InnerExceptions`](https://docs.microsoft.com/dotnet/api/system.aggregateexception.innerexceptions) +property to see the original exceptions thrown by your event handlers. +`AggregateException` also provides +[a number of helpful methods](https://docs.microsoft.com/archive/msdn-magazine/2009/brownfield/aggregating-exceptions) +like `Flatten` and `Handle` to make complex failures easier to work with. + +```C# Snippet:Azure_Core_Samples_EventSamples_Exceptions +var client = new AlarmClient(); +client.Ring += (SyncAsyncEventArgs e) => + throw new InvalidOperationException("Alarm unplugged."); + +try +{ + client.Snooze(); +} +catch (AggregateException ex) +{ + ex.Handle(e => e is InvalidOperationException); + Console.WriteLine("Please switch to your backup alarm."); +} +``` + +## Sync-over-async + +Executing asynchronous code from a sync code path is commonly referred to as +sync-over-async because you're getting sync behavior but still invoking all the +async machinery. See +[Diagnosing .NET Core ThreadPool Starvation with PerfView](https://docs.microsoft.com/archive/blogs/vancem/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall) +for a detailed explanation of how that can cause serious performance problems. +We recommend you use the `SyncAsyncEventArgs.RunSynchronously` flag to avoid +ThreadPool starvation. + +But what about executing synchronous code on an async code path like the "Adding +a synchronous event handler" code sample above? This is perfectly okay. Behind +the scenes, we're effectively doing something like: + +```C# +var task = InvokeHandler(); +if (!task.IsCompleted) +{ + task.Wait(); +} +``` + +Writing sync code in your handler will block before returning a completed `Task` +so there's no need to involve the ThreadPool to run your handler. diff --git a/sdk/core/Azure.Core/samples/README.md b/sdk/core/Azure.Core/samples/README.md index a4f764b3878e..802bf5df6115 100644 --- a/sdk/core/Azure.Core/samples/README.md +++ b/sdk/core/Azure.Core/samples/README.md @@ -14,4 +14,6 @@ description: Samples for the Azure.Core client library - [Response](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Response.md) - [Pipeline](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Pipeline.md) - [Long Running Operations](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/LongRunningOperations.md) +- [Events](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Events.md) +- [Diagnostics](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Diagnostics.md) - [Mocking](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Mocking.md) diff --git a/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs b/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs new file mode 100644 index 000000000000..c2d908730ffc --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Azure.Core.Pipeline; + +namespace Azure.Core +{ + /// + /// Extensions for raising + /// events. + /// + internal static class SyncAsyncEventHandlerExtensions + { + /// + /// Raise an + /// event by executing each of the handlers sequentially (to avoid + /// introducing accidental parallelism in customer code) and collecting + /// any exceptions. + /// + /// Type of the event arguments. + /// The event's delegate. + /// + /// An instance that contains the + /// event data. + /// + /// + /// The name of the type declaring the event to construct a helpful + /// exception message and distributed tracing span. + /// + /// + /// The name of the event to construct a helpful exception message and + /// distributed tracing span. + /// + /// + /// Client diagnostics to wrap all the handlers in a new distributed + /// tracing span. + /// + /// + /// A task that represents running all of the event's handlers. + /// + /// + /// An exception was thrown during the execution of at least one of the + /// event's handlers. + /// + /// + /// Thrown when , , + /// , or + /// are null. + /// + /// + /// Thrown when or + /// are empty. + /// + public static async Task RaiseAsync( + this SyncAsyncEventHandler eventHandler, + T e, + string declaringTypeName, + string eventName, + ClientDiagnostics clientDiagnostics) + where T : SyncAsyncEventArgs + { + Argument.AssertNotNull(e, nameof(e)); + Argument.AssertNotNullOrEmpty(declaringTypeName, nameof(declaringTypeName)); + Argument.AssertNotNullOrEmpty(eventName, nameof(eventName)); + Argument.AssertNotNull(clientDiagnostics, nameof(clientDiagnostics)); + + // Get the invocation list, but return early if there's no work + if (eventHandler == null) { return; } + Delegate[] handlers = eventHandler.GetInvocationList(); + if (handlers == null || handlers.Length == 0) { return; } + + // Wrap handler invocation in a distributed tracing span so it's + // easy for customers to track and measure + string eventFullName = declaringTypeName + "." + eventName; + using DiagnosticScope scope = clientDiagnostics.CreateScope(eventFullName); + scope.Start(); + try + { + // Collect any exceptions raised by handlers + List failures = null; + + // Raise the handlers sequentially so we don't introduce any + // unintentional parallelism in customer code + foreach (Delegate handler in handlers) + { + SyncAsyncEventHandler azureHandler = (SyncAsyncEventHandler)handler; + try + { + Task runHandlerTask = azureHandler(e); + // We can consider logging something when e.RunSynchronously + // is true, but runHandlerTask.IsComplete is false. + // (We'll not bother to check our tests because + // EnsureCompleted on the code path that raised the + // event will catch it for us.) + await runHandlerTask.ConfigureAwait(false); + } + catch (Exception ex) + { + failures ??= new List(); + failures.Add(ex); + } + } + + // Wrap any exceptions in an AggregateException + if (failures?.Count > 0) + { + // Include the event name in the exception for easier debugging + throw new AggregateException( + "Unhandled exception(s) thrown when raising the " + eventFullName + " event.", + failures); + } + } + catch (Exception ex) + { + scope.Failed(ex); + throw; + } + } + } +} diff --git a/sdk/core/Azure.Core/src/SyncAsyncEventArgs.cs b/sdk/core/Azure.Core/src/SyncAsyncEventArgs.cs new file mode 100644 index 000000000000..8b8c5a9bd03f --- /dev/null +++ b/sdk/core/Azure.Core/src/SyncAsyncEventArgs.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; + +namespace Azure +{ + /// + /// Provides data for + /// events that can be invoked either synchronously or asynchronously. + /// + public class SyncAsyncEventArgs : EventArgs + { + /// + /// Gets a value indicating whether the event handler was invoked + /// synchronously or asynchronously. Please see + /// for more details. + /// + /// + /// + /// The same + /// event can be raised from both synchronous and asynchronous code + /// paths depending on whether you're calling sync or async methods on + /// a client. If you write an async handler but raise it from a sync + /// method, the handler will be doing sync-over-async and may cause + /// ThreadPool starvation. See + /// + /// Diagnosing .NET Core ThreadPool Starvation with PerfView for + /// a detailed explanation of how that can cause ThreadPool starvation + /// and serious performance problems. + /// + /// + /// You can use this property to check + /// how the event is being raised and implement your handler + /// accordingly. Here's an example handler that's safe to invoke from + /// both sync and async code paths. + /// + /// var client = new AlarmClient(); + /// client.Ring += async (SyncAsyncEventArgs e) => + /// { + /// if (e.RunSynchronously) + /// { + /// Console.WriteLine("Wake up!"); + /// } + /// else + /// { + /// await Console.Out.WriteLineAsync("Wake up!"); + /// } + /// }; + /// + /// client.Snooze(); // sync call that blocks + /// await client.SnoozeAsync(); // async call that doesn't block + /// + /// + /// + public bool RunSynchronously { get; } + + /// + /// Gets a cancellation token related to the original operation that + /// raised the event. It's important for your handler to pass this + /// token along to any asynchronous or long-running synchronous + /// operations that take a token so cancellation (via something like + /// + /// new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token + /// + /// for example) will correctly propagate. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// A value indicating whether the event handler was invoked + /// synchronously or asynchronously. Please see + /// for more details. + /// + /// + /// A cancellation token related to the original operation that raised + /// the event. It's important for your handler to pass this token + /// along to any asynchronous or long-running synchronous operations + /// that take a token so cancellation will correctly propagate. The + /// default value is . + /// + public SyncAsyncEventArgs(bool runSynchronously, CancellationToken cancellationToken = default) + : base() + { + RunSynchronously = runSynchronously; + CancellationToken = cancellationToken; + } + } +} diff --git a/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs b/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs new file mode 100644 index 000000000000..0de375d646b2 --- /dev/null +++ b/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; + +namespace Azure.Core +{ + /// + /// Represents a method that can handle an event and execute either + /// synchronously or asynchronously. + /// + /// + /// Type of the event arguments deriving or equal to + /// . + /// + /// + /// An instance that contains the event + /// data. + /// + /// + /// A task that represents the handler. You can return + /// if implementing a sync handler. + /// Please see the Remarks section for more details. + /// + /// + /// + /// If you're using the synchronous, blocking methods of a client (i.e., + /// methods without an Async suffix), they will raise events that require + /// handlers to execute synchronously as well. Even though the signature + /// of your handler returns a , you should write regular + /// sync code that blocks and return when + /// finished. + /// + /// var client = new AlarmClient(); + /// client.Ring += (SyncAsyncEventArgs e) => + /// { + /// Console.WriteLine("Wake up!"); + /// return Task.CompletedTask; + /// }; + /// + /// client.Snooze(); + /// + /// If you need to call an async method from a synchronous event handler, + /// you have two options. You can use to + /// queue a task for execution on the ThreadPool without waiting on it to + /// complete. This "fire and forget" approach may not run before your + /// handler finishes executing. Be sure to understand + /// + /// exception handling in the Task Parallel Library to avoid + /// unhandled exceptions tearing down your process. If you absolutely need + /// the async method to execute before returning from your handler, you can + /// call myAsyncTask.GetAwaiter().GetResult(). Please be aware + /// this may cause ThreadPool starvation. See the sync-over-async note in + /// Remarks for more details. + /// + /// + /// If you're using the asynchronous, non-blocking methods of a client + /// (i.e., methods with an Async suffix), they will raise events that + /// expect handlers to execute asynchronously. + /// + /// var client = new AlarmClient(); + /// client.Ring += async (SyncAsyncEventArgs e) => + /// { + /// await Console.Out.WriteLineAsync("Wake up!"); + /// }; + /// + /// await client.SnoozeAsync(); + /// + /// + /// + /// The same event can be raised from both synchronous and asynchronous + /// code paths depending on whether you're calling sync or async methods + /// on a client. If you write an async handler but raise it from a sync + /// method, the handler will be doing sync-over-async and may cause + /// ThreadPool starvation. See the note in Remarks for more details. You + /// should use the + /// property to check how the event is being raised and implement your + /// handler accordingly. Here's an example handler that's safe to invoke + /// from both sync and async code paths. + /// + /// var client = new AlarmClient(); + /// client.Ring += async (SyncAsyncEventArgs e) => + /// { + /// if (e.RunSynchronously) + /// { + /// Console.WriteLine("Wake up!"); + /// } + /// else + /// { + /// await Console.Out.WriteLineAsync("Wake up!"); + /// } + /// }; + /// + /// client.Snooze(); // sync call that blocks + /// await client.SnoozeAsync(); // async call that doesn't block + /// + /// + /// + /// + /// + /// + /// Any exceptions thrown by an event handler will be wrapped in a single + /// AggregateException and thrown from the code that raised the event. You + /// can check the property + /// to see the original exceptions thrown by your event handlers. + /// AggregateException also provides + /// + /// a number of helpful methods like + /// and + /// to make + /// complex failures easier to work with. + /// + /// var client = new AlarmClient(); + /// client.Ring += (SyncAsyncEventArgs e) => + /// throw new InvalidOperationException("Alarm unplugged."); + /// + /// try + /// { + /// client.Snooze(); + /// } + /// catch (AggregateException ex) + /// { + /// ex.Handle(e => e is InvalidOperationException); + /// Console.WriteLine("Please switch to your backup alarm."); + /// } + /// + /// + /// + /// + /// Most Azure client libraries for .NET offer both synchronous and + /// asynchronous methods for calling Azure services. You can distinguish + /// the asynchronous methods by their Async suffix. For example, + /// BlobClient.Download and BlobClient.DownloadAsync make the same + /// underlying REST call and only differ in whether they block. We + /// recommend using our async methods for new applications, but there are + /// perfectly valid cases for using sync methods as well. These dual + /// method invocation semantics allow for flexibility, but require a little + /// extra care when writing event handlers. + /// + /// + /// The SyncAsyncEventHandler is a delegate used by events in Azure client + /// libraries to represent an event handler that can be invoked from either + /// sync or async code paths. It takes event arguments deriving from + /// that contain important information for + /// writing your event handler: + /// + /// + /// + /// is a cancellation + /// token related to the original operation that raised the event. It's + /// important for your handler to pass this token along to any asynchronous + /// or long-running synchronous operations that take a token so cancellation + /// (via something like + /// new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token, + /// for example) will correctly propagate. + /// + /// + /// + /// + /// is a flag indicating + /// whether your handler was invoked synchronously or asynchronously. If + /// you're calling sync methods on your client, you should use sync methods + /// to implement your event handler (you can return + /// ). If you're calling async methods on + /// your client, you should use async methods where possible to implement + /// your event handler. If you're not in control of how the client will be + /// used or want to write safer code, you should check the + /// property and call + /// either sync or async methods as directed. + /// + /// + /// + /// + /// Most events will customize the event data by deriving from + /// and including details about what + /// triggered the event or providing options to react. Many times this + /// will include a reference to the client that raised the event in case + /// you need it for additional processing. + /// + /// + /// + /// + /// + /// When an event using SyncAsyncEventHandler is raised, the handlers will + /// be executed sequentially to avoid introducing any unintended + /// parallelism. The event handlers will finish before returning control + /// to the code path raising the event. This means blocking for events + /// raised synchronously and waiting for the returned to + /// complete for events raised asynchronously. + /// + /// + /// Any exceptions thrown from a handler will be wrapped in a single + /// . If one handler throws an exception, + /// it will not prevent other handlers from running. This is also relevant + /// for cancellation because all handlers are still raised if cancellation + /// occurs. You should both pass + /// to asynchronous or long-running synchronous operations and consider + /// calling + /// in compute heavy handlers. + /// + /// + /// A + /// distributed tracing span is wrapped around your handlers using + /// the event name so you can see how long your handlers took to run, + /// whether they made other calls to Azure services, and details about any + /// exceptions that were thrown. + /// + /// + /// Executing asynchronous code from a sync code path is commonly referred + /// to as sync-over-async because you're getting sync behavior but still + /// invoking all the async machinery. See + /// + /// Diagnosing.NET Core ThreadPool Starvation with PerfView + /// for a detailed explanation of how that can cause serious performance + /// problems. We recommend you use the + /// flag to avoid + /// ThreadPool starvation. + /// + /// + public delegate Task SyncAsyncEventHandler(T e) + where T : SyncAsyncEventArgs; + + // NOTE: You should always use SyncAsyncEventHandlerExtensions.RaiseAsync + // in Azure.Core's shared source to ensure consistent event handling + // semantics. +} diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 339cb2dc29ff..19c3058fc5cb 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -21,17 +21,18 @@ - - - - - - - - - - - + + + + + + + + + + + + diff --git a/sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs b/sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs new file mode 100644 index 000000000000..8ff367fc494a --- /dev/null +++ b/sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs @@ -0,0 +1,700 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class SyncAsyncEventHandlerTests : ClientTestBase + { + public SyncAsyncEventHandlerTests(bool isAsync) : base(isAsync) { } + + #region Helpers + private TestClient GetClient(TimeSpan? workDelay = null) => + InstrumentClient(new TestClient(workDelay)); + + private ClientDiagnostics GetEmptyDiagnostics() => new ClientDiagnostics(new TestClientOptions()); + + public class TestClientOptions : ClientOptions { } + + public class TestSyncAsyncEventArgs : SyncAsyncEventArgs + { + public TestClient Client { get; } + public int Result { get; } + public TestSyncAsyncEventArgs(TestClient client, int result, bool runSynchronously, CancellationToken cancellationToken = default) + : base(runSynchronously, cancellationToken) + { + Client = client; + Result = result; + } + } + + public class TestClient + { + internal virtual ClientDiagnostics ClientDiagnostics { get; } = new ClientDiagnostics(new TestClientOptions()); + public TimeSpan? WorkDelay { get; } + public TestClient() : this(null) { } + public TestClient(TimeSpan? workDelay = null) => WorkDelay = workDelay; + + public virtual event SyncAsyncEventHandler Working; + public virtual event SyncAsyncEventHandler WorkCompleted; + + protected virtual async Task OnWorkingAsync(bool runSynchronously, CancellationToken cancellationToken) => + await Working.RaiseAsync( + new SyncAsyncEventArgs(runSynchronously, cancellationToken), + nameof(TestClient), + nameof(Working), + ClientDiagnostics) + .ConfigureAwait(false); + + protected virtual async Task OnWorkCompletedAsync(int result, bool runSynchronously, CancellationToken cancellationToken) => + await WorkCompleted.RaiseAsync( + new TestSyncAsyncEventArgs(this, result, runSynchronously, cancellationToken), + nameof(TestClient), + nameof(Working), + ClientDiagnostics) + .ConfigureAwait(false); + + public virtual int DoWork(CancellationToken cancellationToken = default) => + DoWorkInternal(async: false, cancellationToken).EnsureCompleted(); + public virtual async Task DoWorkAsync(CancellationToken cancellationToken = default) => + await DoWorkInternal(async: true, cancellationToken).ConfigureAwait(false); + + private async Task DoWorkInternal(bool async, CancellationToken cancellationToken) + { + using DiagnosticScope scope = ClientDiagnostics.CreateScope($"{nameof(TestClient)}.{nameof(DoWork)}"); + try + { + scope.Start(); + await OnWorkingAsync(!async, cancellationToken).ConfigureAwait(false); + int result = 42; + if (WorkDelay != null) + { + if (async) + { + await Task.Delay(WorkDelay.Value, cancellationToken).ConfigureAwait(false); + } + else + { + cancellationToken.WaitHandle.WaitOne(WorkDelay.Value); + } + } + await OnWorkCompletedAsync(result, !async, cancellationToken).ConfigureAwait(false); + return result; + } + catch (Exception ex) + { + scope.Failed(ex); + throw; + } + } + } + + private class TestHandler where T : SyncAsyncEventArgs + { + public TimeSpan? Delay { get; set; } + public Func Callback { get; set; } + public string Throws { get; set; } + + public T LastEventArgs { get; private set; } + public bool Raised => RaisedCount > 0; + public int RaisedCount { get; private set; } = 0; + public bool Completed => CompletedCount > 0; + public int CompletedCount { get; private set; } = 0; + + public async Task Handle(T e) + { + // This is expensive enough we're not doing it in RaiseAsync + // between handlers, but customers could/should do it themselves + // by passing the CancellationToken to any APIs that take it. + e.CancellationToken.ThrowIfCancellationRequested(); + + LastEventArgs = e; + RaisedCount++; + + if (Delay != null) + { + if (e.RunSynchronously) + { + e.CancellationToken.WaitHandle.WaitOne(Delay.Value); + } + else + { + await Task.Delay(Delay.Value, e.CancellationToken); + } + } + + Func callback = Callback; + if (callback != null) + { + await callback(e.RunSynchronously, e.CancellationToken); + } + + if (Throws != null) + { + throw new InvalidOperationException(Throws); + } + + e.CancellationToken.ThrowIfCancellationRequested(); + CompletedCount++; + } + } + #endregion + + #region Basic Mechanics + [Test] + public void AddHandler() + { + TestClient client = GetClient(); + var test = new TestHandler(); + client.Working += test.Handle; + } + + [Test] + public void AddHandler_Null() + { + TestClient client = GetClient(); + client.Working += null; + } + + [Test] + public void AddHandler_Multiple() + { + TestClient client = GetClient(); + client.Working += new TestHandler().Handle; + client.Working += new TestHandler().Handle; + client.Working += new TestHandler().Handle; + client.Working += new TestHandler().Handle; + } + + [Test] + public void RemoveHandler() + { + TestClient client = GetClient(); + var test = new TestHandler(); + client.Working += test.Handle; + client.Working -= test.Handle; + } + + [Test] + public void RemoveHandler_Null() + { + TestClient client = GetClient(); + client.Working -= null; + } + + [Test] + public void RemoveHandler_NotAdded() + { + TestClient client = GetClient(); + var test = new TestHandler(); + client.Working -= test.Handle; + } + + [Test] + public async Task Raise() + { + var test = new TestHandler(); + + TestClient client = GetClient(); + client.Working += test.Handle; + await client.DoWorkAsync(); + + Assert.IsTrue(test.Raised); + Assert.IsTrue(test.Completed); + } + + [Test] + public async Task Raise_None() + { + TestClient client = GetClient(); + await client.DoWorkAsync(); + } + + [Test] + public async Task Raise_Null() + { + TestClient client = GetClient(); + client.Working += null; + await client.DoWorkAsync(); + } + + [Test] + public async Task RemoveHandler_NotRaised() + { + var test = new TestHandler(); + + TestClient client = GetClient(); + client.Working += test.Handle; + client.Working -= test.Handle; + await client.DoWorkAsync(); + + Assert.IsFalse(test.Raised); + } + + [Test] + public async Task AddHandler_Duplicate() + { + var test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + + TestClient client = GetClient(); + client.Working += test.Handle; + client.Working += test.Handle; + await client.DoWorkAsync(); + + Assert.AreEqual(2, test.RaisedCount); + } + + [Test] + public async Task RemoveHandler_Duplicate() + { + var test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + + TestClient client = GetClient(); + client.Working += test.Handle; + client.Working += test.Handle; + client.Working -= test.Handle; + await client.DoWorkAsync(); + + Assert.AreEqual(1, test.RaisedCount); + } + + [Test] + public async Task RemoveHandler_OthersUnaffected() + { + var first = new TestHandler(); + var second = new TestHandler(); + var third = new TestHandler(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + + client.Working -= second.Handle; + + await client.DoWorkAsync(); + + Assert.IsTrue(first.Completed); + Assert.IsTrue(third.Completed); + + Assert.IsFalse(second.Raised); + } + + [Test] + public async Task Raise_DistributedTracing_AddsEventSpan() + { + using ClientDiagnosticListener diagnosticListener = + new ClientDiagnosticListener( + s => s.StartsWith("Azure."), + asyncLocal: true); + TestClient client = GetClient(); + client.Working += + (SyncAsyncEventArgs e) => + { + ClientDiagnosticListener.ProducedDiagnosticScope scope = + diagnosticListener.Scopes.FirstOrDefault( + s => s.Name == $"{nameof(TestClient)}.{nameof(TestClient.Working)}"); + Assert.IsNotNull(scope); + return Task.CompletedTask; + }; + await client.DoWorkAsync(); + } + #endregion + + #region Control Flow + [Test] + public async Task Raise_Waits() + { + var test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + + TestClient client = GetClient(); + client.Working += test.Handle; + await client.DoWorkAsync(); + + Assert.IsTrue(test.Raised); + Assert.IsTrue(test.Completed); + } + + [Test] + public async Task Raise_All() + { + var first = new TestHandler(); + var second = new TestHandler(); + var third = new TestHandler(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + await client.DoWorkAsync(); + + Assert.IsTrue(first.Completed); + Assert.IsTrue(second.Completed); + Assert.IsTrue(third.Completed); + } + + [Test] + public async Task Raise_Slowest_First() + { + var first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var second = new TestHandler(); + var third = new TestHandler(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + await client.DoWorkAsync(); + + Assert.IsTrue(first.Completed); + Assert.IsTrue(second.Completed); + Assert.IsTrue(third.Completed); + } + + [Test] + public async Task Raise_Slowest_Middle() + { + var first = new TestHandler(); + var second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var third = new TestHandler(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + await client.DoWorkAsync(); + + Assert.IsTrue(first.Completed); + Assert.IsTrue(second.Completed); + Assert.IsTrue(third.Completed); + } + + [Test] + public async Task Raise_Slowest_Last() + { + var first = new TestHandler(); + var second = new TestHandler(); + var third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + await client.DoWorkAsync(); + + Assert.IsTrue(first.Completed); + Assert.IsTrue(second.Completed); + Assert.IsTrue(third.Completed); + } + + [Test] + public async Task ThreadSafe_NewNotRaised() + { + var first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var fourth = new TestHandler(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + + first.Callback = (_, _) => { client.Working += fourth.Handle; return Task.CompletedTask; }; + await client.DoWorkAsync(); + + Assert.IsTrue(first.Completed); + Assert.IsTrue(second.Completed); + Assert.IsTrue(third.Completed); + Assert.IsFalse(fourth.Raised); + } + + [Test] + public async Task ThreadSafe_OldStillRaised() + { + var first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + + first.Callback = (_, _) => { client.Working -= first.Handle; return Task.CompletedTask; }; + await client.DoWorkAsync(); + + Assert.IsTrue(first.Completed); + Assert.IsTrue(second.Completed); + Assert.IsTrue(third.Completed); + + await client.DoWorkAsync(); + Assert.AreEqual(1, first.CompletedCount); + Assert.AreEqual(2, second.CompletedCount); + Assert.AreEqual(2, third.CompletedCount); + } + + [Test] + public async Task SequentialProcessing() + { + TestClient client = GetClient(); + StringBuilder text = new StringBuilder(); + Func> makeHandler = + (string before, string after) => + async (SyncAsyncEventArgs e) => + { + text.Append(before); + TimeSpan delay = TimeSpan.FromMilliseconds(50); + if (e.RunSynchronously) + { + e.CancellationToken.WaitHandle.WaitOne(delay); + } + else + { + await Task.Delay(delay, e.CancellationToken); + } + text.Append(after); + }; + client.Working += makeHandler("a", "b"); + client.Working += makeHandler("1", "2"); + client.Working += makeHandler("<", ">"); + await client.DoWorkAsync(); + Assert.AreEqual("ab12<>", text.ToString()); + } + #endregion + + #region EventArgs + [Test] + public async Task Raise_EventArgs() + { + var test = new TestHandler(); + SyncAsyncEventArgs args = new SyncAsyncEventArgs(false); + + TestClient client = GetClient(); + client.Working += test.Handle; + await client.DoWorkAsync(); + + Assert.IsNotNull(test.LastEventArgs); + } + + [Test] + public async Task Raise_EventArgs_All() + { + var first = new TestHandler(); + var second = new TestHandler(); + var third = new TestHandler(); + SyncAsyncEventArgs args = new SyncAsyncEventArgs(false); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + await client.DoWorkAsync(); + + Assert.AreSame(first.LastEventArgs, second.LastEventArgs); + Assert.AreSame(first.LastEventArgs, third.LastEventArgs); + } + + [Test] + public async Task Raise_EventArgs_Custom() + { + var before = new TestHandler(); + var after = new TestHandler(); + TestClient original = new TestClient(); + TestClient client = InstrumentClient(original); + client.Working += before.Handle; + client.WorkCompleted += after.Handle; + int result = await client.DoWorkAsync(); + Assert.True(before.Completed); + Assert.True(after.Completed); + Assert.AreSame(original, after.LastEventArgs.Client); + Assert.AreEqual(result, after.LastEventArgs.Result); + } + #endregion + + #region Cancellation + [Test] + public async Task Cancels_AlreadyFinished() + { + var test = new TestHandler(); + CancellationTokenSource cancellation = new CancellationTokenSource(); + + TestClient client = GetClient(); + client.Working += test.Handle; + await client.DoWorkAsync(cancellation.Token); + cancellation.Cancel(); + + Assert.IsTrue(test.Raised); + Assert.IsTrue(test.Completed); + } + + [Test] + public async Task Cancels_StillRunning() + { + var test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; + CancellationTokenSource cancellation = new CancellationTokenSource(); + + TestClient client = GetClient(); + client.Working += test.Handle; + test.Callback = (_, _) => { cancellation.Cancel(); return Task.CompletedTask; }; + try + { + await client.DoWorkAsync(cancellation.Token); + } + catch (AggregateException) + { + } + + Assert.IsTrue(test.Raised); + Assert.IsFalse(test.Completed); + } + + [Test] + public async Task Cancels_StopsRaising() + { + var first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; + var second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; + var third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; + CancellationTokenSource cancellation = new CancellationTokenSource(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + + first.Callback = (_, _) => { cancellation.Cancel(); return Task.CompletedTask; }; + try + { + await client.DoWorkAsync(cancellation.Token); + } + catch (AggregateException) + { + } + + Assert.IsTrue(first.Raised); + Assert.IsFalse(first.Completed); + Assert.IsFalse(second.Raised); + Assert.IsFalse(second.Completed); + Assert.IsFalse(third.Raised); + Assert.IsFalse(third.Completed); + } + #endregion + + #region Exceptions + [Test] + public async Task Exception_Thrown() + { + var test = new TestHandler() { Throws = "Boom!" }; + + TestClient client = GetClient(); + client.Working += test.Handle; + try + { + await client.DoWorkAsync(); + Assert.Fail("Handler exception was not thrown!"); + } + catch (AggregateException ex) + { + Assert.IsInstanceOf(ex.InnerException); + Assert.AreEqual("Boom!", ex.InnerException.Message); + } + + Assert.IsTrue(test.Raised); + Assert.IsFalse(test.Completed); + } + + [Test] + public async Task Exception_OthersContinue() + { + var first = new TestHandler() { Throws = "Boom!" }; + var second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + var third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; + CancellationTokenSource cancellation = new CancellationTokenSource(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + + try + { + await client.DoWorkAsync(); + Assert.Fail("Handler exception was not thrown!"); + } + catch (AggregateException ex) + { + Assert.IsInstanceOf(ex.InnerException); + Assert.AreEqual("Boom!", ex.InnerException.Message); + } + + Assert.IsTrue(first.Raised); + Assert.IsFalse(first.Completed); + Assert.IsTrue(second.Raised); + Assert.IsTrue(second.Completed); + Assert.IsTrue(third.Raised); + Assert.IsTrue(third.Completed); + } + + [Test] + public async Task Exception_Multiple() + { + var first = new TestHandler() { Throws = nameof(TestClient.Working) }; + var second = new TestHandler() { Throws = "Bar" }; + var third = new TestHandler() { Throws = "Baz" }; + CancellationTokenSource cancellation = new CancellationTokenSource(); + + TestClient client = GetClient(); + client.Working += first.Handle; + client.Working += second.Handle; + client.Working += third.Handle; + + try + { + await client.DoWorkAsync(); + Assert.Fail("Handler exception was not thrown!"); + } + catch (AggregateException ex) + { + var messages = ex.InnerExceptions.Select(e => e.Message).ToList(); + Assert.Contains(nameof(TestClient.Working), messages); + Assert.Contains("Bar", messages); + Assert.Contains("Baz", messages); + Assert.AreEqual(3, messages.Count); + } + + Assert.IsTrue(first.Raised); + Assert.IsFalse(first.Completed); + Assert.IsTrue(second.Raised); + Assert.IsFalse(second.Completed); + Assert.IsTrue(third.Raised); + Assert.IsFalse(third.Completed); + } + + [Test] + public async Task Exception_PreservesAggregateTree() + { + TestClient client = GetClient(); + client.Working += (SyncAsyncEventArgs e) => + throw new AggregateException(new InvalidOperationException("Nested oops.")); + try + { + await client.DoWorkAsync(); + Assert.Fail("Handler exception was not thrown!"); + } + catch (AggregateException outer) + { + Assert.IsInstanceOf(outer.InnerException); + Assert.IsInstanceOf(outer.InnerException.InnerException); + } + } + #endregion + } +} diff --git a/sdk/core/Azure.Core/tests/samples/EventSamples.cs b/sdk/core/Azure.Core/tests/samples/EventSamples.cs new file mode 100644 index 000000000000..f9c6b3b011e5 --- /dev/null +++ b/sdk/core/Azure.Core/tests/samples/EventSamples.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using NUnit.Framework; + +namespace Azure.Core.Samples +{ + public class EventSamples + { + public class AlarmClientOptions : ClientOptions { } + + public class AlarmClient + { + private ClientDiagnostics _clientDiagnostics = new ClientDiagnostics(new AlarmClientOptions()); + + public event SyncAsyncEventHandler Ring; + + public void Snooze(CancellationToken cancellationToken = default) => + SnoozeInternal(true, cancellationToken).GetAwaiter().GetResult(); + + public async Task SnoozeAsync(CancellationToken cancellationToken = default) => + await SnoozeInternal(false, cancellationToken).ConfigureAwait(false); + + protected virtual async Task SnoozeInternal(bool runSynchronously, CancellationToken cancellationToken) + { + // Why does snoozing an alarm always wait 9 minutes? + TimeSpan delay = TimeSpan.FromMilliseconds(900); + if (runSynchronously) + { + cancellationToken.WaitHandle.WaitOne(delay); + } + else + { + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + } + SyncAsyncEventArgs e = new SyncAsyncEventArgs(runSynchronously, cancellationToken); + await Ring.RaiseAsync(e, nameof(AlarmClient), nameof(Ring), _clientDiagnostics).ConfigureAwait(false); + } + } + + [Test] + public void SyncHandler() + { + #region Snippet:Azure_Core_Samples_EventSamples_SyncHandler + var client = new AlarmClient(); + client.Ring += (SyncAsyncEventArgs e) => + { + Console.WriteLine("Wake up!"); + return Task.CompletedTask; + }; + + client.Snooze(); + #endregion + } + + [Test] + public async Task AsyncHandler() + { + #region Snippet:Azure_Core_Samples_EventSamples_AsyncHandler + var client = new AlarmClient(); + client.Ring += async (SyncAsyncEventArgs e) => + { + await Console.Out.WriteLineAsync("Wake up!"); + }; + + await client.SnoozeAsync(); + #endregion + } + [Test] + public async Task CombinedHandler() + { + #region Snippet:Azure_Core_Samples_EventSamples_CombinedHandler + var client = new AlarmClient(); + client.Ring += async (SyncAsyncEventArgs e) => + { + if (e.RunSynchronously) + { + Console.WriteLine("Wake up!"); + } + else + { + await Console.Out.WriteLineAsync("Wake up!"); + } + }; + + client.Snooze(); // sync call that blocks + await client.SnoozeAsync(); // async call that doesn't block + #endregion + } + + [Test] + public void Exceptions() + { + #region Snippet:Azure_Core_Samples_EventSamples_Exceptions + var client = new AlarmClient(); + client.Ring += (SyncAsyncEventArgs e) => + throw new InvalidOperationException("Alarm unplugged."); + + try + { + client.Snooze(); + } + catch (AggregateException ex) + { + ex.Handle(e => e is InvalidOperationException); + Console.WriteLine("Please switch to your backup alarm."); + } + #endregion + } + } +} diff --git a/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs b/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs index 85037ef8f6c2..18ea5c43db0c 100644 --- a/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs +++ b/sdk/search/Azure.Search.Documents/api/Azure.Search.Documents.netstandard2.0.cs @@ -92,10 +92,10 @@ protected SearchIndexingBufferedSender() { } public virtual System.Uri Endpoint { get { throw null; } } public virtual string IndexName { get { throw null; } } public virtual string ServiceName { get { throw null; } } - public event System.Func, System.Threading.CancellationToken, System.Threading.Tasks.Task> ActionAddedAsync { add { } remove { } } - public event System.Func, Azure.Search.Documents.Models.IndexingResult, System.Threading.CancellationToken, System.Threading.Tasks.Task> ActionCompletedAsync { add { } remove { } } - public event System.Func, Azure.Search.Documents.Models.IndexingResult, System.Exception, System.Threading.CancellationToken, System.Threading.Tasks.Task> ActionFailedAsync { add { } remove { } } - public event System.Func, System.Threading.CancellationToken, System.Threading.Tasks.Task> ActionSentAsync { add { } remove { } } + public event Azure.Core.SyncAsyncEventHandler> ActionAdded { add { } remove { } } + public event Azure.Core.SyncAsyncEventHandler> ActionCompleted { add { } remove { } } + public event Azure.Core.SyncAsyncEventHandler> ActionFailed { add { } remove { } } + public event Azure.Core.SyncAsyncEventHandler> ActionSent { add { } remove { } } public virtual void DeleteDocuments(System.Collections.Generic.IEnumerable documents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } public virtual System.Threading.Tasks.Task DeleteDocumentsAsync(System.Collections.Generic.IEnumerable documents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } ~SearchIndexingBufferedSender() { } @@ -107,6 +107,10 @@ protected SearchIndexingBufferedSender() { } public virtual System.Threading.Tasks.Task MergeDocumentsAsync(System.Collections.Generic.IEnumerable documents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual void MergeOrUploadDocuments(System.Collections.Generic.IEnumerable documents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } public virtual System.Threading.Tasks.Task MergeOrUploadDocumentsAsync(System.Collections.Generic.IEnumerable documents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + protected internal virtual System.Threading.Tasks.Task OnActionAddedAsync(Azure.Search.Documents.Models.IndexDocumentsAction action, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + protected internal virtual System.Threading.Tasks.Task OnActionCompletedAsync(Azure.Search.Documents.Models.IndexDocumentsAction action, Azure.Search.Documents.Models.IndexingResult result, System.Threading.CancellationToken cancellationToken) { throw null; } + protected internal virtual System.Threading.Tasks.Task OnActionFailedAsync(Azure.Search.Documents.Models.IndexDocumentsAction action, Azure.Search.Documents.Models.IndexingResult result, System.Exception exception, System.Threading.CancellationToken cancellationToken) { throw null; } + protected internal virtual System.Threading.Tasks.Task OnActionSentAsync(Azure.Search.Documents.Models.IndexDocumentsAction action, System.Threading.CancellationToken cancellationToken) { throw null; } System.Threading.Tasks.ValueTask System.IAsyncDisposable.DisposeAsync() { throw null; } void System.IDisposable.Dispose() { } public virtual void UploadDocuments(System.Collections.Generic.IEnumerable documents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } @@ -2193,6 +2197,23 @@ public enum FacetType Value = 0, Range = 1, } + public partial class IndexActionCompletedEventArgs : Azure.Search.Documents.Models.IndexActionEventArgs + { + public IndexActionCompletedEventArgs(Azure.Search.Documents.SearchIndexingBufferedSender sender, Azure.Search.Documents.Models.IndexDocumentsAction action, Azure.Search.Documents.Models.IndexingResult result, bool runSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) : base (default(Azure.Search.Documents.SearchIndexingBufferedSender), default(Azure.Search.Documents.Models.IndexDocumentsAction), default(bool), default(System.Threading.CancellationToken)) { } + public Azure.Search.Documents.Models.IndexingResult Result { get { throw null; } } + } + public partial class IndexActionEventArgs : Azure.SyncAsyncEventArgs + { + public IndexActionEventArgs(Azure.Search.Documents.SearchIndexingBufferedSender sender, Azure.Search.Documents.Models.IndexDocumentsAction action, bool runSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) : base (default(bool), default(System.Threading.CancellationToken)) { } + public Azure.Search.Documents.Models.IndexDocumentsAction Action { get { throw null; } } + public Azure.Search.Documents.SearchIndexingBufferedSender Sender { get { throw null; } } + } + public partial class IndexActionFailedEventArgs : Azure.Search.Documents.Models.IndexActionEventArgs + { + public IndexActionFailedEventArgs(Azure.Search.Documents.SearchIndexingBufferedSender sender, Azure.Search.Documents.Models.IndexDocumentsAction action, Azure.Search.Documents.Models.IndexingResult result, System.Exception exception, bool runSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) : base (default(Azure.Search.Documents.SearchIndexingBufferedSender), default(Azure.Search.Documents.Models.IndexDocumentsAction), default(bool), default(System.Threading.CancellationToken)) { } + public System.Exception Exception { get { throw null; } } + public Azure.Search.Documents.Models.IndexingResult Result { get { throw null; } } + } public enum IndexActionType { Upload = 0, diff --git a/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj b/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj index 6a4a559fe8ae..7a596c12b601 100644 --- a/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj +++ b/sdk/search/Azure.Search.Documents/src/Azure.Search.Documents.csproj @@ -1,4 +1,4 @@ - + Microsoft Azure.Search.Documents client library 11.2.0-beta.3 @@ -31,9 +31,14 @@ + + + + true + diff --git a/sdk/search/Azure.Search.Documents/src/Batching/AsyncEventExtensions.cs b/sdk/search/Azure.Search.Documents/src/Batching/AsyncEventExtensions.cs deleted file mode 100644 index ced77e55d52a..000000000000 --- a/sdk/search/Azure.Search.Documents/src/Batching/AsyncEventExtensions.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Azure.Search.Documents.Batching -{ - /// - /// Extensions to raise - /// events that wait for every Task to complete and throw every exception. - /// - internal static class AsyncEventExtensions - { - /// - /// Wait for all tasks to be completed and throw every exception. - /// - /// The tasks to execute. - /// A Task representing completion of all handlers. - private static async Task JoinAsync(IEnumerable tasks) - { - if (tasks == null) { return; } - - Task joined = Task.WhenAll(tasks); - try - { - await joined.ConfigureAwait(false); - } - catch (Exception) - { - // awaiting will unwrap the AggregateException which we - // don't want if there were multiple failures that should - // be surfaced - if (joined.Exception?.InnerExceptions?.Count > 1) - { - throw joined.Exception; - } - else - { - throw; - } - } - } - - /// - /// Raise the event. - /// - /// Type of the event argument. - /// The event to raise. - /// The event arguments. - /// A cancellation token. - /// A Task representing completion of all handlers. - public static async Task RaiseAsync( - this Func evt, - T args, - CancellationToken cancellationToken = default) => - await JoinAsync( - evt?.GetInvocationList()?.Select( - f => (f as Func)?.Invoke(args, cancellationToken))) - .ConfigureAwait(false); - - /// - /// Raise the event. - /// - /// Type of the first event argument. - /// Type of the second event argument. - /// The event to raise. - /// The first event argument. - /// The second event argument. - /// A cancellation token. - /// A Task representing completion of all handlers. - public static async Task RaiseAsync( - this Func evt, - T first, - U second, - CancellationToken cancellationToken = default) => - await JoinAsync( - evt?.GetInvocationList()?.Select( - f => (f as Func)?.Invoke(first, second, cancellationToken))) - .ConfigureAwait(false); - - /// - /// Raise the event. - /// - /// Type of the first event argument. - /// Type of the second event argument. - /// Type of the third event argument. - /// The event to raise. - /// The first event argument. - /// The second event argument. - /// The third event argument. - /// A cancellation token. - /// A Task representing completion of all handlers. - public static async Task RaiseAsync( - this Func evt, - T first, - U second, - V third, - CancellationToken cancellationToken = default) => - await JoinAsync( - evt?.GetInvocationList()?.Select( - f => (f as Func)?.Invoke(first, second, third, cancellationToken))) - .ConfigureAwait(false); - } -} diff --git a/sdk/search/Azure.Search.Documents/src/Batching/IndexActionCompletedEventArgs.cs b/sdk/search/Azure.Search.Documents/src/Batching/IndexActionCompletedEventArgs.cs new file mode 100644 index 000000000000..0cf9b1271e6f --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Batching/IndexActionCompletedEventArgs.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using Azure.Core; + +namespace Azure.Search.Documents.Models +{ + /// + /// Provides data for + /// event. + /// + /// + /// The .NET type that maps to the index schema. Instances of this type + /// can be retrieved as documents from the index. You can use + /// for dynamic documents. + /// + public class IndexActionCompletedEventArgs : IndexActionEventArgs + { + /// + /// Gets the of an action that was + /// successfully completed. + /// + public IndexingResult Result { get; } + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The raising the event. + /// + /// + /// The that was completed. + /// + /// + /// The of an action that was successfully + /// completed. + /// + /// + /// A value indicating whether the event handler was invoked + /// synchronously or asynchronously. Please see + /// for more details. + /// + /// + /// A cancellation token related to the original operation that raised + /// the event. It's important for your handler to pass this token + /// along to any asynchronous or long-running synchronous operations + /// that take a token so cancellation will correctly propagate. The + /// default value is . + /// + /// + /// Thrown if , , or + /// are null. + /// + public IndexActionCompletedEventArgs( + SearchIndexingBufferedSender sender, + IndexDocumentsAction action, + IndexingResult result, + bool runSynchronously, + CancellationToken cancellationToken = default) + : base(sender, action, runSynchronously, cancellationToken) + { + Argument.AssertNotNull(result, nameof(result)); + Result = result; + } + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Batching/IndexActionEventArgs.cs b/sdk/search/Azure.Search.Documents/src/Batching/IndexActionEventArgs.cs new file mode 100644 index 000000000000..ba61fdb21d4d --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Batching/IndexActionEventArgs.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using Azure.Core; + +namespace Azure.Search.Documents.Models +{ + /// + /// Provides data for + /// and events. + /// + /// + /// The .NET type that maps to the index schema. Instances of this type + /// can be retrieved as documents from the index. You can use + /// for dynamic documents. + /// + public class IndexActionEventArgs : SyncAsyncEventArgs + { + /// + /// Gets the raising the + /// event. + /// + public SearchIndexingBufferedSender Sender { get; } + + /// + /// Gets the that was added, + /// sent, completed, or failed. + /// + public IndexDocumentsAction Action { get; } + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The raising the event. + /// + /// + /// The that was added, sent, + /// completed, or failed. + /// + /// + /// A value indicating whether the event handler was invoked + /// synchronously or asynchronously. Please see + /// for more details. + /// + /// + /// A cancellation token related to the original operation that raised + /// the event. It's important for your handler to pass this token + /// along to any asynchronous or long-running synchronous operations + /// that take a token so cancellation will correctly propagate. The + /// default value is . + /// + /// + /// Thrown if or + /// are null. + /// + public IndexActionEventArgs( + SearchIndexingBufferedSender sender, + IndexDocumentsAction action, + bool runSynchronously, + CancellationToken cancellationToken = default) + : base(runSynchronously, cancellationToken) + { + Argument.AssertNotNull(sender, nameof(sender)); + Argument.AssertNotNull(action, nameof(action)); + Sender = sender; + Action = action; + } + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Batching/IndexActionFailedEventArgs.cs b/sdk/search/Azure.Search.Documents/src/Batching/IndexActionFailedEventArgs.cs new file mode 100644 index 000000000000..b55f9d2da6bf --- /dev/null +++ b/sdk/search/Azure.Search.Documents/src/Batching/IndexActionFailedEventArgs.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; + +namespace Azure.Search.Documents.Models +{ + /// + /// Provides data for + /// event. + /// + /// + /// The .NET type that maps to the index schema. Instances of this type + /// can be retrieved as documents from the index. You can use + /// for dynamic documents. + /// + public class IndexActionFailedEventArgs : IndexActionEventArgs + { + /// + /// Gets the of an action that failed to + /// complete. The value might be null. + /// + public IndexingResult Result { get; } + + /// + /// Gets the caused by an action that failed to + /// complete. The value might be null. + /// + public Exception Exception { get; } + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The raising the event. + /// + /// + /// The that failed. + /// + /// + /// The of an action that failed to + /// complete. + /// + /// + /// the caused by an action that failed to + /// complete. + /// + /// + /// A value indicating whether the event handler was invoked + /// synchronously or asynchronously. Please see + /// for more details. + /// + /// + /// A cancellation token related to the original operation that raised + /// the event. It's important for your handler to pass this token + /// along to any asynchronous or long-running synchronous operations + /// that take a token so cancellation will correctly propagate. The + /// default value is . + /// + /// + /// Thrown if or + /// are null. + /// + public IndexActionFailedEventArgs( + SearchIndexingBufferedSender sender, + IndexDocumentsAction action, + IndexingResult result, + Exception exception, + bool runSynchronously, + CancellationToken cancellationToken = default) + : base(sender, action, runSynchronously, cancellationToken) + { + // Do not validate - either might be null + Result = result; + Exception = exception; + } + } +} diff --git a/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingBufferedSender.cs b/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingBufferedSender.cs index b14f95c2e881..5b72c89f10c0 100644 --- a/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingBufferedSender.cs +++ b/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingBufferedSender.cs @@ -78,29 +78,24 @@ public class SearchIndexingBufferedSender : IDisposable, IAsyncDisposable private Task _getKeyFieldAccessorTask; /// - /// Async event raised whenever an indexing action is added to the - /// sender. + /// Event raised whenever an indexing action is added to the sender. /// - public event Func, CancellationToken, Task> ActionAddedAsync; + public event SyncAsyncEventHandler> ActionAdded; /// - /// Async event raised whenever an indexing action is sent by the - /// sender. + /// Event raised whenever an indexing action is sent by the sender. /// - public event Func, CancellationToken, Task> ActionSentAsync; + public event SyncAsyncEventHandler> ActionSent; /// - /// Async event raised whenever an indexing action was submitted - /// successfully. + /// Event raised whenever an indexing action was submitted successfully. /// - public event Func, IndexingResult, CancellationToken, Task> ActionCompletedAsync; + public event SyncAsyncEventHandler> ActionCompleted; /// - /// Async event raised whenever an indexing action failed. The - /// or may be null - /// depending on the failure. + /// Event raised whenever an indexing action failed. /// - public event Func, IndexingResult, Exception, CancellationToken, Task> ActionFailedAsync; + public event SyncAsyncEventHandler> ActionFailed; /// /// Protected constructor for mocking. @@ -328,21 +323,28 @@ static Func CompileAccessor(string key) #region Notifications /// - /// Raise the event. + /// Raise the event. /// /// The action being added. /// Cancellation token. /// /// A task that will not complete until every handler does. /// - internal async Task RaiseActionAddedAsync( + protected internal virtual async Task OnActionAddedAsync( IndexDocumentsAction action, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { try { - await ActionAddedAsync - .RaiseAsync(action, cancellationToken) + await ActionAdded.RaiseAsync( + new IndexActionEventArgs( + this, + action, + runSynchronously: false, + cancellationToken), + nameof(SearchIndexingBufferedSender), + nameof(ActionAdded), + SearchClient.ClientDiagnostics) .ConfigureAwait(false); } catch @@ -353,21 +355,28 @@ await ActionAddedAsync } /// - /// Raise the event. + /// Raise the event. /// /// The action being added. /// Cancellation token. /// /// A task that will not complete until every handler does. /// - internal async Task RaiseActionSentAsync( + protected internal virtual async Task OnActionSentAsync( IndexDocumentsAction action, CancellationToken cancellationToken) { try { - await ActionSentAsync - .RaiseAsync(action, cancellationToken) + await ActionSent.RaiseAsync( + new IndexActionEventArgs( + this, + action, + runSynchronously: false, + cancellationToken), + nameof(SearchIndexingBufferedSender), + nameof(ActionSent), + SearchClient.ClientDiagnostics) .ConfigureAwait(false); } catch @@ -378,7 +387,7 @@ await ActionSentAsync } /// - /// Raise the event. + /// Raise the event. /// /// The action being added. /// The result of indexing. @@ -386,15 +395,23 @@ await ActionSentAsync /// /// A task that will not complete until every handler does. /// - internal async Task RaiseActionCompletedAsync( + protected internal virtual async Task OnActionCompletedAsync( IndexDocumentsAction action, IndexingResult result, CancellationToken cancellationToken) { try { - await ActionCompletedAsync - .RaiseAsync(action, result, cancellationToken) + await ActionCompleted.RaiseAsync( + new IndexActionCompletedEventArgs( + this, + action, + result, + runSynchronously: false, + cancellationToken), + nameof(SearchIndexingBufferedSender), + nameof(ActionCompleted), + SearchClient.ClientDiagnostics) .ConfigureAwait(false); } catch @@ -405,7 +422,7 @@ await ActionCompletedAsync } /// - /// Raise the event. + /// Raise the event. /// /// The action being added. /// The result of indexing. @@ -414,7 +431,7 @@ await ActionCompletedAsync /// /// A task that will not complete until every handler does. /// - internal async Task RaiseActionFailedAsync( + protected internal virtual async Task OnActionFailedAsync( IndexDocumentsAction action, IndexingResult result, Exception exception, @@ -422,8 +439,17 @@ internal async Task RaiseActionFailedAsync( { try { - await ActionFailedAsync - .RaiseAsync(action, result, exception, cancellationToken) + await ActionFailed.RaiseAsync( + new IndexActionFailedEventArgs( + this, + action, + result, + exception, + runSynchronously: false, + cancellationToken), + nameof(SearchIndexingBufferedSender), + nameof(ActionFailed), + SearchClient.ClientDiagnostics) .ConfigureAwait(false); } catch diff --git a/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs b/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs index add1f52870f5..ca435dcab33d 100644 --- a/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs +++ b/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs @@ -25,7 +25,7 @@ internal class SearchIndexingPublisher : Publisher> /// /// The sender, which we mostly use for raising events. /// - private SearchIndexingBufferedSender _sender; + private readonly SearchIndexingBufferedSender _sender; /// /// Creates a new SearchIndexingPublisher which immediately starts @@ -100,7 +100,7 @@ protected override async Task OnDocumentsAddedAsync(IEnumerable document in documents) { - await _sender.RaiseActionAddedAsync(document, cancellationToken).ConfigureAwait(false); + await _sender.OnActionAddedAsync(document, cancellationToken).ConfigureAwait(false); } // Add all of the documents and possibly auto flush @@ -121,7 +121,7 @@ protected override async Task OnSubmitBatchAsync(IList> action in batch) { - await _sender.RaiseActionSentAsync(action.Document, cancellationToken).ConfigureAwait(false); + await _sender.OnActionSentAsync(action.Document, cancellationToken).ConfigureAwait(false); } // Send the request to the service @@ -173,7 +173,7 @@ protected override async Task OnSubmitBatchAsync(IList used /// to provide tracing support for the client library. /// - private ClientDiagnostics ClientDiagnostics { get; } + internal ClientDiagnostics ClientDiagnostics { get; } /// /// Gets the REST API version of the Search Service to use when making diff --git a/sdk/search/Azure.Search.Documents/tests/Batching/AsyncEventExtensionsTests.cs b/sdk/search/Azure.Search.Documents/tests/Batching/AsyncEventExtensionsTests.cs deleted file mode 100644 index 4ec95e188070..000000000000 --- a/sdk/search/Azure.Search.Documents/tests/Batching/AsyncEventExtensionsTests.cs +++ /dev/null @@ -1,575 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure.Search.Documents.Batching; -using NUnit.Framework; - -namespace Azure.Search.Documents.Tests -{ - public class AsyncEventExtensionsTests - { - private class TestHandler - { - public TimeSpan? Delay { get; set; } - public string Throws { get; set; } - - public EventArgs LastEventArgs { get; private set; } - public bool Raised => RaisedCount > 0; - public int RaisedCount { get; private set; } = 0; - public bool Completed => CompletedCount > 0; - public int CompletedCount { get; private set; } = 0; - - public async Task Handle(EventArgs e, CancellationToken cancellationToken) - { - LastEventArgs = e; - RaisedCount++; - if (Delay != null) - { - await Task.Delay(Delay.Value, cancellationToken); - } - if (Throws != null) - { - throw new InvalidOperationException(Throws); - } - cancellationToken.ThrowIfCancellationRequested(); - CompletedCount++; - } - } - - private async Task Pause(Action action, TimeSpan? delay = null) - { - await Task.Delay(delay ?? TimeSpan.FromMilliseconds(10)); - action(); - } - - [Test] - public void AddHandler() - { - Func handler = null; - TestHandler test = new TestHandler(); - handler += test.Handle; - } - - [Test] - public void AddHandler_Null() - { - Func handler = null; - handler += null; - } - - [Test] - public void AddHandler_Multiple() - { - Func handler = null; - handler += new TestHandler().Handle; - handler += new TestHandler().Handle; - handler += new TestHandler().Handle; - handler += new TestHandler().Handle; - } - - [Test] - public void RemoveHandler() - { - Func handler = null; - TestHandler test = new TestHandler(); - handler += test.Handle; - handler -= test.Handle; - } - - [Test] - public void RemoveHandler_Null() - { - Func handler = null; - handler -= null; - } - - [Test] - public void RemoveHandler_NotAdded() - { - Func handler = null; - TestHandler test = new TestHandler(); - handler -= test.Handle; - } - - [Test] - public async Task Raise() - { - TestHandler test = new TestHandler(); - - Func handler = null; - handler += test.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsTrue(test.Raised); - Assert.IsTrue(test.Completed); - } - - [Test] - public async Task Raise_None() - { - Func handler = null; - await handler.RaiseAsync(EventArgs.Empty, default); - } - - [Test] - public async Task Raise_Null() - { - Func handler = null; - handler += null; - await handler.RaiseAsync(EventArgs.Empty, default); - } - - [Test] - public async Task Raise_Waits() - { - TestHandler test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - - Func handler = null; - handler += test.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsTrue(test.Raised); - Assert.IsTrue(test.Completed); - } - - [Test] - public async Task Raise_All() - { - TestHandler first = new TestHandler(); - TestHandler second = new TestHandler(); - TestHandler third = new TestHandler(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsTrue(first.Completed); - Assert.IsTrue(second.Completed); - Assert.IsTrue(third.Completed); - } - - [Test] - public async Task Raise_Slowest_First() - { - TestHandler first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler second = new TestHandler(); - TestHandler third = new TestHandler(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsTrue(first.Completed); - Assert.IsTrue(second.Completed); - Assert.IsTrue(third.Completed); - } - - [Test] - public async Task Raise_Slowest_Middle() - { - TestHandler first = new TestHandler(); - TestHandler second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler third = new TestHandler(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsTrue(first.Completed); - Assert.IsTrue(second.Completed); - Assert.IsTrue(third.Completed); - } - - [Test] - public async Task Raise_Slowest_Last() - { - TestHandler first = new TestHandler(); - TestHandler second = new TestHandler(); - TestHandler third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsTrue(first.Completed); - Assert.IsTrue(second.Completed); - Assert.IsTrue(third.Completed); - } - - [Test] - public async Task RemoveHandler_NotRaised() - { - TestHandler test = new TestHandler(); - - Func handler = null; - handler += test.Handle; - handler -= test.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsFalse(test.Raised); - } - - [Test] - public async Task AddHandler_Duplicate() - { - TestHandler test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - - Func handler = null; - handler += test.Handle; - handler += test.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.AreEqual(2, test.RaisedCount); - } - - [Test] - public async Task RemoveHandler_Duplicate() - { - TestHandler test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - - Func handler = null; - handler += test.Handle; - handler += test.Handle; - handler -= test.Handle; - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.AreEqual(1, test.RaisedCount); - } - - [Test] - public async Task RemoveHandler_OthersUnaffected() - { - TestHandler first = new TestHandler(); - TestHandler second = new TestHandler(); - TestHandler third = new TestHandler(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - - handler -= second.Handle; - - await handler.RaiseAsync(EventArgs.Empty, default); - - Assert.IsTrue(first.Completed); - Assert.IsTrue(third.Completed); - - Assert.IsFalse(second.Raised); - } - - [Test] - public async Task Raise_EventArgs() - { - TestHandler test = new TestHandler(); - EventArgs args = new EventArgs(); - - Func handler = null; - handler += test.Handle; - await handler.RaiseAsync(args, default); - - Assert.AreSame(args, test.LastEventArgs); - } - - [Test] - public async Task Raise_EventArgs_All() - { - TestHandler first = new TestHandler(); - TestHandler second = new TestHandler(); - TestHandler third = new TestHandler(); - EventArgs args = new EventArgs(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - await handler.RaiseAsync(args, default); - - Assert.AreSame(args, first.LastEventArgs); - Assert.AreSame(args, second.LastEventArgs); - Assert.AreSame(args, third.LastEventArgs); - } - - [Test] - public async Task ThreadSafe_NewNotRaised() - { - TestHandler first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler fourth = new TestHandler(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - - await Task.WhenAll( - Pause(() => handler += fourth.Handle), - handler.RaiseAsync(EventArgs.Empty, default)); - - Assert.IsTrue(first.Completed); - Assert.IsTrue(second.Completed); - Assert.IsTrue(third.Completed); - Assert.IsFalse(fourth.Raised); - } - - [Test] - public async Task ThreadSafe_OldStillRaised() - { - TestHandler first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - - await Task.WhenAll( - Pause(() => handler -= first.Handle), - handler.RaiseAsync(EventArgs.Empty, default)); - - Assert.IsTrue(first.Completed); - Assert.IsTrue(second.Completed); - Assert.IsTrue(third.Completed); - - await handler.RaiseAsync(EventArgs.Empty, default); - Assert.AreEqual(1, first.CompletedCount); - Assert.AreEqual(2, second.CompletedCount); - Assert.AreEqual(2, third.CompletedCount); - } - - [Test] - public async Task Cancels_AlreadyFinished() - { - TestHandler test = new TestHandler(); - CancellationTokenSource cancellation = new CancellationTokenSource(); - - Func handler = null; - handler += test.Handle; - await handler.RaiseAsync(EventArgs.Empty, cancellation.Token); - cancellation.Cancel(); - - Assert.IsTrue(test.Raised); - Assert.IsTrue(test.Completed); - } - - [Test] - public async Task Cancels_StillRunning() - { - TestHandler test = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; - CancellationTokenSource cancellation = new CancellationTokenSource(); - - Func handler = null; - handler += test.Handle; - try - { - await Task.WhenAll( - Pause(() => cancellation.Cancel()), - handler.RaiseAsync(EventArgs.Empty, cancellation.Token)); - } - catch (TaskCanceledException) - { - } - - Assert.IsTrue(test.Raised); - Assert.IsFalse(test.Completed); - } - - [Test] - public async Task Cancels_All() - { - TestHandler first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; - TestHandler second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; - TestHandler third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; - CancellationTokenSource cancellation = new CancellationTokenSource(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - - try - { - await Task.WhenAll( - Pause(() => cancellation.Cancel()), - handler.RaiseAsync(EventArgs.Empty, cancellation.Token)); - } - catch (TaskCanceledException) - { - } - - Assert.IsTrue(first.Raised); - Assert.IsFalse(first.Completed); - Assert.IsTrue(second.Raised); - Assert.IsFalse(second.Completed); - Assert.IsTrue(third.Raised); - Assert.IsFalse(third.Completed); - } - - [Test] - public async Task Cancels_OnlySlow() - { - TestHandler first = new TestHandler() { Delay = TimeSpan.FromMilliseconds(20) }; - TestHandler second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; - TestHandler third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(500) }; - CancellationTokenSource cancellation = new CancellationTokenSource(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - - try - { - await Task.WhenAll( - Pause( - delay: TimeSpan.FromMilliseconds(100), - action: () => cancellation.Cancel()), - handler.RaiseAsync(EventArgs.Empty, cancellation.Token)); - } - catch (TaskCanceledException) - { - } - - Assert.IsTrue(first.Raised); - Assert.IsTrue(first.Completed); - Assert.IsTrue(second.Raised); - Assert.IsFalse(second.Completed); - Assert.IsTrue(third.Raised); - Assert.IsFalse(third.Completed); - } - - [Test] - public async Task Exception_Thrown() - { - TestHandler test = new TestHandler() { Throws = "Boom!" }; - - Func handler = null; - handler += test.Handle; - try - { - await handler.RaiseAsync(EventArgs.Empty, default); - Assert.Fail("Handler exception was not thrown!"); - } - catch (InvalidOperationException ex) - { - Assert.AreEqual("Boom!", ex.Message); - } - - Assert.IsTrue(test.Raised); - Assert.IsFalse(test.Completed); - } - - [Test] - public async Task Exception_OthersContinue() - { - TestHandler first = new TestHandler() { Throws = "Boom!" }; - TestHandler second = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - TestHandler third = new TestHandler() { Delay = TimeSpan.FromMilliseconds(100) }; - CancellationTokenSource cancellation = new CancellationTokenSource(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - - try - { - await handler.RaiseAsync(EventArgs.Empty, default); - Assert.Fail("Handler exception was not thrown!"); - } - catch (InvalidOperationException ex) - { - Assert.AreEqual("Boom!", ex.Message); - } - - Assert.IsTrue(first.Raised); - Assert.IsFalse(first.Completed); - Assert.IsTrue(second.Raised); - Assert.IsTrue(second.Completed); - Assert.IsTrue(third.Raised); - Assert.IsTrue(third.Completed); - } - - [Test] - public async Task Exception_Multiple() - { - TestHandler first = new TestHandler() { Throws = "Foo" }; - TestHandler second = new TestHandler() { Throws = "Bar" }; - TestHandler third = new TestHandler() { Throws = "Baz" }; - CancellationTokenSource cancellation = new CancellationTokenSource(); - - Func handler = null; - handler += first.Handle; - handler += second.Handle; - handler += third.Handle; - - try - { - await handler.RaiseAsync(EventArgs.Empty, default); - Assert.Fail("Handler exception was not thrown!"); - } - catch (AggregateException ex) - { - var messages = ex.InnerExceptions.Select(e => e.Message).ToList(); - Assert.Contains("Foo", messages); - Assert.Contains("Bar", messages); - Assert.Contains("Baz", messages); - Assert.AreEqual(3, messages.Count); - } - - Assert.IsTrue(first.Raised); - Assert.IsFalse(first.Completed); - Assert.IsTrue(second.Raised); - Assert.IsFalse(second.Completed); - Assert.IsTrue(third.Raised); - Assert.IsFalse(third.Completed); - } - - [Test] - public async Task Args_Single() - { - int x = 0; - Func handler = null; - handler += (i, c) => { x = i; return Task.CompletedTask; }; - await handler.RaiseAsync(7, default); - Assert.AreEqual(7, x); - } - - [Test] - public async Task Args_Double() - { - int x = 0, y = 0; - Func handler = null; - handler += (i, j, c) => { x = i; y = j; return Task.CompletedTask; }; - await handler.RaiseAsync(7, 8, default); - Assert.AreEqual(7, x); - Assert.AreEqual(8, y); - } - - [Test] - public async Task Args_Triple() - { - int x = 0, y = 0, z = 0; - Func handler = null; - handler += (i, j, k, c) => { x = i; y = j; z = k; return Task.CompletedTask; }; - await handler.RaiseAsync(7, 8, 9, default); - Assert.AreEqual(7, x); - Assert.AreEqual(8, y); - Assert.AreEqual(9, z); - } - } -} diff --git a/sdk/search/Azure.Search.Documents/tests/Batching/BatchingTests.cs b/sdk/search/Azure.Search.Documents/tests/Batching/BatchingTests.cs index 62943cfca2de..e8966c6f9e73 100644 --- a/sdk/search/Azure.Search.Documents/tests/Batching/BatchingTests.cs +++ b/sdk/search/Azure.Search.Documents/tests/Batching/BatchingTests.cs @@ -54,21 +54,18 @@ public static SimpleDocument[] GetLargeDocuments(int count, int size = 4092) => private static void AssertNoFailures(SearchIndexingBufferedSender indexer) { - indexer.ActionFailedAsync += - (IndexDocumentsAction doc, - IndexingResult result, - Exception ex, - CancellationToken cancellationToken) => + indexer.ActionFailed += + (IndexActionFailedEventArgs e) => { StringBuilder message = new StringBuilder(); - if (result != null) + if (e.Result != null) { - Assert.IsFalse(result.Succeeded); - message.AppendLine($"key {result.Key} failed with {result.Status}: {result.ErrorMessage}"); + Assert.IsFalse(e.Result.Succeeded); + message.AppendLine($"key {e.Result.Key} failed with {e.Result.Status}: {e.Result.ErrorMessage}"); } - if (message != null) + if (e.Exception != null) { - message.AppendLine(ex.ToString()); + message.AppendLine(e.Exception.ToString()); } Assert.Fail(message.ToString()); return Task.CompletedTask; @@ -78,13 +75,10 @@ private static void AssertNoFailures(SearchIndexingBufferedSender indexer) private static ConcurrentQueue> TrackFailures(SearchIndexingBufferedSender indexer) { ConcurrentQueue> failures = new ConcurrentQueue>(); - indexer.ActionFailedAsync += - (IndexDocumentsAction doc, - IndexingResult result, - Exception ex, - CancellationToken cancellationToken) => + indexer.ActionFailed += + (IndexActionFailedEventArgs e) => { - failures.Enqueue(doc); + failures.Enqueue(e.Action); return Task.CompletedTask; }; return failures; @@ -93,28 +87,22 @@ private static ConcurrentQueue> TrackFailures(SearchI private static ConcurrentDictionary> TrackPending(SearchIndexingBufferedSender indexer) { ConcurrentDictionary> pending = new ConcurrentDictionary>(); - indexer.ActionAddedAsync += - (IndexDocumentsAction doc, - CancellationToken cancellationToken) => + indexer.ActionAdded += + (IndexActionEventArgs e) => { - pending[doc.GetHashCode()] = doc; + pending[e.Action.GetHashCode()] = e.Action; return Task.CompletedTask; }; - indexer.ActionCompletedAsync += - (IndexDocumentsAction doc, - IndexingResult result, - CancellationToken cancellationToken) => + indexer.ActionCompleted += + (IndexActionCompletedEventArgs e) => { - pending.TryRemove(doc.GetHashCode(), out IndexDocumentsAction _); + pending.TryRemove(e.Action.GetHashCode(), out IndexDocumentsAction _); return Task.CompletedTask; }; - indexer.ActionFailedAsync += - (IndexDocumentsAction doc, - IndexingResult result, - Exception ex, - CancellationToken cancellationToken) => + indexer.ActionFailed += + (IndexActionFailedEventArgs e) => { - pending.TryRemove(doc.GetHashCode(), out IndexDocumentsAction _); + pending.TryRemove(e.Action.GetHashCode(), out IndexDocumentsAction _); return Task.CompletedTask; }; return pending; @@ -322,13 +310,10 @@ public async Task Champion_FineGrainedErrors() { AutoFlush = false }); - indexer.ActionFailedAsync += - (IndexDocumentsAction doc, - IndexingResult result, - Exception ex, - CancellationToken cancellationToken) => + indexer.ActionFailed += + (IndexActionFailedEventArgs e) => { - failures.Add(result); + failures.Add(e.Result); return Task.CompletedTask; }; @@ -360,27 +345,22 @@ public async Task Champion_BasicCheckpointing() }); List> pending = new List>(); - indexer.ActionAddedAsync += - (IndexDocumentsAction doc, CancellationToken cancellationToken) => + indexer.ActionAdded += + (IndexActionEventArgs e) => { - pending.Add(doc); + pending.Add(e.Action); return Task.CompletedTask; }; - indexer.ActionCompletedAsync += - (IndexDocumentsAction doc, - IndexingResult result, - CancellationToken cancellationToken) => + indexer.ActionCompleted += + (IndexActionCompletedEventArgs e) => { - pending.Remove(doc); + pending.Remove(e.Action); return Task.CompletedTask; }; - indexer.ActionFailedAsync += - (IndexDocumentsAction doc, - IndexingResult result, - Exception ex, - CancellationToken cancellationToken) => + indexer.ActionFailed += + (IndexActionFailedEventArgs e) => { - pending.Remove(doc); + pending.Remove(e.Action); return Task.CompletedTask; }; @@ -920,7 +900,7 @@ public async Task Notifications_Added() client.CreateIndexingBufferedSender( new SearchIndexingBufferedSenderOptions()); int adds = 0; - indexer.ActionAddedAsync += (a, c) => { adds++; return Task.CompletedTask; }; + indexer.ActionAdded += e => { adds++; return Task.CompletedTask; }; await indexer.UploadDocumentsAsync(data); await DelayAsync(EventDelay, EventDelay); Assert.AreEqual(data.Length, adds); @@ -937,7 +917,7 @@ public async Task Notifications_Sent() client.CreateIndexingBufferedSender( new SearchIndexingBufferedSenderOptions()); int sent = 0; - indexer.ActionSentAsync += (a, c) => { sent++; return Task.CompletedTask; }; + indexer.ActionSent += e => { sent++; return Task.CompletedTask; }; await indexer.UploadDocumentsAsync(data); await indexer.FlushAsync(); await DelayAsync(EventDelay, EventDelay); @@ -955,7 +935,7 @@ public async Task Notifications_Completed() client.CreateIndexingBufferedSender( new SearchIndexingBufferedSenderOptions()); int completed = 0; - indexer.ActionCompletedAsync += (a, r, c) => { completed++; return Task.CompletedTask; }; + indexer.ActionCompleted += e => { completed++; return Task.CompletedTask; }; await indexer.UploadDocumentsAsync(data); await indexer.FlushAsync(); await DelayAsync(EventDelay, EventDelay); @@ -973,7 +953,7 @@ public async Task Notifications_Failed() client.CreateIndexingBufferedSender( new SearchIndexingBufferedSenderOptions()); int failed = 0; - indexer.ActionFailedAsync += (a, r, e, c) => { failed++; return Task.CompletedTask; }; + indexer.ActionFailed += e => { failed++; return Task.CompletedTask; }; await indexer.MergeDocumentsAsync(data); await indexer.FlushAsync(); await DelayAsync(EventDelay, EventDelay); @@ -993,10 +973,10 @@ public async Task Notifications_ExceptionsGetSwallowed() // Throw from every handler bool added = false, sent = false, completed = false, failed = false; - indexer.ActionAddedAsync += (a, c) => { added = true; throw new InvalidOperationException("ActionAddedAsync: Should not be seen!"); }; - indexer.ActionSentAsync += (a, c) => { sent = true; throw new InvalidOperationException("ActionSentAsync: Should not be seen!"); }; - indexer.ActionCompletedAsync += (a, r, c) => { completed = true; throw new InvalidOperationException("ActionCompletedAsync: Should not be seen!"); }; - indexer.ActionFailedAsync += (a, r, e, c) => { failed = true; throw new InvalidOperationException("ActionFailedAsync: Should not be seen!"); }; + indexer.ActionAdded += e => { added = true; throw new InvalidOperationException("ActionAddedAsync: Should not be seen!"); }; + indexer.ActionSent += e => { sent = true; throw new InvalidOperationException("ActionSentAsync: Should not be seen!"); }; + indexer.ActionCompleted += e => { completed = true; throw new InvalidOperationException("ActionCompletedAsync: Should not be seen!"); }; + indexer.ActionFailed += e => { failed = true; throw new InvalidOperationException("ActionFailedAsync: Should not be seen!"); }; // Try to merge first for Failed to fire await indexer.MergeDocumentsAsync(data); @@ -1070,7 +1050,7 @@ public async Task Behavior_Retry(int status) }; AssertNoFailures(indexer); int sent = 0; - indexer.ActionSentAsync += (a, c) => { sent++; return Task.CompletedTask; }; + indexer.ActionSent += e => { sent++; return Task.CompletedTask; }; await indexer.UploadDocumentsAsync(data); await indexer.FlushAsync(); Assert.Less(1, sent); @@ -1096,7 +1076,7 @@ public async Task Behavior_MaxRetries() new IndexingResult(result.Key, false, 503); int attempts = 0; - indexer.ActionSentAsync += (a, c) => { attempts++; return Task.CompletedTask; }; + indexer.ActionSent += e => { attempts++; return Task.CompletedTask; }; await indexer.MergeOrUploadDocumentsAsync(data); await indexer.FlushAsync(); Assert.AreEqual(6, attempts);