Skip to content

Commit dd7a169

Browse files
Make StartCircuit not block the SignalR message loop. Fixes #8274
1 parent 7248ecb commit dd7a169

File tree

3 files changed

+60
-15
lines changed

3 files changed

+60
-15
lines changed

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,21 +104,32 @@ public Task<IEnumerable<string>> PrerenderComponentAsync(Type componentType, Par
104104
});
105105
}
106106

107-
public async Task InitializeAsync(CancellationToken cancellationToken)
107+
public void Initialize(CancellationToken cancellationToken)
108108
{
109-
await Renderer.InvokeAsync(async () =>
109+
// This Initialize method is fire-and-forget as far as the caller is concerned, because
110+
// if it was to await completion, it would be blocking the SignalR message loop. This could
111+
// lead to deadlock, e.g., if the init process itself waited for an incoming SignalR message
112+
// such as the result of a JSInterop call.
113+
Renderer.InvokeAsync(async () =>
110114
{
111-
SetCurrentCircuitHost(this);
112-
113-
for (var i = 0; i < Descriptors.Count; i++)
115+
try
114116
{
115-
var (componentType, domElementSelector) = Descriptors[i];
116-
await Renderer.AddComponentAsync(componentType, domElementSelector);
117-
}
117+
SetCurrentCircuitHost(this);
118118

119-
await OnCircuitOpenedAsync(cancellationToken);
119+
for (var i = 0; i < Descriptors.Count; i++)
120+
{
121+
var (componentType, domElementSelector) = Descriptors[i];
122+
await Renderer.AddComponentAsync(componentType, domElementSelector);
123+
}
120124

121-
await OnConnectionUpAsync(cancellationToken);
125+
await OnCircuitOpenedAsync(cancellationToken);
126+
127+
await OnConnectionUpAsync(cancellationToken);
128+
}
129+
catch (Exception ex)
130+
{
131+
Renderer_UnhandledException(this, ex);
132+
}
122133
});
123134

124135
_initialized = true;

src/Components/Server/src/ComponentHub.cs

Lines changed: 2 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,7 @@ 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+
circuitHost.Initialize(Context.ConnectionAborted);
8180

8281
_circuitRegistry.Register(circuitHost);
8382

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

Lines changed: 37 additions & 2 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;
@@ -39,7 +40,7 @@ public async Task DisposeAsync_DisposesResources()
3940
}
4041

4142
[Fact]
42-
public async Task InitializeAsync_InvokesHandlers()
43+
public void Initialize_InvokesHandlers()
4344
{
4445
// Arrange
4546
var cancellationToken = new CancellationToken();
@@ -74,13 +75,47 @@ public async Task InitializeAsync_InvokesHandlers()
7475
var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object });
7576

7677
// Act
77-
await circuitHost.InitializeAsync(cancellationToken);
78+
circuitHost.Initialize(cancellationToken);
7879

7980
// Assert
8081
handler1.VerifyAll();
8182
handler2.VerifyAll();
8283
}
8384

85+
[Fact]
86+
public void Initialize_ReportsAsyncExceptions()
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+
circuitHost.Initialize(new CancellationToken());
107+
handler.VerifyAll();
108+
109+
// Assert: there was no synchronous exception
110+
Assert.Empty(reportedErrors);
111+
112+
// Act/Assert: if the handler throws later, that gets reported
113+
var ex = new InvalidTimeZoneException();
114+
tcs.SetException(ex);
115+
Assert.Same(ex, reportedErrors.Single().ExceptionObject);
116+
Assert.False(reportedErrors.Single().IsTerminating);
117+
}
118+
84119
[Fact]
85120
public async Task DisposeAsync_InvokesCircuitHandler()
86121
{

0 commit comments

Comments
 (0)