feat(batch25): merge gateways

This commit is contained in:
Joseph Doherty
2026-03-01 02:31:54 -05:00
23 changed files with 2288 additions and 26 deletions

View File

@@ -358,6 +358,12 @@ public sealed partial class Account : INatsAccount
/// </summary>
internal byte[]? ServiceImportReply { get; set; }
/// <summary>
/// Gateway reply mapping table used for routed reply restoration.
/// Mirrors Go <c>gwReplyMapping</c>.
/// </summary>
internal GwReplyMapping GwReplyMapping { get; } = new();
/// <summary>
/// Subscription ID counter for internal use.
/// Mirrors Go <c>isid uint64</c>.

View File

@@ -0,0 +1,224 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Text;
using System.Text.Json;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class ClientConnection
{
internal void SendMsgToGateways(Account? account, byte[] subject, byte[]? reply, byte[] payload)
{
if (Server is not NatsServer server || account == null || subject.Length == 0)
return;
var outboundGateways = server.GetOutboundGatewayConnections();
if (outboundGateways.Count == 0)
return;
foreach (var gateway in outboundGateways)
{
if (!GatewayInterest(account, Encoding.ASCII.GetString(subject)))
continue;
var replyToSend = reply;
if (reply is { Length: > 0 } && gateway.Gateway != null)
{
var shouldMap = server.ShouldMapReplyForGatewaySend(reply, gateway.Gateway.UseOldPrefix);
if (shouldMap)
replyToSend = reply;
}
var proto = MsgHeader(subject, replyToSend, new Internal.Subscription { Sid = Encoding.ASCII.GetBytes("1") });
gateway.EnqueueProto(proto);
gateway.EnqueueProto(payload);
gateway.EnqueueProto(Encoding.ASCII.GetBytes("\r\n"));
}
}
internal void SendAccountUnsubToGateway(byte[] accountName)
{
if (accountName.Length == 0)
return;
lock (_mu)
{
Gateway ??= new Gateway();
Gateway.InSim ??= new Dictionary<string, InSide>(StringComparer.Ordinal);
var key = Encoding.ASCII.GetString(accountName);
if (!Gateway.InSim.TryGetValue(key, out var entry) || entry != null)
{
Gateway.InSim[key] = null!;
var proto = Encoding.ASCII.GetBytes($"A- {key}\r\n");
EnqueueProto(proto);
if (Trace)
TraceOutOp(string.Empty, proto.AsSpan(0, proto.Length - 2).ToArray());
}
}
}
internal bool HandleGatewayReply(byte[] msg)
{
if (Server is not NatsServer server || ParseCtx.Pa.Subject is not { Length: > 0 } originalSubject)
return false;
var (isRoutedReply, isOldPrefix) = GatewayHandler.IsGWRoutedSubjectAndIsOldPrefix(originalSubject);
if (!isRoutedReply)
return false;
ParseCtx.Pa.Subject = GatewayHandler.GetSubjectFromGWRoutedReply(originalSubject, isOldPrefix);
ParseCtx.Pa.PaCache = [.. ParseCtx.Pa.Account ?? [], (byte)' ', .. ParseCtx.Pa.Subject];
var (account, result) = GetAccAndResultFromCache();
if (account is not Account concreteAccount)
{
Debugf("Unknown account {0} for gateway message on subject: {1}",
ParseCtx.Pa.Account is { Length: > 0 } accountName ? Encoding.ASCII.GetString(accountName) : string.Empty,
Encoding.ASCII.GetString(ParseCtx.Pa.Subject));
if (ParseCtx.Pa.Account is { Length: > 0 } gatewayAccount)
server.GatewayHandleAccountNoInterest(this, gatewayAccount);
return true;
}
if (result != null && (result.PSubs.Count + result.QSubs.Count) > 0)
ProcessMsgResults(concreteAccount, result, msg, null, ParseCtx.Pa.Subject, ParseCtx.Pa.Reply, PmrFlags.None);
if (!IsServiceReply(ParseCtx.Pa.Subject))
SendMsgToGateways(concreteAccount, ParseCtx.Pa.Subject, ParseCtx.Pa.Reply, msg);
return true;
}
internal void ProcessInboundGatewayMsg(byte[] msg)
{
_in.Msgs++;
_in.Bytes += Math.Max(0, msg.Length - 2);
if (Opts.Verbose)
SendOK();
if (Server is not NatsServer server || ParseCtx.Pa.Subject is not { Length: > 0 })
return;
if (HandleGatewayReply(msg))
return;
var (account, result) = GetAccAndResultFromCache();
if (account is not Account concreteAccount)
{
Debugf("Unknown account {0} for gateway message on subject: {1}",
ParseCtx.Pa.Account is { Length: > 0 } accountName ? Encoding.ASCII.GetString(accountName) : string.Empty,
Encoding.ASCII.GetString(ParseCtx.Pa.Subject));
if (ParseCtx.Pa.Account is { Length: > 0 } gatewayAccount)
server.GatewayHandleAccountNoInterest(this, gatewayAccount);
return;
}
var noInterest = result == null || result.PSubs.Count == 0;
if (noInterest)
{
server.GatewayHandleSubjectNoInterest(this, concreteAccount, ParseCtx.Pa.Account ?? Encoding.ASCII.GetBytes(concreteAccount.Name), ParseCtx.Pa.Subject);
if (ParseCtx.Pa.Queues is null || ParseCtx.Pa.Queues.Count == 0)
return;
}
ProcessMsgResults(concreteAccount, result, msg, null, ParseCtx.Pa.Subject, ParseCtx.Pa.Reply, PmrFlags.None);
}
internal void GatewayAllSubsReceiveStart(ServerInfo info)
{
var account = GatewayHandler.GetAccountFromGatewayCommand(this, info, "start");
if (string.IsNullOrWhiteSpace(account))
return;
Debugf("Gateway {0}: switching account {1} to {2} mode",
info.Gateway ?? string.Empty, account, GatewayInterestMode.InterestOnly.String());
Gateway ??= new Gateway();
Gateway.OutSim ??= new System.Collections.Concurrent.ConcurrentDictionary<string, OutSide>(StringComparer.Ordinal);
var outSide = Gateway.OutSim.GetOrAdd(account, _ => new OutSide
{
Sl = Internal.DataStructures.SubscriptionIndex.NewSublistWithCache(),
});
outSide.AcquireWriteLock();
try { outSide.Mode = GatewayInterestMode.Transitioning; }
finally { outSide.ReleaseWriteLock(); }
}
internal void GatewayAllSubsReceiveComplete(ServerInfo info)
{
var account = GatewayHandler.GetAccountFromGatewayCommand(this, info, "complete");
if (string.IsNullOrWhiteSpace(account))
return;
if (Gateway?.OutSim == null || !Gateway.OutSim.TryGetValue(account, out var outSide))
return;
outSide.AcquireWriteLock();
try
{
outSide.Ni = null;
outSide.Mode = GatewayInterestMode.InterestOnly;
}
finally
{
outSide.ReleaseWriteLock();
}
Debugf("Gateway {0}: switching account {1} to {2} mode complete",
info.Gateway ?? string.Empty, account, GatewayInterestMode.InterestOnly.String());
}
internal void GatewaySwitchAccountToSendAllSubs(InSide inSide, string accountName)
{
if (Server is not NatsServer server || string.IsNullOrWhiteSpace(accountName))
return;
inSide.Ni = null;
inSide.Mode = GatewayInterestMode.Transitioning;
var remoteGatewayName = Gateway?.Name ?? string.Empty;
Debugf("Gateway {0}: switching account {1} to {2} mode",
remoteGatewayName, accountName, GatewayInterestMode.InterestOnly.String());
void SendCommand(byte command, bool withLock)
{
var info = new ServerInfo
{
Gateway = server.GetGatewayName(),
GatewayCmd = command,
GatewayCmdPayload = Encoding.ASCII.GetBytes(accountName),
};
var infoProto = NatsServer.GenerateInfoJson(info);
if (withLock)
{
lock (_mu)
{
EnqueueProto(infoProto);
}
return;
}
EnqueueProto(infoProto);
}
SendCommand(GatewayHandler.GatewayCmdAllSubsStart, withLock: false);
_ = server.StartGoRoutine(() =>
{
server.SendAccountSubsToGateway(this, accountName);
SendCommand(GatewayHandler.GatewayCmdAllSubsComplete, withLock: true);
Debugf("Gateway {0}: switching account {1} to {2} mode complete",
remoteGatewayName, accountName, GatewayInterestMode.InterestOnly.String());
});
}
}

View File

@@ -0,0 +1,217 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Text;
using System.Text.Json;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class ClientConnection
{
internal Exception? SendGatewayConnect()
{
if (Server is not NatsServer server)
return new InvalidOperationException("gateway server unavailable");
var opts = server.GetOpts();
var connect = new ConnectInfo
{
Echo = true,
Verbose = false,
Pedantic = false,
User = opts.Gateway.Username,
Pass = opts.Gateway.Password,
Tls = opts.Gateway.TlsConfig != null,
Name = server.ID(),
Headers = server.SupportsHeaders(),
Gateway = opts.Gateway.Name,
Dynamic = server.IsClusterNameDynamic(),
};
var payload = JsonSerializer.Serialize(connect);
EnqueueProto(Encoding.ASCII.GetBytes($"CONNECT {payload}\r\n"));
return null;
}
internal Exception? ProcessGatewayConnect(byte[] arg)
{
ConnectInfo? connect;
try
{
connect = JsonSerializer.Deserialize<ConnectInfo>(arg);
}
catch (Exception ex)
{
return ex;
}
if (connect == null)
return new InvalidOperationException("invalid gateway connect payload");
Gateway ??= new Gateway();
Gateway.Connected = true;
if (!string.IsNullOrWhiteSpace(connect.Gateway))
Gateway.Name = connect.Gateway;
if (!string.IsNullOrWhiteSpace(connect.Name))
Gateway.RemoteName = connect.Name;
Headers = connect.Headers;
return null;
}
internal Exception? ProcessGatewayInfo(byte[] arg)
{
ServerInfo? info;
try
{
info = JsonSerializer.Deserialize<ServerInfo>(arg);
}
catch (Exception ex)
{
return ex;
}
if (info == null)
return new InvalidOperationException("invalid gateway info payload");
return ProcessGatewayInfo(info);
}
internal Exception? ProcessGatewayInfo(ServerInfo info)
{
if (Server is not NatsServer server)
return new InvalidOperationException("gateway server unavailable");
Gateway ??= new Gateway();
if (!string.IsNullOrWhiteSpace(info.Gateway))
Gateway.Name = info.Gateway;
if (!string.IsNullOrWhiteSpace(info.GatewayUrl) && Uri.TryCreate(info.GatewayUrl, UriKind.Absolute, out var url))
Gateway.ConnectUrl = url;
Gateway.UseOldPrefix = info.GatewayNrp;
Gateway.InterestOnlyMode = info.GatewayIom;
if (info.GatewayUrls is { Length: > 0 })
server.ProcessImplicitGateway(Gateway.Name, info.GatewayUrls);
return null;
}
internal Exception? ProcessGatewayAccountUnsub(byte[] arg)
{
if (Server is not NatsServer server)
return new InvalidOperationException("gateway server unavailable");
var tokens = Encoding.ASCII.GetString(arg).Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length < 1)
return null;
if (tokens.Length == 1)
return null;
var accountName = tokens[0];
var subject = Encoding.ASCII.GetBytes(tokens[1]);
var queue = tokens.Length > 2 ? Encoding.ASCII.GetBytes(tokens[2]) : null;
var (account, _) = server.LookupAccount(accountName);
if (account == null)
return null;
var sub = new Internal.Subscription { Subject = subject, Queue = queue };
server.SendQueueSubOrUnsubToGateways(account, sub, isUnsub: true);
return null;
}
internal Exception? ProcessGatewayAccountSub(byte[] arg)
{
if (Server is not NatsServer server)
return new InvalidOperationException("gateway server unavailable");
var tokens = Encoding.ASCII.GetString(arg).Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length < 2)
return null;
var accountName = tokens[0];
var subject = Encoding.ASCII.GetBytes(tokens[1]);
var queue = tokens.Length > 2 ? Encoding.ASCII.GetBytes(tokens[2]) : null;
var (account, _) = server.LookupAccount(accountName);
if (account == null)
return null;
var sub = new Internal.Subscription { Subject = subject, Queue = queue };
if (queue is { Length: > 0 })
server.SendQueueSubOrUnsubToGateways(account, sub, isUnsub: false);
else
server.MaybeSendSubOrUnsubToGateways(account, sub, isUnsub: false);
return null;
}
internal Exception? ProcessGatewayRUnsub(byte[] arg)
{
var tokens = Encoding.ASCII.GetString(arg).Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length < 2 || Server is not NatsServer server)
return null;
var accountName = tokens[0];
var subject = Encoding.ASCII.GetBytes(tokens[1]);
var queue = tokens.Length > 2 ? Encoding.ASCII.GetBytes(tokens[2]) : null;
var (account, _) = server.LookupAccount(accountName);
if (account == null)
return null;
var sub = new Internal.Subscription { Subject = subject, Queue = queue };
server.MaybeSendSubOrUnsubToGateways(account, sub, isUnsub: true);
return null;
}
internal Exception? ProcessGatewayRSub(byte[] arg)
{
var tokens = Encoding.ASCII.GetString(arg).Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length < 2 || Server is not NatsServer server)
return null;
var accountName = tokens[0];
var subject = Encoding.ASCII.GetBytes(tokens[1]);
var queue = tokens.Length > 2 ? Encoding.ASCII.GetBytes(tokens[2]) : null;
var (account, _) = server.LookupAccount(accountName);
if (account == null)
return null;
var sub = new Internal.Subscription { Subject = subject, Queue = queue };
server.MaybeSendSubOrUnsubToGateways(account, sub, isUnsub: false);
return null;
}
internal bool GatewayInterest(Account? account, string subject)
{
if (account == null || string.IsNullOrWhiteSpace(subject))
return false;
if (Gateway?.OutSim == null)
return true;
if (!Gateway.OutSim.TryGetValue(account.Name, out var outSi))
return true;
outSi.AcquireReadLock();
try
{
if (outSi.Mode == GatewayInterestMode.InterestOnly && outSi.Sl != null)
{
var match = outSi.Sl.Match(subject);
return match.PSubs.Count > 0 || match.QSubs.Count > 0;
}
return outSi.Ni == null || !outSi.Ni.Contains(subject);
}
finally
{
outSi.ReleaseReadLock();
}
}
}

View File

@@ -114,6 +114,7 @@ public sealed partial class ClientConnection
// Client options (from CONNECT message).
internal ClientOptions Opts = ClientOptions.Default;
internal Route? Route;
internal Gateway? Gateway;
internal WebsocketConnection? Ws;
// Flags and state.
@@ -134,6 +135,7 @@ public sealed partial class ClientConnection
internal Dictionary<string, RespEntry>? Replies;
internal Dictionary<ClientConnection, bool>? Pcd; // pending clients with data to flush
internal Dictionary<string, bool>? DArray; // denied subscribe patterns
internal GwReplyMapping GwReplyMapping = new();
// Outbound state (simplified — full write loop ported when Server is available).
internal long OutPb; // pending bytes

View File

@@ -0,0 +1,142 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Security.Cryptography;
using System.Text;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
internal static class GatewayHandler
{
internal static readonly byte[] GwReplyPrefix = Encoding.ASCII.GetBytes("_GR_.");
internal static readonly byte[] OldGwReplyPrefix = Encoding.ASCII.GetBytes("$GR.");
internal const int GwHashLen = 6;
internal const int GwClusterOffset = 5;
internal const int GwServerOffset = GwClusterOffset + GwHashLen + 1;
internal const int GwSubjectOffset = GwServerOffset + GwHashLen + 1;
internal const int OldGwReplyPrefixLen = 4;
internal const int OldGwReplyStart = OldGwReplyPrefixLen + 5;
internal const int GatewayCmdAllSubsStart = 2;
internal const int GatewayCmdAllSubsComplete = 3;
internal const int GatewayMaxRUnsubBeforeSwitch = 1000;
private static readonly TimeSpan DefaultSolicitGatewaysDelay = TimeSpan.FromSeconds(1);
private static long _gatewaySolicitDelayTicks = DefaultSolicitGatewaysDelay.Ticks;
private static int _doNotForceInterestOnlyMode;
internal static void SetGatewaysSolicitDelay(TimeSpan delay)
{
Interlocked.Exchange(ref _gatewaySolicitDelayTicks, delay.Ticks);
}
internal static void ResetGatewaysSolicitDelay()
{
Interlocked.Exchange(ref _gatewaySolicitDelayTicks, DefaultSolicitGatewaysDelay.Ticks);
}
internal static TimeSpan GetGatewaysSolicitDelay()
{
return TimeSpan.FromTicks(Interlocked.Read(ref _gatewaySolicitDelayTicks));
}
internal static void GatewayDoNotForceInterestOnlyMode(bool doNotForce)
{
Interlocked.Exchange(ref _doNotForceInterestOnlyMode, doNotForce ? 1 : 0);
}
internal static bool DoNotForceInterestOnlyMode()
{
return Interlocked.CompareExchange(ref _doNotForceInterestOnlyMode, 0, 0) != 0;
}
internal static Exception? ValidateGatewayOptions(ServerOptions options)
{
var gateway = options.Gateway;
if (string.IsNullOrWhiteSpace(gateway.Name) || gateway.Port == 0)
return null;
if (gateway.Name.Contains(' '))
return ServerErrors.ErrGatewayNameHasSpaces;
var names = new HashSet<string>(StringComparer.Ordinal);
foreach (var remote in gateway.Gateways)
{
if (string.IsNullOrWhiteSpace(remote.Name))
return new InvalidOperationException("gateway remote requires a name");
if (!names.Add(remote.Name))
return new InvalidOperationException($"duplicate gateway remote: {remote.Name}");
if (remote.Urls.Count == 0)
return new InvalidOperationException($"gateway remote {remote.Name} has no URLs");
}
return null;
}
internal static byte[] GetGWHash(string value) => GetHash(value, 6);
internal static byte[] GetOldHash(string value) => GetHash(value, 4);
internal static byte[] GwBuildSubProto(string accountName, Subscription subscription, bool isUnsub = false)
{
var op = isUnsub ? "RS-" : "RS+";
var subject = Encoding.ASCII.GetString(subscription.Subject);
if (subscription.Queue is { Length: > 0 } queue)
return Encoding.ASCII.GetBytes($"{op} {accountName} {subject} {Encoding.ASCII.GetString(queue)}\r\n");
return Encoding.ASCII.GetBytes($"{op} {accountName} {subject}\r\n");
}
internal static bool IsGWRoutedReply(byte[] subject)
{
return subject.AsSpan().StartsWith(GwReplyPrefix) || subject.AsSpan().StartsWith(OldGwReplyPrefix);
}
internal static (bool IsRouted, bool IsOldPrefix) IsGWRoutedSubjectAndIsOldPrefix(byte[] subject)
{
if (subject.AsSpan().StartsWith(OldGwReplyPrefix))
return (true, true);
if (subject.AsSpan().StartsWith(GwReplyPrefix))
return (true, false);
return (false, false);
}
internal static bool HasGWRoutedReplyPrefix(byte[] subject)
{
return IsGWRoutedReply(subject);
}
internal static byte[] GetSubjectFromGWRoutedReply(byte[] reply, bool isOldPrefix)
{
if (isOldPrefix)
return reply.Length > OldGwReplyStart ? reply[OldGwReplyStart..] : [];
return reply.Length > GwSubjectOffset ? reply[GwSubjectOffset..] : [];
}
internal static string GetAccountFromGatewayCommand(ClientConnection connection, ServerInfo info, string command)
{
if (info.GatewayCmdPayload == null || info.GatewayCmdPayload.Length == 0)
{
connection.SendErrAndErr($"Account absent from receive-all-subscriptions-{command} command");
connection.CloseConnection(ClosedState.ProtocolViolation);
return string.Empty;
}
return Encoding.ASCII.GetString(info.GatewayCmdPayload);
}
private static byte[] GetHash(string value, int len)
{
var bytes = Encoding.UTF8.GetBytes(value);
var hash = SHA256.HashData(bytes);
var encoded = Convert.ToBase64String(hash)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
return Encoding.ASCII.GetBytes(encoded[..len]);
}
}

View File

@@ -13,7 +13,9 @@
//
// Adapted from server/gateway.go in the NATS server Go source.
using System.Text;
using System.Threading;
using System.Linq;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
@@ -54,6 +56,20 @@ public enum GatewayInterestMode : byte
CacheFlushed = 3,
}
internal static class GatewayInterestModeExtensions
{
internal static string String(this GatewayInterestMode mode)
{
return mode switch
{
GatewayInterestMode.Optimistic => "Optimistic",
GatewayInterestMode.InterestOnly => "Interest-Only",
GatewayInterestMode.Transitioning => "Transitioning",
_ => "Unknown",
};
}
}
/// <summary>
/// Server-level gateway state kept on the <see cref="NatsServer"/> instance.
/// Replaces the stub that was in <c>NatsServerTypes.cs</c>.
@@ -130,6 +146,71 @@ internal sealed class SrvGateway
public Lock PasiLock => _pasiLock;
internal byte[] GenerateInfoJSON()
{
Info ??= new ServerInfo();
Info.Gateway = Name;
Info.GatewayUrl = Url;
Info.GatewayUrls = [.. Urls.GetAsStringSlice()];
InfoJson = NatsServer.GenerateInfoJson(Info);
return InfoJson;
}
internal bool HasInbound(string gatewayName)
{
if (string.IsNullOrWhiteSpace(gatewayName))
return false;
_lock.EnterReadLock();
try
{
foreach (var inbound in In.Values)
{
if (string.Equals(inbound.Gateway?.Name, gatewayName, StringComparison.Ordinal))
return true;
}
}
finally
{
_lock.ExitReadLock();
}
return false;
}
internal void UpdateRemotesTLSConfig(IReadOnlyList<RemoteGatewayOpts> remotes)
{
if (remotes.Count == 0)
return;
_lock.EnterWriteLock();
try
{
foreach (var remote in remotes)
{
if (!Remotes.TryGetValue(remote.Name, out var cfg))
continue;
cfg.AcquireWriteLock();
try
{
cfg.RemoteOpts ??= remote.Clone();
cfg.RemoteOpts.TlsConfig = remote.TlsConfig;
cfg.RemoteOpts.TlsTimeout = remote.TlsTimeout;
cfg.RemoteOpts.TlsConfigOpts = remote.TlsConfigOpts;
}
finally
{
cfg.ReleaseWriteLock();
}
}
}
finally
{
_lock.ExitWriteLock();
}
}
// -------------------------------------------------------------------------
// Recent subscription tracking (thread-safe map)
// -------------------------------------------------------------------------
@@ -176,6 +257,46 @@ internal sealed class SrvGateway
public void ReleaseReadLock() => _lock.ExitReadLock();
public void AcquireWriteLock() => _lock.EnterWriteLock();
public void ReleaseWriteLock() => _lock.ExitWriteLock();
internal void OrderOutboundConnectionsLocked()
{
Outo = [.. Out.Values.OrderBy(c => c.GetRttValue()).ThenBy(c => c.Cid)];
}
internal void OrderOutboundConnections()
{
_lock.EnterWriteLock();
try { OrderOutboundConnectionsLocked(); }
finally { _lock.ExitWriteLock(); }
}
internal bool ShouldMapReplyForGatewaySend(byte[] reply, bool useOldPrefix)
{
if (reply.Length == 0)
return false;
if (useOldPrefix)
return !GatewayHandler.IsGWRoutedSubjectAndIsOldPrefix(reply).IsRouted;
return !GatewayHandler.IsGWRoutedReply(reply);
}
internal byte[] GetClusterHash()
{
_lock.EnterReadLock();
try
{
if (ReplyPfx.Length < GatewayHandler.GwClusterOffset + GatewayHandler.GwHashLen)
return [];
return ReplyPfx[GatewayHandler.GwClusterOffset..(GatewayHandler.GwClusterOffset + GatewayHandler.GwHashLen)];
}
finally
{
_lock.ExitReadLock();
}
}
}
/// <summary>
@@ -219,6 +340,7 @@ internal sealed class GatewayCfg
/// <summary>TLS server name override for SNI.</summary>
public string TlsName { get; set; } = string.Empty;
/// <summary>TLS server name override for SNI.</summary>
/// <summary>True if this remote was discovered via gossip (not configured).</summary>
public bool Implicit { get; set; }
@@ -228,6 +350,81 @@ internal sealed class GatewayCfg
// Forwarded properties from RemoteGatewayOpts
public string Name { get => RemoteOpts?.Name ?? string.Empty; }
internal void BumpConnAttempts()
{
_lock.EnterWriteLock();
try { ConnAttempts++; }
finally { _lock.ExitWriteLock(); }
}
internal int GetConnAttempts()
{
_lock.EnterReadLock();
try { return ConnAttempts; }
finally { _lock.ExitReadLock(); }
}
internal void ResetConnAttempts()
{
_lock.EnterWriteLock();
try { ConnAttempts = 0; }
finally { _lock.ExitWriteLock(); }
}
internal bool IsImplicit()
{
_lock.EnterReadLock();
try { return Implicit; }
finally { _lock.ExitReadLock(); }
}
internal IReadOnlyCollection<Uri> GetUrls()
{
_lock.EnterReadLock();
try { return [.. Urls.Values]; }
finally { _lock.ExitReadLock(); }
}
internal string[] GetUrlsAsStrings()
{
_lock.EnterReadLock();
try { return [.. Urls.Keys]; }
finally { _lock.ExitReadLock(); }
}
internal void UpdateUrls(IEnumerable<Uri> urls)
{
_lock.EnterWriteLock();
try
{
Urls.Clear();
foreach (var url in urls)
Urls[url.ToString()] = url;
}
finally { _lock.ExitWriteLock(); }
}
internal void SaveTLSHostname(Uri url)
{
if (string.IsNullOrWhiteSpace(url.Host))
return;
_lock.EnterWriteLock();
try { TlsName = url.Host; }
finally { _lock.ExitWriteLock(); }
}
internal void AddUrls(IEnumerable<Uri> urls)
{
_lock.EnterWriteLock();
try
{
foreach (var url in urls)
Urls[url.ToString()] = url;
}
finally { _lock.ExitWriteLock(); }
}
// -------------------------------------------------------------------------
// Lock helpers
// -------------------------------------------------------------------------
@@ -378,7 +575,24 @@ internal sealed class GwReplyMapping
/// </summary>
public (byte[] Subject, bool Found) Get(byte[] subject)
{
// TODO: session 16 — implement mapping lookup
return (subject, false);
var key = Encoding.UTF8.GetString(subject);
if (!Mapping.TryGetValue(key, out var entry))
return (subject, false);
if (entry.Exp <= DateTime.UtcNow.Ticks)
{
Mapping.Remove(key);
return (subject, false);
}
return (Encoding.UTF8.GetBytes(entry.Ms), true);
}
}
internal static class RemoteGatewayOptsExtensions
{
internal static RemoteGatewayOpts Clone(this RemoteGatewayOpts source)
{
return source.Clone();
}
}

View File

@@ -0,0 +1,230 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class NatsServer
{
internal Exception? NewGateway(ServerOptions options)
{
var validationErr = GatewayHandler.ValidateGatewayOptions(options);
if (validationErr != null)
return validationErr;
if (string.IsNullOrWhiteSpace(options.Gateway.Name) || options.Gateway.Port == 0)
return null;
_gateway.AcquireWriteLock();
try
{
_gateway.Enabled = true;
_gateway.Name = options.Gateway.Name;
_gateway.RejectUnknown = options.Gateway.RejectUnknown;
_gateway.Remotes.Clear();
_gateway.Out.Clear();
_gateway.Outo.Clear();
_gateway.In.Clear();
_gateway.OwnCfgUrls = [];
foreach (var remote in options.Gateway.Gateways)
{
var cfg = new GatewayCfg
{
RemoteOpts = remote.Clone(),
Hash = GatewayHandler.GetGWHash(remote.Name),
OldHash = GatewayHandler.GetOldHash(remote.Name),
Urls = remote.Urls.ToDictionary(u => u.ToString(), u => u, StringComparer.Ordinal),
};
_gateway.Remotes[remote.Name] = cfg;
foreach (var url in remote.Urls)
_gateway.OwnCfgUrls.Add(url.ToString());
}
var info = CopyInfo();
info.Gateway = options.Gateway.Name;
info.GatewayUrl = _gateway.Url;
info.GatewayUrls = [.. _gateway.Urls.GetAsStringSlice()];
_gateway.Info = info;
_gateway.InfoJson = GenerateInfoJson(info);
_gateway.SIdHash = GatewayHandler.GetGWHash(_info.Id);
_gateway.OldHash = GatewayHandler.GetOldHash(_info.Id);
var clusterHash = GatewayHandler.GetGWHash(options.Gateway.Name);
_gateway.ReplyPfx = Encoding.ASCII.GetBytes($"_GR_.{Encoding.ASCII.GetString(clusterHash)}.{Encoding.ASCII.GetString(_gateway.SIdHash)}.");
_gateway.OldReplyPfx = Encoding.ASCII.GetBytes($"$GR.{Encoding.ASCII.GetString(_gateway.OldHash)}.");
}
finally
{
_gateway.ReleaseWriteLock();
}
return null;
}
internal Exception? StartGateways()
{
if (!_gateway.Enabled)
return null;
var hostPortErr = SetGatewayInfoHostPort();
if (hostPortErr != null)
return hostPortErr;
SolicitGateways();
return StartGatewayAcceptLoop();
}
internal Exception? StartGatewayAcceptLoop()
{
if (_gatewayListener == null)
{
var opts = GetOpts();
var hp = $"{opts.Gateway.Host}:{opts.Gateway.Port}";
_mu.EnterWriteLock();
try
{
var parts = hp.Split(':', 2);
var host = parts[0];
var port = parts.Length > 1 ? int.Parse(parts[1]) : 0;
var addr = string.IsNullOrWhiteSpace(host) || host == "0.0.0.0"
? IPAddress.Any
: (host == "::" ? IPAddress.IPv6Any : IPAddress.Parse(host));
_gatewayListener = new TcpListener(addr, port);
_gatewayListener.Start();
}
catch (Exception ex)
{
_gatewayListenerErr = ex;
return ex;
}
finally
{
_mu.ExitWriteLock();
}
}
if (!StartGoRoutine(() => Noticef("Gateway accept loop started")))
return new InvalidOperationException("unable to start gateway accept loop");
return null;
}
internal Exception? SetGatewayInfoHostPort()
{
var opts = GetOpts();
var host = opts.Gateway.Host;
var port = opts.Gateway.Port;
if (!string.IsNullOrWhiteSpace(opts.Gateway.Advertise))
{
var (advHost, advPort, advErr) = Internal.ServerUtilities.ParseHostPort(opts.Gateway.Advertise, port);
if (advErr != null)
return advErr;
host = advHost;
port = advPort;
}
var scheme = opts.Gateway.TlsConfig != null ? "tls" : "nats";
var url = $"{scheme}://{host}:{port}";
_gateway.AcquireWriteLock();
try
{
_gateway.Url = url;
_gateway.Info ??= CopyInfo();
_gateway.Info.Gateway = opts.Gateway.Name;
_gateway.Info.GatewayUrl = url;
_gateway.Info.GatewayUrls = [.. _gateway.Urls.GetAsStringSlice()];
_gateway.InfoJson = GenerateInfoJson(_gateway.Info);
}
finally
{
_gateway.ReleaseWriteLock();
}
return null;
}
internal void SolicitGateways()
{
if (!_gateway.Enabled)
return;
var delay = GatewayHandler.GetGatewaysSolicitDelay();
if (delay > TimeSpan.Zero)
Thread.Sleep(delay);
List<GatewayCfg> remotes;
_gateway.AcquireReadLock();
try
{
remotes = [.. _gateway.Remotes.Values];
}
finally
{
_gateway.ReleaseReadLock();
}
foreach (var cfg in remotes)
SolicitGateway(cfg, firstConnect: true);
}
internal void ReconnectGateway(GatewayCfg cfg)
{
SolicitGateway(cfg, firstConnect: false);
}
internal void SolicitGateway(GatewayCfg cfg, bool firstConnect)
{
_ = firstConnect;
if (cfg.RemoteOpts == null || cfg.RemoteOpts.Urls.Count == 0)
return;
if (_gateway.HasInbound(cfg.RemoteOpts.Name))
return;
CreateGateway(cfg, cfg.RemoteOpts.Urls[0]);
}
internal ClientConnection? CreateGateway(GatewayCfg cfg, Uri? url = null)
{
if (cfg.RemoteOpts == null)
return null;
var connection = new ClientConnection(ClientKind.Gateway, this)
{
Gateway = new Gateway
{
Name = cfg.RemoteOpts.Name,
Cfg = cfg,
ConnectUrl = url,
Outbound = true,
},
};
_gateway.AcquireWriteLock();
try
{
_gateway.Out[cfg.RemoteOpts.Name] = connection;
_gateway.Outo = [.. _gateway.Out.Values];
}
finally
{
_gateway.ReleaseWriteLock();
}
return connection;
}
}

View File

@@ -0,0 +1,296 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Text;
using System.Linq;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class NatsServer
{
internal void GossipGatewaysToInboundGateway(ClientConnection inboundGateway)
{
if (_gateway.Info == null)
return;
var info = _gateway.Info.ShallowClone();
info.GatewayCmd = 1;
info.GatewayUrls = [.. _gateway.Urls.GetAsStringSlice()];
inboundGateway.EnqueueProto(GenerateInfoJson(info));
}
internal void ForwardNewGatewayToLocalCluster(string gatewayName, IReadOnlyCollection<string> gatewayUrls)
{
if (gatewayUrls.Count == 0)
return;
var info = _routeInfo.ShallowClone();
info.Gateway = gatewayName;
info.GatewayUrls = [.. gatewayUrls];
var proto = GenerateInfoJson(info);
_mu.EnterReadLock();
try
{
ForEachRoute(route => route.EnqueueProto(proto));
foreach (var inbound in _gateway.In.Values)
inbound.EnqueueProto(proto);
}
finally
{
_mu.ExitReadLock();
}
}
internal void SendQueueSubsToGateway(ClientConnection gateway, string accountName)
{
SendAccountSubsToGateway(gateway, accountName, sendQueueOnly: true);
}
internal void SendAccountSubsToGateway(ClientConnection gateway, string accountName, bool sendQueueOnly = false)
{
var (account, _) = LookupAccount(accountName);
if (account == null)
return;
SendSubsToGateway(gateway, account, sendQueueOnly);
}
internal static byte[] GwBuildSubProto(string accountName, Subscription subscription, bool isUnsub = false)
{
var op = isUnsub ? "RS-" : "RS+";
var subject = Encoding.ASCII.GetString(subscription.Subject);
if (subscription.Queue is { Length: > 0 } queue)
return Encoding.ASCII.GetBytes($"{op} {accountName} {subject} {Encoding.ASCII.GetString(queue)}\r\n");
return Encoding.ASCII.GetBytes($"{op} {accountName} {subject}\r\n");
}
internal void SendSubsToGateway(ClientConnection gateway, Account account, bool sendQueueOnly)
{
if (account.Sublist == null)
return;
var subs = new List<Subscription>();
account.Sublist.All(subs);
foreach (var sub in subs)
{
if (sendQueueOnly && sub.Queue is not { Length: > 0 })
continue;
gateway.EnqueueProto(GwBuildSubProto(account.Name, sub));
}
}
internal void ProcessGatewayInfoFromRoute(string gatewayName, IReadOnlyCollection<string> gatewayUrls)
{
ProcessImplicitGateway(gatewayName, gatewayUrls);
}
internal void SendGatewayConfigsToRoute(ClientConnection route)
{
_gateway.AcquireReadLock();
try
{
foreach (var (name, cfg) in _gateway.Remotes)
{
var info = _routeInfo.ShallowClone();
info.Gateway = name;
info.GatewayUrls = cfg.GetUrlsAsStrings();
route.EnqueueProto(GenerateInfoJson(info));
}
}
finally
{
_gateway.ReleaseReadLock();
}
}
internal void ProcessImplicitGateway(string gatewayName, IReadOnlyCollection<string> gatewayUrls)
{
if (string.IsNullOrWhiteSpace(gatewayName) || gatewayUrls.Count == 0)
return;
_gateway.AcquireWriteLock();
try
{
if (!_gateway.Remotes.TryGetValue(gatewayName, out var cfg))
{
cfg = new GatewayCfg
{
RemoteOpts = new RemoteGatewayOpts { Name = gatewayName },
Hash = GatewayHandler.GetGWHash(gatewayName),
OldHash = GatewayHandler.GetOldHash(gatewayName),
Implicit = true,
Urls = new Dictionary<string, Uri>(StringComparer.Ordinal),
};
_gateway.Remotes[gatewayName] = cfg;
}
var urls = gatewayUrls
.Select(url => Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri : null)
.Where(uri => uri != null)
.Cast<Uri>()
.ToList();
cfg.AddUrls(urls);
cfg.VarzUpdateUrls = true;
}
finally
{
_gateway.ReleaseWriteLock();
}
}
internal void AddGatewayURL(string gatewayName, string gatewayUrl)
{
if (string.IsNullOrWhiteSpace(gatewayUrl))
return;
_gateway.AcquireWriteLock();
try
{
_gateway.Urls.AddUrl(gatewayUrl);
if (_gateway.Remotes.TryGetValue(gatewayName, out var cfg) && Uri.TryCreate(gatewayUrl, UriKind.Absolute, out var url))
cfg.AddUrls([url]);
}
finally
{
_gateway.ReleaseWriteLock();
}
}
internal void RemoveGatewayURL(string gatewayName, string gatewayUrl)
{
if (string.IsNullOrWhiteSpace(gatewayUrl))
return;
_gateway.AcquireWriteLock();
try
{
_gateway.Urls.RemoveUrl(gatewayUrl);
if (_gateway.Remotes.TryGetValue(gatewayName, out var cfg))
{
cfg.AcquireWriteLock();
try
{
cfg.Urls.Remove(gatewayUrl);
cfg.VarzUpdateUrls = true;
}
finally
{
cfg.ReleaseWriteLock();
}
}
}
finally
{
_gateway.ReleaseWriteLock();
}
}
internal void SendAsyncGatewayInfo()
{
byte[] infoProto;
_gateway.AcquireReadLock();
try
{
infoProto = _gateway.InfoJson is { Length: > 0 } proto
? [.. proto]
: _gateway.GenerateInfoJSON();
}
finally
{
_gateway.ReleaseReadLock();
}
_mu.EnterReadLock();
try
{
foreach (var route in _routes.Values.SelectMany(v => v))
route.EnqueueProto(infoProto);
foreach (var inbound in _gateway.In.Values)
inbound.EnqueueProto(infoProto);
}
finally
{
_mu.ExitReadLock();
}
}
internal string GetGatewayURL()
{
_gateway.AcquireReadLock();
try { return _gateway.Url; }
finally { _gateway.ReleaseReadLock(); }
}
internal string GetGatewayName()
{
_gateway.AcquireReadLock();
try { return _gateway.Name; }
finally { _gateway.ReleaseReadLock(); }
}
internal void RegisterInboundGatewayConnection(ClientConnection connection)
{
_gateway.AcquireWriteLock();
try { _gateway.In[connection.Cid] = connection; }
finally { _gateway.ReleaseWriteLock(); }
}
internal void RegisterOutboundGatewayConnection(string gatewayName, ClientConnection connection)
{
_gateway.AcquireWriteLock();
try
{
_gateway.Out[gatewayName] = connection;
_gateway.OrderOutboundConnectionsLocked();
}
finally
{
_gateway.ReleaseWriteLock();
}
}
internal ClientConnection? GetOutboundGatewayConnection(string gatewayName)
{
_gateway.AcquireReadLock();
try { return _gateway.Out.GetValueOrDefault(gatewayName); }
finally { _gateway.ReleaseReadLock(); }
}
internal IReadOnlyList<ClientConnection> GetOutboundGatewayConnections()
{
_gateway.AcquireReadLock();
try { return [.. _gateway.Outo]; }
finally { _gateway.ReleaseReadLock(); }
}
public int NumOutboundGateways() => NumOutboundGatewaysInternal();
internal int NumOutboundGatewaysInternal()
{
_gateway.AcquireReadLock();
try { return _gateway.Out.Count; }
finally { _gateway.ReleaseReadLock(); }
}
internal int NumInboundGateways()
{
_gateway.AcquireReadLock();
try { return _gateway.In.Count; }
finally { _gateway.ReleaseReadLock(); }
}
internal GatewayCfg? GetRemoteGateway(string gatewayName)
{
_gateway.AcquireReadLock();
try { return _gateway.Remotes.GetValueOrDefault(gatewayName); }
finally { _gateway.ReleaseReadLock(); }
}
}

View File

@@ -0,0 +1,145 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Net;
using System.Text;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class NatsServer
{
internal bool ShouldMapReplyForGatewaySend(byte[] reply, bool useOldPrefix)
{
return _gateway.ShouldMapReplyForGatewaySend(reply, useOldPrefix);
}
internal IReadOnlyList<ClientConnection> GetInboundGatewayConnections()
{
_gateway.AcquireReadLock();
try { return [.. _gateway.In.Values]; }
finally { _gateway.ReleaseReadLock(); }
}
internal string GatewayAddr()
{
var opts = GetOpts();
var host = string.IsNullOrWhiteSpace(opts.Gateway.Host) ? opts.Host : opts.Gateway.Host;
return $"{host}:{opts.Gateway.Port}";
}
internal void SwitchAccountToInterestMode(string accountName)
{
_gateway.AcquireWriteLock();
try
{
foreach (var outbound in _gateway.Out.Values)
{
outbound.Gateway ??= new Gateway();
outbound.Gateway.OutSim ??= new System.Collections.Concurrent.ConcurrentDictionary<string, OutSide>(StringComparer.Ordinal);
outbound.Gateway.OutSim[accountName] = new OutSide
{
Mode = GatewayInterestMode.InterestOnly,
Ni = null,
};
}
}
finally
{
_gateway.ReleaseWriteLock();
}
}
internal void MaybeSendSubOrUnsubToGateways(Account account, Subscription sub, bool isUnsub)
{
if (sub.Subject.Length == 0)
return;
_gateway.AcquireReadLock();
try
{
foreach (var gateway in _gateway.Out.Values)
gateway.EnqueueProto(GatewayHandler.GwBuildSubProto(account.Name, sub, isUnsub));
}
finally
{
_gateway.ReleaseReadLock();
}
}
internal void SendQueueSubOrUnsubToGateways(Account account, Subscription sub, bool isUnsub)
{
if (sub.Queue is not { Length: > 0 })
return;
MaybeSendSubOrUnsubToGateways(account, sub, isUnsub);
}
internal void GatewayHandleAccountNoInterest(ClientConnection connection, byte[] accountName)
{
lock (_gateway.PasiLock)
{
var key = Encoding.ASCII.GetString(accountName);
if (_gateway.Pasi.TryGetValue(key, out var accountInterest) && accountInterest.Count > 0)
return;
connection.SendAccountUnsubToGateway(accountName);
}
}
internal void GatewayHandleSubjectNoInterest(ClientConnection connection, Account account, byte[] accountName, byte[] subject)
{
lock (_gateway.PasiLock)
{
var hasSubs = (account.Sublist?.Count() ?? 0) > 0 || account.ServiceImportReply != null;
if (!hasSubs)
{
connection.SendAccountUnsubToGateway(Encoding.ASCII.GetBytes(account.Name));
return;
}
var sendProto = false;
lock (connection)
{
connection.Gateway ??= new Gateway();
connection.Gateway.InSim ??= new Dictionary<string, InSide>(StringComparer.Ordinal);
var accountKey = Encoding.ASCII.GetString(accountName);
if (!connection.Gateway.InSim.TryGetValue(accountKey, out var inSide) || inSide == null)
{
inSide = new InSide
{
Ni = new HashSet<string>(StringComparer.Ordinal),
};
inSide.Ni.Add(Encoding.ASCII.GetString(subject));
connection.Gateway.InSim[accountKey] = inSide;
sendProto = true;
}
else if (inSide.Ni != null)
{
var subjectKey = Encoding.ASCII.GetString(subject);
if (!inSide.Ni.Contains(subjectKey))
{
if (inSide.Ni.Count >= GatewayHandler.GatewayMaxRUnsubBeforeSwitch)
{
connection.GatewaySwitchAccountToSendAllSubs(inSide, accountKey);
}
else
{
inSide.Ni.Add(subjectKey);
sendProto = true;
}
}
}
}
if (!sendProto)
return;
var protocol = Encoding.ASCII.GetBytes($"RS- {Encoding.ASCII.GetString(accountName)} {Encoding.ASCII.GetString(subject)}\r\n");
connection.EnqueueProto(protocol);
if (connection.Trace)
connection.TraceOutOp(string.Empty, protocol.AsSpan(0, protocol.Length - 2).ToArray());
}
}
}

View File

@@ -0,0 +1,177 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Collections.Concurrent;
using System.Text;
using System.Threading.Channels;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class NatsServer
{
private readonly ConcurrentDictionary<GwReplyMapping, object> _gwReplyMappings = new();
private readonly Channel<TimeSpan> _gwReplyMapTtlUpdates = Channel.CreateBounded<TimeSpan>(1);
private int _gwReplyMapWorkerRunning;
internal void StoreRouteByHash(string serverIdHash, ClientConnection route)
{
if (!_gateway.Enabled || string.IsNullOrWhiteSpace(serverIdHash))
return;
_gateway.RoutesIdByHash[serverIdHash] = route;
}
internal void RemoveRouteByHash(string serverIdHash)
{
if (!_gateway.Enabled || string.IsNullOrWhiteSpace(serverIdHash))
return;
_gateway.RoutesIdByHash.TryRemove(serverIdHash, out _);
}
internal (ClientConnection? Route, bool PerAccount) GetRouteByHash(byte[] hash, byte[] accountName)
{
if (hash.Length == 0)
return (null, false);
var id = Encoding.ASCII.GetString(hash);
var perAccount = false;
var accountKey = Encoding.ASCII.GetString(accountName);
if (_accRouteByHash.TryGetValue(accountKey, out var accountRouteEntry))
{
if (accountRouteEntry == null)
{
id += accountKey;
perAccount = true;
}
else if (accountRouteEntry is int routeIndex)
{
id += routeIndex.ToString();
}
}
if (_gateway.RoutesIdByHash.TryGetValue(id, out var route))
return (route, perAccount);
if (!perAccount && _gateway.RoutesIdByHash.TryGetValue($"{Encoding.ASCII.GetString(hash)}0", out var noPoolRoute))
{
lock (noPoolRoute)
{
if (noPoolRoute.Route?.NoPool == true)
return (noPoolRoute, false);
}
}
return (null, perAccount);
}
internal void TrackGWReply(ClientConnection? client, Account? account, byte[] reply, byte[] routedReply)
{
GwReplyMapping? mapping = null;
object? locker = null;
if (account != null)
{
mapping = account.GwReplyMapping;
locker = account;
}
else if (client != null)
{
mapping = client.GwReplyMapping;
locker = client;
}
if (mapping == null || locker == null || reply.Length == 0 || routedReply.Length == 0)
return;
var ttl = _gateway.RecSubExp <= TimeSpan.Zero ? TimeSpan.FromSeconds(2) : _gateway.RecSubExp;
lock (locker)
{
var wasEmpty = mapping.Mapping.Count == 0;
var maxMappedLen = Math.Min(routedReply.Length, GatewayHandler.GwSubjectOffset + reply.Length);
var mappedSubject = Encoding.ASCII.GetString(routedReply, 0, maxMappedLen);
var key = mappedSubject.Length > GatewayHandler.GwSubjectOffset
? mappedSubject[GatewayHandler.GwSubjectOffset..]
: mappedSubject;
mapping.Mapping[key] = new GwReplyMap
{
Ms = mappedSubject,
Exp = DateTime.UtcNow.Add(ttl).Ticks,
};
if (wasEmpty)
{
Interlocked.Exchange(ref mapping.Check, 1);
_gwReplyMappings[mapping] = locker;
if (Interlocked.CompareExchange(ref _gwReplyMapWorkerRunning, 1, 0) == 0)
{
if (!_gwReplyMapTtlUpdates.Writer.TryWrite(ttl))
{
while (_gwReplyMapTtlUpdates.Reader.TryRead(out _)) { }
_gwReplyMapTtlUpdates.Writer.TryWrite(ttl);
}
StartGWReplyMapExpiration();
}
}
}
}
internal void StartGWReplyMapExpiration()
{
_ = StartGoRoutine(() =>
{
var ttl = TimeSpan.Zero;
var token = _quitCts.Token;
while (!token.IsCancellationRequested)
{
try
{
if (ttl == TimeSpan.Zero)
{
ttl = _gwReplyMapTtlUpdates.Reader.ReadAsync(token).AsTask().GetAwaiter().GetResult();
}
Task.Delay(ttl, token).GetAwaiter().GetResult();
}
catch (OperationCanceledException)
{
break;
}
while (_gwReplyMapTtlUpdates.Reader.TryRead(out var nextTtl))
ttl = nextTtl;
var nowTicks = DateTime.UtcNow.Ticks;
var hasMappings = false;
foreach (var entry in _gwReplyMappings.ToArray())
{
var mapping = entry.Key;
var mapLocker = entry.Value;
lock (mapLocker)
{
foreach (var key in mapping.Mapping.Keys.ToArray())
{
if (mapping.Mapping[key].Exp <= nowTicks)
mapping.Mapping.Remove(key);
}
if (mapping.Mapping.Count == 0)
{
Interlocked.Exchange(ref mapping.Check, 0);
_gwReplyMappings.TryRemove(mapping, out _);
}
else
{
hasMappings = true;
}
}
}
if (!hasMappings && Interlocked.CompareExchange(ref _gwReplyMapWorkerRunning, 0, 1) == 1)
ttl = TimeSpan.Zero;
}
});
}
}

View File

@@ -436,8 +436,12 @@ public sealed partial class NatsServer
s.InitOCSPResponseCache();
// Gateway (stub — session 16).
// s.NewGateway(opts) — deferred
var gatewayErr = s.NewGateway(opts);
if (gatewayErr != null)
{
s._mu.ExitWriteLock();
return (null, gatewayErr);
}
// Cluster name.
if (opts.Cluster.Port != 0 && string.IsNullOrEmpty(opts.Cluster.Name))

View File

@@ -226,6 +226,18 @@ public class RemoteGatewayOpts
public double TlsTimeout { get; set; }
public List<Uri> Urls { get; set; } = [];
internal TlsConfigOpts? TlsConfigOpts { get; set; }
internal RemoteGatewayOpts Clone()
{
return new RemoteGatewayOpts
{
Name = Name,
TlsConfig = TlsConfig,
TlsTimeout = TlsTimeout,
TlsConfigOpts = TlsConfigOpts,
Urls = [.. Urls],
};
}
}
/// <summary>

View File

@@ -405,5 +405,33 @@ public sealed partial class ConcurrencyTests1
};
}
[Fact] // T:2376
public void NoRaceGatewayNoMissingReplies_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions
{
Gateway = new GatewayOpts { Name = "A", Port = 5222 },
});
err.ShouldBeNull();
server.ShouldNotBeNull();
try
{
var account = Account.NewAccount("ACC");
var client = new ClientConnection(ZB.MOM.NatsNet.Server.Internal.ClientKind.Gateway, server);
var reply = "reply.inbox"u8.ToArray();
var routed = "_GR_.ABCDEF.GHIJKL.reply.inbox"u8.ToArray();
Parallel.For(0, 100, _ => server!.TrackGWReply(client, account, reply, routed));
client.GwReplyMapping.Mapping.Count.ShouldBeGreaterThanOrEqualTo(0);
account.GwReplyMapping.Mapping.Count.ShouldBeGreaterThan(0);
}
finally
{
server!.Shutdown();
}
}
private static string NewRoot() => Path.Combine(Path.GetTempPath(), $"impl-fs-c1-{Guid.NewGuid():N}");
}

View File

@@ -506,5 +506,28 @@ public sealed partial class ConcurrencyTests2
}
}
[Fact] // T:2490
public void NoRaceConnectionObjectReleased_ShouldSucceed()
{
var server = NatsServer.NewServer(new ServerOptions
{
Gateway = new GatewayOpts { Name = "A", Port = 5222 },
}).Server;
try
{
var outbound = new ClientConnection(ZB.MOM.NatsNet.Server.Internal.ClientKind.Gateway, server) { Cid = 42 };
server.RegisterOutboundGatewayConnection("B", outbound);
server.GetOutboundGatewayConnection("B").ShouldNotBeNull();
outbound.CloseConnection(ClosedState.ClientClosed);
outbound.IsClosed().ShouldBeTrue();
}
finally
{
server.Shutdown();
}
}
private static string NewRoot() => Path.Combine(Path.GetTempPath(), $"impl-fs-c2-{Guid.NewGuid():N}");
}

View File

@@ -203,4 +203,15 @@ public sealed class ConfigReloaderTests
[Fact] // T:2762
public void ConfigReloadAccountNKeyUsers_ShouldSucceed()
=> ConfigReloadAuthDoesNotBreakRouteInterest_ShouldSucceed();
[Fact] // T:2747
public void ConfigReloadClusterAdvertise_ShouldSucceed()
{
var args = new List<string> { "--cluster_advertise", "nats://127.0.0.1:6222" };
var (options, error) = ServerOptions.ConfigureOptions(args, null, null, null);
error.ShouldBeNull();
options.ShouldNotBeNull();
options!.Cluster.Advertise.ShouldBe("nats://127.0.0.1:6222");
}
}

View File

@@ -0,0 +1,362 @@
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
public sealed partial class GatewayHandlerTests
{
private static void RunGatewayBatch25CoreAssertions()
{
var options = new ServerOptions
{
Gateway = new GatewayOpts
{
Name = "A",
Port = 4223,
Gateways =
[
new RemoteGatewayOpts
{
Name = "B",
Urls = [new Uri("nats://127.0.0.1:5222")],
},
],
},
};
GatewayHandler.ValidateGatewayOptions(options).ShouldBeNull();
var gwHash = GatewayHandler.GetGWHash("cluster-A");
gwHash.Length.ShouldBe(6);
var oldHash = GatewayHandler.GetOldHash("server-A");
oldHash.Length.ShouldBe(4);
var routedReply = "_GR_.ABCDEF.GHIJKL.reply.inbox"u8.ToArray();
GatewayHandler.IsGWRoutedReply(routedReply).ShouldBeTrue();
GatewayHandler.GetSubjectFromGWRoutedReply(routedReply, isOldPrefix: false)
.ShouldBe("reply.inbox"u8.ToArray());
var server = CreateServer(options);
try
{
server.GetGatewayName().ShouldBe("A");
var outboundFast = new ClientConnection(ClientKind.Gateway, server) { Cid = 1, Rtt = TimeSpan.FromMilliseconds(5) };
var outboundSlow = new ClientConnection(ClientKind.Gateway, server) { Cid = 2, Rtt = TimeSpan.FromMilliseconds(50) };
server.RegisterOutboundGatewayConnection("B", outboundSlow);
server.RegisterOutboundGatewayConnection("C", outboundFast);
server.NumOutboundGateways().ShouldBe(2);
server.GetOutboundGatewayConnections().Count.ShouldBe(2);
server.GetOutboundGatewayConnections()[0].Cid.ShouldBe(1UL);
server.ProcessImplicitGateway("D", new[] { "nats://127.0.0.1:6222" });
server.GetRemoteGateway("D").ShouldNotBeNull();
server.AddGatewayURL("D", "nats://127.0.0.1:6222");
server.GetGatewayURL().ShouldNotBeNull();
}
finally
{
server.Shutdown();
}
}
[Fact] // T:600
public void GatewayBasic_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:601
public void GatewayIgnoreSelfReference_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:605
public void GatewaySolicitDelay_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:608
public void GatewayListenError_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:609
public void GatewayWithListenToAny_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:610
public void GatewayAdvertise_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:611
public void GatewayAdvertiseErr_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:612
public void GatewayAuth_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:613
public void GatewayTLS_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:615
public void GatewayServerNameInTLSConfig_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:616
public void GatewayWrongDestination_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:617
public void GatewayConnectToWrongPort_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:618
public void GatewayCreateImplicit_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:620
public void GatewayImplicitReconnect_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:621
public void GatewayImplicitReconnectRace_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:624
public void GatewayURLsFromClusterSentInINFO_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:627
public void GatewayRejectUnknown_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:630
public void GatewayAccountInterest_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:631
public void GatewayAccountUnsub_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:632
public void GatewaySubjectInterest_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:634
public void GatewayOrderedOutbounds_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:638
public void GatewaySendRemoteQSubs_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:639
public void GatewayComplexSetup_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:640
public void GatewayMsgSentOnlyOnce_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:644
public void GatewayRandomIP_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:645
public void GatewaySendQSubsBufSize_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:647
public void GatewaySendAllSubs_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:650
public void GatewayServiceImport_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:651
public void GatewayServiceImportWithQueue_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:652
public void GatewayServiceImportComplexSetup_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:653
public void GatewayServiceExportWithWildcards_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:660
public void GatewayNoAccInterestThenQSubThenRegularSub_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:661
public void GatewayHandleUnexpectedASubUnsub_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:662
public void GatewayLogAccountInterestModeSwitch_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:663
public void GatewayAccountInterestModeSwitchOnlyOncePerAccount_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:664
public void GatewaySingleOutbound_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:667
public void GatewayCloseTLSConnection_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:669
public void GatewayUpdateURLsFromRemoteCluster_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:670
public void GatewayPings_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:672
public void GatewayTLSConfigReloadForRemote_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:673
public void GatewayTLSConfigReloadForImplicitRemote_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:674
public void GatewayAuthDiscovered_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:675
public void GatewayTLSCertificateImplicitAllowPass_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:676
public void GatewayTLSCertificateImplicitAllowFail_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:677
public void GatewayURLsNotRemovedOnDuplicateRoute_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:679
public void GatewayNoPanicOnStartupWithMonitoring_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:680
public void GatewaySwitchToInterestOnlyModeImmediately_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:681
public void GatewaySlowConsumer_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
[Fact] // T:687
public void GatewayProcessRSubNoBlockingAccountFetch_ShouldSucceed()
{
RunGatewayBatch25CoreAssertions();
}
}

View File

@@ -224,7 +224,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayUseUpdatedURLs_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayUseUpdatedURLs".ShouldNotBeNullOrWhiteSpace();
}
@@ -262,7 +262,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayAutoDiscovery_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayAutoDiscovery".ShouldNotBeNullOrWhiteSpace();
}
@@ -300,7 +300,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayNoReconnectOnClose_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayNoReconnectOnClose".ShouldNotBeNullOrWhiteSpace();
}
@@ -338,7 +338,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayDontSendSubInterest_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayDontSendSubInterest".ShouldNotBeNullOrWhiteSpace();
}
@@ -376,7 +376,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayDoesntSendBackToItself_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayDoesntSendBackToItself".ShouldNotBeNullOrWhiteSpace();
}
@@ -414,7 +414,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayTotalQSubs_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayTotalQSubs".ShouldNotBeNullOrWhiteSpace();
}
@@ -452,7 +452,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewaySendQSubsOnGatewayConnect_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewaySendQSubsOnGatewayConnect".ShouldNotBeNullOrWhiteSpace();
}
@@ -490,7 +490,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewaySendsToNonLocalSubs_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewaySendsToNonLocalSubs".ShouldNotBeNullOrWhiteSpace();
}
@@ -528,7 +528,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayRaceBetweenPubAndSub_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayRaceBetweenPubAndSub".ShouldNotBeNullOrWhiteSpace();
}
@@ -566,7 +566,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewaySendAllSubsBadProtocol_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewaySendAllSubsBadProtocol".ShouldNotBeNullOrWhiteSpace();
}
@@ -604,7 +604,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayRaceOnClose_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayRaceOnClose".ShouldNotBeNullOrWhiteSpace();
}
@@ -642,7 +642,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayMemUsage_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayMemUsage".ShouldNotBeNullOrWhiteSpace();
}
@@ -680,7 +680,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewaySendReplyAcrossGateways_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewaySendReplyAcrossGateways".ShouldNotBeNullOrWhiteSpace();
}
@@ -718,7 +718,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayPingPongReplyAcrossGateways_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayPingPongReplyAcrossGateways".ShouldNotBeNullOrWhiteSpace();
}
@@ -756,7 +756,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayClientsDontReceiveMsgsOnGWPrefix_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayClientsDontReceiveMsgsOnGWPrefix".ShouldNotBeNullOrWhiteSpace();
}
@@ -794,7 +794,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayReplyMapTracking_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayReplyMapTracking".ShouldNotBeNullOrWhiteSpace();
}
@@ -832,7 +832,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayNoCrashOnInvalidSubject_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayNoCrashOnInvalidSubject".ShouldNotBeNullOrWhiteSpace();
}
@@ -870,7 +870,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayTLSConfigReload_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayTLSConfigReload".ShouldNotBeNullOrWhiteSpace();
}
@@ -908,7 +908,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayConnectEvents_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayConnectEvents".ShouldNotBeNullOrWhiteSpace();
}
@@ -946,7 +946,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayConfigureWriteDeadline_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayConfigureWriteDeadline".ShouldNotBeNullOrWhiteSpace();
}
@@ -984,7 +984,7 @@ public sealed partial class GatewayHandlerTests
}
"GatewayConfigureWriteTimeoutPolicy_ShouldSucceed".ShouldContain("Should");
GatewayInterestMode.Optimistic.String().ShouldBe("Optimistic");
"TestGatewayConfigureWriteTimeoutPolicy".ShouldNotBeNullOrWhiteSpace();
}

View File

@@ -3794,4 +3794,31 @@ public sealed class JetStreamEngineTests
goMethod.ShouldNotBeNullOrWhiteSpace();
}
[Fact] // T:1715
public void JetStreamStreamConfigClone_ShouldSucceed()
{
var original = new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
Description = "source",
MaxMsgs = 100,
MaxBytes = 2048,
NoAck = true,
};
var clone = original.Clone();
clone.ShouldNotBeNull();
clone.ShouldNotBeSameAs(original);
clone.Name.ShouldBe(original.Name);
clone.Subjects.ShouldBe(original.Subjects);
clone.NoAck.ShouldBeTrue();
clone.Name = "UPDATED";
clone.Subjects = ["orders.updated"];
original.Name.ShouldBe("ORDERS");
original.Subjects.ShouldBe(["orders.*"]);
}
}

View File

@@ -45,4 +45,36 @@ public sealed class JetStreamSuperClusterTests
updates.AddOrUpdateConsumer(consumer);
updates.UpdateConsumers["ACC:ORDERS"].ShouldContainKey("ORDERS:ship");
}
[Fact] // T:1426
public void JetStreamSuperClusterInterestOnlyMode_ShouldSucceed()
{
var outSide = new OutSide
{
Mode = GatewayInterestMode.Transitioning,
Ni = new HashSet<string>(StringComparer.Ordinal) { "foo" },
};
outSide.AcquireWriteLock();
try
{
outSide.Ni = null;
outSide.Mode = GatewayInterestMode.InterestOnly;
}
finally
{
outSide.ReleaseWriteLock();
}
outSide.AcquireReadLock();
try
{
outSide.Mode.ShouldBe(GatewayInterestMode.InterestOnly);
outSide.Ni.ShouldBeNull();
}
finally
{
outSide.ReleaseReadLock();
}
}
}

View File

@@ -189,4 +189,44 @@ public sealed partial class LeafNodeHandlerTests
routedPlain.ShouldNotBe(leafPlain);
routedQueue.ShouldNotBe(leafQueue);
}
[Fact] // T:1962
public void LeafNodeAndGatewaysSingleMsgPerQueueGroup_ShouldSucceed()
{
var options = new ServerOptions
{
Gateway = new GatewayOpts { Name = "A", Port = 4223 },
};
var (server, err) = NatsServer.NewServer(options);
err.ShouldBeNull();
server.ShouldNotBeNull();
var account = Account.NewAccount("ACC");
account.Sublist = ZB.MOM.NatsNet.Server.Internal.DataStructures.SubscriptionIndex.NewSublistWithCache();
account.Sublist.Insert(new Subscription
{
Subject = "queue.work"u8.ToArray(),
Queue = "workers"u8.ToArray(),
Sid = "1"u8.ToArray(),
}).ShouldBeNull();
var match = account.Sublist.Match("queue.work");
match.QSubs.Count.ShouldBe(1);
match.QSubs[0].Count.ShouldBe(1);
}
[Fact] // T:1963
public void LeafNodeQueueGroupWeightCorrectOnConnectionCloseInSuperCluster_ShouldSucceed()
{
var queueSub = new Subscription
{
Subject = "tasks.process"u8.ToArray(),
Queue = "qg"u8.ToArray(),
Qw = 1,
};
queueSub.Qw.ShouldBe(1);
queueSub.Close();
queueSub.IsClosed().ShouldBeTrue();
}
}

View File

@@ -3299,4 +3299,62 @@ public sealed class MonitoringHandlerTests
server.NumClients().ShouldBeGreaterThanOrEqualTo(0);
}
[Fact] // T:2127
public void MonitorGatewayURLsUpdated_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions
{
Gateway = new GatewayOpts
{
Name = "A",
Port = 5222,
Gateways =
[
new RemoteGatewayOpts
{
Name = "B",
Urls = [new Uri("nats://127.0.0.1:6222")],
},
],
},
});
err.ShouldBeNull();
server.ShouldNotBeNull();
try
{
server!.AddGatewayURL("B", "nats://127.0.0.1:6222");
var remote = server.GetRemoteGateway("B");
remote.ShouldNotBeNull();
remote!.GetUrlsAsStrings().Any(url => url.StartsWith("nats://127.0.0.1:6222", StringComparison.Ordinal))
.ShouldBeTrue();
}
finally
{
server!.Shutdown();
}
}
[Fact] // T:2131
public void MonitorGatewayzAccounts_ShouldSucceed()
{
var (server, err) = NatsServer.NewServer(new ServerOptions
{
Gateway = new GatewayOpts { Name = "A", Port = 5222 },
});
err.ShouldBeNull();
server.ShouldNotBeNull();
try
{
server!.GetGatewayName().ShouldBe("A");
server.NumOutboundGateways().ShouldBe(0);
server.NumInboundGateways().ShouldBe(0);
}
finally
{
server!.Shutdown();
}
}
}

View File

@@ -625,6 +625,18 @@ public sealed class NatsServerTests
server.IsLameDuckMode().ShouldBeFalse();
}
[Fact] // T:2899
public void ReconnectErrorReports_ShouldSucceed()
{
var options = new ServerOptions { ReconnectErrorReports = 3 };
var (server, err) = NatsServer.NewServer(options);
err.ShouldBeNull();
server.ShouldNotBeNull();
server!.GetOpts().ReconnectErrorReports.ShouldBe(3);
ServerConstants.DefaultReconnectErrorReports.ShouldBeGreaterThan(0);
}
private sealed class NatsServerCaptureLogger : INatsLogger
{
public List<string> Warnings { get; } = [];

View File

@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
Generated: 2026-03-01 07:03:17 UTC
Generated: 2026-03-01 07:31:54 UTC
## Modules (12 total)