Skip to main content

recon_sim/
trace.rs

1//! The record of what a run actually did.
2//!
3//! Properties are asserted over this rather than over protocol internals: a trace says what
4//! was sent, what arrived, what was lost, and what each protocol claimed to deliver, which is
5//! exactly the vocabulary the guarantees are written in.
6
7use recon_core::{NodeId, Protocol, Time, TimerId, WriteKind};
8
9/// Names one operation given to a process, so a caller can find in the trace the thing it just
10/// asked for.
11///
12/// Minted by the run, like [`recon_core::TimerId`], and for the same reason: one source per run
13/// means two operations cannot share an identity. Unlike a timer handle it never reaches a
14/// protocol — commands are unchanged, and nothing above the simulator sees one.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct OpId(pub u64);
17
18impl core::fmt::Display for OpId {
19    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20        write!(f, "op{}", self.0)
21    }
22}
23
24/// Why an operation never reached the process it was given to.
25///
26/// Carried rather than flattened away, because the next thing built on this has to tell these
27/// apart: an operation refused by a stall certainly did not happen, where one lost to a crash may
28/// have been half-done by the incarnation that died.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum NotBegun {
31    /// The process had crashed and not restarted. Its volatile state went with it.
32    Crashed,
33    /// The process was stalled. A command is a call from the layer above, on that process, so a
34    /// stalled process's layer above is stalled with it — there was nothing to make the call.
35    Stalled,
36    /// There is no such process in this run.
37    NotAProcess,
38}
39
40/// Why a message never arrived.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum DropReason {
43    /// The network lost it — ordinary fair-loss behaviour.
44    Lost,
45    /// Sender and recipient were in different partitions.
46    Partitioned,
47    /// The recipient had crashed.
48    RecipientCrashed,
49    /// The session carrying it ended before it arrived.
50    SessionEnded,
51    /// Sent in the instant a session ended, before its successor could open. A transport does
52    /// not reopen a connection in the instant it closed one; the next instant's send does.
53    NoSession,
54    /// The sender had crashed before the message left.
55    SenderCrashed,
56}
57
58/// One thing that happened, in order.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum TraceEvent<M, I, N, C> {
61    /// A protocol asked for a message to be transmitted.
62    Sent { at: Time, from: NodeId, to: NodeId, msg: M },
63    /// A message was handed to the recipient protocol.
64    Delivered { at: Time, from: NodeId, to: NodeId, msg: M },
65    /// A message a process addressed to **itself**, handed over without the network.
66    ///
67    /// Not a [`TraceEvent::Sent`], because it crosses no wire: a driver is what turns a request to
68    /// send into a packet, and a packet addressed to the process it came from is a hand-off between
69    /// two roles in one state machine. Counting it among what a run put on the network overstates
70    /// what a deployment costs, and giving it a delivery bound makes every phase of a co-located
71    /// protocol slower than it is.
72    ///
73    /// Recorded, and not merely elided, because ceasing to be a network message must not mean
74    /// ceasing to be observable. A leader learning of a higher ballot from its own acceptor is a
75    /// refusal like any other, and a suite that could not see it lost a non-vacuity floor two
76    /// registered safety tests depend on — which is what an earlier draft, eliding it inside the
77    /// protocol, actually did.
78    HandedToSelf { at: Time, node: NodeId, msg: M },
79    /// A message was not delivered.
80    Dropped { at: Time, from: NodeId, to: NodeId, msg: M, reason: DropReason },
81    /// The network scheduled a second copy of a message.
82    Duplicated { at: Time, from: NodeId, to: NodeId, msg: M },
83    /// A message was selected for extreme delay.
84    Reordered { at: Time, from: NodeId, to: NodeId, msg: M },
85    /// A timer previously set by a protocol fired.
86    TimerFired { at: Time, node: NodeId, id: TimerId },
87    /// A protocol delivered on its guarantee to the layer above.
88    Indicated { at: Time, node: NodeId, ind: I },
89    /// A session was established between two processes.
90    SessionOpened { at: Time, a: NodeId, b: NodeId, epoch: u64 },
91    /// A session ended. Anything still in flight may have been discarded.
92    SessionEnded { at: Time, a: NodeId, b: NodeId, epoch: u64, reason: DropReason },
93    /// A message discarded because the session carrying it ended.
94    SuffixLost { at: Time, from: NodeId, to: NodeId, msg: M },
95    /// A process crashed, losing its volatile state.
96    Crashed { at: Time, node: NodeId },
97    /// A process was suspended, keeping its state.
98    Suspended { at: Time, node: NodeId },
99    /// A suspended process resumed, and everything held for it was dispatched.
100    Resumed { at: Time, node: NodeId },
101    /// A crashed process restarted, and took its startup branch.
102    Restarted { at: Time, node: NodeId },
103    /// A durable write. `kind` distinguishes rewriting metadata from appending, so a claim about
104    /// a protocol's write cost can be checked rather than asserted.
105    Wrote { at: Time, node: NodeId, kind: WriteKind },
106    /// A process died inside a write. Whether that write landed is decided by the seed and is
107    /// deliberately not recorded: the point of the fault is that nobody knows until the recovered
108    /// process reads its storage back.
109    DiedWriting { at: Time, node: NodeId },
110    /// A process was given an operation, and handled it.
111    ///
112    /// Recorded when the handler ran, not when the command was scheduled. A handler's effects
113    /// cannot precede the handler, so this is a valid left-hand end of the interval containing the
114    /// operation's effect, and a tighter one than the moment the caller asked — which matters,
115    /// because a suite that schedules several commands at one instant would otherwise show them all
116    /// overlapping each other.
117    Invoked { at: Time, node: NodeId, op: OpId, cmd: C },
118    /// A process was given an operation and never handled it.
119    ///
120    /// Recorded rather than discarded silently: an operation asked for and never begun is not the
121    /// same as one never asked for, and a record that cannot tell them apart is one a checker would
122    /// reason from falsely.
123    NotInvoked { at: Time, node: NodeId, op: OpId, cmd: C, why: NotBegun },
124    /// A process narrated a decision it took.
125    ///
126    /// The one event here that is not something that *happened to* a process. It is in the same
127    /// account, on the same clock, precisely so that a claim can be read against the run — a
128    /// process saying it refused an announcement is a process from which no acceptance followed,
129    /// and a test can require that rather than trust it.
130    ///
131    /// Recorded **before** the writes and effects of the handler that narrated it: a note marks the
132    /// decision, and the write and the sends are what the decision led to.
133    Said { at: Time, node: NodeId, note: N },
134    /// A restarted process was given back what it had written. `had_state` is false when it had
135    /// written nothing and started as if for the first time.
136    Recovered { at: Time, node: NodeId, had_state: bool },
137}
138
139impl<M, I, N, C> TraceEvent<M, I, N, C> {
140    pub fn at(&self) -> Time {
141        match self {
142            TraceEvent::Sent { at, .. }
143            | TraceEvent::HandedToSelf { at, .. }
144            | TraceEvent::Delivered { at, .. }
145            | TraceEvent::Dropped { at, .. }
146            | TraceEvent::Duplicated { at, .. }
147            | TraceEvent::Reordered { at, .. }
148            | TraceEvent::TimerFired { at, .. }
149            | TraceEvent::Indicated { at, .. }
150            | TraceEvent::SessionOpened { at, .. }
151            | TraceEvent::SessionEnded { at, .. }
152            | TraceEvent::SuffixLost { at, .. }
153            | TraceEvent::Crashed { at, .. }
154            | TraceEvent::Suspended { at, .. }
155            | TraceEvent::Resumed { at, .. }
156            | TraceEvent::Restarted { at, .. }
157            | TraceEvent::Wrote { at, .. }
158            | TraceEvent::DiedWriting { at, .. }
159            | TraceEvent::Said { at, .. }
160            | TraceEvent::Invoked { at, .. }
161            | TraceEvent::NotInvoked { at, .. }
162            | TraceEvent::Recovered { at, .. } => *at,
163        }
164    }
165}
166
167/// An ordered log of everything a run did.
168#[derive(Debug, Clone)]
169pub struct Trace<M, I, N, C> {
170    events: Vec<TraceEvent<M, I, N, C>>,
171}
172
173impl<M, I, N, C> Default for Trace<M, I, N, C> {
174    fn default() -> Self {
175        Trace { events: Vec::new() }
176    }
177}
178
179impl<M, I, N, C> Trace<M, I, N, C> {
180    pub(crate) fn push(&mut self, e: TraceEvent<M, I, N, C>) {
181        self.events.push(e);
182    }
183
184    pub fn events(&self) -> &[TraceEvent<M, I, N, C>] {
185        &self.events
186    }
187
188    /// Every operation that was handled, in order: which process, its identity, and the command.
189    ///
190    /// The left-hand ends of the intervals a checker needs. Pairing them with the indications that
191    /// completed them is not something the trace does — see the `simulation` capability.
192    pub fn invocations(&self) -> impl Iterator<Item = (NodeId, OpId, &C)> {
193        self.events.iter().filter_map(|e| match e {
194            TraceEvent::Invoked { node, op, cmd, .. } => Some((*node, *op, cmd)),
195            _ => None,
196        })
197    }
198
199    /// When `op` was handled, if it was.
200    pub fn invoked_at(&self, op: OpId) -> Option<Time> {
201        self.events.iter().find_map(|e| match e {
202            TraceEvent::Invoked { at, op: o, .. } if *o == op => Some(*at),
203            _ => None,
204        })
205    }
206
207    /// Every operation that never reached the process it was given to, and why.
208    pub fn not_begun(&self) -> impl Iterator<Item = (NodeId, OpId, NotBegun)> {
209        self.events.iter().filter_map(|e| match e {
210            TraceEvent::NotInvoked { node, op, why, .. } => Some((*node, *op, *why)),
211            _ => None,
212        })
213    }
214
215    /// Why `op` never began, if it did not. `None` covers both "it began" and "no such operation",
216    /// which [`Trace::invocations`] distinguishes.
217    pub fn why_not_begun(&self, op: OpId) -> Option<NotBegun> {
218        self.not_begun().find(|(_, o, _)| *o == op).map(|(_, _, why)| why)
219    }
220
221    /// Every decision narrated, in order, with the process that narrated it.
222    ///
223    /// Empty unless the run was asked to record them — see `Sim::record_notes`.
224    pub fn notes(&self) -> impl Iterator<Item = (NodeId, &N)> {
225        self.events.iter().filter_map(|e| match e {
226            TraceEvent::Said { node, note, .. } => Some((*node, note)),
227            _ => None,
228        })
229    }
230
231    /// Every decision narrated by `node`, in order.
232    pub fn notes_at(&self, node: NodeId) -> impl Iterator<Item = &N> {
233        self.notes().filter(move |(n, _)| *n == node).map(|(_, x)| x)
234    }
235
236    pub fn len(&self) -> usize {
237        self.events.len()
238    }
239
240    pub fn is_empty(&self) -> bool {
241        self.events.is_empty()
242    }
243
244    /// Every indication raised, in order, with the process that raised it.
245    pub fn indications(&self) -> impl Iterator<Item = (NodeId, &I)> {
246        self.events.iter().filter_map(|e| match e {
247            TraceEvent::Indicated { node, ind, .. } => Some((*node, ind)),
248            _ => None,
249        })
250    }
251
252    /// Every indication raised by `node`, in order.
253    pub fn indications_at(&self, node: NodeId) -> impl Iterator<Item = &I> {
254        self.indications().filter(move |(n, _)| *n == node).map(|(_, i)| i)
255    }
256
257    /// Every message actually handed to a recipient.
258    pub fn deliveries(&self) -> impl Iterator<Item = (NodeId, NodeId, &M)> {
259        self.events.iter().filter_map(|e| match e {
260            TraceEvent::Delivered { from, to, msg, .. } => Some((*from, *to, msg)),
261            _ => None,
262        })
263    }
264
265    /// Every message a protocol asked to transmit **over the network**.
266    ///
267    /// A message a process addressed to itself is not among these: it crosses no wire, so it is not
268    /// something the run put on the network, and a cost counted from here is a cost a deployment
269    /// would pay. [`ProtoTrace::exchanges`] is the one to ask when what matters is that the message
270    /// happened rather than where it went.
271    pub fn sends(&self) -> impl Iterator<Item = (NodeId, NodeId, &M)> {
272        self.events.iter().filter_map(|e| match e {
273            TraceEvent::Sent { from, to, msg, .. } => Some((*from, *to, msg)),
274            _ => None,
275        })
276    }
277
278    /// Every message a process handed to itself, without the network.
279    pub fn handed_to_self(&self) -> impl Iterator<Item = (NodeId, &M)> {
280        self.events.iter().filter_map(|e| match e {
281            TraceEvent::HandedToSelf { node, msg, .. } => Some((*node, msg)),
282            _ => None,
283        })
284    }
285
286    /// Every message a protocol asked to transmit, wherever it went — the network's and the
287    /// hand-offs together, in the order they happened.
288    ///
289    /// This is what to ask when the question is whether an exchange *happened*: whether a ballot
290    /// was refused, whether a majority answered, whether two processes talked at all. A protocol
291    /// whose roles are co-located answers itself as readily as it answers a peer, and a reader that
292    /// saw only [`ProtoTrace::sends`] would conclude the exchange never took place. That is not
293    /// hypothetical: it emptied a non-vacuity floor two registered safety tests depend on.
294    pub fn exchanges(&self) -> impl Iterator<Item = (NodeId, NodeId, &M)> {
295        self.events.iter().filter_map(|e| match e {
296            TraceEvent::Sent { from, to, msg, .. } => Some((*from, *to, msg)),
297            TraceEvent::HandedToSelf { node, msg, .. } => Some((*node, *node, msg)),
298            _ => None,
299        })
300    }
301
302    /// How many messages were dropped, for any reason.
303    pub fn drops(&self) -> usize {
304        self.events.iter().filter(|e| matches!(e, TraceEvent::Dropped { .. })).count()
305    }
306
307    /// How many messages were dropped for a specific reason.
308    pub fn drops_because(&self, reason: DropReason) -> usize {
309        self.events
310            .iter()
311            .filter(|e| matches!(e, TraceEvent::Dropped { reason: r, .. } if *r == reason))
312            .count()
313    }
314
315    pub fn duplicates(&self) -> usize {
316        self.events.iter().filter(|e| matches!(e, TraceEvent::Duplicated { .. })).count()
317    }
318
319    pub fn reorderings(&self) -> usize {
320        self.events.iter().filter(|e| matches!(e, TraceEvent::Reordered { .. })).count()
321    }
322
323    /// How many sessions ended during the run.
324    pub fn session_ends(&self) -> usize {
325        self.events.iter().filter(|e| matches!(e, TraceEvent::SessionEnded { .. })).count()
326    }
327
328    /// How many messages were discarded because a session carrying them ended.
329    pub fn suffix_losses(&self) -> usize {
330        self.events.iter().filter(|e| matches!(e, TraceEvent::SuffixLost { .. })).count()
331    }
332
333    /// The epochs at which sessions were established, in order.
334    pub fn session_epochs(&self) -> impl Iterator<Item = (NodeId, NodeId, u64)> + '_ {
335        self.events.iter().filter_map(|e| match e {
336            TraceEvent::SessionOpened { a, b, epoch, .. } => Some((*a, *b, *epoch)),
337            _ => None,
338        })
339    }
340
341    /// How many entries were appended.
342    pub fn appends(&self) -> usize {
343        self.events
344            .iter()
345            .filter(|e| matches!(e, TraceEvent::Wrote { kind: WriteKind::Append, .. }))
346            .count()
347    }
348
349    /// How many times the metadata was replaced.
350    pub fn metadata_writes(&self) -> usize {
351        self.events
352            .iter()
353            .filter(|e| matches!(e, TraceEvent::Wrote { kind: WriteKind::Set, .. }))
354            .count()
355    }
356
357    /// How many writes happened, of either kind.
358    pub fn writes(&self) -> usize {
359        self.events.iter().filter(|e| matches!(e, TraceEvent::Wrote { .. })).count()
360    }
361
362    /// How many times a process died inside a write.
363    ///
364    /// Not how many writes were *lost*: the seed decides that, and the trace does not say, which
365    /// is the whole content of the fault.
366    pub fn deaths_in_writes(&self) -> usize {
367        self.events.iter().filter(|e| matches!(e, TraceEvent::DiedWriting { .. })).count()
368    }
369
370    /// How many restarts recovered durable state, as opposed to starting afresh.
371    pub fn recoveries_with_state(&self) -> usize {
372        self.events
373            .iter()
374            .filter(|e| matches!(e, TraceEvent::Recovered { had_state: true, .. }))
375            .count()
376    }
377
378    pub fn timer_fires(&self) -> usize {
379        self.events.iter().filter(|e| matches!(e, TraceEvent::TimerFired { .. })).count()
380    }
381
382    pub fn delivery_count(&self) -> usize {
383        self.events.iter().filter(|e| matches!(e, TraceEvent::Delivered { .. })).count()
384    }
385
386    /// How many messages the run put on the **network**. A hand-off to oneself is not among them;
387    /// see [`ProtoTrace::sends`].
388    pub fn send_count(&self) -> usize {
389        self.events.iter().filter(|e| matches!(e, TraceEvent::Sent { .. })).count()
390    }
391
392    /// How many messages a protocol asked to transmit, wherever they went — the counterpart of
393    /// [`ProtoTrace::exchanges`].
394    pub fn exchange_count(&self) -> usize {
395        self.events
396            .iter()
397            .filter(|e| matches!(e, TraceEvent::Sent { .. } | TraceEvent::HandedToSelf { .. }))
398            .count()
399    }
400
401    pub fn indication_count(&self) -> usize {
402        self.events.iter().filter(|e| matches!(e, TraceEvent::Indicated { .. })).count()
403    }
404}
405
406/// The trace type for a given protocol.
407///
408/// A trace names all four of a protocol's outward vocabularies — what it was asked, what crossed
409/// the wire, what it concluded, what it said — so writing them out is four associated types every
410/// time. The same shape as `recon_core::ProtoCx`, and for the same reason.
411pub type ProtoTrace<P> =
412    Trace<<P as Protocol>::Msg, <P as Protocol>::Ind, <P as Protocol>::Note, <P as Protocol>::Cmd>;
413
414/// One event of a given protocol's trace.
415pub type ProtoTraceEvent<P> = TraceEvent<
416    <P as Protocol>::Msg,
417    <P as Protocol>::Ind,
418    <P as Protocol>::Note,
419    <P as Protocol>::Cmd,
420>;