feat: add graceful shutdown, accept loop backoff, and task tracking
This commit is contained in:
@@ -547,3 +547,115 @@ public class ServerIdentityTests
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class GracefulShutdownTests
|
||||
{
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_disconnects_all_clients()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
var server = new NatsServer(new NatsOptions { Port = port }, NullLoggerFactory.Instance);
|
||||
_ = server.StartAsync(CancellationToken.None);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
// Connect 2 raw TCP clients
|
||||
using var client1 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await client1.ConnectAsync(IPAddress.Loopback, port);
|
||||
var buf = new byte[4096];
|
||||
await client1.ReceiveAsync(buf, SocketFlags.None); // INFO
|
||||
|
||||
using var client2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await client2.ConnectAsync(IPAddress.Loopback, port);
|
||||
await client2.ReceiveAsync(buf, SocketFlags.None); // INFO
|
||||
|
||||
// Send CONNECT so both are registered
|
||||
await client1.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPING\r\n"));
|
||||
await client2.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPING\r\n"));
|
||||
|
||||
// Wait for PONG from both (confirming they are registered)
|
||||
using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
await client1.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||
await client2.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||
|
||||
server.ClientCount.ShouldBe(2);
|
||||
|
||||
await server.ShutdownAsync();
|
||||
|
||||
server.ClientCount.ShouldBe(0);
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WaitForShutdown_blocks_until_shutdown()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
var server = new NatsServer(new NatsOptions { Port = port }, NullLoggerFactory.Instance);
|
||||
_ = server.StartAsync(CancellationToken.None);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
// Start WaitForShutdown in background
|
||||
var waitTask = Task.Run(() => server.WaitForShutdown());
|
||||
|
||||
// Give it a moment -- it should NOT complete yet
|
||||
await Task.Delay(200);
|
||||
waitTask.IsCompleted.ShouldBeFalse();
|
||||
|
||||
// Trigger shutdown
|
||||
await server.ShutdownAsync();
|
||||
|
||||
// WaitForShutdown should complete within 5 seconds
|
||||
var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(5)));
|
||||
completed.ShouldBe(waitTask);
|
||||
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShutdownAsync_is_idempotent()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
var server = new NatsServer(new NatsOptions { Port = port }, NullLoggerFactory.Instance);
|
||||
_ = server.StartAsync(CancellationToken.None);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
// Call ShutdownAsync 3 times -- should not throw
|
||||
await server.ShutdownAsync();
|
||||
await server.ShutdownAsync();
|
||||
await server.ShutdownAsync();
|
||||
|
||||
server.IsShuttingDown.ShouldBeTrue();
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Accept_loop_waits_for_active_clients()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
var server = new NatsServer(new NatsOptions { Port = port }, NullLoggerFactory.Instance);
|
||||
_ = server.StartAsync(CancellationToken.None);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
// Connect a client
|
||||
using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await client.ConnectAsync(IPAddress.Loopback, port);
|
||||
var buf = new byte[4096];
|
||||
await client.ReceiveAsync(buf, SocketFlags.None); // INFO
|
||||
await client.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPING\r\n"));
|
||||
using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
await client.ReceiveAsync(buf, SocketFlags.None, readCts.Token); // PONG
|
||||
|
||||
// ShutdownAsync should complete within 10 seconds (doesn't hang)
|
||||
var shutdownTask = server.ShutdownAsync();
|
||||
var completed = await Task.WhenAny(shutdownTask, Task.Delay(TimeSpan.FromSeconds(10)));
|
||||
completed.ShouldBe(shutdownTask);
|
||||
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user