Skip to main content

recon_protocols/
best_effort_broadcast.rs

1//! Best-effort broadcast.
2//!
3//! Cachin, Guerraoui & Rodrigues, Module 3.1 and Algorithm 3.1 ("Basic Broadcast").
4//!
5//! **Status: deployable. Space: bounded by membership.** This layer holds only the process set;
6//! everything else belongs to the link beneath it.
7//!
8//! Sends the message individually to every process over perfect links. If the sender is
9//! correct, every correct process delivers it. If the sender crashes partway through, some
10//! processes may deliver and others may not — that is the guarantee this abstraction
11//! deliberately does not make, and the reason the stronger broadcasts exist.
12//!
13//! ```text
14//! upon event ⟨ beb, Broadcast | m ⟩ do
15//!     forall q ∈ Π do
16//!         trigger ⟨ pl, Send | q, m ⟩;
17//!
18//! upon event ⟨ pl, Deliver | p, m ⟩ do
19//!     trigger ⟨ beb, Deliver | p, m ⟩;
20//! ```
21//!
22//! Π includes the sender, so a process broadcasts to itself the same way it broadcasts to
23//! everyone else. Self-delivery is not a special case.
24//!
25//! This layer adds nothing to the wire: its message type is the perfect link's, unchanged, and
26//! its indication handler is pure forwarding. It is the second of the three protocols in this
27//! stack to contribute no header of its own.
28//!
29//! # Over a link that reports scope boundaries
30//!
31//! `L` is a parameter, so this one module is both the perfect-link broadcast above and what
32//! `session_best_effort_broadcast` used to be. The algorithm is unchanged either way — Algorithm
33//! 3.1 does not mention links — but what it can promise is not.
34//!
35//! Over a perfect link, a message sent to a correct process arrives; the link retransmits until it
36//! does. Over a session link it may not: a session can end with the message in flight, and that
37//! link does not retry. So validity holds only while the sessions carrying a broadcast hold.
38//!
39//! This layer cannot repair that. It keeps nothing but the process set — no copy of what it sent,
40//! no record of who received — so there is nothing to resend from, and giving it one would be
41//! state growing with messages, which `docs/bounded-space.md` forbids. What it can do is refuse to
42//! conceal it: both boundary reports are passed upward, because they are the only signal the
43//! layers above have, and one of them — uniform reliable broadcast — can act on what this layer
44//! cannot.
45//!
46//! ```text
47//! BEB1 [session]  Best-effort validity   — [always] over a link that cannot end
48//! BEB2 [always]   No duplication
49//! BEB3 [always]   No creation
50//! ```
51//!
52//! The scope annotation is the link's, not this layer's: over a link whose guarantees never lapse,
53//! `[session]` is vacuous and BEB1 reads as the book states it.
54//!
55//! One request is not in Module 3.1. [`Cmd::SendTo`] sends to a single member of Π — same wire
56//! message, same link, strictly fewer recipients, no new communication step. It exists so a layer
57//! above can answer a scope that has just come back without paying for a fan-out to everyone
58//! else, and it is a narrowing of `Broadcast` rather than an addition to the module.
59
60use core::marker::PhantomData;
61use core::time::Duration;
62use recon_core::{NodeId, ProtoCx, Protocol, TimerId};
63use std::collections::BTreeSet;
64
65use crate::link::{Boundary, Link, LinkInd, VolatileLink};
66use crate::perfect_link::PerfectLink;
67
68/// Requests from the layer above.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum Cmd<P> {
71    Broadcast(P),
72    /// Send to one member only.
73    ///
74    /// Not part of Module 3.1, which has only `Broadcast`. It exists so a layer above can answer a
75    /// scope that has just come back without re-sending to everyone else: same wire message, same
76    /// link, strictly fewer recipients. No new communication step, so no guarantee of Module 3.1
77    /// is affected — this is a narrowing of `Broadcast`, not an addition to it.
78    SendTo {
79        to: NodeId,
80        msg: P,
81    },
82}
83
84/// Indications to the layer above.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum Ind<P> {
87    Deliver {
88        from: NodeId,
89        msg: P,
90    },
91    /// The scope with `peer` ended at `epoch`. A broadcast in flight to it may have been lost, and
92    /// this layer cannot say which — it has no redundancy to bridge with, so it propagates.
93    ///
94    /// Raised only over a link that reports boundaries; over a perfect link this never occurs.
95    /// Declaring it regardless is the price of one implementation serving both, and it is a price
96    /// paid by the layer above rather than by the link, which is what
97    /// `docs/scope-annotated-modules.md` forbids only of the link.
98    SessionEnded {
99        peer: NodeId,
100        epoch: u64,
101    },
102    /// A scope with `peer` is in force at `epoch`. Anything to be resent can be resent now.
103    SessionEstablished {
104        peer: NodeId,
105        epoch: u64,
106    },
107}
108
109/// Translate the link's indication into this layer's — Algorithm 3.1's second handler, plus the
110/// propagation of a boundary this layer cannot bridge.
111fn forward<P, L: Link<P>>(ind: L::Ind) -> Ind<P> {
112    match L::classify(ind) {
113        LinkInd::Deliver { from, msg } => Ind::Deliver { from, msg },
114        LinkInd::Boundary(Boundary::Ended { peer, epoch }) => Ind::SessionEnded { peer, epoch },
115        LinkInd::Boundary(Boundary::Established { peer, epoch }) => {
116            Ind::SessionEstablished { peer, epoch }
117        }
118    }
119}
120
121/// Fan-out to every process over perfect links.
122///
123/// `L` is the link beneath, and it is a parameter rather than a fixed type. What this layer needs
124/// of it is stated in one bound and nowhere else: [`Link`], the port. Anything satisfying it — a
125/// session link, a logged link, or an application's own driver — can carry this broadcast without
126/// either side being edited. That is the seam `docs/conditional-guarantees.md` describes, made
127/// checkable.
128///
129/// Fan-out needs nothing of a scope boundary, so the bound is the port and nothing more. This
130/// layer composes over every link there is, and passes a boundary upward untouched because it has
131/// no redundancy with which to repair one.
132///
133/// It defaults to [`PerfectLink`], so the ordinary stack is still written `BestEffortBroadcast<P>`.
134#[derive(Debug)]
135pub struct BestEffortBroadcast<P, L = PerfectLink<P>> {
136    /// Π — every process in the system, including this one.
137    peers: BTreeSet<NodeId>,
138    link: L,
139    _payload: PhantomData<fn() -> P>,
140}
141
142impl<P, L> BestEffortBroadcast<P, L> {
143    /// Broadcast among `peers`, which must include `me`, over a link the caller supplies.
144    pub fn with_link(me: NodeId, peers: impl IntoIterator<Item = NodeId>, link: L) -> Self {
145        let mut peers: BTreeSet<NodeId> = peers.into_iter().collect();
146        peers.insert(me);
147        BestEffortBroadcast { peers, link, _payload: PhantomData }
148    }
149
150    /// The processes this broadcasts to, in a stable order.
151    pub fn peers(&self) -> impl Iterator<Item = NodeId> + '_ {
152        self.peers.iter().copied()
153    }
154
155    /// The link beneath, for a caller that has reason to inspect its own.
156    pub fn link(&self) -> &L {
157        &self.link
158    }
159}
160
161impl<P> BestEffortBroadcast<P, PerfectLink<P>> {
162    /// Broadcast among `peers`, which must include `me`, over the book's perfect link.
163    pub fn new(me: NodeId, peers: impl IntoIterator<Item = NodeId>, interval: Duration) -> Self {
164        Self::with_link(me, peers, PerfectLink::new(me, interval))
165    }
166
167    /// How many distinct messages the link below has delivered upward.
168    ///
169    /// Specific to the perfect link, so it lives here rather than on every link.
170    pub fn delivered_count(&self) -> usize {
171        self.link.delivered_count()
172    }
173}
174
175impl<P: Clone, L> Protocol for BestEffortBroadcast<P, L>
176where
177    L: VolatileLink<P>,
178{
179    type Cmd = Cmd<P>;
180    type Ind = Ind<P>;
181    type Msg = L::Msg;
182    /// Whatever the link's guarantees are conditional on, since this layer adds no condition of
183    /// its own and cannot bridge the link's.
184    type Scope = L::Scope;
185    type Note = crate::Note;
186    /// Keeps nothing durably: a crash loses everything this protocol knows.
187    type Meta = core::convert::Infallible;
188    type Entry = core::convert::Infallible;
189
190    fn on_cmd(&mut self, cmd: Cmd<P>, cx: &mut ProtoCx<'_, Self>) {
191        let link = &mut self.link;
192        let peers = &self.peers;
193        cx.with_child(core::convert::identity, forward::<P, L>, |ccx| match cmd {
194            // Algorithm 3.1: `forall q in Π do trigger <pl, Send | q, m>`. Π includes the sender,
195            // so a correct process delivers its own broadcast.
196            Cmd::Broadcast(msg) => {
197                for &q in peers {
198                    link.on_cmd(L::send(q, msg.clone()), ccx);
199                }
200            }
201            // The same send, to one member of Π rather than all of it.
202            Cmd::SendTo { to, msg } => {
203                debug_assert!(peers.contains(&to), "a directed send addresses a member of Π");
204                link.on_cmd(L::send(to, msg), ccx);
205            }
206        });
207    }
208
209    fn on_msg(&mut self, from: NodeId, msg: L::Msg, cx: &mut ProtoCx<'_, Self>) {
210        let link = &mut self.link;
211        cx.with_child(core::convert::identity, forward::<P, L>, |ccx| link.on_msg(from, msg, ccx));
212    }
213
214    fn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>) {
215        let link = &mut self.link;
216        cx.with_child(core::convert::identity, forward::<P, L>, |ccx| link.on_timer(id, ccx));
217    }
218
219    /// Hand the scope ending down to the link, which is the layer that knows what it means.
220    ///
221    /// `Scope` is the link's, so leaving this to the trait's default would take a scope event the
222    /// driver raised and drop it — the layer above would never learn its guarantees had lapsed,
223    /// and neither would the link. That is the failure `docs/conditional-guarantees.md` calls
224    /// cardinal, and the default is silent about committing it, which is why this handler exists
225    /// even though its body only forwards.
226    fn on_scope_event(&mut self, scope: L::Scope, cx: &mut ProtoCx<'_, Self>) {
227        let link = &mut self.link;
228        cx.with_child(core::convert::identity, forward::<P, L>, |ccx| {
229            link.on_scope_event(scope, ccx)
230        });
231    }
232}