Skip to main content

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}