recon_protocols/note.rs
1//! What a protocol says about a decision it took.
2//!
3//! The simulator's trace records what happened *to* a protocol — what it sent, what was delivered,
4//! what it wrote. It cannot record what the protocol **decided**, and it is completely silent about
5//! a decision whose outcome was to do nothing. That is the case that has cost this project the most:
6//! a leader trusted by everyone that announced nothing left no trace event at all, and finding it
7//! meant bisecting the whole stack by hand.
8//!
9//! # A note earns its place only where the trace cannot say it
10//!
11//! The rule, and it is the one that keeps narration from decaying. A note beside
12//! `cx.indicate(Ind::StartEpoch { ts, leader })` restating the same thing adds nothing a reader
13//! could not already see, and the two can now disagree — which is exactly how the docstring that
14//! quoted a deadlocking resend clause its own comment said a test had replaced went wrong.
15//!
16//! Three things qualify:
17//!
18//! - a decision that produced no effect at all — a message refused, a candidate already passed;
19//! - *why* an effect was produced, where the effect alone is ambiguous — `epoch_change` sends the
20//! same `NACK` on the wire whether it is refusing an announcement or volunteering how far it has
21//! reached, and a reader of the trace cannot tell those apart;
22//! - nothing else, yet. The vocabulary grows one narrated module at a time.
23//!
24//! # One vocabulary per run
25//!
26//! A note's type belongs to the run rather than to a layer, exactly as a `TimerId` does: a composed
27//! stack narrates in one vocabulary, and a note passes through composition untouched, with no
28//! mapper and nothing for a parent to re-wrap. A parent letting a child's note through is not
29//! endorsing it — the decision was the child's.
30//!
31//! That is why every protocol in this crate declares `type Note = Note`, including the twenty-five
32//! that narrate nothing. The alternative — each layer naming its own vocabulary and parents
33//! converting — was built and discarded: it put a `where` clause relating two type parameters on
34//! every layer generic over a port, to express something no layer actually wanted to do.
35
36use recon_core::NodeId;
37
38/// A decision a protocol took, in the vocabulary this stack narrates in.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Note {
41 /// An announcement of a new epoch was refused. The trace holds the refusal that went back on
42 /// the wire; it does not hold what was wrong with the announcement.
43 EpochRefused { from: NodeId, ts: u64, why: Refusal },
44 /// A peer's report of how far it had reached was not acted on. **Nothing at all reaches the
45 /// trace from this decision** — it is the shape of silence that cost the most to diagnose.
46 ReportIgnored { from: NodeId, nts: u64, why: Refusal },
47 /// This process volunteered how far it had reached to a leader that may never have been told it
48 /// had become one. The same `NACK` as a refusal goes on the wire, so the trace cannot tell the
49 /// two decisions apart; this says which it was.
50 ReachReported { leader: NodeId, nts: u64 },
51 /// A leader was preempted by a higher ballot and did **not** take up another, because Ω no
52 /// longer trusts it. **Nothing at all reaches the trace from this decision** — a leader that
53 /// stops competing sends no message, sets no timer and raises no indication, so a run in which
54 /// it correctly stood down and one in which it was never told apart look identical.
55 LeadershipYielded { to: NodeId, round: u64 },
56 /// A proposal was dropped because this leader already had one for the slot — Figure 7's
57 /// `if ∄c' : ⟨s, c'⟩ ∈ proposals`, which is what keeps at most one commander per slot. The
58 /// decision produces no effect whatever.
59 ProposalIgnored { slot: u64 },
60 /// A replica held requests it was not allowed to propose for, because `slot_in` had reached
61 /// `slot_out + WINDOW` — Invariant R5. **Nothing at all reaches the trace from this decision**:
62 /// a replica whose window is full sends nothing and looks exactly like one with nothing to do,
63 /// which is the shape the fourth liveness violation wears.
64 WindowFull { slot_in: u64, slot_out: u64 },
65 /// A proposal outstanding past its threshold was proposed **again for the same slot** — the
66 /// replica's half of Liu et al.'s fourth liveness fix. Where the leader is on this machine the
67 /// proposal is a function call, so the repeat can reach the trace as nothing whatever; this
68 /// says it happened, and the trace cannot tell it from a first proposal in any case.
69 SlotReproposed { slot: u64 },
70 /// A replica's own command lost its slot to somebody else's and went back into `requests` —
71 /// Figure 1's `if c'' ≠ c' then requests := requests ∪ {c''}`. The decision produces no effect
72 /// until the command is proposed again at a later slot, and a replica that dropped it instead
73 /// would lose an append in silence.
74 ProposalDisplaced { slot: u64 },
75 /// State for every slot below `slot` was discarded, because enough replicas have applied it —
76 /// the source's §4.2. **Nothing at all reaches the trace from this decision**: collecting sends
77 /// no message, sets no timer and raises no indication, so a run that collects and one that has
78 /// stopped collecting look identical from outside. Which matters, because stopping is the
79 /// specified behaviour when too few replicas remain to report.
80 CollectedBelow { slot: u64 },
81 /// A replica asked a peer for the decisions from `slot` onwards, because the consensus beneath
82 /// it has collected them and can no longer answer. **Nothing at all reaches the trace from the
83 /// decision itself**, and the request that follows is indistinguishable on the wire from any
84 /// other; this says why it was made.
85 CaughtUpFrom { slot: u64 },
86}
87
88/// Why something was refused or ignored.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum Refusal {
91 /// The sender is not the process this one currently trusts.
92 NotTrusted { trusted: NodeId },
93 /// The timestamp offered is not above what this process has already reached.
94 NotAhead { reached: u64 },
95 /// This process does not trust itself, so acting on the report is not its business.
96 NotLeader { trusted: NodeId },
97}