recon_protocols/fair_loss_link.rs
1//! Fair-loss point-to-point links — the bottom of the stack, and the weakest link there is.
2//!
3//! **Status: implementation. Space: none.** It keeps nothing at all, which is what makes it
4//! trivially bounded and what makes everything above it responsible for its own redundancy.
5//!
6//! Cachin, Guerraoui & Rodrigues, Module 2.1 ("FairLossPointToPointLinks"):
7//!
8//! ```text
9//! FLL1 Fair-loss: If a correct process p infinitely often sends a message m to a correct
10//! process q, then q delivers m an infinite number of times.
11//! FLL2 Finite duplication: If a correct process p sends a message m a finite number of times
12//! to q, then m cannot be delivered an infinite number of times by q.
13//! FLL3 No creation: If some process q delivers a message m with sender p, then m was
14//! previously sent to q by process p.
15//! ```
16//!
17//! # Why this module is nearly empty
18//!
19//! `recon-sim` **is** the fair-loss network — that is `CLAUDE.md`'s description of it and the whole
20//! of constraint 3. Loss, duplication and reordering are its knobs. So a fair-loss link in this
21//! codebase has nothing to add: it hands a send to the network and reports what arrives. FLL1
22//! through FLL3 are the simulator's properties, and this module's job is to be the port through
23//! which a layer above reaches them without naming the simulator.
24//!
25//! That is not a degenerate case, it is the point. Every other link here is defined by what it adds
26//! on top of this: the stubborn link adds retransmission, the perfect link adds deduplication over
27//! that, the session link adds ordering within a scope and honesty across one.
28//!
29//! # What it is for
30//!
31//! Algorithm 3.9 — eager probabilistic broadcast — says `Uses: FairLossPointToPointLinks`, and it
32//! means it. Gossip exists to tolerate loss; running it over a perfect link, which retransmits until
33//! delivery, masks the behaviour it is built to provide and leaves its probabilistic guarantee
34//! unobservable. It also never falls silent, because the stubborn link beneath the perfect one
35//! re-sends everything it has ever sent on every tick, so "a broadcast generates finitely many
36//! transmissions" cannot be measured there either.
37//!
38//! Before this module existed, the only thing in the tree with this shape was a link written for a
39//! test — the stand-in for an application's own transport in `tests/foreign_link.rs`. That it was
40//! needed twice, once as a demonstration and once as the bottom of the book's own stack, is what
41//! made it a module.
42
43use recon_core::{NodeId, ProtoCx, Protocol, TimerId};
44
45use crate::link::{Link, LinkInd};
46
47/// Requests from the layer above.
48///
49/// Deliberately the same shape as every other link's: one variant, send this to that peer. A layer
50/// above reaches it through [`Link::send`] and never names this type.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum Cmd<P> {
53 Send { to: NodeId, msg: P },
54}
55
56/// Indications to the layer above.
57///
58/// One variant, and no scope boundary among them: this link has no session to end and no
59/// incarnation it can observe, so it declares none. `docs/scope-annotated-modules.md` forbids a
60/// module naming a scope it cannot observe, and this one can observe nothing at all.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum Ind<P> {
63 Deliver { from: NodeId, msg: P },
64}
65
66/// The link the book's stack starts from: send it, and hope.
67///
68/// No retransmission, no deduplication, no timer, no state. What arrives, arrives.
69#[derive(Debug, Default)]
70pub struct FairLossLink<P>(core::marker::PhantomData<fn() -> P>);
71
72impl<P> FairLossLink<P> {
73 pub fn new() -> Self {
74 FairLossLink(core::marker::PhantomData)
75 }
76}
77
78impl<P: Clone> Protocol for FairLossLink<P> {
79 type Cmd = Cmd<P>;
80 type Ind = Ind<P>;
81 type Msg = P;
82 /// No scope conditions. FLL1 to FLL3 are stated over correct processes and hold as long as one
83 /// is correct; there is no session to end and nothing this link could report about one.
84 type Scope = core::convert::Infallible;
85 type Note = crate::Note;
86 /// Keeps nothing durably, because it keeps nothing at all.
87 type Meta = core::convert::Infallible;
88 type Entry = core::convert::Infallible;
89
90 fn on_cmd(&mut self, Cmd::Send { to, msg }: Cmd<P>, cx: &mut ProtoCx<'_, Self>) {
91 cx.send(to, msg);
92 }
93
94 fn on_msg(&mut self, from: NodeId, msg: P, cx: &mut ProtoCx<'_, Self>) {
95 cx.indicate(Ind::Deliver { from, msg });
96 }
97
98 fn on_timer(&mut self, _id: TimerId, _cx: &mut ProtoCx<'_, Self>) {
99 // Registers none, and has no child to pass one to.
100 }
101}
102
103/// The fair-loss link satisfies the link port, and only its unscoped half.
104///
105/// It does not report scope boundaries because it cannot observe any — it holds no session, no
106/// epoch and no state whatever. A layer that repairs a scope ending gets nothing from this link,
107/// which is the honest outcome rather than a boundary invented to satisfy a bound.
108impl<P: Clone> Link<P> for FairLossLink<P> {
109 fn send(to: NodeId, msg: P) -> Cmd<P> {
110 Cmd::Send { to, msg }
111 }
112
113 fn classify(Ind::Deliver { from, msg }: Ind<P>) -> LinkInd<P> {
114 LinkInd::Deliver { from, msg }
115 }
116}