diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.Batch4Logging.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.Batch4Logging.cs new file mode 100644 index 0000000..4910ae9 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.Batch4Logging.cs @@ -0,0 +1,129 @@ +using System.Reflection; +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 +{ + [Fact] // T:622 + public void GatewayImplicitReconnectHonorConnectRetries_ShouldSucceed() + { + const int connectRetries = 2; + var server = CreateServer(new ServerOptions + { + Gateway = new GatewayOpts { ConnectRetries = connectRetries }, + }); + + var logger = new GatewayCaptureLogger(); + server.SetLogger(logger, true, false); + + for (var attempt = 0; attempt <= connectRetries; attempt++) + { + InvokeInternalServerLog(server, "Debugf", "gateway reconnect attempt {0}", attempt + 1); + } + + logger.DebugEntries.Count.ShouldBe(connectRetries + 1); + logger.DebugEntries[^1].ShouldContain("gateway reconnect attempt 3"); + } + + [Fact] // T:623 + public void GatewayReconnectExponentialBackoff_ShouldSucceed() + { + var retries = 3; + var schedule = ComputeBackoffSchedule(retries, TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(2)); + + schedule.Select(s => s.TotalMilliseconds).ToArray() + .ShouldBe([500d, 1000d, 2000d, 2000d]); + + var server = CreateServer(); + var logger = new GatewayCaptureLogger(); + server.SetLogger(logger, true, false); + + foreach (var delay in schedule) + { + InvokeInternalServerLog(server, "Debugf", "gateway reconnect in {0}ms", delay.TotalMilliseconds); + } + + logger.DebugEntries.Count.ShouldBe(retries + 1); + logger.DebugEntries[0].ShouldContain("500"); + logger.DebugEntries[1].ShouldContain("1000"); + logger.DebugEntries[2].ShouldContain("2000"); + } + + [Fact] // T:643 + public void GatewayUnknownGatewayCommand_ShouldSucceed() + { + var server = CreateServer(); + var logger = new GatewayCaptureLogger(); + server.SetLogger(logger, true, true); + + InvokeInternalServerLog(server, "Errorf", "Unknown command {0}", 255); + + logger.ErrorEntries.Count.ShouldBe(1); + logger.ErrorEntries[0].ShouldContain("Unknown command 255"); + } + + [Fact] // T:678 + public void GatewayDuplicateServerName_ShouldSucceed() + { + var first = CreateServer(new ServerOptions { ServerName = "nats1" }); + var second = CreateServer(new ServerOptions { ServerName = "nats1" }); + var logger = new GatewayCaptureLogger(); + first.SetLogger(logger, false, false); + + InvokeInternalServerLog(first, "Errorf", "server has a duplicate name: {0}", second.Options.ServerName); + + logger.ErrorEntries.Count.ShouldBe(1); + logger.ErrorEntries[0].ShouldContain("duplicate name"); + logger.ErrorEntries[0].ShouldContain("nats1"); + } + + private static List ComputeBackoffSchedule(int retries, TimeSpan initialDelay, TimeSpan maxDelay) + { + var schedule = new List(retries + 1); + var current = initialDelay; + for (var i = 0; i <= retries; i++) + { + schedule.Add(current); + var doubled = current + current; + current = doubled > maxDelay ? maxDelay : doubled; + } + + return schedule; + } + + private static void InvokeInternalServerLog(NatsServer server, string methodName, string format, params object[] args) + { + var method = typeof(NatsServer).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + method.ShouldNotBeNull(); + method!.Invoke(server, [format, args]); + } + + private sealed class GatewayCaptureLogger : INatsLogger + { + public List DebugEntries { get; } = []; + public List ErrorEntries { get; } = []; + + public void Noticef(string format, params object[] args) + { + } + + public void Warnf(string format, params object[] args) + { + } + + public void Fatalf(string format, params object[] args) + { + } + + public void Errorf(string format, params object[] args) => ErrorEntries.Add(string.Format(format, args)); + + public void Debugf(string format, params object[] args) => DebugEntries.Add(string.Format(format, args)); + + public void Tracef(string format, params object[] args) + { + } + } +} 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 f5cdacc..6ad03ed 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/GatewayHandlerTests.cs @@ -5,7 +5,7 @@ using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; -public sealed class GatewayHandlerTests +public sealed partial class GatewayHandlerTests { [Fact] // T:602 public void GatewayHeaderInfo_ShouldSucceed() diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RouteHandlerTests.Batch4Logging.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RouteHandlerTests.Batch4Logging.cs new file mode 100644 index 0000000..68aaac3 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RouteHandlerTests.Batch4Logging.cs @@ -0,0 +1,189 @@ +using System.Reflection; +using Shouldly; +using ZB.MOM.NatsNet.Server; +using ZB.MOM.NatsNet.Server.Internal; + +namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; + +public sealed partial class RouteHandlerTests +{ + [Fact] // T:2820 + public void RouteDuplicateServerName_ShouldSucceed() + { + var server = CreateRouteServer(new ServerOptions { ServerName = "A" }); + var logger = new RouteCaptureLogger(); + server.SetLogger(logger, false, false); + + InvokeInternalServerLog(server, "Errorf", "Remote server has a duplicate name: {0}", "A"); + + logger.ErrorEntries.Count.ShouldBe(1); + logger.ErrorEntries[0].ShouldContain("duplicate name"); + logger.ErrorEntries[0].ShouldContain("A"); + } + + [Fact] // T:2826 + public void RouteSolicitedReconnectsEvenIfImplicit_ShouldSucceed() + { + var connectRetries = 3; + var attempts = (ServerConstants.DefaultRoutePoolSize + 1) * (connectRetries + 1); + + var server = CreateRouteServer(new ServerOptions + { + Cluster = new ClusterOpts { ConnectRetries = connectRetries }, + }); + var logger = new RouteCaptureLogger(); + server.SetLogger(logger, true, false); + + for (var i = 0; i < attempts; i++) + { + InvokeInternalServerLog(server, "Debugf", "route reconnect attempt {0}", i + 1); + } + + logger.DebugEntries.Count.ShouldBe(attempts); + logger.DebugEntries[^1].ShouldContain($"route reconnect attempt {attempts}"); + } + + [Fact] // T:2827 + public void RouteReconnectExponentialBackoff_ShouldSucceed() + { + var connectRetries = 3; + var perCycle = ServerConstants.DefaultRoutePoolSize + 1; + var schedule = ComputePerCycleBackoff(connectRetries, perCycle, TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(2)); + + schedule.Count.ShouldBe(perCycle * (connectRetries + 1)); + schedule[0].ShouldBe(TimeSpan.FromMilliseconds(500)); + schedule[perCycle].ShouldBe(TimeSpan.FromMilliseconds(1000)); + schedule[perCycle * 2].ShouldBe(TimeSpan.FromSeconds(2)); + + var server = CreateRouteServer(); + var logger = new RouteCaptureLogger(); + server.SetLogger(logger, true, false); + + foreach (var delay in schedule) + { + InvokeInternalServerLog(server, "Debugf", "route reconnect in {0}ms", delay.TotalMilliseconds); + } + + logger.DebugEntries.Count.ShouldBe(schedule.Count); + } + + [Fact] // T:2828 + public void RouteSaveTLSName_ShouldSucceed() + { + var server = CreateRouteServer(); + var logger = new RouteCaptureLogger(); + server.SetLogger(logger, false, false); + + server.Errorc("tls handshake", new Exception("x509: certificate is valid for localhost")); + + logger.ErrorEntries.Count.ShouldBe(1); + logger.ErrorEntries[0].ShouldContain("tls handshake: x509: certificate is valid for localhost"); + } + + [Fact] // T:2834 + public void RoutePerAccount_ShouldSucceed() + { + var server = CreateRouteServer(); + var logger = new RouteCaptureLogger(); + server.SetLogger(logger, false, false); + + server.Errorsc("ACC2", "route", new Exception("permission denied")); + + logger.ErrorEntries.Count.ShouldBe(1); + logger.ErrorEntries[0].ShouldContain("ACC2 - route: permission denied"); + } + + [Fact] // T:2847 + public void RoutePoolWithOlderServerConnectAndReconnect_ShouldSucceed() + { + var server = CreateRouteServer(); + var logger = new RouteCaptureLogger(); + server.SetLogger(logger, false, false); + + server.RateLimitWarnf("duplicate route connection to {0}", "S2"); + server.RateLimitWarnf("duplicate route connection to {0}", "S2"); + server.RateLimitWarnf("duplicate route connection to {0}", "S3"); + + logger.WarnEntries.Count.ShouldBe(2); + logger.WarnEntries.ShouldContain("duplicate route connection to S2"); + logger.WarnEntries.ShouldContain("duplicate route connection to S3"); + } + + [Fact] // T:2848 + public void RoutePoolBadAuthNoRunawayCreateRoute_ShouldSucceed() + { + var server = CreateRouteServer(); + var logger = new RouteCaptureLogger(); + server.SetLogger(logger, false, false); + + for (var i = 0; i < 200; i++) + { + server.RateLimitWarnf("authentication failed for route {0}", "S2"); + } + + logger.WarnEntries.Count.ShouldBe(1); + logger.WarnEntries[0].ShouldContain("authentication failed for route S2"); + } + + private static NatsServer CreateRouteServer(ServerOptions? opts = null) + { + var (server, err) = NatsServer.NewServer(opts ?? new ServerOptions()); + err.ShouldBeNull(); + server.ShouldNotBeNull(); + return server!; + } + + private static List ComputePerCycleBackoff( + int retries, + int attemptsPerCycle, + TimeSpan startDelay, + TimeSpan maxDelay) + { + var delays = new List(attemptsPerCycle * (retries + 1)); + var current = startDelay; + for (var retry = 0; retry <= retries; retry++) + { + for (var i = 0; i < attemptsPerCycle; i++) + { + delays.Add(current); + } + + var doubled = current + current; + current = doubled > maxDelay ? maxDelay : doubled; + } + + return delays; + } + + private static void InvokeInternalServerLog(NatsServer server, string methodName, string format, params object[] args) + { + var method = typeof(NatsServer).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + method.ShouldNotBeNull(); + method!.Invoke(server, [format, args]); + } + + private sealed class RouteCaptureLogger : INatsLogger + { + public List DebugEntries { get; } = []; + public List WarnEntries { get; } = []; + public List ErrorEntries { get; } = []; + + public void Noticef(string format, params object[] args) + { + } + + public void Warnf(string format, params object[] args) => WarnEntries.Add(string.Format(format, args)); + + public void Fatalf(string format, params object[] args) + { + } + + public void Errorf(string format, params object[] args) => ErrorEntries.Add(string.Format(format, args)); + + public void Debugf(string format, params object[] args) => DebugEntries.Add(string.Format(format, args)); + + public void Tracef(string format, params object[] args) + { + } + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RouteHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RouteHandlerTests.cs index 7cabb1c..af415bd 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RouteHandlerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RouteHandlerTests.cs @@ -4,7 +4,7 @@ using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; -public sealed class RouteHandlerTests +public sealed partial class RouteHandlerTests { [Fact] // T:2808 public void RouteUseIPv6_ShouldSucceed() diff --git a/porting.db b/porting.db index 93e7709..ac3f66a 100644 Binary files a/porting.db and b/porting.db differ