Skip to content

Commit 03357bf

Browse files
Components: don't block the SignalR loop during init. Fixes #8274 (#8863)
1 parent 18b81ba commit 03357bf

File tree

3 files changed

+68
-13
lines changed

3 files changed

+68
-13
lines changed

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,31 @@ public async Task InitializeAsync(CancellationToken cancellationToken)
108108
{
109109
await Renderer.InvokeAsync(async () =>
110110
{
111-
SetCurrentCircuitHost(this);
112-
113-
for (var i = 0; i < Descriptors.Count; i++)
111+
try
114112
{
115-
var (componentType, domElementSelector) = Descriptors[i];
116-
await Renderer.AddComponentAsync(componentType, domElementSelector);
117-
}
113+
SetCurrentCircuitHost(this);
114+
_initialized = true; // We're ready to accept incoming JSInterop calls from here on
118115

119-
await OnCircuitOpenedAsync(cancellationToken);
116+
await OnCircuitOpenedAsync(cancellationToken);
117+
await OnConnectionUpAsync(cancellationToken);
120118

121-
await OnConnectionUpAsync(cancellationToken);
119+
// We add the root components *after* the circuit is flagged as open.
120+
// That's because AddComponentAsync waits for quiescence, which can take
121+
// arbitrarily long. In the meantime we might need to be receiving and
122+
// processing incoming JSInterop calls or similar.
123+
for (var i = 0; i < Descriptors.Count; i++)
124+
{
125+
var (componentType, domElementSelector) = Descriptors[i];
126+
await Renderer.AddComponentAsync(componentType, domElementSelector);
127+
}
128+
}
129+
catch (Exception ex)
130+
{
131+
// We have to handle all our own errors here, because the upstream caller
132+
// has to fire-and-forget this
133+
Renderer_UnhandledException(this, ex);
134+
}
122135
});
123-
124-
_initialized = true;
125136
}
126137

127138
public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)

src/Components/Server/src/ComponentHub.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public override Task OnDisconnectedAsync(Exception exception)
6464
/// <summary>
6565
/// Intended for framework use only. Applications should not call this method directly.
6666
/// </summary>
67-
public async Task<string> StartCircuit(string uriAbsolute, string baseUriAbsolute)
67+
public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
6868
{
6969
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
7070

@@ -76,8 +76,11 @@ public async Task<string> StartCircuit(string uriAbsolute, string baseUriAbsolut
7676

7777
circuitHost.UnhandledException += CircuitHost_UnhandledException;
7878

79-
// If initialization fails, this will throw. The caller will fail if they try to call into any interop API.
80-
await circuitHost.InitializeAsync(Context.ConnectionAborted);
79+
// Fire-and-forget the initialization process, because we can't block the
80+
// SignalR message loop (we'd get a deadlock if any of the initialization
81+
// logic relied on receiving a subsequent message from SignalR), and it will
82+
// take care of its own errors anyway.
83+
_ = circuitHost.InitializeAsync(Context.ConnectionAborted);
8184

8285
_circuitRegistry.Register(circuitHost);
8386

src/Components/Server/test/Circuits/CircuitHostTest.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Text.Encodings.Web;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -81,6 +82,46 @@ public async Task InitializeAsync_InvokesHandlers()
8182
handler2.VerifyAll();
8283
}
8384

85+
[Fact]
86+
public async Task InitializeAsync_ReportsOwnAsyncExceptions()
87+
{
88+
// Arrange
89+
var handler = new Mock<CircuitHandler>(MockBehavior.Strict);
90+
var tcs = new TaskCompletionSource<object>();
91+
var reportedErrors = new List<UnhandledExceptionEventArgs>();
92+
93+
handler
94+
.Setup(h => h.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()))
95+
.Returns(tcs.Task)
96+
.Verifiable();
97+
98+
var circuitHost = TestCircuitHost.Create(handlers: new[] { handler.Object });
99+
circuitHost.UnhandledException += (sender, errorInfo) =>
100+
{
101+
Assert.Same(circuitHost, sender);
102+
reportedErrors.Add(errorInfo);
103+
};
104+
105+
// Act
106+
var initializeAsyncTask = circuitHost.InitializeAsync(new CancellationToken());
107+
108+
// Assert: No synchronous exceptions
109+
handler.VerifyAll();
110+
Assert.Empty(reportedErrors);
111+
112+
// Act: Trigger async exception
113+
var ex = new InvalidTimeZoneException();
114+
tcs.SetException(ex);
115+
116+
// Assert: The top-level task still succeeds, because the intended usage
117+
// pattern is fire-and-forget.
118+
await initializeAsyncTask;
119+
120+
// Assert: The async exception was reported via the side-channel
121+
Assert.Same(ex, reportedErrors.Single().ExceptionObject);
122+
Assert.False(reportedErrors.Single().IsTerminating);
123+
}
124+
84125
[Fact]
85126
public async Task DisposeAsync_InvokesCircuitHandler()
86127
{

0 commit comments

Comments
 (0)