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>;