Skip to main content

krabka_kraft_core/core/
membership.rs

1//! Voter-set updates and the KIP-853 adjacent-voter view.
2//!
3//! A replica applies a `VotersRecord` as soon as it reads one, before the
4//! record commits, so it must keep both sides of the one in-flight
5//! transition addressable. This module holds that bookkeeping and the role
6//! changes a membership change forces on the local replica.
7
8use {
9    super::QuorumStateMachine,
10    crate::{
11        action::{Action, TimerKind},
12        role::Role,
13        types::{NodeId, ReplicaKey, SimInstant},
14    },
15    krabka_voters::VoterSet,
16};
17
18impl QuorumStateMachine {
19    /// Apply the latest voter set read from the Raft log or a snapshot.
20    ///
21    /// KIP-853 requires replicas to use an uncommitted `VotersRecord`
22    /// immediately. The durable engine owns record history and invokes this
23    /// method again with the preceding set if the log is truncated.
24    pub fn apply_voter_set(&mut self, voters: VoterSet, now: SimInstant) -> Vec<Action> {
25        let was_voter = self.is_voter();
26        let leader_id = self.state.leader_id;
27        if self.state.voters != voters {
28            self.adjacent_voters = Some(self.state.voters.clone());
29        }
30        self.state.voters = voters;
31
32        if let Role::Leader {
33            replicas,
34            fetched_voters,
35            ..
36        } = &mut self.role
37        {
38            replicas.retain(|id, _| self.state.voters.contains(*id) && *id != self.me);
39            for id in self.state.voters.ids() {
40                if id != self.me {
41                    replicas.entry(id).or_default();
42                }
43            }
44            // Start a fresh check-quorum window against the new configuration,
45            // tally and deadline together. Re-arming matters because a sole
46            // voter arms nothing at promotion, so without it the leader of a
47            // cluster that has just grown past one voter would never run
48            // check-quorum again for its epoch; it also gives an added voter a
49            // full window to make its first Fetch rather than deposing the
50            // leader the instant the record applies. Emptying the tally is the
51            // other half: a contact counted before the change belongs to the
52            // window that just ended, and carrying it over would let one stale
53            // fetch plus one fresh one re-arm a window no majority reached.
54            fetched_voters.clear();
55            if self.runs_check_quorum() {
56                return vec![Action::ResetTimer {
57                    kind: TimerKind::CheckQuorum,
58                    deadline: self.check_quorum_deadline(now),
59                }];
60            }
61            return Vec::new();
62        }
63
64        match (was_voter, self.is_voter()) {
65            (true, false) => {
66                let fetch_deadline = now.saturating_add_ms(self.fetch_timeout_ms);
67                self.role = Role::Observer {
68                    leader_id,
69                    fetch_deadline,
70                };
71                let mut actions = vec![Action::TransitionedTo(self.role.name())];
72                if let Some(leader_id) = leader_id {
73                    actions.push(Action::SendFetch { leader_id });
74                    actions.push(Action::ResetTimer {
75                        kind: TimerKind::Fetch,
76                        deadline: fetch_deadline,
77                    });
78                }
79                actions
80            }
81            (false, true) => {
82                if let Some(leader_id) = leader_id {
83                    let fetch_deadline = now.saturating_add_ms(self.fetch_timeout_ms);
84                    self.role = Role::Follower {
85                        leader_id,
86                        fetch_deadline,
87                        has_fetched_from_leader: false,
88                    };
89                    vec![
90                        Action::TransitionedTo(self.role.name()),
91                        Action::SendFetch { leader_id },
92                        Action::ResetTimer {
93                            kind: TimerKind::Fetch,
94                            deadline: fetch_deadline,
95                        },
96                    ]
97                } else {
98                    let election_deadline = self.election_deadline(now);
99                    self.role = Role::Unattached { election_deadline };
100                    vec![
101                        Action::TransitionedTo(self.role.name()),
102                        Action::ResetTimer {
103                            kind: TimerKind::Election,
104                            deadline: election_deadline,
105                        },
106                    ]
107                }
108            }
109            _ => Vec::new(),
110        }
111    }
112
113    /// Apply a committed `KRaftVersionRecord`.
114    pub fn set_kraft_version(&mut self, version: u16) { self.state.kraft_version = version; }
115
116    /// Forget the preceding voter view once the latest voter record commits.
117    pub fn commit_voter_set(&mut self) { self.adjacent_voters = None; }
118
119    fn voter_key_matches(&self, voters: &VoterSet, key: ReplicaKey) -> bool {
120        voters.get(key.id).is_some_and(|voter| {
121            self.state.kraft_version == 0 || voter.directory_id == key.directory_id
122        })
123    }
124
125    pub(super) fn local_voter_directory_matches(&self, directory_id: uuid::Uuid) -> bool {
126        self.state
127            .voters
128            .get(self.me)
129            .is_none_or(|voter| self.state.kraft_version == 0 || voter.directory_id == directory_id)
130    }
131
132    /// Voter `id` from the current voter set, or from the adjacent set of an
133    /// uncommitted KIP-853 change. A leader that only the adjacent set names is
134    /// still followed (see `handle_begin_quorum_epoch`), so its endpoints are
135    /// looked up the same way.
136    #[must_use]
137    pub fn current_or_adjacent_voter_entry(&self, id: NodeId) -> Option<&krabka_voters::Voter> {
138        self.state.voters.get(id).or_else(|| {
139            let voters = self.adjacent_voters.as_ref()?;
140            voters.get(id)
141        })
142    }
143
144    pub(super) fn current_or_adjacent_voter(&self, id: NodeId) -> bool {
145        self.state.voters.contains(id)
146            || self
147                .adjacent_voters
148                .as_ref()
149                .is_some_and(|voters| voters.contains(id))
150    }
151
152    pub(super) fn current_or_adjacent_voter_key(&self, key: ReplicaKey) -> bool {
153        self.voter_key_matches(&self.state.voters, key)
154            || self
155                .adjacent_voters
156                .as_ref()
157                .is_some_and(|voters| self.voter_key_matches(voters, key))
158    }
159
160    pub(super) fn same_voter(&self, left: ReplicaKey, right: ReplicaKey) -> bool {
161        left.id == right.id
162            && (self.state.kraft_version == 0 || left.directory_id == right.directory_id)
163    }
164
165    /// Complete removal of the local leader after the reduced voter set has
166    /// committed. Fetch serving continues until the engine invokes this edge.
167    ///
168    /// The removed leader becomes a discovering observer and arms its fetch
169    /// timer. No later leader announces itself to a node outside the voter
170    /// set, so the Fetch that the timer sends is how this node finds the new
171    /// leader. Kafka's removed leader also goes on as an observer of its
172    /// successor, and `DescribeQuorum` on that successor lists it.
173    pub fn finish_local_leader_removal(&mut self, now: SimInstant) -> Vec<Action> {
174        if self.is_voter() || !self.role.is_leader() {
175            return Vec::new();
176        }
177        let epoch = self.state.leader_epoch;
178        let preferred_successors = self.preferred_successors();
179        self.state.leader_id = None;
180        let fetch_deadline = now.saturating_add_ms(self.fetch_timeout_ms);
181        self.role = Role::Observer {
182            leader_id: None,
183            fetch_deadline,
184        };
185        vec![
186            Action::SendEndQuorumEpoch {
187                epoch,
188                preferred_successors,
189            },
190            Action::PersistQuorumState,
191            Action::TransitionedTo(self.role.name()),
192            Action::ResetTimer {
193                kind: TimerKind::Fetch,
194                deadline: fetch_deadline,
195            },
196        ]
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use {
203        super::*,
204        crate::{
205            core::test_support::{FakeLog, machine, voters, win_election},
206            event::Event,
207        },
208        assert2::check,
209    };
210
211    #[test]
212    fn kraft_version_and_commit_voter_set() {
213        let mut m = machine(NodeId(1), &[NodeId(1), NodeId(2)]);
214        m.set_kraft_version(2);
215        check!(m.quorum_state().kraft_version == 2);
216
217        m.adjacent_voters = Some(voters(&[NodeId(1)]));
218        m.commit_voter_set();
219        check!(m.adjacent_voters.is_none());
220    }
221
222    #[test]
223    fn same_voter_and_key_matching() {
224        let mut m = machine(NodeId(1), &[NodeId(1), NodeId(2)]);
225        let k1 = ReplicaKey {
226            id: NodeId(1),
227            directory_id: uuid::Uuid::from_u128(100),
228        };
229        let k2 = ReplicaKey {
230            id: NodeId(1),
231            directory_id: uuid::Uuid::from_u128(200),
232        };
233        let k3 = ReplicaKey {
234            id: NodeId(2),
235            directory_id: uuid::Uuid::from_u128(100),
236        };
237
238        // kraft_version == 0: compares only id
239        m.set_kraft_version(0);
240        check!(m.same_voter(k1, k2));
241        check!(!m.same_voter(k1, k3));
242
243        // kraft_version > 0: compares both id and directory_id
244        m.set_kraft_version(1);
245        check!(!m.same_voter(k1, k2));
246        check!(m.same_voter(k1, k1));
247        check!(!m.same_voter(k1, k3));
248    }
249
250    #[test]
251    fn current_or_adjacent_voter_queries() {
252        let mut m = machine(NodeId(1), &[NodeId(1), NodeId(2)]);
253        m.adjacent_voters = Some(voters(&[NodeId(3)]));
254
255        check!(m.current_or_adjacent_voter(NodeId(1)));
256        check!(m.current_or_adjacent_voter(NodeId(2)));
257        check!(m.current_or_adjacent_voter(NodeId(3)));
258        check!(!m.current_or_adjacent_voter(NodeId(4)));
259
260        check!(m.current_or_adjacent_voter_entry(NodeId(1)).is_some());
261        check!(m.current_or_adjacent_voter_entry(NodeId(3)).is_some());
262        check!(m.current_or_adjacent_voter_entry(NodeId(4)).is_none());
263    }
264
265    #[test]
266    fn finish_local_leader_removal_guards_and_transitions() {
267        let mut m = machine(NodeId(1), &[NodeId(1), NodeId(2)]);
268        let log = FakeLog {
269            end: 5,
270            last_epoch: 1,
271        };
272        // 1. If still voter, returns empty
273        check!(m.finish_local_leader_removal(SimInstant(10)).is_empty());
274
275        // 2. Win election to become leader
276        win_election(&mut m, &log, &[NodeId(2)], SimInstant(20));
277        check!(m.role().is_leader());
278
279        // Still a voter: returns empty
280        check!(m.finish_local_leader_removal(SimInstant(30)).is_empty());
281
282        // 3. Apply a voter set that excludes self (NodeId 1 is now non-voter)
283        m.apply_voter_set(voters(&[NodeId(2), NodeId(3)]), SimInstant(40));
284        check!(!m.is_voter());
285        check!(m.role().is_leader());
286
287        // Now finish_local_leader_removal succeeds: it hands the epoch to the
288        // remaining voters and becomes a discovering observer whose fetch
289        // timer is armed, so its next Fetch finds the new leader.
290        let epoch = m.quorum_state().leader_epoch;
291        let preferred_successors = m.preferred_successors();
292        let actions = m.finish_local_leader_removal(SimInstant(50));
293        let fetch_deadline = SimInstant(50).saturating_add_ms(m.fetch_timeout_ms);
294        check!(
295            (m.role().clone(), m.quorum_state().leader_id, actions)
296                == (
297                    Role::Observer {
298                        leader_id: None,
299                        fetch_deadline,
300                    },
301                    None,
302                    vec![
303                        Action::SendEndQuorumEpoch {
304                            epoch,
305                            preferred_successors,
306                        },
307                        Action::PersistQuorumState,
308                        Action::TransitionedTo("Observer"),
309                        Action::ResetTimer {
310                            kind: TimerKind::Fetch,
311                            deadline: fetch_deadline,
312                        },
313                    ]
314                )
315        );
316    }
317
318    #[test]
319    fn apply_voter_set_role_transitions_and_adjacent_tracking() {
320        let mut m = machine(NodeId(1), &[NodeId(1), NodeId(2)]);
321        check!(m.adjacent_voters.is_none());
322
323        // Applying SAME voter set does not set adjacent_voters
324        m.apply_voter_set(voters(&[NodeId(1), NodeId(2)]), SimInstant(10));
325        check!(m.adjacent_voters.is_none());
326
327        // Applying DIFFERENT voter set sets adjacent_voters to previous set
328        m.apply_voter_set(voters(&[NodeId(1), NodeId(2), NodeId(3)]), SimInstant(20));
329        check!(m.adjacent_voters.is_some());
330        check!(
331            m.adjacent_voters
332                .as_ref()
333                .unwrap()
334                .ids()
335                .iter()
336                .copied()
337                .collect::<Vec<_>>()
338                == vec![NodeId(1), NodeId(2)]
339        );
340
341        // Transition from voter to observer: (was_voter: true, is_voter: false)
342        let mut m2 = machine(NodeId(1), &[NodeId(1), NodeId(2)]);
343        let log = FakeLog {
344            end: 5,
345            last_epoch: 1,
346        };
347        m2.on_event(
348            Event::ReceiveBeginQuorumEpoch {
349                leader_id: NodeId(2),
350                leader_epoch: 4,
351            },
352            &log,
353            SimInstant(10),
354        );
355        check!(matches!(m2.role(), Role::Follower { .. }));
356        let actions = m2.apply_voter_set(voters(&[NodeId(2), NodeId(3)]), SimInstant(30));
357        check!(matches!(m2.role(), Role::Observer { .. }));
358        check!(actions.iter().any(|a| matches!(a, Action::SendFetch {
359            leader_id: NodeId(2)
360        })));
361
362        // Transition from observer to voter: (was_voter: false, is_voter: true)
363        let mut m3 = machine(NodeId(1), &[NodeId(2), NodeId(3)]);
364        check!(matches!(m3.role(), Role::Observer { .. }));
365        let actions =
366            m3.apply_voter_set(voters(&[NodeId(1), NodeId(2), NodeId(3)]), SimInstant(40));
367        check!(matches!(m3.role(), Role::Unattached { .. }));
368        check!(actions.iter().any(|a| matches!(a, Action::ResetTimer {
369            kind: TimerKind::Election,
370            ..
371        })));
372    }
373}