using System.Net.Http.Json; using NATS.Client.Core; namespace NATS.E2E.Tests.Infrastructure; public sealed class ClusterFixture : IAsyncLifetime { private NatsServerProcess _server1 = null!; private NatsServerProcess _server2 = null!; private NatsServerProcess _server3 = null!; public int Port1 => _server1.Port; public int Port2 => _server2.Port; public int Port3 => _server3.Port; public async Task InitializeAsync() { var clusterPort1 = NatsServerProcess.AllocateFreePort(); var clusterPort2 = NatsServerProcess.AllocateFreePort(); var clusterPort3 = NatsServerProcess.AllocateFreePort(); var routes = $""" nats-route://127.0.0.1:{clusterPort1} nats-route://127.0.0.1:{clusterPort2} nats-route://127.0.0.1:{clusterPort3} """; string MakeConfig(string name, int clusterPort) => $$""" server_name: {{name}} cluster { name: e2e-cluster listen: 127.0.0.1:{{clusterPort}} pool_size: 1 routes: [ {{routes}} ] } """; _server1 = NatsServerProcess.WithConfig(MakeConfig("node1", clusterPort1), enableMonitoring: true, extraArgs: ["-DV"]); _server2 = NatsServerProcess.WithConfig(MakeConfig("node2", clusterPort2), enableMonitoring: true, extraArgs: ["-DV"]); _server3 = NatsServerProcess.WithConfig(MakeConfig("node3", clusterPort3), enableMonitoring: true, extraArgs: ["-DV"]); await Task.WhenAll( _server1.StartAsync(), _server2.StartAsync(), _server3.StartAsync()); // Poll until all 3 nodes each report 2 connected routes (full mesh) await WaitForFullMeshAsync(); } private async Task WaitForFullMeshAsync() { using var http = new HttpClient(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30)); using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200)); var monitorPorts = new[] { _server1.MonitorPort!.Value, _server2.MonitorPort!.Value, _server3.MonitorPort!.Value }; while (await timer.WaitForNextTickAsync(timeout.Token).ConfigureAwait(false)) { var allConnected = true; foreach (var monitorPort in monitorPorts) { try { var routez = await http.GetFromJsonAsync( $"http://127.0.0.1:{monitorPort}/routez", timeout.Token); if (routez?.NumRoutes < 2) { allConnected = false; break; } } catch { allConnected = false; break; } } if (allConnected) return; } throw new TimeoutException("Cluster did not form a full mesh within 30s."); } public async Task DisposeAsync() { await Task.WhenAll( _server1.DisposeAsync().AsTask(), _server2.DisposeAsync().AsTask(), _server3.DisposeAsync().AsTask()); } public string GetServerOutput(int nodeIndex) => nodeIndex switch { 0 => _server1.Output, 1 => _server2.Output, 2 => _server3.Output, _ => throw new ArgumentOutOfRangeException(nameof(nodeIndex)), }; public NatsConnection CreateClient(int nodeIndex = 0) { var port = nodeIndex switch { 0 => Port1, 1 => Port2, 2 => Port3, _ => throw new ArgumentOutOfRangeException(nameof(nodeIndex)), }; return new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{port}" }); } private sealed class Routez { [System.Text.Json.Serialization.JsonPropertyName("num_routes")] public int NumRoutes { get; init; } } } [CollectionDefinition("E2E-Cluster")] public class ClusterCollection : ICollectionFixture;