From 8aa6c1822a8400db5772e563b54847e239d7cc88 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Tue, 9 Jan 2024 12:00:52 +1100 Subject: [PATCH] Avoid cancellation messages in logs (#1590) * Avoid cancellation messages in logs When clients unsubscribe from resources or console logs, suppress any logging due to cancellation exceptions. This helps avoid log messages like this: ``` fail: Grpc.AspNetCore.Server.ServerCallHandler[6] Error when executing service method 'WatchResourceConsoleLogs'. System.OperationCanceledException: The operation was canceled. at System.Threading.Channels.AsyncOperation`1.GetResult(Int16 token) at Aspire.Hosting.Extensions.ChannelExtensions.GetBatches[T](Channel`1 channel, CancellationToken cancellationToken)+MoveNext() in D:\repos\aspire\src\Aspire.Hosting\Extensions\ChannelExtensions.cs:line 33 at Aspire.Hosting.Extensions.ChannelExtensions.GetBatches[T](Channel`1 channel, CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource.GetResult() at Aspire.Hosting.Dashboard.FileLogSource.GetAsyncEnumerator(CancellationToken cancellationToken)+MoveNext() in D:\repos\aspire\src\Aspire.Hosting\Dashboard\FileLogSource.cs:line 26 at Aspire.Hosting.Dashboard.FileLogSource.GetAsyncEnumerator(CancellationToken cancellationToken)+MoveNext() in D:\repos\aspire\src\Aspire.Hosting\Dashboard\FileLogSource.cs:line 26 at Aspire.Hosting.Dashboard.FileLogSource.GetAsyncEnumerator(CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource.GetResult() at Aspire.Hosting.Dashboard.DashboardServiceData.<>c__DisplayClass6_0.<g__Enumerate|0>d.MoveNext() in D:\repos\aspire\src\Aspire.Hosting\Dashboard\DashboardServiceData.cs:line 54 --- End of stack trace from previous location --- at Aspire.Hosting.Dashboard.DashboardServiceData.<>c__DisplayClass6_0.<g__Enumerate|0>d.MoveNext() in D:\repos\aspire\src\Aspire.Hosting\Dashboard\DashboardServiceData.cs:line 54 --- End of stack trace from previous location --- at Aspire.Hosting.Dashboard.DashboardServiceData.<>c__DisplayClass6_0.<g__Enumerate|0>d.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token) at Aspire.Hosting.Dashboard.DashboardService.WatchResourceConsoleLogs(WatchResourceConsoleLogsRequest request, IServerStreamWriter`1 responseStream, ServerCallContext context) in D:\repos\aspire\src\Aspire.Hosting\Dashboard\DashboardService.cs:line 101 at Aspire.Hosting.Dashboard.DashboardService.WatchResourceConsoleLogs(WatchResourceConsoleLogsRequest request, IServerStreamWriter`1 responseStream, ServerCallContext context) in D:\repos\aspire\src\Aspire.Hosting\Dashboard\DashboardService.cs:line 101 at Grpc.Shared.Server.ServerStreamingServerMethodInvoker`3.Invoke(HttpContext httpContext, ServerCallContext serverCallContext, TRequest request, IServerStreamWriter`1 streamWriter) at Grpc.Shared.Server.ServerStreamingServerMethodInvoker`3.Invoke(HttpContext httpContext, ServerCallContext serverCallContext, TRequest request, IServerStreamWriter`1 streamWriter) at Grpc.AspNetCore.Server.Internal.CallHandlers.ServerStreamingServerCallHandler`3.HandleCallAsyncCore(HttpContext httpContext, HttpContextServerCallContext serverCallContext) at Grpc.AspNetCore.Server.Internal.CallHandlers.ServerCallHandlerBase`3.g__AwaitHandleCall|8_0(HttpContextServerCallContext serverCallContext, Method`2 method, Task handleCall) ``` * Catch IOException too, and check cancellation token --- .../Dashboard/DashboardService.cs | 92 ++++++++++++------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/src/Aspire.Hosting/Dashboard/DashboardService.cs b/src/Aspire.Hosting/Dashboard/DashboardService.cs index e21e9d34c7..06074c0071 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardService.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardService.cs @@ -47,42 +47,54 @@ public override async Task WatchResources( IServerStreamWriter responseStream, ServerCallContext context) { - var (initialData, updates) = serviceData.SubscribeResources(); - - var data = new InitialResourceData(); - - foreach (var resource in initialData) + try { - data.Resources.Add(Resource.FromSnapshot(resource)); + await WatchResourcesInternal().ConfigureAwait(false); + } + catch (Exception ex) when (ex is OperationCanceledException or IOException && context.CancellationToken.IsCancellationRequested) + { + // Ignore cancellation and just return. Note that cancelled writes throw IOException. } - await responseStream.WriteAsync(new() { InitialData = data }).ConfigureAwait(false); - - await foreach (var batch in updates.WithCancellation(context.CancellationToken)) + async Task WatchResourcesInternal() { - WatchResourcesChanges changes = new(); + var (initialData, updates) = serviceData.SubscribeResources(); + + var data = new InitialResourceData(); - foreach (var update in batch) + foreach (var resource in initialData) { - var change = new WatchResourcesChange(); + data.Resources.Add(Resource.FromSnapshot(resource)); + } - if (update.ChangeType is ResourceSnapshotChangeType.Upsert) - { - change.Upsert = Resource.FromSnapshot(update.Resource); - } - else if (update.ChangeType is ResourceSnapshotChangeType.Delete) - { - change.Delete = new() { ResourceName = update.Resource.Name, ResourceType = update.Resource.ResourceType }; - } - else + await responseStream.WriteAsync(new() { InitialData = data }).ConfigureAwait(false); + + await foreach (var batch in updates.WithCancellation(context.CancellationToken)) + { + WatchResourcesChanges changes = new(); + + foreach (var update in batch) { - throw new FormatException($"Unexpected {nameof(ResourceSnapshotChange)} type: {update.ChangeType}"); + var change = new WatchResourcesChange(); + + if (update.ChangeType is ResourceSnapshotChangeType.Upsert) + { + change.Upsert = Resource.FromSnapshot(update.Resource); + } + else if (update.ChangeType is ResourceSnapshotChangeType.Delete) + { + change.Delete = new() { ResourceName = update.Resource.Name, ResourceType = update.Resource.ResourceType }; + } + else + { + throw new FormatException($"Unexpected {nameof(ResourceSnapshotChange)} type: {update.ChangeType}"); + } + + changes.Value.Add(change); } - changes.Value.Add(change); + await responseStream.WriteAsync(new() { Changes = changes }, context.CancellationToken).ConfigureAwait(false); } - - await responseStream.WriteAsync(new() { Changes = changes }, context.CancellationToken).ConfigureAwait(false); } } @@ -91,23 +103,35 @@ public override async Task WatchResourceConsoleLogs( IServerStreamWriter responseStream, ServerCallContext context) { - var subscription = serviceData.SubscribeConsoleLogs(request.ResourceName); - - if (subscription is null) + try + { + await WatchResourceConsoleLogsInternal().ConfigureAwait(false); + } + catch (Exception ex) when (ex is OperationCanceledException or IOException && context.CancellationToken.IsCancellationRequested) { - return; + // Ignore cancellation and just return. Note that cancelled writes throw IOException. } - await foreach (var group in subscription.WithCancellation(context.CancellationToken)) + async Task WatchResourceConsoleLogsInternal() { - WatchResourceConsoleLogsUpdate update = new(); + var subscription = serviceData.SubscribeConsoleLogs(request.ResourceName); - foreach (var (content, isErrorMessage) in group) + if (subscription is null) { - update.LogLines.Add(new ConsoleLogLine() { Text = content, IsStdErr = isErrorMessage }); + return; } - await responseStream.WriteAsync(update, context.CancellationToken).ConfigureAwait(false); + await foreach (var group in subscription.WithCancellation(context.CancellationToken)) + { + WatchResourceConsoleLogsUpdate update = new(); + + foreach (var (content, isErrorMessage) in group) + { + update.LogLines.Add(new ConsoleLogLine() { Text = content, IsStdErr = isErrorMessage }); + } + + await responseStream.WriteAsync(update, context.CancellationToken).ConfigureAwait(false); + } } } }