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:
node.peerAddrsis never populated (baradadb.nimcreates the node fromconfig.raftPeersbut never parses addresses) — all sends silently no-op (raft.nim:560-561).- No election timer runs —
tickis only called from tests; a deployed node never starts an election. handleAppendEntrieson the wire path does not reset the election timer — even if timers ran, followers would start elections despite a healthy leader.- Raft state persistence is disabled in server startup (
dataDirnot passed tonewRaftNode,baradadb.nim:333). - 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_PEERSentries extended from barenodeIdtonodeId@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.nimnext to the existingBARADB_RAFT_*env handling (config.nim:174-179), producingraftPeerAddrs: Table[string, (string, int)]on BaraConfig. Malformed entries fail startup with a clear error (config-time, not runtime). baradadb.nimstartup copiesconfig.raftPeerAddrsintonode.peerAddrsand passes the data dir (see §3).
2. Election timer in production
- A
timerLoopasync proc incore/raft.nim(next toheartbeatLoop, raft.nim:625): every 50ms callstick(node); on election timeout calls the existingstartElectionpath (raft.nim:659-685), which sends RequestVote overRaftNetwork.send. RaftNetwork.run(raft.nim:633) startstimerLoopalongsideheartbeatLoopviaasyncCheck— 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: passconfig.dataDir(or a raft subdirectory of it — check whatnewRaftNodeexpects;saveState/loadStatewriteraft_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 existingrecvExact-style helper pattern used by the main server (core/server.nim:312-329hasrecvExact/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).
applyCommandhook 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:
- Config parsing (unit,
tests/test_all.nimor bugfix_test): peers with and without@host:port, malformed entries error clearly. - Framing (unit): partial-read feed of a serialized message through the new read path (socketpair or chunked strings) — message reassembles.
- E2E cluster (new file
tests/raft_e2e_test.nim): start 3 realbuild/baradadbprocesses 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 aRAFT STATUStext 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. - 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.runalready runs underasyncCheck; 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
RaftNodeinstance, verified by the E2E test (no spurious elections under a healthy leader).