pub trait Protocol {
type Cmd;
type Ind;
type Msg;
type Meta;
type Entry;
type Scope;
type Note;
// Required methods
fn on_cmd(&mut self, cmd: Self::Cmd, cx: &mut ProtoCx<'_, Self>);
fn on_msg(
&mut self,
from: NodeId,
msg: Self::Msg,
cx: &mut ProtoCx<'_, Self>,
);
fn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>);
// Provided methods
fn on_init(&mut self, _cx: &mut ProtoCx<'_, Self>) { ... }
fn on_recovery(&mut self, _cx: &mut ProtoCx<'_, Self>) { ... }
fn on_scope_event(
&mut self,
_scope: Self::Scope,
_cx: &mut ProtoCx<'_, Self>,
) { ... }
}Expand description
A synchronous protocol state machine.
Implementors handle three kinds of event — a command from the layer above, a message from a peer, and a timer fire — and emit effects through the context. Handling an event runs to completion: it cannot await, cannot be suspended, and cannot be cancelled part-way, so a state transition is atomic with respect to whatever drives it.
The four associated types are the protocol’s ports, in Kompics terms: what it accepts from above, what it promises upward, what it puts on the wire, and what it schedules.
Required Associated Types§
Sourcetype Meta
type Meta
The durable value this protocol rewrites: a position, a count, an epoch. Small enough that rewriting it costs nothing.
A protocol keeping nothing durably declares this and Protocol::Entry as
core::convert::Infallible, and then a write cannot be constructed — the same
check-rather-than-trust that Protocol::Scope uses.
Sourcetype Scope
type Scope
Scopes whose boundaries this protocol’s guarantees depend on, and which it can observe.
A guarantee is rarely absolute. It holds while some condition does — a transport session,
a retention window — and the end of that condition is an event the protocol must be told
about, not an implementation detail beneath it. See docs/scope-annotated-modules.md.
Both boundaries travel here, not only the ending. A scope beginning is what makes a bridge possible at all: an ending says a suffix may be gone, and only the beginning of the successor says where to send it again. A port carrying just the ending would name a problem with no event on which to act.
A protocol with no such condition declares core::convert::Infallible. That is not a
convention: an uninhabited type has no values, so a scope event cannot be constructed for
it and Protocol::on_scope_event can never be called. The absence is checked rather than
trusted, and such a protocol writes no handler at all.
A scope may only be named by a protocol that can observe its end. Naming one it cannot detect creates an obligation no implementation can discharge and no test can exercise.
Sourcetype Note
type Note
The vocabulary in which this protocol narrates its decisions.
A record of effects says what a protocol did. It cannot say what the protocol decided, and in particular it is silent about a decision whose outcome was to do nothing — a message refused, a candidate already passed, an announcement not made. Those are the cases that have cost this project the most, because a silence leaves nothing behind to read.
A protocol narrating nothing declares core::convert::Infallible, as with
Protocol::Scope and for the same reason: an uninhabited type has no values, so
Cx::note cannot be called for it. The absence is checked rather than trusted.
A vocabulary belongs to the run, not to a layer, exactly as a TimerId does. A
composed stack narrates in one, and a note passes through composition untouched — no
mapper, no conversion, nothing for a parent to re-wrap. A parent never restates a child’s
decision, because the decision was the child’s; it merely lets it through, which is why
Cx::with_child hands the child the parent’s own note sink the way it hands down the
source of timer identities.
Narration is output-only. Nothing a protocol can observe reveals whether anything received a note, so no behaviour may depend on one and a run is reproducible whether or not it was read.
Required Methods§
Sourcefn on_cmd(&mut self, cmd: Self::Cmd, cx: &mut ProtoCx<'_, Self>)
fn on_cmd(&mut self, cmd: Self::Cmd, cx: &mut ProtoCx<'_, Self>)
Handle a request from the layer above.
Sourcefn on_msg(&mut self, from: NodeId, msg: Self::Msg, cx: &mut ProtoCx<'_, Self>)
fn on_msg(&mut self, from: NodeId, msg: Self::Msg, cx: &mut ProtoCx<'_, Self>)
Handle a message received from from.
Sourcefn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>)
fn on_timer(&mut self, id: TimerId, cx: &mut ProtoCx<'_, Self>)
Handle a timer that fired somewhere in this protocol or in what it composes.
The identity says nothing about which layer registered it, so a protocol that composes children hands the expiry to each of them, and a protocol that registered timers acts only on one it registered. A protocol that registered none does nothing but pass it on. That is the price of a timer’s type not encoding the composition path, and it is a handful of calls deep rather than a type that grows with the stack.
Provided Methods§
Sourcefn on_init(&mut self, _cx: &mut ProtoCx<'_, Self>)
fn on_init(&mut self, _cx: &mut ProtoCx<'_, Self>)
Begin, on a first start — when nothing has been written down.
Exactly one of this and Protocol::on_recovery runs at startup, which is the branch the
book draws with ⟨ Init ⟩ and ⟨ Recovery ⟩. It matters because some first-start work must
not happen on a restart: writing an initial value down is the standard case, and doing it
again on recovery would overwrite what was being recovered.
The constructor cannot serve this purpose. It runs in both cases — it is the common prefix of the two branches, not one of them — and it cannot emit effects, so it has nowhere to put a write. Volatile setup belongs there; anything a first start must do belongs here.
Sourcefn on_recovery(&mut self, _cx: &mut ProtoCx<'_, Self>)
fn on_recovery(&mut self, _cx: &mut ProtoCx<'_, Self>)
Resume after a crash, reading what survived.
Distinct from construction, and deliberately: the algorithms that need it act on recovering — re-announcing what they had already delivered, re-sending what was still pending — and those are effects, which a constructor cannot emit.
What survived is read from Cx::storage rather than handed over, since a protocol may
need only part of it. Exactly one of this and Protocol::on_init runs at startup.
Nothing else is dispatched between being told to recover and this returning, which is what makes it safe to hold state not yet loaded.
Sourcefn on_scope_event(&mut self, _scope: Self::Scope, _cx: &mut ProtoCx<'_, Self>)
fn on_scope_event(&mut self, _scope: Self::Scope, _cx: &mut ProtoCx<'_, Self>)
Handle a boundary of a scope this protocol’s guarantees depend on — its end, or the beginning of the one that succeeds it.
Scope events travel downward, like messages: they originate outside the stack and are routed by each layer to whichever child cares, in the concrete type the bottom layer declares. What travels back up is an indication — a layer that cannot restore its guarantee says so in its own terms.
The default does nothing, which is unreachable for a protocol whose Scope is uninhabited.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".