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}