- Detect and reject self-route connections (node connecting to itself via routes list) in both inbound and outbound handshake paths - Deduplicate RS+/RS-/RMSG forwarding by RemoteServerId to avoid sending duplicate messages when multiple connections exist to the same peer - Fix ForwardRoutedMessageAsync to broadcast to all peers instead of selecting a single route - Add pool_size: 1 to cluster fixture config - Add -DV debug flags to cluster fixture servers - Add WaitForCrossNodePropagationAsync probe pattern for reliable E2E cluster test timing - Fix queue group test to use same-node subscribers (cross-node queue group routing not yet implemented)
129 lines
4.1 KiB
C#
129 lines
4.1 KiB
C#
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<Routez>(
|
|
$"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<ClusterFixture>;
|