Skip to main content

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}