Skip to main content

recon_protocols/
stubborn_link.rs

1//! Stubborn point-to-point links.
2//!
3//! Cachin, Guerraoui & Rodrigues, Module 2.2 and Algorithm 2.1 ("Retransmit Forever").
4//!
5//! **Status: academic. Space: unbounded — and unboundable.** This is how a perfect link is built
6//! when the only thing underneath is a lossy datagram service — the simulator's situation, and not
7//! a deployment's, where TCP and QUIC retransmit already. It stays because everything above needs a
8//! perfect link and the simulator offers only fair-loss.
9//!
10//! `sent` grows with every transmission, and that is not a defect to be fixed: **it is `SL1`.** The
11//! property below is that a message sent once is delivered *an infinite number of times*, so a link
12//! that let go would not be a bounded stubborn link — it would not be a stubborn link. There is no
13//! bounded form of this abstraction, and the answer to wanting one is [`crate::session_link`], not
14//! a smaller `sent`. See `docs/bounded-space.md`.
15//!
16//! What is true is that nothing retires an entry unless the layer above stops it, and nothing in
17//! this repository does — the perfect link never does, because Algorithm 2.2 never does. Every
18//! entry is therefore re-sent on every tick, and the cost grows with everything ever sent.
19//!
20//! Turns a fair-loss network into one where a message sent between correct processes is
21//! eventually delivered, by retransmitting it at a fixed interval. The cost is unbounded
22//! duplication: the recipient delivers the message infinitely often. Suppressing that is the
23//! perfect link's job, not this one's.
24//!
25//! ```text
26//! upon event ⟨ sl, Init ⟩ do
27//!     sent := ∅;
28//!     starttimer(Δ);
29//!
30//! upon event ⟨ sl, Send | q, m ⟩ do
31//!     trigger ⟨ fll, Send | q, m ⟩;
32//!     sent := sent ∪ {(q, m)};
33//!
34//! upon event ⟨ Timeout ⟩ do
35//!     forall (q, m) ∈ sent do trigger ⟨ fll, Send | q, m ⟩;
36//!     starttimer(Δ);
37//!
38//! upon event ⟨ fll, Deliver | p, m ⟩ do
39//!     trigger ⟨ sl, Deliver | p, m ⟩;
40//! ```
41//!
42//! # Departures from the page
43//!
44//! - The book's `⟨ Init ⟩` starts the timer unconditionally and runs it for ever. This arms it
45//!   lazily instead, when there is something to retransmit; see `arm`. Behaviourally equivalent,
46//!   and it lets a run reach quiescence rather than tick indefinitely.
47//! - Each transmission is named by a [`SendId`] so the layer above can retire it with
48//!   [`Cmd::Stop`]. The book has no such request and never lets go. See [`Cmd::Send`] for the
49//!   precondition that naming brings with it.
50//!
51//!   Conservative, and worth saying why: `SL1` is conditioned on a sender that sends `m` *once*,
52//!   and says nothing about one that retracts. So with no caller the behaviour is exactly the
53//!   book's, and a layer that genuinely knows a transmission is finished may stop it without
54//!   costing a property. Nothing in this repository calls it yet; only this module's own suite does.
55
56use core::time::Duration;
57use recon_core::{NodeId, ProtoCx, Protocol, TimerId};
58use std::collections::BTreeMap;
59
60/// Identifies one stubborn transmission, so it can later be stopped.
61///
62/// The book retransmits forever and never stops, which is `SL1` rather than an oversight. A caller
63/// that knows a transmission is finished may nonetheless let go of it, so each one is named. That
64/// is a retraction, which the property does not speak about — not a bound on the property.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct SendId(pub u64);
67
68/// Requests from the layer above.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum Cmd<M> {
71    /// Transmit `msg` to `to`, and keep transmitting it until stopped.
72    ///
73    /// **`id` must not name a transmission that is still live.** Reusing one replaces the earlier
74    /// transmission outright: it stops being retransmitted, and SL1 lapses for it silently —
75    /// there is no indication, and nothing above is told a promise was withdrawn. The identifier
76    /// is the caller's to allocate, so the uniqueness is the caller's to hold; `debug_assert`
77    /// checks it in debug builds.
78    Send { id: SendId, to: NodeId, msg: M },
79    /// Stop retransmitting the transmission named `id`.
80    Stop { id: SendId },
81}
82
83/// Indications to the layer above.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum Ind<M> {
86    /// A message arrived. May be raised many times for one transmission.
87    Deliver { from: NodeId, msg: M },
88}
89
90/// Retransmits until told to stop.
91///
92/// Adds nothing to the wire: what it transmits is exactly the payload it was given. That is why
93/// a three-layer stack built on it carries only one header.
94#[derive(Debug)]
95pub struct StubbornLink<M> {
96    interval: Duration,
97    sent: BTreeMap<SendId, (NodeId, M)>,
98    /// The retransmission timer outstanding, if any. A handle rather than a flag: an expiry this
99    /// link is no longer waiting on is then recognisable, where `true` said only that something
100    /// was pending.
101    tick: Option<TimerId>,
102}
103
104impl<M> StubbornLink<M> {
105    /// Retransmit everything outstanding every `interval`.
106    pub fn new(interval: Duration) -> Self {
107        StubbornLink { interval, sent: BTreeMap::new(), tick: None }
108    }
109
110    /// How many transmissions are still being retried.
111    pub fn outstanding(&self) -> usize {
112        self.sent.len()
113    }
114
115    /// The retransmission interval.
116    pub fn interval(&self) -> Duration {
117        self.interval
118    }
119}
120
121impl<M: Clone> StubbornLink<M> {
122    /// Arm the retransmission timer if there is anything to retransmit.
123    ///
124    /// The book starts a timer at initialisation and runs it forever. Arming lazily is
125    /// equivalent in behaviour and leaves no timer running when nothing is outstanding — which
126    /// also means a run reaches quiescence instead of ticking indefinitely.
127    fn arm(&mut self, cx: &mut ProtoCx<'_, Self>) {
128        if self.tick.is_none() && !self.sent.is_empty() {
129            self.tick = Some(cx.set_timer(self.interval));
130        }
131    }
132}
133
134impl<M: Clone> Protocol for StubbornLink<M> {
135    type Cmd = Cmd<M>;
136    type Ind = Ind<M>;
137    type Msg = M;
138    /// No scope conditions: this protocol's guarantees do not lapse.
139    type Scope = core::convert::Infallible;
140    type Note = crate::Note;
141    /// Keeps nothing durably: a crash loses everything this protocol knows.
142    type Meta = core::convert::Infallible;
143    type Entry = core::convert::Infallible;
144
145    fn on_cmd(&mut self, cmd: Cmd<M>, cx: &mut ProtoCx<'_, Self>) {
146        match cmd {
147            Cmd::Send { id, to, msg } => {
148                // Reuse would retire the earlier transmission with no indication that its SL1 had
149                // lapsed. See the precondition on `Cmd::Send`.
150                debug_assert!(!self.sent.contains_key(&id), "SendId {id:?} is still live");
151                cx.send(to, msg.clone());
152                self.sent.insert(id, (to, msg));
153                self.arm(cx);
154            }
155            Cmd::Stop { id } => {
156                self.sent.remove(&id);
157            }
158        }
159    }
160
161    fn on_msg(&mut self, from: NodeId, msg: M, cx: &mut ProtoCx<'_, Self>) {
162        // No creation: what is delivered upward is exactly what arrived, attributed to whoever
163        // the network says sent it.
164        cx.indicate(Ind::Deliver { from, msg });
165    }
166
167    fn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>) {
168        // Every layer above hands every expiry down, so most of what arrives here was registered
169        // by somebody else. Only the one this link is waiting on does anything — which also makes
170        // an expiry this link has superseded recognisable rather than merely indistinguishable.
171        if self.tick != Some(id) {
172            return;
173        }
174        for (to, msg) in self.sent.values() {
175            cx.send(*to, msg.clone());
176        }
177        self.tick = None;
178        self.arm(cx);
179    }
180}