Fix headless session hang when cleanup throws - #21781
Merged
MrJul merged 3 commits intoJul 20, 2026
Merged
Conversation
A dispatcher job that throws during work-item cleanup leaves the dispatch task incomplete and prevents later work from running. The session contract requires every queued dispatch to reach a terminal state. Add behavioral coverage that posts a throwing cleanup job, expects its exception from Dispatch, and verifies the assembly session can process the next dispatch. The shared NUnit test runs under both PerTest and PerAssembly projects.
An exception during application cleanup escapes the work item before its completion source is settled, faults the private consumer task, and leaves current and future Dispatch calls waiting forever. Capture cleanup failures as the work item's exception so the consumer can continue. Restore synchronization context, locator scope, and dispatcher state in finally blocks before processing later work.
NathanDrake2406
marked this pull request as ready for review
July 15, 2026 08:00
|
You can test this PR using the following package version. |
maxkatz6
approved these changes
Jul 18, 2026
MrJul
enabled auto-merge
July 20, 2026 18:26
|
You can test this PR using the following package version. |
MrJul
added a commit
that referenced
this pull request
Jul 29, 2026
* test(headless): reproduce cleanup exception dispatch hang A dispatcher job that throws during work-item cleanup leaves the dispatch task incomplete and prevents later work from running. The session contract requires every queued dispatch to reach a terminal state. Add behavioral coverage that posts a throwing cleanup job, expects its exception from Dispatch, and verifies the assembly session can process the next dispatch. The shared NUnit test runs under both PerTest and PerAssembly projects. * fix(headless): report dispatch cleanup exceptions An exception during application cleanup escapes the work item before its completion source is settled, faults the private consumer task, and leaves current and future Dispatch calls waiting forever. Capture cleanup failures as the work item's exception so the consumer can continue. Restore synchronization context, locator scope, and dispatcher state in finally blocks before processing later work. --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does the pull request do?
Fixes a deterministic
HeadlessUnitTestSessionhang when a dispatcher job throws during work-item cleanup. The cleanup exception is now reported through the affectedDispatchtask, and the session remains able to process later work.This is the cleanup-stage counterpart to the application-construction fix in #21688.
What is the current behavior?
Cleanup runs before a dispatch's
TaskCompletionSourceis settled, as required by #21223. Ifapplication.Dispose()throws, the exception bypasses the completion code and escapes the queued action. The private consumer task faults, the currentDispatchtask never completes, and all later dispatches remain queued forever.What is the updated/expected behavior with this PR?
The affected
Dispatchtask faults with the cleanup exception. Required synchronization context, locator scope, and dispatcher state restoration still runs, so the consumer remains healthy and a subsequent dispatch completes normally.The regression test posts a dispatcher job that throws during cleanup, verifies the exact
InvalidOperationExceptionis surfaced, then verifies another dispatch returns successfully. It runs in both NUnit isolation projects.Validation on macOS ARM64 with .NET 10:
How was the solution implemented (if it's not obvious)?
DispatchCorecatches exceptions from application disposal and stores them as the work item's result before completing itsTaskCompletionSource. Cleanup callbacks usefinallyblocks to restore ambient state even when a dispatcher cleanup job throws.The commits follow the repository's bug-fix convention: the first commit adds the failing behavioral test, and the second commit contains the fix.
Checklist
Breaking changes
None. The failure path now completes with the original cleanup exception instead of hanging indefinitely.
Obsoletions / Deprecations
None.
Fixed issues
Fixes #21770