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}