Skip to content

Fix dotnet-watch Hot Reload deadlock on platforms with a startup SynchronizationContext#54131

Closed
kotlarmilos wants to merge 1 commit intodotnet:mainfrom
kotlarmilos:fix-hr-applier-deadlock
Closed

Fix dotnet-watch Hot Reload deadlock on platforms with a startup SynchronizationContext#54131
kotlarmilos wants to merge 1 commit intodotnet:mainfrom
kotlarmilos:fix-hr-applier-deadlock

Conversation

@kotlarmilos
Copy link
Copy Markdown
Member

@kotlarmilos kotlarmilos commented Apr 28, 2026

Description

This PR wraps the applier bootstrap in Listener.Listen in Task.Run(...) so that the awaits inside InitializeAsync resume on the threadpool instead of the startup-hook thread. The previous code did a synchronous InitializeAsync(ct).GetAwaiter().GetResult() directly on the startup thread. On platforms whose .NET startup-hook thread carries a SynchronizationContext (iOS Mono and CoreCLR, Android Mono and CoreCLR), the await inside WebSocketTransport.ConnectAsync captures that SC for its continuation, and the synchronous wait blocks the only thread that could resume it.

With this fix, end-to-end dotnet watch Hot Reload works across mobile on CoreCLR and Mono.

…hronizationContext

Wrap the applier bootstrap in Listener.Listen in Task.Run(...) so the
awaits inside InitializeAsync resume on the threadpool instead of the
startup-hook thread. The previous code did a synchronous
InitializeAsync(ct).GetAwaiter().GetResult() directly on the startup
thread. On platforms whose .NET startup-hook thread carries a
SynchronizationContext (iOS Mono and CoreCLR, Android Mono and CoreCLR),
the await inside WebSocketTransport.ConnectAsync captures that SC for
its continuation, and the synchronous wait blocks the only thread that
could resume it.

With this fix, end-to-end dotnet watch Hot Reload works across mobile
on CoreCLR and Mono.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 28, 2026 17:41
@kotlarmilos kotlarmilos requested review from a team and tmat as code owners April 28, 2026 17:41
@kotlarmilos kotlarmilos added this to the 11.0.1xx milestone Apr 28, 2026
@kotlarmilos kotlarmilos self-assigned this Apr 28, 2026
@kotlarmilos kotlarmilos marked this pull request as draft April 28, 2026 17:45
Copy link
Copy Markdown
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

Fixes a startup deadlock in dotnet-watch Hot Reload on platforms where the startup-hook thread has a SynchronizationContext (e.g., iOS/Android), by ensuring the initial Hot Reload agent bootstrap runs on the thread pool.

Changes:

  • Replace a synchronous InitializeAsync(...).GetResult() on the startup-hook thread with Task.Run(...).GetResult() to avoid SynchronizationContext capture.
  • Add explanatory comments describing the deadlock scenario and why the thread-pool hop is required.
Show a summary per file
File Description
src/Dotnet.Watch/HotReloadAgent.Host/Listener.cs Runs initial hot reload initialization on the thread pool to avoid SC-related deadlock during startup.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment on lines +37 to +43
// block execution of the app until initial updates are applied.
// Run on the thread pool (Task.Run) so that awaits inside InitializeAsync
// do not capture the calling thread's SynchronizationContext. Otherwise,
// on platforms where the startup-hook thread has a sync context (e.g. iOS,
// Android), the synchronous wait below would deadlock when an awaited
// continuation tries to resume on the same blocked thread.
Task.Run(() => InitializeAsync(cancellationToken), cancellationToken).GetAwaiter().GetResult();
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The deadlock fix is important, but it isn’t currently covered by a regression test. Since the repo already has unit tests that instantiate Listener (e.g., test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs), consider adding a test that runs Listener.Listen from a thread with a non-null SynchronizationContext and uses a test Transport.SendAsync that performs an await (e.g., Task.Yield) to validate that initialization completes (i.e., no sync-context deadlock) within a timeout.

Copilot generated this review using guidance from repository custom instructions.
@kotlarmilos
Copy link
Copy Markdown
Member Author

Fixed in #54023

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.

2 participants