From be1eb3392ef94baac433b59f790e8f0c5a49e33c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 1 Mar 2026 15:13:48 -0500 Subject: [PATCH] feat(boot): add server boot integration tests and fix ValueTask sync blocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix Shutdown()/LameDuckMode() deadlock caused by calling .GetAwaiter().GetResult() on pending ValueTask from Channel.ReadAsync(). ValueTask does not support synchronous blocking — must convert via .AsTask() first. Add two integration tests validating the full Start() → AcceptLoop → client connection → Shutdown lifecycle. --- .../NatsServer.Lifecycle.cs | 6 +- .../ServerBootTests.cs | 115 ++++++++++++++++++ reports/current.md | 2 +- 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ServerBootTests.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs index 3812705..a0f9eb1 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs @@ -127,8 +127,9 @@ public sealed partial class NatsServer } // Wait for accept loops to exit. + // Must use .AsTask() because ValueTask cannot be synchronously blocked on. for (int i = 0; i < doneExpected; i++) - _done.Reader.ReadAsync().GetAwaiter().GetResult(); + _done.Reader.ReadAsync().AsTask().GetAwaiter().GetResult(); // Wait for all goroutines. _grWg.Wait(); @@ -655,8 +656,9 @@ public sealed partial class NatsServer ShutdownRaftNodes(); // Wait for accept loops. + // Must use .AsTask() because ValueTask cannot be synchronously blocked on. for (int i = 0; i < expected; i++) - _ldmCh.Reader.ReadAsync().GetAwaiter().GetResult(); + _ldmCh.Reader.ReadAsync().AsTask().GetAwaiter().GetResult(); _mu.EnterWriteLock(); var clients = new List(_clients.Values); diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ServerBootTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ServerBootTests.cs new file mode 100644 index 0000000..fe44ff1 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ServerBootTests.cs @@ -0,0 +1,115 @@ +// Copyright 2013-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Net; +using System.Text; +using Shouldly; +using ZB.MOM.NatsNet.Server; + +namespace ZB.MOM.NatsNet.Server.IntegrationTests; + +/// +/// End-to-end server boot tests that validate the full +/// Start() → AcceptLoop → client connection lifecycle. +/// +[Trait("Category", "Integration")] +public sealed class ServerBootTests : IDisposable +{ + private readonly string _storeDir; + + public ServerBootTests() + { + _storeDir = Path.Combine(Path.GetTempPath(), $"natsnet-test-{Guid.NewGuid():N}"); + } + + public void Dispose() + { + try { Directory.Delete(_storeDir, true); } catch { } + } + + /// + /// Validates that a server can boot, accept a TCP connection, and send + /// a NATS protocol INFO line. This proves the full Start() → AcceptLoop + /// → CreateClient pipeline works end-to-end. + /// + /// + /// Validates that a server can boot, bind a port, and accept a TCP connection. + /// Note: GenerateClientInfoJSON is currently a stub (returns empty), so we only + /// verify the TCP handshake succeeds — not the INFO protocol line. + /// + [Fact] + public async Task ServerBoot_AcceptsTcpConnection_ShouldSucceed() + { + // Arrange — create server with ephemeral port + var opts = new ServerOptions + { + Host = "127.0.0.1", + Port = 0, // ephemeral + }; + + var (server, err) = NatsServer.NewServer(opts); + err.ShouldBeNull("NewServer should succeed"); + server.ShouldNotBeNull(); + + try + { + // Act — start the server + server!.Start(); + + // Get the actual bound port + var addr = server.Addr() as IPEndPoint; + addr.ShouldNotBeNull("Server should have a listener address after Start()"); + addr!.Port.ShouldBeGreaterThan(0); + + // Connect a raw TCP client — proves AcceptLoop is working + using var tcp = new System.Net.Sockets.TcpClient(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await tcp.ConnectAsync(addr.Address, addr.Port, cts.Token); + + tcp.Connected.ShouldBeTrue(); + + // Verify the server registered the client + await Task.Delay(100); // Give CreateClient a moment to run + server.NumClients().ShouldBeGreaterThan(0); + } + finally + { + server!.Shutdown(); + } + } + + /// + /// Validates that Shutdown() after Start() completes cleanly. + /// Uses DontListen to skip TCP binding — tests lifecycle only. + /// + [Fact] + public void ServerBoot_StartAndShutdown_ShouldSucceed() + { + var opts = new ServerOptions + { + Host = "127.0.0.1", + Port = 0, + DontListen = true, + }; + + var (server, err) = NatsServer.NewServer(opts); + err.ShouldBeNull(); + server.ShouldNotBeNull(); + + server!.Start(); + server.Running().ShouldBeTrue(); + + server.Shutdown(); + server.Running().ShouldBeFalse(); + } +} diff --git a/reports/current.md b/reports/current.md index 300d21f..bc49679 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-03-01 20:04:26 UTC +Generated: 2026-03-01 20:13:49 UTC ## Modules (12 total)