Skip to content

Don't swallow timeout exceptions on DistributedApplicationTestingBuilder.StartAsync()#15477

Open
afscrome wants to merge 1 commit intomicrosoft:mainfrom
afscrome:dont-swallow-timeouts
Open

Don't swallow timeout exceptions on DistributedApplicationTestingBuilder.StartAsync()#15477
afscrome wants to merge 1 commit intomicrosoft:mainfrom
afscrome:dont-swallow-timeouts

Conversation

@afscrome
Copy link
Contributor

@afscrome afscrome commented Mar 21, 2026

Description

#8886 updated DcpExecutor to swallow OperationCancellationExceptions . It was intended to avoid propagating cancellation errors when ctrl + c was pressed interactively. But this implementation had a couple of issues:

  • 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 undesireable

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 calling StartAsync(), you're probably wanting to do something with the application once StartAsync() finishes.
  • RunAsync() however supresses the cancelation exception IHostApplicationLifetime.ApplicationStopping.IsCancellationRequested

Fixes #13728

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?

Copilot AI review requested due to automatic review settings March 21, 2026 23:04
@github-actions
Copy link
Contributor

github-actions bot commented Mar 21, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15477

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15477"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DcpExecutor to swallow OperationCanceledException only after IHostApplicationLifetime.ApplicationStopping is signaled.
  • Add/adjust tests to distinguish user-cancellation vs host-shutdown during startup.
  • Improve test IHostApplicationLifetime utility 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.

Comment on lines +403 to +409
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);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines 2626 to 2627
hostApplicationLifetime ?? new TestHostApplicationLifetime(),
resourceLoggerService,
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@afscrome afscrome force-pushed the dont-swallow-timeouts branch from 7bba3d7 to 1c0ba6f Compare March 22, 2026 00:10
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StartAsync swallows OperationCancelledExceptions

3 participants