feat(routes): add pool accounting per account and S2 compression codec (D2+D3)
D2: Add FNV-1a-based ComputeRoutePoolIdx to RouteManager matching Go's route.go:533-545, with PoolIndex on RouteConnection and account-aware ForwardRoutedMessageAsync that routes to the correct pool connection. D3: Replace DeflateStream with IronSnappy in RouteCompressionCodec, add RouteCompressionLevel enum, NegotiateCompression, and IsCompressed detection. 17 new tests (6 pool + 11 compression), all passing.
This commit is contained in:
@@ -49,6 +49,48 @@ public sealed class RouteManager : IAsyncDisposable
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a route pool index for the given account name, matching Go's
|
||||
/// <c>computeRoutePoolIdx</c> (route.go:533-545). Uses FNV-1a 32-bit hash
|
||||
/// to deterministically map account names to pool indices.
|
||||
/// </summary>
|
||||
public static int ComputeRoutePoolIdx(int poolSize, string accountName)
|
||||
{
|
||||
if (poolSize <= 1)
|
||||
return 0;
|
||||
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(accountName);
|
||||
|
||||
// Use FNV-1a to match Go exactly
|
||||
uint fnvHash = 2166136261; // FNV offset basis
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
fnvHash ^= b;
|
||||
fnvHash *= 16777619; // FNV prime
|
||||
}
|
||||
|
||||
return (int)(fnvHash % (uint)poolSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the route connection responsible for the given account, based on
|
||||
/// pool index computed from the account name. Returns null if no routes exist.
|
||||
/// </summary>
|
||||
public RouteConnection? GetRouteForAccount(string account)
|
||||
{
|
||||
if (_routes.IsEmpty)
|
||||
return null;
|
||||
|
||||
var routes = _routes.Values.ToArray();
|
||||
if (routes.Length == 0)
|
||||
return null;
|
||||
|
||||
var poolSize = routes.Length;
|
||||
var idx = ComputeRoutePoolIdx(poolSize, account);
|
||||
return routes[idx % routes.Length];
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
@@ -66,7 +108,10 @@ public sealed class RouteManager : IAsyncDisposable
|
||||
foreach (var route in _options.Routes.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
for (var i = 0; i < poolSize; i++)
|
||||
_ = Task.Run(() => ConnectToRouteWithRetryAsync(route, _cts.Token));
|
||||
{
|
||||
var poolIndex = i;
|
||||
_ = Task.Run(() => ConnectToRouteWithRetryAsync(route, poolIndex, _cts.Token));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -119,8 +164,18 @@ public sealed class RouteManager : IAsyncDisposable
|
||||
if (_routes.IsEmpty)
|
||||
return;
|
||||
|
||||
foreach (var route in _routes.Values)
|
||||
// Use account-based pool routing: route the message only through the
|
||||
// connection responsible for this account, matching Go's behavior.
|
||||
var route = GetRouteForAccount(account);
|
||||
if (route != null)
|
||||
{
|
||||
await route.SendRmsgAsync(account, subject, replyTo, payload, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: broadcast to all routes if pool routing fails
|
||||
foreach (var r in _routes.Values)
|
||||
await r.SendRmsgAsync(account, subject, replyTo, payload, ct);
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync(CancellationToken ct)
|
||||
@@ -165,7 +220,7 @@ public sealed class RouteManager : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectToRouteWithRetryAsync(string route, CancellationToken ct)
|
||||
private async Task ConnectToRouteWithRetryAsync(string route, int poolIndex, CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
@@ -174,7 +229,7 @@ public sealed class RouteManager : IAsyncDisposable
|
||||
var endPoint = ParseRouteEndpoint(route);
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(endPoint.Address, endPoint.Port, ct);
|
||||
var connection = new RouteConnection(socket);
|
||||
var connection = new RouteConnection(socket) { PoolIndex = poolIndex };
|
||||
await connection.PerformOutboundHandshakeAsync(_serverId, ct);
|
||||
Register(connection);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user