recon_protocols/link.rs
1//! The link port: what a layer above the link may depend on, and the whole of what it may.
2//!
3//! `docs/conditional-guarantees.md` states the seam this project is built around: *layers above the
4//! link may depend on its `Cmd` and `Ind` types and nothing else, so a session-aware or logged
5//! implementation can be swapped through*. This module is that sentence made checkable. A layer
6//! above bounds on [`Link`]; an implementation below satisfies it; neither names the other.
7//!
8//! # Why the vocabulary is not pinned to one pair of types
9//!
10//! The obvious port is a bound pinning the types exactly —
11//! `Protocol<Cmd = pl::Cmd<P>, Ind = pl::Ind<P>>`. It works for the perfect link and fails for the
12//! session link, whose `Ind` has three variants rather than one, and admitting both is the entire
13//! point: the four `session_*` broadcast modules exist because a layer written against one pair of
14//! types cannot compose over the other.
15//!
16//! So a link keeps its own `Cmd` and `Ind` — they are its vocabulary, not the port's — and the port
17//! supplies the two translations a layer above actually needs: build a send, and recognise a
18//! delivery. Nothing else about the link is visible.
19//!
20//! # Scope boundaries, and why there is no second trait for them
21//!
22//! A session link reports that a session ended or was established. A perfect link cannot: it has no
23//! means of observing either, and `docs/scope-annotated-modules.md` forbids a module declaring a
24//! scope it cannot observe (Definition 2a, Corollary 8.1). So [`Link::classify`] returns
25//! [`LinkInd::Boundary`] only for a link that can actually see one, and that is the whole of the
26//! mechanism.
27//!
28//! There was a `ScopedLink` marker trait here, so that a layer whose liveness depends on being told
29//! about a re-establishment could bound on it, making it a compile error to compose that layer over
30//! a perfect link. It was deleted, because once the four `session_*` broadcast modules had
31//! collapsed into their base modules — the job it was introduced to do — **nothing bounded on it**.
32//! Uniform reliable broadcast, the one layer that should have, could not: its resend is reached
33//! from the arm handling the child's indications, which lives in the `Link` impl, so the tighter
34//! bound would have fallen on every link including the perfect one.
35//!
36//! What keeps that resend honest is this module's own guarantee rather than a bound: a boundary is
37//! never classified for a link that cannot observe one, so over a perfect link the path is
38//! unreachable rather than merely unused. `tests/link_port.rs` pins both halves — that the perfect
39//! link's classification never yields a boundary, and that the session link's yields one for each
40//! variant that reports it.
41//!
42//! Reintroduce it when a layer genuinely cannot be written without the bound. Until then it is an
43//! abstraction ahead of its consumer, which is what `CLAUDE.md` constraint 4 warns against and what
44//! this change's own `design.md` recorded as a risk against this very trait.
45
46use recon_core::{NodeId, Protocol};
47
48/// What a layer above the link may depend on, and the whole of what it may.
49///
50/// `P` is the payload the layer above sends. A link is free to wrap it — the perfect link adds a
51/// message identifier — which is why [`Link::send`] builds the request rather than the layer above
52/// constructing one, and why [`Link::classify`] takes the payload back out.
53///
54/// Satisfying the port is a decision, not an accident of shape. An earlier draft made it a blanket
55/// impl over every `Protocol` with the right associated types, which meant a protocol became a link
56/// by coincidence; a link now says so. A protocol that has not is rejected when the project is
57/// built:
58///
59/// ```compile_fail
60/// # struct NotALink;
61/// # #[derive(Debug, Clone, PartialEq, Eq)]
62/// # struct Whatever;
63/// # impl recon_core::Protocol for NotALink {
64/// # type Cmd = Whatever;
65/// # type Ind = Whatever;
66/// # type Msg = Whatever;
67/// # type Scope = core::convert::Infallible;
68/// # type Meta = core::convert::Infallible;
69/// # type Entry = core::convert::Infallible;
70/// # fn on_cmd(&mut self, _: Whatever, _: &mut recon_core::ProtoCx<'_, Self>) {}
71/// # fn on_msg(&mut self, _: recon_core::NodeId, _: Whatever,
72/// # _: &mut recon_core::ProtoCx<'_, Self>) {}
73/// # fn on_timer(&mut self, _: recon_core::TimerId,
74/// # _: &mut recon_core::ProtoCx<'_, Self>) {}
75/// # }
76/// fn requires_a_link<L: recon_protocols::link::Link<u32>>() {}
77/// requires_a_link::<NotALink>();
78/// ```
79pub trait Link<P>: Protocol<Note = crate::Note> {
80 /// The request that sends `msg` to `to`.
81 ///
82 /// A constructor rather than a fixed type, because the request is the link's own vocabulary.
83 fn send(to: NodeId, msg: P) -> Self::Cmd;
84
85 /// What this indication means to the layer above.
86 ///
87 /// Total, and deliberately so: a layer above maps its child's indications with one function, so
88 /// a link that could report something unclassifiable would leave that layer with a case it
89 /// could only drop — and silently absorbing a scope end is this project's cardinal sin.
90 fn classify(ind: Self::Ind) -> LinkInd<P>;
91}
92
93/// What an indication from any link amounts to, in the port's own vocabulary.
94///
95/// The layer above matches on this rather than on the link's own indication type, which is how one
96/// implementation of a broadcast serves every link.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum LinkInd<P> {
99 /// A message arrived.
100 Deliver { from: NodeId, msg: P },
101 /// The scope the link's guarantees hold within changed. Only a link that can observe one
102 /// ever reports this.
103 Boundary(Boundary),
104}
105
106/// A boundary of the scope within which a link's guarantees hold.
107///
108/// Named here rather than in any one link, because a layer above reacts to the boundary without
109/// caring which implementation raised it.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum Boundary {
112 /// The scope with `peer` ended at `epoch`. Anything sent to that peer and not yet delivered may
113 /// have been lost, and the link cannot say which.
114 Ended { peer: NodeId, epoch: u64 },
115 /// A scope with `peer` is in force at `epoch`. This is the moment on which anything that must
116 /// be resent can be.
117 Established { peer: NodeId, epoch: u64 },
118}
119
120/// A link that keeps nothing durable *the layer above has to know about*.
121///
122/// Every layer in this crate composes over one, because none of them declares a storage vocabulary
123/// on their child's behalf. It is a conjunction of bounds rather than a capability — hence the
124/// blanket impl, which is the opposite of what [`Link`] does deliberately — and it exists so that
125/// the conjunction is written once instead of at every composing layer.
126///
127/// A logged link keeps a great deal durable and does not satisfy this. Composing a broadcast over
128/// one is not possible today for that reason, and the bound is where the limitation lives, so it is
129/// the one place to revisit when it is wanted.
130pub trait VolatileLink<P>:
131 Link<P, Meta = core::convert::Infallible, Entry = core::convert::Infallible>
132{
133}
134
135impl<P, L> VolatileLink<P> for L where
136 L: Link<P, Meta = core::convert::Infallible, Entry = core::convert::Infallible>
137{
138}