Skip to content

fix(aspire): preserve caller cancellation over startup cleanup faults - #7447

Merged
thomhurst merged 1 commit into
mainfrom
issue-7282-startup-cancellation
Nov 3, 2026
Merged

thomhurst merged 1 commit into
mainfrom
issue-7282-startup-cancellation

Conversation

@thomhurst

@thomhurst thomhurst commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Caller cancellation could surface a losing startup wait's cleanup fault instead of OperationCanceledException. Linked cancellation reaches all waits, so a cleanup fault can win the task race before a cancelled wait's continuation runs. This also fails unrelated PR CI, including #7400 (run 41566483900).

Check the caller token after the startup race settles, before reading its result. Caller cancellation takes precedence at that point; the existing finally block still cancels and joins every wait and observes cleanup faults. No public API changes.

Add a deterministic regression where cancellation and faulted waits are present before the race is consumed. It fails against the original waiter with the cleanup InvalidOperationException and passes with this fix.

Closes #7282

Validation

.NET SDK 10.0.403, macOS arm64:

  • Regression failed before the fix.
  • dotnet run --project tests/TUnit.Aspire.Tests --framework net10.0 -- --treenode-filter '/*/*/ResourceStartupWaiterTests/*': 11 passed, zero failed.
  • git diff --check passed.

Existing build warnings are tracked by #7410. Full Aspire integration validation is left to CI; this targeted run covers startup outcome and cleanup behavior.

Summary by CodeRabbit

  • Bug Fixes
    • Startup waits now honor caller cancellation when cancellation and wait failures occur together.

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

ResourceStartupWaiter.WaitAsync now gives caller cancellation precedence after the readiness/failure race completes. A test verifies that it throws OperationCanceledException with the caller’s token when both waits fault.

Changes: Caller cancellation outcome

Layer / File(s) Summary
Cancellation precedence and validation
src/TUnit.Aspire.Core/ResourceStartupWaiter.cs, tests/TUnit.Aspire.Tests/ResourceStartupWaiterTests.cs
WaitAsync checks the caller’s token before processing the race result. The test verifies that caller cancellation is thrown with the caller’s token when both waits fault.

Estimated code review effort: 2 (Simple) | ~8 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

  • thomhurst/TUnit#7279: This PR introduced ResourceStartupWaiter.WaitAsync and its readiness/failure race, which this change updates.
Merge Risk: Minimal · up to fa0fa

The startup cancellation behavior is covered by the regression test, with no actionable merge-blocking risk identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title names the main change: caller cancellation now takes precedence over faults raised by startup cleanup in the startup waiter. The raw summary confirms that WaitAsync checks the caller's cance…
Linked Issues check Passed Directly linked issue #7282 requires that caller cancellation surface OperationCanceledException when a losing wait faults during cleanup. At head fa0fa9f, ResourceStartupWaiter.WaitAsync calls cancel…
Out of Scope Changes check Passed The whole-PR diff against merge base 87d2a30 changes two files: ResourceStartupWaiter.cs (+3 lines, one ThrowIfCancellationRequested check with a comment) and ResourceStartupWaiterTests.cs (+23 lines,…
Full details: Title check

Explanation

The title names the main change: caller cancellation now takes precedence over faults raised by startup cleanup in the startup waiter. The raw summary confirms that WaitAsync checks the caller's cancellation token after the readiness/failure race and before reading its result, and that a regression test covers faulted waits. The title is specific, uses the conventional fix(aspire) prefix, and fits the changeset. It is 69 characters, within normal length limits.

Full details: Linked Issues check

Explanation

Directly linked issue #7282 requires that caller cancellation surface OperationCanceledException when a losing wait faults during cleanup. At head fa0fa9f, ResourceStartupWaiter.WaitAsync calls cancellationToken.ThrowIfCancellationRequested() immediately after await Task.WhenAny(readyTask, anyFailureTask) and before the result is read (src/TUnit.Aspire.Core/ResourceStartupWaiter.cs, lines 27-34). A cancelled caller therefore throws OperationCanceledException with the caller token, not the losing wait's InvalidOperationException. The existing finally block still cancels and joins all waits and swallows their faults, so the race outcome remains owned by the waiter. The new test CallerCancellation_TakesPrecedenceOverAnAlreadyFaultedWait (tests/TUnit.Aspire.Tests/ResourceStartupWaiterTests.cs) cancels during setup, supplies faulted ready and failure waits, and asserts OperationCanceledException whose CancellationToken equals the caller token. Both faulted tasks are already complete when WhenAny runs, so the test exercises the reported race without timing dependence. The PR reports that the test failed before the fix; I did not execute it. Residual note: if the caller cancels after all waits succeed, the method now throws OperationCanceledException where it previously returned null. This follows the caller-precedence rule and does not conflict with #7282.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff against merge base 87d2a30 changes two files: ResourceStartupWaiter.cs (+3 lines, one ThrowIfCancellationRequested check with a comment) and ResourceStartupWaiterTests.cs (+23 lines, one regression test). Both changes directly implement the #7282 objective. The PR description's CI, promotion, and auto-merge details describe process, not code, and introduce no repository changes. No public API changes are present, and the diff shows none. No unrelated code change was found.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 2 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ast-grep (0.51.1)

ast-grep could not parse rule config: /ast-grep-rules/ast-grep-essentials/rules/typescript/security/insecure-websocket-typescript.yml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A rabbit waits beside the gate
The caller’s token sets the state
Two waits may fault; the check comes first
Cancellation answers, not the worst
The test confirms the token’s name
And checks the outcome all the same

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a2ca663a-517e-454c-a24e-5cc9c79c36b2
Commits

Reviewing files that changed from the base of the PR and between 87d2a30 and fa0fa9f.

Files selected for processing: 2
  • src/TUnit.Aspire.Core/ResourceStartupWaiter.cs
  • tests/TUnit.Aspire.Tests/ResourceStartupWaiterTests.cs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-03T05:06:16.463213Z fa0fa9f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Review

The fix is small and targets the right spot. ThrowIfCancellationRequested() runs after the WhenAny race settles and before any result is read. That stops a faulted cleanup wait from replacing the caller's cancellation. The finally block still joins every wait. The regression test is deterministic: it cancels inside the fault factory, so the fault is already in place when the race is consumed.

Minor observations (non-blocking):

  • Cancellation now wins over a real outcome. If the caller cancels after readiness or a real failure has already completed but before the continuation resumes, the method throws OperationCanceledException and drops that result. For startup waiting, treating cancellation as authoritative is reasonable and matches the PR's stated intent. A short comment saying this is intentional would help future readers.
  • Alternative design. The root cause is that the waits' faults and cancellations race each other. You could instead wrap waitForReady and waitForFailure to swallow or normalise exceptions raised while waitsCts.IsCancellationRequested is set. That fixes it at the source, but it is more code and could hide real faults. The current one-line check is simpler and I prefer it.
  • Test. Failure(string name, CancellationToken token) has unused parameters. That is fine, but a discard-style lambda would be a bit tidier.

No correctness issues found. Looks good to merge once CI is green.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: fa0fa9fb25

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thomhurst

Copy link
Copy Markdown
Owner Author

Rechecked every review surface at fa0fa9f: Claude and Codex both report no blocking findings; there are no review threads. Claude's cancellation precedence observation matches the stated contract and existing regression name; the source comment already explains caller cancellation wins. The alternative normalization and unused-parameter suggestions are optional and do not warrant expanding this fix.

Promoting this unchanged head to run full CI and unblock #7400 via #7282. The ready-for-review Claude run must pass and its review be read before auto-merge is armed. No source changes or local tests in this review-only unit.

@thomhurst
thomhurst marked this pull request as ready for review November 3, 2026 05:05
@thomhurst

Copy link
Copy Markdown
Owner Author

Promotion confirmed with head unchanged at fa0fa9f. Waiting for ready-for-review Claude run 41581794658. Full CI has started. Next unit: read this promotion review, refresh all review surfaces, and arm auto-merge with scripts/Enable-AutoMerge.ps1 if the gate passes. Auto-merge remains unarmed until that review passes. Once this PR merges and closes #7282, refresh #7400 onto main for fresh CI.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Review

The fix is small and correct. Checking cancellationToken after Task.WhenAny settles and before the result is read means caller cancellation wins over a cleanup fault from a losing wait. The finally block still cancels and joins every wait, so no faults go unobserved. The regression test is deterministic: cancellation happens during setup and both waits are already faulted when the race is consumed.

Minor notes, none blocking:

  • Late cancellation: if a wait completes successfully and the caller cancels just before the check, the method now throws OperationCanceledException for a startup that finished. That is reasonable, since the caller asked to cancel, and the old code had a similar race. A short comment saying this is intentional would help.
  • Test: Failure takes name and token parameters it doesn't use. The Cancel() side effect in the factory is a slightly indirect way to set up the scenario, but the existing comment explains it well enough.

Approve.

@thomhurst
thomhurst enabled auto-merge (squash) November 3, 2026 05:06
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge.

Summary

ResourceStartupWaiter.WaitAsync now checks caller cancellation before reading the startup race result.

  • Aspire startup reports caller cancellation when a losing wait faults.

Reviews (1) · Last reviewed commit: "Fix #7282: preserve caller cancellation ..." · Reviewed by Greptile

@thomhurst

Copy link
Copy Markdown
Owner Author

Promotion review run 41581794658 passed. Re-read reviews, inline comments, all conversation comments, and review threads: Claude and Codex approve current head fa0fa9f; no unresolved or blocking findings. The non-blocking notes remain dispositioned in the earlier comment.

scripts/Enable-AutoMerge.ps1 passed and armed squash auto-merge pinned to this head. Waiting for full CI, including .NET run 41581793924. No code changes or local tests in this unit. After this merges and closes #7282, refresh #7400 onto main for fresh CI. The earlier review-run blocker has cleared; no on-hold label is needed.

@thomhurst

Copy link
Copy Markdown
Owner Author

Run 41581793924 is still active, but its Windows job 125738087455 has already failed on the known #7325 polling-throughput flake, independently of the Aspire change. Implemented #7325 in #7448; all 27 polling tests pass locally. After #7448 merges, refresh this PR onto main for fresh CI. The existing current-head Claude and Codex verdicts have no blocking findings. No rerun requested: fix the diagnosed cause first.

This was referenced Nov 4, 2026

This branch was previously deployed

1 inactive deployment
Pull Requests — fa0fa9fb Deployed Nov 3, 2026 by thomhurst via modularpipeline (ubuntu-latest) #20809
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.

Flaky: ResourceStartupWaiterTests.EveryExit_CancelsAndJoinsPendingWaits(cancellation, True) surfaces cleanup fault instead of cancellation

1 participant