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:
@@ -22,6 +22,36 @@ public sealed class RouteConnection(Socket socket) : IAsyncDisposable
|
|||||||
/// for a given account. See <see cref="RouteManager.ComputeRoutePoolIdx"/>.
|
/// for a given account. See <see cref="RouteManager.ComputeRoutePoolIdx"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PoolIndex { get; set; }
|
public int PoolIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The pool size agreed upon during handshake negotiation with the remote peer.
|
||||||
|
/// Defaults to 0 (no pooling / pre-negotiation state). Set after handshake completes.
|
||||||
|
/// Go reference: server/route.go negotiateRoutePool.
|
||||||
|
/// </summary>
|
||||||
|
public int NegotiatedPoolSize { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Negotiates the effective route pool size between local and remote peers.
|
||||||
|
/// Returns <c>Math.Min(localPoolSize, remotePoolSize)</c>, but returns 0 if
|
||||||
|
/// either side is 0 for backward compatibility with peers that do not support pooling.
|
||||||
|
/// Go reference: server/route.go negotiateRoutePool.
|
||||||
|
/// </summary>
|
||||||
|
public static int NegotiatePoolSize(int localPoolSize, int remotePoolSize)
|
||||||
|
{
|
||||||
|
if (localPoolSize == 0 || remotePoolSize == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return Math.Min(localPoolSize, remotePoolSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the result of pool size negotiation to this connection.
|
||||||
|
/// </summary>
|
||||||
|
internal void SetNegotiatedPoolSize(int negotiatedPoolSize)
|
||||||
|
{
|
||||||
|
NegotiatedPoolSize = negotiatedPoolSize;
|
||||||
|
}
|
||||||
|
|
||||||
public Func<RemoteSubscription, Task>? RemoteSubscriptionReceived { get; set; }
|
public Func<RemoteSubscription, Task>? RemoteSubscriptionReceived { get; set; }
|
||||||
public Func<RouteMessage, Task>? RoutedMessageReceived { get; set; }
|
public Func<RouteMessage, Task>? RoutedMessageReceived { get; set; }
|
||||||
|
|
||||||
|
|||||||
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