# Server Boot Parity Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Wire up `NatsServer.Start()` to call all subsystem startup methods matching Go's `Server.Start()` sequence, enabling a fully booting server that accepts client connections. **Architecture:** The .NET port already has every subsystem startup method ported. The gap is that `Start()` stops after system account setup instead of continuing through the full startup sequence. The fix is pure wiring — calling existing methods in order — plus 2 trivial no-op stubs and an MQTT guard. **Tech Stack:** .NET 10, C# latest, xUnit, Shouldly, NATS.Client.Core 2.7.2 --- ## Task 1: Add `CheckAuthForWarnings()` stub **Files:** - Modify: `dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Auth.cs` (add method at end of class, before closing `}`) **Step 1: Write the failing test** No dedicated test needed — this is a no-op stub. The build itself will verify the method exists when Task 3 calls it from `Start()`. **Step 2: Add the stub method** Add the following to `NatsServer.Auth.cs`, at the end of the partial class body (before the final `}`): ```csharp // ========================================================================= // CheckAuthForWarnings (feature 3049 — Start parity) // ========================================================================= /// /// Checks for insecure auth configurations and logs warnings. /// Mirrors Go Server.checkAuthforWarnings() in server/server.go. /// Stub — full implementation deferred. /// internal void CheckAuthForWarnings() { // No-op stub. Go logs warnings about: // - Password auth without TLS // - Token auth without TLS // - NKey auth without TLS // These are informational warnings only. } ``` **Step 3: Build to verify it compiles** Run: `dotnet build dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj` Expected: Build succeeded. 0 Error(s) **Step 4: Commit** ```bash git add dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Auth.cs git commit -m "feat: add CheckAuthForWarnings stub for Start() parity" ``` --- ## Task 2: Add `StartDelayedApiResponder()` stub **Files:** - Modify: `dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.JetStreamCore.cs` (add method) **Step 1: Add the stub method** Add the following to `NatsServer.JetStreamCore.cs`, inside the partial class: ```csharp // ========================================================================= // Delayed API responder (feature 3049 — Start parity) // ========================================================================= /// /// Starts the delayed JetStream API response handler goroutine. /// Started regardless of JetStream being enabled (can be enabled via config reload). /// Mirrors Go Server.delayedAPIResponder() in server/jetstream_api.go. /// Stub — full implementation deferred. /// internal void StartDelayedApiResponder() { StartGoRoutine(() => { // No-op: exits when quit is signaled. _quitCts.Token.WaitHandle.WaitOne(); }); } ``` **Step 2: Build to verify it compiles** Run: `dotnet build dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj` Expected: Build succeeded. 0 Error(s) **Step 3: Commit** ```bash git add dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.JetStreamCore.cs git commit -m "feat: add StartDelayedApiResponder stub for Start() parity" ``` --- ## Task 3: Guard MQTT `StartMqtt()` to not throw **Files:** - Modify: `dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttHandler.cs:136–137` **Step 1: Change StartMqtt from throwing to logging a warning** Replace the existing line at `MqttHandler.cs:136-137`: ```csharp public static void StartMqtt(this NatsServer server) => throw new NotImplementedException("TODO: session 22"); ``` With: ```csharp public static void StartMqtt(this NatsServer server) { server.Warnf("MQTT listener not yet implemented; skipping MQTT startup"); } ``` Note: `Warnf` is a public method on `NatsServer` (verified in `NatsServer.Logging.cs`). **Step 2: Build to verify it compiles** Run: `dotnet build dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj` Expected: Build succeeded. 0 Error(s) **Step 3: Commit** ```bash git add dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttHandler.cs git commit -m "fix: guard StartMqtt to warn instead of throwing NotImplementedException" ``` --- ## Task 4: Wire up `Start()` body to match Go's sequence This is the core task. Replace the body of `Start()` in `NatsServer.Init.cs:976–1037` with the full Go-parity startup sequence. **Files:** - Modify: `dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs:970–1037` **Step 1: Replace the Start() method** Replace lines 970–1037 (the `/// ` through the closing `}` of `Start()`) with the complete implementation below. The new code mirrors Go `server.go:2263–2575` line-by-line: ```csharp /// /// Starts the server (non-blocking). Writes startup log lines, starts all /// subsystems (monitoring, JetStream, gateways, websocket, leafnodes, routes, /// MQTT), then begins the client accept loop. /// Mirrors Go Server.Start in server.go:2263–2575. /// public void Start() { Noticef("Starting nats-server"); var gc = string.IsNullOrEmpty(ServerConstants.GitCommit) ? "not set" : ServerConstants.GitCommit; var opts = GetOpts(); _mu.EnterReadLock(); var leafNoCluster = _leafNoCluster; _mu.ExitReadLock(); var clusterName = leafNoCluster ? string.Empty : ClusterName(); Noticef(" Version: {0}", ServerConstants.Version); Noticef(" Git: [{0}]", gc); if (!string.IsNullOrEmpty(clusterName)) Noticef(" Cluster: {0}", clusterName); Noticef(" Name: {0}", _info.Name); if (opts.JetStream) Noticef(" Node: {0}", GetHash(_info.Name)); Noticef(" ID: {0}", _info.Id); // Check for insecure configurations. CheckAuthForWarnings(); // Avoid RACE between Start() and Shutdown(). Interlocked.Exchange(ref _running, 1); _mu.EnterWriteLock(); _leafNodeEnabled = opts.LeafNode.Port != 0 || opts.LeafNode.Remotes.Count > 0; _mu.ExitWriteLock(); lock (_grMu) { _grRunning = true; } StartRateLimitLogExpiration(); // Pprof http endpoint for the profiler. if (opts.ProfPort != 0) { StartProfiler(); } if (!string.IsNullOrEmpty(opts.ConfigFile)) { Noticef("Using configuration file: {0}", opts.ConfigFile); } var hasOperators = opts.TrustedOperators.Count > 0; if (hasOperators) { Noticef("Trusted Operators"); } if (hasOperators && string.IsNullOrEmpty(opts.SystemAccount)) { Warnf("Trusted Operators should utilize a System Account"); } if (opts.MaxPayload > ServerConstants.MaxPayloadMaxSize) { Warnf("Maximum payloads over {0} are generally discouraged and could lead to poor performance", ServerConstants.MaxPayloadMaxSize); } // Log the pid to a file. if (!string.IsNullOrEmpty(opts.PidFile)) { var pidErr = LogPid(); if (pidErr != null) { Fatalf("Could not write pidfile: {0}", pidErr); return; } } // Setup system account which will start the eventing stack. if (!string.IsNullOrEmpty(opts.SystemAccount)) { var saErr = SetSystemAccount(opts.SystemAccount); if (saErr != null) { Fatalf("Can't set system account: {0}", saErr); return; } } else if (!opts.NoSystemAccount) { SetDefaultSystemAccount(); } // Start monitoring before enabling other subsystems. var monErr = StartMonitoring(); if (monErr != null) { Fatalf("Can't start monitoring: {0}", monErr); return; } // Start up resolver machinery. var ar = AccountResolver(); if (ar != null) { var arErr = ar.Start(this); if (arErr != null) { Fatalf("Could not start resolver: {0}", arErr); return; } } // Start expiration of mapped GW replies. StartGWReplyMapExpiration(); // Check if JetStream has been enabled. if (opts.JetStream) { var sa = SystemAccount(); if (sa != null && sa.JsLimitsCount() > 0) { Fatalf("Not allowed to enable JetStream on the system account"); return; } var cfg = new JetStreamConfig { StoreDir = opts.StoreDir, SyncInterval = opts.SyncInterval, SyncAlways = opts.SyncAlways, MaxMemory = opts.JetStreamMaxMemory, MaxStore = opts.JetStreamMaxStore, Domain = opts.JetStreamDomain, CompressOk = true, UniqueTag = opts.JetStreamUniqueTag, }; var jsErr = EnableJetStream(cfg); if (jsErr != null) { Fatalf("Can't start JetStream: {0}", jsErr); return; } } // Delayed API response handling. StartDelayedApiResponder(); // Start OCSP Stapling monitoring. StartOCSPMonitoring(); // Configure OCSP Response Cache. StartOCSPResponseCache(); // Start up gateway if needed. if (opts.Gateway.Port != 0) { var gwErr = StartGateways(); if (gwErr != null) { Fatalf("Can't start gateways: {0}", gwErr); return; } } // Start websocket server if needed. if (opts.Websocket.Port != 0) { StartWebsocketServer(); } // Start up listen if we want to accept leaf node connections. if (opts.LeafNode.Port != 0) { var lnErr = StartLeafNodeAcceptLoop(); if (lnErr != null) { Fatalf("Can't start leaf node listener: {0}", lnErr); return; } } // Solicit remote servers for leaf node connections. if (opts.LeafNode.Remotes.Count > 0) { SolicitLeafNodeRemotes(opts.LeafNode.Remotes); } // The Routing routine needs to wait for the client listen // port to be opened and potential ephemeral port selected. var clientListenReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); // MQTT if (opts.Mqtt.Port != 0) { this.StartMqtt(); } // Start up routing as well if needed. if (opts.Cluster.Port != 0) { StartGoRoutine(() => { StartRouting(); }); } if (!string.IsNullOrEmpty(opts.PortsFileDir)) { LogPorts(); } // We've finished starting up. _startupComplete.TrySetResult(); // Wait for clients. if (!opts.DontListen) { AcceptLoop(clientListenReady); } // Bring OCSP Response cache online after accept loop started. StartOCSPResponseCache(); Noticef("Server is ready"); } ``` **Important notes for the implementer:** - `GetHash` is a static method in `NatsServer` — verify it exists. If not found, skip the `Noticef(" Node: ...")` line. - `SystemAccount()` is a public method on `NatsServer` — verified exists in `NatsServer.Accounts.cs`. - `JsLimitsCount()` must exist on `Account`. If not, use a check like `sa.HasJsLimits()` or skip that guard with a TODO comment. - `StartRouting()` in .NET does not take a `clientListenReady` parameter (Go's does). The `clientListenReady` TCS is created for forward-compatibility but is currently only passed to `AcceptLoop`. - The `CompressOk` property on `JetStreamConfig` — verify exact property name. May be `CompressOK`. - The `UniqueTag` property on `JetStreamConfig` — verify exists. If not, omit. **Step 2: Build to verify it compiles** Run: `dotnet build dotnet/src/ZB.MOM.NatsNet.Server/ZB.MOM.NatsNet.Server.csproj` Expected: Build succeeded. 0 Error(s) If there are compilation errors (e.g., missing properties like `CompressOk`, `UniqueTag`, `JsLimitsCount`), fix by: - Using the correct property name (check the class definition) - Adding a stub property if it's truly missing - Commenting out the line with a `// TODO` if it references unavailable API **Step 3: Run existing unit tests to verify no regressions** Run: `dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ --no-build` Expected: 2659 passed, ~53 skipped, 0 failed **Step 4: Run existing integration tests to verify no regressions** Run: `dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ --no-build` Expected: ~113 passed, ~854 skipped, 0 failed **Step 5: Commit** ```bash git add dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs git commit -m "feat: complete Start() with full subsystem startup sequence Wire up Start() to call all subsystem startup methods matching Go's Server.Start() sequence: monitoring, resolver, JetStream, gateways, websocket, leafnodes, MQTT, routing, accept loop." ``` --- ## Task 5: Write server boot validation integration test **Files:** - Create: `dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ServerBootTests.cs` **Step 1: Write the test** Create `ServerBootTests.cs`: ```csharp // 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 NATS.Client.Core; 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 exchange /// a NATS protocol handshake (INFO line). This proves the full Start() → /// AcceptLoop → CreateClient pipeline works end-to-end. /// [Fact] public async Task ServerBoot_AcceptsClientConnection_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 and read the INFO line using var tcp = new System.Net.Sockets.TcpClient(); await tcp.ConnectAsync(addr.Address, addr.Port); using var stream = tcp.GetStream(); stream.ReadTimeout = 5000; var buffer = new byte[4096]; var bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length)); bytesRead.ShouldBeGreaterThan(0, "Should receive data from server"); var response = Encoding.UTF8.GetString(buffer, 0, bytesRead); response.ShouldStartWith("INFO ", "Server should send INFO line on connect"); } finally { server!.Shutdown(); } } /// /// Validates that Shutdown() after Start() completes cleanly without errors. /// [Fact] public void ServerBoot_StartAndShutdown_ShouldSucceed() { var opts = new ServerOptions { Host = "127.0.0.1", Port = 0, DontListen = true, // Don't open TCP listener — just test lifecycle }; var (server, err) = NatsServer.NewServer(opts); err.ShouldBeNull(); server.ShouldNotBeNull(); // Should not throw server!.Start(); server.Running().ShouldBeTrue(); server.Shutdown(); server.Running().ShouldBeFalse(); } } ``` **Step 2: Build the integration tests** Run: `dotnet build dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/` Expected: Build succeeded. 0 Error(s) **Step 3: Run the new tests** Run: `dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ --filter "FullyQualifiedName~ServerBootTests" --no-build -v normal` Expected: 2 passed, 0 failed If `ServerBoot_AcceptsClientConnection_ShouldSucceed` fails: - Check if `AcceptLoop` is actually being called (add a `Noticef` before the call) - Check if the listener is null after `AcceptLoop` returns - Check if `CreateClient` throws — look at the error in the test output - If the INFO line isn't received, the client-side protocol handshake may be hanging — check `CreateClient` and the initial write path If `ServerBoot_StartAndShutdown_ShouldSucceed` fails: - Check if `Start()` throws an exception from one of the subsystem calls - The `DontListen = true` flag should skip `AcceptLoop` — verify the `if (!opts.DontListen)` guard **Step 4: Run the full test suite to confirm no regressions** Run: `dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.Tests/` Expected: 2659 passed, ~53 skipped, 0 failed Run: `dotnet test dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/` Expected: ~115 passed (113 old + 2 new), ~854 skipped, 0 failed **Step 5: Commit** ```bash git add dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ServerBootTests.cs git commit -m "test: add server boot validation integration tests Two tests verifying full Start() lifecycle: - ServerBoot_AcceptsClientConnection: connects TCP, reads INFO line - ServerBoot_StartAndShutdown: verifies clean lifecycle with DontListen" ``` --- ## Verification Checklist After all tasks are complete: - [ ] `dotnet build` — 0 errors - [ ] Unit tests — 2659 pass, ~53 skip, 0 fail - [ ] Integration tests — ~115 pass, ~854 skip, 0 fail (2 new tests pass) - [ ] New `ServerBoot_AcceptsClientConnection_ShouldSucceed` — PASSES - [ ] New `ServerBoot_StartAndShutdown_ShouldSucceed` — PASSES - [ ] `Start()` method body matches Go's `Server.Start()` call sequence