Skip to content

Fix headless session hang when cleanup throws - #21781

Merged
MrJul merged 3 commits into
AvaloniaUI:mainfrom
NathanDrake2406:nathan/fix-headless-session-cleanup-hang
Jul 20, 2026
Merged

Fix headless session hang when cleanup throws#21781
MrJul merged 3 commits into
AvaloniaUI:mainfrom
NathanDrake2406:nathan/fix-headless-session-cleanup-hang

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

What does the pull request do?

Fixes a deterministic HeadlessUnitTestSession hang when a dispatcher job throws during work-item cleanup. The cleanup exception is now reported through the affected Dispatch task, 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 TaskCompletionSource is settled, as required by #21223. If application.Dispose() throws, the exception bypasses the completion code and escapes the queued action. The private consumer task faults, the current Dispatch task never completes, and all later dispatches remain queued forever.

What is the updated/expected behavior with this PR?

The affected Dispatch task 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 InvalidOperationException is surfaced, then verifies another dispatch returns successfully. It runs in both NUnit isolation projects.

Validation on macOS ARM64 with .NET 10:

  • NUnit PerAssembly: 74 total, 73 passed, 1 intentional skip
  • NUnit PerTest: 74 total, 73 passed, 1 intentional skip
  • xUnit PerAssembly: 71 total, 70 passed, 1 intentional skip
  • xUnit PerTest: 71 total, 70 passed, 1 intentional skip

How was the solution implemented (if it's not obvious)?

DispatchCore catches exceptions from application disposal and stores them as the work item's result before completing its TaskCompletionSource. Cleanup callbacks use finally blocks 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

  • Added unit tests (if possible)?
  • Added XML documentation to any related classes? No public API changes.
  • Consider submitting a PR to https://github.com/AvaloniaUI/avalonia-docs with user documentation. No documentation change is needed for this internal failure-path fix.

Breaking changes

None. The failure path now completes with the original cleanup exception instead of hanging indefinitely.

Obsoletions / Deprecations

None.

Fixed issues

Fixes #21770

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
NathanDrake2406 marked this pull request as ready for review July 15, 2026 08:00
@MrJul MrJul added bug area-headless backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Jul 15, 2026
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0067478-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul
MrJul enabled auto-merge July 20, 2026 18:26
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0067561-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul
MrJul added this pull request to the merge queue Jul 20, 2026
Merged via the queue into AvaloniaUI:main with commit 0b3243e Jul 20, 2026
9 checks passed
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>
@MrJul MrJul added backported-12.1.x and removed backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants