Components: don't block the SignalR loop during init. Fixes #8274 (#8863)

This commit is contained in:
Steve Sanderson 2019-03-28 16:42:55 +00:00 committed by GitHub
parent 18b81bacce
commit 03357bf92b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 69 additions and 14 deletions

View File

@ -108,20 +108,31 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
await 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);
_initialized = true; // We're ready to accept incoming JSInterop calls from here on
await OnCircuitOpenedAsync(cancellationToken);
await OnConnectionUpAsync(cancellationToken);
// We add the root components *after* the circuit is flagged as open.
// That's because AddComponentAsync waits for quiescence, which can take
// arbitrarily long. In the meantime we might need to be receiving and
// processing incoming JSInterop calls or similar.
for (var i = 0; i < Descriptors.Count; i++)
{
var (componentType, domElementSelector) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, domElementSelector);
}
}
catch (Exception ex)
{
// We have to handle all our own errors here, because the upstream caller
// has to fire-and-forget this
Renderer_UnhandledException(this, ex);
}
await OnCircuitOpenedAsync(cancellationToken);
await OnConnectionUpAsync(cancellationToken);
});
_initialized = true;
}
public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)

View File

@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.Server
/// <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);
@ -76,8 +76,11 @@ namespace Microsoft.AspNetCore.Components.Server
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);
// Fire-and-forget the initialization process, because we can't block the
// SignalR message loop (we'd get a deadlock if any of the initialization
// logic relied on receiving a subsequent message from SignalR), and it will
// take care of its own errors anyway.
_ = circuitHost.InitializeAsync(Context.ConnectionAborted);
_circuitRegistry.Register(circuitHost);

View File

@ -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;
@ -81,6 +82,46 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
handler2.VerifyAll();
}
[Fact]
public async Task InitializeAsync_ReportsOwnAsyncExceptions()
{
// 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
var initializeAsyncTask = circuitHost.InitializeAsync(new CancellationToken());
// Assert: No synchronous exceptions
handler.VerifyAll();
Assert.Empty(reportedErrors);
// Act: Trigger async exception
var ex = new InvalidTimeZoneException();
tcs.SetException(ex);
// Assert: The top-level task still succeeds, because the intended usage
// pattern is fire-and-forget.
await initializeAsyncTask;
// Assert: The async exception was reported via the side-channel
Assert.Same(ex, reportedErrors.Single().ExceptionObject);
Assert.False(reportedErrors.Single().IsTerminating);
}
[Fact]
public async Task DisposeAsync_InvokesCircuitHandler()
{