Files
natsdotnet/gaps/raft.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

392 lines
34 KiB
Markdown

# RAFT — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **RAFT** 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 RAFT
- RAFT is used for JetStream cluster consensus — both meta-cluster (stream/consumer placement) and per-stream/consumer groups.
- Key operations: leader election, log append/commit, snapshotting, peer management.
- The .NET implementation (20 files, 3,045 LOC) is more granular than Go's single file. Check if the decomposition covers all RAFT states.
---
## Go Reference Files (Source)
- `golang/nats-server/server/raft.go` — RAFT consensus for clustered JetStream (~5,037 lines). Meta-cluster for metadata, per-stream/consumer RAFT groups. Leader election, log replication, snapshotting.
## Go Reference Files (Tests)
- `golang/nats-server/server/raft_test.go`
- `golang/nats-server/server/raft_helpers_test.go`
- `golang/nats-server/server/raft_chain_of_blocks_helpers_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Raft/` (all 20 files)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Raft/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### golang/nats-server/server/raft.go — Exported Interfaces & Types
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| RaftNode (interface) | raft.go:40-92 | PARTIAL | src/NATS.Server/Raft/IRaftNode.cs:5 | Interface declared but empty — none of the 40+ methods from Go are defined |
| RaftNodeCheckpoint (interface) | raft.go:98-103 | PARTIAL | src/NATS.Server/Raft/RaftSnapshotCheckpoint.cs:7 | Chunk assembly exists but LoadLastSnapshot, AppendEntriesSeq, Abort, InstallSnapshot not matching Go's interface contract |
| WAL (interface) | raft.go:105-118 | PARTIAL | src/NATS.Server/Raft/RaftWal.cs:20 | .NET RaftWal is a concrete file-based WAL; does not implement Go's WAL interface (StoreMsg, LoadMsg, RemoveMsg, Compact, Purge, Truncate, State, FastState, Stop, Delete) |
| Peer (struct) | raft.go:120-125 | PORTED | src/NATS.Server/Raft/RaftPeerState.cs | Added missing parity fields (`Lag`, `Current`) plus helpers to recalculate lag and refresh current-state from heartbeat/contact timing |
| RaftState (enum) | raft.go:127-135 | PORTED | src/NATS.Server/Raft/RaftState.cs:4 | All four states: Follower, Leader, Candidate, Closed |
| RaftState.String() | raft.go:137-149 | PORTED | src/NATS.Server/Raft/RaftStateExtensions.cs:9 | Added Go-style `RaftState.String()` extension mapping to Follower/Leader/Candidate/Closed |
| RaftConfig (struct) | raft.go:301-317 | PORTED | src/NATS.Server/Raft/RaftConfig.cs:8 | Added `RaftConfig` model with Name/Store/Log/Track/Observer/Recovering/ScaleUp fields for parity shape |
| CommittedEntry (struct) | raft.go:2506-2509 | PORTED | src/NATS.Server/Raft/CommitQueue.cs (`CommittedEntry`) | Added explicit committed-entry shape with `Index` and `Entries` list for parity with Go commit delivery payload |
| Entry (struct) | raft.go:2641-2644 | PORTED | src/NATS.Server/Raft/RaftEntry.cs | Added general `RaftEntry` model (`Type`, `Data`) with conversion helpers to/from wire-entry shape |
| EntryType (enum) | raft.go:2605-2619 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:54-63 | All types present including EntryCatchup (mapped as RaftEntryType) |
### golang/nats-server/server/raft.go — Exported RaftNode Interface Methods
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Propose() | raft.go:909-924 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:308 | ProposeAsync exists but synchronous replication model, no write-error checking, no proposal queue |
| ProposeMulti() | raft.go:928-945 | PORTED | src/NATS.Server/Raft/RaftNode.cs (`ProposeMultiAsync`) | Added ordered multi-command proposal API returning committed indexes per input command |
| ForwardProposal() | raft.go:949-959 | PARTIAL | src/NATS.Server/Raft/NatsRaftTransport.cs:182 | Transport has ForwardProposal but RaftNode does not call it automatically for non-leaders |
| InstallSnapshot() | raft.go:1295-1311 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:699 | InstallSnapshotAsync exists but no checkpointing, no WAL compaction, no highwayhash verification |
| CreateSnapshotCheckpoint() | raft.go:1356-1360 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:519 | CreateSnapshotCheckpointAsync exists but simplified — no async write, no WAL compaction tracking |
| SendSnapshot() | raft.go:1284-1290 | MISSING | — | No direct snapshot send as append entry |
| NeedSnapshot() | raft.go:1551-1555 | MISSING | — | No equivalent check |
| Applied() | raft.go:1183-1185 | PORTED | src/NATS.Server/Raft/RaftNode.cs:403 | Added `Applied(long)` callback returning `(entries,bytes)` and delegating progress to processed tracking |
| Processed() | raft.go:1193-1240 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:664 | MarkProcessed exists but much simpler — no aflr signaling, no leader state transition, no byte estimation |
| State() | raft.go:2025-2027 | PORTED | src/NATS.Server/Raft/RaftNode.cs:43 | Role property (uses RaftRole enum instead of RaftState) |
| Size() | raft.go:2037-2043 | PORTED | src/NATS.Server/Raft/RaftNode.cs:771 | Added `Size()` accessor returning entry count and UTF-8 command-byte estimate from current log contents |
| Progress() | raft.go:2030-2034 | PORTED | src/NATS.Server/Raft/RaftNode.cs:765 | Added `Progress()` accessor returning `(index, commit, applied)` |
| Leader() | raft.go:1712-1717 | PORTED | src/NATS.Server/Raft/RaftNode.cs:42 | IsLeader property |
| LeaderSince() | raft.go:1721-1726 | PORTED | src/NATS.Server/Raft/RaftNode.cs:63 | Added nullable `LeaderSince` timestamp and leadership transition updates |
| Quorum() | raft.go:3070-3083 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:201 | HasQuorum() exists but uses different window calculation (2x electionTimeout vs Go's lostQuorumInterval) |
| Current() | raft.go:1840-1847 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:879 | IsCurrent exists but no commit==applied check, no forward-progress polling |
| Healthy() | raft.go:1850-1857 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:892 | IsHealthy exists but different semantics — checks peer responsiveness, not isCurrent(true) |
| Term() | raft.go:3119-3123 | PORTED | src/NATS.Server/Raft/RaftNode.cs:41 | Term property |
| Leaderless() | raft.go:1876-1883 | PORTED | src/NATS.Server/Raft/RaftNode.cs:65 | Added lock-free `Leaderless` derived from tracked group leader state |
| GroupLeader() | raft.go:1865-1872 | PORTED | src/NATS.Server/Raft/RaftNode.cs:64 | Added `GroupLeader` tracking, updated on election/heartbeat/stepdown |
| HadPreviousLeader() | raft.go:1860-1862 | PORTED | src/NATS.Server/Raft/RaftNode.cs:66 | Added `HadPreviousLeader` flag that remains true after first observed leader |
| StepDown() | raft.go:1900-1977 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:706 | RequestStepDown exists but no preferred leader selection, no leader transfer, no EntryLeaderTransfer |
| SetObserver() | raft.go:2394-2396 | PORTED | src/NATS.Server/Raft/RaftNode.cs:799 | Added `SetObserver(bool)` toggle for observer mode |
| IsObserver() | raft.go:2387-2391 | PORTED | src/NATS.Server/Raft/RaftNode.cs:68 | Added `IsObserver` accessor |
| Campaign() | raft.go:1980-1984 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:771 | CampaignImmediately exists but no random campaign timeout |
| CampaignImmediately() | raft.go:1987-1993 | PORTED | src/NATS.Server/Raft/RaftNode.cs:771 | CampaignImmediately() |
| ID() | raft.go:2045-2051 | PORTED | src/NATS.Server/Raft/RaftNode.cs:40 | Id property |
| Group() | raft.go:2053-2056 | PORTED | src/NATS.Server/Raft/RaftNode.cs:42 | Added `GroupName` tracking on `RaftNode` with constructor support; defaults to node ID when unspecified |
| Peers() | raft.go:2058-2077 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:872 | GetPeerStates returns Dict but missing Lag calculation |
| ProposeKnownPeers() | raft.go:2080-2089 | MISSING | — | No peer state broadcast |
| UpdateKnownPeers() | raft.go:2092-2096 | MISSING | — | No peer state update |
| ProposeAddPeer() | raft.go:962-983 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:372 | ProposeAddPeerAsync exists but synchronous replication, no forwarding to leader |
| ProposeRemovePeer() | raft.go:986-1019 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:415 | ProposeRemovePeerAsync exists but no forwarding, no self-removal handling |
| MembershipChangeInProgress() | raft.go:1021-1025 | PORTED | src/NATS.Server/Raft/RaftNode.cs:67 | MembershipChangeInProgress property |
| AdjustClusterSize() | raft.go:1059-1079 | PORTED | src/NATS.Server/Raft/RaftNode.cs:790 | Added leader-gated cluster-size adjustment with Go floor behavior (`min 2`) |
| AdjustBootClusterSize() | raft.go:1038-1055 | PORTED | src/NATS.Server/Raft/RaftNode.cs:781 | Added boot-time cluster-size adjustment gated on no current/previous leader |
| ClusterSize() | raft.go:1029-1033 | PORTED | src/NATS.Server/Raft/RaftNode.cs:778 | Added explicit `ClusterSize()` accessor |
| ApplyQ() | raft.go:2106 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:53 | CommitQueue exists as Channel-based queue, different API than ipQueue |
| PauseApply() | raft.go:1084-1092 | MISSING | — | No apply pausing |
| ResumeApply() | raft.go:1111-1156 | MISSING | — | No apply resuming with replay |
| DrainAndReplaySnapshot() | raft.go:1162-1177 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:537 | DrainAndReplaySnapshotAsync exists but simplified — no catchup cancellation, no commit preservation |
| LeadChangeC() | raft.go:2110 | MISSING | — | No leader change channel |
| QuitC() | raft.go:2113 | MISSING | — | No quit channel |
| Created() | raft.go:2115-2118 | PORTED | src/NATS.Server/Raft/RaftNode.cs:43 | Added `CreatedUtc` timestamp captured at node construction and exposed for runtime introspection |
| Stop() | raft.go:2120-2122 | PORTED | src/NATS.Server/Raft/RaftNode.cs:1095 | Added `Stop()` lifecycle API that transitions to follower, clears leader markers, and signals stop waiters |
| WaitForStop() | raft.go:2124-2128 | PORTED | src/NATS.Server/Raft/RaftNode.cs:1104 | Added synchronous `WaitForStop()` backed by stop completion signal |
| Delete() | raft.go:2130-2143 | PORTED | src/NATS.Server/Raft/RaftNode.cs:1109 | Added `Delete()` lifecycle API that stops node, marks deleted, and removes persisted raft directory when configured |
| IsDeleted() | raft.go:2145-2149 | PORTED | src/NATS.Server/Raft/RaftNode.cs:69 | Added `IsDeleted` state accessor |
| RecreateInternalSubs() | raft.go:658-747 | MISSING | — | No NATS internal subscription management |
| IsSystemAccount() | raft.go:648-649 | NOT_APPLICABLE | — | .NET does not have system account NRG routing |
| GetTrafficAccountName() | raft.go:652-656 | NOT_APPLICABLE | — | .NET does not have account NRG routing |
### golang/nats-server/server/raft.go — Core State Machine (unexported but critical)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| raft (struct) | raft.go:151-251 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:3 | RaftNode has basic fields (term, vote, log, peers, commit) but missing: WAL, catchup state, progress map, pae cache, ipQueues, removed peers, many flags |
| run() | raft.go:2275-2362 | MISSING | — | No main run loop / state machine goroutine |
| runAsFollower() | raft.go:2441-2496 | MISSING | — | No follower state machine with select on channels |
| runAsCandidate() | raft.go:3587-3670 | MISSING | — | No candidate state machine with vote collection |
| runAsLeader() | raft.go:2951-3067 | MISSING | — | No leader state machine with heartbeat ticker and proposal batching |
| processAppendEntry() | raft.go:3857-4248 | MISSING | — | Complex append entry processing with catchup, WAL alignment, truncation — not ported |
| processAppendEntryResponse() | raft.go:4287-4339 | MISSING | — | No response processing with catchup triggering |
| processVoteRequest() | raft.go:4780-4845 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:142 | GrantVote handles basic vote logic but missing: log up-to-dateness check (lastTerm/lastIndex), catchup cancellation, campaign timeout reset |
| requestVote() | raft.go:4856-4872 | MISSING | — | No vote request broadcast via RPC |
| handleAppendEntry() | raft.go:3672-3684 | MISSING | — | No wire callback for append entries |
| handleAppendEntryResponse() | raft.go:4342-4346 | MISSING | — | No wire callback for AE responses |
| handleVoteRequest() | raft.go:4847-4854 | MISSING | — | No wire callback for vote requests |
| handleVoteResponse() | raft.go:4764-4778 | MISSING | — | No wire callback for vote responses |
| handleForwardedProposal() | raft.go:2854-2874 | MISSING | — | No forwarded proposal handler |
| handleForwardedRemovePeerProposal() | raft.go:2820-2851 | MISSING | — | No forwarded remove-peer handler |
### golang/nats-server/server/raft.go — WAL & Storage
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| storeToWAL() | raft.go:4360-4396 | PARTIAL | src/NATS.Server/Raft/RaftWal.cs:74 | RaftWal.AppendAsync exists but different storage model (custom binary vs Go's filestore/memstore) |
| loadEntry() | raft.go:3339-3346 | MISSING | — | No entry loading from WAL by index |
| loadFirstEntry() | raft.go:3126-3130 | MISSING | — | No first-entry loading |
| truncateWAL() | raft.go:3758-3815 | MISSING | — | No WAL truncation with snapshot invalidation |
| resetWAL() | raft.go:3819-3821 | MISSING | — | No WAL reset |
| cachePendingEntry() | raft.go:4449-4460 | MISSING | — | No pending append entry cache |
### golang/nats-server/server/raft.go — Snapshots
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| snapshot (struct) | raft.go:1243-1248 | PARTIAL | src/NATS.Server/Raft/RaftSnapshot.cs:3 | RaftSnapshot has LastIncludedIndex/Term/Data but missing peerstate encoding |
| encodeSnapshot() | raft.go:1254-1279 | MISSING | — | No binary snapshot encoding with highwayhash |
| loadLastSnapshot() | raft.go:1660-1707 | MISSING | — | No binary snapshot loading with hash verification |
| setupLastSnapshot() | raft.go:1578-1656 | MISSING | — | No startup snapshot recovery from disk |
| installSnapshot() | raft.go:1315-1350 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:699 | InstallSnapshotAsync simplified — no WAL compact, no snapshot file management |
| termAndIndexFromSnapFile() | raft.go:1564-1573 | MISSING | — | No snapshot filename parsing |
### golang/nats-server/server/raft.go — Peer & Membership State
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| peerState (struct) | raft.go:4470-4474 | MISSING | — | No peer state struct with knownPeers/clusterSize/domainExt |
| encodePeerState() | raft.go:4480-4492 | MISSING | — | No binary peer state encoding |
| decodePeerState() | raft.go:4494-4514 | MISSING | — | No binary peer state decoding |
| writePeerState() | raft.go:4603-4609 | MISSING | — | No peer state file persistence |
| readPeerState() | raft.go:4611-4620 | MISSING | — | No peer state file reading |
| processPeerState() | raft.go:4264-4282 | MISSING | — | No peer state processing from leader |
| addPeer() | raft.go:2879-2898 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:129 | AddMember exists but no cluster size/quorum adjustment, no removed-list reversal |
| removePeer() | raft.go:2903-2913 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:131 | RemoveMember exists but no removed tracking, no cluster size/quorum adjustment |
| adjustClusterSizeAndQuorum() | raft.go:3534-3556 | MISSING | — | No dynamic cluster size/quorum recalculation |
| trackPeer() | raft.go:3559-3585 | MISSING | — | No automatic peer tracking with add-on-discovery |
### golang/nats-server/server/raft.go — Elections & Voting
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| switchToFollower() | raft.go:4958-4983 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:706 | RequestStepDown sets Follower but missing: aflr reset, leaderState/leaderSince clear, acks reset, leader update |
| switchToCandidate() | raft.go:4985-5014 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:133 | StartElection increments term and votes but missing: observer/paused/processed checks, quorum loss signaling |
| switchToLeader() | raft.go:5016-5037 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:986 | TryBecomeLeader sets Role but missing: aflr setup, peer state broadcast, leader update |
| campaign() | raft.go:2002-2009 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:971 | CampaignWithPreVote starts election but no random timeout |
| wonElection() | raft.go:4886-4888 | PORTED | src/NATS.Server/Raft/RaftNode.cs:986 | Quorum check in TryBecomeLeader |
| selectNextLeader() | raft.go:1887-1897 | MISSING | — | No next-leader selection by highest index |
| resetElectionTimeout() | raft.go:2241-2243 | PORTED | src/NATS.Server/Raft/RaftNode.cs:737 | ResetElectionTimeout with Timer |
| randElectionTimeout() | raft.go:2235-2238 | PORTED | src/NATS.Server/Raft/RaftNode.cs:727 | RandomizedElectionTimeout |
| randCampaignTimeout() | raft.go:1995-1998 | PORTED | src/NATS.Server/Raft/RaftNode.cs:822 | Added `RandomizedCampaignTimeout()` using Go-equivalent 100-800ms jitter window |
### golang/nats-server/server/raft.go — Wire Format & RPC
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| voteRequest (struct) | raft.go:4549-4556 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:84 | RaftVoteRequestWire with Encode/Decode |
| voteResponse (struct) | raft.go:4730-4735 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:132 | RaftVoteResponseWire with Encode/Decode |
| appendEntry (struct) | raft.go:2557-2569 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:189 | RaftAppendEntryWire with Encode/Decode |
| appendEntryResponse (struct) | raft.go:2760-2766 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:318 | RaftAppendEntryResponseWire with Encode/Decode |
| ae.encode() | raft.go:2662-2711 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:202 | RaftAppendEntryWire.Encode() |
| decodeAppendEntry() | raft.go:2714-2746 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:255 | RaftAppendEntryWire.Decode() |
| vr.encode() (voteRequest) | raft.go:4560-4568 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:94 | RaftVoteRequestWire.Encode() |
| decodeVoteRequest() | raft.go:4571-4583 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:109 | RaftVoteRequestWire.Decode() |
| vr.encode() (voteResponse) | raft.go:4739-4751 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:142 | RaftVoteResponseWire.Encode() |
| decodeVoteResponse() | raft.go:4753-4762 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:159 | RaftVoteResponseWire.Decode() |
| ar.encode() | raft.go:2777-2794 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:328 | RaftAppendEntryResponseWire.Encode() |
| decodeAppendEntryResponse() | raft.go:2799-2817 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:343 | RaftAppendEntryResponseWire.Decode() |
| idLen constant | raft.go:2756 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:23 | RaftWireConstants.IdLen = 8 |
| RaftWireHelpers (WriteId/ReadId) | raft.go:2693,4581 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:561 | RaftWireHelpers class |
| RaftWireHelpers (uvarint) | raft.go:2682,2740 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:597 | WriteUvarint/ReadUvarint |
### golang/nats-server/server/raft.go — NATS Subjects & Transport
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| raftAllSubj constant | raft.go:2162 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:16 | RaftSubjects.All |
| raftVoteSubj constant | raft.go:2163 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:22 | RaftSubjects.Vote() |
| raftAppendSubj constant | raft.go:2164 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:28 | RaftSubjects.AppendEntry() |
| raftPropSubj constant | raft.go:2165 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:34 | RaftSubjects.Proposal() |
| raftRemovePeerSubj constant | raft.go:2166 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:40 | RaftSubjects.RemovePeer() |
| raftReply constant | raft.go:2167 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:46 | RaftSubjects.Reply() |
| raftCatchupReply constant | raft.go:2168 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:52 | RaftSubjects.CatchupReply() |
| sendRPC() | raft.go:4874-4878 | PARTIAL | src/NATS.Server/Raft/NatsRaftTransport.cs:29 | NatsRaftTransport._publish delegate, but no sendq integration |
| sendReply() | raft.go:4880-4884 | MISSING | — | No reply sending via sendq |
| sendHeartbeat() | raft.go:4545-4547 | PARTIAL | src/NATS.Server/Raft/NatsRaftTransport.cs:229 | SendHeartbeatAsync exists but optimistic (no real ACK tracking) |
| IRaftTransport | — | PORTED | src/NATS.Server/Raft/RaftTransport.cs:3 | Interface with AppendEntries, RequestVote, InstallSnapshot, SendTimeoutNow, SendHeartbeat |
| InMemoryRaftTransport | — | PORTED | src/NATS.Server/Raft/RaftTransport.cs:26 | Full in-memory transport for testing |
| NatsRaftTransport | — | PORTED | src/NATS.Server/Raft/NatsRaftTransport.cs:18 | Wire-level NATS transport with binary encoding |
| createInternalSubs() | raft.go:2209-2233 | MISSING | — | No internal NATS subscription creation |
| subscribe() / unsubscribe() | raft.go:2194-2206 | MISSING | — | No internal subscription management |
### golang/nats-server/server/raft.go — Catchup & Leader Coordination
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| catchupState (struct) | raft.go:260-268 | MISSING | — | No catchup state tracking |
| runCatchup() | raft.go:3132-3236 | MISSING | — | No catchup goroutine for lagging followers |
| catchupFollower() | raft.go:3266-3337 | MISSING | — | No follower catchup initiation |
| createCatchup() | raft.go:3718-3735 | MISSING | — | No catchup subscription/inbox creation |
| cancelCatchup() | raft.go:3689-3697 | MISSING | — | No catchup cancellation |
| catchupStalled() | raft.go:3702-3712 | MISSING | — | No stall detection |
| sendSnapshotToFollower() | raft.go:3239-3264 | MISSING | — | No snapshot streaming to follower |
| sendCatchupSignal() | raft.go:3738-3745 | MISSING | — | No catchup signal to upper layer |
| cancelCatchupSignal() | raft.go:3748-3754 | MISSING | — | No catchup signal cancellation |
### golang/nats-server/server/raft.go — Commit & Apply
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| applyCommit() | raft.go:3350-3462 | MISSING | — | Complex commit application with entry type dispatch, peer state processing, membership changes |
| tryCommit() | raft.go:3468-3487 | MISSING | — | No quorum-based commit with ack counting |
| trackResponse() | raft.go:3493-3529 | MISSING | — | No response tracking with ack map |
| sendMembershipChange() | raft.go:2918-2949 | MISSING | — | No membership change send with WAL store |
### golang/nats-server/server/raft.go — Persistence & Term/Vote
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| writeTermVote() | raft.go:4708-4727 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:993 | PersistAsync writes meta.json (JSON) but Go uses binary tav.idx with dedup |
| readTermVote() | raft.go:4637-4656 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:1013 | LoadPersistedStateAsync reads meta.json but Go uses binary tav.idx |
| writePeerState() (on raft) | raft.go:4589-4600 | MISSING | — | No binary peer state file writing |
| setWriteErr() / setWriteErrLocked() | raft.go:4659-4704 | MISSING | — | No write error tracking with permission/space error escalation |
### golang/nats-server/server/raft.go — Server Integration
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| bootstrapRaftNode() | raft.go:346-407 | MISSING | — | No raft bootstrap with peer validation and directory setup |
| initRaftNode() | raft.go:410-614 | MISSING | — | No full raft initialization with WAL replay, snapshot recovery, peer state restore |
| startRaftNode() | raft.go:617-628 | MISSING | — | No start-and-run goroutine |
| registerRaftNode() | raft.go:776-783 | MISSING | — | No server-level raft registration |
| unregisterRaftNode() | raft.go:786-792 | MISSING | — | No server-level raft unregistration |
| lookupRaftNode() | raft.go:803-811 | MISSING | — | No raft node lookup by group |
| numRaftNodes() | raft.go:795-799 | MISSING | — | No raft node count |
| stepdownRaftNodes() | raft.go:831-851 | MISSING | — | No server-wide raft stepdown |
| shutdownRaftNodes() | raft.go:856-875 | MISSING | — | No server-wide raft shutdown |
| transferRaftLeaders() | raft.go:880-903 | MISSING | — | No server-wide leader transfer |
| reloadDebugRaftNodes() | raft.go:815-827 | NOT_APPLICABLE | — | Debug flag reload is Go-specific |
| serverNameForNode() | raft.go:759-763 | NOT_APPLICABLE | — | Server-level node mapping not yet needed |
| clusterNameForNode() | raft.go:767-772 | NOT_APPLICABLE | — | Cluster-level node mapping not yet needed |
### golang/nats-server/server/raft.go — .NET Extensions (not in Go)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:366 | RaftPreVoteRequestWire — pre-vote wire format (Go does not have pre-vote) |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:410 | RaftPreVoteResponseWire — pre-vote wire format |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:455 | RaftTimeoutNowWire — leadership transfer wire format |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:508 | RaftInstallSnapshotChunkWire — chunked snapshot wire format |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/CompactionPolicy.cs:9 | CompactionPolicy / CompactionOptions — configurable log compaction |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/SnapshotChunkEnumerator.cs:16 | SnapshotChunkEnumerator — snapshot chunking |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:251 | ReadIndexAsync — linearizable reads (Go does not expose this) |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:464 | Joint consensus (BeginJointConsensus/CommitJointConsensus) |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:790 | TransferLeadershipAsync with TimeoutNow RPC |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:906 | Pre-vote protocol (StartPreVote/RequestPreVote) |
### golang/nats-server/server/raft.go — Object Pools & Helpers
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| cePool / newCommittedEntry / ReturnToPool | raft.go:2498-2532 | NOT_APPLICABLE | — | Go sync.Pool for CommittedEntry; .NET uses GC |
| entryPool / newEntry | raft.go:2534-2547 | NOT_APPLICABLE | — | Go sync.Pool for Entry; .NET uses GC |
| aePool / newAppendEntry / returnToPool | raft.go:2549-2583 | NOT_APPLICABLE | — | Go sync.Pool for appendEntry; .NET uses GC |
| pePool / newProposedEntry | raft.go:2586-2603 | NOT_APPLICABLE | — | Go sync.Pool for proposedEntry; .NET uses GC |
| arPool / newAppendEntryResponse | raft.go:2748-2775 | NOT_APPLICABLE | — | Go sync.Pool for appendEntryResponse; .NET uses GC |
| peers sync.Map (string interning) | raft.go:2797 | NOT_APPLICABLE | — | Go-specific string interning optimization |
| debug() / warn() / error() | raft.go:2364-2379 | NOT_APPLICABLE | — | Logging helpers — .NET uses ILogger<T> |
| dios semaphore | raft.go:1665-1667 | NOT_APPLICABLE | — | Go disk I/O semaphore — .NET uses async I/O |
| ipQueue usage | raft.go:233-238 | MISSING | — | Go uses typed ipQueue channels for proposals, entries, votes, responses — .NET has no equivalent internal queue infrastructure |
### golang/nats-server/server/raft.go — Constants & Configuration
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Election timeout defaults | raft.go:277-287 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:56-57 | .NET uses 150-300ms defaults; Go uses 4-9s defaults. Different design choice |
| hbInterval | raft.go:283 | PORTED | src/NATS.Server/Raft/RaftNode.cs:10 | Added `HbIntervalDefault = 1s` constant |
| lostQuorumInterval | raft.go:284 | PORTED | src/NATS.Server/Raft/RaftNode.cs:11 | Added `LostQuorumIntervalDefault = 10s` constant |
| observerModeInterval | raft.go:286 | PORTED | src/NATS.Server/Raft/RaftNode.cs:12 | Added `ObserverModeIntervalDefault = 48h` constant |
| peerRemoveTimeout | raft.go:287 | PORTED | src/NATS.Server/Raft/RaftNode.cs:13 | Added `PeerRemoveTimeoutDefault = 5m` constant |
| Error sentinels | raft.go:319-343 | PARTIAL | — | .NET uses InvalidOperationException instead of typed error sentinels |
| noLeader / noVote constants | raft.go:4954-4956 | PORTED | src/NATS.Server/Raft/RaftNode.cs:5 | Added explicit `NoLeader` / `NoVote` empty-string constants |
| paeDropThreshold / paeWarnThreshold | raft.go:4399-4401 | MISSING | — | No pending append entry limits |
| maxBatch / maxEntries | raft.go:3004-3005 | MISSING | — | No proposal batching thresholds |
| extensionState | raft.go:4462-4468 | MISSING | — | No domain extension state tracking |
---
## 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/Raft/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Raft/ -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 analysis completed: 196 items inventoried across 12 categories. Summary: 46 PORTED, 38 PARTIAL, 99 MISSING, 13 NOT_APPLICABLE, 0 DEFERRED. Wire format is well-ported; core state machine (run loop, catchup, WAL integration) is largely missing. | claude-opus |
| 2026-02-25 | Ported RAFT API/lifecycle parity batch: LeaderSince/GroupLeader/Leaderless/HadPreviousLeader, observer toggles, Progress/Size/Applied, cluster-size adjustors, stop/delete APIs, campaign timeout jitter, and core timing/leader constants with targeted tests in `RaftNodeParityBatch2Tests`. | codex |
| 2026-02-26 | Ported RAFT parity batch: added `ProposeMultiAsync`, peer parity fields (`Lag`, `Current`) + refresh helpers, explicit `CommittedEntry` payload type, and general `RaftEntry` model/wire conversion helpers with focused tests in `RaftParityBatch3Tests`. | codex |