feat(cluster): add implicit route and gateway discovery via INFO gossip

Implements ProcessImplicitRoute and ForwardNewRouteInfoToKnownServers on RouteManager,
and ProcessImplicitGateway on GatewayManager, mirroring Go server/route.go and
server/gateway.go INFO gossip-based peer discovery. Adds ConnectUrls to ServerInfo
and introduces GatewayInfo model. 12 new unit tests in ImplicitDiscoveryTests.
This commit is contained in:
Joseph Doherty
2026-02-25 03:05:35 -05:00
parent e09835ca70
commit bfe7a71fcd
5 changed files with 337 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
namespace NATS.Server.Gateways;
/// <summary>
/// Information about a remote gateway cluster received during implicit discovery.
/// Go reference: server/gateway.go — implicit gateway discovery via INFO gossip.
/// </summary>
public sealed record GatewayInfo
{
/// <summary>Name of the remote gateway cluster.</summary>
public required string Name { get; init; }
/// <summary>URLs for connecting to the remote gateway cluster.</summary>
public required string[] Urls { get; init; }
}

View File

@@ -16,6 +16,7 @@ public sealed class GatewayManager : IAsyncDisposable
private readonly Action<GatewayMessage> _messageSink;
private readonly ILogger<GatewayManager> _logger;
private readonly ConcurrentDictionary<string, GatewayConnection> _connections = new(StringComparer.Ordinal);
private readonly HashSet<string> _discoveredGateways = new(StringComparer.OrdinalIgnoreCase);
private long _forwardedJetStreamClusterMessages;
private CancellationTokenSource? _cts;
@@ -44,6 +45,29 @@ public sealed class GatewayManager : IAsyncDisposable
_logger = logger;
}
/// <summary>
/// Gateway clusters auto-discovered via INFO gossip.
/// Go reference: server/gateway.go processImplicitGateway.
/// </summary>
public IReadOnlyCollection<string> DiscoveredGateways
{
get { lock (_discoveredGateways) return _discoveredGateways.ToList(); }
}
/// <summary>
/// Processes a gateway info message from a peer, discovering new gateway clusters.
/// Go reference: server/gateway.go:800-850 (processImplicitGateway).
/// </summary>
public void ProcessImplicitGateway(GatewayInfo gwInfo)
{
ArgumentNullException.ThrowIfNull(gwInfo);
lock (_discoveredGateways)
{
_discoveredGateways.Add(gwInfo.Name);
}
}
public Task StartAsync(CancellationToken ct)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);

View File

@@ -89,6 +89,10 @@ public sealed class ServerInfo
[JsonPropertyName("tls_available")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool TlsAvailable { get; set; }
[JsonPropertyName("connect_urls")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string[]? ConnectUrls { get; set; }
}
public sealed class ClientOptions

View File

@@ -3,6 +3,7 @@ using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using NATS.Server.Configuration;
using NATS.Server.Protocol;
using NATS.Server.Subscriptions;
namespace NATS.Server.Routes;
@@ -18,6 +19,8 @@ public sealed class RouteManager : IAsyncDisposable
private readonly Action<RouteMessage> _routedMessageSink;
private readonly ConcurrentDictionary<string, RouteConnection> _routes = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, byte> _connectedServerIds = new(StringComparer.Ordinal);
private readonly HashSet<string> _discoveredRoutes = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _knownRouteUrls = new(StringComparer.OrdinalIgnoreCase);
private CancellationTokenSource? _cts;
private Socket? _listener;
@@ -49,6 +52,62 @@ public sealed class RouteManager : IAsyncDisposable
_logger = logger;
}
/// <summary>
/// Routes auto-discovered via INFO gossip from peers.
/// Go reference: server/route.go processImplicitRoute.
/// </summary>
public IReadOnlyCollection<string> DiscoveredRoutes
{
get { lock (_discoveredRoutes) return _discoveredRoutes.ToList(); }
}
/// <summary>
/// Event raised when new route info should be forwarded to known peers.
/// Go reference: server/route.go forwardNewRouteInfoToKnownServers.
/// </summary>
public event Action<List<string>>? OnForwardInfo;
/// <summary>
/// Processes connect_urls from a peer's INFO message. Any URLs not already
/// known are added to DiscoveredRoutes for solicited connection.
/// Go reference: server/route.go:1500-1550 (processImplicitRoute).
/// </summary>
public void ProcessImplicitRoute(ServerInfo serverInfo)
{
if (serverInfo.ConnectUrls is null || serverInfo.ConnectUrls.Length == 0)
return;
lock (_discoveredRoutes)
{
foreach (var url in serverInfo.ConnectUrls)
{
if (!_knownRouteUrls.Contains(url))
{
_discoveredRoutes.Add(url);
}
}
}
}
/// <summary>
/// Forwards new peer URL information to all known route connections.
/// Go reference: server/route.go forwardNewRouteInfoToKnownServers.
/// </summary>
public void ForwardNewRouteInfoToKnownServers(string newPeerUrl)
{
OnForwardInfo?.Invoke([newPeerUrl]);
}
/// <summary>
/// Adds a URL to the known route set. Used during initialization and testing.
/// </summary>
public void AddKnownRoute(string url)
{
lock (_discoveredRoutes)
{
_knownRouteUrls.Add(url);
}
}
/// <summary>
/// Returns a route pool index for the given account name, matching Go's