feat: add gateway connection registration with state tracking (Gap 11.7)
Adds GatewayConnectionState enum, GatewayRegistration record with atomic message counters, and a full registry API on GatewayManager: RegisterGateway, UpdateState, GetRegistration, GetAllRegistrations, UnregisterGateway, GetConnectedGatewayCount, IncrementMessagesSent, IncrementMessagesReceived. Covers 11 new tests in GatewayRegistrationTests.cs (all passing).
This commit is contained in:
@@ -7,6 +7,36 @@ using NATS.Server.Subscriptions;
|
|||||||
|
|
||||||
namespace NATS.Server.Gateways;
|
namespace NATS.Server.Gateways;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lifecycle states for a registered gateway connection.
|
||||||
|
/// Go reference: server/gateway.go gwConnState / gwReplyMapping.
|
||||||
|
/// </summary>
|
||||||
|
public enum GatewayConnectionState
|
||||||
|
{
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Draining,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks the registration and runtime metrics for a single named gateway connection.
|
||||||
|
/// Go reference: server/gateway.go srvGateway / outboundGateway structs.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GatewayRegistration
|
||||||
|
{
|
||||||
|
internal long _messagesSent;
|
||||||
|
internal long _messagesReceived;
|
||||||
|
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public GatewayConnectionState State { get; set; } = GatewayConnectionState.Connecting;
|
||||||
|
public DateTime ConnectedAtUtc { get; set; }
|
||||||
|
public DateTime? DisconnectedAtUtc { get; set; }
|
||||||
|
public string? RemoteAddress { get; set; }
|
||||||
|
public long MessagesSent { get => Interlocked.Read(ref _messagesSent); set => Interlocked.Exchange(ref _messagesSent, value); }
|
||||||
|
public long MessagesReceived { get => Interlocked.Read(ref _messagesReceived); set => Interlocked.Exchange(ref _messagesReceived, value); }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controls retry timing for outbound gateway reconnections using exponential backoff with jitter.
|
/// Controls retry timing for outbound gateway reconnections using exponential backoff with jitter.
|
||||||
/// Go reference: server/gateway.go solicitGateway / reconnectGateway delay logic.
|
/// Go reference: server/gateway.go solicitGateway / reconnectGateway delay logic.
|
||||||
@@ -44,6 +74,7 @@ public sealed class GatewayManager : IAsyncDisposable
|
|||||||
private readonly ConcurrentDictionary<string, GatewayConnection> _connections = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, GatewayConnection> _connections = new(StringComparer.Ordinal);
|
||||||
private readonly HashSet<string> _discoveredGateways = new(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> _discoveredGateways = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly ConcurrentDictionary<string, int> _reconnectAttempts = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, int> _reconnectAttempts = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentDictionary<string, GatewayRegistration> _registrations = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private long _forwardedJetStreamClusterMessages;
|
private long _forwardedJetStreamClusterMessages;
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
@@ -194,6 +225,85 @@ public sealed class GatewayManager : IAsyncDisposable
|
|||||||
return conn.GetAccountSubscriptions(account);
|
return conn.GetAccountSubscriptions(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Gateway connection registry (Gap 11.7) ────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a new gateway by name, starting in the Connecting state.
|
||||||
|
/// Go reference: server/gateway.go solicitGateway creates outbound entry before dialling.
|
||||||
|
/// </summary>
|
||||||
|
public void RegisterGateway(string name, string? remoteAddress = null)
|
||||||
|
{
|
||||||
|
var reg = new GatewayRegistration
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
State = GatewayConnectionState.Connecting,
|
||||||
|
RemoteAddress = remoteAddress,
|
||||||
|
};
|
||||||
|
_registrations[name] = reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the connection state of a registered gateway.
|
||||||
|
/// Setting Connected stamps ConnectedAtUtc; setting Disconnected stamps DisconnectedAtUtc.
|
||||||
|
/// Go reference: server/gateway.go gwConnState transitions.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateState(string name, GatewayConnectionState state)
|
||||||
|
{
|
||||||
|
if (!_registrations.TryGetValue(name, out var reg)) return;
|
||||||
|
reg.State = state;
|
||||||
|
if (state == GatewayConnectionState.Connected)
|
||||||
|
reg.ConnectedAtUtc = DateTime.UtcNow;
|
||||||
|
else if (state == GatewayConnectionState.Disconnected)
|
||||||
|
reg.DisconnectedAtUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the registration for the named gateway, or null if not registered.
|
||||||
|
/// Go reference: server/gateway.go server.getOutboundGatewayConnection.
|
||||||
|
/// </summary>
|
||||||
|
public GatewayRegistration? GetRegistration(string name)
|
||||||
|
=> _registrations.TryGetValue(name, out var reg) ? reg : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a snapshot of all current gateway registrations.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<GatewayRegistration> GetAllRegistrations()
|
||||||
|
=> [.. _registrations.Values];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the named gateway registration.
|
||||||
|
/// Go reference: server/gateway.go outboundGateway teardown.
|
||||||
|
/// </summary>
|
||||||
|
public void UnregisterGateway(string name)
|
||||||
|
=> _registrations.TryRemove(name, out _);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of gateways currently in the Connected state.
|
||||||
|
/// Go reference: server/gateway.go numOutboundGatewayConnections.
|
||||||
|
/// </summary>
|
||||||
|
public int GetConnectedGatewayCount()
|
||||||
|
=> _registrations.Values.Count(r => r.State == GatewayConnectionState.Connected);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atomically increments the messages-sent counter for the named gateway.
|
||||||
|
/// Go reference: server/gateway.go outboundGateway.msgs.
|
||||||
|
/// </summary>
|
||||||
|
public void IncrementMessagesSent(string name)
|
||||||
|
{
|
||||||
|
if (_registrations.TryGetValue(name, out var reg))
|
||||||
|
Interlocked.Increment(ref reg._messagesSent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atomically increments the messages-received counter for the named gateway.
|
||||||
|
/// Go reference: server/gateway.go inboundGateway.msgs.
|
||||||
|
/// </summary>
|
||||||
|
public void IncrementMessagesReceived(string name)
|
||||||
|
{
|
||||||
|
if (_registrations.TryGetValue(name, out var reg))
|
||||||
|
Interlocked.Increment(ref reg._messagesReceived);
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_cts == null)
|
if (_cts == null)
|
||||||
|
|||||||
163
tests/NATS.Server.Tests/Gateways/GatewayRegistrationTests.cs
Normal file
163
tests/NATS.Server.Tests/Gateways/GatewayRegistrationTests.cs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NATS.Server.Configuration;
|
||||||
|
using NATS.Server.Gateways;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests.Gateways;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for gateway connection registration and state tracking (Gap 11.7).
|
||||||
|
/// Go reference: server/gateway.go srvGateway / outboundGateway struct and gwConnState transitions.
|
||||||
|
/// </summary>
|
||||||
|
public class GatewayRegistrationTests
|
||||||
|
{
|
||||||
|
// Go: server/gateway.go solicitGateway creates an outbound entry before dialling.
|
||||||
|
[Fact]
|
||||||
|
public void RegisterGateway_creates_registration()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
|
||||||
|
manager.GetRegistration("gw-east").ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go initial gwConnState is gwConnecting before the dial completes.
|
||||||
|
[Fact]
|
||||||
|
public void RegisterGateway_starts_in_connecting_state()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
var reg = manager.GetRegistration("gw-east")!;
|
||||||
|
|
||||||
|
reg.State.ShouldBe(GatewayConnectionState.Connecting);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go gwConnState transitions (Connecting → Connected, Connected → Disconnected, etc.).
|
||||||
|
[Fact]
|
||||||
|
public void UpdateState_changes_state()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
|
||||||
|
manager.UpdateState("gw-east", GatewayConnectionState.Connected);
|
||||||
|
|
||||||
|
manager.GetRegistration("gw-east")!.State.ShouldBe(GatewayConnectionState.Connected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go getOutboundGatewayConnection returns nil for unknown names.
|
||||||
|
[Fact]
|
||||||
|
public void GetRegistration_returns_null_for_unknown()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
|
||||||
|
manager.GetRegistration("does-not-exist").ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go server.gateways map stores all configured outbound gateways.
|
||||||
|
[Fact]
|
||||||
|
public void GetAllRegistrations_returns_all()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
manager.RegisterGateway("gw-west");
|
||||||
|
|
||||||
|
var all = manager.GetAllRegistrations();
|
||||||
|
|
||||||
|
all.Count.ShouldBe(2);
|
||||||
|
all.Select(r => r.Name).ShouldContain("gw-east");
|
||||||
|
all.Select(r => r.Name).ShouldContain("gw-west");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go outboundGateway teardown removes entry from server.gateways.
|
||||||
|
[Fact]
|
||||||
|
public void UnregisterGateway_removes_registration()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
|
||||||
|
manager.UnregisterGateway("gw-east");
|
||||||
|
|
||||||
|
manager.GetRegistration("gw-east").ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go numOutboundGatewayConnections counts only fully-connected entries.
|
||||||
|
[Fact]
|
||||||
|
public void GetConnectedGatewayCount_counts_connected_only()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
manager.RegisterGateway("gw-east"); // Connecting
|
||||||
|
manager.RegisterGateway("gw-west"); // Connecting
|
||||||
|
manager.RegisterGateway("gw-south"); // → Connected
|
||||||
|
|
||||||
|
manager.UpdateState("gw-south", GatewayConnectionState.Connected);
|
||||||
|
|
||||||
|
manager.GetConnectedGatewayCount().ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go outboundGateway.msgs.outMsgs incremented per forwarded message.
|
||||||
|
[Fact]
|
||||||
|
public void IncrementMessagesSent_increments()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
|
||||||
|
manager.IncrementMessagesSent("gw-east");
|
||||||
|
manager.IncrementMessagesSent("gw-east");
|
||||||
|
manager.IncrementMessagesSent("gw-east");
|
||||||
|
|
||||||
|
manager.GetRegistration("gw-east")!.MessagesSent.ShouldBe(3L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go inboundGateway.msgs.inMsgs incremented per received message.
|
||||||
|
[Fact]
|
||||||
|
public void IncrementMessagesReceived_increments()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
|
||||||
|
manager.IncrementMessagesReceived("gw-east");
|
||||||
|
manager.IncrementMessagesReceived("gw-east");
|
||||||
|
|
||||||
|
manager.GetRegistration("gw-east")!.MessagesReceived.ShouldBe(2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go outboundGateway.remoteName / remoteIP stored for monitoring.
|
||||||
|
[Fact]
|
||||||
|
public void Registration_stores_remote_address()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
|
||||||
|
manager.RegisterGateway("gw-east", remoteAddress: "10.0.0.1:7222");
|
||||||
|
|
||||||
|
manager.GetRegistration("gw-east")!.RemoteAddress.ShouldBe("10.0.0.1:7222");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go: server/gateway.go gwConnState Connected transition stamps connected-at time.
|
||||||
|
[Fact]
|
||||||
|
public void UpdateState_to_connected_stamps_ConnectedAtUtc()
|
||||||
|
{
|
||||||
|
var manager = BuildManager();
|
||||||
|
manager.RegisterGateway("gw-east");
|
||||||
|
var before = DateTime.UtcNow;
|
||||||
|
|
||||||
|
manager.UpdateState("gw-east", GatewayConnectionState.Connected);
|
||||||
|
|
||||||
|
var after = DateTime.UtcNow;
|
||||||
|
var stamp = manager.GetRegistration("gw-east")!.ConnectedAtUtc;
|
||||||
|
stamp.ShouldBeGreaterThanOrEqualTo(before);
|
||||||
|
stamp.ShouldBeLessThanOrEqualTo(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static GatewayManager BuildManager() =>
|
||||||
|
new GatewayManager(
|
||||||
|
new GatewayOptions { Name = "TEST", Host = "127.0.0.1", Port = 0 },
|
||||||
|
new ServerStats(),
|
||||||
|
"S1",
|
||||||
|
_ => { },
|
||||||
|
_ => { },
|
||||||
|
NullLogger<GatewayManager>.Instance);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user