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:
Joseph Doherty
2026-02-25 11:54:52 -05:00
parent 684254ad86
commit 5fd23571dc
2 changed files with 273 additions and 0 deletions

View File

@@ -7,6 +7,36 @@ using NATS.Server.Subscriptions;
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>
/// Controls retry timing for outbound gateway reconnections using exponential backoff with jitter.
/// 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 HashSet<string> _discoveredGateways = 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 CancellationTokenSource? _cts;
@@ -194,6 +225,85 @@ public sealed class GatewayManager : IAsyncDisposable
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()
{
if (_cts == null)