feat: add route propagation and bootstrap js gateway leaf services
This commit is contained in:
14
tests/NATS.Server.Tests/GatewayLeafBootstrapTests.cs
Normal file
14
tests/NATS.Server.Tests/GatewayLeafBootstrapTests.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class GatewayLeafBootstrapTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Server_bootstraps_gateway_and_leaf_managers_when_configured()
|
||||
{
|
||||
await using var server = await TestServerFactory.CreateWithGatewayAndLeafAsync();
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
server.Stats.Gateways.ShouldBeGreaterThanOrEqualTo(0);
|
||||
server.Stats.Leafs.ShouldBeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
13
tests/NATS.Server.Tests/JetStreamStartupTests.cs
Normal file
13
tests/NATS.Server.Tests/JetStreamStartupTests.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class JetStreamStartupTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task JetStream_enabled_server_starts_service()
|
||||
{
|
||||
await using var server = await TestServerFactory.CreateJetStreamEnabledAsync();
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
server.Stats.JetStreamEnabled.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,55 @@ internal static class TestServerFactory
|
||||
|
||||
return new ClusterTestServer(server, cts);
|
||||
}
|
||||
|
||||
public static async Task<ClusterTestServer> CreateWithGatewayAndLeafAsync()
|
||||
{
|
||||
var options = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
Gateway = new GatewayOptions
|
||||
{
|
||||
Name = "G1",
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
},
|
||||
LeafNode = new LeafNodeOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
},
|
||||
};
|
||||
|
||||
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
return new ClusterTestServer(server, cts);
|
||||
}
|
||||
|
||||
public static async Task<ClusterTestServer> CreateJetStreamEnabledAsync()
|
||||
{
|
||||
var options = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
JetStream = new JetStreamOptions
|
||||
{
|
||||
StoreDir = Path.Combine(Path.GetTempPath(), $"nats-js-{Guid.NewGuid():N}"),
|
||||
MaxMemoryStore = 1024 * 1024,
|
||||
MaxFileStore = 10 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
|
||||
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
return new ClusterTestServer(server, cts);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ClusterTestServer(NatsServer server, CancellationTokenSource cts) : IAsyncDisposable
|
||||
|
||||
141
tests/NATS.Server.Tests/RouteSubscriptionPropagationTests.cs
Normal file
141
tests/NATS.Server.Tests/RouteSubscriptionPropagationTests.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class RouteSubscriptionPropagationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Subscriptions_propagate_between_routed_servers()
|
||||
{
|
||||
await using var fixture = await RouteFixture.StartTwoNodeClusterAsync();
|
||||
|
||||
await fixture.SubscribeOnServerBAsync("foo.*");
|
||||
var hasInterest = await fixture.ServerAHasRemoteInterestAsync("foo.bar");
|
||||
|
||||
hasInterest.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RouteFixture : IAsyncDisposable
|
||||
{
|
||||
private readonly NatsServer _serverA;
|
||||
private readonly NatsServer _serverB;
|
||||
private readonly CancellationTokenSource _ctsA;
|
||||
private readonly CancellationTokenSource _ctsB;
|
||||
private Socket? _subscriberOnB;
|
||||
|
||||
private RouteFixture(NatsServer serverA, NatsServer serverB, CancellationTokenSource ctsA, CancellationTokenSource ctsB)
|
||||
{
|
||||
_serverA = serverA;
|
||||
_serverB = serverB;
|
||||
_ctsA = ctsA;
|
||||
_ctsB = ctsB;
|
||||
}
|
||||
|
||||
public static async Task<RouteFixture> StartTwoNodeClusterAsync()
|
||||
{
|
||||
var optsA = new NatsOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
Cluster = new ClusterOptions
|
||||
{
|
||||
Name = Guid.NewGuid().ToString("N"),
|
||||
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 = Guid.NewGuid().ToString("N"),
|
||||
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();
|
||||
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
while (!timeout.IsCancellationRequested && (serverA.Stats.Routes == 0 || serverB.Stats.Routes == 0))
|
||||
await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
|
||||
|
||||
return new RouteFixture(serverA, serverB, ctsA, ctsB);
|
||||
}
|
||||
|
||||
public async Task SubscribeOnServerBAsync(string subject)
|
||||
{
|
||||
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(IPAddress.Loopback, _serverB.Port);
|
||||
_subscriberOnB = sock;
|
||||
|
||||
await ReadLineAsync(sock); // INFO
|
||||
await sock.SendAsync(Encoding.ASCII.GetBytes($"CONNECT {{}}\r\nSUB {subject} 1\r\nPING\r\n"));
|
||||
await ReadUntilAsync(sock, "PONG");
|
||||
}
|
||||
|
||||
public async Task<bool> ServerAHasRemoteInterestAsync(string subject)
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
while (!timeout.IsCancellationRequested)
|
||||
{
|
||||
if (_serverA.HasRemoteInterest(subject))
|
||||
return true;
|
||||
await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_subscriberOnB?.Dispose();
|
||||
await _ctsA.CancelAsync();
|
||||
await _ctsB.CancelAsync();
|
||||
_serverA.Dispose();
|
||||
_serverB.Dispose();
|
||||
_ctsA.Dispose();
|
||||
_ctsB.Dispose();
|
||||
}
|
||||
|
||||
private static async Task<string> ReadLineAsync(Socket sock)
|
||||
{
|
||||
var buf = new byte[4096];
|
||||
var n = await sock.ReceiveAsync(buf, SocketFlags.None);
|
||||
return Encoding.ASCII.GetString(buf, 0, n);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadUntilAsync(Socket sock, string expected)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var buf = new byte[4096];
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
|
||||
while (!sb.ToString().Contains(expected, StringComparison.Ordinal))
|
||||
{
|
||||
var n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token);
|
||||
if (n == 0)
|
||||
break;
|
||||
sb.Append(Encoding.ASCII.GetString(buf, 0, n));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user