feat: add cluster config hot reload (Gap 14.4)
Adds ClusterConfigChangeResult and ApplyClusterConfigChanges to ConfigReloader, comparing route/gateway/leaf URL sets between old and new NatsOptions and reporting added/removed routes for connection reconciliation on hot reload.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
// Tests for ConfigReloader.ApplyClusterConfigChanges.
|
||||
// Go reference: golang/nats-server/server/reload.go — routesOption.Apply, gatewayOption.Apply.
|
||||
|
||||
using NATS.Server;
|
||||
using NATS.Server.Configuration;
|
||||
using Shouldly;
|
||||
|
||||
namespace NATS.Server.Tests.Configuration;
|
||||
|
||||
public class ClusterConfigReloadTests
|
||||
{
|
||||
// ─── helpers ────────────────────────────────────────────────────
|
||||
|
||||
private static NatsOptions WithRoutes(params string[] routes)
|
||||
{
|
||||
var opts = new NatsOptions();
|
||||
opts.Cluster = new ClusterOptions { Routes = [..routes] };
|
||||
return opts;
|
||||
}
|
||||
|
||||
private static NatsOptions WithGatewayRemotes(params string[] urls)
|
||||
{
|
||||
var opts = new NatsOptions();
|
||||
opts.Gateway = new GatewayOptions
|
||||
{
|
||||
RemoteGateways = [new RemoteGatewayOptions { Urls = [..urls] }]
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
|
||||
private static NatsOptions WithLeafRemotes(params string[] urls)
|
||||
{
|
||||
var opts = new NatsOptions();
|
||||
opts.LeafNode = new LeafNodeOptions { Remotes = [..urls] };
|
||||
return opts;
|
||||
}
|
||||
|
||||
// ─── tests ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void No_changes_returns_no_changes()
|
||||
{
|
||||
// Go reference: reload.go routesOption.Apply — no-op when sets are equal
|
||||
var old = WithRoutes("nats://server1:6222", "nats://server2:6222");
|
||||
var updated = WithRoutes("nats://server1:6222", "nats://server2:6222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeFalse();
|
||||
result.RouteUrlsChanged.ShouldBeFalse();
|
||||
result.GatewayUrlsChanged.ShouldBeFalse();
|
||||
result.LeafUrlsChanged.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Route_url_added_detected()
|
||||
{
|
||||
// Go reference: reload.go routesOption.Apply — new route triggers reconnect
|
||||
var old = WithRoutes("nats://server1:6222");
|
||||
var updated = WithRoutes("nats://server1:6222", "nats://server2:6222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.RouteUrlsChanged.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Route_url_removed_detected()
|
||||
{
|
||||
// Go reference: reload.go routesOption.Apply — removed route triggers disconnect
|
||||
var old = WithRoutes("nats://server1:6222", "nats://server2:6222");
|
||||
var updated = WithRoutes("nats://server1:6222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.RouteUrlsChanged.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gateway_url_changed_detected()
|
||||
{
|
||||
// Go reference: reload.go gatewayOption.Apply — gateway remotes reconciled on reload
|
||||
var old = WithGatewayRemotes("nats://gw1:7222");
|
||||
var updated = WithGatewayRemotes("nats://gw2:7222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.GatewayUrlsChanged.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Leaf_url_changed_detected()
|
||||
{
|
||||
// Go reference: reload.go leafNodeOption.Apply — leaf remotes reconciled on reload
|
||||
var old = WithLeafRemotes("nats://hub:5222");
|
||||
var updated = WithLeafRemotes("nats://hub2:5222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.LeafUrlsChanged.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Multiple_changes_detected()
|
||||
{
|
||||
// Go reference: reload.go — multiple topology changes in a single reload
|
||||
var old = new NatsOptions
|
||||
{
|
||||
Cluster = new ClusterOptions { Routes = ["nats://r1:6222"] },
|
||||
Gateway = new GatewayOptions { RemoteGateways = [new RemoteGatewayOptions { Urls = ["nats://gw1:7222"] }] }
|
||||
};
|
||||
var updated = new NatsOptions
|
||||
{
|
||||
Cluster = new ClusterOptions { Routes = ["nats://r1:6222", "nats://r2:6222"] },
|
||||
Gateway = new GatewayOptions { RemoteGateways = [new RemoteGatewayOptions { Urls = ["nats://gw2:7222"] }] }
|
||||
};
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.RouteUrlsChanged.ShouldBeTrue();
|
||||
result.GatewayUrlsChanged.ShouldBeTrue();
|
||||
result.LeafUrlsChanged.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Same_urls_different_order_no_change()
|
||||
{
|
||||
// Go reference: reload.go — order-independent URL comparison
|
||||
var old = WithRoutes("nats://server1:6222", "nats://server2:6222");
|
||||
var updated = WithRoutes("nats://server2:6222", "nats://server1:6222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeFalse();
|
||||
result.RouteUrlsChanged.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddedRouteUrls_lists_new_routes()
|
||||
{
|
||||
// Go reference: reload.go routesOption.Apply — identifies routes to dial
|
||||
var old = WithRoutes("nats://server1:6222");
|
||||
var updated = WithRoutes("nats://server1:6222", "nats://server2:6222", "nats://server3:6222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.AddedRouteUrls.Count.ShouldBe(2);
|
||||
result.AddedRouteUrls.ShouldContain("nats://server2:6222");
|
||||
result.AddedRouteUrls.ShouldContain("nats://server3:6222");
|
||||
result.RemovedRouteUrls.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovedRouteUrls_lists_removed_routes()
|
||||
{
|
||||
// Go reference: reload.go routesOption.Apply — identifies routes to close
|
||||
var old = WithRoutes("nats://server1:6222", "nats://server2:6222", "nats://server3:6222");
|
||||
var updated = WithRoutes("nats://server1:6222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.RemovedRouteUrls.Count.ShouldBe(2);
|
||||
result.RemovedRouteUrls.ShouldContain("nats://server2:6222");
|
||||
result.RemovedRouteUrls.ShouldContain("nats://server3:6222");
|
||||
result.AddedRouteUrls.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_to_non_empty_detected()
|
||||
{
|
||||
// Go reference: reload.go routesOption.Apply — nil→populated triggers dial
|
||||
var old = new NatsOptions(); // no Cluster configured
|
||||
var updated = WithRoutes("nats://server1:6222");
|
||||
|
||||
var result = ConfigReloader.ApplyClusterConfigChanges(old, updated);
|
||||
|
||||
result.HasChanges.ShouldBeTrue();
|
||||
result.RouteUrlsChanged.ShouldBeTrue();
|
||||
result.AddedRouteUrls.ShouldContain("nats://server1:6222");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user