recon_protocols/session_link.rs
1//! A link whose guarantees come from an underlying session.
2//!
3//! **Status: deployable. Space: bounded by membership.**
4//!
5//! This is what would run over TCP or QUIC. It does not retransmit and it does not deduplicate,
6//! because within a session the transport does neither — it delivers reliably and in order, or
7//! the session ends. So this link holds one epoch per peer and nothing per message, which makes
8//! it the first in this repository to satisfy the rule in `docs/bounded-space.md`.
9//!
10//! Compare the perfect link, which obtains the same guarantees from a stubborn link by
11//! retransmitting for ever and remembering every identifier it has seen. That is how you build a
12//! perfect link when you have nothing underneath — the simulator's situation, not a deployment's.
13//! The deployable link needs *less* state, not more.
14//!
15//! # What it will not pretend
16//!
17//! A session ends and an unknown suffix of what was in flight is gone. The perfect link has no
18//! way to express that and would carry on as if nothing happened; the previous attempt at this
19//! project did exactly that, and `docs/postmortem.md` records what it cost. Here the ending is a
20//! scope, reported upward as an indication naming the peer and the new epoch, so the layer above
21//! must decide what its own guarantee does about it.
22//!
23//! ```text
24//! SL1 [session(q)] Reliable ordered delivery: while a session with q holds, every message sent
25//! to q is delivered, in order, exactly once.
26//! SL2 [always] No creation: a message is delivered only if it was previously sent.
27//! ```
28//!
29//! In the notation of `docs/scope-annotated-modules.md`, SL1's scope is well-formed: the session's
30//! end is an event this link is told about, so it can react to it, report it, and be tested
31//! against it.
32
33use recon_core::{NodeId, ProtoCx, Protocol, SessionEvent, TimerId};
34use std::collections::BTreeMap;
35
36/// What crosses the wire: the payload, unchanged.
37///
38/// The session supplies ordering and reliability, so this layer adds no header at all — no
39/// sequence number, no identifier. It is the thinnest link in the repository.
40pub type Wire<P> = P;
41
42/// Requests from the layer above.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum Cmd<P> {
45 Send { to: NodeId, msg: P },
46}
47
48/// Indications to the layer above.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum Ind<P> {
51 /// A message arrived. Ordered with respect to others from the same peer in the same session.
52 Deliver { from: NodeId, msg: P },
53 /// The session with `peer` ended at `epoch`. Anything sent to that peer and not yet delivered
54 /// may have been lost, and this link cannot say which. Nothing can be done about it yet.
55 SessionEnded { peer: NodeId, epoch: u64 },
56 /// A session with `peer` is in force at `epoch`. This is the moment on which anything that
57 /// must be resent can be.
58 SessionEstablished { peer: NodeId, epoch: u64 },
59}
60
61/// Reliable ordered delivery within a session, and honesty across one.
62#[derive(Debug, Default)]
63pub struct SessionLink<P> {
64 /// The epoch **currently in force** for each peer: inserted on an establishment and removed
65 /// on the matching ending. One entry per peer with a live session, and nothing that grows
66 /// with messages.
67 ///
68 /// Removing is the point. Keeping the number after the ending would report a dead session as
69 /// current, which is the opposite of what this layer exists to say.
70 epochs: BTreeMap<NodeId, u64>,
71 _payload: core::marker::PhantomData<P>,
72}
73
74impl<P> SessionLink<P> {
75 pub fn new() -> Self {
76 SessionLink { epochs: BTreeMap::new(), _payload: core::marker::PhantomData }
77 }
78
79 /// The epoch in force with `peer`, or `None` when there is no session with it.
80 pub fn epoch(&self, peer: NodeId) -> Option<u64> {
81 self.epochs.get(&peer).copied()
82 }
83
84 /// How many peers this link currently holds a session with. Its entire footprint.
85 pub fn tracked_peers(&self) -> usize {
86 self.epochs.len()
87 }
88}
89
90impl<P: Clone> Protocol for SessionLink<P> {
91 type Cmd = Cmd<P>;
92 type Ind = Ind<P>;
93 type Msg = Wire<P>;
94 type Scope = SessionEvent;
95 type Note = crate::Note;
96 /// Keeps nothing durably: a crash loses everything this protocol knows.
97 type Meta = core::convert::Infallible;
98 type Entry = core::convert::Infallible;
99
100 fn on_cmd(&mut self, Cmd::Send { to, msg }: Cmd<P>, cx: &mut ProtoCx<'_, Self>) {
101 // No sequence number, no retransmission buffer, no record kept. The session is
102 // responsible for getting it there or for telling us it could not.
103 cx.send(to, msg);
104 }
105
106 fn on_msg(&mut self, from: NodeId, msg: Wire<P>, cx: &mut ProtoCx<'_, Self>) {
107 // No deduplication: within a session the transport does not duplicate.
108 cx.indicate(Ind::Deliver { from, msg });
109 }
110
111 fn on_timer(&mut self, _id: TimerId, _cx: &mut ProtoCx<'_, Self>) {
112 // Registers none, and has no child to pass one to.
113 }
114
115 fn on_scope_event(&mut self, event: SessionEvent, cx: &mut ProtoCx<'_, Self>) {
116 // The one thing this link exists to do that a perfect link cannot: say so. Both events
117 // are reported — the ending because a suffix may be gone, the establishment because it is
118 // the only moment on which anything can be resent.
119 match event {
120 SessionEvent::Ended { peer, epoch } => {
121 // The session is gone, so the epoch is not current any more. Leaving it would
122 // have `epoch()` report a dead session as live.
123 self.epochs.remove(&peer);
124 cx.indicate(Ind::SessionEnded { peer, epoch });
125 }
126 SessionEvent::Established { peer, epoch } => {
127 self.epochs.insert(peer, epoch);
128 cx.indicate(Ind::SessionEstablished { peer, epoch });
129 }
130 }
131 }
132}
133
134/// The session link satisfies the link port, and reports scope boundaries through it.
135///
136/// It classifies its two boundary indications as boundaries, which is a claim that it can observe
137/// them — and it can: the simulator raises a session ending and an establishment, and this link is
138/// where they enter the stack. That is what a layer above needs in order to repair a lost suffix,
139/// and what the perfect link cannot offer.
140impl<P> crate::link::Link<P> for SessionLink<P>
141where
142 P: Clone,
143{
144 fn send(to: NodeId, msg: P) -> Cmd<P> {
145 Cmd::Send { to, msg }
146 }
147
148 fn classify(ind: Ind<P>) -> crate::link::LinkInd<P> {
149 use crate::link::{Boundary, LinkInd};
150 match ind {
151 Ind::Deliver { from, msg } => LinkInd::Deliver { from, msg },
152 Ind::SessionEnded { peer, epoch } => LinkInd::Boundary(Boundary::Ended { peer, epoch }),
153 Ind::SessionEstablished { peer, epoch } => {
154 LinkInd::Boundary(Boundary::Established { peer, epoch })
155 }
156 }
157 }
158}