Files
natsdotnet/tests/NATS.Server.Clustering.Tests/Routes/PoolSizeNegotiationTests.cs
Joseph Doherty 615752cdc2 refactor: extract NATS.Server.Clustering.Tests project
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.
2026-03-12 15:31:58 -04:00

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);
}
}