<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged raft</title>
<link>https://coderlegion.com/tag/raft</link>
<description>Powered by Question2Answer</description>
<item>
<title>I Built a Full Raft Consensus Engine in Rust at 17 — Here's Every Gritty Detail</title>
<link>https://coderlegion.com/23824/i-built-a-full-raft-consensus-engine-in-rust-at-17-heres-every-gritty-detail</link>
<description>&lt;h2 id=&quot;i-built-a-full-raft-consensus-engine-in-rust-at-17-here-s-every-gritty-detail&quot;&gt;I Built a Full Raft Consensus Engine in Rust at 17 — Here's Every Gritty Detail&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I'm 17. This is my first distributed systems project.&lt;/p&gt;
&lt;p&gt;Raft is the consensus algorithm that powers etcd, Consul, TiKV, and a dozen other production systems. The paper makes it look straightforward — leader election, log replication, snapshots. But implementing it from scratch? Every paragraph in the paper expands into dozens of edge cases. Figure 8 alone (the &quot;previous term entries can't be committed directly&quot; rule) caused me three rewrites.&lt;/p&gt;
&lt;p&gt;This implementation is &lt;strong&gt;4,400+ lines of Rust across 7 crates, 52 tests, real gRPC networking, disk persistence with sled, log compaction via InstallSnapshot, and a chaos harness that kills nodes and partitions networks&lt;/strong&gt;. No etcd code. No TiKV dependencies. Pure paper-to-production.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-actually-is&quot;&gt;What This Actually Is&lt;/h3&gt;
&lt;p&gt;A complete, tested Raft implementation that you can run right now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# In-process 3-node demo
cargo run --example kv-demo

# Or run a real cluster across 3 terminals
cargo build --release
# Terminal 1: cargo run --release -p raft-node -- --node-id 1 --peers 2@127.0.0.1:8081,3@127.0.0.1:8082 --port 8080
# Terminal 2: cargo run --release -p raft-node -- --node-id 2 --peers 1@127.0.0.1:8080,3@127.0.0.1:8082 --port 8081
# Terminal 3: cargo run --release -p raft-node -- --node-id 3 --peers 1@127.0.0.1:8080,2@127.0.0.1:8081 --port 8082
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leader election with randomized timeouts (300-500ms) and proper log-up-to-date checks&lt;/li&gt;
&lt;li&gt;Log replication with Figure 8 compliance — previous-term entries commit indirectly only&lt;/li&gt;
&lt;li&gt;Fast backup optimization — followers return &lt;code&gt;conflict_term&lt;/code&gt;/&lt;code&gt;conflict_index&lt;/code&gt;, leader skips entire terms in one RTT&lt;/li&gt;
&lt;li&gt;Persistence before RPC replies (Figure 2 requirement) — &lt;code&gt;current_term&lt;/code&gt;, &lt;code&gt;voted_for&lt;/code&gt;, log entries flushed to sled&lt;/li&gt;
&lt;li&gt;Snapshotting with InstallSnapshot RPC — log compaction, boundary handling, 15 tests&lt;/li&gt;
&lt;li&gt;Chaos testing — network partitions, node kills, leader isolation, process-level chaos with 5 real OS processes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What's not done (honest gaps):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dynamic membership changes (joint consensus)&lt;/li&gt;
&lt;li&gt;ReadIndex/LeaseRead for linearizable reads&lt;/li&gt;
&lt;li&gt;WAN optimization / batching&lt;/li&gt;
&lt;li&gt;WAL-based persistence (sled works but isn't crash-consistent like a WAL)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-7-crates-clean-boundaries&quot;&gt;Architecture — 7 Crates, Clean Boundaries&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;raft/
├── Cargo.toml
├── crates/
│   ├── raft-core/          # Pure logic — 40 unit tests
│   │   ├── types.rs
│   │   ├── state.rs
│   │   ├── election.rs
│   │   ├── replication.rs
│   │   └── snapshot.rs
│   ├── raft-rpc/           # gRPC transport (tonic)
│   ├── raft-storage/       # Persistence abstraction
│   │   ├── RaftStorage trait
│   │   ├── SledStorage
│   │   └── MemStorage
│   ├── raft-node/          # Standalone binary
│   └── raft-chaos/         # Chaos harness
└── examples/
    ├── kv-demo/
    └── process-chaos/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this separation matters:&lt;/strong&gt; The core logic has &lt;em&gt;zero&lt;/em&gt; I/O. No network, no disk, no time. Pure functions. Unit tests are deterministic and fast. Storage backends are swappable without touching consensus logic. The chaos harness injects failures at the network layer — not the logic layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;leader-election-the-part-everyone-gets-wrong&quot;&gt;Leader Election — The Part Everyone Gets Wrong&lt;/h3&gt;
&lt;p&gt;The paper says: &quot;If election timeout elapses without receiving AppendEntries, become candidate.&quot; Simple, right?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wrong.&lt;/strong&gt; Here's what actually happens in &lt;code&gt;election.rs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn on_election_timeout(&amp;amp;mut self) -&amp;gt; Vec&amp;lt;NodeAction&amp;gt; {
    self.become_candidate();  // term++, vote for self

    let args = RequestVoteArgs {
        term: self.current_term,
        candidate_id: self.id,
        last_log_index: self.last_log_index(),
        last_log_term: self.last_log_term(),
    };

    self.peers.iter()
        .map(|&amp;amp;peer| NodeAction::SendRequestVote(peer, args.clone()))
        .collect()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the &lt;em&gt;vote granting&lt;/em&gt; logic is where bugs live (§5.4.1):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn is_log_up_to_date(&amp;amp;self, candidate_last_term: u64, candidate_last_index: u64) -&amp;gt; bool {
    let my_last_term = self.last_log_term();
    let my_last_index = self.last_log_index();

    if candidate_last_term != my_last_term {
        candidate_last_term &amp;gt; my_last_term  // Higher term wins
    } else {
        candidate_last_index &amp;gt;= my_last_index  // Same term? Longer log wins
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The subtlety that catches everyone:&lt;/strong&gt; A candidate with term 3 and log length 1 beats a follower with term 2 and log length 100. Term &lt;em&gt;always&lt;/em&gt; wins. This prevents the &quot;stale leader with massive log&quot; problem.&lt;/p&gt;
&lt;p&gt;My test suite hammers this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;12 election tests covering grants, rejections, stale terms, split votes, term conversion&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_no_two_leaders_same_term&lt;/code&gt; spins up 5 nodes simultaneously — all become candidates, all vote for themselves, &lt;em&gt;none&lt;/em&gt; win (majority = 3, each has 1 vote). Beautiful.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;log-replication-where-figure-8-bites-you&quot;&gt;Log Replication — Where Figure 8 Bites You&lt;/h3&gt;
&lt;p&gt;Leader receives command → appends to local log → sends AppendEntries → majority replicates → commit → apply. Standard stuff.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Except Figure 8.&lt;/strong&gt; The paper shows a scenario where a leader commits an entry from a &lt;em&gt;previous&lt;/em&gt; term, then crashes. New leader doesn't have that entry. Data loss.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; A leader can &lt;em&gt;only&lt;/em&gt; directly commit entries from its &lt;strong&gt;current term&lt;/strong&gt;. Previous-term entries commit &lt;em&gt;indirectly&lt;/em&gt; when a current-term entry after them commits.&lt;/p&gt;
&lt;p&gt;My implementation in &lt;code&gt;replication.rs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn advance_commit_index(&amp;amp;mut self) {
    if self.state != NodeState::Leader { return; }

    let mut match_indices: Vec&amp;lt;u64&amp;gt; = self.match_index.values().copied().collect();
    match_indices.push(self.last_log_index());
    match_indices.sort_unstable_by(|a, b| b.cmp(a));

    for &amp;amp;n in &amp;amp;match_indices {
        if n &amp;gt; self.commit_index {
            if let Some(entry) = self.get_entry(n) {
                // FIGURE 8 RULE: only commit current-term entries directly
                if entry.term == self.current_term {
                    let replicated = match_indices.iter().filter(|&amp;amp;&amp;amp;idx| idx &amp;gt;= n).count();
                    if self.is_majority(replicated) {
                        self.commit_index = n;
                        break;
                    }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The test that proves it works&lt;/strong&gt; (&lt;code&gt;test_advance_commit_index_figure8&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// Leader has: index 1 (term 1), index 2-3 (term 2)
// Followers replicated up to index 1 (term 1 entry)
leader.match_index.insert(2, 1);
leader.match_index.insert(3, 1);
leader.advance_commit_index();
// commit_index stays 0 — term 1 entry NOT committed directly

// Now followers replicate index 2 (term 2)
leader.match_index.insert(2, 2);
leader.match_index.insert(3, 2);
leader.advance_commit_index();
// commit_index = 2 — term 2 entry commits, drags term 1 along indirectly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This test alone caught 3 bugs in my early implementation. &lt;strong&gt;Write the Figure 8 test first. Always.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;fast-backup-the-optimization-that-makes-raft-practical&quot;&gt;Fast Backup — The Optimization That Makes Raft Practical&lt;/h3&gt;
&lt;p&gt;Without fast backup, a follower 10,000 entries behind takes 10,000 round-trips to catch up. The paper's appendix describes the fix: followers return &lt;code&gt;conflict_term&lt;/code&gt; and &lt;code&gt;conflict_index&lt;/code&gt; on rejection. Leader jumps &lt;code&gt;next_index&lt;/code&gt; past the entire conflicting term.&lt;/p&gt;
&lt;p&gt;My implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn handle_append_entries_reply(
    &amp;amp;mut self,
    peer: NodeId,
    reply: AppendEntriesReply,
) -&amp;gt; Option&amp;lt;NodeAction&amp;gt; {
    if reply.success {
        // Update match_index, next_index normally
        ...
    } else {
        // FAST BACKUP
        if let Some(conflict_term) = reply.conflict_term {
            let mut new_next = reply.conflict_index.unwrap_or(1);
            for entry in self.log.iter().rev() {
                if entry.term == conflict_term {
                    new_next = entry.index + 1;  // Jump PAST the term
                    break;
                }
            }
            self.next_index.insert(peer, new_next.max(1));
        } else if let Some(conflict_index) = reply.conflict_index {
            self.next_index.insert(peer, conflict_index.max(1));
        } else {
            // Fallback: decrement by 1 (slow path)
            let current = self.next_index.get(&amp;amp;peer).copied().unwrap_or(1);
            self.next_index.insert(peer, (current - 1).max(1));
        }
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Test proof&lt;/strong&gt; (&lt;code&gt;test_fast_backup_convergence&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leader has terms: [1, 1, 3, 5] at indices [1, 2, 3, 4]&lt;/li&gt;
&lt;li&gt;Follower rejects at index 4, says &quot;I have term 1 at index 4, first index of term 1 is 1&quot;&lt;/li&gt;
&lt;li&gt;Leader jumps &lt;code&gt;next_index&lt;/code&gt; from 5 → 3 (skips all of term 1 in one round-trip)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Real-world impact:&lt;/strong&gt; A node 1M entries behind catches up in ~log(terms) round-trips, not O(entries). This is the difference between &quot;toy&quot; and &quot;production.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;persistence-the-before-reply-rule&quot;&gt;Persistence — The &quot;Before Reply&quot; Rule&lt;/h3&gt;
&lt;p&gt;Figure 2 is explicit: &lt;strong&gt;persist before responding.&lt;/strong&gt; My &lt;code&gt;grpc_server.rs&lt;/code&gt; enforces this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;async fn request_vote(&amp;amp;self, request: Request&amp;lt;ProtoRequestVoteArgs&amp;gt;) -&amp;gt; ... {
    let core_args = proto_to_core_request_vote_args(args);
    let mut node = self.state.node.write().await;
    let reply = node.handle_request_vote(core_args);
    
    // PERSIST BEFORE REPLY — Figure 2 requirement
    let _ = self.state.storage.save_current_term(node.current_term);
    let _ = self.state.storage.save_voted_for(node.voted_for);
    
    Ok(Response::new(core_request_vote_reply_to_proto(reply)))
}

async fn append_entries(&amp;amp;self, request: Request&amp;lt;ProtoAppendEntriesArgs&amp;gt;) -&amp;gt; ... {
    let core_args = proto_to_core_append_entries_args(args);
    let mut node = self.state.node.write().await;
    let reply = node.handle_append_entries(core_args);
    
    // Persist log entries BEFORE responding
    if reply.success {
        for entry in &amp;amp;node.log {
            if entry.index &amp;gt;= core_args.prev_log_index {
                let _ = self.state.storage.save_log_entry(entry);
            }
        }
    }
    let _ = self.state.storage.save_current_term(node.current_term);
    Ok(Response::new(core_append_entries_reply_to_proto(reply)))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;SledStorage&lt;/strong&gt; uses bincode serialization with a 64MB cache. Each write flushes to disk. On restart, the node loads &lt;code&gt;current_term&lt;/code&gt;, &lt;code&gt;voted_for&lt;/code&gt;, and the full log — resumes exactly where it left off.&lt;/p&gt;
&lt;p&gt;Crash recovery test: Kill a node mid-replication, restart it, verify it has the same committed state. Works.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;snapshotting-log-compaction-done-right&quot;&gt;Snapshotting — Log Compaction Done Right&lt;/h3&gt;
&lt;p&gt;Logs grow forever. Snapshots fix this. Figure 12 + Figure 7.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Leader side&lt;/strong&gt; (&lt;code&gt;state.rs&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn snapshot(&amp;amp;mut self, include_index: u64) -&amp;gt; Option&amp;lt;(Vec&amp;lt;u8&amp;gt;, u64, u64)&amp;gt; {
    // Validations: must be committed, applied, past last snapshot
    if include_index &amp;lt;= self.last_included_index
        || include_index &amp;gt; self.commit_index
        || include_index &amp;gt; self.last_applied {
        return None;
    }

    // Collect all applied commands up to include_index
    let mut data = Vec::new();
    for entry in &amp;amp;self.log {
        if entry.index &amp;lt;= include_index {
            data.extend_from_slice(&amp;amp;entry.command);
            data.push(b'\n');
        }
    }

    let last_included_term = self.get_entry(include_index)
        .map_or(self.last_included_term, |e| e.term);

    // Discard compacted entries
    self.log.retain(|e| e.index &amp;gt; include_index);
    self.last_included_index = include_index;
    self.last_included_term = last_included_term;

    if self.last_applied &amp;lt; include_index {
        self.last_applied = include_index;
    }

    Some((data, include_index, last_included_term))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Follower side&lt;/strong&gt; (&lt;code&gt;handle_install_snapshot&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn handle_install_snapshot(&amp;amp;mut self, args: InstallSnapshotArgs) -&amp;gt; InstallSnapshotReply {
    // Rule 1: Stale term → reject
    if args.term &amp;lt; self.current_term { ... }

    // Rule 2: Higher term → become follower
    if args.term &amp;gt; self.current_term { self.become_follower(args.term); }

    // Rule 3: Snapshot behind our snapshot → reject (stale)
    if args.last_included_index &amp;lt;= self.last_included_index { ... }

    // Rule 4: Install it
    let existing_term = self.get_entry(args.last_included_index).map(|e| e.term);
    
    // If boundary matches, keep entries after; else discard ALL log
    if existing_term == Some(args.last_included_term) {
        self.log.retain(|e| e.index &amp;gt; args.last_included_index);
    } else {
        self.log.clear();
    }

    self.last_included_index = args.last_included_index;
    self.last_included_term = args.last_included_term;

    // Advance commit/applied past snapshot
    if self.last_applied &amp;lt; args.last_included_index {
        self.last_applied = args.last_included_index;
    }
    if self.commit_index &amp;lt; args.last_included_index {
        self.commit_index = args.last_included_index;
    }

    InstallSnapshotReply {
        term: self.current_term,
        reset_election_timer: true,
        snapshot_data: Some(args.data),
        last_included_index: Some(args.last_included_index),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;15 snapshot tests&lt;/strong&gt; cover: basic compaction, stale rejection, uncommitted rejection, unapplied rejection, boundary preservation, empty log after compact, InstallSnapshot basic, stale term/index rejection, log discard, matching boundary kept, mismatched boundary discarded, term_at_index with snapshot, apply after snapshot, multiple snapshots.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;chaos-testing-breaking-it-on-purpose&quot;&gt;Chaos Testing — Breaking It On Purpose&lt;/h3&gt;
&lt;p&gt;Unit tests verify &lt;em&gt;correctness&lt;/em&gt;. Chaos tests verify &lt;em&gt;resilience&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;raft-chaos&lt;/code&gt; spins up real nodes (in-process or real processes), then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partitions&lt;/strong&gt; networks bidirectionally (&lt;code&gt;HashSet&amp;lt;(NodeId, NodeId)&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kills&lt;/strong&gt; nodes (clears peers, sets to follower)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heals&lt;/strong&gt; partitions&lt;/li&gt;
&lt;li&gt;Verifies safety (no split-brain) and liveness (new leader elected)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The 5 chaos tests:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Test &lt;/th&gt;&lt;th&gt;What It Verifies&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_chaos_leader_election&lt;/code&gt; &lt;/td&gt;&lt;td&gt;3-node cluster elects leader within 5s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_chaos_command_submission&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Commands replicate to all nodes' state machines&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_chaos_node_survives_majority&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Kill leader → new election in majority partition&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_chaos_partition_minority_cannot_commit&lt;/code&gt; &lt;/td&gt;&lt;td&gt;2-node minority partitioned → no commits, majority continues&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;test_chaos_partition_leader_isolated&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Isolate leader → majority elects new leader → heal → old leader steps down&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Process chaos&lt;/strong&gt; (&lt;code&gt;examples/process-chaos&lt;/code&gt;): Spawns 5 &lt;strong&gt;real OS processes&lt;/strong&gt; via &lt;code&gt;cargo run --release -p raft-node&lt;/code&gt;, kills the leader process, verifies new election. This catches bugs the in-process harness misses (file descriptor limits, port binding races, sled locking).&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-kv-demo-see-it-run&quot;&gt;The KV Demo — See It Run&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo run --example kv-demo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;=== 3-node Raft cluster started ===
Nodes on ports [9001, 9002, 9003]
Waiting for leader election...
Leader elected: node 2
Submitted: &quot;name Alice&quot; -&amp;gt; index=1, term=1
Submitted: &quot;name Bob&quot; -&amp;gt; index=2, term=1
Submitted: &quot;lang Rust&quot; -&amp;gt; index=3, term=1
Submitted: &quot;lang Python&quot; -&amp;gt; index=4, term=1

=== Cluster state ===
Node 1: state=Follower, term=1, log_len=4, commit=4, applied=4, store={&quot;name&quot;: &quot;Bob&quot;, &quot;lang&quot;: &quot;Python&quot;}
Node 2: state=Leader, term=1, log_len=4, commit=4, applied=4, store={&quot;name&quot;: &quot;Bob&quot;, &quot;lang&quot;: Python&quot;}
Node 3: state=Follower, term=1, log_len=4, commit=4, applied=4, store={&quot;name&quot;: &quot;Bob&quot;, &quot;lang&quot;: &quot;Python&quot;}

All nodes consistent: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Real consensus. Real replication. Real state machines.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;spec-reference&quot;&gt;Spec Reference&lt;/h3&gt;
&lt;p&gt;Built from &lt;strong&gt;Ongaro &amp;amp; Ousterhout's &quot;In Search of an Understandable Consensus Algorithm&quot; (2014)&lt;/strong&gt; — the Raft paper. Every field name, every rule, follows Figure 2 directly.&lt;/p&gt;
&lt;p&gt;Test plan follows &lt;strong&gt;MIT 6.5840 (Distributed Systems) Raft lab structure&lt;/strong&gt; — each stage isolates a bug class:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stage A: Leader election&lt;/li&gt;
&lt;li&gt;Stage B: Log replication&lt;/li&gt;
&lt;li&gt;Stage C: Persistence + crash recovery&lt;/li&gt;
&lt;li&gt;Stage D: Log compaction / snapshotting&lt;/li&gt;
&lt;li&gt;Stage E: Chaos testing&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/subhansh-dev/raft-rs&quot;&gt;https://github.com/subhansh-dev/raft-rs&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;portfolio:&lt;/strong&gt; &lt;em&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://subhansh.dev&quot;&gt;https://subhansh.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23824/i-built-a-full-raft-consensus-engine-in-rust-at-17-heres-every-gritty-detail</guid>
<pubDate>Thu, 30 Jul 2026 19:18:17 +0000</pubDate>
</item>
</channel>
</rss>