Skip to content

Commit 012ba12

Browse files
committed
Tests -these with pausing will fail.
1 parent 02125bd commit 012ba12

File tree

2 files changed

+134
-1
lines changed

2 files changed

+134
-1
lines changed

src/Components/Server/src/Circuits/CircuitRegistry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ private Task PauseAndDisposeCircuitEntry(DisconnectedCircuitEntry entry)
307307
return Task.CompletedTask;
308308
}
309309

310-
private async Task PauseAndDisposeCircuitHost(CircuitHost circuitHost, bool saveStateToClient)
310+
internal async Task PauseAndDisposeCircuitHost(CircuitHost circuitHost, bool saveStateToClient)
311311
{
312312
await _circuitPersistenceManager.PauseCircuitAsync(circuitHost, saveStateToClient);
313313
circuitHost.UnhandledException -= CircuitHost_UnhandledException;

src/Components/Server/test/Circuits/CircuitRegistryTest.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,17 @@ protected override void OnEntryEvicted(object key, object value, EvictionReason
620620
base.OnEntryEvicted(key, value, reason, state);
621621
OnAfterEntryEvicted?.Invoke();
622622
}
623+
624+
public void TriggerEviction(object key, object value, EvictionReason reason)
625+
{
626+
OnEntryEvicted(key, value, reason, null);
627+
}
628+
629+
public async Task SimulateEvictionAndDispose(CircuitHost circuitHost)
630+
{
631+
// Directly call PauseAndDisposeCircuitHost which is what eviction does
632+
await PauseAndDisposeCircuitHost(circuitHost, saveStateToClient: false);
633+
}
623634
}
624635

625636
private class TestCircuitPersistenceProvider : ICircuitPersistenceProvider
@@ -678,4 +689,126 @@ private static (CircuitRegistry Registry, TestCircuitPersistenceProvider Provide
678689
persistenceManager);
679690
return (registry, provider);
680691
}
692+
693+
[Fact]
694+
public async Task PauseAfterTermination_DoesNotThrow()
695+
{
696+
var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory();
697+
var options = new CircuitOptions();
698+
699+
var circuitHost = new TestCircuitHostForRaceConditions(
700+
circuitIdFactory.CreateCircuitId(),
701+
CreateServiceScope(),
702+
options);
703+
704+
var persistenceProvider = new TestCircuitPersistenceProvider();
705+
var registry = new TestCircuitRegistry(circuitIdFactory, options, persistenceProvider);
706+
registry.Register(circuitHost);
707+
708+
// First terminate the circuit - it calls circuitHost.DisposeAsync()
709+
await registry.TerminateAsync(circuitHost.CircuitId);
710+
711+
// Then try to pause - it will try to resolve services from the DI scope that is already disposed
712+
await registry.PauseAndDisposeCircuitHost(circuitHost, saveStateToClient: true);
713+
}
714+
715+
[Fact]
716+
public async Task PauseAfterEviction_DoesNotThrow()
717+
{
718+
var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory();
719+
var options = new CircuitOptions();
720+
721+
var circuitHost = new TestCircuitHostForRaceConditions(
722+
circuitIdFactory.CreateCircuitId(),
723+
CreateServiceScope(),
724+
options);
725+
726+
var persistenceProvider = new TestCircuitPersistenceProvider();
727+
var registry = new TestCircuitRegistry(circuitIdFactory, options, persistenceProvider);
728+
registry.Register(circuitHost);
729+
730+
// First simulate eviction by calling the same method that eviction calls
731+
await registry.SimulateEvictionAndDispose(circuitHost);
732+
733+
// Then try to pause - it will try to resolve services from the DI scope that is already disposed
734+
await registry.PauseAndDisposeCircuitHost(circuitHost, saveStateToClient: true);
735+
}
736+
737+
[Fact]
738+
public async Task EvictionAndTermination_DoesNotThrow()
739+
{
740+
var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory();
741+
var options = new CircuitOptions();
742+
743+
var circuitHost = new TestCircuitHostForRaceConditions(
744+
circuitIdFactory.CreateCircuitId(),
745+
CreateServiceScope(),
746+
options);
747+
748+
var persistenceProvider = new TestCircuitPersistenceProvider();
749+
var registry = new TestCircuitRegistry(circuitIdFactory, options, persistenceProvider);
750+
registry.Register(circuitHost);
751+
752+
// Simulate race condition: eviction and termination happening concurrently
753+
await registry.SimulateEvictionAndDispose(circuitHost);
754+
await registry.TerminateAsync(circuitHost.CircuitId);
755+
}
756+
757+
private static AsyncServiceScope CreateServiceScope()
758+
{
759+
var serviceCollection = new ServiceCollection();
760+
serviceCollection.AddSingleton(sp => new ComponentStatePersistenceManager(
761+
NullLoggerFactory.Instance.CreateLogger<ComponentStatePersistenceManager>(), sp));
762+
serviceCollection.AddSingleton(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
763+
var serviceProvider = serviceCollection.BuildServiceProvider();
764+
return serviceProvider.CreateAsyncScope();
765+
}
766+
767+
private class TestCircuitHostForRaceConditions : CircuitHost
768+
{
769+
public TestCircuitHostForRaceConditions(
770+
CircuitId circuitId,
771+
AsyncServiceScope scope,
772+
CircuitOptions options)
773+
: base(
774+
circuitId,
775+
scope,
776+
options,
777+
new CircuitClientProxy(Mock.Of<ISingleClientProxy>(), Guid.NewGuid().ToString()),
778+
CreateRemoteRenderer(),
779+
Array.Empty<ComponentDescriptor>(),
780+
new RemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>()),
781+
new RemoteNavigationManager(Mock.Of<ILogger<RemoteNavigationManager>>()),
782+
Array.Empty<CircuitHandler>(),
783+
new CircuitMetrics(new TestMeterFactory()),
784+
new CircuitActivitySource(),
785+
NullLogger<CircuitHost>.Instance)
786+
{
787+
}
788+
789+
private static RemoteRenderer CreateRemoteRenderer()
790+
{
791+
var clientProxy = new CircuitClientProxy(Mock.Of<ISingleClientProxy>(), Guid.NewGuid().ToString());
792+
var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
793+
var componentsActivitySource = new ComponentsActivitySource();
794+
var serviceProvider = new Mock<IServiceProvider>();
795+
serviceProvider
796+
.Setup(services => services.GetService(typeof(IJSRuntime)))
797+
.Returns(jsRuntime);
798+
serviceProvider
799+
.Setup(services => services.GetService(typeof(ComponentsActivitySource)))
800+
.Returns(componentsActivitySource);
801+
var serverComponentDeserializer = Mock.Of<IServerComponentDeserializer>();
802+
803+
return new RemoteRenderer(
804+
serviceProvider.Object,
805+
NullLoggerFactory.Instance,
806+
new CircuitOptions(),
807+
clientProxy,
808+
serverComponentDeserializer,
809+
NullLogger.Instance,
810+
jsRuntime,
811+
new CircuitJSComponentInterop(new CircuitOptions()));
812+
}
813+
}
681814
}

0 commit comments

Comments
 (0)