using Microsoft.Extensions.Logging.Abstractions; using NATS.Client.Core; using NATS.Server.Configuration; namespace NATS.Server.Tests.Routes; /// /// Tests cluster route formation and message forwarding between servers. /// Ported from Go: server/routes_test.go — TestRouteConfig, TestSeedSolicitWorks. /// public class RouteConfigTests { [Fact] public async Task Two_servers_form_full_mesh_cluster() { // Reference: Go TestSeedSolicitWorks — verifies that two servers // with one pointing Routes at the other form a connected cluster. var clusterName = Guid.NewGuid().ToString("N"); var optsA = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, }, }; var serverA = new NatsServer(optsA, NullLoggerFactory.Instance); var ctsA = new CancellationTokenSource(); _ = serverA.StartAsync(ctsA.Token); await serverA.WaitForReadyAsync(); var optsB = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, Routes = [serverA.ClusterListen!], }, }; var serverB = new NatsServer(optsB, NullLoggerFactory.Instance); var ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); try { // Wait for both servers to see a route connection using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && (Interlocked.Read(ref serverA.Stats.Routes) == 0 || Interlocked.Read(ref serverB.Stats.Routes) == 0)) { await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } Interlocked.Read(ref serverA.Stats.Routes).ShouldBeGreaterThan(0); Interlocked.Read(ref serverB.Stats.Routes).ShouldBeGreaterThan(0); } finally { await ctsA.CancelAsync(); await ctsB.CancelAsync(); serverA.Dispose(); serverB.Dispose(); ctsA.Dispose(); ctsB.Dispose(); } } [Fact] public async Task Route_forwards_messages_between_clusters() { // Reference: Go TestSeedSolicitWorks — sets up a seed + one server, // subscribes on one, publishes on the other, verifies delivery. var clusterName = Guid.NewGuid().ToString("N"); var optsA = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, }, }; var serverA = new NatsServer(optsA, NullLoggerFactory.Instance); var ctsA = new CancellationTokenSource(); _ = serverA.StartAsync(ctsA.Token); await serverA.WaitForReadyAsync(); var optsB = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, Routes = [serverA.ClusterListen!], }, }; var serverB = new NatsServer(optsB, NullLoggerFactory.Instance); var ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); try { // Wait for route formation using var routeTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!routeTimeout.IsCancellationRequested && (Interlocked.Read(ref serverA.Stats.Routes) == 0 || Interlocked.Read(ref serverB.Stats.Routes) == 0)) { await Task.Delay(50, routeTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } // Connect subscriber to server A await using var subscriber = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{serverA.Port}", }); await subscriber.ConnectAsync(); await using var sub = await subscriber.SubscribeCoreAsync("foo"); await subscriber.PingAsync(); // Wait for remote interest to propagate from A to B using var interestTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!interestTimeout.IsCancellationRequested && !serverB.HasRemoteInterest("foo")) { await Task.Delay(50, interestTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } // Connect publisher to server B and publish await using var publisher = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{serverB.Port}", }); await publisher.ConnectAsync(); await publisher.PublishAsync("foo", "Hello"); // Verify message arrives on server A's subscriber using var receiveTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var msg = await sub.Msgs.ReadAsync(receiveTimeout.Token); msg.Data.ShouldBe("Hello"); } finally { await ctsA.CancelAsync(); await ctsB.CancelAsync(); serverA.Dispose(); serverB.Dispose(); ctsA.Dispose(); ctsB.Dispose(); } } [Fact] public async Task Route_reconnects_after_peer_restart() { // Verifies that when a peer is stopped and restarted, the route // re-forms and message forwarding resumes. var clusterName = Guid.NewGuid().ToString("N"); var optsA = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, }, }; var serverA = new NatsServer(optsA, NullLoggerFactory.Instance); var ctsA = new CancellationTokenSource(); _ = serverA.StartAsync(ctsA.Token); await serverA.WaitForReadyAsync(); var clusterListenA = serverA.ClusterListen!; var optsB = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, Routes = [clusterListenA], }, }; var serverB = new NatsServer(optsB, NullLoggerFactory.Instance); var ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); try { // Wait for initial route formation using var timeout1 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout1.IsCancellationRequested && (Interlocked.Read(ref serverA.Stats.Routes) == 0 || Interlocked.Read(ref serverB.Stats.Routes) == 0)) { await Task.Delay(50, timeout1.Token).ContinueWith(_ => { }, TaskScheduler.Default); } Interlocked.Read(ref serverA.Stats.Routes).ShouldBeGreaterThan(0); // Stop server B await ctsB.CancelAsync(); serverB.Dispose(); ctsB.Dispose(); // Wait for server A to notice the route drop using var dropTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!dropTimeout.IsCancellationRequested && Interlocked.Read(ref serverA.Stats.Routes) != 0) { await Task.Delay(50, dropTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } // Restart server B with the same cluster route target var optsB2 = new NatsOptions { Host = "127.0.0.1", Port = 0, Cluster = new ClusterOptions { Name = clusterName, Host = "127.0.0.1", Port = 0, Routes = [clusterListenA], }, }; serverB = new NatsServer(optsB2, NullLoggerFactory.Instance); ctsB = new CancellationTokenSource(); _ = serverB.StartAsync(ctsB.Token); await serverB.WaitForReadyAsync(); // Wait for route to re-form using var timeout2 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout2.IsCancellationRequested && (Interlocked.Read(ref serverA.Stats.Routes) == 0 || Interlocked.Read(ref serverB.Stats.Routes) == 0)) { await Task.Delay(50, timeout2.Token).ContinueWith(_ => { }, TaskScheduler.Default); } Interlocked.Read(ref serverA.Stats.Routes).ShouldBeGreaterThan(0); Interlocked.Read(ref serverB.Stats.Routes).ShouldBeGreaterThan(0); // Verify message forwarding works after reconnect await using var subscriber = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{serverA.Port}", }); await subscriber.ConnectAsync(); await using var sub = await subscriber.SubscribeCoreAsync("bar"); await subscriber.PingAsync(); // Wait for remote interest to propagate using var interestTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!interestTimeout.IsCancellationRequested && !serverB.HasRemoteInterest("bar")) { await Task.Delay(50, interestTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } await using var publisher = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{serverB.Port}", }); await publisher.ConnectAsync(); await publisher.PublishAsync("bar", "AfterReconnect"); using var receiveTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var msg = await sub.Msgs.ReadAsync(receiveTimeout.Token); msg.Data.ShouldBe("AfterReconnect"); } finally { await ctsA.CancelAsync(); await ctsB.CancelAsync(); serverA.Dispose(); serverB.Dispose(); ctsA.Dispose(); ctsB.Dispose(); } } }