recon_protocols/eventual_leader_detector.rs
1//! Ω — an eventual leader detector.
2//!
3//! **Status: implementation. Space: bounded by membership.**
4//!
5//! Cachin, Guerraoui & Rodrigues, Module 2.9 and Algorithm 2.8 ("Monarchical Eventual Leader
6//! Detection"), quoted from the book:
7//!
8//! ```text
9//! Algorithm 2.8: Monarchical Eventual Leader Detection
10//! Implements: EventualLeaderDetector, instance Ω.
11//! Uses: EventuallyPerfectFailureDetector, instance ◇P.
12//!
13//! upon event ⟨ Ω, Init ⟩ do
14//! suspected := ∅;
15//! leader := ⊥;
16//!
17//! upon event ⟨ ◇P, Suspect | p ⟩ do
18//! suspected := suspected ∪ {p};
19//!
20//! upon event ⟨ ◇P, Restore | p ⟩ do
21//! suspected := suspected \ {p};
22//!
23//! upon leader ≠ maxrank(Π \ suspected) do
24//! leader := maxrank(Π \ suspected);
25//! trigger ⟨ Ω, Trust | leader ⟩;
26//! ```
27//!
28//! # The detector beneath is a parameter, and defaults to `◇P`
29//!
30//! Algorithm 2.8 says `Uses: EventuallyPerfectFailureDetector`, and
31//! [`crate::eventually_perfect_failure_detector`] is what that names. `D` defaults to it, so the
32//! plain [`EventualLeaderDetector`] is the algorithm as written.
33//!
34//! It can also be composed over [`crate::perfect_failure_detector`], which is **strictly stronger**:
35//! it never suspects a correct process and never retracts, so `suspected` only grows and the
36//! `Restore` arm is unreachable. That composition is named in [`crate::stacks`] and was this
37//! module's only form until `◇P` existed. The difference is not academic in the fail-recovery model:
38//! under `P` a suspicion is permanent, `maxrank` only ever walks *downward* through the membership,
39//! and a process that crashed and recovered can never lead again.
40//!
41//! **An Ω that is never wrong is a trap, and naming it is the point.** It makes every test of the
42//! layers above vacuous: Paxos exists to stay safe while the leader detector lies, and over an
43//! honest detector that property is untestable. So the suites that matter withdraw the detector's
44//! accuracy — the timing assumption both detectors rest on is what they remove — and this module is
45//! then wrong in exactly the way Ω is allowed to be. `tests/eventual_leader_detector.rs` checks that
46//! it *can* disagree before anything is built on it.
47//!
48//! # `maxrank`, and what rank means here
49//!
50//! The book leaves `rank` as any fixed injective map from processes to integers. Here it is the
51//! [`NodeId`] ordering, and `maxrank` takes the greatest — so leadership passes downward through the
52//! membership as processes are suspected, and two processes with the same suspicions always agree.
53//! Which direction it runs does not matter for correctness; that it is a *function of the suspected
54//! set alone* does, and that is what the suite pins.
55//!
56//! ```text
57//! ELD1 [eventual] Eventual accuracy — eventually every correct process trusts the same correct
58//! process. Before then it may trust a crashed process, or disagree.
59//! ```
60//!
61//! `ELD1` inherits its condition from the detector beneath: over `◇P` it holds while `◇P2` does, and
62//! `◇P2` is itself conditional on the delay cap and on the network settling. The chain is stated at
63//! each link rather than collapsed into one unqualified claim.
64
65use recon_core::{Child, NodeId, ProtoCx, Protocol, TimerId};
66use std::collections::BTreeSet;
67
68use crate::detector::{DetectorInd, VolatileDetector};
69use crate::eventually_perfect_failure_detector::{self as dp, EventuallyPerfectFailureDetector};
70
71/// Requests from the layer above.
72///
73/// Uninhabited: detection begins at initialisation and there is nothing to ask for, exactly as
74/// [`crate::perfect_failure_detector`] has it.
75pub type Cmd = core::convert::Infallible;
76
77/// Indications to the layer above.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum Ind {
80 /// `⟨ Ω, Trust | leader ⟩` — this process now trusts `leader`.
81 ///
82 /// Raised when the trusted process *changes*, and not otherwise. A layer above uses it to start
83 /// an epoch, and an epoch costs an abort, so repeating an unchanged answer would be pure loss.
84 Trust { leader: NodeId },
85}
86
87/// Trust the highest-ranked process not currently suspected.
88#[derive(Debug)]
89pub struct EventualLeaderDetector<D: VolatileDetector = EventuallyPerfectFailureDetector> {
90 /// Π — every process, including this one.
91 peers: BTreeSet<NodeId>,
92 /// `suspected`. Grows on a suspicion and shrinks on its withdrawal — over `P`, which raises no
93 /// withdrawal, it only grows.
94 suspected: BTreeSet<NodeId>,
95 /// `leader`. `None` is the book's `⊥`, before anything has been trusted.
96 leader: Option<NodeId>,
97 detector: Child<D>,
98}
99
100impl EventualLeaderDetector<EventuallyPerfectFailureDetector> {
101 /// Ω among `peers`, over the eventually perfect detector Algorithm 2.8 names.
102 ///
103 /// `detect_after` is the silence a peer is allowed *to begin with*: `◇P` adapts it, and will
104 /// suspect correct processes while it is below what the network actually needs — which is a
105 /// fault this module faithfully passes on, and which the suites above deliberately provoke.
106 pub fn new(
107 me: NodeId,
108 peers: impl IntoIterator<Item = NodeId>,
109 heartbeat: core::time::Duration,
110 detect_after: core::time::Duration,
111 ) -> Self {
112 let config = dp::Config::new(heartbeat, detect_after, detect_after * 20);
113 Self::with_detector(me, peers, |me, all| {
114 EventuallyPerfectFailureDetector::new(me, all, config)
115 })
116 }
117}
118
119impl<D: VolatileDetector> EventualLeaderDetector<D> {
120 /// Ω among `peers`, over whatever detector `build` supplies.
121 pub fn with_detector(
122 me: NodeId,
123 peers: impl IntoIterator<Item = NodeId>,
124 build: impl FnOnce(NodeId, BTreeSet<NodeId>) -> D,
125 ) -> Self {
126 let mut peers: BTreeSet<NodeId> = peers.into_iter().collect();
127 peers.insert(me);
128 let detector = build(me, peers.clone());
129 EventualLeaderDetector {
130 peers,
131 suspected: BTreeSet::new(),
132 leader: None,
133 detector: Child::new(detector),
134 }
135 }
136
137 /// Who this process currently trusts, if anyone.
138 pub fn leader(&self) -> Option<NodeId> {
139 self.leader
140 }
141
142 /// The processes currently suspected by the detector beneath, as this layer has recorded them.
143 pub fn suspected(&self) -> impl Iterator<Item = NodeId> + '_ {
144 self.suspected.iter().copied()
145 }
146
147 /// `maxrank(Π \ suspected)` — the greatest [`NodeId`] not suspected.
148 ///
149 /// A pure function of the suspected set, which is what makes two processes with the same
150 /// suspicions agree without exchanging anything.
151 fn maxrank(&self) -> Option<NodeId> {
152 self.peers.iter().rev().find(|p| !self.suspected.contains(p)).copied()
153 }
154
155 /// `upon leader ≠ maxrank(Π \ suspected)` — a standing condition, re-evaluated after every
156 /// change to `suspected`.
157 fn reconsider(&mut self, cx: &mut ProtoCx<'_, Self>) {
158 let candidate = self.maxrank();
159 if candidate != self.leader
160 && let Some(leader) = candidate
161 {
162 self.leader = candidate;
163 cx.indicate(Ind::Trust { leader });
164 }
165 }
166
167 /// Run the detector, then act on what it reported.
168 fn through_detector(
169 &mut self,
170 cx: &mut ProtoCx<'_, Self>,
171 f: impl FnOnce(&mut D, &mut ProtoCx<'_, D>),
172 ) {
173 let mut inds = self.detector.run(cx, core::convert::identity, f);
174 let changed = !inds.is_empty();
175 for ind in inds.drain(..) {
176 match D::classify(ind) {
177 // `upon event ⟨ ◇P, Suspect | p ⟩`
178 DetectorInd::Suspect { node } => {
179 self.suspected.insert(node);
180 }
181 // `upon event ⟨ ◇P, Restore | p ⟩`. Over a detector that never retracts this is
182 // unreachable rather than merely unused — see `crate::detector`.
183 DetectorInd::Restore { node } => {
184 self.suspected.remove(&node);
185 }
186 }
187 }
188 self.detector.reclaim(inds);
189 if changed {
190 self.reconsider(cx);
191 }
192 }
193}
194
195impl<D: VolatileDetector> Protocol for EventualLeaderDetector<D> {
196 type Cmd = Cmd;
197 type Ind = Ind;
198 type Msg = D::Msg;
199 /// No scope conditions of its own.
200 type Scope = core::convert::Infallible;
201 type Note = crate::Note;
202 /// Keeps nothing durably: a restarted process suspects nobody and trusts afresh.
203 type Meta = core::convert::Infallible;
204 type Entry = core::convert::Infallible;
205
206 fn on_cmd(&mut self, cmd: Cmd, _: &mut ProtoCx<'_, Self>) {
207 match cmd {}
208 }
209
210 fn on_msg(&mut self, from: NodeId, msg: D::Msg, cx: &mut ProtoCx<'_, Self>) {
211 self.through_detector(cx, |d, ccx| d.on_msg(from, msg, ccx));
212 }
213
214 fn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>) {
215 self.through_detector(cx, |d, ccx| d.on_timer(id, ccx));
216 }
217
218 /// `upon event ⟨ Ω, Init ⟩` — and immediately a first `Trust`, since with nobody suspected
219 /// `maxrank(Π)` is already defined and the standing condition already holds.
220 fn on_init(&mut self, cx: &mut ProtoCx<'_, Self>) {
221 self.through_detector(cx, |d, ccx| d.on_init(ccx));
222 self.reconsider(cx);
223 }
224}