Fix dotnet-watch Hot Reload deadlock on platforms with a startup SynchronizationContext#54131
Fix dotnet-watch Hot Reload deadlock on platforms with a startup SynchronizationContext#54131kotlarmilos wants to merge 1 commit intodotnet:mainfrom
Conversation
…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>
There was a problem hiding this comment.
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 withTask.Run(...).GetResult()to avoidSynchronizationContextcapture. - 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
| // 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(); |
There was a problem hiding this comment.
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.
|
Fixed in #54023 |
Description
This PR wraps the applier bootstrap in
Listener.ListeninTask.Run(...)so that the awaits insideInitializeAsyncresume on the threadpool instead of the startup-hook thread. The previous code did a synchronousInitializeAsync(ct).GetAwaiter().GetResult()directly on the startup thread. On platforms whose .NET startup-hook thread carries aSynchronizationContext(iOS Mono and CoreCLR, Android Mono and CoreCLR), theawaitinsideWebSocketTransport.ConnectAsynccaptures that SC for its continuation, and the synchronous wait blocks the only thread that could resume it.With this fix, end-to-end
dotnet watchHot Reload works across mobile on CoreCLR and Mono.