feat: add route pool size negotiation (Gap 13.3)
Add NegotiatePoolSize static method and NegotiatedPoolSize property to RouteConnection, and ConfiguredPoolSize / GetEffectivePoolSize to RouteManager. Includes 14 tests covering negotiation semantics, backward compatibility (zero means no pooling), default state, and deterministic pool index computation.
This commit is contained in:
210
tests/NATS.Server.Tests/Routes/PoolSizeNegotiationTests.cs
Normal file
210
tests/NATS.Server.Tests/Routes/PoolSizeNegotiationTests.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
// 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.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user