Files
natsdotnet/gaps/routes.md
Joseph Doherty c30e67a69d Fix E2E test gaps and add comprehensive E2E + parity test suites
- 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
2026-03-12 14:09:23 -04:00

191 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Routes — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Routes** 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 Routes
- Route connections use the same `client.go` read/write loop but with `ClientKind = ROUTER`.
- Route pooling sends different accounts over different connections to avoid head-of-line blocking.
- `RS+`/`RS-` are subscription interest propagation messages between clustered servers.
- `RMSG` is the routed message format (differs from client `MSG`).
---
## Go Reference Files (Source)
- `golang/nats-server/server/route.go` — Full-mesh cluster routes (~3,300 lines). Route pooling (default 3 connections per peer). Account-specific dedicated routes. Protocol: `RS+`/`RS-` for subscribe propagation, `RMSG` for routed messages.
## Go Reference Files (Tests)
- `golang/nats-server/server/routes_test.go`
- `golang/nats-server/test/routes_test.go` (integration)
- `golang/nats-server/test/new_routes_test.go` (integration)
## .NET Implementation Files (Source)
- `src/NATS.Server/Routes/` (all files)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Routes/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### `golang/nats-server/server/route.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `RouteType` (type alias + consts `Implicit`/`Explicit`) | route.go:3644 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs` — implicit/explicit distinction tracked in `RouteConnection` handshake and `RouteManager.ConnectToRouteWithRetryAsync` | No explicit enum; Implicit/Explicit distinction is encoded in how routes are established (solicited vs inbound) |
| `route` struct (unexported) | route.go:5694 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:8` | Fields `remoteID`, `poolIdx`, `accName`, `noPool`, `compression`, `gossipMode` are present. Fields for `lnoc`, `lnocu`, `jetstream`, `connectURLs`, `wsConnURLs`, `gatewayURL`, `leafnodeURL`, `hash`, `idHash`, `startNewRoute`, `retry` are MISSING — not modelled in .NET |
| `routeInfo` struct (unexported) | route.go:97101 | MISSING | — | Used internally for deferred pool-connection creation after first PONG; no .NET equivalent |
| `gossipDefault`/`gossipDisabled`/`gossipOverride` consts | route.go:104108 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:1719` | Gossip mode constants are defined as byte constants (`GossipDefault`, `GossipDisabled`, `GossipOverride`) |
| `connectInfo` struct | route.go:110124 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:378` (`BuildConnectInfoJson`) | Connect payload now includes parity fields: `echo`, `verbose`, `pedantic`, `tls_required`, `headers`, `cluster`, `dynamic`, `lnoc`, `lnocu` |
| `ConProto`/`InfoProto` protocol format strings | route.go:127130 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:1112,8395` | CONNECT/INFO format constants added, but active wire handshake remains simplified `ROUTE <serverId>` rather than full CONNECT/INFO exchange |
| `clusterTLSInsecureWarning` const | route.go:134 | NOT_APPLICABLE | — | TLS not yet implemented in .NET port; warning string has no counterpart |
| `defaultRouteMaxPingInterval` const | route.go:140 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:20` | `DefaultRouteMaxPingInterval` constant added |
| `routeConnectDelay`/`routeConnectMaxDelay`/`routeMaxPingInterval` vars | route.go:145148 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:1415,20,751757` | Route reconnect delay now uses bounded exponential backoff (`ComputeRetryDelay`) with dedicated delay/max constants; still not runtime-configurable from route config |
| `(c *client) removeReplySub` | route.go:151 | MISSING | — | Reply-sub cleanup for remote reply subs not implemented |
| `(c *client) processAccountSub` | route.go:167 | NOT_APPLICABLE | — | Gateway-only path; gateway sub interest not in routes module |
| `(c *client) processAccountUnsub` | route.go:174 | NOT_APPLICABLE | — | Gateway-only path |
| `(c *client) processRoutedOriginClusterMsgArgs` | route.go:182 | MISSING | — | LMSG (origin-cluster routed msg) arg parsing not implemented; .NET only handles RMSG |
| `(c *client) processRoutedHeaderMsgArgs` | route.go:281 | MISSING | — | HMSG (header msg from route) arg parsing not implemented |
| `(c *client) processRoutedMsgArgs` | route.go:378 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:187222` (`ReadFramesAsync`) | .NET parses basic RMSG account/subject/reply/size. Missing: queue-group routing indicators (`+`/`|`), header size field, multi-field arg parsing |
| `(c *client) processInboundRoutedMsg` | route.go:460 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:219221` | .NET fires `RoutedMessageReceived` callback; missing: stats update, gateway reply handling (`handleGatewayReply`), account lookup and fanout via `processMsgResults` |
| `(c *client) sendRouteConnect` | route.go:503 | MISSING | — | Outbound CONNECT protocol (with cluster auth, TLS flags, etc.) not sent; .NET uses a simpler `ROUTE <serverId>` handshake |
| `computeRoutePoolIdx` | route.go:538 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:149` (`ComputeRoutePoolIdx`) | FNV-1a 32-bit hash, identical algorithm |
| `(c *client) processRouteInfo` | route.go:549 | MISSING | — | Full INFO processing (cluster name negotiation, compression negotiation, duplicate detection, account route setup) not implemented; .NET handshake is a simple ID exchange |
| `(s *Server) negotiateRouteCompression` | route.go:897 | PARTIAL | `src/NATS.Server/Routes/RouteCompressionCodec.cs:82` (`NegotiateCompression`) | .NET has the negotiation logic; but integration into handshake (INFO exchange, switching compression writer/reader mid-stream) is MISSING |
| `(s *Server) updateRemoteRoutePerms` | route.go:953 | MISSING | — | Route permission update on INFO reload not implemented |
| `(s *Server) sendAsyncInfoToClients` | route.go:1015 | PORTED | `src/NATS.Server/NatsServer.cs:181` | Added `UpdateServerINFOAndSendINFOToClients()` INFO refresh/broadcast path to connected clients; validated with targeted socket-level parity test. |
| `(s *Server) processImplicitRoute` | route.go:1043 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:115` (`ProcessImplicitRoute`) | Now guards discovered URLs with `HasThisRouteConfigured` and normalized duplicate checks; pinned-account re-solicitation and duplicate-ID handling remain missing |
| `(s *Server) hasThisRouteConfigured` | route.go:1104 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:164` | Implemented with normalized URL matching against explicit configured routes and known route URLs |
| `(s *Server) forwardNewRouteInfoToKnownServers` | route.go:1139 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:127` (`ForwardNewRouteInfoToKnownServers`) | .NET raises an event with the new peer URL; missing: gossip mode logic (`gossipDefault`/`gossipDisabled`/`gossipOverride`), pinned-account route filtering, serialized INFO JSON sending |
| `(c *client) canImport` | route.go:1226 | MISSING | — | Route import permission check not implemented |
| `(c *client) canExport` | route.go:1235 | MISSING | — | Route export permission check not implemented |
| `(c *client) setRoutePermissions` | route.go:1244 | MISSING | — | Route permission mapping (Import→Publish, Export→Subscribe) not implemented |
| `asubs` struct | route.go:1263 | NOT_APPLICABLE | — | Internal Go helper to group subscriptions by account during cleanup; .NET uses LINQ equivalents |
| `getAccNameFromRoutedSubKey` | route.go:1273 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:176` (`GetAccNameFromRoutedSubKey`) | Routed-sub key parsing helper added and validated with parity tests |
| `(c *client) getRoutedSubKeyInfo` | route.go:1290 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:179` (`GetRoutedSubKeyInfo`) | Routed-sub key decomposition helper added (route/account/subject/queue) and covered by tests |
| `(c *client) removeRemoteSubs` | route.go:1299 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:195` (`RemoveRemoteSubs`), `src/NATS.Server/Routes/RouteManager.cs:593` (`WatchRouteAsync`), `src/NATS.Server/NatsServer.cs:960` (`RemoveRemoteSubscriptionsForRoute`) | Route close/removal now triggers remote-sub cleanup from account SubLists when the last connection for a remote server is gone |
| `(c *client) removeRemoteSubsForAcc` | route.go:1352 | PARTIAL | `src/NATS.Server/Subscriptions/SubList.cs:229` (`RemoveRemoteSubsForAccount`), `src/NATS.Server/Routes/RouteManager.cs:286` (`UnregisterAccountRoute`), `src/NATS.Server/NatsServer.cs:966` (`RemoveRemoteSubscriptionsForRouteAccount`) | Per-account cleanup path is now wired on dedicated-route unregistration; full dedicated-route transition parity remains incomplete |
| `(c *client) parseUnsubProto` | route.go:1366 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:357` (`TryParseRemoteUnsub`) | Dedicated RS-/LS- parser now handles account/subject/optional-queue extraction and is used by frame processing |
| `(c *client) processRemoteUnsub` | route.go:1404 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:177185` | .NET fires `RemoteSubscriptionReceived` with `IsRemoval=true`; missing: sub key lookup and removal from SubList, gateway/leafnode interest updates |
| `(c *client) processRemoteSub` | route.go:1489 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:167175` | .NET fires `RemoteSubscriptionReceived`; missing: key construction with type byte prefix, account lookup/creation, permission check (`canExport`), SubList insertion, gateway/leafnode updates, queue-weight delta tracking |
| `(c *client) addRouteSubOrUnsubProtoToBuf` | route.go:1729 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:179` (`SendRouteSubOrUnSubProtosAsync`) | Added low-level buffered route-proto sender that batches RS+/RS-/LS+/LS- control lines into a single write/flush. |
| `(s *Server) sendSubsToRoute` | route.go:1781 | MISSING | — | Bulk send of local subscription interest to newly connected route not implemented; .NET only propagates incremental sub/unsub |
| `(c *client) sendRouteSubProtos` | route.go:1881 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:146` (`SendRouteSubProtosAsync`) | Added batched RS+ emission from remote-subscription models with queue/weight support. |
| `(c *client) sendRouteUnSubProtos` | route.go:1890 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:165` (`SendRouteUnSubProtosAsync`) | Added batched RS- emission from remote-subscription models. |
| `(c *client) sendRouteSubOrUnSubProtos` | route.go:1898 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:179` (`SendRouteSubOrUnSubProtosAsync`) | Added low-level batch sender for arbitrary route sub/unsub protocol lines. |
| `(s *Server) createRoute` | route.go:1935 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:447,462` (`HandleInboundRouteAsync`/`ConnectToRouteWithRetryAsync`) | .NET creates a RouteConnection and performs handshake; missing: TLS setup, auth timeout timer, CONNECT protocol sending, INFO JSON sending, compression negotiation, ping timer |
| `routeShouldDelayInfo` | route.go:2082 | MISSING | — | Logic to delay initial INFO until pool connection auth is confirmed not implemented |
| `(s *Server) generateRouteInitialInfoJSON` | route.go:2090 | MISSING | — | Route INFO JSON generation (with nonce, pool index, gossip mode, compression) not implemented |
| `(s *Server) addRoute` | route.go:2113 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:496` (`Register`) | .NET registers route in dictionary; missing: pool index management, duplicate detection with `handleDuplicateRoute`, per-account route registration in `accRoutes`, `sendSubsToRoute` call, gateway/leafnode URL propagation, `forwardNewRouteInfoToKnownServers` |
| `hasSolicitedRoute` | route.go:2438 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:721` | Implemented helper to query whether a given remote server currently has a solicited route |
| `upgradeRouteToSolicited` | route.go:2458 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:730` | Implemented route upgrade helper to flip an existing route into solicited mode |
| `handleDuplicateRoute` | route.go:2473 | MISSING | — | Duplicate route resolution (close extra connection, preserve retry flag) not implemented |
| `(c *client) importFilter` | route.go:2510 | MISSING | — | Permission-based subscription filter for sending to routes not implemented |
| `(s *Server) updateRouteSubscriptionMap` | route.go:2519 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:381,392` (`PropagateLocalSubscription`/`PropagateLocalUnsubscription`) | .NET broadcasts RS+/RS- to all routes; missing: account routePoolIdx-based routing, queue-weight dedup (`sqmu`/`lqws`), no-pool route handling, gateway/leafnode interest updates |
| `(s *Server) startRouteAcceptLoop` | route.go:2696 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:333` (`StartAsync`) | .NET binds and starts accept loop, solicits configured routes; missing: cluster name logging, TLS config on accept, routeInfo construction, advertise/NoAdvertise, LeafNode/Gateway URL propagation |
| `(s *Server) setRouteInfoHostPortAndIP` | route.go:2829 | MISSING | — | Route INFO host/port/IP with Cluster.Advertise support not implemented |
| `(s *Server) StartRouting` | route.go:2849 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:333` (`StartAsync`) | Functionally equivalent: starts accept loop and solicits routes |
| `(s *Server) reConnectToRoute` | route.go:2861 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:512` (`ConnectToRouteWithRetryAsync`) | Retry loop now uses bounded exponential backoff and route validity guard; jitter, explicit/implicit-specific delay behavior, and quit-channel parity remain missing |
| `(s *Server) routeStillValid` | route.go:2881 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:180` | Implemented reconnect guard that validates configured and discovered route URLs using normalized comparisons |
| `(s *Server) connectToRoute` | route.go:2890 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:512` (`ConnectToRouteWithRetryAsync`) | Exponential backoff and route-validity checks are implemented; ConnectRetries/ConnectBackoff config parity, routes-to-self exclusion, and DNS randomization are still missing |
| `(c *client) isSolicitedRoute` | route.go:2976 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:35,370` | `IsSolicited` state is tracked on route connections and exposed via `IsSolicitedRoute()` helper |
| `(s *Server) saveRouteTLSName` | route.go:2985 | NOT_APPLICABLE | — | TLS not yet implemented in .NET port |
| `(s *Server) solicitRoutes` | route.go:2996 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:347354` | .NET solicits configured routes with pool connections; missing: per-account (pinned) route solicitation, `saveRouteTLSName` |
| `(c *client) processRouteConnect` | route.go:3011 | MISSING | — | Parsing and validation of inbound CONNECT from route (cluster name check, wrong-port detection, LNOC/LNOCU flags) not implemented; .NET uses a simpler handshake |
| `(s *Server) removeAllRoutesExcept` | route.go:3085 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:602` (`RemoveAllRoutesExcept`) | Equivalent behavior: remove all routes not in the keep-set |
| `(s *Server) removeRoute` | route.go:3113 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:632` (`RemoveRoute`) | Remove path now also cleans hash index and per-account route mappings tied to removed connections; gateway/leafnode URL withdrawal, noPool counters, and reconnect-after-noPool logic remain missing |
| `(s *Server) isDuplicateServerName` | route.go:3233 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:748` | Duplicate server-name detection helper implemented against current connected-server ID set |
| `(s *Server) forEachNonPerAccountRoute` | route.go:3263 | NOT_APPLICABLE | — | Internal Go iterator over route slice; .NET uses `_routes.Values` LINQ directly |
| `(s *Server) forEachRoute` | route.go:3277 | NOT_APPLICABLE | — | Internal Go iterator; .NET enumerates `_routes` and `_accountRoutes` directly |
| `(s *Server) forEachRouteIdx` | route.go:3292 | NOT_APPLICABLE | — | Internal Go pool-index iterator; .NET `ComputeRoutePoolIdx` achieves equivalent selection |
| `(s *Server) forEachRemote` | route.go:3305 | NOT_APPLICABLE | — | Internal Go iterator (first non-nil per remote); .NET has no equivalent but uses LINQ |
---
## 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/Routes/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Routes/ -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-26 | Ported async INFO broadcast parity slice: wired/validated `UpdateServerINFOAndSendINFOToClients()` as the `sendAsyncInfoToClients` equivalent and added targeted socket broadcast test (`RouteInfoBroadcastParityBatch4Tests`). | codex |
| 2026-02-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: 57 Go symbols classified across route.go (3,314 lines). Counts: PORTED 4, PARTIAL 21, MISSING 23, NOT_APPLICABLE 9, DEFERRED 0 | auto |
| 2026-02-25 | Ported route parity helper batch: gossip/default constants, connect-info parity fields, configured-route/reconnect guard helpers, solicited-route helpers, duplicate-server-name detection, RS-/LS- parser, and LS+/LS- + queue-weight wire helpers; updated row statuses and notes | codex |
| 2026-02-25 | Ported routed-sub key helpers and remote-sub cleanup parity batch: added `getAccNameFromRoutedSubKey`/`getRoutedSubKeyInfo` equivalents plus route-close and per-account cleanup plumbing with targeted tests | codex |
| 2026-02-25 | Ported route batch-proto parity batch: added buffered batch sender APIs (`SendRouteSubProtosAsync`, `SendRouteUnSubProtosAsync`, `SendRouteSubOrUnSubProtosAsync`) for RS+/RS-/LS+/LS- protocol frames with targeted tests (`RouteBatchProtoParityBatch3Tests`) | codex |