Skip to content

Graceful reconnection alternative #12853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public sealed class ComponentEndpointConventionBuilder : IHubEndpointConventionBuilder
{
private readonly IEndpointConventionBuilder _endpointConventionBuilder;
private readonly IEndpointConventionBuilder _hubEndpoint;
private readonly IEndpointConventionBuilder _disconnectEndpoint;

internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder)
internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder hubEndpoint, IEndpointConventionBuilder disconnectEndpoint)
{
_endpointConventionBuilder = endpointConventionBuilder;
_hubEndpoint = hubEndpoint;
_disconnectEndpoint = disconnectEndpoint;
}

/// <summary>
Expand All @@ -23,7 +25,8 @@ internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder endpointC
/// <param name="convention">The convention to add to the builder.</param>
public void Add(Action<EndpointBuilder> convention)
{
_endpointConventionBuilder.Add(convention);
_hubEndpoint.Add(convention);
_disconnectEndpoint.Add(convention);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,17 @@ public static ComponentEndpointConventionBuilder MapBlazorHub(
throw new ArgumentNullException(nameof(configureOptions));
}

return new ComponentEndpointConventionBuilder(endpoints.MapHub<ComponentHub>(path, configureOptions)).AddComponent(componentType, selector);
var hubEndpoint = endpoints.MapHub<ComponentHub>(path, configureOptions);

var disconnectEndpoint = endpoints.Map(
(path.EndsWith("/") ? path : path + "/") + "disconnect/",
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build())
.WithDisplayName("Blazor disconnect");

return new ComponentEndpointConventionBuilder(
hubEndpoint,
disconnectEndpoint)
.AddComponent(componentType, selector);
}
}
}
103 changes: 103 additions & 0 deletions src/Components/Server/src/CircuitDisconnectMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Components.Server
{
// We use a middlware so that we can use DI.
internal class CircuitDisconnectMiddleware
{
private const string CircuitIdKey = "circuitId";

public CircuitDisconnectMiddleware(
ILogger<CircuitDisconnectMiddleware> logger,
CircuitRegistry registry,
CircuitIdFactory circuitIdFactory,
RequestDelegate next)
{
Logger = logger;
Registry = registry;
CircuitIdFactory = circuitIdFactory;
Next = next;
}

public ILogger<CircuitDisconnectMiddleware> Logger { get; }
public CircuitRegistry Registry { get; }
public CircuitIdFactory CircuitIdFactory { get; }
public RequestDelegate Next { get; }

public async Task Invoke(HttpContext context)
{
if (!HttpMethods.IsPost(context.Request.Method))
{
context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
return;
}

var (hasCircuitId, circuitId) = await TryGetCircuitIdAsync(context);
if (!hasCircuitId)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}

await TerminateCircuitGracefully(circuitId);

context.Response.StatusCode = StatusCodes.Status200OK;
}

private async Task<(bool, string)> TryGetCircuitIdAsync(HttpContext context)
{
try
{
if (!context.Request.HasFormContentType)
{
return (false, null);
}

var form = await context.Request.ReadFormAsync();
if (!form.TryGetValue(CircuitIdKey, out var circuitId) || !CircuitIdFactory.ValidateCircuitId(circuitId))
{
return (false, null);
}

return (true, circuitId);
}
catch
{
return (false, null);
}
}

private async Task TerminateCircuitGracefully(string circuitId)
{
try
{
await Registry.Terminate(circuitId);
Log.CircuitTerminatedGracefully(Logger, circuitId);
}
catch (Exception e)
{
Log.UnhandledExceptionInCircuit(Logger, circuitId, e);
}
}

private class Log
{
private static readonly Action<ILogger, string, Exception> _circuitTerminatedGracefully =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, "CircuitTerminatedGracefully"), "Circuit '{CircuitId}' terminated gracefully");

private static readonly Action<ILogger, string, Exception> _unhandledExceptionInCircuit =
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(2, "UnhandledExceptionInCircuit"), "Unhandled exception in circuit {CircuitId} while terminating gracefully.");

public static void CircuitTerminatedGracefully(ILogger logger, string circuitId) => _circuitTerminatedGracefully(logger, circuitId, null);

public static void UnhandledExceptionInCircuit(ILogger logger, string circuitId, Exception exception) => _unhandledExceptionInCircuit(logger, circuitId, exception);
}
}
}
32 changes: 23 additions & 9 deletions src/Components/Server/src/Circuits/CircuitRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,6 @@ public void Register(CircuitHost circuitHost)
}
}

public void PermanentDisconnect(CircuitHost circuitHost)
{
if (ConnectedCircuits.TryRemove(circuitHost.CircuitId, out _))
{
Log.CircuitDisconnectedPermanently(_logger, circuitHost.CircuitId);
circuitHost.Client.SetDisconnected();
}
}

public virtual Task DisconnectAsync(CircuitHost circuitHost, string connectionId)
{
Log.CircuitDisconnectStarted(_logger, circuitHost.CircuitId, connectionId);
Expand Down Expand Up @@ -297,6 +288,29 @@ private void DisposeTokenSource(DisconnectedCircuitEntry entry)
}
}

public ValueTask Terminate(string circuitId)
{
CircuitHost circuitHost;
DisconnectedCircuitEntry entry = default;
lock (CircuitRegistryLock)
{
if (ConnectedCircuits.TryGetValue(circuitId, out circuitHost) || DisconnectedCircuits.TryGetValue(circuitId, out entry))
{
circuitHost ??= entry.CircuitHost;
DisconnectedCircuits.Remove(circuitHost.CircuitId);
ConnectedCircuits.TryRemove(circuitHost.CircuitId, out _);
Log.CircuitDisconnectedPermanently(_logger, circuitHost.CircuitId);
circuitHost.Client.SetDisconnected();
}
else
{
return default;
}
}

return circuitHost?.DisposeAsync() ?? default;
}

private readonly struct DisconnectedCircuitEntry
{
public DisconnectedCircuitEntry(CircuitHost circuitHost, CancellationTokenSource tokenSource)
Expand Down
29 changes: 1 addition & 28 deletions src/Components/Server/src/ComponentHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,34 +68,7 @@ public override Task OnDisconnectedAsync(Exception exception)
return Task.CompletedTask;
}

if (exception != null)
{
return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId);
}
else
{
// The client will gracefully disconnect when using websockets by correctly closing the TCP connection.
// This happens when the user closes a tab, navigates away from the page or reloads the page.
// In these situations we know the user is done with the circuit, so we can get rid of it at that point.
// This is important to be able to more efficiently manage resources, specially memory.
return TerminateCircuitGracefully(circuitHost);
}
}

private async Task TerminateCircuitGracefully(CircuitHost circuitHost)
{
try
{
Log.CircuitTerminatedGracefully(_logger, circuitHost.CircuitId);
_circuitRegistry.PermanentDisconnect(circuitHost);
await circuitHost.DisposeAsync();
}
catch (Exception e)
{
Log.UnhandledExceptionInCircuit(_logger, circuitHost.CircuitId, e);
}

await _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId);
return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId);
}

/// <summary>
Expand Down
Loading