From f27c837934e9eb68a74aca4e1a5d3fabbe33d40c Mon Sep 17 00:00:00 2001 From: tg-msft Date: Mon, 25 Jan 2021 11:50:51 -0800 Subject: [PATCH 1/3] Add SyncAsyncEventHandler --- sdk/core/Azure.Core/api/Azure.Core.net461.cs | 7 + sdk/core/Azure.Core/api/Azure.Core.net5.0.cs | 7 + .../api/Azure.Core.netstandard2.0.cs | 7 + sdk/core/Azure.Core/samples/Events.md | 190 +++++ sdk/core/Azure.Core/samples/README.md | 2 + .../Shared/SyncAsyncEventHandlerExtensions.cs | 113 +++ sdk/core/Azure.Core/src/SyncAsyncEventArgs.cs | 94 +++ .../Azure.Core/src/SyncAsyncEventHandler.cs | 217 ++++++ .../Azure.Core/tests/Azure.Core.Tests.csproj | 23 +- .../tests/SyncAsyncEventHandlerTests.cs | 700 ++++++++++++++++++ .../Azure.Core/tests/samples/EventSamples.cs | 115 +++ .../Azure.Search.Documents.netstandard2.0.cs | 29 +- .../src/Azure.Search.Documents.csproj | 7 +- .../src/Batching/AsyncEventExtensions.cs | 108 --- .../Batching/IndexActionCompletedEventArgs.cs | 68 ++ .../src/Batching/IndexActionEventArgs.cs | 72 ++ .../Batching/IndexActionFailedEventArgs.cs | 80 ++ .../Batching/SearchIndexingBufferedSender.cs | 86 ++- .../src/Batching/SearchIndexingPublisher.cs | 10 +- .../src/SearchClient.cs | 2 +- .../Batching/AsyncEventExtensionsTests.cs | 575 -------------- .../tests/Batching/BatchingTests.cs | 102 +-- 22 files changed, 1818 insertions(+), 796 deletions(-) create mode 100644 sdk/core/Azure.Core/samples/Events.md create mode 100644 sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs create mode 100644 sdk/core/Azure.Core/src/SyncAsyncEventArgs.cs create mode 100644 sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs create mode 100644 sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs create mode 100644 sdk/core/Azure.Core/tests/samples/EventSamples.cs delete mode 100644 sdk/search/Azure.Search.Documents/src/Batching/AsyncEventExtensions.cs create mode 100644 sdk/search/Azure.Search.Documents/src/Batching/IndexActionCompletedEventArgs.cs create mode 100644 sdk/search/Azure.Search.Documents/src/Batching/IndexActionEventArgs.cs create mode 100644 sdk/search/Azure.Search.Documents/src/Batching/IndexActionFailedEventArgs.cs delete mode 100644 sdk/search/Azure.Search.Documents/tests/Batching/AsyncEventExtensionsTests.cs diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 5690e26096cc..2c44cf68d0b1 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -181,6 +181,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 { @@ -405,6 +411,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 e7a6b0df30ff..7c2931b510be 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -181,6 +181,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 { @@ -405,6 +411,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 5690e26096cc..2c44cf68d0b1 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -181,6 +181,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 { @@ -405,6 +411,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..8dc66bcbc5cc --- /dev/null +++ b/sdk/core/Azure.Core/samples/Events.md @@ -0,0 +1,190 @@ +# 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 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 work great, 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`. Finally, +we wrap a [distributed tracing span](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Diagnostics.md#distributed-tracing) +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/en-us/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..bc4907cc445e --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs @@ -0,0 +1,113 @@ +// 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. + /// + 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 + using DiagnosticScope scope = clientDiagnostics.CreateScope(declaringTypeName + "." + eventName); + 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're 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 " + declaringTypeName + "." + eventName + " 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..f9c49373abe3 --- /dev/null +++ b/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs @@ -0,0 +1,217 @@ +// 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 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 work great, 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 . + /// Finally, we wrap a + /// + /// distributed tracing span 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..a9217cc34a3e 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..c59cb52ce86c --- /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()); + + private async Task Pause(Action action, TimeSpan? delay = null) + { + await Task.Delay(delay ?? TimeSpan.FromMilliseconds(10)); + action(); + } + + 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 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); + } + } + 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; + + await Task.WhenAll( + Pause(() => client.Working += fourth.Handle), + 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; + + await Task.WhenAll( + Pause(() => client.Working -= first.Handle), + 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; + try + { + await Task.WhenAll( + Pause(() => cancellation.Cancel()), + 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; + + try + { + await Task.WhenAll( + Pause(() => cancellation.Cancel()), + 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..4e3110fc84d0 100644 --- a/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs +++ b/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs @@ -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); From dd6b1450116d4c8bfd205148c81352145232acb3 Mon Sep 17 00:00:00 2001 From: tg-msft Date: Mon, 25 Jan 2021 22:31:25 -0800 Subject: [PATCH 2/3] PR Feedback --- sdk/core/Azure.Core/samples/Events.md | 25 ++++++++++--------- .../Shared/SyncAsyncEventHandlerExtensions.cs | 14 +++++++++-- .../Azure.Core/src/SyncAsyncEventHandler.cs | 20 +++++++-------- .../Azure.Core/tests/Azure.Core.Tests.csproj | 24 +++++++++--------- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/sdk/core/Azure.Core/samples/Events.md b/sdk/core/Azure.Core/samples/Events.md index 8dc66bcbc5cc..05ff40c7b784 100644 --- a/sdk/core/Azure.Core/samples/Events.md +++ b/sdk/core/Azure.Core/samples/Events.md @@ -4,13 +4,14 @@ [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 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 work great, but require a little extra care when writing event handlers. +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 address the needs of our customers, 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 @@ -50,10 +51,10 @@ 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`. Finally, -we wrap a [distributed tracing span](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Diagnostics.md#distributed-tracing) -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. +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 @@ -145,7 +146,7 @@ Any exceptions thrown by an event handler will be wrapped in a single [`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/en-us/archive/msdn-magazine/2009/brownfield/aggregating-exceptions) +[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 diff --git a/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs b/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs index bc4907cc445e..16c010542282 100644 --- a/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs +++ b/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs @@ -45,6 +45,15 @@ internal static class SyncAsyncEventHandlerExtensions /// 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, @@ -65,7 +74,8 @@ public static async Task RaiseAsync( // Wrap handler invocation in a distributed tracing span so it's // easy for customers to track and measure - using DiagnosticScope scope = clientDiagnostics.CreateScope(declaringTypeName + "." + eventName); + string eventFullName = declaringTypeName + "." + eventName; + using DiagnosticScope scope = clientDiagnostics.CreateScope(eventFullName); scope.Start(); try { @@ -99,7 +109,7 @@ public static async Task RaiseAsync( { // Include the event name in the exception for easier debugging throw new AggregateException( - "Unhandled exception(s) thrown when raising the " + declaringTypeName + "." + eventName + " event.", + "Unhandled exception(s) thrown when raising the " + eventFullName + " event.", failures); } } diff --git a/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs b/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs index f9c49373abe3..d714e6442333 100644 --- a/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs +++ b/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs @@ -128,15 +128,15 @@ namespace Azure.Core /// /// /// - /// Most Azure client libraries offer both synchronous and asynchronous - /// methods for calling Azure services. You can distinguish the - /// asynchronous methods by their Async suffix. For example, + /// 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 work great, but require a little extra care - /// when writing event handlers. + /// method invocation semantics address the needs of our customers, but + /// require a little extra care when writing event handlers. /// /// /// The SyncAsyncEventHandler is a delegate used by events in Azure client @@ -189,12 +189,12 @@ namespace Azure.Core /// 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 . - /// Finally, we wrap a + /// Finally, a /// - /// distributed tracing span 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. + /// 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 diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index a9217cc34a3e..19c3058fc5cb 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -21,18 +21,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + From 45baee79b58355e9e607e32537012e3307ed8254 Mon Sep 17 00:00:00 2001 From: tg-msft Date: Fri, 29 Jan 2021 11:15:49 -0800 Subject: [PATCH 3/3] PR feedback and making the tests less flaky --- sdk/core/Azure.Core/samples/Events.md | 18 +++++++--- .../Shared/SyncAsyncEventHandlerExtensions.cs | 2 +- .../Azure.Core/src/SyncAsyncEventHandler.cs | 22 ++++++++---- .../tests/SyncAsyncEventHandlerTests.cs | 36 +++++++++---------- .../src/Batching/SearchIndexingPublisher.cs | 2 +- 5 files changed, 49 insertions(+), 31 deletions(-) diff --git a/sdk/core/Azure.Core/samples/Events.md b/sdk/core/Azure.Core/samples/Events.md index 05ff40c7b784..76b020e8354f 100644 --- a/sdk/core/Azure.Core/samples/Events.md +++ b/sdk/core/Azure.Core/samples/Events.md @@ -10,8 +10,8 @@ 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 address the needs of our customers, but require a -little extra care when writing event handlers. +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 @@ -49,9 +49,17 @@ 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`. Finally, -a [distributed tracing span](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Diagnostics.md#distributed-tracing) +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. diff --git a/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs b/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs index 16c010542282..c2d908730ffc 100644 --- a/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs +++ b/sdk/core/Azure.Core/src/Shared/SyncAsyncEventHandlerExtensions.cs @@ -92,7 +92,7 @@ public static async Task RaiseAsync( Task runHandlerTask = azureHandler(e); // We can consider logging something when e.RunSynchronously // is true, but runHandlerTask.IsComplete is false. - // (We're not bother to check our tests because + // (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); diff --git a/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs b/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs index d714e6442333..0de375d646b2 100644 --- a/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs +++ b/sdk/core/Azure.Core/src/SyncAsyncEventHandler.cs @@ -135,8 +135,8 @@ namespace Azure.Core /// 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 address the needs of our customers, but - /// require a little extra care when writing event handlers. + /// 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 @@ -187,10 +187,20 @@ namespace Azure.Core /// 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 . - /// Finally, a - /// + /// 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 diff --git a/sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs b/sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs index c59cb52ce86c..8ff367fc494a 100644 --- a/sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs +++ b/sdk/core/Azure.Core/tests/SyncAsyncEventHandlerTests.cs @@ -22,12 +22,6 @@ private TestClient GetClient(TimeSpan? workDelay = null) => private ClientDiagnostics GetEmptyDiagnostics() => new ClientDiagnostics(new TestClientOptions()); - private async Task Pause(Action action, TimeSpan? delay = null) - { - await Task.Delay(delay ?? TimeSpan.FromMilliseconds(10)); - action(); - } - public class TestClientOptions : ClientOptions { } public class TestSyncAsyncEventArgs : SyncAsyncEventArgs @@ -106,6 +100,7 @@ private async Task DoWorkInternal(bool async, CancellationToken cancellatio 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; } @@ -123,6 +118,7 @@ public async Task Handle(T e) LastEventArgs = e; RaisedCount++; + if (Delay != null) { if (e.RunSynchronously) @@ -134,10 +130,18 @@ public async Task Handle(T e) 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++; } @@ -404,9 +408,8 @@ public async Task ThreadSafe_NewNotRaised() client.Working += second.Handle; client.Working += third.Handle; - await Task.WhenAll( - Pause(() => client.Working += fourth.Handle), - client.DoWorkAsync()); + first.Callback = (_, _) => { client.Working += fourth.Handle; return Task.CompletedTask; }; + await client.DoWorkAsync(); Assert.IsTrue(first.Completed); Assert.IsTrue(second.Completed); @@ -426,9 +429,8 @@ public async Task ThreadSafe_OldStillRaised() client.Working += second.Handle; client.Working += third.Handle; - await Task.WhenAll( - Pause(() => client.Working -= first.Handle), - client.DoWorkAsync()); + first.Callback = (_, _) => { client.Working -= first.Handle; return Task.CompletedTask; }; + await client.DoWorkAsync(); Assert.IsTrue(first.Completed); Assert.IsTrue(second.Completed); @@ -542,11 +544,10 @@ public async Task Cancels_StillRunning() TestClient client = GetClient(); client.Working += test.Handle; + test.Callback = (_, _) => { cancellation.Cancel(); return Task.CompletedTask; }; try { - await Task.WhenAll( - Pause(() => cancellation.Cancel()), - client.DoWorkAsync(cancellation.Token)); + await client.DoWorkAsync(cancellation.Token); } catch (AggregateException) { @@ -569,11 +570,10 @@ public async Task Cancels_StopsRaising() client.Working += second.Handle; client.Working += third.Handle; + first.Callback = (_, _) => { cancellation.Cancel(); return Task.CompletedTask; }; try { - await Task.WhenAll( - Pause(() => cancellation.Cancel()), - client.DoWorkAsync(cancellation.Token)); + await client.DoWorkAsync(cancellation.Token); } catch (AggregateException) { diff --git a/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs b/sdk/search/Azure.Search.Documents/src/Batching/SearchIndexingPublisher.cs index 4e3110fc84d0..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