Files
Baradb/docs/superpowers/specs/2026-07-30-raft-network-bootstrap-design.md
T

5.4 KiB

Networked Raft Bootstrap (C3a) — Design

Date: 2026-07-30 Status: Approved direction (user: "продължи"); implementation follows.

Problem

Raft in BaraDB is half-wired for real networking. core/raft.nim already has a TCP transport (RaftNetwork, binary framing, serialization, election-over- TCP proven by a test), but in production:

  1. node.peerAddrs is never populated (baradadb.nim creates the node from config.raftPeers but never parses addresses) — all sends silently no-op (raft.nim:560-561).
  2. No election timer runstick is only called from tests; a deployed node never starts an election.
  3. handleAppendEntries on the wire path does not reset the election timer — even if timers ran, followers would start elections despite a healthy leader.
  4. Raft state persistence is disabled in server startup (dataDir not passed to newRaftNode, baradadb.nim:333).
  5. Framing reads assume full TCP reads (recv(4)/recv(payloadLen), raft.nim:604-611) — partial reads corrupt the stream.

Result: BARADB_RAFT_ENABLED=true today starts a listening socket that can never elect anyone. This phase wires what exists; it does NOT change the SQL write path (that is C3b) or add membership/snapshots (C3c).

Goal

A 3-node BaraDB cluster started with ordinary config elects a leader over TCP, maintains it with heartbeats, and re-elects after the leader is killed — verified end-to-end with real server processes.

Non-goals: SQL writes through Raft (C3b), membership changes, snapshots, reconnect/backoff hardening, TLS/auth on the raft port (C3c).

Design

1. Peer address configuration

  • Env (existing mechanism): BARADB_RAFT_PEERS entries extended from bare nodeId to nodeId@host:port. Comma-separated, e.g. BARADB_RAFT_PEERS="n1@127.0.0.1:9473,n2@127.0.0.1:9474,n3@127.0.0.1:9475". Bare entries (no @) keep current meaning (peer id, no address).
  • Parsing lives in core/config.nim next to the existing BARADB_RAFT_* env handling (config.nim:174-179), producing raftPeerAddrs: Table[string, (string, int)] on BaraConfig. Malformed entries fail startup with a clear error (config-time, not runtime).
  • baradadb.nim startup copies config.raftPeerAddrs into node.peerAddrs and passes the data dir (see §3).

2. Election timer in production

  • A timerLoop async proc in core/raft.nim (next to heartbeatLoop, raft.nim:625): every 50ms calls tick(node); on election timeout calls the existing startElection path (raft.nim:659-685), which sends RequestVote over RaftNetwork.send.
  • RaftNetwork.run (raft.nim:633) starts timerLoop alongside heartbeatLoop via asyncCheck — single place, no baradadb.nim changes beyond startup wiring.
  • Wire-path timer reset: in the message-receive handling of RaftNetwork, after a valid AppendEntries (or heartbeat) is processed, reset the follower's election timer (ElectionTimer.lastHeartbeat = now) — wherever the existing handler processes inbound AppendEntries, matching what the in-process tests do manually.

3. State persistence on

  • baradadb.nim:333: pass config.dataDir (or a raft subdirectory of it — check what newRaftNode expects; saveState/loadState write raft_state.bin) so currentTerm/votedFor/log survive restarts, per the Raft spec (and the TLA models).

4. Framing robustness

  • Replace the recv(4) / recv(payloadLen) assumptions with the existing recvExact-style helper pattern used by the main server (core/server.nim:312-329 has recvExact/recvExactWithTimeout — mirror the approach inside raft.nim; do NOT import server.nim into raft.nim).

5. What does NOT change

  • SQL write path (ReplicationManager, server.nim) — untouched.
  • Raft state machine, message format, serialization — untouched (the TLA- faithfulness tests pin them).
  • applyCommand hook in baradadb.nim — stays as-is; committed entries (from tests / future C3b) still apply to the default DB.

Testing

TDD where feasible; the headline test is end-to-end:

  1. Config parsing (unit, tests/test_all.nim or bugfix_test): peers with and without @host:port, malformed entries error clearly.
  2. Framing (unit): partial-read feed of a serialized message through the new read path (socketpair or chunked strings) — message reassembles.
  3. E2E cluster (new file tests/raft_e2e_test.nim): start 3 real build/baradadb processes with raft enabled on distinct ports (client/raft ports offset per node), wait for a leader (queryable how? — simplest: each node logs its role; or a RAFT STATUS text command on the client port if one exists cheaply — decide in the plan), kill the leader, assert a new leader is elected within ~5s. Clean teardown.
  4. Existing suites stay green (nimble test, 659+ [OK]) — especially the in-process raft suites and the 3-node election TCP test.

Risks

  • Election-timer/async interactions with the main server's async loop — RaftNetwork.run already runs under asyncCheck; timerLoop follows the same pattern. Watch for CPU spin (50ms sleep, not busy loop).
  • Port allocation in the E2E test (parallel CI) — use the same time-derived port offset pattern as tests/nimforum_smoke_test.nim.
  • Timer reset wiring point: the inbound message path must reach the node the timer ticks — same RaftNode instance, verified by the E2E test (no spurious elections under a healthy leader).