raft-kv — Raft from Scratch in Rust
A replicated key-value store built on my own implementation of the Raft consensus algorithm, written to be defensible line-by-line rather than big. The whole workspace is std-only — no tokio, no serde, no rand. One small PRNG, one hand-rolled wire codec, one event loop; everything interesting is the consensus logic and the tests that hammer it.
Sans-I/O core
raft-core contains no sockets, no clocks, no threads. A node is a deterministic state machine: the host calls tick() on its own schedule, feeds messages to handle(), submits commands with propose(), and executes the effects the node emits:
for effect in node.take_effects() {
// PersistHardState / PersistLog -> make durable *before* later sends
// Send { to, message } -> put on the wire, best-effort
// Apply { index, data } -> feed the state machine, in order
} The production server honors that contract with real files and TCP. The test harness honors it with an in-memory “disk” and a simulated network. Same core, byte for byte — that inversion is the whole point.
Deterministic simulation testing
Clusters run on a virtual clock with seeded randomness: message delay, loss, duplication, reordering, partitions, crashes, and restarts — where restarted nodes are rebuilt from the mirrored persistence effects, so the durability contract is tested rather than assumed. After every step the harness asserts Raft’s safety properties:
- Election Safety — at most one leader per term, ever
- Log Matching — same (index, term) means same command, across all disks
- State Machine Safety — a slot that applies once applies identically everywhere
- Term monotonicity — no node’s term ever decreases
Scenarios include a 5-node cluster under 20% message loss with duplication and node bounces across 25 seeds, a minority-partitioned leader whose proposals must never commit anywhere, and a full-cluster power cut. Any failure reproduces exactly from its seed. The whole suite runs in about a second.
The server
One event-loop thread owns the Raft node, the KV map, and durable storage; plain blocking threads feed it through a channel. All operations — reads included — go through the log, so reads are linearizable at the cost of a round of consensus. Storage is an atomically-rewritten hard-state file plus an append-only framed log that survives torn tails from a crash mid-append.
Verified live: 3-node cluster, put/get/del through any node (followers redirect to the leader), kill the leader mid-session — a new leader takes over with nothing lost, and the restarted node catches up from its own disk.
Deliberately not there yet
Snapshots/log compaction, ReadIndex reads, pre-vote, and membership changes — in that order. Scoping those out of v1 kept the correctness surface small enough to actually test to death.