recon_core/protocol.rs
1//! The protocol contract.
2
3use crate::{Cx, Effect, NodeId, Time, TimerId};
4use rand::RngCore;
5
6/// A synchronous protocol state machine.
7///
8/// Implementors handle three kinds of event — a command from the layer above, a message from a
9/// peer, and a timer fire — and emit effects through the context. Handling an event runs to
10/// completion: it cannot await, cannot be suspended, and cannot be cancelled part-way, so a
11/// state transition is atomic with respect to whatever drives it.
12///
13/// The four associated types are the protocol's ports, in Kompics terms: what it accepts from
14/// above, what it promises upward, what it puts on the wire, and what it schedules.
15pub trait Protocol {
16 /// Requests from the layer above.
17 type Cmd;
18 /// Indications to the layer above — this protocol delivering on its guarantee.
19 type Ind;
20 /// What crosses the wire to a peer running the same protocol.
21 type Msg;
22 /// The durable value this protocol rewrites: a position, a count, an epoch. Small enough that
23 /// rewriting it costs nothing.
24 ///
25 /// A protocol keeping nothing durably declares this and [`Protocol::Entry`] as
26 /// [`core::convert::Infallible`], and then a write cannot be constructed — the same
27 /// check-rather-than-trust that [`Protocol::Scope`] uses.
28 type Meta;
29
30 /// The durable entries this protocol appends: what accumulates.
31 type Entry;
32
33 /// Scopes whose boundaries this protocol's guarantees depend on, and which it can observe.
34 ///
35 /// A guarantee is rarely absolute. It holds while some condition does — a transport session,
36 /// a retention window — and the end of that condition is an event the protocol must be told
37 /// about, not an implementation detail beneath it. See `docs/scope-annotated-modules.md`.
38 ///
39 /// **Both** boundaries travel here, not only the ending. A scope beginning is what makes a
40 /// bridge possible at all: an ending says a suffix may be gone, and only the beginning of the
41 /// successor says where to send it again. A port carrying just the ending would name a
42 /// problem with no event on which to act.
43 ///
44 /// A protocol with no such condition declares [`core::convert::Infallible`]. That is not a
45 /// convention: an uninhabited type has no values, so a scope event cannot be constructed for
46 /// it and [`Protocol::on_scope_event`] can never be called. The absence is checked rather than
47 /// trusted, and such a protocol writes no handler at all.
48 ///
49 /// A scope may only be named by a protocol that can observe its end. Naming one it cannot
50 /// detect creates an obligation no implementation can discharge and no test can exercise.
51 type Scope;
52
53 /// The vocabulary in which this protocol narrates its decisions.
54 ///
55 /// A record of effects says what a protocol *did*. It cannot say what the protocol *decided*,
56 /// and in particular it is silent about a decision whose outcome was to do nothing — a message
57 /// refused, a candidate already passed, an announcement not made. Those are the cases that have
58 /// cost this project the most, because a silence leaves nothing behind to read.
59 ///
60 /// A protocol narrating nothing declares [`core::convert::Infallible`], as with
61 /// [`Protocol::Scope`] and for the same reason: an uninhabited type has no values, so
62 /// [`Cx::note`] cannot be called for it. The absence is checked rather than trusted.
63 ///
64 /// **A vocabulary belongs to the run, not to a layer**, exactly as a [`TimerId`] does. A
65 /// composed stack narrates in one, and a note passes through composition untouched — no
66 /// mapper, no conversion, nothing for a parent to re-wrap. A parent never restates a child's
67 /// decision, because the decision was the child's; it merely lets it through, which is why
68 /// `Cx::with_child` hands the child the parent's own note sink the way it hands down the
69 /// source of timer identities.
70 ///
71 /// Narration is output-only. Nothing a protocol can observe reveals whether anything received a
72 /// note, so no behaviour may depend on one and a run is reproducible whether or not it was
73 /// read.
74 type Note;
75
76 /// Handle a request from the layer above.
77 fn on_cmd(&mut self, cmd: Self::Cmd, cx: &mut ProtoCx<'_, Self>);
78
79 /// Handle a message received from `from`.
80 fn on_msg(&mut self, from: NodeId, msg: Self::Msg, cx: &mut ProtoCx<'_, Self>);
81
82 /// Handle a timer that fired somewhere in this protocol or in what it composes.
83 ///
84 /// The identity says nothing about which layer registered it, so a protocol that composes
85 /// children hands the expiry to each of them, and a protocol that registered timers acts only
86 /// on one it registered. A protocol that registered none does nothing but pass it on. That is
87 /// the price of a timer's type not encoding the composition path, and it is a handful of calls
88 /// deep rather than a type that grows with the stack.
89 fn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>);
90
91 /// Begin, on a first start — when nothing has been written down.
92 ///
93 /// Exactly one of this and [`Protocol::on_recovery`] runs at startup, which is the branch the
94 /// book draws with `⟨ Init ⟩` and `⟨ Recovery ⟩`. It matters because some first-start work must
95 /// *not* happen on a restart: writing an initial value down is the standard case, and doing it
96 /// again on recovery would overwrite what was being recovered.
97 ///
98 /// The constructor cannot serve this purpose. It runs in both cases — it is the common prefix
99 /// of the two branches, not one of them — and it cannot emit effects, so it has nowhere to put
100 /// a write. Volatile setup belongs there; anything a first start must *do* belongs here.
101 fn on_init(&mut self, _cx: &mut ProtoCx<'_, Self>) {}
102
103 /// Resume after a crash, reading what survived.
104 ///
105 /// Distinct from construction, and deliberately: the algorithms that need it *act* on
106 /// recovering — re-announcing what they had already delivered, re-sending what was still
107 /// pending — and those are effects, which a constructor cannot emit.
108 ///
109 /// What survived is read from [`Cx::storage`] rather than handed over, since a protocol may
110 /// need only part of it. Exactly one of this and [`Protocol::on_init`] runs at startup.
111 ///
112 /// Nothing else is dispatched between being told to recover and this returning, which is what
113 /// makes it safe to hold state not yet loaded.
114 fn on_recovery(&mut self, _cx: &mut ProtoCx<'_, Self>) {}
115
116 /// Handle a boundary of a scope this protocol's guarantees depend on — its end, or the
117 /// beginning of the one that succeeds it.
118 ///
119 /// Scope events travel *downward*, like messages: they originate outside the stack and are
120 /// routed by each layer to whichever child cares, in the concrete type the bottom layer
121 /// declares. What travels back up is an indication — a layer that cannot restore its
122 /// guarantee says so in its own terms.
123 ///
124 /// The default does nothing, which is unreachable for a protocol whose `Scope` is uninhabited.
125 fn on_scope_event(&mut self, _scope: Self::Scope, _cx: &mut ProtoCx<'_, Self>) {}
126}
127
128/// The context type for a given protocol.
129pub type ProtoCx<'a, P> = Cx<
130 'a,
131 <P as Protocol>::Msg,
132 <P as Protocol>::Ind,
133 <P as Protocol>::Note,
134 <P as Protocol>::Meta,
135 <P as Protocol>::Entry,
136>;
137
138/// The effect type for a given protocol.
139pub type ProtoEffect<P> = Effect<<P as Protocol>::Msg, <P as Protocol>::Ind>;
140
141/// An event a protocol can be given. Used by drivers and by the test helper.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum Event<C, M, S> {
144 Cmd(C),
145 Msg {
146 from: NodeId,
147 msg: M,
148 },
149 /// A timer registered by this protocol, or by something it composes, has fired.
150 Timer(TimerId),
151 /// A scope this protocol's guarantees depended on has ended.
152 ScopeEvent(S),
153 /// This process is starting for the first time, with nothing written down.
154 Init,
155 /// This process restarted, and something it wrote down survived.
156 Recovery,
157}
158
159/// The event type for a given protocol.
160pub type ProtoEvent<P> = Event<<P as Protocol>::Cmd, <P as Protocol>::Msg, <P as Protocol>::Scope>;
161
162/// Deliver one event to `p` and return the effects it emitted.
163///
164/// Restores the ergonomics of a pure function for tests — `assert_eq!(step(..), [..])` — without
165/// making production paths allocate a vector per event. Intended for tests; drivers own a
166/// reusable buffer and call the handlers directly.
167///
168/// **For a protocol driven alone.** This starts the timer identities at zero on every call, so a
169/// composition driven through it hands two layers the same handle and each accepts the other's
170/// expiry as its own — a wrong test that need not fail. Use [`step_with`] for a stack, and
171/// [`step_in`] for a protocol whose writes must survive between calls. The distinction is not
172/// expressible in the type: a composed protocol looks like any other from here.
173pub fn step<P: Protocol + ?Sized>(
174 p: &mut P,
175 event: ProtoEvent<P>,
176 now: Time,
177 rng: &mut dyn RngCore,
178) -> Vec<ProtoEffect<P>> {
179 let mut store = crate::store::MemStore::default();
180 step_in(p, event, now, rng, &mut store)
181}
182
183/// Deliver one event to `p` against a store the caller owns, and return the effects it emitted.
184///
185/// [`step`] gives the protocol a fresh store each call, which is right for one that keeps nothing
186/// durably and wrong for one that does — a write in one call would be invisible in the next. A
187/// test that cares about what survives passes its own store here and can inspect it afterwards.
188pub fn step_in<P: Protocol + ?Sized>(
189 p: &mut P,
190 event: ProtoEvent<P>,
191 now: Time,
192 rng: &mut dyn RngCore,
193 store: &mut dyn crate::store::Store<P::Meta, P::Entry>,
194) -> Vec<ProtoEffect<P>> {
195 let mut next_timer = 0;
196 step_with(p, event, now, rng, store, &mut next_timer)
197}
198
199/// Deliver one event to `p` against a timer identity source the caller owns.
200///
201/// [`step`] and [`step_in`] start identities at zero on every call, which is right for a protocol
202/// driven alone and wrong for a composed one: two layers would each be handed identity zero, and
203/// each would accept the other's expiry as its own. A driver owns one source for a whole run — see
204/// `Sim` — and a test driving a stack by hand must do the same.
205///
206/// Nothing listens for what the protocol narrates. Use [`step_noting`] to read that too.
207pub fn step_with<P: Protocol + ?Sized>(
208 p: &mut P,
209 event: ProtoEvent<P>,
210 now: Time,
211 rng: &mut dyn RngCore,
212 store: &mut dyn crate::store::Store<P::Meta, P::Entry>,
213 next_timer: &mut u64,
214) -> Vec<ProtoEffect<P>> {
215 step_noting(p, event, now, rng, store, next_timer, &mut crate::NoNotes)
216}
217
218/// [`step_with`], collecting what the protocol narrates as well as what it emitted.
219///
220/// The two are returned separately here because a caller stepping a protocol by hand has no trace
221/// to interleave them into. Under the simulator they land in one account, in order, which is what
222/// lets a claim be checked against what happened.
223pub fn step_noting<P: Protocol + ?Sized>(
224 p: &mut P,
225 event: ProtoEvent<P>,
226 now: Time,
227 rng: &mut dyn RngCore,
228 store: &mut dyn crate::store::Store<P::Meta, P::Entry>,
229 next_timer: &mut u64,
230 notes: &mut dyn crate::NoteSink<P::Note>,
231) -> Vec<ProtoEffect<P>> {
232 let mut effects = Vec::new();
233 {
234 let mut cx = Cx::new(&mut effects, now, rng, store, next_timer, notes);
235 match event {
236 Event::Cmd(c) => p.on_cmd(c, &mut cx),
237 Event::Msg { from, msg } => p.on_msg(from, msg, &mut cx),
238 Event::Timer(id) => p.on_timer(id, &mut cx),
239 Event::ScopeEvent(s) => p.on_scope_event(s, &mut cx),
240 Event::Init => p.on_init(&mut cx),
241 Event::Recovery => p.on_recovery(&mut cx),
242 }
243 }
244 effects
245}