Skip to content

Commit 18a7438

Browse files
committed
code style + tests
1 parent a9d3d13 commit 18a7438

File tree

2 files changed

+238
-2
lines changed

2 files changed

+238
-2
lines changed

ManagedCode.Orleans.SignalR.Core/SignalR/OrleansHubLifetimeManager.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,8 +591,6 @@ private static bool IsLocalObjectReferenceException(Exception ex)
591591

592592
private void OnApplicationStopping()
593593
{
594-
var tasks = new List<Task>(_connections.Count);
595-
596594
foreach (var connection in _connections)
597595
{
598596
var subscription = connection.Features.Get<Subscription>();
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using ManagedCode.Orleans.SignalR.Core.Config;
4+
using ManagedCode.Orleans.SignalR.Core.SignalR;
5+
using ManagedCode.Orleans.SignalR.Tests.Cluster;
6+
using ManagedCode.Orleans.SignalR.Tests.Infrastructure.Logging;
7+
using ManagedCode.Orleans.SignalR.Tests.TestApp;
8+
using ManagedCode.Orleans.SignalR.Tests.TestApp.Hubs;
9+
using Microsoft.AspNetCore.SignalR;
10+
using Microsoft.AspNetCore.SignalR.Client;
11+
using Microsoft.AspNetCore.SignalR.Protocol;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Shouldly;
14+
using Xunit;
15+
using Xunit.Abstractions;
16+
17+
namespace ManagedCode.Orleans.SignalR.Tests;
18+
19+
[Collection(nameof(SmokeCluster))]
20+
public class OrleansHubLifetimeManagerShutdownTests : IAsyncLifetime
21+
{
22+
private readonly SmokeClusterFixture _siloCluster;
23+
private readonly TestOutputHelperAccessor _loggerAccessor = new();
24+
private readonly ITestOutputHelper _output;
25+
private TestWebApplication? _app;
26+
27+
public OrleansHubLifetimeManagerShutdownTests(SmokeClusterFixture siloCluster, ITestOutputHelper output)
28+
{
29+
_siloCluster = siloCluster;
30+
_output = output;
31+
_loggerAccessor.Output = output;
32+
}
33+
34+
public Task InitializeAsync()
35+
{
36+
_app = new TestWebApplication(_siloCluster, port: 8105, loggerAccessor: _loggerAccessor);
37+
return Task.CompletedTask;
38+
}
39+
40+
public Task DisposeAsync()
41+
{
42+
DisposeApp();
43+
return Task.CompletedTask;
44+
}
45+
46+
[Fact]
47+
public async Task ApplicationStoppingShouldRemoveAllConnectionsFromCoordinator()
48+
{
49+
var app = EnsureApp();
50+
var first = app.CreateSignalRClient(nameof(SimpleTestHub));
51+
var second = app.CreateSignalRClient(nameof(SimpleTestHub));
52+
var firstProbe = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
53+
var secondProbe = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
54+
using var firstHandler = first.On("ShutdownProbe", () => firstProbe.TrySetResult(true));
55+
using var secondHandler = second.On("ShutdownProbe", () => secondProbe.TrySetResult(true));
56+
57+
await first.StartAsync();
58+
await second.StartAsync();
59+
await first.InvokeAsync<int>("Plus", 2, 3);
60+
await second.InvokeAsync<int>("Plus", 3, 4);
61+
62+
var coordinator = NameHelperGenerator.GetConnectionCoordinatorGrain<SimpleTestHub>(_siloCluster.Cluster.Client);
63+
var probeMessage = new InvocationMessage("ShutdownProbe", Array.Empty<object?>());
64+
65+
var firstId = first.ConnectionId ?? throw new InvalidOperationException("First connection is missing its identifier.");
66+
var secondId = second.ConnectionId ?? throw new InvalidOperationException("Second connection is missing its identifier.");
67+
68+
(await coordinator.SendToConnection(probeMessage, firstId)).ShouldBeTrue("Coordinator should reach the first connection before shutdown.");
69+
(await coordinator.SendToConnection(probeMessage, secondId)).ShouldBeTrue("Coordinator should reach the second connection before shutdown.");
70+
71+
var firstDelivered = await Task.WhenAny(firstProbe.Task, Task.Delay(TimeSpan.FromSeconds(5)));
72+
firstDelivered.ShouldBe(firstProbe.Task, "First connection never observed shutdown probe.");
73+
var secondDelivered = await Task.WhenAny(secondProbe.Task, Task.Delay(TimeSpan.FromSeconds(5)));
74+
secondDelivered.ShouldBe(secondProbe.Task, "Second connection never observed shutdown probe.");
75+
76+
_output.WriteLine("Disposing test host to trigger ApplicationStopping.");
77+
DisposeApp();
78+
await Task.Delay(TimeSpan.FromSeconds(1));
79+
80+
var firstRemoved = await coordinator.SendToConnection(probeMessage, firstId);
81+
var secondRemoved = await coordinator.SendToConnection(probeMessage, secondId);
82+
firstRemoved.ShouldBeFalse("Coordinator still tracks the first connection after shutdown.");
83+
secondRemoved.ShouldBeFalse("Coordinator still tracks the second connection after shutdown.");
84+
85+
await first.DisposeAsync();
86+
await second.DisposeAsync();
87+
}
88+
89+
[Fact]
90+
public async Task ApplicationStoppingShouldFlushCoordinatorState()
91+
{
92+
var app = EnsureApp();
93+
var connection = app.CreateSignalRClient(nameof(SimpleTestHub));
94+
var probe = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
95+
using var handler = connection.On("ShutdownProbe", () => probe.TrySetResult("pong"));
96+
97+
await connection.StartAsync();
98+
await connection.InvokeAsync<int>("Plus", 1, 1);
99+
var connectionId = connection.ConnectionId ?? throw new InvalidOperationException("Connection id is missing after start.");
100+
_output.WriteLine("Connection established with id {0}.", connectionId);
101+
102+
var coordinator = NameHelperGenerator.GetConnectionCoordinatorGrain<SimpleTestHub>(_siloCluster.Cluster.Client);
103+
var message = new InvocationMessage("ShutdownProbe", Array.Empty<object?>());
104+
105+
var sendResult = await coordinator.SendToConnection(message, connectionId);
106+
sendResult.ShouldBeTrue("Coordinator could not reach active connection.");
107+
var delivered = await Task.WhenAny(probe.Task, Task.Delay(TimeSpan.FromSeconds(5)));
108+
delivered.ShouldBe(probe.Task, "Probe invocation never reached the client.");
109+
110+
DisposeApp();
111+
await Task.Delay(TimeSpan.FromSeconds(1));
112+
113+
var staleResult = await coordinator.SendToConnection(message, connectionId);
114+
staleResult.ShouldBeFalse("Coordinator still tracks stale connection after shutdown.");
115+
116+
await Task.Delay(TestDefaults.ClientTimeout + TimeSpan.FromSeconds(1));
117+
var repeatedResult = await coordinator.SendToConnection(message, connectionId);
118+
repeatedResult.ShouldBeFalse("Stale connection revived after expected timeout window.");
119+
120+
await connection.DisposeAsync();
121+
}
122+
123+
private TestWebApplication EnsureApp()
124+
{
125+
return _app ?? throw new InvalidOperationException("Test host is not initialised.");
126+
}
127+
128+
private void DisposeApp()
129+
{
130+
if (_app is null)
131+
{
132+
return;
133+
}
134+
135+
_app.Dispose();
136+
_app = null;
137+
}
138+
}
139+
140+
[Collection(nameof(KeepAliveDisabledCluster))]
141+
public class OrleansHubLifetimeManagerShutdownNoKeepAliveTests : IAsyncLifetime
142+
{
143+
private readonly KeepAliveDisabledClusterFixture _siloCluster;
144+
private readonly TestOutputHelperAccessor _loggerAccessor = new();
145+
private readonly ITestOutputHelper _output;
146+
private TestWebApplication? _app;
147+
148+
public OrleansHubLifetimeManagerShutdownNoKeepAliveTests(KeepAliveDisabledClusterFixture siloCluster, ITestOutputHelper output)
149+
{
150+
_siloCluster = siloCluster;
151+
_output = output;
152+
_loggerAccessor.Output = output;
153+
}
154+
155+
public Task InitializeAsync()
156+
{
157+
_app = new TestWebApplication(
158+
_siloCluster,
159+
port: 8106,
160+
loggerAccessor: _loggerAccessor,
161+
configureServices: services =>
162+
{
163+
services.PostConfigure<OrleansSignalROptions>(options =>
164+
{
165+
options.KeepEachConnectionAlive = false;
166+
options.ClientTimeoutInterval = TimeSpan.FromSeconds(2);
167+
});
168+
services.PostConfigure<HubOptions>(options =>
169+
{
170+
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
171+
options.KeepAliveInterval = null;
172+
});
173+
});
174+
return Task.CompletedTask;
175+
}
176+
177+
public Task DisposeAsync()
178+
{
179+
DisposeHost();
180+
return Task.CompletedTask;
181+
}
182+
183+
[Fact]
184+
public async Task ShutdownShouldRemoveConnectionsWithoutKeepAlive()
185+
{
186+
var app = EnsureApp();
187+
var connection = app.CreateSignalRClient(nameof(SimpleTestHub));
188+
var probe = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
189+
using var handler = connection.On("ShutdownProbe", () => probe.TrySetResult(true));
190+
191+
await connection.StartAsync();
192+
await connection.InvokeAsync<int>("Plus", 1, 2);
193+
var connectionId = connection.ConnectionId ?? throw new InvalidOperationException("Connection identifier missing.");
194+
195+
var clusterClient = _siloCluster.Cluster.Client;
196+
var coordinator = NameHelperGenerator.GetConnectionCoordinatorGrain<SimpleTestHub>(clusterClient);
197+
var partitionId = await coordinator.GetPartitionForConnection(connectionId);
198+
var partitionGrain = NameHelperGenerator.GetConnectionPartitionGrain<SimpleTestHub>(clusterClient, partitionId);
199+
var message = new InvocationMessage("ShutdownProbe", Array.Empty<object?>());
200+
201+
(await coordinator.SendToConnection(message, connectionId))
202+
.ShouldBeTrue("Coordinator failed to reach connection before shutdown when keep-alive disabled.");
203+
204+
var delivered = await Task.WhenAny(probe.Task, Task.Delay(TimeSpan.FromSeconds(5)));
205+
delivered.ShouldBe(probe.Task, "Connection did not receive coordinator probe before shutdown.");
206+
207+
(await partitionGrain.SendToConnection(message, connectionId))
208+
.ShouldBeTrue("Partition grain failed to deliver probe before shutdown.");
209+
210+
_output.WriteLine("Disposing disabled keep-alive host to trigger shutdown cleanup.");
211+
DisposeHost();
212+
await Task.Delay(TimeSpan.FromSeconds(1));
213+
214+
var coordinatorResult = await coordinator.SendToConnection(message, connectionId);
215+
coordinatorResult.ShouldBeFalse("Coordinator still tracks connection after shutdown without keep-alive.");
216+
217+
var partitionResult = await partitionGrain.SendToConnection(message, connectionId);
218+
partitionResult.ShouldBeFalse("Partition grain still tracks connection after shutdown without keep-alive.");
219+
220+
await connection.DisposeAsync();
221+
}
222+
223+
private TestWebApplication EnsureApp()
224+
{
225+
return _app ?? throw new InvalidOperationException("Test host is not initialised.");
226+
}
227+
228+
private void DisposeHost()
229+
{
230+
if (_app is null)
231+
{
232+
return;
233+
}
234+
235+
_app.Dispose();
236+
_app = null;
237+
}
238+
}

0 commit comments

Comments
 (0)