Followers, candidates and leaders with terms, one vote per term, randomized election timeouts and heartbeats, on a simulated network where a partition's minority can never elect a leader.
If follower B promotes itself because leader A went quiet, and A was only slow, you have two leaders: split-brain (Day 5, section 1.1). Raft's leader election avoids it with terms and majorities (sections 3.2 and 3.3; Ongaro and Ousterhout, "In Search of an Understandable Consensus Algorithm", USENIX ATC 2014). Every node votes at most once per term, a candidate needs a majority of the whole cluster, and two majorities always share a node, so no term can have two leaders.
Implement RaftNode(node_id, peers, clock, rng, send, election_timeout=(0.15, 0.30), heartbeat_interval=0.05):
| Member | Is / does |
|---|---|
state |
"follower", "candidate" or "leader"; every node starts as a follower |
term |
the current term, starting at 0 |
voted_for |
who this node voted for in the current term, or None |
leader_id |
the leader this node knows of in the current term (itself when it leads), or None |
tick() |
called by the network every few milliseconds: run the timers |
receive(src, msg) |
handle one message from node src |
send(dst, msg) sends a message (a dict) to another node. The four messages, with exactly these fields:
| Message | Fields |
|---|---|
| RequestVote | {"type": "RequestVote", "term": t, "candidate": id} |
| its reply | {"type": "RequestVoteReply", "term": t, "granted": bool} |
| AppendEntries (a heartbeat: no log entries in this lab) | {"type": "AppendEntries", "term": t, "leader": id} |
| its reply | {"type": "AppendEntriesReply", "term": t, "success": bool} |
The rules (the paper's Figure 2, election parts only):
term is above yours: adopt that term, become a follower, clear voted_for and leader_id, then handle the message.now >= deadline) starts an election in tick(): term += 1, become candidate, vote for itself, send RequestVote to every peer. It wins once it holds votes from a majority of the whole cluster ((len(peers) + 1) // 2 + 1, its own vote included), counting each voter once (replies can arrive twice), and only replies of its current term count.granted = False if the request's term is below yours. Otherwise grant it if you haven't voted this term or already voted for this same candidate, and record the vote: one vote per term, first come, first served. Every request gets a reply carrying your (possibly just updated) term.leader_id to itself and sends AppendEntries to every peer at once, then again in tick() whenever heartbeat_interval has passed since the last round.success = False (that's how an old leader learns the new term). Otherwise become (or stay) a follower of that leader, set leader_id, reset your election timer and reply success = True. A candidate that hears from a leader of its own term steps down this way.deadline = now + rng.uniform(lo, hi), a fresh random draw from election_timeout. Reset it at construction, when you start an election, when you grant a vote, when you accept a heartbeat, and when a higher term makes you a follower. That last reset is this model's choice, so a deposed leader doesn't start an election the moment it steps down; the paper's Figure 2 resets the timer when a node starts an election, grants a vote or hears from the current leader. Call rng.uniform nowhere else: the tests script the draws.clock.now(), and don't change a message you receive (a duplicate delivers the same object again).Cluster (Given) wires nodes together on a zero-latency network: step() advances the clock 10 ms, ticks every node in id order, then delivers every queued message and every message those trigger, in send order. It can partition({...}, {...}), heal() and pause nodes (cluster.down: a paused node neither ticks nor receives, like a long GC pause).
In JavaScript and TypeScript it's new RaftNode(nodeId, peers, clock, rng, send, { electionTimeout: [0.15, 0.3], heartbeatInterval: 0.05 }) with state, term, votedFor, leaderId (null for none), tick() and receive(src, msg); messages are plain objects with the same fields.
The chaos tests run five nodes for two simulated seconds while the scheduler delivers messages in random order, duplicates some and partitions the network (the test also splits off random two-node minorities): no term may ever have two leaders, and no node's term may go backwards.
This is leader election only. There's no log, so no log replication, no commit index, and no election restriction: real Raft's RequestVote carries the candidate's last log index and term, and a voter refuses a candidate whose log is less up to date than its own (section 5.4.1 of the paper). Without it a node missing committed entries could become leader and lose them; it doesn't matter here only because there is nothing to lose. Also left out: persisting term and voted_for to disk before replying (a real node must, or a restart could let it vote twice in one term), membership changes, and timeouts on RPCs. The 150 to 300 ms timeout range is the paper's example.
Write your code, then run 3 example tests and 9 hidden tests. Each run is limited to 8 seconds: an infinite loop gets stopped and the runtime restarts, your code stays.