diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 1ea6eba1c7..a53d61b247 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -194,6 +194,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private async Task OnCircuitOpenedAsync(CancellationToken cancellationToken) { + Log.CircuitOpened(_logger, Circuit.Id); + for (var i = 0; i < _circuitHandlers.Length; i++) { var circuitHandler = _circuitHandlers[i]; @@ -210,6 +212,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public async Task OnConnectionUpAsync(CancellationToken cancellationToken) { + Log.ConnectionUp(_logger, Circuit.Id, Client.ConnectionId); + try { await HandlerLock.WaitAsync(cancellationToken); @@ -235,6 +239,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public async Task OnConnectionDownAsync(CancellationToken cancellationToken) { + Log.ConnectionDown(_logger, Circuit.Id, Client.ConnectionId); + try { await HandlerLock.WaitAsync(cancellationToken); @@ -265,6 +271,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private async Task OnCircuitDownAsync() { + Log.CircuitClosed(_logger, Circuit.Id); + for (var i = 0; i < _circuitHandlers.Length; i++) { var circuitHandler = _circuitHandlers[i]; @@ -283,13 +291,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { Log.DisposingCircuit(_logger, CircuitId); - await Renderer.InvokeAsync((Func)(async () => + await Renderer.InvokeAsync(async () => { await OnConnectionDownAsync(CancellationToken.None); await OnCircuitDownAsync(); Renderer.Dispose(); _scope.Dispose(); - })); + }); } private void AssertInitialized() @@ -314,11 +322,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { private static readonly Action _unhandledExceptionInvokingCircuitHandler; private static readonly Action _disposingCircuit; + private static readonly Action _onCircuitOpened; + private static readonly Action _onConnectionUp; + private static readonly Action _onConnectionDown; + private static readonly Action _onCircuitClosed; private static class EventIds { public static readonly EventId ExceptionInvokingCircuitHandlerMethod = new EventId(100, "ExceptionInvokingCircuitHandlerMethod"); public static readonly EventId DisposingCircuit = new EventId(101, "DisposingCircuitHost"); + public static readonly EventId OnCircuitOpened = new EventId(102, "OnCircuitOpened"); + public static readonly EventId OnConnectionUp = new EventId(103, "OnConnectionUp"); + public static readonly EventId OnConnectionDown = new EventId(104, "OnConnectionDown"); + public static readonly EventId OnCircuitClosed = new EventId(105, "OnCircuitClosed"); } static Log() @@ -332,6 +348,26 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits LogLevel.Trace, EventIds.DisposingCircuit, "Disposing circuit with identifier {CircuitId}"); + + _onCircuitOpened = LoggerMessage.Define( + LogLevel.Debug, + EventIds.OnCircuitOpened, + "Opening circuit with id {CircuitId}."); + + _onConnectionUp = LoggerMessage.Define( + LogLevel.Debug, + EventIds.OnConnectionUp, + "Circuit id {CircuitId} connected using connection {ConnectionId}."); + + _onConnectionDown = LoggerMessage.Define( + LogLevel.Debug, + EventIds.OnConnectionDown, + "Circuit id {CircuitId} disconnected from connection {ConnectionId}."); + + _onCircuitClosed = LoggerMessage.Define( + LogLevel.Debug, + EventIds.OnCircuitClosed, + "Closing circuit with id {CircuitId}."); } public static void UnhandledExceptionInvokingCircuitHandler(ILogger logger, CircuitHandler handler, string handlerMethod, Exception exception) @@ -345,6 +381,17 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } public static void DisposingCircuit(ILogger logger, string circuitId) => _disposingCircuit(logger, circuitId, null); + + public static void CircuitOpened(ILogger logger, string circuitId) => _onCircuitOpened(logger, circuitId, null); + + public static void ConnectionUp(ILogger logger, string circuitId, string connectionId) => + _onConnectionUp(logger, circuitId, connectionId, null); + + public static void ConnectionDown(ILogger logger, string circuitId, string connectionId) => + _onConnectionDown(logger, circuitId, connectionId, null); + + public static void CircuitClosed(ILogger logger, string circuitId) => + _onCircuitClosed(logger, circuitId, null); } } } diff --git a/src/Components/Server/src/Circuits/CircuitRegistry.cs b/src/Components/Server/src/Circuits/CircuitRegistry.cs index d101af8a38..d061721255 100644 --- a/src/Components/Server/src/Circuits/CircuitRegistry.cs +++ b/src/Components/Server/src/Circuits/CircuitRegistry.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Components.Server.Circuits { @@ -82,6 +83,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public virtual Task DisconnectAsync(CircuitHost circuitHost, string connectionId) { + Log.CircuitDisconnectStarted(_logger, circuitHost.CircuitId, connectionId); + Task circuitHandlerTask; lock (CircuitRegistryLock) { @@ -104,44 +107,60 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits protected virtual bool DisconnectCore(CircuitHost circuitHost, string connectionId) { - if (!ConnectedCircuits.TryGetValue(circuitHost.CircuitId, out circuitHost)) + var circuitId = circuitHost.CircuitId; + if (!ConnectedCircuits.TryGetValue(circuitId, out circuitHost)) { + Log.CircuitNotActive(_logger, circuitId); + // Guard: The circuit might already have been marked as inactive. return false; } if (!string.Equals(circuitHost.Client.ConnectionId, connectionId, StringComparison.Ordinal)) { + Log.CircuitConnectedToDifferentConnection(_logger, circuitId, circuitHost.Client.ConnectionId); + // The circuit is associated with a different connection. One way this could happen is when // the client reconnects with a new connection before the OnDisconnect for the older // connection is executed. Do nothing return false; } - var result = ConnectedCircuits.TryRemove(circuitHost.CircuitId, out circuitHost); + var result = ConnectedCircuits.TryRemove(circuitId, out circuitHost); Debug.Assert(result, "This operation operates inside of a lock. We expect the previously inspected value to be still here."); circuitHost.Client.SetDisconnected(); RegisterDisconnectedCircuit(circuitHost); + + Log.CircuitMarkedDisconnected(_logger, circuitId); + return true; } public void RegisterDisconnectedCircuit(CircuitHost circuitHost) { + var cancellationTokenSource = new CancellationTokenSource(_options.DisconnectedCircuitRetentionPeriod); var entryOptions = new MemoryCacheEntryOptions { - AbsoluteExpiration = DateTimeOffset.UtcNow.Add(_options.DisconnectedCircuitRetentionPeriod), Size = 1, PostEvictionCallbacks = { _postEvictionCallback }, + ExpirationTokens = + { + new CancellationChangeToken(cancellationTokenSource.Token), + }, }; - DisconnectedCircuits.Set(circuitHost.CircuitId, circuitHost, entryOptions); + var entry = new DisconnectedCircuitEntry(circuitHost, cancellationTokenSource); + DisconnectedCircuits.Set(circuitHost.CircuitId, entry, entryOptions); } public virtual async Task ConnectAsync(string circuitId, IClientProxy clientProxy, string connectionId, CancellationToken cancellationToken) { + Log.CircuitConnectStarted(_logger, circuitId); + if (!_circuitIdFactory.ValidateCircuitId(circuitId)) { + Log.InvalidCircuitId(_logger, circuitId); return null; } @@ -159,6 +178,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits if (circuitHost == null) { + Log.FailedToReconnectToCircuit(_logger, circuitId); // Failed to connect. Nothing to do here. return null; } @@ -181,6 +201,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits await circuitHost.OnConnectionUpAsync(cancellationToken); }); + Log.ReconnectionSucceeded(_logger, circuitId); } await circuitHandlerTask; @@ -190,36 +211,45 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits protected virtual (CircuitHost circuitHost, bool previouslyConnected) ConnectCore(string circuitId, IClientProxy clientProxy, string connectionId) { - if (ConnectedCircuits.TryGetValue(circuitId, out var circuitHost)) + if (ConnectedCircuits.TryGetValue(circuitId, out var connectedCircuitHost)) { + Log.ConnectingToActiveCircuit(_logger, circuitId, connectionId); + // The host is still active i.e. the server hasn't detected the client disconnect. // However the client reconnected establishing a new connection. - circuitHost.Client.Transfer(clientProxy, connectionId); - return (circuitHost, true); + connectedCircuitHost.Client.Transfer(clientProxy, connectionId); + return (connectedCircuitHost, true); } - if (DisconnectedCircuits.TryGetValue(circuitId, out circuitHost)) + if (DisconnectedCircuits.TryGetValue(circuitId, out DisconnectedCircuitEntry disconnectedEntry)) { + Log.ConnectingToDisconnectedCircuit(_logger, circuitId, connectionId); + // The host was in disconnected state. Transfer it to ConnectedCircuits so that it's no longer considered disconnected. + // First discard the CancellationTokenSource so that the cache entry does not expire. + DisposeTokenSource(disconnectedEntry); + DisconnectedCircuits.Remove(circuitId); - ConnectedCircuits.TryAdd(circuitId, circuitHost); + ConnectedCircuits.TryAdd(circuitId, disconnectedEntry.CircuitHost); - circuitHost.Client.Transfer(clientProxy, connectionId); - - return (circuitHost, false); + disconnectedEntry.CircuitHost.Client.Transfer(clientProxy, connectionId); + return (disconnectedEntry.CircuitHost, false); } return default; } - private void OnEntryEvicted(object key, object value, EvictionReason reason, object state) + protected virtual void OnEntryEvicted(object key, object value, EvictionReason reason, object state) { switch (reason) { case EvictionReason.Expired: + case EvictionReason.TokenExpired: case EvictionReason.Capacity: // Kick off the dispose in the background. - _ = DisposeCircuitHost((CircuitHost)value); + var disconnectedEntry = (DisconnectedCircuitEntry)value; + Log.CircuitEvicted(_logger, disconnectedEntry.CircuitHost.CircuitId, reason); + _ = DisposeCircuitEntry(disconnectedEntry); break; case EvictionReason.Removed: @@ -232,11 +262,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } - private async Task DisposeCircuitHost(CircuitHost circuitHost) + private async Task DisposeCircuitEntry(DisconnectedCircuitEntry entry) { + DisposeTokenSource(entry); + try { - await circuitHost.DisposeAsync(); + await entry.CircuitHost.DisposeAsync(); } catch (Exception ex) { @@ -244,30 +276,168 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } + private void DisposeTokenSource(DisconnectedCircuitEntry entry) + { + try + { + entry.TokenSource.Dispose(); + } + catch (Exception ex) + { + Log.ExceptionDisposingTokenSource(_logger, ex); + } + } + + private readonly struct DisconnectedCircuitEntry + { + public DisconnectedCircuitEntry(CircuitHost circuitHost, CancellationTokenSource tokenSource) + { + CircuitHost = circuitHost; + TokenSource = tokenSource; + } + + public CircuitHost CircuitHost { get; } + public CancellationTokenSource TokenSource { get; } + } + private static class Log { - private static readonly Action _unhandledExceptionDisposingCircuitHost; + private static readonly Action _exceptionDisposingCircuitHost; + private static readonly Action _unhandledExceptionDisposingTokenSource; + private static readonly Action _circuitReconnectStarted; + private static readonly Action _invalidCircuitId; + private static readonly Action _connectingToActiveCircuit; + private static readonly Action _connectingToDisconnectedCircuit; + private static readonly Action _failedToReconnectToCircuit; + private static readonly Action _reconnectionSucceeded; + private static readonly Action _circuitDisconnectStarted; + private static readonly Action _circuitNotActive; + private static readonly Action _circuitConnectedToDifferentConnection; + private static readonly Action _circuitMarkedDisconnected; + private static readonly Action _circuitEvicted; private static class EventIds { public static readonly EventId ExceptionDisposingCircuit = new EventId(100, "ExceptionDisposingCircuit"); + public static readonly EventId ExceptionDisposingTokenSource = new EventId(101, "ExceptionDisposingTokenSource"); + public static readonly EventId AttemptingToReconnect = new EventId(102, "AttemptingToReconnect"); + public static readonly EventId InvalidCircuitId = new EventId(103, "InvalidCircuitId"); + public static readonly EventId ConnectingToActiveCircuit = new EventId(104, "ConnectingToActiveCircuit"); + public static readonly EventId ConnectingToDisconnectedCircuit = new EventId(105, "ConnectingToDisconnectedCircuit"); + public static readonly EventId FailedToReconnectToCircuit = new EventId(106, "FailedToReconnectToCircuit"); + public static readonly EventId CircuitDisconnectStarted = new EventId(107, "CircuitDisconnectStarted"); + public static readonly EventId CircuitNotActive = new EventId(108, "CircuitNotActive"); + public static readonly EventId CircuitConnectedToDifferentConnection = new EventId(109, "CircuitConnectedToDifferentConnection"); + public static readonly EventId CircuitMarkedDisconnected = new EventId(110, "CircuitMarkedDisconnected"); + public static readonly EventId CircuitEvicted = new EventId(111, "CircuitEvicted"); } static Log() { - _unhandledExceptionDisposingCircuitHost = LoggerMessage.Define( + _exceptionDisposingCircuitHost = LoggerMessage.Define( LogLevel.Error, EventIds.ExceptionDisposingCircuit, "Unhandled exception disposing circuit host: {Message}"); + + _unhandledExceptionDisposingTokenSource = LoggerMessage.Define( + LogLevel.Trace, + EventIds.ExceptionDisposingTokenSource, + "Exception thrown when disposing token source: {Message}"); + + _circuitReconnectStarted = LoggerMessage.Define( + LogLevel.Debug, + EventIds.AttemptingToReconnect, + "Attempting to reconnect to Circuit with id {CircuitId}."); + + _invalidCircuitId = LoggerMessage.Define( + LogLevel.Debug, + EventIds.InvalidCircuitId, + "Failed to validate circuit id {CircuitId}."); + + _connectingToActiveCircuit = LoggerMessage.Define( + LogLevel.Debug, + EventIds.ConnectingToActiveCircuit, + "Transferring active circuit {CircuitId} to connection {ConnectionId}."); + + _connectingToDisconnectedCircuit = LoggerMessage.Define( + LogLevel.Debug, + EventIds.ConnectingToDisconnectedCircuit, + "Transfering disconnected circuit {CircuitId} to connection {ConnectionId}."); + + _failedToReconnectToCircuit = LoggerMessage.Define( + LogLevel.Debug, + EventIds.FailedToReconnectToCircuit, + "Failed to reconnect to a circuit with id {CircuitId}."); + + _reconnectionSucceeded = LoggerMessage.Define( + LogLevel.Debug, + EventIds.FailedToReconnectToCircuit, + "Reconnect to circuit with id {CircuitId} succeeded."); + + _circuitDisconnectStarted = LoggerMessage.Define( + LogLevel.Debug, + EventIds.CircuitDisconnectStarted, + "Attempting to disconnect circuit with id {CircuitId} from connection {ConnectionId}."); + + _circuitNotActive = LoggerMessage.Define( + LogLevel.Debug, + EventIds.CircuitNotActive, + "Failed to disconnect circuit with id {CircuitId}. The circuit is not active."); + + _circuitConnectedToDifferentConnection = LoggerMessage.Define( + LogLevel.Debug, + EventIds.CircuitConnectedToDifferentConnection, + "Failed to disconnect circuit with id {CircuitId}. The circuit is connected to {ConnectionId}."); + + _circuitMarkedDisconnected = LoggerMessage.Define( + LogLevel.Debug, + EventIds.CircuitMarkedDisconnected, + "Circuit with id {CircuitId} is disconnected."); + + _circuitEvicted = LoggerMessage.Define( + LogLevel.Debug, + EventIds.CircuitEvicted, + "Circuit with id {CircuitId} evicted due to {EvictionReason}."); } - public static void UnhandledExceptionDisposingCircuitHost(ILogger logger, Exception exception) - { - _unhandledExceptionDisposingCircuitHost( - logger, - exception.Message, - exception); - } + public static void UnhandledExceptionDisposingCircuitHost(ILogger logger, Exception exception) => + _exceptionDisposingCircuitHost(logger, exception.Message, exception); + + public static void ExceptionDisposingTokenSource(ILogger logger, Exception exception) => + _unhandledExceptionDisposingTokenSource(logger, exception.Message, exception); + + public static void CircuitConnectStarted(ILogger logger, string circuitId) => + _circuitReconnectStarted(logger, circuitId, null); + + public static void InvalidCircuitId(ILogger logger, string circuitId) => + _invalidCircuitId(logger, circuitId, null); + + public static void ConnectingToActiveCircuit(ILogger logger, string circuitId, string connectionId) => + _connectingToActiveCircuit(logger, circuitId, connectionId, null); + + public static void ConnectingToDisconnectedCircuit(ILogger logger, string circuitId, string connectionId) => + _connectingToDisconnectedCircuit(logger, circuitId, connectionId, null); + + public static void FailedToReconnectToCircuit(ILogger logger, string circuitId) => + _failedToReconnectToCircuit(logger, circuitId, null); + + public static void ReconnectionSucceeded(ILogger logger, string circuitId) => + _reconnectionSucceeded(logger, circuitId, null); + + public static void CircuitDisconnectStarted(ILogger logger, string circuitId, string connectionId) => + _circuitDisconnectStarted(logger, circuitId, connectionId, null); + + public static void CircuitNotActive(ILogger logger, string circuitId) => + _circuitNotActive(logger, circuitId, null); + + public static void CircuitConnectedToDifferentConnection(ILogger logger, string circuitId, string connectionId) => + _circuitConnectedToDifferentConnection(logger, circuitId, connectionId, null); + + public static void CircuitMarkedDisconnected(ILogger logger, string circuitId) => + _circuitMarkedDisconnected(logger, circuitId, null); + + public static void CircuitEvicted(ILogger logger, string circuitId, EvictionReason evictionReason) => + _circuitEvicted(logger, circuitId, evictionReason, null); } } } diff --git a/src/Components/Server/test/Circuits/CircuitRegistryTest.cs b/src/Components/Server/test/Circuits/CircuitRegistryTest.cs index 54e85ba323..ab9a237935 100644 --- a/src/Components/Server/test/Circuits/CircuitRegistryTest.cs +++ b/src/Components/Server/test/Circuits/CircuitRegistryTest.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -64,7 +66,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits var registry = CreateRegistry(circuitIdFactory); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); - registry.DisconnectedCircuits.Set(circuitHost.CircuitId, circuitHost, new MemoryCacheEntryOptions { Size = 1 }); + registry.RegisterDisconnectedCircuit(circuitHost); var newClient = Mock.Of(); var newConnectionId = "new-id"; @@ -90,7 +92,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits var registry = CreateRegistry(circuitIdFactory); var handler = new Mock { CallBase = true }; var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), handlers: new[] { handler.Object }); - registry.DisconnectedCircuits.Set(circuitHost.CircuitId, circuitHost, new MemoryCacheEntryOptions { Size = 1 }); + registry.RegisterDisconnectedCircuit(circuitHost); var newClient = Mock.Of(); var newConnectionId = "new-id"; @@ -313,16 +315,108 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _)); } + [Fact] + public void CircuitRegistryUsesConfiguredMaxRetainedDisconnectedCircuitsValue() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var maxCircuits = 3; + var circuitOptions = new CircuitOptions + { + MaxRetainedDisconnectedCircuits = maxCircuits, + }; + var registry = new TestCircuitRegistry(circuitIdFactory, circuitOptions); + var hosts = Enumerable.Range(0, maxCircuits + 2) + .Select(_ => TestCircuitHost.Create()) + .ToArray(); + + // Act + for (var i = 0; i < hosts.Length; i++) + { + registry.RegisterDisconnectedCircuit(hosts[i]); + } + + // Assert + for (var i = 0; i < maxCircuits; i++) + { + Assert.True(registry.DisconnectedCircuits.TryGetValue(hosts[i].CircuitId, out var _)); + } + + // Additional circuits do not get registered. + Assert.False(registry.DisconnectedCircuits.TryGetValue(hosts[maxCircuits].CircuitId, out var _)); + Assert.False(registry.DisconnectedCircuits.TryGetValue(hosts[maxCircuits + 1].CircuitId, out var _)); + } + + [Fact] + public async Task DisconnectedCircuitIsRemovedAfterConfiguredTimeout() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var circuitOptions = new CircuitOptions + { + DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(3), + }; + var registry = new TestCircuitRegistry(circuitIdFactory, circuitOptions); + var tcs = new TaskCompletionSource(); + + registry.OnAfterEntryEvicted = () => + { + tcs.TrySetResult(new object()); + }; + var circuitHost = TestCircuitHost.Create(); + + registry.RegisterDisconnectedCircuit(circuitHost); + + // Act + // Verify it's present in the dictionary. + Assert.True(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out var _)); + await Task.Run(() => tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10))); + Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out var _)); + } + + [Fact] + public async Task ReconnectBeforeTimeoutDoesNotGetEntryToBeEvicted() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var circuitOptions = new CircuitOptions + { + DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(8), + }; + var registry = new TestCircuitRegistry(circuitIdFactory, circuitOptions); + var tcs = new TaskCompletionSource(); + + registry.OnAfterEntryEvicted = () => + { + tcs.TrySetResult(new object()); + }; + var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); + + registry.RegisterDisconnectedCircuit(circuitHost); + await registry.ConnectAsync(circuitHost.CircuitId, Mock.Of(), "new-connection", default); + + // Act + await Task.Run(() => tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10))); + + // Verify it's still connected + Assert.True(registry.ConnectedCircuits.TryGetValue(circuitHost.CircuitId, out var cacheValue)); + Assert.Same(circuitHost, cacheValue); + // Nothing should be disconnected. + Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out var _)); + } + private class TestCircuitRegistry : CircuitRegistry { - public TestCircuitRegistry(CircuitIdFactory factory) - : base(Options.Create(new CircuitOptions()), NullLogger.Instance, factory) + public TestCircuitRegistry(CircuitIdFactory factory, CircuitOptions circuitOptions = null) + : base(Options.Create(circuitOptions ?? new CircuitOptions()), NullLogger.Instance, factory) { } public ManualResetEventSlim BeforeConnect { get; set; } public ManualResetEventSlim BeforeDisconnect { get; set; } + public Action OnAfterEntryEvicted { get; set; } + protected override (CircuitHost, bool) ConnectCore(string circuitId, IClientProxy clientProxy, string connectionId) { if (BeforeConnect != null) @@ -342,6 +436,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits return base.DisconnectCore(circuitHost, connectionId); } + + protected override void OnEntryEvicted(object key, object value, EvictionReason reason, object state) + { + base.OnEntryEvicted(key, value, reason, state); + OnAfterEntryEvicted(); + } } private static CircuitRegistry CreateRegistry(CircuitIdFactory factory = null)