speckl/docs/kraft-protocol-reference.md
Scott Scoble 03de892259 docs: comprehensive KRaft protocol reference for speckl#22
M1.1 research deliverable — detailed reference document covering:

- Complete Raft state machine (TLA+ spec): 10 actions with preconditions
- KRaft state hierarchy: 7 states (Unattached, Prospective, Candidate,
  Follower, Leader, Resigned, Detached) with per-state variables
- Detailed transition table with triggers and preconditions
- Pre-Vote protocol (KIP-996) mechanics and verification targets
- Resignation protocol (graceful leader step-down)
- 6 vanilla Raft invariants (I1-I6) formalized for Z3
- 6 KRaft-specific invariants (K1-K6) including observer consistency
- Snapshot invariants (KIP-630)
- 4 known Raft/KRaft bugs with verification targets
- 7 open verification targets for SpeckDL/Z3
- Producer acks and consumer group semantics (deferred to M1.3)
- Message types and KIP reference table
2026-05-09 21:54:09 -07:00

32 KiB
Raw Permalink Blame History

Kafka KRaft Protocol Reference

Source material for writing examples/KafkaKRaft.speck (speckl#22)

Scope

This reference covers the consensus core of KRaft — the state machine for leader election, log replication, and commit advancement. It does NOT cover:

  • Full broker functionality (topic creation, partition assignment, consumer group coordination)
  • Data plane (produce/fetch request handling)
  • Controller-specific logic (metadata quorums, configuration management)
  • Network transport (this is a logical model, not a network protocol spec)

The boundary: if it touches currentTerm, votedFor, log, commitIndex, or state transitions, it's in scope.

In scope (M1.1): Core Raft consensus — leader election, log replication, commit advancement, and all 6 safety invariants, including KRaft's extensions (Unattached, Resigned, Prospective states, Pre-Vote, snapshots).

Deferred to M1.3: Producer ack semantics, consumer group rebalancing, transaction coordinator.

Out of scope: Full Kafka broker (produce/fetch handling), controller-specific logic, network layer.

Sources

  1. Raft TLA+ Specification — Diego Ongaro, 2014. The canonical formal spec. Available at: github.com/ongardie/raft.tla
  2. "In Search of an Understandable Consensus Algorithm" — Ongaro & Ousterhout, 2014 (USENIX ATC). Includes safety proof.
  3. "Consensus: Bridging Theory and Practice" — Ongaro, 2014 (PhD dissertation). Extended TLA+ spec, implementation concerns.
  4. KIP-595: A Raft Protocol for the Metadata Quorum — The foundational KIP that introduced KRaft. cwiki.apache.org/confluence/display/KAFKA/KIP-595
  5. KIP-630: Kafka Raft Snapshot — Snapshot mechanism for KRaft metadata log. Status: Adopted.
  6. KIP-996: Pre-Vote for KRaft — Adds Pre-Vote phase to prevent disruptive servers. Status: Implementing.
  7. KIP-853: KRaft Controller Membership Changes — Dynamic quorum membership changes. Status: Implemented.
  8. Apache Kafka source — kafka/raft/ package. Key files: KafkaRaftClient.java, QuorumState.java, LeaderState.java, CandidateState.java, FollowerState.java, UnattachedState.java, ProspectiveState.java, ResignedState.java, EpochState.java
  9. Red Hat Deep Dive — "A Deep Dive into Apache Kafka's KRaft Protocol" (developers.redhat.com, 2025). Covers KRaft v1 based on Kafka 4.1.0.

1. Raft State Machine (from TLA+ Spec)

Reference: Source [1], [3]

1.1 State Variables

Per server (i ∈ Server):

Variable Type Description
currentTerm Nat Current term number (monotonically increasing)
state {Follower, Candidate, Leader} Server role
votedFor Server ∪ {Nil} Candidate voted for in current term, or Nil
log Seq(LogEntry) Sequence of log entries (index = position)
commitIndex Nat Index of highest log entry known to be committed
nextIndex Server → Nat (Leader only) Next log index to send to each follower
matchIndex Server → Nat (Leader only) Highest log index replicated on each follower
votesResponded Set(Server) (Candidate only) Servers that responded to RequestVote
votesGranted Set(Server) (Candidate only) Servers that granted vote

Global:

Variable Type Description
messages Bag(Message) In-flight messages (requests/responses)

1.2 LogEntry Type

LogEntry == { term: Nat, value: Value }

1.3 Quorum

Quorum == {i ∈ SUBSET(Server) : Cardinality(i) * 2 > Cardinality(Server)}

Simple majority. The key property: any two quorums overlap.

1.4 Initial State

currentTerm[i] = 1
state[i] = Follower
votedFor[i] = Nil
log[i] = <<>> (empty sequence)
commitIndex[i] = 0

1.5 Actions (State Transitions)

Action 1: Timeout → Start Election

Preconditions:

  • state[i] ∈ {Follower, Candidate}

Effects:

  • state[i] ← Candidate
  • currentTerm[i] ← currentTerm[i] + 1
  • votedFor[i] ← Nil
  • votesResponded[i] ← {}
  • votesGranted[i] ← {}

Action 2: RequestVote Request (Candidate → Peer)

Preconditions:

  • state[i] = Candidate
  • j ∉ votesResponded[i]

Effects:

  • Send RequestVoteRequest with {mterm: currentTerm[i], mlastLogTerm: LastTerm(log[i]), mlastLogIndex: Len(log[i]), msource: i, mdest: j}

Action 3: HandleRequestVoteRequest (Peer → Candidate)

Preconditions:

  • m.mterm ≤ currentTerm[i]

Log check (logOk):

logOk ≡ m.mlastLogTerm > LastTerm(log[i])
       ∨ (m.mlastLogTerm = LastTerm(log[i]) ∧ m.mlastLogIndex ≥ Len(log[i]))

Grant conditions:

grant ≡ m.mterm = currentTerm[i] ∧ logOk ∧ votedFor[i] ∈ {Nil, j}

Effects (if grant):

  • votedFor[i] ← j

Effects (always):

  • Reply with RequestVoteResponse{mterm: currentTerm[i], mvoteGranted: grant}

Action 4: HandleRequestVoteResponse (Candidate receives vote)

Preconditions:

  • m.mterm = currentTerm[i]

Effects:

  • votesResponded[i] ← votesResponded[i] ∪ {j}
  • If m.mvoteGranted: votesGranted[i] ← votesGranted[i] ∪ {j}

Action 5: BecomeLeader (Candidate → Leader)

Preconditions:

  • state[i] = Candidate
  • votesGranted[i] ∈ Quorum

Effects:

  • state[i] ← Leader
  • nextIndex[i] ← [j ∈ Server → Len(log[i]) + 1] for all j
  • matchIndex[i] ← [j ∈ Server → 0] for all j

Action 6: AppendEntries Request (Leader → Follower)

Preconditions:

  • i ≠ j
  • state[i] = Leader

Effects:

  • Send AppendEntriesRequest with entries from nextIndex[i][j] to min(Len(log[i]), nextIndex[i][j]) (at most 1 entry per message in the spec)
  • Include mprevLogIndex = nextIndex[i][j] - 1, mprevLogTerm, mcommitIndex

Action 7: HandleAppendEntriesRequest (Follower receives entries)

Preconditions:

  • m.mterm ≤ currentTerm[i]

Log consistency check (logOk):

logOk ≡ m.mprevLogIndex = 0
       ∨ (m.mprevLogIndex ≤ Len(log[i]) ∧ m.mprevLogTerm = log[i][m.mprevLogIndex].term)

Effects (if logOk AND m.mterm ≥ currentTerm[i]):

  • Reset to Follower if Candidate
  • Update currentTerm[i] ← m.mterm if needed
  • Append/conflict resolution: if mprevLogIndex > 0, entries after mprevLogIndex that conflict are truncated
  • commitIndex[i] ← min(m.mcommitIndex, Len(log[i]))
  • Reply AppendEntriesResponse{msuccess: TRUE, mmatchIndex: ...}

Effects (if !logOk OR m.mterm < currentTerm[i]):

  • Reply AppendEntriesResponse{msuccess: FALSE, mmatchIndex: 0}

Action 8: AdvanceCommitIndex (Leader)

Preconditions:

  • state[i] = Leader

Effects:

  • Find highest N such that N > commitIndex[i] and a quorum of matchIndex[i][k] ≥ N
  • Only commit entries from current term: log[i][N].term = currentTerm[i]
  • commitIndex[i] ← N

Action 9: ClientRequest (Leader appends entry)

Preconditions:

  • state[i] = Leader

Effects:

  • Append [term: currentTerm[i], value: v] to log[i]

Action 10: Restart (Server crash recovery)

Effects:

  • state[i] ← Follower
  • Reset volatile state (votesResponded, votesGranted, nextIndex, matchIndex, commitIndex)
  • Preserve persistent state (currentTerm, votedFor, log)

2. KRaft Extensions Over Vanilla Raft

References: Sources [4], [5], [6], [7], [8], [9]

KRaft is Kafka's adaptation of Raft for metadata management. Key differences from vanilla Raft:

  1. No ZooKeeper: KRaft replaces the ZooKeeper-based metadata quorum with a Raft-based one.
  2. Metadata log: The replicated log contains metadata records (topic configs, partition assignments), not user data.
  3. Controller node: KRaft has an "active controller" concept — the leader of the metadata quorum acts as the controller for the Kafka cluster.
  4. Snapshotting: KRaft supports snapshot-based log compaction (KIP-630).
  5. Voters and observers: Not all nodes are voters; some are observer nodes that replicate but don't vote.
  6. Resignation: A leader can voluntarily step down (immediate or graceful).
  7. Pre-Vote: KRaft implements Pre-Vote (KIP-996) to prevent disruptive servers from forcing unnecessary elections.
  8. Epochs instead of terms: KRaft uses "epoch" terminology instead of "term" (functionally equivalent).
  9. Additional states: KRaft adds Unattached, Prospective, Resigned, and Detached states beyond Follower/Candidate/Leader.

2.1 KRaft State Machine

The KRaft state machine is implemented in QuorumState.java. Unlike vanilla Raft's three-state model (Follower/Candidate/Leader), KRaft has a richer set of epoch-based states, all implementing the EpochState interface.

State Hierarchy

EpochState (interface)
├── UnattachedState      — Known epoch, unknown leader (initial startup or learned higher epoch)
├── ProspectiveState      — Pre-Vote candidate; gathering pre-votes before becoming Candidate
├── CandidateState         — Standard Raft candidate; actively requesting votes
├── FollowerState          — Standard Raft follower; replicating from a leader
├── LeaderState            — Standard Raft leader; accepting client writes
├── ResignedState          — Gracefully stepped-down leader; still processing resignations
└── DetachedState          — Observer or non-voter; replicating but cannot vote

Transition Diagram

                    ┌──────────────────────────────┐
                    │                              │
                    ▼                              │
              ┌──────────┐    Pre-Vote OK          │
              │Unattached│────────────────┐         │
              └──────────┘                │         │
                    │                      ▼         │
                    │              ┌────────────┐    │
         Discover   │              │ Prospective│    │
         Leader     │              │  (Pre-Vote)│    │
                    │              └────────────┘    │
                    │                    │           │
                    │         Pre-Vote   │           │
                    │         succeeded  │           │
                    │                    ▼           │
                    │           ┌────────────┐       │
                    │           │ Candidate  │───────┤ Won election
                    │           └────────────┘       │
                    │                 │               │
                    │       Lost      │ Won election  │
                    │       election  │ (quorum)      │
                    │                 ▼               │
                    │         ┌────────────┐          │
                    └───────► │  Follower  │◄────────┤
                              └────────────┘          │
                                    │                  │
                              Become leader             │
                                    │                  │
                                    ▼                  │
                              ┌────────────┐           │
                              │   Leader   │───────────┘ Higher term
                              └────────────┘
                                    │
                              Resign (graceful)
                                    │
                                    ▼
                              ┌────────────┐
                              │  Resigned  │
                              └────────────┘

Additionally, DetachedState is used for observers (non-voters) who track the leader but don't participate in elections.


3. KRaft State Variables (from Kafka Source)

Reference: Source [8] — Apache Kafka kafka/raft/ package

3.1 Common State (QuorumState)

Variable Type Description
localId Int This node's replica ID
voters Set(Int) Set of voter replica IDs
observers Set(Int) Set of observer replica IDs
epochState EpochState Current state object (one of the states below)
currentClaimedEpoch Int Epoch this node last claimed (for persistence)

3.2 UnattachedState

When a node knows about an epoch but hasn't discovered the leader yet, or is starting up.

Variable Type Description
epoch Int The epoch this node is in
votedId Int? Replica ID voted for in this epoch, or -1 (unvoted)

3.3 ProspectiveState (Pre-Vote, KIP-996)

A node that has started Pre-Vote but hasn't transitioned to Candidate yet. This prevents disruptive servers from forcing elections.

Variable Type Description
epoch Int The epoch for which Pre-Vote is being conducted
votedId Int? Vote (always self during Pre-Vote)
hasSufficientVote Bool Whether a quorum of pre-votes has been received
preVoteResponded Set(Int) Voters that responded to Pre-Vote
preVoteGranted Set(Int) Voters that granted Pre-Vote
gracePeriodExpired Bool Whether the election timeout has expired

Transition to Candidate: Only when hasSufficientVote = true AND gracePeriodExpired = true.

3.4 CandidateState

Standard Raft candidate with KRaft-specific additions.

Variable Type Description
epoch Int Current election epoch
votedId Int Always localId (votes for self)
votes Set(Int) Voter IDs that granted a vote
isVoteGranted Bool Whether a quorum of votes has been received
isVoteRejected Bool Whether the election is doomed (quorum impossible)
highWatermark Long Last known high watermark at election start
retryVoteQuorumGracePeriod Bool Whether to retry sending vote requests

3.5 FollowerState

Standard Raft follower with KRaft-specific additions.

Variable Type Description
epoch Int Current epoch
leaderId Int ID of the current leader
votedId Int? Replica ID voted for (or -1)
highWatermark Long Last known high watermark
lastCaughtUpTimeMs Long Timestamp of last successful replication
lingerHighWatermark Long High watermark at which we started lingering
isLingering Bool Whether this follower is in linger mode (delaying transition)

3.6 LeaderState

Standard Raft leader with KRaft-specific additions.

Variable Type Description
epoch Int Current epoch (same as currentTerm)
leaderId Int This node's ID
highWatermark Long Current high watermark (equivalent to commitIndex)
voterStates Map(Int, VoterState) Per-voter tracking: nextOffset, matchOffset, lastCaughtUpTime
localId Int This node's replica ID
retryForVoterEnabled Bool Whether to retry AppendEntries to slow voters

VoterState sub-structure:

Variable Type Description
nextOffset Long Next log offset to send to this voter (= nextIndex)
matchOffset Long Highest offset known to be replicated on this voter (= matchIndex)
lastCaughtUpTimeMs Long Timestamp of last successful catch-up
lastSentTimeMs Long Timestamp of last AppendEntries sent
isVoter Bool Whether this replica is a voter

3.7 ResignedState

A leader that has gracefully stepped down. Not present in vanilla Raft.

Variable Type Description
epoch Int The epoch when the leader resigned
leaderId Int Former leader's ID

Key behavior: A resigned leader does not accept new client writes. It continues to process in-flight requests until they complete or time out. It then transitions to Unattached or Follower depending on what it learns about the new epoch.

3.8 DetachedState (Observer)

Used for observer (non-voter) nodes.

Variable Type Description
epoch Int Current epoch
leaderId Int ID of the leader being followed
highWatermark Long Last known high watermark

Key behavior: Detached observers replicate the log but never vote or become candidates. They can transition to Follower or Unattached if they learn about a new epoch.


4. KRaft State Transitions (Detailed)

Reference: Source [8], [9]

4.1 Complete Transition Table

From To Trigger Preconditions
Unattached Prospective Election timeout + Pre-Vote enabled votedId == -1, hasSufficientVote == false
Unattached Follower DiscoverLeader with valid leader leaderId known, epoch matches
Prospective Candidate Pre-Vote quorum reached + grace period expired hasSufficientVote ∧ gracePeriodExpired
Prospective Follower DiscoverLeader OR higher AppendEntries leaderId known or higher term received
Candidate Leader Election won (quorum of votes) isVoteGranted == true
Candidate Follower DiscoverLeader OR higher term AppendEntries leaderId known or higher term received
Follower Prospective Election timeout + no heartbeat votedId == -1, Pre-Vote enabled
Follower Candidate Election timeout + Pre-Vote disabled votedId == -1
Leader Resigned Graceful shutdown / Resign RPC Leader decides to step down
Leader Follower Higher term received m.epoch > currentEpoch
Resigned Unattached Resign processing complete + no leader known All in-flight requests handled
Resigned Follower Learn about new leader leaderId known in new epoch
Detached Follower Promoted to voter Node gains voting right
Any Unattached Higher epoch learned (no leader known) m.epoch > currentEpoch

4.2 Pre-Vote Protocol (KIP-996)

Reference: Source [6]

Pre-Vote prevents disruptive servers (those that are partitioned or have stale logs) from forcing unnecessary elections.

Protocol:

  1. Before becoming Candidate, a server first enters Prospective state and sends Pre-Vote requests.
  2. Pre-Vote Request: {lastOffset: Long, lastEpoch: Int} — the prospective candidate advertises its log position.
  3. Pre-Vote Response: {granted: Bool} — a server grants Pre-Vote only if:
    • The prospective candidate's log is at least as up-to-date as the responder's
    • The responder has not heard from a valid leader recently (within election timeout)
  4. Transition to Candidate: Only after receiving a quorum of Pre-Vote grants AND the grace period expires.

Why Pre-Vote matters for verification:

  • Prevents a partitioned node with a stale term from disrupting the cluster
  • The invariant is: a node only enters Candidate state if it could potentially win an election
  • This eliminates a class of livelock scenarios where nodes continuously force elections they can't win

4.3 Resignation Protocol

Reference: Source [8], [9]

A KRaft leader can gracefully resign (step down) without crashing. This is not in vanilla Raft.

Resign process:

  1. Leader transitions to Resigned state
  2. Stops accepting new client writes
  3. Continues to serve in-flight AppendEntries responses
  4. Other nodes see an election timeout and start a new election
  5. Resigned leader may learn about the new leader and transition to Follower

Verification concern: During the resign window, the old leader's committed entries are still valid, but no new entries can be committed. This is a liveness property (eventually a new leader is elected), not a safety concern.


5. Safety Invariants

References: Sources [1], [2], [3]

These are the invariants we need to prove in SpeckDL/Z3.

5.1 Raft Paper Invariants (vanilla Raft)

I1: Election Safety. At most one leader per term.

∀ i, j ∈ Server:
  state[i] = Leader ∧ state[j] = Leader ∧ currentTerm[i] = currentTerm[j] → i = j

I2: Leader Completeness. If a log entry is committed in term T, then all leaders for terms > T have that entry.

This is the core invariant. Proved via the "Leader Completeness Property" in the Raft paper.
Inductive argument: a new leader must have all committed entries from prior terms 
because it received votes from a quorum that overlaps with the quorum that committed 
those entries.

I3: Log Matching. If two entries in different logs have the same index and term, then all preceding entries are identical.

∀ i, j ∈ Server, idx:
  log[i][idx].term = log[j][idx].term → log[i][1..idx] = log[j][1..idx]

I4: Leader Append-Only. A leader never overwrites or deletes entries in its log.

∀ i: state[i] = Leader → log[i] only grows (entries appended, never removed)

I5: Term Monotonicity. Current term never decreases.

∀ i: currentTerm'[i] ≥ currentTerm[i]

I6: Commit Safety. An entry is committed only if a quorum has replicated it.

commitIndex[i] advances only when a quorum of matchIndex[i][k] ≥ N
Additionally: only entries from the current term can be committed via leader
advance (restriction from Raft paper Figure 8).

5.2 KRaft-Specific Invariants

K1: Quorum Overlap. Any two quorums in the same epoch share at least one voter.

∀ Q1, Q2 ∈ Quorum: Q1 ∩ Q2 ≠ ∅

K2: High Watermark Monotonicity. A node's highWatermark never decreases.

∀ i: highWatermark'[i] ≥ highWatermark[i]

K3: High Watermark Validity. A node's highWatermark is always ≤ the log end offset.

∀ i: highWatermark[i] ≤ logEndOffset[i]

K4: Observer Consistency. An observer's log is a prefix of some voter's log that was committed.

∀ observer o, voter v, offset idx:
  o.hasOffset(idx) → ∃v ∈ Voters: v.committed(idx) ∧ o.log[1..idx] = v.log[1..idx]

K5: Leader Uniqueness (KRaft). At most one leader per epoch across all nodes.

∀ i, j ∈ Replicas: 
  state[i] = Leader ∧ state[j] = Leader ∧ epoch[i] = epoch[j] → i = j

K6: Vote Uniqueness. A voter votes for at most one candidate per epoch.

∀ i ∈ Voters, epoch e: votedFor[i][e] is either Nil or a single replica ID

6. Snapshots (KIP-630)

Reference: Source [5]

KRaft supports snapshot-based log compaction for the metadata log.

6.1 Snapshot Mechanics

  • Snapshot trigger: When the log grows beyond a configured threshold
  • Snapshot contents: Full metadata state at a given offset (topic configs, partition assignments, etc.)
  • Snapshot delivery: Leader sends snapshots to followers that are lagging (similar to Raft's InstallSnapshot)

6.2 State Changes for Snapshots

Variable Type Description
snapshotId {epoch: Int, endOffset: Long} Identifies a snapshot by epoch and end offset
lastSnapshotOffset Long Offset of the last snapshot on this node
recoveryState Enum {RECOVERING, COMPLETE} — whether snapshot recovery is in progress

6.3 Invariants for Snapshots

S1: Snapshot Offset Monotonicity. Snapshot offsets increase monotonically.

snapshotId.endOffset[i]' ≥ snapshotId.endOffset[i]

S2: Snapshot Consistency. A snapshot at offset N contains all entries committed before N.

∀ i, snapshot at offset N: 
  snapshot.epoch = log[N].term ∧ snapshot contains all committed entries up to N

7. Known Verification Gaps and Bugs

References: Smart Casual Verification blog, raft-dev Google Group, Source [6]

7.1 Known Bugs in Raft Implementations

Bug 1: Single-Server Membership Change Safety Bug (2015)

  • Source: raft-dev Google Group, reported by Ongaro
  • Issue: The original Raft paper's algorithm for single-server membership changes has a safety bug. If two configuration changes overlap, the cluster can lose quorum overlap.
  • Mitigation: Joint consensus (adding then removing in separate steps) is safe. KRaft uses joint consensus for membership changes (KIP-853).
  • Formal verification target: Prove that KRaft's joint consensus approach maintains quorum overlap during membership changes.

Bug 2: Stale Leader Committing Old-Term Entries (Figure 8 Bug)

  • Source: Raft paper, Figure 8
  • Issue: A leader must not commit entries from previous terms directly. It can only commit entries from its current term, and previous-term entries are committed indirectly when the current-term entry is committed.
  • Status: Correctly handled in KRaft (AdvanceCommitIndex only advances for current-term entries).
  • Formal verification target: Prove that AdvanceCommitIndex only advances commitIndex when the entry at the target index has term == currentTerm.

Bug 3: Log Truncation During Leader Change

  • Source: Common implementation bug across multiple Raft implementations
  • Issue: When a new leader overwrites conflicting entries on a follower, the truncation must be atomic. If the node crashes mid-truncation, the log can be left in an inconsistent state.
  • Status: KRaft handles this by truncating before appending, but edge cases around partial truncation exist.
  • Formal verification target: Prove that log truncation is atomic (either fully applied or not at all).

Bug 4: Disruptive Servers (Pre-Vote Target)

  • Source: KIP-996, Ongaro dissertation §9.6
  • Issue: A server that is partitioned or has a stale network connection can repeatedly force elections, disrupting the cluster. Without Pre-Vote, a partitioned server increments its term and forces elections it can't win.
  • Status: KIP-996 (Pre-Vote) addresses this by requiring a server to confirm it can win before starting an election.
  • Formal verification target: Prove that with Pre-Vote, a node that cannot win an election never transitions to Candidate state.

7.2 Smart Casual Verification Findings

Reference: Decentralized Thoughts blog, "Smart Casual Verification"

In a systematic review of consensus protocol implementations, researchers found:

  1. 5 safety bugs — violations of agreement or integrity properties
  2. 1 liveness bug — violation of termination under expected conditions
  3. All bugs were found through informal reasoning about protocol invariants, not formal proof
  4. Key pattern: most bugs occur in edge cases around term changes and log conflicts

7.3 Open Verification Targets

These are properties that should be verified in the SpeckDL/Z3 spec:

  1. Election safety under network partitions: Prove I1 (election safety) holds even during partitions.
  2. Log matching after truncation: Prove I3 (log matching) holds after follower log truncation.
  3. Leader completeness after Pre-Vote: Prove I2 (leader completeness) holds with the Pre-Vote extension.
  4. High watermark advancement: Prove that highWatermark only advances when a quorum has replicated.
  5. Graceful resignation liveness: Prove that after a leader resigns, a new leader is eventually elected (liveness, assuming fair loss and eventual delivery).
  6. Observer divergence: Prove that observers cannot diverge from voters (K4).
  7. Joint consensus safety (KIP-853): Prove that during membership changes, quorum overlap is maintained.

8. Producer Acknowledgement Semantics (for M1.3)

Deferred — included here for reference only.

acks=0 (Fire and forget)

  • Producer sends and doesn't wait for response
  • No durability guarantee
  • Invariant: None (messages can be lost)

acks=1 (Leader acknowledgment)

  • Producer waits for leader to write to local log
  • Durability: survives leader failure, but NOT follower failure
  • Invariant: If ack received, entry exists in leader's log

acks=all (ISR acknowledgment)

  • Producer waits for all in-sync replicas (ISR) to acknowledge
  • Durability: survives any failure that leaves a quorum
  • Invariant: If ack received, entry is committed (quorum has replicated)

Exactly-once Semantics

  • Requires idempotent producer + transaction coordinator
  • Idempotent producer: assigns sequence numbers; broker deduplicates
  • Transaction coordinator: two-phase commit across partitions

Key Invariants for Producer Acks

  1. acks=all → no data loss on leader failover (bounded to quorum availability)
  2. Idempotent producer → no duplicate messages for same PID + sequence number
  3. Transaction atomicity → all partitions in a transaction commit or none do

9. Consumer Group Rebalancing (for M1.3)

Deferred — included here for reference only.

States

  • Empty → Joining → Stable → Rebalancing → Stable

Actions

  • JoinGroup: consumer requests to join
  • SyncGroup: consumer receives assignment
  • Heartbeat: consumer indicates liveness
  • LeaveGroup: consumer departs
  • Rebalance: triggered by membership change or topic partition change

Invariants

  1. No concurrent assignments to the same partition
  2. All partitions assigned when group is stable
  3. Offset monotonicity: committed offsets never decrease
  4. No two consumers assigned the same partition simultaneously

10. KIPs Reference

KIP Title Status Relevance
KIP-500 Next Generation Client Protocol Adopted Foundation for KRaft
KIP-595 A Raft Protocol for the Metadata Quorum Adopted Core KRaft protocol design
KIP-630 Kafka Raft Snapshot Adopted Snapshot/compaction for metadata log
KIP-642 Dynamic Configuration in KRaft Adopted Dynamic config updates via KRaft
KIP-853 KRaft Controller Membership Changes Implemented Joint consensus for membership changes
KIP-996 Pre-Vote for KRaft Implementing Pre-Vote phase to prevent disruptive elections
KIP-830 KRaft Metadata Quorum Adopted Metadata quorum implementation details

Appendix A: Message Types (KRaft)

Internal KRaft RPC Messages

Message Direction Fields
VoteRequest Candidate → Voter candidateId, epoch, lastOffset, lastEpoch
VoteResponse Voter → Candidate epoch, voteGranted, leaderId
BeginQuorumEpoch Leader → Follower leaderId, epoch
EndQuorumEpoch Leader → Voter (resign) leaderId, epoch
FetchSnapshot Follower → Leader snapshotId, offset
Fetch Follower → Leader followerId, epoch, lastFetchedEpoch, lastFetchedOffset, maxBytes
FetchResponse Leader → Follower epoch, leaderId, highWatermark, records

Note: KRaft uses BeginQuorumEpoch as the heartbeat mechanism (instead of vanilla Raft's empty AppendEntries).


Appendix B: Key Differences from Vanilla Raft

Aspect Vanilla Raft KRaft
Term naming "Term" "Epoch" (functionally equivalent)
Heartbeat Empty AppendEntries BeginQuorumEpoch
Resignation Not specified EndQuorumEpoch (graceful shutdown)
Pre-Vote Not in original paper ProspectiveState (KIP-996)
States Follower, Candidate, Leader + Unattached, Prospective, Resigned, Detached
Snapshot InstallSnapshot FetchSnapshot (KIP-630)
Membership Single-server or joint Joint consensus only (KIP-853)
Log content Arbitrary client values Metadata records (topic configs, partition assignments)
Quorum All servers vote Voters vote, observers replicate
Client ID Not specified Replica ID (localId)

Document compiled from: Raft TLA+ spec (Ongaro 2014), Apache Kafka source code (kafka/raft/), KIP-595/630/642/853/996, Red Hat KRaft deep dive, Smart Casual Verification findings.