1use {
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 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 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 pub fn set_kraft_version(&mut self, version: u16) { self.state.kraft_version = version; }
115
116 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 #[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 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 m.set_kraft_version(0);
240 check!(m.same_voter(k1, k2));
241 check!(!m.same_voter(k1, k3));
242
243 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 check!(m.finish_local_leader_removal(SimInstant(10)).is_empty());
274
275 win_election(&mut m, &log, &[NodeId(2)], SimInstant(20));
277 check!(m.role().is_leader());
278
279 check!(m.finish_local_leader_removal(SimInstant(30)).is_empty());
281
282 m.apply_voter_set(voters(&[NodeId(2), NodeId(3)]), SimInstant(40));
284 check!(!m.is_voter());
285 check!(m.role().is_leader());
286
287 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 m.apply_voter_set(voters(&[NodeId(1), NodeId(2)]), SimInstant(10));
325 check!(m.adjacent_voters.is_none());
326
327 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 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 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}