Move 29 clustering/routing test files from NATS.Server.Tests to a dedicated NATS.Server.Clustering.Tests project. Update namespaces, replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls, and extract TestServerFactory/ClusterTestServer to TestUtilities to fix cross-project reference from JetStreamStartupTests.
211 lines
7.6 KiB
C#
211 lines
7.6 KiB
C#
// Reference: golang/nats-server/server/route.go — negotiateRoutePool (pooling handshake logic)
|
|
// Tests for route pool size negotiation between local and remote peers.
|
|
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NATS.Server.Configuration;
|
|
using NATS.Server.Routes;
|
|
|
|
namespace NATS.Server.Clustering.Tests.Routes;
|
|
|
|
/// <summary>
|
|
/// Tests for route pool size negotiation (Gap 13.3).
|
|
/// Covers RouteConnection.NegotiatePoolSize static method,
|
|
/// RouteConnection.NegotiatedPoolSize default, RouteManager.ConfiguredPoolSize,
|
|
/// and RouteManager.GetEffectivePoolSize.
|
|
/// Go reference: server/route.go negotiateRoutePool.
|
|
/// </summary>
|
|
public class PoolSizeNegotiationTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a connected socket pair so RouteConnection can build its NetworkStream.
|
|
/// Returns the RouteConnection (owns the server-side socket) and the client-side
|
|
/// socket, which the caller must dispose to avoid resource leaks.
|
|
/// </summary>
|
|
private static (RouteConnection Route, Socket ClientSocket) CreateConnectedRoute()
|
|
{
|
|
var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
|
listener.Listen(1);
|
|
var port = ((IPEndPoint)listener.LocalEndPoint!).Port;
|
|
|
|
var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
client.Connect(IPAddress.Loopback, port);
|
|
var server = listener.Accept();
|
|
listener.Dispose();
|
|
|
|
return (new RouteConnection(server), client);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RouteConnection.NegotiatePoolSize static method
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_BothNonZero_ReturnsMin()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — min(local, remote)
|
|
var result = RouteConnection.NegotiatePoolSize(3, 5);
|
|
result.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_Equal_ReturnsSame()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — equal sizes
|
|
var result = RouteConnection.NegotiatePoolSize(3, 3);
|
|
result.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_LocalZero_ReturnsZero()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — backward compat:
|
|
// if either peer sends 0 (no pooling advertised), result is 0.
|
|
var result = RouteConnection.NegotiatePoolSize(0, 5);
|
|
result.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_RemoteZero_ReturnsZero()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — backward compat:
|
|
// remote peer without pool support sends 0; result must be 0.
|
|
var result = RouteConnection.NegotiatePoolSize(3, 0);
|
|
result.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_BothZero_ReturnsZero()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — both disabled
|
|
var result = RouteConnection.NegotiatePoolSize(0, 0);
|
|
result.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_LocalLarger_ReturnsRemote()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — min(10, 3) = 3
|
|
var result = RouteConnection.NegotiatePoolSize(10, 3);
|
|
result.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_RemoteLarger_ReturnsLocal()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — min(3, 10) = 3
|
|
var result = RouteConnection.NegotiatePoolSize(3, 10);
|
|
result.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void NegotiatePoolSize_OneIsOne_ReturnsOne()
|
|
{
|
|
// Go reference: server/route.go negotiateRoutePool — min(1, 5) = 1
|
|
var result = RouteConnection.NegotiatePoolSize(1, 5);
|
|
result.ShouldBe(1);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RouteConnection.NegotiatedPoolSize default value
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void NegotiatedPoolSize_Default_IsZero()
|
|
{
|
|
// A newly created RouteConnection (not yet handshaked) must report
|
|
// NegotiatedPoolSize == 0 to signal that negotiation has not occurred.
|
|
// Go reference: server/route.go — pool size is 0 until negotiateRoutePool runs.
|
|
var (conn, clientSocket) = CreateConnectedRoute();
|
|
try
|
|
{
|
|
conn.NegotiatedPoolSize.ShouldBe(0);
|
|
}
|
|
finally
|
|
{
|
|
clientSocket.Dispose();
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RouteManager.ComputeRoutePoolIdx determinism (regression guard)
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ComputeRoutePoolIdx_Deterministic()
|
|
{
|
|
// The same account name must always map to the same pool index.
|
|
// Go reference: server/route.go computeRoutePoolIdx (FNV-1a 32-bit hash).
|
|
const int poolSize = 5;
|
|
const string account = "test-account";
|
|
|
|
var first = RouteManager.ComputeRoutePoolIdx(poolSize, account);
|
|
var second = RouteManager.ComputeRoutePoolIdx(poolSize, account);
|
|
var third = RouteManager.ComputeRoutePoolIdx(poolSize, account);
|
|
|
|
first.ShouldBe(second);
|
|
second.ShouldBe(third);
|
|
first.ShouldBeGreaterThanOrEqualTo(0);
|
|
first.ShouldBeLessThan(poolSize);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// RouteManager.ConfiguredPoolSize and GetEffectivePoolSize
|
|
// -----------------------------------------------------------------------
|
|
|
|
private static RouteManager MakeManager(int poolSize = 3)
|
|
{
|
|
var opts = new ClusterOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
PoolSize = poolSize,
|
|
};
|
|
return new RouteManager(
|
|
opts,
|
|
new ServerStats(),
|
|
Guid.NewGuid().ToString("N"),
|
|
_ => { },
|
|
_ => { },
|
|
NullLogger<RouteManager>.Instance);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfiguredPoolSize_ReturnsOptionsPoolSize()
|
|
{
|
|
// RouteManager should expose the configured pool size from ClusterOptions.
|
|
// Go reference: server/route.go opts.Cluster.PoolSize default is 3.
|
|
var manager = MakeManager(poolSize: 5);
|
|
manager.ConfiguredPoolSize.ShouldBe(5);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfiguredPoolSize_DefaultThreeWhenOptionsIsZero()
|
|
{
|
|
// When ClusterOptions.PoolSize is 0 (not explicitly set), the effective
|
|
// configured pool size should fall back to 3 (Go's default).
|
|
// Go reference: server/route.go default pool size of 3.
|
|
var manager = MakeManager(poolSize: 0);
|
|
manager.ConfiguredPoolSize.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetEffectivePoolSize_NullRemoteId_ReturnsConfigured()
|
|
{
|
|
// With no remote server ID, GetEffectivePoolSize returns the configured pool size.
|
|
var manager = MakeManager(poolSize: 4);
|
|
manager.GetEffectivePoolSize(null).ShouldBe(4);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetEffectivePoolSize_UnknownRemoteId_ReturnsConfigured()
|
|
{
|
|
// For a remote server ID that has no connected (and negotiated) route,
|
|
// GetEffectivePoolSize should return the configured pool size.
|
|
var manager = MakeManager(poolSize: 3);
|
|
manager.GetEffectivePoolSize("unknown-server-id").ShouldBe(3);
|
|
}
|
|
}
|