From a83339fe710206ad37d1e5a61b328a25bb86d9b8 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 01:47:07 -0500 Subject: [PATCH] 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