Skip to content

Commit

Permalink
Make StartCircuit not block the SignalR message loop. Fixes #8274
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSandersonMS committed Mar 27, 2019
1 parent 7248ecb commit dd7a169
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 15 deletions.
31 changes: 21 additions & 10 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,32 @@ public Task<IEnumerable<string>> PrerenderComponentAsync(Type componentType, Par
});
}

public async Task InitializeAsync(CancellationToken cancellationToken)
public void Initialize(CancellationToken cancellationToken)
{
await Renderer.InvokeAsync(async () =>
// This Initialize method is fire-and-forget as far as the caller is concerned, because
// if it was to await completion, it would be blocking the SignalR message loop. This could
// lead to deadlock, e.g., if the init process itself waited for an incoming SignalR message
// such as the result of a JSInterop call.
Renderer.InvokeAsync(async () =>
{
SetCurrentCircuitHost(this);

for (var i = 0; i < Descriptors.Count; i++)
try
{
var (componentType, domElementSelector) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, domElementSelector);
}
SetCurrentCircuitHost(this);

await OnCircuitOpenedAsync(cancellationToken);
for (var i = 0; i < Descriptors.Count; i++)
{
var (componentType, domElementSelector) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, domElementSelector);
}

await OnConnectionUpAsync(cancellationToken);
await OnCircuitOpenedAsync(cancellationToken);

await OnConnectionUpAsync(cancellationToken);
}
catch (Exception ex)
{
Renderer_UnhandledException(this, ex);
}
});

_initialized = true;
Expand Down
5 changes: 2 additions & 3 deletions src/Components/Server/src/ComponentHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public override Task OnDisconnectedAsync(Exception exception)
/// <summary>
/// Intended for framework use only. Applications should not call this method directly.
/// </summary>
public async Task<string> StartCircuit(string uriAbsolute, string baseUriAbsolute)
public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
{
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);

Expand All @@ -76,8 +76,7 @@ public async Task<string> StartCircuit(string uriAbsolute, string baseUriAbsolut

circuitHost.UnhandledException += CircuitHost_UnhandledException;

// If initialization fails, this will throw. The caller will fail if they try to call into any interop API.
await circuitHost.InitializeAsync(Context.ConnectionAborted);
circuitHost.Initialize(Context.ConnectionAborted);

_circuitRegistry.Register(circuitHost);

Expand Down
39 changes: 37 additions & 2 deletions src/Components/Server/test/Circuits/CircuitHostTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -39,7 +40,7 @@ public async Task DisposeAsync_DisposesResources()
}

[Fact]
public async Task InitializeAsync_InvokesHandlers()
public void Initialize_InvokesHandlers()
{
// Arrange
var cancellationToken = new CancellationToken();
Expand Down Expand Up @@ -74,13 +75,47 @@ public async Task InitializeAsync_InvokesHandlers()
var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object });

// Act
await circuitHost.InitializeAsync(cancellationToken);
circuitHost.Initialize(cancellationToken);

// Assert
handler1.VerifyAll();
handler2.VerifyAll();
}

[Fact]
public void Initialize_ReportsAsyncExceptions()
{
// Arrange
var handler = new Mock<CircuitHandler>(MockBehavior.Strict);
var tcs = new TaskCompletionSource<object>();
var reportedErrors = new List<UnhandledExceptionEventArgs>();

handler
.Setup(h => h.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()))
.Returns(tcs.Task)
.Verifiable();

var circuitHost = TestCircuitHost.Create(handlers: new[] { handler.Object });
circuitHost.UnhandledException += (sender, errorInfo) =>
{
Assert.Same(circuitHost, sender);
reportedErrors.Add(errorInfo);
};

// Act
circuitHost.Initialize(new CancellationToken());
handler.VerifyAll();

// Assert: there was no synchronous exception
Assert.Empty(reportedErrors);

// Act/Assert: if the handler throws later, that gets reported
var ex = new InvalidTimeZoneException();
tcs.SetException(ex);
Assert.Same(ex, reportedErrors.Single().ExceptionObject);
Assert.False(reportedErrors.Single().IsTerminating);
}

[Fact]
public async Task DisposeAsync_InvokesCircuitHandler()
{
Expand Down

0 comments on commit dd7a169

Please sign in to comment.