Skip to main content

recon_protocols/
perfect_link.rs

1//! Perfect point-to-point links.
2//!
3//! Cachin, Guerraoui & Rodrigues, Module 2.3 and Algorithm 2.2 ("Eliminate Duplicates").
4//!
5//! **Status: academic as written. Space: unbounded.** Within a TCP or QUIC session, PL1–PL3 come
6//! from the transport. The deployable equivalent is a *session link*: those guarantees from the
7//! session, plus an event when the session changes and an unknown suffix may have been lost. It
8//! needs less state than this, not more — within a session the transport does not duplicate, so
9//! there is nothing to deduplicate.
10//!
11//! `delivered` here grows with every message received. See `docs/bounded-space.md`.
12//!
13//! Built over the stubborn link, which delivers a message infinitely often. This layer keeps
14//! the first copy of each message and discards the rest, yielding reliable delivery with no
15//! duplication and no creation.
16//!
17//! ```text
18//! upon event ⟨ pl, Send | q, m ⟩ do
19//!     trigger ⟨ sl, Send | q, m ⟩;
20//!
21//! upon event ⟨ sl, Deliver | p, m ⟩ do
22//!     if m ∉ delivered then
23//!         delivered := delivered ∪ {m};
24//!         trigger ⟨ pl, Deliver | p, m ⟩;
25//! ```
26//!
27//! **One deliberate departure.** The book deduplicates on the message *content*, `m`, which
28//! silently assumes every message is distinct. Send the same bytes twice on purpose and the
29//! second is swallowed. This implementation tags each transmission with an identifier — the
30//! sender and a sequence number — and deduplicates on that instead, so a genuine resend of
31//! identical content is delivered twice, as the layer above expects.
32//!
33//! That identifier is the only thing this stack puts on the wire: the stubborn link below adds
34//! nothing, and best-effort broadcast above adds nothing.
35//!
36//! # The counter lives exactly as long as the set it keys
37//!
38//! Both are volatile, and a crash takes both, so the pairing holds: a restarted sender re-mints
39//! `(me, 1)` at exactly the point where every recipient's `delivered` has also been forgotten,
40//! and the recipient re-delivers the old messages anyway —
41//! `no_duplication_does_not_survive_the_recipient_restarting` records that. PL2 is scoped to an
42//! incarnation here, which is the crash-stop model's premise: a crashed process is not correct,
43//! and nothing is promised across the restart the simulator can nevertheless perform.
44//!
45//! The hazard to watch for is the mismatched pair — a durable set keyed by a volatile counter,
46//! where the recipient remembers what the sender has forgotten and discards new messages as
47//! duplicates. [`crate::logged_link`] is that configuration, and makes the counter durable.
48
49use core::time::Duration;
50use recon_core::{NodeId, ProtoCx, Protocol, TimerId};
51use serde::{Deserialize, Serialize};
52use std::collections::BTreeSet;
53
54use crate::stubborn_link::{self as sl, StubbornLink};
55
56/// Names one transmission uniquely across the system.
57///
58/// The sender plus a per-sender sequence number. Deduplicating on this rather than on message
59/// content is what lets identical payloads be sent twice and delivered twice.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
61pub struct MsgId {
62    pub src: NodeId,
63    pub seq: u64,
64}
65
66/// What crosses the wire: the identifier, and the payload it belongs to.
67///
68/// The single header in the three-layer stack.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct Wire<P> {
71    pub id: MsgId,
72    pub payload: P,
73}
74
75/// Requests from the layer above.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum Cmd<P> {
78    Send { to: NodeId, msg: P },
79}
80
81/// Indications to the layer above.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum Ind<P> {
84    Deliver { from: NodeId, msg: P },
85}
86
87/// Reliable delivery, exactly once, over a stubborn link.
88#[derive(Debug)]
89pub struct PerfectLink<P> {
90    me: NodeId,
91    seq: u64,
92    delivered: BTreeSet<MsgId>,
93    stubborn: StubbornLink<Wire<P>>,
94    /// Indications the child raised during a handler, awaiting this protocol's attention.
95    /// Reused across events.
96    inbox: Vec<sl::Ind<Wire<P>>>,
97}
98
99impl<P> PerfectLink<P> {
100    /// A perfect link for process `me`, retransmitting every `interval` underneath.
101    pub fn new(me: NodeId, interval: Duration) -> Self {
102        PerfectLink {
103            me,
104            seq: 0,
105            delivered: BTreeSet::new(),
106            stubborn: StubbornLink::new(interval),
107            inbox: Vec::new(),
108        }
109    }
110
111    /// How many distinct messages have been delivered upward.
112    pub fn delivered_count(&self) -> usize {
113        self.delivered.len()
114    }
115
116    /// How many transmissions the layer below is still retrying.
117    pub fn outstanding(&self) -> usize {
118        self.stubborn.outstanding()
119    }
120}
121
122impl<P: Clone> PerfectLink<P> {
123    /// Run the child, then apply what it reported: keep the first copy of each identifier and
124    /// drop the rest.
125    ///
126    /// Every handler is this, differing only in which child method it calls. Collecting the
127    /// ceremony here rather than repeating it three times is what keeps the handlers readable —
128    /// and it leaves the borrow structure visible in one place instead of hiding it in a macro.
129    fn with_stubborn(
130        &mut self,
131        cx: &mut ProtoCx<'_, Self>,
132        f: impl FnOnce(&mut StubbornLink<Wire<P>>, &mut ProtoCx<'_, StubbornLink<Wire<P>>>),
133    ) {
134        let mut inbox = core::mem::take(&mut self.inbox);
135        {
136            let stubborn = &mut self.stubborn;
137            cx.with_child_consuming(core::convert::identity, &mut inbox, |ccx| f(stubborn, ccx));
138        }
139        for ind in inbox.drain(..) {
140            let sl::Ind::Deliver { from, msg: Wire { id, payload } } = ind;
141            if self.delivered.insert(id) {
142                cx.indicate(Ind::Deliver { from, msg: payload });
143            }
144        }
145        self.inbox = inbox;
146    }
147}
148
149impl<P: Clone> Protocol for PerfectLink<P> {
150    type Cmd = Cmd<P>;
151    type Ind = Ind<P>;
152    type Msg = Wire<P>;
153    /// No scope conditions: this protocol's guarantees do not lapse.
154    type Scope = core::convert::Infallible;
155    type Note = crate::Note;
156    /// Keeps nothing durably: a crash loses everything this protocol knows.
157    type Meta = core::convert::Infallible;
158    type Entry = core::convert::Infallible;
159
160    fn on_cmd(&mut self, Cmd::Send { to, msg }: Cmd<P>, cx: &mut ProtoCx<'_, Self>) {
161        self.seq += 1;
162        let id = MsgId { src: self.me, seq: self.seq };
163        let wire = Wire { id, payload: msg };
164        self.with_stubborn(cx, |sl, ccx| {
165            sl.on_cmd(sl::Cmd::Send { id: sl::SendId(id.seq), to, msg: wire }, ccx)
166        });
167    }
168
169    fn on_msg(&mut self, from: NodeId, msg: Wire<P>, cx: &mut ProtoCx<'_, Self>) {
170        self.with_stubborn(cx, |sl, ccx| sl.on_msg(from, msg, ccx));
171    }
172
173    fn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>) {
174        self.with_stubborn(cx, |sl, ccx| sl.on_timer(id, ccx));
175    }
176}
177
178/// The perfect link satisfies the link port, and reports no scope boundary.
179///
180/// [`Link::classify`](crate::link::Link::classify) here never yields
181/// [`LinkInd::Boundary`](crate::link::LinkInd::Boundary): PL2's no-duplication holds within one
182/// incarnation of the recipient, and this link has no means of observing that incarnation ending. A
183/// link that reported a boundary it cannot see would be asserting something it does not know, which
184/// `docs/scope-annotated-modules.md` forbids by Definition 2a.
185///
186/// **Nothing in the type system enforces that**, and [`crate::link`] records why: a `ScopedLink`
187/// marker trait existed for exactly this and was deleted for want of a consumer — the one layer
188/// that should have bounded on it could not, because its resend lives in the `Link` impl and the
189/// tighter bound would have fallen on every link. What keeps it honest instead is the
190/// classification itself, and `tests/link_port.rs` pins both halves: that this link's
191/// classification never yields a boundary, and that the session link's yields one for each variant
192/// that reports it.
193impl<P> crate::link::Link<P> for PerfectLink<P>
194where
195    P: Clone,
196{
197    fn send(to: NodeId, msg: P) -> Cmd<P> {
198        Cmd::Send { to, msg }
199    }
200
201    fn classify(Ind::Deliver { from, msg }: Ind<P>) -> crate::link::LinkInd<P> {
202        crate::link::LinkInd::Deliver { from, msg }
203    }
204}