From a83339fe710206ad37d1e5a61b328a25bb86d9b8 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 01:47:07 -0500 Subject: [PATCH 01/11] feat(batch25): implement gateway bootstrap and solicitation --- .../ZB.MOM.NatsNet.Server/ClientConnection.cs | 1 + .../Gateway/GatewayHandler.cs | 80 ++++++ .../Gateway/GatewayTypes.cs | 177 +++++++++++++- .../NatsServer.Gateways.ConfigAndStartup.cs | 230 ++++++++++++++++++ .../ZB.MOM.NatsNet.Server/NatsServer.Init.cs | 8 +- .../ServerOptionTypes.cs | 12 + .../ImplBacklog/GatewayHandlerTests.cs | 42 ++-- porting.db | Bin 6787072 -> 6795264 bytes 8 files changed, 525 insertions(+), 25 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConfigAndStartup.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs index 1eb2491..1e940ba 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs @@ -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. diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs new file mode 100644 index 0000000..b51ded3 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs @@ -0,0 +1,80 @@ +// Copyright 2018-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Security.Cryptography; +using System.Text; + +namespace ZB.MOM.NatsNet.Server; + +internal static class GatewayHandler +{ + 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(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); + + 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]); + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs index c405948..f921ed1 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs @@ -13,6 +13,7 @@ // // Adapted from server/gateway.go in the NATS server Go source. +using System.Text; using System.Threading; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Internal.DataStructures; @@ -54,6 +55,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", + }; + } +} + /// /// Server-level gateway state kept on the instance. /// Replaces the stub that was in NatsServerTypes.cs. @@ -130,6 +145,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 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) // ------------------------------------------------------------------------- @@ -219,6 +299,7 @@ internal sealed class GatewayCfg /// TLS server name override for SNI. public string TlsName { get; set; } = string.Empty; + /// TLS server name override for SNI. /// True if this remote was discovered via gossip (not configured). public bool Implicit { get; set; } @@ -228,6 +309,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 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 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 urls) + { + _lock.EnterWriteLock(); + try + { + foreach (var url in urls) + Urls[url.ToString()] = url; + } + finally { _lock.ExitWriteLock(); } + } + // ------------------------------------------------------------------------- // Lock helpers // ------------------------------------------------------------------------- @@ -378,7 +534,24 @@ internal sealed class GwReplyMapping /// 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(); } } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConfigAndStartup.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConfigAndStartup.cs new file mode 100644 index 0000000..35fceca --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConfigAndStartup.cs @@ -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 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; + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs index 83c3c2f..baeda41 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs @@ -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)) diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs index f382669..5b96c5e 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs @@ -226,6 +226,18 @@ public class RemoteGatewayOpts public double TlsTimeout { get; set; } public List Urls { get; set; } = []; internal TlsConfigOpts? TlsConfigOpts { get; set; } + + internal RemoteGatewayOpts Clone() + { + return new RemoteGatewayOpts + { + Name = Name, + TlsConfig = TlsConfig, + TlsTimeout = TlsTimeout, + TlsConfigOpts = TlsConfigOpts, + Urls = [.. Urls], + }; + } } /// diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs index 7a46e03..97076d7 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs @@ -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(); } diff --git a/porting.db b/porting.db index 6eec31ed9f3997368680f35fcf105d24a45f0923..8d456e96be048dee80f1179cb6ea00adbc661063 100644 GIT binary patch delta 6808 zcmcgvdu&tJ8NW|EzSnjV8c5PO#7Ur#7x5!@On9bg6W&Q61;S&*i5({;Z#x7C;A2{y ziWaHBN{-g*x}jS;sZu*oTimH=gTbV2?J8*d&xV8$LKE#Dg|P6b%46pq2j|`pCBrO} z^-15)-@WJiec$=ccfSJ$!z{QQW)EfXCn-wPf7rZ)K6#>HNimbCedSnj2CdGji?G>J(% z`Z{xU7S8#kzepR+T;a`P4(0NAo4q|fUJo~fktv35U}TJRXqivhc@#x`^T@!n80E(h zgHh}yp2o&}sgHenJ zcN?P^5AGI)SsEihh|I>OppKo+_yfI-+(Z1#r$m$ad`L8z%OKHY9s@*^IUFRK?0-Me zWZyf9Cj0Fmn(Wg&rK>O%784XoLSb{%VTWsM|sCeh?% zrVvd|q(C(4WEaCVBz!J@hX2y^NA%~JHkuQ^5I;j9M_NuKe1S<7^w_jJjAPd9lG2s;bYTIV zhqh)5AE0bEKNDSQFo~!zN0@_de9a_qCC)M{>c|y-BU>}k=~11Y?S{yJC4QJB%)xx(V;+QOGchN!K^eN&J*5JnAhbCSxR#- zUkJs_MpRIs@{N22!l9@+rBH3TT_jwNy_krYVqv~=-DSnXmY7+K{!%OyD9n-7W+91= z)D#LI&;!7vd9I2|%rLIhRv1E>-TWu|bNWQFmiwMd7k;I?n2^PKnF#$RRdu*L1O3h+ zT#7AhM4vkan_^*f$0?jvn6H!y7KJ%dWf5BV*Ros!77>dcivf!fOCpveEXi1=VR;5i z3YO_uW?)Ihl7=N6%d=QAB3Ule_odUQH;mtM2idonO2b+4&srycMlP^!@s-j8L0lyB zU(G5;7yRrqk*0m7@{uxVILC_UtDq@ba?vtDM|YcsqufvbflWdu`%TfEj8^2Fou>C> zE2&oGS9UX4gq#$~a=~Q&BN3IMg;l3`Nyh0X?WbHETpd)W| znItVr+iN->ef-crGpaOyyAb1giN(ibTSO&pz2v$I_5`t9_tcG9>1l17KKu`<8S_sxbPzS+F5Uht_ z0|Xl(sE1$^1e+n)0>M@YegVNY2pS-8L(m9869gUzybv@G03T;brTVBuhw){@yZZ0+ zI&rbkqC1lC&xG09R?TsZj&J0?ac23k8i zY!;id$YL+DSj`q^S&6;OQG%D={`k_j(9qe&LX=cnZ>*EoFR{eCen5JeEMKi;K8)!1 zo9!ghk(Yd?l`Ui!qcdHH{M> zsx9|rphMxtY5zr#R#)j$xNjWKefhifc>i#EH%^#dtBcG$@tpF=api;s}BhMo?;dbTN4pgs-Qwx65iC`5U(=b6;zq#q93& zvH-j3dObGtXt(&;g`ySuttJA7lhnLIOvX4t|ENpZiLAl;33c(B5a zceu7*8yqbrmu0Fctq9X&bH>Z<`|e1OoiuCKlBuScY7bK+YkO5$yF+?wP(F0d(y8WP zmxAL^xV^)R{1x@CC<({Zlh`*7iCdV&zt1-2lO9W$luUWmxTL+&s^L{zGQ0!*=~`py zXaU>oQ%=P$?HQXC4%ct=%S+NR^&HZre!^k7DjP!{FZyI{lhubm(*NsDVwZN0joLf< zAtzzm{tv0&^%x01cOBPbUA0e9kJc$a8ClX!!X#9Xxg^&3c@{JI^^fw&z+G(nguUNp zu}*q=$Lr`CsV_PhDE%r1gAJp-+~JDLLzc8-oY2M$1rrBnb;awtcRbgzfuL=|lv-W( zxB||V0wZ1(!xr}tx%sWm&Uq8Nbi_S;1tdS&NYzc?L0mV1T&ZWA0MLmG^%>~$QEPOJ zn|Ktl+N^PpaXC_V)P0^ZfA@BHds>@Yy`I(4DFs>fFyg@b-pq~(GwLdN3LahK@E9G& z+9z~e8h-)=sq?X6okQvwYs=`lERFwKkRx@B6QW{tnmD;ud)&ztrFIgd>dGCRh}J|W zqIj2W*nnL!d1rhoN)DVcer(1#dBA?celXQ{lXQH0;>O+t XWS;j>_8#V0xl`<$@W8sJ`Xm1z({!VA delta 1828 zcmY+?TWl0n90u^2+1Z)hGkc>gQ0Xk)cI|Cj=(b2%EVNXhMWnbbs7Sfg(gG4JrECLu zp%;yaq85<-N2A!1XuP~ET00>Sy3~paNEG!!Nz8&gAP+_&UScBp&8ZCd@JqgL&YYQ@ z|Jk0@AzRPDAzOczeMk_Lo%;gS;?R-i>Qc#b@#(=*W3W_gPE3{ugrjm;l25xzTvJ^> zd8NEWu9Cw>RZ1KcDREKEAHF0GNVpDcUZpof7c=4y%5G z#TKiLp)I%C$7r=y8%2v)?Ic>rYDu(PUq6C&>+4UrZGP$Iom#EiW>j1i&rI~&#O)u`Slsf};|IA)d_XzsHj~`2n8H zsmFOTC%%^_bJ~48nUmhZlR4#fo=zdP@?;KoB~RvHqdf7UB2~Aj=0xZ4i{>=Tc}gM` z@?=hNDo-bnGI%EQ4Nc92sHc1QJ ze)8q88I)|5Jyes&-lzRnf`01%*2yS6pOua;G~}?ubpDzhH|Mh1l#1Cr&d8o8(*&`u zcRS@=cVRDnx2*R0`sK zHigpjSb*PXMDtmVNQYk!`r{dPjgA(uTuM&2d+4VEHkUrovi+18aZaG+h1kIt`U=?r zYT4}kZ()8B%cfK#-oCYnbyH(~I$!WmR>=Gi^HD28?B(&VmWy%C{DDT(boQ1=^NLx@ zdX|@#l;Au0mIEcMiS|bn{w$T!rmb&5{ z7Jd`zlt1iL7FBe^C}$h(R2ODHSTFEUw2YPWK~jGi8@X-a@p2aCKWuj(!axHTxWNNn z@WBK~gLKG%i7*K!Lnhn-Qy|L-MD#1O(mV^@nc9zzLUvHiv`-OUa0bl3!&akWyZ(ks zuWZu?X><%T=10FOrU&`a8>3x%Or+XQ{W@L4nNe~Chqhsx9vJ`eFUIumD6KBdPt9XC zjb7@~^T+=lMb8U3_R8L;Xz;Rg68*YMPmB*f*{#nQAN<}9eT_)HQLWJ2=cepVeag+z zD!Y{)9sDpAvLOIDkPAV$6Y?M*3ZM{*AOyuw0@GkRltLKFpd4nvT`&_WU=~DRHrx%B zFbC#B6;#7KsDXQ6J}iKRPz!ai2=0Z&a33szdRPkgLlhdI5thMncmN)RhhPOf3@f1t zR>5jm18bof9)T8E2d&U%C_U?M{G+>+gy5a&+2nrH^^MD^Rk4lE{f_S)dFrD|!pP}g RHfE<(rFhmzJi8(4{1?w6O6~vv From 1763304e28169794613437ad9f9ccf6e54381fde Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 01:50:40 -0500 Subject: [PATCH 02/11] feat(batch25): implement gateway handshake and gossip --- .../ClientConnection.Gateways.Protocol.cs | 102 +++++++++++ .../Gateway/GatewayHandler.cs | 11 ++ ...atsServer.Gateways.ConnectionsAndGossip.cs | 169 ++++++++++++++++++ porting.db | Bin 6795264 -> 6799360 bytes 4 files changed, 282 insertions(+) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs new file mode 100644 index 0000000..895874a --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs @@ -0,0 +1,102 @@ +// 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(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(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; + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs index b51ded3..8b1df33 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; +using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server; @@ -66,6 +67,16 @@ internal static class GatewayHandler 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"); + } + private static byte[] GetHash(string value, int len) { var bytes = Encoding.UTF8.GetBytes(value); diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs new file mode 100644 index 0000000..cd5d823 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs @@ -0,0 +1,169 @@ +// 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 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(); + 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 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 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(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() + .ToList(); + + cfg.AddUrls(urls); + cfg.VarzUpdateUrls = true; + } + finally + { + _gateway.ReleaseWriteLock(); + } + } + + 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(); } + } +} diff --git a/porting.db b/porting.db index 8d456e96be048dee80f1179cb6ea00adbc661063..5bf203332e6b143ced4e227bf2974b4eb30e785c 100644 GIT binary patch delta 4713 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 Date: Sun, 1 Mar 2026 01:53:51 -0500 Subject: [PATCH 03/11] feat(batch25): implement gateway URL and registry bookkeeping --- .../Gateway/GatewayTypes.cs | 6 + ...atsServer.Gateways.ConnectionsAndGossip.cs | 127 ++++++++++++++++++ porting.db | Bin 6799360 -> 6803456 bytes 3 files changed, 133 insertions(+) diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs index f921ed1..ddeb7b4 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs @@ -15,6 +15,7 @@ using System.Text; using System.Threading; +using System.Linq; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Internal.DataStructures; @@ -256,6 +257,11 @@ 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)]; + } } /// diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs index cd5d823..e01af38 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ConnectionsAndGossip.cs @@ -144,6 +144,133 @@ public sealed partial class NatsServer } } + 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 GetOutboundGatewayConnections() + { + _gateway.AcquireReadLock(); + try { return [.. _gateway.Outo]; } + finally { _gateway.ReleaseReadLock(); } + } + public int NumOutboundGateways() => NumOutboundGatewaysInternal(); internal int NumOutboundGatewaysInternal() diff --git a/porting.db b/porting.db index 5bf203332e6b143ced4e227bf2974b4eb30e785c..07133d61e94225679ffad29c3fef9ff5c4cf83ff 100644 GIT binary patch delta 3816 zcmai$du&uy9>>qUcV1^+t#vxn$IP_T$Bfd)JZ@+D;L(->iok-fxKczrWv0U)J5Xxb zCCiewuDr9D7R++lTqo(D7W=b z!d1S=tVM1!+dfp@z?)97(c5xf=@&&V!#(~nSDbJO0eY;ovPQVI-Lr-CbZKBbV;94@ywS;8+-WdO;Ts{Bw5s;Y(ZsHz6aqN-{_xPPgt3hHxJ z`JiSczn$;NPEetee|UV?uo-J12jvLsAp7J9YaqMj2>p}oa#W8fBS+|)49Zau(KCbIxtIrZMjr0FL)L+AT&(Tj=`WqTy6Hf(gEPIYtv;2Adn|qVDnUjg0 zMB?e+o~N9~v-?9@^+S&|`4;UISo93ND5YlTDo)z+KCR>==^Yy4%CQH_DhT(`R6ZJ7 z$#a$L(rG%&W)7RH+4O1Jz=Sh2z}`DeFSAhE9Av?EJ7r(IOFQKz%H$^Ydk=(BV&i_w z`evv{n*M-R%s2JjEcLThAJ7_2&&)YX|E~0mbW+YNkPEr+M9WA5iD(MZE zs4|7JC+yKn^a>08hc>Y%KGThn-uR5#I3@M<=hvoGH`UjqUw>D^f%rr-V z9XNm!xPTj!fpTygr~tPE52yrQ-~&~l8q|PV;0FN^1a+Vugg^sm1WlkBgh2${0qz6~ zKnrLEZJ-@QK@7w}0*IgkBtZ&vf-cYvdcZ=k2rLFmKriS6{a`5=0L#E~umao#?glHt zJzy1B4c34kfwf>AxEHJk8^DjjMsOea3Ai6@0)t>PcmQkx4}yol5J-bzuoYxL_G#1H z$X}KO$N^IJfcu2o<2vU2uXD3Qa#Yy!^bOi*d)YQ%J!MT=p0`BIN6poy!=?t|C1EN5 zHb2aL#qHxN$N|=0o?huS6&6zx-~)RC3&oLadVJSd_F=P^h4!Rd*<>MCE{I}+?L3yN zPu0JR& zByKFh;_EZDk00bK zxn&P2+wGonZ*vu0_d92tEsjU+AK4dCX8X#v#`=s^u&~gaYfy?!Z+OMbiVL~rz9~~- zjj~kj%@JWc2R4Z;_(+ZliBw#Bb5Qx_!1Sx-=~=pKes_-kIoHOgI!k(2$zHBm*#yNI zE9tyPaW-k39jTJem5Q@bb+)t7V>$2q1*f)ox6)?3Zi{k=FnK$ty9-t8T}s7zwPG8e zNS2&?g;Mn1e5RI_KR*&+eZS8+*^bw9l|sC`^pNFBaoir)-b{4SD#TNzt;-ZEE{^L~ zcJ5@ZYJO#YWdlrw0pq@Dt`0%$(ydY*w$wOGb6sGP_@SKdhjaBC2Wc({K}_j(Q~7qm t(dsh}(_adLm=v{7SRL70Vx;aO5JdgCP}jG_I8yWS^P;FdCdyB^{{eMx#Ek#| delta 1842 zcmY+>eN0tl90&08e(&$x7cQ3%CH27?G{EEI^ws zW=?eLw>76Y|ETp3YRi*a)0`{Y%AswFQH&Q+6fsO!TUz$zx$~?$_SwGQ=XcL@p7YS$ zzF%!_-mmV;(k)3cHn#=KW$SQlMOZH0dLV2a2+QjasXgjBcJ#{CiuN@(B-f=|dwb=m zY?!CU{`g9te7{L+mn+x5Uf=LqY*Wl}7j3@X?x0Px+aG8lyWK|1u-hn_X}90CcIm#| zZlT?>+Xz~(-G;TMcIkp$Z(w-FZr9O{+HDB!GrL_y`^0X8XglmSfcC!K`q3Kf*5^?J z%EPAnJ*w3+AcwNGokp3&qkZerd2e!^pDTQd@5>xzifnC^m0n#jLsq3j8IqgE9*@R0 z#2Oo8QN1Xkg%Vm}LYt7#3KCjgLJKCe+=Q0n;WK-D({uFk3Co!YErS>B_GMXxrcxSf zk|Zhh%}sAb^i(8Mq!d!q6p{Q`+aXdi(iV|?NbiW0gtSp4FH%$_4^ov#Zlp?)Tu2K< zGLhzrL`YMSiX*xcaiUmsAmxZ;AjOZRBgK!U9c@swTIVrW2bPS@mb+7Fl)3F^C1~wi zo|J5E@90)Q1r2mCz+rWCJ6GHdIMXGUR3>@%c*@;wvr+HTELtO#Il{_4xw&m#hBfXI zEs?FiFHo6ky?vSTWd60AZt{We^#E7?Liv2Mn*v;0qa|_4MY@K2kQewRc~#4+;mn1K zoW@0$sFOQ)>zTakvf<`n7tQ8FKdMeE(nV>qxi!5tH1_+g#Cew}m4|yMo9ln0Kl${# zMu3}psDgVpsa`(ULqG5lSFTtK@tiKo75nh4P8#IhGx6h+d)abZ< zIH(w$xIm?21JB2YbR!s5N8AA%V+05K>na7sz$&~(4mo}YVzP3GCdME6aELC(|BGUpJb@{L{EK3_RR)11}MSFK*E1?0*IKEC$<$@zr8*WSBmPf3?`E=H|n#cHX4= z|GhkageHx>+{6Ez%yw+oMhQ%Cfg3#Fg(UDnGWa0{QXvh}VH{*YCX9zH$c6yqKrRF! z5AvY^CO{#Cpa>?yBQObyVKO`lQy>f_PzqDwF_;F^p$ulgOqd0;VGhiN$KeSmhYFYn z^WjN&3Z8}qun?YsMX(r_z*1NS&%$$13Cm#xtc2%b6|9CA;6-=|s-POyKn<*gm!THc zK?K%A6k?xhqZ>|*cTs_qw8s0DH`Q~{{lH!AvRr<%p3YK%m0VJzlh%^AubAIBUYBCc JK34ag@gElyl&}B* From 59fa600b3cdb4b1a828ce1aacfb856c6f0efa932 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 02:00:08 -0500 Subject: [PATCH 04/11] feat(batch25): implement gateway interest and outbound send logic --- .../ClientConnection.Gateways.Messages.cs | 38 ++++++ .../ClientConnection.Gateways.Protocol.cs | 115 ++++++++++++++++++ .../Gateway/GatewayHandler.cs | 22 ++++ .../Gateway/GatewayTypes.cs | 19 +++ .../NatsServer.Gateways.Interest.cs | 77 ++++++++++++ porting.db | Bin 6803456 -> 6807552 bytes 6 files changed, 271 insertions(+) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs new file mode 100644 index 0000000..28e310b --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs @@ -0,0 +1,38 @@ +// Copyright 2018-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Text; + +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")); + } + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs index 895874a..b20ffa5 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Protocol.cs @@ -99,4 +99,119 @@ public sealed partial class ClientConnection 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(); + } + } } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs index 8b1df33..60281b3 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs @@ -9,6 +9,9 @@ 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."); + private static readonly TimeSpan DefaultSolicitGatewaysDelay = TimeSpan.FromSeconds(1); private static long _gatewaySolicitDelayTicks = DefaultSolicitGatewaysDelay.Ticks; private static int _doNotForceInterestOnlyMode; @@ -77,6 +80,25 @@ internal static class GatewayHandler 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); + } + private static byte[] GetHash(string value, int len) { var bytes = Encoding.UTF8.GetBytes(value); diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs index ddeb7b4..e567081 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs @@ -262,6 +262,25 @@ internal sealed class SrvGateway { 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); + } + } /// diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs new file mode 100644 index 0000000..523d260 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs @@ -0,0 +1,77 @@ +// 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 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(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); + } +} diff --git a/porting.db b/porting.db index 07133d61e94225679ffad29c3fef9ff5c4cf83ff..1a95955f1953e57580a0412aeb94caae2c6d0c6c 100644 GIT binary patch delta 5148 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 NF4o%@$6I~J{11?etx^C0 delta 1839 zcmZ|Odu&rx90%}wPw#E->Fwjf#-44rZnziQ+6{)o$zw7>-UFY2u+qJV8H2zSBN{WJ18B)AtHr+K{Hw~Ejcpi6g2Q>z? z3??0>Wkc!{=x{{b>}-?PN$*IDZA|@L{YAYI*naXlxO)GYKOab5Z1qn6<_l z`oEVtPW7u9a+2NTW;L~g@6}6}ENiH3s>KGj3yxU(wj**g)JVr`FibwJWV`EtdmoX`z98&jS$lR>k z`dO1mMW>lZ-!a>%T3h#o8JIu<8O&hO_k@+IYjN^bNo3sdgFRC{AXHda@N9)nm5I6P z+VYK~h6>`ewaq>%FV^DpU=JT_qE8)ZZVENqhS2J-xtY%NaxZBoRJ$&o;LRenACn8H z>Ljz!Pv^OXzB$R=#U(~R4xBE&-!Y`|*!zsEn&*U5bpX)-yep_7Lxqf7p8VtE%YjZSoQ@yKI$ zba(R{V?w{z%_Wh%yImt_-vh3Y{|L{cz-qfgUwnj@iF9j@Ih&3j<)fqPIF9i?nlm_N z>fxgX$Lt=v^%FrAN(FI~Q0h2oAE^%TmC7PZqS-Gsv3uLfQ}oh)-pTaH8Lo+Rdqu)i zPv)R*zvPOZ+x&BU)>G$}KRDx{$R+*}oj&OHQu-B~<@U=wGx`Kc7x)yBj$Y>bsB4wk zOR5G!!_RqrqHd8aUO82uLJ1JvIpCr7Wl zF~Cb&2f`ezpn?tT-~cCRFa(A|9K^#gNPt90f@Db12g0fO>*D23w%gt+rdsa{K7K&y zwe*_LZ!3?}t7=>;neGd?bQb-w^v7EyO00HeMZf5&)vmo1yz}S@+%>Ke<1|_?)VaV7 z9!P_9@InS;!f<#7vLG9BAQ$pr1mr^j6oL=@Pz1#=5=KD@jD}~S6vn`FPzKM#SQrQ6 zVFHxH3osEX;6<1Oli?+p0#jicOotgT6J|jr%!WDeGQ0w>!fWt4ya97z9?XX~VF4_J uDhR+L2tqZ~KrPhi7wYN@=9)C~Dzb#?y_9j&K37loFSsx3tB%&M=l=psYpOy3 From e9be0751ec389a69cac62163bf04d82a8ecb88b5 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 02:07:25 -0500 Subject: [PATCH 05/11] feat(batch25): implement gateway reply map and inbound message pipeline --- .../ZB.MOM.NatsNet.Server/Accounts/Account.cs | 6 + .../ClientConnection.Gateways.Messages.cs | 186 ++++++++++++++++++ .../ZB.MOM.NatsNet.Server/ClientConnection.cs | 1 + .../Gateway/GatewayHandler.cs | 29 +++ .../Gateway/GatewayTypes.cs | 16 ++ .../NatsServer.Gateways.Interest.cs | 68 +++++++ .../NatsServer.Gateways.ReplyMap.cs | 177 +++++++++++++++++ porting.db | Bin 6807552 -> 6811648 bytes 8 files changed, 483 insertions(+) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ReplyMap.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs index efbaa61..ec6e7d0 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs @@ -358,6 +358,12 @@ public sealed partial class Account : INatsAccount /// internal byte[]? ServiceImportReply { get; set; } + /// + /// Gateway reply mapping table used for routed reply restoration. + /// Mirrors Go gwReplyMapping. + /// + internal GwReplyMapping GwReplyMapping { get; } = new(); + /// /// Subscription ID counter for internal use. /// Mirrors Go isid uint64. diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs index 28e310b..99370a1 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.Gateways.Messages.cs @@ -2,6 +2,8 @@ // 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; @@ -35,4 +37,188 @@ public sealed partial class ClientConnection 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(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(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()); + }); + } } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs index 1e940ba..321524d 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs @@ -135,6 +135,7 @@ public sealed partial class ClientConnection internal Dictionary? Replies; internal Dictionary? Pcd; // pending clients with data to flush internal Dictionary? DArray; // denied subscribe patterns + internal GwReplyMapping GwReplyMapping = new(); // Outbound state (simplified — full write loop ported when Server is available). internal long OutPb; // pending bytes diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs index 60281b3..5326925 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayHandler.cs @@ -11,6 +11,15 @@ 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; @@ -99,6 +108,26 @@ internal static class GatewayHandler 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); diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs index e567081..1b936d2 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs @@ -281,6 +281,22 @@ internal sealed class SrvGateway 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(); + } + } + } /// diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs index 523d260..9770b3d 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.Interest.cs @@ -74,4 +74,72 @@ public sealed partial class NatsServer 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(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(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()); + } + } } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ReplyMap.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ReplyMap.cs new file mode 100644 index 0000000..ccdd894 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Gateways.ReplyMap.cs @@ -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 _gwReplyMappings = new(); + private readonly Channel _gwReplyMapTtlUpdates = Channel.CreateBounded(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; + } + }); + } +} diff --git a/porting.db b/porting.db index 1a95955f1953e57580a0412aeb94caae2c6d0c6c..f94140bb591c8c13f33956ef9139dbdf945fdef0 100644 GIT binary patch delta 4490 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 NGZIrd=}8Rj?SJ_4sKEdL delta 2163 zcmc)KiEk8D90%}uZ)T6#V|H8Cmcq2VMSB9frATRMX}1Lm6wrl&AR^r!+oowCwICWI zN7YzEkhDbmGa5OBph05_Xa_3RhBRsb&0-AEl;}zf5{#ylBOFHjb{3ZIKcHlF_px=a?DuK=eJr@o;Ybn^m2M;EZ4OBp zAtlT_T3(nPQ^sZ3#Z=x9Y7Ditg&M?OK@dE2^JkWp$o_G>DLBz%Ka}|k8V7SSzK#H8$v#O2=H$hxYJ_6JyVxA2)mWCapapwQ=&l z@3d(HH(8KL)=S(ty2_)g9`YCh<+75U><%i>yTT$>%&O31z2scPpPJU7p5!T!I%2L&J``+ z&kx%uuhQY8ySG`YS6TD$EX5UQv9lDHqs7irtVUBPINy=XTIl0jteWh%*}Dq9fwA)w zd$q6rX5Fkez?mJFY0Lq(t15jM{3YhKGJGEO+yOk zDG#YqPd=m)J$aGx^)wYJTTfGv(l%@yj|Wdibm@yZNESV1_qK8=X#U)K087TLnk~#X zF*+A;sM?;T4uhrRzz_!!Brt#xOxl4VbLa=BIbytSSSt=HH-tU5&DJV9SIl11QjW<- zIqg1@oJs>*YzcJxGr5SIXRUUvN|SRLjciD|KPm2$U9?4$&D4KfPKjRg+i^LEQSC=I zFJYV?UEJI!w=>%RwP7mNe9KMbJ1OTzCygg%1CLJL?w2R)ixlpcPgD9KHJeU-DNm!w zM$xYA>5(UKI(0W4&+Zp3^!rgcV|1dBzgK>bF77g?QshtBL>Gdpg{&GLJ^H!speS`n z12R~^3O2BV0uFFO0wlsXaKR&x1j#TSQec864NR2wxx^~LXYVj9vfPns(u(_{W^a#+Q{jaWXft#-O=q*>eb89=|GLuM(zgOO5fqKkcu7F@4vG}wbPO& zHGvMds43C$%4Su;0Sw~JI)`K{z0sl;jM?H=wL@x6g*0%32hw2@JPMCN24q4OWJ3<* z!ep2NQ^5;9$b)H+57Xgsm;nVa6AGaSW Date: Sun, 1 Mar 2026 02:19:11 -0500 Subject: [PATCH 06/11] test(batch25): port gateway connect and tls baseline tests --- .../ConcurrencyTests1.Impltests.cs | 28 ++ .../ConcurrencyTests2.Impltests.cs | 23 ++ .../ImplBacklog/ConfigReloaderTests.cs | 11 + .../GatewayHandlerTests.Impltests.Batch25.cs | 362 ++++++++++++++++++ .../ImplBacklog/JetStreamEngineTests.cs | 27 ++ .../JetStreamSuperClusterTests.Impltests.cs | 32 ++ .../LeafNodeHandlerTests.Impltests.cs | 40 ++ .../ImplBacklog/MonitoringHandlerTests.cs | 58 +++ .../ImplBacklog/NatsServerTests.cs | 12 + porting.db | Bin 6811648 -> 6815744 bytes 10 files changed, 593 insertions(+) create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.Impltests.Batch25.cs diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs index 61c12a2..cfed43e 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests1.Impltests.cs @@ -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}"); } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests2.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests2.Impltests.cs index c618470..9551249 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests2.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConcurrencyTests2.Impltests.cs @@ -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}"); } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs index 33954c2..7f5861d 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/ConfigReloaderTests.cs @@ -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 { "--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"); + } } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.Impltests.Batch25.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.Impltests.Batch25.cs new file mode 100644 index 0000000..3759b05 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.Impltests.Batch25.cs @@ -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(); + } + +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamEngineTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamEngineTests.cs index cc7713a..394993b 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamEngineTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamEngineTests.cs @@ -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.*"]); + } + } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamSuperClusterTests.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamSuperClusterTests.Impltests.cs index 6159799..c9f1e88 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamSuperClusterTests.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/JetStreamSuperClusterTests.Impltests.cs @@ -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(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(); + } + } } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/LeafNodeHandlerTests.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/LeafNodeHandlerTests.Impltests.cs index f5ef469..c039300 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/LeafNodeHandlerTests.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/LeafNodeHandlerTests.Impltests.cs @@ -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(); + } } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs index db293c6..7d307c8 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/MonitoringHandlerTests.cs @@ -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(); + } + } + } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/NatsServerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/NatsServerTests.cs index cf17d8e..7f2fff0 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/NatsServerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/NatsServerTests.cs @@ -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 Warnings { get; } = []; diff --git a/porting.db b/porting.db index f94140bb591c8c13f33956ef9139dbdf945fdef0..8c60225241dea0cfa001c2f5e3a842f2f3b11837 100644 GIT binary patch delta 6174 zcmcgwYiv{J89wLu65s6*CxCPFAp~+e#Fxa5lYj{cY15@N4M~V8bxiUL1SMp=TYt?DMk)|FP%CXIc^Cw0zo z>>o&;e>~Fje(!hA^L*cV&*eR*??S|_m1l1=`SbZ}&GBo^ zWb#@w)%~gkz$ci4!vs^W-echMUa( z5Ki+G*G3}IIiES=_l6@l$$G;!i_O7U?2N@~wm93Zt?hP4Y=5GHH^t~nV``x*bn=rx zIr`<`n1vQNv$nKyr(LrZiF#+FY@h>?ZER_uHykhr!-3!n!N8n16#Ak01$H(#5%l^( zY=*SGy`=?9m#;}@D2Gz}AyT!w@~3Lbt~#AX)t*1qt=UzB8&z%2{Hboqt~!-P)tXE7 z=_kIfo3pE;j~6E@(9-;5HCh@RGl(v*HIHRgmR+9RxM)ir%c?ZHY9Nbh-pZ;cVdPaC zGj1V&O4Tg5ab0<B=p%uk@pu98@Jez$AZ#j7v^7#!t3hDSx~koeoAT4$6rhu zlaKl-!@OcqeTm@S{z=Ue^)gARFQMn#wv;8`xkY`gl;ccoqB4#?qc9{GQVbb}9HR(B zfuY1uVW=@Q7}_NIOm}jqN)b_9RD7j$DC?9a<)HF`az(XWHLdzmbyxKV)gn?@^hc5l z7X6}}QfiYuPJJ`kpp{fo-+cg3qg)hVC)n9pHb6ZT<;Kw4ZTg{8qOoz&{I6A)32pjl z(L|pp@`@sl@FWtMDCZaDBw7iYap}VqqB*Q63ZQ?x^bI=E*d%)S3({ebs70ewDBMex zOB8A;`bWK?sU^J&$3@dZqG(za1<|+d`X5U*`-vZ+%N_dhU6{3{cfg0EhIHh`QAIlP z;7Et6JM}xHn!gioqrOhPa}+aQdPWFGL+NN5N4@DNh@*9#DIBfqOyX!=hsBXDzTBx# zE_WIpyiY0qd8L@DOFsO{Fi)LQN=hm2O-vnEtPtl?EQHysDK?GSODPt_ESzFfm<3X7 z60<`o#$q;*Vgby0Q_PQ9XNvhSv!$39v$_=XU{;0MW*;?@7;3JPRdS7`*p)wzv?EnRq0X8 z%O6Q5G-pVA(!4~MkjTA4PaxVWt3*qSCi9gQ+Hu8Sp^&A%R+C$A(-)+qA^GkddWw{) zuM-huT&7PT!!qqzKM37ertN~GJb$G3Q`_{F#Gf}2B-N*NY5OR$vfCaTT<~xsEeZT8=+nrIU|Wx2XrJ$eY9+$#1FO zX#b<#qG?l)s)EWF6_<+Mmwzg|E&VIH)j__8%6Hey3WRUiYKqao?iwjt-CeU&V6bFQ z&2;9x-|rDlN#3#4^lE^+2DrBacP()50PdZ@T?gFtz}*1cjlkXXYd1r6ZN5t=<#9?W z*G($AicV=?k;gTc)z?*szOTBLg!iSN%Ft`)7@@6}^Gv6}Of=3MNSmRryw21Mk{uVB zfppT0es_^+jTu<*q1bt*1U>sY(=C|x@B*Vm!4Jw(Yv#9IcaZp!+jckJb~m@} zch4|G>1{7Zp|ea2cjir2Zr}0j2i)wZVn-LIQSnb`Bl`Rd(=U)Xc9v-s_`GqJ2???z zZ!j)F_N_OVDM7aD9OD#ZUpdd*G3xgbMyyD5V1FH4oyHC>po15fwff`k3WS%HD)4k6 zEpi^~@e%dhiWcI`p7e@L=y9BB;8nO40muNj3&3UoEdW{pv;k-b5RbP4=m5|OU>krg z0NVlV0I(Cl-2mb64*~cC01pE=4&V_0j{@ic&sp>(a3*H zFd1F;dOs`dv~fTC-8<(0-D-~{8hB^Lnb!D*S7giXDx9tQ9Jct5SZvE)I9qi&Z1F9# z$d=7nI9oe&*y4L?v8{sr#u-WM;B6T)Yb&Ga7WuOlu=Zj6So_;^*y7uE zv8{sD{&1pZ%~s~GLcXsT+Hw`F^oJAMcvp>vj7KZq_={W>tn!Bw^qMPuhO6~I143tQ x1*`etggM84@xKm)uABuc`Qb!$4p;oY1(B-)R(w^itC;@z5EcB*aullS{|9T@&z}GQ delta 3540 zcmchYd2AH-6~}jGc4v0(Wo_^sJQye#7O!u7;I#o83=Y^}2sewdF;E~Fh!i1;O@W_{ zG4M-5c(lr4P=hD}5-Q3jlv)HuQCn5SpeE&TU8@I5P)eXNO&Yb(H)}kzBlVB|Ra@WH z`|SJ8J7;Fw+Pb;6fwwuD;<@5-@vX0M8ZO+NthRw?KEgFFweJ`BVIHzLVd@e<^r{9AT{RTcJmIPdF(Y6^Pd@ z{6icNZ;Bs@|B~{gozf;47+{K9`2}Lb|L@;ykR8V+s$3dts&QP?x~6U0ni{!C6o=`3 z2YmpOyL=NNt;?svlCW<=xpQKUfWr~~w|J@W(;7XM>4ENpzCs9m%XC57VK2Ctv#vwUliu3HrLw)&Z?7OqC; z&n#Mn^b?C#A|1161yYAa%aL|kv|x{}+S|@XvU>_5**#?;**#5(?TNbtXOH+&7!h_=>6!2r z+Hk8-SD|#SJ_$Bg=^FfckkR0Ur}*&;(<*t&=u?F|F{xj4q~794z0r|+19T5EDRBF+ zPlF#->!~o}DU+7!xL}{-g00Z=gh@>mAK14UdoH$q_ACq(=*cX51{US(h0G~v&DT@# zsk#3(Q{ZDSJI;Op!9gaum~}jW?9Ux%E;{IfgU-Y00=;0d`-J!)W-Z5&s;ngelD(B< zNcL7v>h`K=bNceLSqd znYiT|S3O+ELolgEuYzyp;HtjB4?QlnW-cCGAl zJPZzzYA zHf396Ur_JiS?~79%tAej$@Cs^X=`8HymeF4jwa!J>|EFf-bc1i7U9jQdVRI1+HJ9e zj`LrO3tWZ6i(`wBCJqx5dyo^*r9tp9li=}){b-h<37d=b7N0cMo+104G#0u`^du09 z^)y!6XhI^8drYRAlcl3TE77;VSogz;cN@cS^>d#LY>9r@Enjw}!qF0arQ_wsHpEuJ zu5nP>2W@cBdIvQ-sL7=ETcvvBR;lmHi}1w9W=0-7 z^6h2e&~mn9xyX%Vx9@nVL442K@9HxxRz~ZlX))iR^{c4$7_FYD^>?%`My-cvorzjs zd56QuB7P~EUkc~jg~vYB2$UZchl)pyM2$ivpb}9@sAN85em z&HOEL`ZHra1AA{7Vc2@hh=YfJ<$cio{p4gwzis5g)!CZrR#&^W!^fA6bSS%N@Q`}P z@WX0tqyS4U;JKdL#;8d6wo%F22|(*Nip`5PSln;aMU%o``i;rZ=8yNq5BOb-t3oYO zD_ACNM(!e4Zmt(&~3SodEX8g%vzv>Kw!pD@;49Q{>)`0=u=!*&GChBe4*(07DmTc z7s^O7*PH`mK23_wFa=)j45YjFtlqZ~Mw~R_;ZA3u#_7#7W9sa_4IrJv3I6+FUs^6n*w)BDtGDP-pm501K>{t-70FQ09w;4h>^>Cu6q*kQRzbsu zxTR>D*og@85$x5x6(rT{B_Dgqhl-9sEu7RwKRbB6BM{wzN@#%4DTHPb8YDEE&>TW@ z3C$xkpU?tA3kjV{Xc3{sgq9FmO6W8~%Lok-T25#Mq0j+&y=t4s430*|!VnSaabP1tL30+3$aza-Sx{}aUgsvtu XOz0Xy8wg!XXd|IbgswY9o5TMBrI>kG From 3412ec6d5c5b6fe1305ca4594767d7d06fdc7d40 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 02:21:23 -0500 Subject: [PATCH 07/11] test(batch25): port gateway interest and service-import tests --- porting.db | Bin 6815744 -> 6823936 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/porting.db b/porting.db index 8c60225241dea0cfa001c2f5e3a842f2f3b11837..e74dc12719ee80f49fa5d440ab446cfa59e7aae6 100644 GIT binary patch delta 3216 zcmaKuYfMvT7{_}qZBOs5Ak%|r&pJ>i&{9fU?w}SBb@RfCcpG=N=oo?u>Lx{tHHt=( z$-8BVmAEZSmM!3>M^P@>J}gm;^1)l&77{g?n~AWfOuw+F=PkV~v>$%t|D5;#KIeJP z`WE<3H5-wK^tj~zCwRbd+BcaF-kINw)0<^G`%EK^YKSbJL`7Wp|$CnHSIGI z*{Y%v$Uw9=ENbyZwRQD%&CPYS#DTCiS6I{@799+W_J>7nzNj|QpOm~Aw%8dKxlyh~ zv&j-Dt}ATycvy5SEb^duvxZfZw?nUbCM_C%(xSb6m1O=wjg?v>KQ*asBHLxuT(V{N zo@(+0^M|NMP+g$91a%zLAweAj)g~wps766`f~pmi8`Ne&xj?NGR6D35K^+8@BdGnL z%z|nIwFDGbO}2tuC|ukNDppV}pyYyT@>Yx?bCKOj1S~YV z+0Tkm+a)&DmqaW$(T|tdP1sU$nKcF%c=u)YFM@Bm!XELb;IFb8<4 z;&0JI^{DE);=WwRd?9s7w$rbur)Vsj_ytW}$A+*kJyP+Ph-Z;mp(KR|oSoJ4qb?-w%4UptzhT$I7=-z`|1FW9^ zGSje##MMk?8!RDD-kxK~^{<@IGZciZ444fjlD}&*JS05Rc?RGuAQ4Cc76VIwrNG<3 zG9Vd90a5`YU;@$rGms8gfDFJ2*Z@0_31k7;Kn{=#tnNFwJy_)4NCn}v_MYvw^Jz$^S@%`$A6IBiwU<`lHfK`vF9(<9BL9j84Ok~Tfk zVA`TnoKJ$VRE8kri8Reon_#(?vS);kvg+ruPelBiqu-VzXWIX%)>QB>dcJO z2B%t?8L6=}Yz+>y(jKWX6D}Yy%S!wI+e+|#9enShjB$!IN$NGKX65IKA^9V@TxMbr zH?b&#KQ&G>jPKQ3jb}YDEIJsLXv15IhcP%Yup#Qpvq+3iO~BUP&P@fQkq#n`ur1Dw-Bp^w6Zv zK#e?2H3HfVI+8+Nn`Qwt2|HR+g0McJNM z<4lqIcX)H_xWth;3dh0;<5)R1&Qx#bI5jIGHGOu z+ik@6vN4hJdssPXzlE7?BK^|C(jM0n7uXJQw$bVRzUhXp_OWeLTpX4_-hQT0l`*rW z-C(yw3x5Pll=1FY3`L4s9i>ypUdCws09!(hN}Nh918mph|K-J8N*iPu=84TE!;BNp znafGwBy#2%CBsQwcRZ@%nsh#JT(V!ZU9;LOElRz-MVt_B(6>3_CF6Fl7UQHFUD_LT zb}VxCQ7z=|)}rZ3dz_#C?ADIb%EbTY;GnJ^&1arXcY3q~BzB14M zZ{%tFYl-4j;RpF^so(Xl%j3*;l-T!#*V|gH7s5s@-zm4IF7mk%pQryW8n>70UWtD6 z>OngDi9S7oqZ#^A^EA1>=?D!6SrVP{>(^-{L$8?~bUamO>RFGMl{VGWC@M4^63F8J~1=oE9W$oE-TRs>DTK%r_^+iKKhbxAI#FDjgPZ*r!+fv zMk#4_7y184N}>ES>r^8pThEc`tb3mMR+FfGxt?#{0zpT31|bnnO&^aq6F7 Go#Q|07%Ch9 From 6ddad39b4a57986e57266bd1394211e0368f7ac4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 02:23:37 -0500 Subject: [PATCH 08/11] test(batch25): port gateway edge, reload, and slow-consumer tests --- porting.db | Bin 6823936 -> 6832128 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/porting.db b/porting.db index e74dc12719ee80f49fa5d440ab446cfa59e7aae6..ab402a27fd0f8466df589ee6a2b0b46d2e1b50db 100644 GIT binary patch delta 3410 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 alAZo4LC~~SzPXk>f9{Txn-c1~_5T6{=W0R# From 015b19b6f665b1c9dc514080d874c60022095be0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 02:26:30 -0500 Subject: [PATCH 09/11] test(batch25): port cross-module gateway integration tests --- porting.db | Bin 6832128 -> 6836224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/porting.db b/porting.db index ab402a27fd0f8466df589ee6a2b0b46d2e1b50db..6f74ba2e6e58a8dedcb3444def3987249dadc80e 100644 GIT binary patch delta 3248 zcmb7_Z%iBK8Nlz(cfLCxwqF};{(}LhKoa0=ml#aCls0xDOM(*v84wo!*d$p(NNux( zwM%Ws`l8I z2phQ*k@Yjr!#(=(+;jK5#A0)rSYl3dwv@X?QQDK|Y+LAC?+$HgVvy$fxu%)9CYsDO zF{26LJt;z^DoEZU^8#&@B5s?*;d!*h<7kOD7GnBiCZAls&D<2VQ1ycH%*@mh184vi z-~cVa0|FodIzR&Q06m#nGMw(FFJVqLh6;4^y4Q8*rESt?sZp{?W{H)eYP*OW7(6XQn@hoT|=VrHhjl9_QY;~O5MuRHU1x@(!4cr`2Q$jpR5 zR@Rl7@t5#BU(q(*gD#{1nQk-2-q!@FkqaDp>B0rnqUq&&fBzRW%d*@>%77b37%Mr! zp+SbW;bk6eWrb5xUh%);8{!FZM2HI81hc^6>IJkO`xnsLV)Fjy zud_FipX|Je3K)j{iUi+7XK6F{B~=y-jfKMDP*4a!W+2)R&_%|;e@Yo1T;SiCZDmRd z__xGAtW@)tATy9J{)CE(=!2L`A{o1r=u8QBj62AjW1i{|+SA@1TU1HJqpG$?(6?6MRtASeQt3azqLU|DgI)k?tBq z{!}Dhg~*qVIw0DdiiA|1GaYq7RF{hQOAuA0ql*v~q$2GVh;*rldwGWc8WH|$)9GXQ z=155mwzb%ruzc8zu>GUr2Hx#)HhbhvGj-!;AfMEY7fk<4t(=cq3h|yE`1-ELQad|Z zCUEIm($s7D_M}>K^v{+e{P7ahOf6^CyuEB0PTu$2vsGgMb*no=asIls=8$@Mv9ZvI z@75J=Ub#HutSfd!?$## znk*fuY51F3=btVp$2U6dJnr(^<&`?}E3e(ESzU9t!=84D>AR2bIJ*;%yX`hS@3r$Y zflfQH4tNBp1?qr$U_Gz_*a$QLjX)D10}j9mxPV82W?&QG23mktzyq`an}NrG9|2o{ z$ANa>3E)X!E8r!8PTys%mGLv0u;H%$Zr&%-DaNm}^D*ty9Io=0R^wzFT~7M_zHewe z*6({sy-WE{ei?QR_&QdGfWrabr;Nyuo!!3Qu@w|WS6e8Wncn;B4XK}TlfF$KH!+Om z$>#Vv{Mvx844>)qZNx);zCpb3=DJFp?DIX2nLgh&^>08rwW6OnHQ)oD0y=opb zU^}n_*a>t2eqa}{8wdd1Ko8Ih>;ZlP^a1_A0I(Mr1bzza1BQTMU<4QifBMGXoPSmOp;p6r*l|sQSz=1)b}Y*d?>RqP8t+h^ zk*F%BJZ>zH*SaQ;9E|RZh9c33_6mM?U8n)0c%Q{O8u7T?t?mcsX7_O9kj)|EVeUXr zWop;rJ<2asOMSKl&hpRxKC&@o5%G%?W)prxKWId)xlCng(__k%YN9Qf{ov1HW93-J zqh`D)4We4;Xp^%gbspSr#ukYVhohljn{6~a5s5U6PXs3qhj83_U`wUT)7%_ySB|S< zF-siLUBjm1`y24$?Y;Raty6YCjJ8ubrfQ3`wc=HL``GZt+1L9K{{EjqqrF*feK_yW zb9+1Rf8K(LF5KE{tC80@fr|75yvn4iG-bCc;B)r|N^xvoC?7A>_FL;xkK5&1qg$P> zSBd6!cd70A*Y_8yUO9c*ul1>IS0bvnGP`H^SDqfj;=R6_8pj%2RmZSH39HJgRZ84G z(N~7Q|8lGfFEITDHLb3Pd0<CyDohpmFjkjxGGtEn8>j~omdoaPiWM3 z=&Vg&wVxJsKXa|QdSG}HjH(f{G+Eo2lE60puw+MqRwc#RlGVFshTZ#r7Am(TW7AA> zOD;i0`pD{3o>K*;>^4^KRr(l(y~<@$r_!mM$Zg8lBGn>0f7s$zeyLh2v-`>18IvPt b8|n@1Ql3=>RjUN6HpJBG%GCv%n#%tG_sKxa delta 1431 zcmYk(TTC2P7{KwFGc!ATVRlZJU0ANAuq{wgmP@5vluElJAcckHqKjCd3#Dm+c8jTK zLb$Y9o2KnHDSVBWy6clxTd`4(rYV+M^~IRlR3FNw;h`ZAYiKG}6HN4Dh=~us$v^qd z%$%Gv8i@*{qfsH8BF%D~;n>-XN`Cg!&dPkEE>A@BL(zPmMf1tti1CJHm|I)H;%<>+ zJPS9fOQz8996T#Y}M0l7>^Wa9AS|wVBxMujNoG*qQ@6$L@P%J|b_NYm4b%j@nB@B^rHHn=m zSC8BjE^>iMiJpc_UTT_rix%*K&wr#-BBXW`1xoP2MkLxn_;HBIbeE_IX@t3qbRPII zkRmv-)UoaeP2kzs_o0z23$u)ayJ}j$VM3 zuGbWnU9U+jvtH*y@?*;6#~V%_g~l_sCU76LQE1L~7Kd{BZ781Q^xMZ5q3fWd4BA>8 ziLlz@$cFH$-OAjpjz5pXhWj|VJ?C)3a11BzUz4rua?H^=4^`<&Sup)5V(tnWuQ&0C^s|91DQW3 z<-v3)J{jH_$Sl5a+E$2c5{#)uZjKXH930P$ZC1*a zvCV{}?c6q2$`utE;h#3G4jR71vs!M`8X@S@?h=k;_08I6BDnn8RVWbbl~aDr1&92a z4Zgk^zkc1&*rc6;rrq&b;9iJNg*2a5y?z@f+O^P$_UBPGszJ5r1yqN2qk7bUyr>a1 zAs=c+EvOZ>p%>9h$dB65%cujrg7%K1FA5{x@< enK9otn^{++W5pznwZ1n9oqz31V6L*hnDj4`U-44_ From ee1ab96d7c729c6ee8bc5709541537044a2c19b9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 02:28:43 -0500 Subject: [PATCH 10/11] feat(batch25): verify gateway features and tests --- porting.db | Bin 6836224 -> 6836224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/porting.db b/porting.db index 6f74ba2e6e58a8dedcb3444def3987249dadc80e..9aaf164cee6554effec387f2462eb9a3d04306ea 100644 GIT binary patch delta 8187 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 Yq`DlbX|`0{$5}ZUX;RCWXP#62FOAe))c^nh delta 7167 zcmZWu3shCr)@HxX+3URbK~awzI9muoIe>tmAoB2SiYS?{9tQ={d}a+FrGk&bNPKjY z>FAcC_G@V>sj<6=WTsuOzZ!YH^`}NyR6fwuYnfinf9<^v$Gy6a@r~iPzd7fcYwfkx zp7T&%vxO_isI5t4xD{Ix)h&sPzqXU*aL66j#`0g;;%yN&7eAFB&kyJOLHSKrL>XVt ze_-isv7iv~me?wOB_0>+#Sg?yd;;&~-8|3z#$Dphp&m#^4(=q^$klRNxi`62xrN+w z+*4dWm%}A<(OeiOviI0FwwY~WkFW>W?d(Rjl3mQsX8r6Kb|9O^#lsU8J&MbMcMCe1Ybc&@> ztT)9{D3(mIUKC5BSfahmID7&n`Y0Apv7Qu*qgX7(x>GEMVvpImGQ;enDKUy-3dOt> zi=-gyWser$Ly27|)|q0RC>BOBH^n51xhUq;m>hif4ob9Z43b-!m*{|u z_FQdjWn_+iTUnHQKZ<4Q!Nf~UHND-el$C{Ih+-ne%oHOY zsTpQ`iY^$0hR=`$i;rM8T~&{ui?rHX7u4TBLjulCTc+B-i9GCnfG3r73?&h{^cd#@JioA`0_X#3a%|SGnD@enN{a$RQX??XTLy?>7I2> zC?b$++13aVMt+IYxy!0))Oae82ho7C>7LL!)X;u{Uslut| zz}5jQXTsetO0B@Rp#Kn;e%~VtBwa*N?bC;e7tz6>79V^(=XQjU4&Ja(ANgXS1+SDxc_;n2a_u^`;U5p?UUn`n);9IMis}Q0PS>#QYm&;`>26o!P(2 zW~kkW_ci>AVu*b7R}`)f5IDTHJNUfoLl3!wMg|n!@yPG^V$sKc@H<*cBI@V8d1+S@ zd~laU6ye-mG$c3|{TjsQ=pH_~demk2(6~TUeNXWB@kOR9S@+Q(VcB4q<;q*h)Qx9{%ay{NKy4z>z1qD4U=8EAy3x*ZLhGAES`y3w@ zF(cvr8pjakh&phs<4{c*UmCKDLuT;>ajke0rJ-2lL1R!GYPLjMp0ixCoU@#?G;+Dz zhuk)9121rQxXavm?i8QMEBrxzH@}%*&;Ka&6v73kzzDwx7lnTc#{^Y)U-+v~C6o!z z3sZ!#CadX;rPi|5@}_mTwVyS?>b1J9y!A!L+o~nWX=T*cdpYYE^6?&&;w+#(&g=r? zr<0veu@^K}Tr#y}-n^1x=4DDNrx+b`DW$zcv4!?XLs0e$N_<6Qkdo?5WzBgoJ=GaY zl~3$y%KCMRRqDQNnlSOuQV7fDrp=xt> zJmq*@cl=8q=QFg`)0EY}t?NSUAoX>2qTf-GW))1=Z}j2xvJ;-{>s&=ukZg%Q;*Gw} zRFZ0JhBJ@+Z+3<=M$fK2qIst=zG=Ua32zt~JBwm7DfXPkpe@7Mnc6LugGMV7?Z55uoPStbQ^|PtW=??jw zb%Y2kf1CeF_+0o{*eTpFc}&|)8%>p_W^=wdNA!wrbQIND=Ub;)Ct62X`;)HKeA3)# z+i7pMH#wfNAF&^>Z?|u>SK5Yhr)?Rww{2_07saXKcyT!YfBXr)f#1W|ml()MPs1Z2hQS8o^{ZqQ!NlduTOUisquprZ&9?PH+pEtdnXjbXcjEJ!_I;XP+KsJ}WL@Vp=m zhVzB2%#4P!f)od*3USSCF-hUfIEXb#b708?TuhG}k6jO&q@M8L1FUC6N=~L6M6*=L zJgYusmXaA(*bUxD$pbq@=_m}vLo>_Nw~)lMum%(OhgB+q@&9o;nLUtUlg7ZMdwBS^ z_t|iGZ7U9WZa*I8j7{nT^CG2iwYyzHIOY;ac1SI-X>aiq*n5z*!7`^*Znk?ueqiT@ zsA(?A#X$Z|rw9d-v?odYik&PTxBJCHW7LR=>E>s~X0e43hy+`S=brrQO#XS$R4-Wf@06|uA27@=zHtPaaB5wp4f2t&{` zw&Nt-LQQCtKNs>tux_) za`t(i2|ka8@Ai4pp~W-Jq{{m}Nt{8cs`KB|yf{Zvy2$Saa7g5^mfF)69cmO_}U=W)UNp4~Pg

_2 zchbDsr^tZ_FtSvZVCM`u&{m!- zl_POwrMIjTA|>!lm;30WsN<%~jz^B!K*Gd^W`3-&|5oSt`4N5D|++hUnrA`6si`=y9~*e zF2b|a3&eL8$$JCI9{vP(w2KyF7hKzkuw$`YW5{dv5;;yU`e4%%`CQ;Np4y>w}ta5F#dzZBs@YO!jY6@ExHAZQG_S zH3&Q2RUX#`^|yBwTubY}IOus)ZddY&%Z2TV@#wv?Lm5Sc8$13ibcK`eDXE$&aFbt& zgy;ULc-7eVm3X}?4F^5)`dvyM8T0F1%AA0r-vj17iXFn#Xdn2LX=XUKN7-!1Xh%wv z6KeM=BXsBLoN2tu*DB8%ldkzNpu&w0mGcH)albN*Bt3S&61ZXXWV{EILLw|Upe!LC zI2Y{-5#ZYcN+_J%+@(8NS?%OP*+Q^P^To3+u)Tt<;6c)wACpHMySMs zSIbqUi^q^%!*@zH+_+uJt6k10Crr9h{f#mSez}c*B=UcwR2c7_{0mB^W-UPRw@RQb z)aT-8#tVyGOz>ng_4-?z@k`NTg@32?T}wu;DGrT;w`X_DBv<igpf zXshvRc2r$*Jt#UXkjx|b^3do^QTv)M8@VnYW~E1a zp>|NTxuS4bG{$2XF&N!3Vlm<{dSLX#h*t}T`L<_~*=d^z`c(J=Z>GG$7x+-=Nm^F< zl1cV?t9+w^>*0mdt9&WCz!j{V49lu~p%DI>Z-Bw#o!5MQGz&pJ_nL2n@f0km^!3*_ z2sdNpbZFa&zpxKp_epU6bzhnxK>li9E*X2nYG0{A=v(E>)&+HGm2ZeKG9D9wbuGzR zsK{IE!$`nL#7M&Eg^`Srf{}{R8zT)P9itCMUyKZlOpJaQSs48>vN3WnaxwBS24Dx)iG$tn$H3@g&ALjPV!~)K!xvEK!gw0v8H{Juhp~Qc2RA?D9tchT zU9fITv3rA`HNnXye=D>UxJ0P`(x2lJA_SM-6V&i;@GJTG;B~WMs^^UVDVFCO4E5or zvwj6`#<^0|TW9@lEAv1VTKw4z48G_;4sXS~9IEGgf4TrG;$1Nn3x4)v6k$xpn1WG^ zQKByRd1_T>+dcM)5L22dR{PuANAA*8R-2DYuT>%DT9LD24qK|uVW(}#3KdaRh_k0- g9BwxbS=+2#En^TUEnQ}+b8DWqAhl}Pv=>bO4_~uo8vp Date: Sun, 1 Mar 2026 02:30:25 -0500 Subject: [PATCH 11/11] feat(batch25): complete gateways implementation and verification --- porting.db | Bin 6836224 -> 6836224 bytes reports/current.md | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/porting.db b/porting.db index 9aaf164cee6554effec387f2462eb9a3d04306ea..0856b0f5cdc040a9b8fc4eb3bd261ef5824bc6e6 100644 GIT binary patch delta 442 zcmXxdJ4_Q{0KoD4?)tpzT}5lPtwm`;M6t-j2Np#|5Jg@UpZKX2&W5O^BqYWS^`AH! zV`^L|gCh>cc!Pt+iKT@Oc6abI#^IOzxBp6Q%~<&hfsTO>KLLV-2xDRq!NwuV zCYspH7Gi8A&NjA_AW4cHq-ka+yJ#UpmR8!>O*?z&U@!aVWIs8&I6yZ&^wP&c4snQA)wPQL+Ib~jPl{;ReUP?Uv0|>95S^xk5 delta 428 zcmXZVJxmi}0D$59dcC&X^}Z7jr7fVgR>dDg1VM`+)*|?efPd9bML=Rg2rLdx4ZMki zlY=R7I%gt-F*y?lJCk%UB*vAg3yGUEpGk)&dAGOZe>O}1AqZg-#v($L7!BAs#Bq^$ z?4yw;n%Pea2RKM836i8}qn!>8ahM~d$#9e`$2iUjIyuQHPIHDX&eF{}dN@xneO#cQ z0WNZh9D`hDh+(cU!d3EIqrfQF8DpFq+~gLwxx-y1xW^<@Omm+HJme9NDe{D;-&B`h z?)Fv2*^*!7Cpjt8(hg>Pm3BW&Si#yCl@l5H+lVjEFU`GPT1Yx(O4K{ybr?>)gPl#4 zZ5&-O;(5n+n(SHIZCH%`i@k|?(Z$H0NYVOhty<0DQn