Consensus: Raft
Implementation-first walkthrough for election, replication, and commit safety.
1 / 20
Implementation-first walkthrough for election, replication, and commit safety.
CurrentTerm: monotonic term.VotedFor: candidate ID or -1.LogTerms: term per log entry.CommitIndex: highest known committed index.LastLogIndex=0, LastLogTerm=0.currentTerm.Grant iff:
candidateTerm >= currentTermvotedFor empty or same candidateif candidateTerm < currentTerm { return false }
if votedFor != -1 && votedFor != candidateID { return false }
if candidateLastTerm != localLastTerm {
return candidateLastTerm > localLastTerm
}
return candidateLastIndex >= localLastIndex
Accept only if prev pointer matches.
Reject conditions:
prevLogIndex > len(log)prevLogIndex > 0 && log[prevLogIndex-1] != prevLogTermAccept action:
[1..prevLogIndex]entriesnewLog := append([]int{}, log[:prevLogIndex]...)
newLog = append(newLog, entries...)
Follower pipeline:
Conflict merge rule:
idx := rpc.PrevLogIndex
for i := 0; i < len(rpc.Entries); i++ {
pos := idx + 1 + i
if pos <= len(state.LogTerms) {
if state.LogTerms[pos-1] != rpc.Entries[i] {
state.LogTerms = append([]int(nil), state.LogTerms[:pos-1]...)
state.LogTerms = append(state.LogTerms, rpc.Entries[i:]...)
idx = pos - 1 + len(rpc.Entries[i:])
goto commit
}
} else {
state.LogTerms = append(state.LogTerms, rpc.Entries[i:]...)
idx = pos - 1 + len(rpc.Entries[i:])
goto commit
}
}
idx = rpc.PrevLogIndex + len(rpc.Entries)
commit:
state.CommitIndex = min(rpc.LeaderCommit, len(state.LogTerms))
Goal: largest N where majority replicated and logTerms[N-1] == currentTerm.
Implementation trick:
matchIndex descendingindexes[len/2]len(logTerms)candidate := sorted[len(sorted)/2]
for i := candidate; i >= 1; i-- {
if logTerms[i-1] == currentTerm { return i }
}
return 0
Without filter, leader may commit an old-term entry only due to current quorum shape, which can break Raft safety during leadership changes.
Practical rule in this pack:
Follower conflict reply: (conflictTerm, conflictIndex).
Leader next index:
conflictTerm == 0 -> conflictIndexconflictTerm -> lastIndex(conflictTerm)+1conflictIndexfor i := len(leaderLog)-1; i >= 0; i-- {
if leaderLog[i] == conflictTerm { return i + 2 }
}
return conflictIndex
Drop all entries <= snapshotIndex.
if snapshotIndex <= 0 { return copy(logTerms) }
if snapshotIndex >= len(logTerms) { return []int{} }
return copy(logTerms[snapshotIndex:])
Notes:
snapshotIndex.snapshotTerm exists in signature but not used in this exercise.Model intervals by effective heartbeats.
State:
lastHeartbeat starts at 0.Per heartbeat hb:
t = last+timeout, ... while t < hblast = hbAfter all heartbeats:
t <= end.Boundary rule:
if timeout <= 0 || end < 0 { return []int{} }
last := 0
for _, hb := range heartbeats {
if hb <= last { continue }
if hb > end {
for t := last + timeout; t <= end; t += timeout { out = append(out, t) }
return out
}
for t := last + timeout; t < hb; t += timeout { out = append(out, t) }
last = hb
}
for t := last + timeout; t <= end; t += timeout { out = append(out, t) }
Compute per-config quorum commit, then take min.
Per config:
matchIndex (default 0 if missing)indices[len(config)/2]Final:
joint = min(oldCommit, newCommit)
Normalize rpc.Kind, term-bump first, then dispatch.
Vote branch:
VotedFor and grantAppend branch:
CommitIndex = min(LeaderCommit, len(LogTerms))kind := strings.ToLower(strings.TrimSpace(rpc.Kind))
if rpcTerm > state.CurrentTerm {
state.CurrentTerm = rpcTerm
state.VotedFor = -1
}
switch kind {
case "vote":
// vote logic
case "append":
// append logic
default:
// unchanged
}
Keep helper functions small and reusable:
isUpToDateprevLogMatchesminInitial:
term=3, votedFor=-1, log=[1,2,2], commit=2Vote RPC (term=4, candidate=9, last=(3,4)):
Append RPC (term=4, prev=(3,2), entries=[4,4], leaderCommit=4):
[1,2,2,4,4]4This is the follower-side Raft control loop in practice.