|
| 1 | +# Azure.Core Event samples |
| 2 | + |
| 3 | +**NOTE:** Samples in this file only apply to packages following the |
| 4 | +[Azure SDK Design Guidelines](https://azure.github.io/azure-sdk/dotnet_introduction.html). |
| 5 | +The names of these packages usually start with `Azure`. |
| 6 | + |
| 7 | +Most Azure client libraries for .NET offer both synchronous and asynchronous |
| 8 | +methods for calling Azure services. You can distinguish the asynchronous |
| 9 | +methods by their `Async` suffix. For example, `BlobClient.Download` and |
| 10 | +`BlobClient.DownloadAsync` make the same underlying REST call and only differ in |
| 11 | +whether they block. We recommend using our async methods for new applications, |
| 12 | +but there are perfectly valid cases for using sync methods as well. These dual |
| 13 | +method invocation semantics allow for flexibility, but require a little extra |
| 14 | +care when writing event handlers. |
| 15 | + |
| 16 | +The `SyncAsyncEventHandler` is a delegate used by events in Azure client |
| 17 | +libraries to represent an event handler that can be invoked from either sync or |
| 18 | +async code paths. It takes event arguments deriving from `SyncAsyncEventArgs` |
| 19 | +that contain important information for writing your event handler. |
| 20 | + |
| 21 | +- `SyncAsyncEventArgs.CancellationToken` is a cancellation token related to the |
| 22 | + original operation that raised the event. It's important for your handler to |
| 23 | + pass this token along to any asynchronous or long-running synchronous |
| 24 | + operations that take a token so cancellation (via something like |
| 25 | + `new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token`, for example) |
| 26 | + will correctly propagate. |
| 27 | + |
| 28 | +- There is a `SyncAsyncEventArgs.RunSynchronously` flag indicating whether your |
| 29 | + handler was invoked synchronously or asynchronously. In general, |
| 30 | + |
| 31 | + - If you're calling sync methods on your client, you should use sync methods |
| 32 | + to implement your event handler. You can return `Task.CompletedTask`. |
| 33 | + - If you're calling async methods on your client, you should use async |
| 34 | + methods where possible to implement your event handler. |
| 35 | + - If you're not in control of how the client will be used or want to write |
| 36 | + safer code, you should check the `RunSynchronously` property and call |
| 37 | + either sync or async methods as directed. |
| 38 | + |
| 39 | + There are code examples of all three situations below to compare. Please also |
| 40 | + see the note at the very end discussing the dangers of sync-over-async to |
| 41 | + understand the risks of not using the `RunSynchronously` flag. |
| 42 | + |
| 43 | +- Most events will customize the event data by deriving from `SyncAsyncEventArgs` |
| 44 | + and including details about what triggered the event or providing options to |
| 45 | + react. Many times this will include a reference to the client that raised the |
| 46 | + event in case you need it for additional processing. |
| 47 | + |
| 48 | +When an event using `SyncAsyncEventHandler` is raised, the handlers will be |
| 49 | +executed sequentially to avoid introducing any unintended parallelism. The |
| 50 | +event handlers will finish before returning control to the code path raising the |
| 51 | +event. This means blocking for events raised synchronously and waiting for the |
| 52 | +returned `Task` to complete for events raised asynchronously. |
| 53 | + |
| 54 | +Any exceptions thrown from a handler will be wrapped in a single |
| 55 | +`AggregateException`. If one handler throws an exception, it will not prevent |
| 56 | +other handlers from running. This is also relevant for cancellation because all |
| 57 | +handlers are still raised if cancellation occurs. You should both pass |
| 58 | +`SyncAsyncEventArgs.CancellationToken` to asynchronous or long-running |
| 59 | +synchronous operations and consider calling `CancellationToken.ThrowIfCancellationRequested` |
| 60 | +in compute heavy handlers. |
| 61 | + |
| 62 | +A [distributed tracing span](https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/samples/Diagnostics.md#distributed-tracing) |
| 63 | +is wrapped around your handlers using the event name so you can see how long |
| 64 | +your handlers took to run, whether they made other calls to Azure services, and |
| 65 | +details about any exceptions that were thrown. |
| 66 | + |
| 67 | +The rest of the code samples are using a fictitious `AlarmClient` to demonstrate |
| 68 | +how to handle `SyncAsyncEventHandler` events. There are `Snooze` and |
| 69 | +`SnoozeAsync` methods that both raise a `Ring` event. |
| 70 | + |
| 71 | +## Adding a synchronous event handler |
| 72 | + |
| 73 | +If you're using the synchronous, blocking methods of a client (i.e., methods |
| 74 | +without an `Async` suffix), they will raise events that require handlers to |
| 75 | +execute synchronously as well. Even though the signature of your handler |
| 76 | +returns a `Task`, you should write regular sync code that blocks and return |
| 77 | +`Task.CompletedTask` when finished. |
| 78 | + |
| 79 | +```C# Snippet:Azure_Core_Samples_EventSamples_SyncHandler |
| 80 | +var client = new AlarmClient(); |
| 81 | +client.Ring += (SyncAsyncEventArgs e) => |
| 82 | +{ |
| 83 | + Console.WriteLine("Wake up!"); |
| 84 | + return Task.CompletedTask; |
| 85 | +}; |
| 86 | + |
| 87 | +client.Snooze(); |
| 88 | +``` |
| 89 | + |
| 90 | +If you need to call an async method from a synchronous event handler, you have |
| 91 | +two options: |
| 92 | + |
| 93 | +- You can use [`Task.Run`](https://docs.microsoft.com/dotnet/api/system.threading.tasks.task.run) |
| 94 | + to queue a task for execution on the ThreadPool without waiting on it to |
| 95 | + complete. This "fire and forget" approach may not run before your handler |
| 96 | + finishes executing. Be sure to understand |
| 97 | + [exception handling in the Task Parallel Library](https://docs.microsoft.com/dotnet/standard/parallel-programming/exception-handling-task-parallel-library) |
| 98 | + to avoid unhandled exceptions tearing down your process. |
| 99 | +- If you absolutely need the async method to execute before returning from your |
| 100 | + handler, you can call `myAsyncTask.GetAwaiter().GetResult()`. Please be aware |
| 101 | + this may cause ThreadPool starvation. See the sync-over-async note below for |
| 102 | + more details. |
| 103 | + |
| 104 | +## Adding an asynchronous event handler |
| 105 | + |
| 106 | +If you're using the asynchronous, non-blocking methods of a client (i.e., |
| 107 | +methods with an `Async` suffix), they will raise events that expect handlers to |
| 108 | +execute asynchronously. |
| 109 | + |
| 110 | +```C# Snippet:Azure_Core_Samples_EventSamples_AsyncHandler |
| 111 | +var client = new AlarmClient(); |
| 112 | +client.Ring += async (SyncAsyncEventArgs e) => |
| 113 | +{ |
| 114 | + await Console.Out.WriteLineAsync("Wake up!"); |
| 115 | +}; |
| 116 | + |
| 117 | +await client.SnoozeAsync(); |
| 118 | +``` |
| 119 | + |
| 120 | +## Handlers that can be called sync or async |
| 121 | + |
| 122 | +The same event can be raised from both synchronous and asynchronous code paths |
| 123 | +depending on whether you're calling sync or async methods on a client. If you |
| 124 | +write an async handler but raise it from a sync method, the handler will be |
| 125 | +doing sync-over-async and may cause ThreadPool starvation. See the note at the |
| 126 | +bottom for more details. |
| 127 | + |
| 128 | +You should use the `SyncAsyncEventArgs.RunSynchronously` property to check how |
| 129 | +the event is being raised and implement your handler accordingly. Here's an |
| 130 | +example handler that's safe to invoke from both sync and async code paths. |
| 131 | + |
| 132 | +```C# Snippet:Azure_Core_Samples_EventSamples_CombinedHandler |
| 133 | +var client = new AlarmClient(); |
| 134 | +client.Ring += async (SyncAsyncEventArgs e) => |
| 135 | +{ |
| 136 | + if (e.RunSynchronously) |
| 137 | + { |
| 138 | + Console.WriteLine("Wake up!"); |
| 139 | + } |
| 140 | + else |
| 141 | + { |
| 142 | + await Console.Out.WriteLineAsync("Wake up!"); |
| 143 | + } |
| 144 | +}; |
| 145 | + |
| 146 | +client.Snooze(); // sync call that blocks |
| 147 | +await client.SnoozeAsync(); // async call that doesn't block |
| 148 | +``` |
| 149 | + |
| 150 | +## Handling exceptions |
| 151 | + |
| 152 | +Any exceptions thrown by an event handler will be wrapped in a single |
| 153 | +[`AggregateException`](https://docs.microsoft.com/dotnet/api/system.aggregateexception) and thrown from the code that raised the event. You can check the |
| 154 | +[`AggregateException.InnerExceptions`](https://docs.microsoft.com/dotnet/api/system.aggregateexception.innerexceptions) |
| 155 | +property to see the original exceptions thrown by your event handlers. |
| 156 | +`AggregateException` also provides |
| 157 | +[a number of helpful methods](https://docs.microsoft.com/archive/msdn-magazine/2009/brownfield/aggregating-exceptions) |
| 158 | +like `Flatten` and `Handle` to make complex failures easier to work with. |
| 159 | + |
| 160 | +```C# Snippet:Azure_Core_Samples_EventSamples_Exceptions |
| 161 | +var client = new AlarmClient(); |
| 162 | +client.Ring += (SyncAsyncEventArgs e) => |
| 163 | + throw new InvalidOperationException("Alarm unplugged."); |
| 164 | + |
| 165 | +try |
| 166 | +{ |
| 167 | + client.Snooze(); |
| 168 | +} |
| 169 | +catch (AggregateException ex) |
| 170 | +{ |
| 171 | + ex.Handle(e => e is InvalidOperationException); |
| 172 | + Console.WriteLine("Please switch to your backup alarm."); |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +## Sync-over-async |
| 177 | + |
| 178 | +Executing asynchronous code from a sync code path is commonly referred to as |
| 179 | +sync-over-async because you're getting sync behavior but still invoking all the |
| 180 | +async machinery. See |
| 181 | +[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) |
| 182 | +for a detailed explanation of how that can cause serious performance problems. |
| 183 | +We recommend you use the `SyncAsyncEventArgs.RunSynchronously` flag to avoid |
| 184 | +ThreadPool starvation. |
| 185 | + |
| 186 | +But what about executing synchronous code on an async code path like the "Adding |
| 187 | +a synchronous event handler" code sample above? This is perfectly okay. Behind |
| 188 | +the scenes, we're effectively doing something like: |
| 189 | + |
| 190 | +```C# |
| 191 | +var task = InvokeHandler(); |
| 192 | +if (!task.IsCompleted) |
| 193 | +{ |
| 194 | + task.Wait(); |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +Writing sync code in your handler will block before returning a completed `Task` |
| 199 | +so there's no need to involve the ThreadPool to run your handler. |
0 commit comments