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,236 @@
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server;
using NATS.Server.Gateways;
using NATS.Server.Protocol;
using NATS.Server.Routes;
namespace NATS.Server.Tests;
// Go reference: server/route.go processImplicitRoute, server/gateway.go processImplicitGateway
public class ImplicitDiscoveryTests
{
[Fact]
public void ProcessImplicitRoute_discovers_new_peer()
{
// Go reference: server/route.go processImplicitRoute
var mgr = RouteManagerTestHelper.Create();
var serverInfo = new ServerInfo
{
ServerId = "server-2",
ServerName = "server-2",
Version = "0.1.0",
Host = "0.0.0.0",
Port = 4222,
ConnectUrls = ["nats://10.0.0.2:6222", "nats://10.0.0.3:6222"],
};
mgr.ProcessImplicitRoute(serverInfo);
mgr.DiscoveredRoutes.ShouldContain("nats://10.0.0.2:6222");
mgr.DiscoveredRoutes.ShouldContain("nats://10.0.0.3:6222");
}
[Fact]
public void ProcessImplicitRoute_skips_known_peers()
{
// Go reference: server/route.go processImplicitRoute — skip already-known
var mgr = RouteManagerTestHelper.Create();
mgr.AddKnownRoute("nats://10.0.0.2:6222");
var serverInfo = new ServerInfo
{
ServerId = "server-2",
ServerName = "server-2",
Version = "0.1.0",
Host = "0.0.0.0",
Port = 4222,
ConnectUrls = ["nats://10.0.0.2:6222", "nats://10.0.0.3:6222"],
};
mgr.ProcessImplicitRoute(serverInfo);
mgr.DiscoveredRoutes.Count.ShouldBe(1); // only 10.0.0.3 is new
mgr.DiscoveredRoutes.ShouldContain("nats://10.0.0.3:6222");
}
[Fact]
public void ProcessImplicitRoute_with_null_urls_is_noop()
{
// Go reference: server/route.go processImplicitRoute — nil ConnectUrls guard
var mgr = RouteManagerTestHelper.Create();
var serverInfo = new ServerInfo
{
ServerId = "server-2",
ServerName = "server-2",
Version = "0.1.0",
Host = "0.0.0.0",
Port = 4222,
ConnectUrls = null,
};
mgr.ProcessImplicitRoute(serverInfo);
mgr.DiscoveredRoutes.Count.ShouldBe(0);
}
[Fact]
public void ProcessImplicitRoute_with_empty_urls_is_noop()
{
// Go reference: server/route.go processImplicitRoute — empty ConnectUrls guard
var mgr = RouteManagerTestHelper.Create();
var serverInfo = new ServerInfo
{
ServerId = "server-2",
ServerName = "server-2",
Version = "0.1.0",
Host = "0.0.0.0",
Port = 4222,
ConnectUrls = [],
};
mgr.ProcessImplicitRoute(serverInfo);
mgr.DiscoveredRoutes.Count.ShouldBe(0);
}
[Fact]
public void ProcessImplicitGateway_discovers_new_gateway()
{
// Go reference: server/gateway.go processImplicitGateway
var mgr = GatewayManagerTestHelper.Create();
var gwInfo = new GatewayInfo
{
Name = "cluster-B",
Urls = ["nats://10.0.1.1:7222"],
};
mgr.ProcessImplicitGateway(gwInfo);
mgr.DiscoveredGateways.ShouldContain("cluster-B");
}
[Fact]
public void ProcessImplicitGateway_with_null_throws()
{
// Go reference: server/gateway.go processImplicitGateway — null guard
var mgr = GatewayManagerTestHelper.Create();
Should.Throw<ArgumentNullException>(() => mgr.ProcessImplicitGateway(null!));
}
[Fact]
public void ProcessImplicitGateway_deduplicates_same_cluster()
{
// Go reference: server/gateway.go processImplicitGateway — idempotent discovery
var mgr = GatewayManagerTestHelper.Create();
var gwInfo = new GatewayInfo { Name = "cluster-B", Urls = ["nats://10.0.1.1:7222"] };
mgr.ProcessImplicitGateway(gwInfo);
mgr.ProcessImplicitGateway(gwInfo);
mgr.DiscoveredGateways.Count.ShouldBe(1);
}
[Fact]
public void ForwardNewRouteInfo_invokes_event()
{
// Go reference: server/route.go forwardNewRouteInfoToKnownServers
var mgr = RouteManagerTestHelper.Create();
var forwarded = new List<string>();
mgr.OnForwardInfo += urls => forwarded.AddRange(urls);
mgr.ForwardNewRouteInfoToKnownServers("nats://10.0.0.5:6222");
forwarded.ShouldContain("nats://10.0.0.5:6222");
}
[Fact]
public void ForwardNewRouteInfo_with_no_handler_does_not_throw()
{
// Go reference: server/route.go forwardNewRouteInfoToKnownServers — no subscribers
var mgr = RouteManagerTestHelper.Create();
Should.NotThrow(() => mgr.ForwardNewRouteInfoToKnownServers("nats://10.0.0.5:6222"));
}
[Fact]
public void AddKnownRoute_prevents_later_discovery()
{
// Go reference: server/route.go processImplicitRoute — pre-seeded known routes
var mgr = RouteManagerTestHelper.Create();
mgr.AddKnownRoute("nats://10.0.0.9:6222");
var serverInfo = new ServerInfo
{
ServerId = "server-3",
ServerName = "server-3",
Version = "0.1.0",
Host = "0.0.0.0",
Port = 4222,
ConnectUrls = ["nats://10.0.0.9:6222"],
};
mgr.ProcessImplicitRoute(serverInfo);
mgr.DiscoveredRoutes.Count.ShouldBe(0);
}
[Fact]
public void ConnectUrls_is_serialized_when_set()
{
// Go reference: server/route.go INFO message includes connect_urls
var info = new ServerInfo
{
ServerId = "s1",
ServerName = "s1",
Version = "0.1.0",
Host = "0.0.0.0",
Port = 4222,
ConnectUrls = ["nats://10.0.0.1:4222"],
};
var json = System.Text.Json.JsonSerializer.Serialize(info);
json.ShouldContain("connect_urls");
json.ShouldContain("nats://10.0.0.1:4222");
}
[Fact]
public void ConnectUrls_is_omitted_when_null()
{
// Go reference: server/route.go INFO omits connect_urls when empty
var info = new ServerInfo
{
ServerId = "s1",
ServerName = "s1",
Version = "0.1.0",
Host = "0.0.0.0",
Port = 4222,
ConnectUrls = null,
};
var json = System.Text.Json.JsonSerializer.Serialize(info);
json.ShouldNotContain("connect_urls");
}
}
public static class RouteManagerTestHelper
{
public static RouteManager Create()
{
var options = new ClusterOptions { Name = "test-cluster", Host = "127.0.0.1", Port = 0 };
var stats = new ServerStats();
return new RouteManager(options, stats, "server-1", _ => { }, _ => { }, NullLogger<RouteManager>.Instance);
}
}
public static class GatewayManagerTestHelper
{
public static GatewayManager Create()
{
var options = new GatewayOptions { Name = "cluster-A", Host = "127.0.0.1", Port = 0 };
var stats = new ServerStats();
return new GatewayManager(options, stats, "server-1", _ => { }, _ => { }, NullLogger<GatewayManager>.Instance);
}
}