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
738 lines
No EOL
32 KiB
Markdown
738 lines
No EOL
32 KiB
Markdown
# 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.* |