Election and Replication
Pre-vote leader election and log replication with the consistency check and conflict back-up, as a pure step function.
Roles and time
RaftCore tracks four roles — Follower, PreCandidate, Candidate,
Leader — and advances purely on logical time: tick() drives every
timeout, and there is no Instant/SystemTime anywhere in the core.
Election timeouts are randomized per node, drawn from a seeded PRNG held
inside the core, so the same construction seed reproduces the same election
timing run after run — a requirement for the
deterministic simulation to be
meaningful at all.
Pre-vote: elections without disruption
A naive Raft election lets any follower that stops hearing from a leader increment its term and start soliciting votes. That's a problem for a node that's merely partitioned, not actually needed: it keeps incrementing its term on every timeout, and the moment it rejoins the cluster, its inflated term forces a legitimate, working leader to step down for no real reason.
RaftCore runs a pre-vote round first: on election timeout, a follower
sends RequestVote { pre_vote: true } without bumping its own term. Peers
answer honestly — would they vote for this candidate, hypothetically — using
the same up-to-date-log check a real vote uses, but the responder doesn't
persist anything or mutate voted_for for a pre-vote. Only if a majority
grants the pre-vote does the node become a real Candidate: increment its
term, vote for itself, persist that hard state, and broadcast a real
RequestVote. A partitioned node's pre-votes simply never reach a quorum,
so it never disrupts a healthy cluster. Any message carrying a higher term
than the node's own — pre-vote or real — steps it down to Follower and
persists the observed term.
Vote granting
A peer grants a real vote iff the candidate's log is at least as
up-to-date as its own (compared by last_log_term, then last_log_index
as a tiebreak) and it hasn't already voted for a different candidate
this term. voted_for is persisted before the grant is sent — reversing
that order would let a crash between "decide to grant" and "persist the
vote" produce a double vote across a restart, which is exactly what the
core's restart test pins down.
Replication and the consistency check
AppendEntries carries prev_log_index/prev_log_term so a follower can
check that its log agrees with the leader's immediately before the new
entries. On a mismatch, the follower rejects and returns a conflict_index
hint rather than making the leader retry one index at a time — that hint is
what lets a badly lagging follower catch up in a handful of round trips
instead of one per missing entry. On a match, the follower truncates any
conflicting suffix (truncate_suffix — see Log Store)
before appending the new entries. Heartbeats are just empty AppendEntries
calls, reusing the exact same consistency-check path as a real replication
round.
The leader tracks next_index/match_index per peer and updates them from
each ack — advancing on success, backing up using conflict_index on
rejection.
Commit advancement: the current-term rule
The leader advances its commit index to the highest index that's
replicated on a majority of match_index values — but only counting
entries that belong to the current term. This is Raft §5.4.2's
guardrail against a classic bug: committing an older-term entry purely by
replica count can be undone by a later leader that legitimately overwrites
it, which would mean a "committed" entry silently disappears. RaftCore's
simulation pins this down with a dedicated scenario — a leader appends an
entry, gets partitioned before it commits, and a new leader is elected and
overwrites the uncommitted entry — asserting the old entry never gets
counted as committed by count alone. Followers adopt
min(leader_commit, last_index) as their own commit index once they've
matched the leader's log up to that point.
See Read-Index for how commit advancement feeds linearizable reads, and Safety Invariants & Simulation for how all of this is checked under fault injection.