1use std::collections::HashMap;
42
43use crate::{
44 lease::LeaseConfig,
45 record::{self, CoordinationKey, CoordinationRecord, MemberId, RecordKind, Role},
46};
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct RosterEntry {
55 pub member: MemberId,
57 pub offset: i64,
59 pub registered_at: i64,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Eq)]
67pub struct RoleState {
68 pub roster: Vec<RosterEntry>,
70 pub lease: Option<record::Lease>,
72}
73
74impl RoleState {
75 #[must_use]
82 pub fn from_records<I>(role: Role, records: I) -> Self
83 where
84 I: IntoIterator<Item = (i64, CoordinationKey, CoordinationRecord)>,
85 {
86 let mut builder = RoleStateBuilder::new(role);
87 for (offset, key, record) in records {
88 builder.apply(offset, key, record);
89 }
90 builder.build()
91 }
92
93 #[must_use]
98 pub fn holder(&self) -> Option<&MemberId> {
99 self.lease.as_ref().map(|lease| &lease.member)
100 }
101
102 #[must_use]
104 pub fn entry(&self, member: &MemberId) -> Option<&RosterEntry> {
105 self.roster.iter().find(|entry| entry.member == *member)
106 }
107
108 #[must_use]
116 pub fn rank_of(&self, member: &MemberId) -> Option<usize> {
117 self.entry(member)?;
118 let holder = self.holder();
119 if holder == Some(member) {
120 return Some(0);
121 }
122 self.roster
123 .iter()
124 .filter(|entry| Some(&entry.member) != holder)
125 .position(|entry| entry.member == *member)
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134struct MemberSlot {
135 offset: i64,
136 registered_at: Option<i64>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141struct LeaseSlot {
142 offset: i64,
143 lease: Option<record::Lease>,
144}
145
146#[derive(Debug, Clone)]
156pub struct RoleStateBuilder {
157 role: Role,
158 members: HashMap<MemberId, MemberSlot>,
159 lease: Option<LeaseSlot>,
160}
161
162impl RoleStateBuilder {
163 #[must_use]
165 pub fn new(role: Role) -> Self {
166 Self {
167 role,
168 members: HashMap::new(),
169 lease: None,
170 }
171 }
172
173 #[must_use]
175 pub const fn role(&self) -> &Role {
176 &self.role
177 }
178
179 pub fn apply(&mut self, offset: i64, key: CoordinationKey, record: CoordinationRecord) {
187 if key.role != self.role {
188 return;
189 }
190 match (key.kind, record) {
191 (RecordKind::Registration, CoordinationRecord::Registration(registration)) => {
192 if let Some(member) = key.member {
193 self.put_member(offset, member, Some(registration.registered_at));
194 }
195 }
196 (RecordKind::Registration, CoordinationRecord::Tombstone) => {
197 if let Some(member) = key.member {
198 self.put_member(offset, member, None);
199 }
200 }
201 (RecordKind::Lease, CoordinationRecord::Lease(lease)) => {
202 self.put_lease(offset, Some(lease));
203 }
204 (RecordKind::Lease, CoordinationRecord::Tombstone) => self.put_lease(offset, None),
205 (RecordKind::Registration, CoordinationRecord::Lease(_))
206 | (RecordKind::Lease, CoordinationRecord::Registration(_)) => {}
207 }
208 }
209
210 #[must_use]
212 pub fn build(self) -> RoleState {
213 let mut roster: Vec<RosterEntry> = self
214 .members
215 .into_iter()
216 .filter_map(|(member, slot)| {
217 slot.registered_at.map(|registered_at| RosterEntry {
218 member,
219 offset: slot.offset,
220 registered_at,
221 })
222 })
223 .collect();
224 roster.sort_unstable_by_key(|entry| entry.offset);
225 RoleState {
226 roster,
227 lease: self.lease.and_then(|slot| slot.lease),
228 }
229 }
230
231 fn put_member(&mut self, offset: i64, member: MemberId, registered_at: Option<i64>) {
232 let slot = MemberSlot {
233 offset,
234 registered_at,
235 };
236 match self.members.get_mut(&member) {
237 Some(held) if held.offset >= offset => {}
238 Some(held) => *held = slot,
239 None => {
240 self.members.insert(member, slot);
241 }
242 }
243 }
244
245 fn put_lease(&mut self, offset: i64, lease: Option<record::Lease>) {
246 if self
247 .lease
248 .as_ref()
249 .is_some_and(|held| held.offset >= offset)
250 {
251 return;
252 }
253 self.lease = Some(LeaseSlot { offset, lease });
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
259pub enum Decision {
260 Hold,
263 Challenge,
265 Wait {
267 until_millis: i64,
270 },
271 NotRegistered,
274}
275
276#[must_use]
304pub fn evaluate(
305 state: &RoleState,
306 me: &MemberId,
307 now_millis: i64,
308 config: &LeaseConfig,
309) -> Decision {
310 let (Some(entry), Some(rank)) = (state.entry(me), state.rank_of(me)) else {
311 return Decision::NotRegistered;
312 };
313 let challenge_at_millis = match state.lease.as_ref() {
314 Some(lease) => {
315 let timing = config.timing(lease);
316 if lease.member == *me && timing.is_live_at(now_millis) {
317 return Decision::Hold;
318 }
319 timing.challenge_at_millis(rank)
320 }
321 None => entry
322 .registered_at
323 .saturating_add(config.challenge_delay_millis(rank)),
324 };
325 if now_millis >= challenge_at_millis {
326 Decision::Challenge
327 } else {
328 Decision::Wait {
329 until_millis: challenge_at_millis,
330 }
331 }
332}
333
334#[cfg(test)]
335mod model;
336
337#[cfg(test)]
338mod tests {
339 use assert2::{assert, check};
340 use krabka_units::secs;
341
342 use super::*;
343 use crate::record::{FencingToken, Registration};
344
345 type LogRecord = (i64, CoordinationKey, CoordinationRecord);
347
348 fn role() -> Role {
349 Role::new("controller").expect("the role name is well formed")
350 }
351
352 fn other_role() -> Role {
353 Role::new("compactor").expect("the role name is well formed")
354 }
355
356 fn member(name: &str) -> MemberId {
357 MemberId::new(name).expect("the member id is well formed")
358 }
359
360 fn token(epoch: i16) -> FencingToken {
361 FencingToken::new(11, epoch).expect("the token is not negative")
362 }
363
364 fn config() -> LeaseConfig {
365 LeaseConfig::new(secs(30), secs(10), secs(5)).expect("the extents work together")
366 }
367
368 fn registration(offset: i64, role: &Role, name: &str, registered_at: i64) -> LogRecord {
369 (
370 offset,
371 CoordinationKey::registration(role.clone(), member(name)),
372 CoordinationRecord::Registration(Registration {
373 member: member(name),
374 registered_at,
375 }),
376 )
377 }
378
379 fn deregistration(offset: i64, role: &Role, name: &str) -> LogRecord {
380 (
381 offset,
382 CoordinationKey::registration(role.clone(), member(name)),
383 CoordinationRecord::Tombstone,
384 )
385 }
386
387 fn lease_record(
388 offset: i64,
389 role: &Role,
390 name: &str,
391 granted_at: i64,
392 deadline: i64,
393 ) -> LogRecord {
394 (
395 offset,
396 CoordinationKey::lease(role.clone()),
397 CoordinationRecord::Lease(record::Lease {
398 member: member(name),
399 token: token(1),
400 granted_at,
401 deadline,
402 }),
403 )
404 }
405
406 fn lease_tombstone(offset: i64, role: &Role) -> LogRecord {
407 (
408 offset,
409 CoordinationKey::lease(role.clone()),
410 CoordinationRecord::Tombstone,
411 )
412 }
413
414 fn entry(name: &str, offset: i64, registered_at: i64) -> RosterEntry {
415 RosterEntry {
416 member: member(name),
417 offset,
418 registered_at,
419 }
420 }
421
422 fn state_with(roster: Vec<RosterEntry>, lease: Option<record::Lease>) -> RoleState {
423 RoleState { roster, lease }
424 }
425
426 fn live_lease(name: &str) -> record::Lease {
427 record::Lease {
428 member: member(name),
429 token: token(1),
430 granted_at: 0,
431 deadline: 30_000,
432 }
433 }
434
435 #[test]
436 fn the_roster_follows_offset_order_and_not_arrival_order() {
437 let role = role();
438 let records = vec![
439 registration(30, &role, "node-3", 300),
440 registration(10, &role, "node-1", 100),
441 registration(20, &role, "node-2", 200),
442 ];
443
444 let state = RoleState::from_records(role, records);
445
446 check!(
447 state
448 == state_with(
449 vec![
450 entry("node-1", 10, 100),
451 entry("node-2", 20, 200),
452 entry("node-3", 30, 300),
453 ],
454 None,
455 )
456 );
457 }
458
459 #[test]
460 fn a_later_registration_moves_a_member_to_the_tail() {
461 let role = role();
462 let records = vec![
463 registration(10, &role, "node-1", 100),
464 registration(20, &role, "node-2", 200),
465 registration(30, &role, "node-3", 300),
466 registration(40, &role, "node-1", 400),
468 ];
469
470 let state = RoleState::from_records(role, records);
471
472 check!(
473 state
474 == state_with(
475 vec![
476 entry("node-2", 20, 200),
477 entry("node-3", 30, 300),
478 entry("node-1", 40, 400),
479 ],
480 None,
481 )
482 );
483 check!(state.rank_of(&member("node-1")) == Some(2));
484 }
485
486 #[test]
487 fn a_tombstone_removes_a_member_and_a_later_record_brings_it_back() {
488 let role = role();
489 let cases: Vec<(&str, Vec<LogRecord>, RoleState)> = vec![
490 (
491 "tombstone removes",
492 vec![
493 registration(10, &role, "node-1", 100),
494 registration(20, &role, "node-2", 200),
495 deregistration(30, &role, "node-1"),
496 ],
497 state_with(vec![entry("node-2", 20, 200)], None),
498 ),
499 (
500 "registration after a tombstone rejoins at the tail",
501 vec![
502 registration(10, &role, "node-1", 100),
503 registration(20, &role, "node-2", 200),
504 deregistration(30, &role, "node-1"),
505 registration(40, &role, "node-1", 400),
506 ],
507 state_with(
508 vec![entry("node-2", 20, 200), entry("node-1", 40, 400)],
509 None,
510 ),
511 ),
512 (
513 "a tombstone of a lower offset does not revive a member",
514 vec![
515 registration(10, &role, "node-1", 100),
516 registration(40, &role, "node-1", 400),
517 deregistration(20, &role, "node-1"),
518 ],
519 state_with(vec![entry("node-1", 40, 400)], None),
520 ),
521 ];
522
523 for (name, records, expected) in cases {
524 let state = RoleState::from_records(role.clone(), records);
525
526 check!(state == expected, "{name}");
527 }
528 }
529
530 #[test]
531 fn the_lease_of_the_highest_offset_wins_and_a_tombstone_clears_it() {
532 let role = role();
533 let cases: Vec<(&str, Vec<LogRecord>, Option<record::Lease>)> = vec![
534 (
535 "the last lease wins",
536 vec![
537 lease_record(10, &role, "node-1", 0, 30_000),
538 lease_record(20, &role, "node-2", 10_000, 40_000),
539 ],
540 Some(record::Lease {
541 member: member("node-2"),
542 token: token(1),
543 granted_at: 10_000,
544 deadline: 40_000,
545 }),
546 ),
547 (
548 "a tombstone clears the lease",
549 vec![
550 lease_record(10, &role, "node-1", 0, 30_000),
551 lease_tombstone(20, &role),
552 ],
553 None,
554 ),
555 (
556 "a lease of a lower offset does not overwrite a later one",
557 vec![
558 lease_record(20, &role, "node-2", 10_000, 40_000),
559 lease_record(10, &role, "node-1", 0, 30_000),
560 ],
561 Some(record::Lease {
562 member: member("node-2"),
563 token: token(1),
564 granted_at: 10_000,
565 deadline: 40_000,
566 }),
567 ),
568 ];
569
570 for (name, records, expected) in cases {
571 let state = RoleState::from_records(role.clone(), records);
572
573 check!(state.lease == expected, "{name}");
574 }
575 }
576
577 #[test]
578 fn the_builder_keeps_the_role_it_collects() {
579 let role = role();
580 let mut builder = RoleStateBuilder::new(role.clone());
581 let (offset, key, record) = registration(10, &role, "node-1", 100);
582 builder.apply(offset, key, record);
583
584 check!(builder.role() == &role);
585 check!(builder.build() == state_with(vec![entry("node-1", 10, 100)], None));
586 }
587
588 #[test]
589 fn the_builder_ignores_the_records_of_another_role() {
590 let role = role();
591 let other = other_role();
592 let records = vec![
593 registration(10, &role, "node-1", 100),
594 registration(20, &other, "node-9", 200),
595 lease_record(30, &other, "node-9", 200, 40_000),
596 ];
597
598 let state = RoleState::from_records(role, records);
599
600 check!(state == state_with(vec![entry("node-1", 10, 100)], None));
601 }
602
603 #[test]
604 fn the_holder_is_out_of_the_rank_order_and_keeps_rank_zero() {
605 let state = state_with(
606 vec![
607 entry("node-1", 10, 100),
608 entry("node-2", 20, 200),
609 entry("node-3", 30, 300),
610 ],
611 Some(live_lease("node-2")),
612 );
613
614 check!(state.holder() == Some(&member("node-2")));
615 check!(state.rank_of(&member("node-2")) == Some(0));
616 check!(state.rank_of(&member("node-1")) == Some(0));
617 check!(state.rank_of(&member("node-3")) == Some(1));
618 check!(state.rank_of(&member("node-9")) == None);
619 }
620
621 #[test]
622 fn a_member_outside_the_roster_gets_no_rank_and_no_challenge() {
623 let state = state_with(vec![entry("node-1", 10, 100)], Some(live_lease("node-1")));
624
625 check!(evaluate(&state, &member("node-9"), 0, &config()) == Decision::NotRegistered);
626 }
627
628 #[test]
629 fn the_live_holder_holds_and_every_standby_waits_its_stagger() {
630 let state = state_with(
631 vec![
632 entry("node-1", 10, 100),
633 entry("node-2", 20, 200),
634 entry("node-3", 30, 300),
635 ],
636 Some(live_lease("node-1")),
637 );
638 let config = config();
639
640 let cases = [
641 ("node-1", Decision::Hold),
642 (
643 "node-2",
644 Decision::Wait {
645 until_millis: 30_000,
646 },
647 ),
648 (
649 "node-3",
650 Decision::Wait {
651 until_millis: 35_000,
652 },
653 ),
654 ];
655
656 for (name, expected) in cases {
657 check!(
658 evaluate(&state, &member(name), 1_000, &config) == expected,
659 "{name}"
660 );
661 }
662 }
663
664 #[test]
665 fn the_ranks_challenge_one_stagger_apart_after_the_deadline() {
666 let state = state_with(
667 vec![
668 entry("node-1", 10, 100),
669 entry("node-2", 20, 200),
670 entry("node-3", 30, 300),
671 ],
672 Some(live_lease("node-1")),
673 );
674 let config = config();
675
676 let cases = [
677 (
679 29_999,
680 "node-2",
681 Decision::Wait {
682 until_millis: 30_000,
683 },
684 ),
685 (30_000, "node-2", Decision::Challenge),
686 (
687 30_000,
688 "node-3",
689 Decision::Wait {
690 until_millis: 35_000,
691 },
692 ),
693 (
694 34_999,
695 "node-3",
696 Decision::Wait {
697 until_millis: 35_000,
698 },
699 ),
700 (35_000, "node-3", Decision::Challenge),
701 (29_999, "node-1", Decision::Hold),
703 (30_000, "node-1", Decision::Challenge),
704 ];
705
706 for (now_millis, name, expected) in cases {
707 check!(
708 evaluate(&state, &member(name), now_millis, &config) == expected,
709 "{name} at {now_millis}"
710 );
711 }
712 }
713
714 #[test]
715 fn with_no_lease_the_registration_instant_anchors_the_stagger() {
716 let state = state_with(
717 vec![
718 entry("node-1", 10, 1_000),
719 entry("node-2", 20, 2_000),
720 entry("node-3", 30, 3_000),
721 ],
722 None,
723 );
724 let config = config();
725
726 let cases = [
727 (1_000, "node-1", Decision::Challenge),
728 (
729 1_000,
730 "node-2",
731 Decision::Wait {
732 until_millis: 7_000,
733 },
734 ),
735 (7_000, "node-2", Decision::Challenge),
736 (
737 1_000,
738 "node-3",
739 Decision::Wait {
740 until_millis: 13_000,
741 },
742 ),
743 (13_000, "node-3", Decision::Challenge),
744 ];
745
746 for (now_millis, name, expected) in cases {
747 check!(
748 evaluate(&state, &member(name), now_millis, &config) == expected,
749 "{name} at {now_millis}"
750 );
751 }
752 }
753
754 #[test]
755 fn a_recovered_member_defers_to_the_member_that_replaced_it() {
756 let role = role();
757 let config = config();
758 let records = vec![
759 registration(10, &role, "node-1", 0),
760 registration(20, &role, "node-2", 0),
761 lease_record(30, &role, "node-1", 0, 30_000),
762 deregistration(40, &role, "node-1"),
764 lease_record(50, &role, "node-2", 30_000, 60_000),
765 registration(60, &role, "node-1", 31_000),
766 ];
767
768 let state = RoleState::from_records(role, records);
769
770 check!(state.holder() == Some(&member("node-2")));
771 check!(state.rank_of(&member("node-1")) == Some(0));
772 assert!(let Some(recovered) = state.entry(&member("node-1")));
775 check!(recovered.offset == 60);
776 check!(
777 evaluate(&state, &member("node-1"), 31_000, &config)
778 == Decision::Wait {
779 until_millis: 60_000
780 }
781 );
782 check!(evaluate(&state, &member("node-2"), 31_000, &config) == Decision::Hold);
783 }
784
785 #[test]
786 fn a_third_member_that_rejoins_ranks_behind_the_other_standby() {
787 let role = role();
788 let config = config();
789 let records = vec![
790 registration(10, &role, "node-1", 0),
791 registration(20, &role, "node-2", 0),
792 registration(30, &role, "node-3", 0),
793 lease_record(40, &role, "node-1", 0, 30_000),
794 deregistration(50, &role, "node-2"),
795 registration(60, &role, "node-2", 5_000),
796 ];
797
798 let state = RoleState::from_records(role, records);
799
800 check!(state.rank_of(&member("node-3")) == Some(0));
802 check!(state.rank_of(&member("node-2")) == Some(1));
803 check!(evaluate(&state, &member("node-3"), 30_000, &config) == Decision::Challenge);
804 check!(
805 evaluate(&state, &member("node-2"), 30_000, &config)
806 == Decision::Wait {
807 until_millis: 35_000
808 }
809 );
810 }
811
812 #[test]
813 fn a_lone_member_with_no_lease_challenges_at_once() {
814 let state = state_with(vec![entry("node-1", 10, 1_000)], None);
815
816 check!(evaluate(&state, &member("node-1"), 1_000, &config()) == Decision::Challenge);
817 check!(evaluate(&state, &member("node-1"), 9_000, &config()) == Decision::Challenge);
818 }
819
820 #[test]
821 fn an_empty_role_state_has_no_holder_and_no_ranks() {
822 let state = RoleState::default();
823
824 check!(state.holder() == None);
825 check!(state.rank_of(&member("node-1")) == None);
826 check!(evaluate(&state, &member("node-1"), 0, &config()) == Decision::NotRegistered);
827 }
828}