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 () => await Renderer.InvokeAsync(async () =>
{ {
SetCurrentCircuitHost(this); try
for (var i = 0; i < Descriptors.Count; i++)
{ {
var (componentType, domElementSelector) = Descriptors[i]; SetCurrentCircuitHost(this);
await Renderer.AddComponentAsync(componentType, domElementSelector); _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) 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> /// <summary>
/// Intended for framework use only. Applications should not call this method directly. /// Intended for framework use only. Applications should not call this method directly.
/// </summary> /// </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); var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
@ -76,8 +76,11 @@ namespace Microsoft.AspNetCore.Components.Server
circuitHost.UnhandledException += CircuitHost_UnhandledException; circuitHost.UnhandledException += CircuitHost_UnhandledException;
// If initialization fails, this will throw. The caller will fail if they try to call into any interop API. // Fire-and-forget the initialization process, because we can't block the
await circuitHost.InitializeAsync(Context.ConnectionAborted); // 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); _circuitRegistry.Register(circuitHost);

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -81,6 +82,46 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
handler2.VerifyAll(); 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] [Fact]
public async Task DisposeAsync_InvokesCircuitHandler() public async Task DisposeAsync_InvokesCircuitHandler()
{ {