| File: Backchannel\AppHostRpcTargetCancellationTests.cs | Web Access |
| Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting.Backchannel; /// <summary> /// Tests that cancellation of streaming RPC methods on <see cref="AppHostRpcTarget"/> completes /// the stream gracefully (i.e. no <see cref="OperationCanceledException"/> propagates to callers). /// </summary> [Trait("Partition", "4")] public class AppHostRpcTargetCancellationTests(ITestOutputHelper outputHelper) { // ------------------------------------------------------------------------- // GetAppHostLogEntriesAsync // ------------------------------------------------------------------------- [Fact] public async Task GetAppHostLogEntriesAsync_WhenOuterTokenAlreadyCancelled_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); using var cts = new CancellationTokenSource(); await cts.CancelAsync(); // Iterating with an already-cancelled token should complete without throwing. await foreach (var _ in target.GetAppHostLogEntriesAsync(cts.Token)) { } await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } [Fact] public async Task GetAppHostLogEntriesAsync_WhenOuterTokenCancelledDuringIteration_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); using var cts = new CancellationTokenSource(); // Start iterating; cancel the token after starting but let it complete normally. var iterationTask = Task.Run(async () => { await foreach (var _ in target.GetAppHostLogEntriesAsync(cts.Token)) { } }); await cts.CancelAsync(); // Should finish without exception within a short timeout. await iterationTask.WaitAsync(TimeSpan.FromSeconds(10)); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } [Fact] public async Task GetAppHostLogEntriesAsync_WhenShutdownCtsCancelled_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); var iterationTask = Task.Run(async () => { await foreach (var _ in target.GetAppHostLogEntriesAsync(CancellationToken.None)) { } }); // Simulate the shutdown path (same as RequestStopAsync calling _shutdownCts.Cancel()). target.CancelInflightRpcCalls(); await iterationTask.WaitAsync(TimeSpan.FromSeconds(10)); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } // ------------------------------------------------------------------------- // GetPublishingActivitiesAsync // ------------------------------------------------------------------------- [Fact] public async Task GetPublishingActivitiesAsync_WhenOuterTokenAlreadyCancelled_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); using var cts = new CancellationTokenSource(); await cts.CancelAsync(); await foreach (var _ in target.GetPublishingActivitiesAsync(cts.Token)) { } await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } [Fact] public async Task GetPublishingActivitiesAsync_WhenOuterTokenCancelledDuringIteration_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); using var cts = new CancellationTokenSource(); var iterationTask = Task.Run(async () => { await foreach (var _ in target.GetPublishingActivitiesAsync(cts.Token)) { } }); await cts.CancelAsync(); await iterationTask.WaitAsync(TimeSpan.FromSeconds(10)); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } [Fact] public async Task GetPublishingActivitiesAsync_WhenShutdownCtsCancelled_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); var iterationTask = Task.Run(async () => { await foreach (var _ in target.GetPublishingActivitiesAsync(CancellationToken.None)) { } }); target.CancelInflightRpcCalls(); await iterationTask.WaitAsync(TimeSpan.FromSeconds(10)); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } // ------------------------------------------------------------------------- // GetResourceStatesAsync // ------------------------------------------------------------------------- [Fact] public async Task GetResourceStatesAsync_WhenOuterTokenAlreadyCancelled_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); using var cts = new CancellationTokenSource(); await cts.CancelAsync(); await foreach (var _ in target.GetResourceStatesAsync(cts.Token)) { } await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } [Fact] public async Task GetResourceStatesAsync_WhenOuterTokenCancelledDuringIteration_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); using var cts = new CancellationTokenSource(); var iterationTask = Task.Run(async () => { await foreach (var _ in target.GetResourceStatesAsync(cts.Token)) { } }); await cts.CancelAsync(); await iterationTask.WaitAsync(TimeSpan.FromSeconds(10)); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } [Fact] public async Task GetResourceStatesAsync_WhenShutdownCtsCancelled_StreamCompletesWithoutException() { using var builder = TestDistributedApplicationBuilder.Create(outputHelper); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); var target = app.Services.GetRequiredService<AppHostRpcTarget>(); var iterationTask = Task.Run(async () => { await foreach (var _ in target.GetResourceStatesAsync(CancellationToken.None)) { } }); target.CancelInflightRpcCalls(); await iterationTask.WaitAsync(TimeSpan.FromSeconds(10)); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } // ------------------------------------------------------------------------- // BackchannelService — socket waiting when stoppingToken fires // ------------------------------------------------------------------------- [Fact] public async Task BackchannelService_WhenStoppingTokenFiredWhileWaitingForConnection_AppStopsCleanly() { // Configure a socket path but never connect a client, so AcceptAsync will be // waiting when the app host stops and fires the stoppingToken. using var builder = TestDistributedApplicationBuilder.Create(outputHelper); builder.Configuration[KnownConfigNames.UnixSocketPath] = UnixSocketHelper.GetBackchannelSocketPath(); using var app = builder.Build(); await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(60)); // Stop without ever connecting — BackchannelService's AcceptAsync is still waiting. // Prior to the fix this would surface an unhandled OperationCanceledException; // after the fix it should stop cleanly. await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(60)); } }