Don't swallow timeout exceptions on DistributedApplicationTestingBuilder.StartAsync()#15477
Don't swallow timeout exceptions on DistributedApplicationTestingBuilder.StartAsync()#15477afscrome wants to merge 1 commit intomicrosoft:mainfrom
DistributedApplicationTestingBuilder.StartAsync()#15477Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15477Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15477" |
There was a problem hiding this comment.
Pull request overview
Narrows cancellation suppression in DcpExecutor so DistributedApplicationTestingBuilder.StartAsync() no longer returns successfully when startup is canceled due to caller cancellation/timeouts, while still avoiding surfacing cancellations caused by host shutdown (e.g., Ctrl+C).
Changes:
- Update
DcpExecutorto swallowOperationCanceledExceptiononly afterIHostApplicationLifetime.ApplicationStoppingis signaled. - Add/adjust tests to distinguish user-cancellation vs host-shutdown during startup.
- Improve test
IHostApplicationLifetimeutility to provide cancelable lifetime tokens.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Hosting.Tests/Utils/TestHostApplicationLifetime.cs | Implement real lifetime cancellation tokens for tests and add disposal. |
| tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs | Add coverage for StartAsync cancellation behavior during startup. |
| tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs | Split/rename tests to validate user-cancel vs host-shutdown cancellation behavior; extend test executor factory to accept host lifetime. |
| src/Aspire.Hosting/DistributedApplicationLifecycle.cs | Avoid printing “Press Ctrl+C…” when startup is already canceled. |
| src/Aspire.Hosting/Dcp/DcpExecutor.cs | Inject IHostApplicationLifetime and gate OCE suppression on ApplicationStopping. |
| await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => | ||
| { | ||
| using var cts = new CancellationTokenSource(); | ||
| var task = app.StartAsync(cts.Token); | ||
| app.Services.GetRequiredService<IHostApplicationLifetime>().StopApplication(); | ||
| await task; | ||
| }).DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); |
There was a problem hiding this comment.
Test name says StartAsync should not throw when the host is shutting down during startup, but the assertion currently expects an OperationCanceledException. This will fail once cancellation is correctly suppressed for host shutdown; update the test to assert successful completion (and optionally assert ApplicationStopping was signaled) instead of asserting an exception.
| await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => | |
| { | |
| using var cts = new CancellationTokenSource(); | |
| var task = app.StartAsync(cts.Token); | |
| app.Services.GetRequiredService<IHostApplicationLifetime>().StopApplication(); | |
| await task; | |
| }).DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); | |
| var applicationLifetime = app.Services.GetRequiredService<IHostApplicationLifetime>(); | |
| var applicationStoppingTcs = new TaskCompletionSource<object?>(); | |
| using var registration = applicationLifetime.ApplicationStopping.Register(() => applicationStoppingTcs.TrySetResult(null)); | |
| using var cts = new CancellationTokenSource(); | |
| var startTask = app.StartAsync(cts.Token); | |
| applicationLifetime.StopApplication(); | |
| await startTask.DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); | |
| await applicationStoppingTcs.Task.DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); |
| hostApplicationLifetime ?? new TestHostApplicationLifetime(), | ||
| resourceLoggerService, |
There was a problem hiding this comment.
TestHostApplicationLifetime now implements IDisposable, but this helper creates a new instance inline when hostApplicationLifetime is null and never disposes it. Consider requiring callers to pass an instance (and dispose it in the test), or store the created instance and ensure it’s disposed when the executor/test completes.
7bba3d7 to
1c0ba6f
Compare
…lder.StartAsync()` - The implementation only occurred around part of the startup process, so it was still possible to see cancellation ex - It impacted all cancellations, (including test timeouts) which was undesirable. To fix this, I've taken an alternative solution by moving the suppression logic into `DistributedApplication`. - `StartAsync()` will always throw on a cancellation as if you're calling `StartAsync()`, you're probably wanting to do something with the application once `StartAsync()` finishes. - `RunAsync()` however suppresses the cancellation exception when`IHostApplicationLifetime.ApplicationStopping.IsCancellationRequested` (which is how `ctrl+c` stops the host)
1c0ba6f to
30a5def
Compare
Description
#8886 updated
DcpExecutorto swallowOperationCancellationExceptions. It was intended to avoid propagating cancellation errors whenctrl + cwas pressed interactively. But this implementation had a couple of issues:To fix this, I've taken an alternative solution by moving the suppression ogic into
DistributedApplication.StartAsync()will always throw on a cancelaltion as if you're callingStartAsync(), you're probably wanting to do something with the application onceStartAsync()finishes.RunAsync()however supresses the cancelation exceptionIHostApplicationLifetime.ApplicationStopping.IsCancellationRequestedFixes #13728
Checklist
<remarks />and<code />elements on your triple slash comments?aspire.devissue: