- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
286 lines
32 KiB
Markdown
286 lines
32 KiB
Markdown
# Leaf Nodes — Gap Analysis
|
|
|
|
> This file tracks what has and hasn't been ported from Go to .NET for the **Leaf Nodes** module.
|
|
> See [stillmissing.md](stillmissing.md) for the full LOC comparison across all modules.
|
|
|
|
## LLM Instructions: How to Analyze This Category
|
|
|
|
### Step 1: Read the Go Reference Files
|
|
|
|
Read each Go source file listed below. For every file:
|
|
|
|
1. Extract all **exported types** (structs, interfaces, type aliases)
|
|
2. Extract all **exported methods** on those types (receiver functions)
|
|
3. Extract all **exported standalone functions**
|
|
4. Note **key constants, enums, and protocol states**
|
|
5. Note **important unexported helpers** that implement core logic (functions >20 lines)
|
|
6. Pay attention to **concurrency patterns** (goroutines, mutexes, channels) — these map to different .NET patterns
|
|
|
|
### Step 2: Read the .NET Implementation Files
|
|
|
|
Read all `.cs` files in the .NET directories listed below. For each Go symbol found in Step 1:
|
|
|
|
1. Search for a matching type, method, or function in .NET
|
|
2. If found, compare the behavior: does it handle the same edge cases? Same error paths?
|
|
3. If partially implemented, note what's missing
|
|
4. If not found, note it as MISSING
|
|
|
|
### Step 3: Cross-Reference Tests
|
|
|
|
Compare Go test functions against .NET test methods:
|
|
|
|
1. For each Go `Test*` function, check if a corresponding .NET `[Fact]` or `[Theory]` exists
|
|
2. Note which test scenarios are covered and which are missing
|
|
3. Check the parity DB (`docs/test_parity.db`) for existing mappings:
|
|
```bash
|
|
sqlite3 docs/test_parity.db "SELECT go_test, dotnet_test, confidence FROM test_mappings tm JOIN go_tests gt ON tm.go_test_id=gt.rowid JOIN dotnet_tests dt ON tm.dotnet_test_id=dt.rowid WHERE gt.go_file LIKE '%PATTERN%'"
|
|
```
|
|
|
|
### Step 4: Classify Each Item
|
|
|
|
Use these status values:
|
|
|
|
| Status | Meaning |
|
|
|--------|---------|
|
|
| **PORTED** | Equivalent exists in .NET with matching behavior |
|
|
| **PARTIAL** | .NET implementation exists but is incomplete (missing edge cases, error handling, or features) |
|
|
| **MISSING** | No .NET equivalent found — needs to be ported |
|
|
| **NOT_APPLICABLE** | Go-specific pattern that doesn't apply to .NET (build tags, platform-specific goroutine tricks, etc.) |
|
|
| **DEFERRED** | Intentionally skipped for now (document why) |
|
|
|
|
### Step 5: Fill In the Gap Inventory
|
|
|
|
Add rows to the Gap Inventory table below. Group by Go source file. Include the Go file and line number so a porting LLM can jump directly to the reference implementation.
|
|
|
|
### Key Porting Notes for Leaf Nodes
|
|
|
|
- Leaf nodes only share subscription interest with the hub — no full mesh.
|
|
- Loop detection uses the `$LDS.` subject prefix.
|
|
- Leaf connections use `ClientKind = LEAF`.
|
|
- Leaf nodes can connect through WebSocket and support TLS.
|
|
|
|
---
|
|
|
|
## Go Reference Files (Source)
|
|
|
|
- `golang/nats-server/server/leafnode.go` — Hub-and-spoke topology for edge deployments (~3,470 lines). Only subscribed subjects shared with hub. Loop detection via `$LDS.` prefix.
|
|
|
|
## Go Reference Files (Tests)
|
|
|
|
- `golang/nats-server/server/leafnode_test.go`
|
|
- `golang/nats-server/server/leafnode_proxy_test.go`
|
|
- `golang/nats-server/test/leafnode_test.go` (integration)
|
|
|
|
## .NET Implementation Files (Source)
|
|
|
|
- `src/NATS.Server/LeafNodes/` (all files)
|
|
|
|
## .NET Implementation Files (Tests)
|
|
|
|
- `tests/NATS.Server.Tests/LeafNodes/`
|
|
|
|
---
|
|
|
|
## Gap Inventory
|
|
|
|
<!-- After analysis, fill in this table. Group rows by Go source file. -->
|
|
|
|
### golang/nats-server/server/leafnode.go
|
|
|
|
#### Constants and Types
|
|
|
|
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|
|
|-----------|:-------------|--------|:----------------|-------|
|
|
| `leafnodeTLSInsecureWarning` (const) | `golang/nats-server/server/leafnode.go:47` | PORTED | `src/NATS.Server/LeafNodes/LeafNodeManager.cs` | Warning logged in `DisableLeafConnect`; same semantic intent, no separate constant |
|
|
| `leafNodeReconnectDelayAfterLoopDetected` (const) | `golang/nats-server/server/leafnode.go:50` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:19`, `src/NATS.Server/LeafNodes/LeafConnection.cs` | Delay constant is now consumed by `LeafConnection.LeafProcessErr` for loop ERR processing. Remaining: reconnect loop still does not schedule by this delay automatically |
|
|
| `leafNodeReconnectAfterPermViolation` (const) | `golang/nats-server/server/leafnode.go:54` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:20`, `src/NATS.Server/LeafNodes/LeafConnection.cs` | Delay constant is now consumed by `LeafPermViolation` / `LeafSubPermViolation`. Remaining: no enforced wait-before-redial in reconnect worker |
|
|
| `leafNodeReconnectDelayAfterClusterNameSame` (const) | `golang/nats-server/server/leafnode.go:57` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:21`, `src/NATS.Server/LeafNodes/LeafConnection.cs` | Delay constant is now consumed by `LeafProcessErr` cluster-name path. Remaining: reconnect loop integration not complete |
|
|
| `leafNodeLoopDetectionSubjectPrefix` (const `"$LDS."`) | `golang/nats-server/server/leafnode.go:60` | PORTED | `src/NATS.Server/LeafNodes/LeafLoopDetector.cs:5` | `LeafLoopPrefix = "$LDS."` |
|
|
| `leafNodeWSPath` (const `"/leafnode"`) | `golang/nats-server/server/leafnode.go:64` | PORTED | `src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs` | Path constant is implicit in the WS adapter; not a named constant in .NET |
|
|
| `leafNodeWaitBeforeClose` (const 5s) | `golang/nats-server/server/leafnode.go:68` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:22` | Constant is defined (`LeafNodeWaitBeforeClose = 5s`), but close-path wait timer behavior is not yet wired |
|
|
| `leaf` (unexported struct) | `golang/nats-server/server/leafnode.go:71` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs` | `LeafConnection` now tracks role flags (`IsSolicited`, `IsSpoke`, `Isolated`) and helper predicates. Missing: `smap`, `tsub/tsubt`, `compression`, `gwSub`, remote cluster/server metadata parity |
|
|
| `leafNodeCfg` (unexported struct) | `golang/nats-server/server/leafnode.go:107` | PARTIAL | `src/NATS.Server/Configuration/LeafNodeOptions.cs:7` (`RemoteLeafOptions`) | Added runtime parity fields/helpers (`CurrentUrl`, `TlsName`, URL user-info, connect-delay storage, round-robin URL picker). Remaining gaps: perms and JS migrate timer wiring |
|
|
| `leafConnectInfo` (unexported struct) | `golang/nats-server/server/leafnode.go:2001` | PORTED | `src/NATS.Server/LeafNodes/LeafConnectInfo.cs` | CONNECT payload DTO now represented with Go-parity JSON fields (`jwt`, `nkey`, `sig`, `hub`, `cluster`, `headers`, `jetstream`, `compression`, `remote_account`, `proto`) |
|
|
|
|
#### Methods on `client` (receiver functions)
|
|
|
|
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|
|
|-----------|:-------------|--------|:----------------|-------|
|
|
| `(c *client) isSolicitedLeafNode()` | `golang/nats-server/server/leafnode.go:121` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs:29,169` | Solicited role is tracked (`IsSolicited`) and exposed via `IsSolicitedLeafNode()` |
|
|
| `(c *client) isSpokeLeafNode()` | `golang/nats-server/server/leafnode.go:127` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs:35,170` | Spoke role is tracked (`IsSpoke`) and exposed via `IsSpokeLeafNode()` |
|
|
| `(c *client) isHubLeafNode()` | `golang/nats-server/server/leafnode.go:131` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs:171` | Hub-role helper implemented as the complement of spoke role (`!IsSpoke`) |
|
|
| `(c *client) isIsolatedLeafNode()` | `golang/nats-server/server/leafnode.go:135` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs:41,172` | Isolation flag is tracked (`Isolated`) and exposed via `IsIsolatedLeafNode()` |
|
|
| `(c *client) sendLeafConnect(clusterName, headers)` | `golang/nats-server/server/leafnode.go:969` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs` (`SendLeafConnectAsync`) | Added CONNECT protocol writer that serializes `LeafConnectInfo` JSON payload and writes `CONNECT <json>` |
|
|
| `(c *client) leafClientHandshakeIfNeeded(remote, opts)` | `golang/nats-server/server/leafnode.go:1402` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs:80` | .NET `PerformOutboundHandshakeAsync` performs the handshake but without TLS negotiation or TLS-first logic |
|
|
| `(c *client) processLeafnodeInfo(info)` | `golang/nats-server/server/leafnode.go:1426` | MISSING | — | Complex INFO protocol processing (TLS negotiation, compression selection, URL updates, permission updates). Not ported |
|
|
| `(c *client) updateLeafNodeURLs(info)` | `golang/nats-server/server/leafnode.go:1711` | MISSING | — | Dynamically updates remote URL list from async INFO. Not ported |
|
|
| `(c *client) doUpdateLNURLs(cfg, scheme, URLs)` | `golang/nats-server/server/leafnode.go:1732` | MISSING | — | Helper for `updateLeafNodeURLs`. Not ported |
|
|
| `(c *client) remoteCluster()` | `golang/nats-server/server/leafnode.go:2235` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs` (`RemoteCluster`) | Handshake parser now captures `cluster=...` attribute and exposes it via `RemoteCluster()` |
|
|
| `(c *client) updateSmap(sub, delta, isLDS)` | `golang/nats-server/server/leafnode.go:2522` | MISSING | — | Core subject-map delta updates. .NET has `PropagateLocalSubscription` but no per-connection smap with refcounting |
|
|
| `(c *client) forceAddToSmap(subj)` | `golang/nats-server/server/leafnode.go:2567` | MISSING | — | Force-inserts a subject into the smap. Not ported |
|
|
| `(c *client) forceRemoveFromSmap(subj)` | `golang/nats-server/server/leafnode.go:2584` | MISSING | — | Force-removes a subject from the smap. Not ported |
|
|
| `(c *client) sendLeafNodeSubUpdate(key, n)` | `golang/nats-server/server/leafnode.go:2607` | PORTED | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:294` | `PropagateLocalSubscription` / `PropagateLocalUnsubscription` now mirror send-side parity: spoke subscribe-permission gate (with `$LDS.`/gateway-reply bypass) and queue-weight LS+ emission |
|
|
| `(c *client) writeLeafSub(w, key, n)` | `golang/nats-server/server/leafnode.go:2687` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs:135` | `SendLsPlusAsync` now emits `LS+ <account> <subject> <queue> <n>` for queue subscriptions with weight, and `SendLsMinusAsync` mirrors `LS-` framing parity |
|
|
| `(c *client) processLeafSub(argo)` | `golang/nats-server/server/leafnode.go:2720` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs:308` | Read loop parses LS+ lines including optional queue weight. Missing: loop detection check, permission check, subscription insertion into SubList, route/gateway propagation, and Go-equivalent delta/refcount updates |
|
|
| `(c *client) handleLeafNodeLoop(sendErr)` | `golang/nats-server/server/leafnode.go:2860` | PARTIAL | `src/NATS.Server/LeafNodes/LeafLoopDetector.cs:13` | `IsLooped` detects the condition. Missing: sending the error back to remote, closing connection, setting reconnect delay |
|
|
| `(c *client) processLeafUnsub(arg)` | `golang/nats-server/server/leafnode.go:2875` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs:200` | Read loop parses LS- lines. Missing: SubList removal, route/gateway propagation |
|
|
| `(c *client) processLeafHeaderMsgArgs(arg)` | `golang/nats-server/server/leafnode.go:2917` | MISSING | — | Parses LMSG header arguments (header size + total size for NATS headers protocol). Not ported |
|
|
| `(c *client) processLeafMsgArgs(arg)` | `golang/nats-server/server/leafnode.go:3001` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs:213` | .NET read loop parses LMSG lines. Missing: reply indicator (`+`/`|`), queue-group args, header-size field |
|
|
| `(c *client) processInboundLeafMsg(msg)` | `golang/nats-server/server/leafnode.go:3072` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:248` | `ForwardMessageAsync` forwards to all connections; inbound path calls `_messageSink`. Missing: SubList match + fanout to local subscribers, L1 result cache, gateway forwarding |
|
|
| `(c *client) leafSubPermViolation(subj)` | `golang/nats-server/server/leafnode.go:3148` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs` (`LeafSubPermViolation`) | Added subscription violation handler that applies solicited reconnect delay. Remaining: close/log side effects are not yet mirrored |
|
|
| `(c *client) leafPermViolation(pub, subj)` | `golang/nats-server/server/leafnode.go:3155` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs` (`LeafPermViolation`) | Added shared violation handler applying permission reconnect delay for solicited links. Remaining: close/log and error emission path not fully ported |
|
|
| `(c *client) leafProcessErr(errStr)` | `golang/nats-server/server/leafnode.go:3177` | PARTIAL | `src/NATS.Server/LeafNodes/LeafConnection.cs` (`LeafProcessErr`) | Added ERR classifier for permission/loop/cluster-name cases that drives reconnect-delay selection. Remaining: full remote ERR processing and close semantics |
|
|
| `(c *client) setLeafConnectDelayIfSoliciting(delay)` | `golang/nats-server/server/leafnode.go:3196` | PORTED | `src/NATS.Server/LeafNodes/LeafConnection.cs` (`SetLeafConnectDelayIfSoliciting`, `GetConnectDelay`) | Solicited-only delay setter/getter implemented and covered by parity tests |
|
|
| `(c *client) leafNodeGetTLSConfigForSolicit(remote)` | `golang/nats-server/server/leafnode.go:3215` | MISSING | — | Derives TLS config for solicited connection. .NET has no real TLS handshake for leaf nodes |
|
|
| `(c *client) leafNodeSolicitWSConnection(opts, rURL, remote)` | `golang/nats-server/server/leafnode.go:3253` | PARTIAL | `src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs` | `WebSocketStreamAdapter` adapts a WebSocket to a Stream. Missing: HTTP upgrade negotiation (`GET /leafnode` request/response), TLS handshake, compression negotiation, no-masking header |
|
|
|
|
#### Methods on `Server`
|
|
|
|
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|
|
|-----------|:-------------|--------|:----------------|-------|
|
|
| `(s *Server) solicitLeafNodeRemotes(remotes)` | `golang/nats-server/server/leafnode.go:144` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:200` | `StartAsync` iterates `_options.Remotes` and spawns `ConnectSolicitedWithRetryAsync`. Missing: credentials file validation, system account delay, disabled-remote filtering, per-remote NKey/JWT auth |
|
|
| `(s *Server) remoteLeafNodeStillValid(remote)` | `golang/nats-server/server/leafnode.go:200` | PORTED | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:102` | Implemented remote validity guard (configured in remotes/remoteLeaves and not disabled); retry loop now short-circuits when invalid |
|
|
| `(s *Server) updateRemoteLeafNodesTLSConfig(opts)` | `golang/nats-server/server/leafnode.go:432` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:157` | `UpdateTlsConfig` updates cert/key paths. Missing: actual TLS config propagation to existing connections |
|
|
| `(s *Server) reConnectToRemoteLeafNode(remote)` | `golang/nats-server/server/leafnode.go:458` | PORTED | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:583` | `ConnectSolicitedWithRetryAsync` implements reconnect loop with exponential backoff |
|
|
| `(s *Server) setLeafNodeNonExportedOptions()` | `golang/nats-server/server/leafnode.go:549` | NOT_APPLICABLE | — | Sets test-only options (dialTimeout, resolver). .NET uses DI/options; no direct equivalent needed |
|
|
| `(s *Server) connectToRemoteLeafNode(remote, firstConnect)` | `golang/nats-server/server/leafnode.go:625` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:583` | `ConnectSolicitedWithRetryAsync` covers basic TCP connect + retry. Missing: proxy tunnel support, system account delay for first connect, JetStream migrate timer, `isLeafConnectDisabled` check |
|
|
| `(s *Server) clearObserverState(remote)` | `golang/nats-server/server/leafnode.go:768` | MISSING | — | Clears JetStream RAFT observer state after reconnect. RAFT not ported |
|
|
| `(s *Server) checkJetStreamMigrate(remote)` | `golang/nats-server/server/leafnode.go:802` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:367` | `CheckJetStreamMigrate` validates domain conflicts. Missing: actual RAFT StepDown/SetObserver calls |
|
|
| `(s *Server) isLeafConnectDisabled()` | `golang/nats-server/server/leafnode.go:844` | PORTED | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:90` | `IsLeafConnectDisabled(remoteUrl)` / `IsGloballyDisabled` |
|
|
| `(s *Server) startLeafNodeAcceptLoop()` | `golang/nats-server/server/leafnode.go:875` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:200` | `StartAsync` starts an accept loop. Missing: TLS setup, INFO JSON generation, nonce generation, async INFO propagation, `setLeafNodeInfoHostPortAndIP` |
|
|
| `(s *Server) copyLeafNodeInfo()` | `golang/nats-server/server/leafnode.go:1083` | MISSING | — | Deep-copies the leaf node INFO struct. No Info type in .NET leaf module |
|
|
| `(s *Server) addLeafNodeURL(urlStr)` | `golang/nats-server/server/leafnode.go:1096` | MISSING | — | Adds a leaf URL from route and regenerates INFO JSON. Not ported |
|
|
| `(s *Server) removeLeafNodeURL(urlStr)` | `golang/nats-server/server/leafnode.go:1108` | MISSING | — | Removes a leaf URL and regenerates INFO JSON. Not ported |
|
|
| `(s *Server) generateLeafNodeInfoJSON()` | `golang/nats-server/server/leafnode.go:1122` | MISSING | — | Regenerates the serialized INFO JSON bytes. Not ported |
|
|
| `(s *Server) sendAsyncLeafNodeInfo()` | `golang/nats-server/server/leafnode.go:1131` | MISSING | — | Sends async INFO to all connected leaf nodes (URL list updates). Not ported |
|
|
| `(s *Server) createLeafNode(conn, rURL, remote, ws)` | `golang/nats-server/server/leafnode.go:1140` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:566` | `HandleInboundAsync` / `ConnectSolicitedAsync` create connections. Missing: `client` struct setup, maxPay/maxSubs, TLS negotiation, websocket detection, auth timer, temp client registration, read/write loop spawning |
|
|
| `(s *Server) negotiateLeafCompression(c, didSolicit, infoCompression, co)` | `golang/nats-server/server/leafnode.go:1648` | MISSING | — | Negotiates S2 compression mode between hub and leaf. Not ported |
|
|
| `(s *Server) setLeafNodeInfoHostPortAndIP()` | `golang/nats-server/server/leafnode.go:1763` | MISSING | — | Sets advertise host/port for leaf INFO. Not ported |
|
|
| `(s *Server) addLeafNodeConnection(c, srvName, clusterName, checkForDup)` | `golang/nats-server/server/leafnode.go:1811` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:693` | `ValidateRemoteLeafNode` checks for duplicate + domain conflict. Missing: JetStream domain API deny-list merging, system account detection, RAFT observer mode toggling, JS mapping table setup, actual `s.leafs` map insertion |
|
|
| `(s *Server) removeLeafNodeConnection(c)` | `golang/nats-server/server/leafnode.go:1975` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:671` | `WatchConnectionAsync` removes connection on close. Missing: `gwSub` removal from gwLeafSubs, `tsubt` timer stop, proxyKey removal |
|
|
| `(s *Server) checkInternalSyncConsumers(acc)` | `golang/nats-server/server/leafnode.go:2193` | MISSING | — | Kicks JetStream source/mirror consumers after leaf connect. Requires full JetStream integration |
|
|
| `(s *Server) sendPermsAndAccountInfo(c)` | `golang/nats-server/server/leafnode.go:2244` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:292` | `SendPermsAndAccountInfo` syncs allow-lists to connection. Missing: actual INFO JSON protocol send to remote, `IsSystemAccount` flag, `ConnectInfo` flag |
|
|
| `(s *Server) initLeafNodeSmapAndSendSubs(c)` | `golang/nats-server/server/leafnode.go:2264` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:325` | `InitLeafNodeSmapAndSendSubs` sends LS+ for a list of subjects. Missing: gathering subs from SubList, loop detection subject, gateway interest, siReply prefix, tsub transient map, spoke-only local sub filtering |
|
|
| `(s *Server) updateInterestForAccountOnGateway(accName, sub, delta)` | `golang/nats-server/server/leafnode.go:2428` | MISSING | — | Called from gateway code to update leaf node smap. Requires gateway integration |
|
|
| `(s *Server) leafNodeResumeConnectProcess(c)` | `golang/nats-server/server/leafnode.go:3369` | MISSING | — | Sends CONNECT protocol and starts write loop on solicited leaf. Not ported (no CONNECT handshake) |
|
|
| `(s *Server) leafNodeFinishConnectProcess(c)` | `golang/nats-server/server/leafnode.go:3409` | MISSING | — | Registers leaf with account, initialises smap, sends sys connect event. Not ported |
|
|
|
|
#### Methods on `Account`
|
|
|
|
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|
|
|-----------|:-------------|--------|:----------------|-------|
|
|
| `(acc *Account) updateLeafNodesEx(sub, delta, hubOnly)` | `golang/nats-server/server/leafnode.go:2442` | MISSING | — | Propagates sub interest delta to all leaf connections for the account, with hub-only option |
|
|
| `(acc *Account) updateLeafNodes(sub, delta)` | `golang/nats-server/server/leafnode.go:2515` | PARTIAL | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:265` | `PropagateLocalSubscription` / `PropagateLocalUnsubscription` broadcast to all connections. Missing: per-connection smap refcounting, isolated/hub-only filtering, origin-cluster filtering |
|
|
|
|
#### Methods on `leafNodeCfg`
|
|
|
|
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|
|
|-----------|:-------------|--------|:----------------|-------|
|
|
| `(cfg *leafNodeCfg) pickNextURL()` | `golang/nats-server/server/leafnode.go:510` | PORTED | `src/NATS.Server/Configuration/LeafNodeOptions.cs:40` (`RemoteLeafOptions.PickNextUrl`) | Round-robin URL picker implemented with per-remote current URL tracking |
|
|
| `(cfg *leafNodeCfg) getCurrentURL()` | `golang/nats-server/server/leafnode.go:525` | PORTED | `src/NATS.Server/Configuration/LeafNodeOptions.cs:55` (`GetCurrentUrl`) | Current selected URL accessor implemented |
|
|
| `(cfg *leafNodeCfg) getConnectDelay()` | `golang/nats-server/server/leafnode.go:533` | PORTED | `src/NATS.Server/Configuration/LeafNodeOptions.cs:61` (`GetConnectDelay`) | Per-remote connect-delay getter implemented |
|
|
| `(cfg *leafNodeCfg) setConnectDelay(delay)` | `golang/nats-server/server/leafnode.go:541` | PORTED | `src/NATS.Server/Configuration/LeafNodeOptions.cs:67` (`SetConnectDelay`) | Per-remote connect-delay setter implemented |
|
|
| `(cfg *leafNodeCfg) cancelMigrateTimer()` | `golang/nats-server/server/leafnode.go:761` | PORTED | `src/NATS.Server/Configuration/LeafNodeOptions.cs` (`StartMigrateTimer`, `CancelMigrateTimer`) | Added per-remote migrate timer handle with cancellation semantics |
|
|
| `(cfg *leafNodeCfg) saveTLSHostname(u)` | `golang/nats-server/server/leafnode.go:858` | PORTED | `src/NATS.Server/Configuration/LeafNodeOptions.cs:73` (`SaveTlsHostname`) | TLS hostname extraction from URL implemented |
|
|
| `(cfg *leafNodeCfg) saveUserPassword(u)` | `golang/nats-server/server/leafnode.go:866` | PORTED | `src/NATS.Server/Configuration/LeafNodeOptions.cs:83` (`SaveUserPassword`) | Username/password extraction from URL user-info implemented |
|
|
|
|
#### Standalone Functions
|
|
|
|
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|
|
|-----------|:-------------|--------|:----------------|-------|
|
|
| `validateLeafNode(o *Options)` | `golang/nats-server/server/leafnode.go:214` | MISSING | — | Validates all leaf node config options (accounts, operator mode, TLS, proxy, compression). Not ported |
|
|
| `checkLeafMinVersionConfig(mv)` | `golang/nats-server/server/leafnode.go:343` | MISSING | — | Validates minimum version string. Not ported |
|
|
| `validateLeafNodeAuthOptions(o)` | `golang/nats-server/server/leafnode.go:357` | MISSING | — | Validates single-user vs. multi-user leaf auth config. Not ported |
|
|
| `validateLeafNodeProxyOptions(remote)` | `golang/nats-server/server/leafnode.go:377` | MISSING | — | Validates HTTP proxy options for WebSocket leaf remotes. Not ported |
|
|
| `newLeafNodeCfg(remote)` | `golang/nats-server/server/leafnode.go:470` | PARTIAL | `src/NATS.Server/Configuration/LeafNodeOptions.cs` | `RemoteLeafOptions` covers URLs and credentials. Missing: URL randomization, per-URL TLS hostname/password extraction, WS TLS detection |
|
|
| `establishHTTPProxyTunnel(proxyURL, targetHost, timeout, username, password)` | `golang/nats-server/server/leafnode.go:565` | MISSING | — | Establishes an HTTP CONNECT tunnel through an HTTP proxy for WebSocket leaf connections. Not ported |
|
|
| `keyFromSub(sub)` | `golang/nats-server/server/leafnode.go:2638` | PORTED | `src/NATS.Server/LeafNodes/LeafSubKey.cs:19` (`KeyFromSub`) | Helper now builds `subject` or `subject queue` keys matching Go key shape |
|
|
| `keyFromSubWithOrigin(sub)` | `golang/nats-server/server/leafnode.go:2664` | PORTED | `src/NATS.Server/LeafNodes/LeafSubKey.cs:27` (`KeyFromSubWithOrigin`) | Routed key builder now emits `R ...` and `L ...` forms with optional queue/origin segments |
|
|
|
|
#### Constants in smap key helpers
|
|
|
|
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|
|
|-----------|:-------------|--------|:----------------|-------|
|
|
| `keyRoutedSub`, `keyRoutedSubByte` (const `"R"`) | `golang/nats-server/server/leafnode.go:2651` | PORTED | `src/NATS.Server/LeafNodes/LeafSubKey.cs:11-12` | Routed-sub key prefix constants are defined for parity |
|
|
| `keyRoutedLeafSub`, `keyRoutedLeafSubByte` (const `"L"`) | `golang/nats-server/server/leafnode.go:2653` | PORTED | `src/NATS.Server/LeafNodes/LeafSubKey.cs:13-14` | Routed-leaf-sub key prefix constants are defined for parity |
|
|
| `sharedSysAccDelay` (const 250ms) | `golang/nats-server/server/leafnode.go:562` | PORTED | `src/NATS.Server/LeafNodes/LeafSubKey.cs:16` | Shared system-account connect delay constant added (`250ms`) |
|
|
| `connectProcessTimeout` (const 2s) | `golang/nats-server/server/leafnode.go:3365` | PORTED | `src/NATS.Server/LeafNodes/LeafSubKey.cs:17` | Connect-process timeout constant added (`2s`) |
|
|
|
|
#### .NET-only additions (no Go equivalent — extensions)
|
|
|
|
| .NET Symbol | .NET File:Line | Notes |
|
|
|-------------|:---------------|-------|
|
|
| `LeafConnection.SetPermissions()` | `src/NATS.Server/LeafNodes/LeafConnection.cs:66` | Allows-list sync API — exposes what Go does internally |
|
|
| `LeafNodeManager.DisableLeafConnect()` / `EnableLeafConnect()` | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:98` | Per-remote disable/enable API (Go uses a simple bool flag) |
|
|
| `LeafNodeManager.DisableAllLeafConnections()` / `EnableAllLeafConnections()` | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:121` | Global disable API |
|
|
| `LeafNodeManager.ComputeBackoff(attempt)` | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:541` | Exponential backoff utility (Go uses fixed `reconnectDelay + jitter`) |
|
|
| `LeafNodeManager.RegisterLeafNodeCluster()` / `UnregisterLeafNodeCluster()` | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:446` | Cluster topology registry (partially mirrors Go's `registerLeafNodeCluster`) |
|
|
| `LeafHubSpokeMapper.Map()` | `src/NATS.Server/LeafNodes/LeafHubSpokeMapper.cs:77` | Account mapping hub↔spoke (Go does this inline in client publish path) |
|
|
| `LeafHubSpokeMapper.IsSubjectAllowed()` | `src/NATS.Server/LeafNodes/LeafHubSpokeMapper.cs:92` | Allow/deny list filtering (mirrors Go's permission check in `sendLeafNodeSubUpdate`) |
|
|
| `LeafLoopDetector.Mark()` / `IsLooped()` / `TryUnmark()` | `src/NATS.Server/LeafNodes/LeafLoopDetector.cs` | Loop detection helpers (Go does this inline in `processLeafSub`) |
|
|
| `WebSocketStreamAdapter` | `src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs` | WebSocket→Stream adapter; Go uses its own ws read/write path in client.go |
|
|
| `LeafPermSyncResult`, `LeafTlsReloadResult`, `LeafValidationResult` | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:758` | Result types for .NET API surface (Go returns error tuples) |
|
|
| `JetStreamMigrationResult`, `JetStreamMigrationStatus` | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:804` | JetStream migration result type |
|
|
| `LeafClusterInfo` | `src/NATS.Server/LeafNodes/LeafNodeManager.cs:829` | Cluster topology entry (partial Go analog) |
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Status | Count |
|
|
|--------|-------|
|
|
| PORTED | 29 |
|
|
| PARTIAL | 28 |
|
|
| MISSING | 26 |
|
|
| NOT_APPLICABLE | 1 |
|
|
| DEFERRED | 0 |
|
|
| **Total** | **84** |
|
|
|
|
### Key Gaps
|
|
|
|
The .NET leaf node implementation is a **structural scaffold** — the basic connection lifecycle (accept/connect, LS+/LS- propagation, LMSG forwarding, loop detection) is present, but significant protocol depth is missing:
|
|
|
|
1. **CONNECT flow is only partially wired**: .NET now has `LeafConnectInfo` + `SendLeafConnectAsync`, but solicited connection flow still primarily handshakes with `LEAF <id>` and does not yet fully mirror Go connect-process sequencing.
|
|
2. **No smap (subject map)**: Go maintains a per-connection reference-counted map (`leaf.smap`) to deduplicate LS+/LS- traffic. .NET broadcasts blindly to all connections.
|
|
3. **No INFO protocol handling**: Dynamic URL list updates, compression negotiation, and permission updates over async INFO are unimplemented.
|
|
4. **No compression**: S2 compression negotiation between hub and leaf is entirely absent.
|
|
5. **No HTTP proxy tunnel**: `establishHTTPProxyTunnel` for WebSocket-via-proxy leaf connections is not ported.
|
|
6. **No SubList integration**: Inbound leaf subscriptions (LS+) are not inserted into the server's SubList trie, so received leaf subscriptions do not create actual server-side subscription state for message routing.
|
|
7. **No route/gateway propagation**: When a leaf sub arrives, Go also updates routes and gateways; .NET does not.
|
|
8. **No reconnect delay enforcement**: After loop detection, permission violations, or cluster name collision, Go enforces a 30-second reconnect delay; .NET detects but does not enforce.
|
|
9. **No JetStream RAFT integration**: `clearObserverState`, `checkJetStreamMigrate` (actual RAFT step-down), and `checkInternalSyncConsumers` are all absent.
|
|
10. **WebSocket path incomplete**: The `WebSocketStreamAdapter` adapts the byte stream, but the HTTP upgrade handshake (`GET /leafnode` with `Sec-WebSocket-Key`) is not implemented for solicited WS leaf connections.
|
|
|
|
---
|
|
|
|
## Keeping This File Updated
|
|
|
|
After porting work is completed:
|
|
|
|
1. **Update status**: Change `MISSING → PORTED` or `PARTIAL → PORTED` for each item completed
|
|
2. **Add .NET path**: Fill in the ".NET Equivalent" column with the actual file:line
|
|
3. **Re-count LOC**: Update the LOC numbers in `stillmissing.md`:
|
|
```bash
|
|
# Re-count .NET source LOC for this module
|
|
find src/NATS.Server/LeafNodes/ -name '*.cs' -type f -exec cat {} + | wc -l
|
|
# Re-count .NET test LOC for this module
|
|
find tests/NATS.Server.Tests/LeafNodes/ -name '*.cs' -type f -exec cat {} + | wc -l
|
|
```
|
|
4. **Add a changelog entry** below with date and summary of what was ported
|
|
5. **Update the parity DB** if new test mappings were created:
|
|
```bash
|
|
sqlite3 docs/test_parity.db "INSERT INTO test_mappings (go_test_id, dotnet_test_id, confidence, notes) VALUES (?, ?, 'manual', 'ported in YYYY-MM-DD session')"
|
|
```
|
|
|
|
## Change Log
|
|
|
|
| Date | Change | By |
|
|
|------|--------|----|
|
|
| 2026-02-25 | File created with LLM analysis instructions | auto |
|
|
| 2026-02-25 | Full gap inventory populated: 62 symbols classified (5 PORTED, 18 PARTIAL, 38 MISSING, 1 NOT_APPLICABLE) | claude-sonnet-4-6 |
|
|
| 2026-02-25 | Ported leaf helper parity batch: role predicates on `LeafConnection`, remote-validity guard in reconnect loop, remote leaf config URL/delay/TLS/userinfo helpers, and reconnect/wait constants; added focused tests and updated gap statuses | codex |
|
|
| 2026-02-25 | Ported leaf smap-key parity helper batch: added routed key constants and key builders (`KeyFromSub`, `KeyFromSubWithOrigin`) plus `sharedSysAccDelay` and `connectProcessTimeout` constants with focused tests | codex |
|
|
| 2026-02-26 | Ported leaf ERR/connect-delay/connect-info parity batch: added `LeafConnectInfo`, `SendLeafConnectAsync`, `RemoteCluster()` parsing, solicited connect-delay handlers (`SetLeafConnectDelayIfSoliciting`, `LeafProcessErr`, permission-violation helpers), and `RemoteLeafOptions` migrate timer cancellation helpers with focused parity tests | codex |
|
|
| 2026-02-26 | Ported leaf LS+ queue-weight parity batch: added weighted LS+ emission/parsing (`SendLsPlusAsync` overload + read-loop queue-weight extraction), updated leaf manager propagation API to pass weights, and added focused parity tests | codex |
|
|
| 2026-02-26 | Ported leaf send-side permission gate parity for spoke links: `PropagateLocalSubscription` now enforces spoke subscribe allow-list semantics (with loop/gateway bypass subjects), with wire-level focused tests | codex |
|