Skip to main content

layover_core/
stall.rs

1//! A chain that stopped and will not start again on its own.
2//!
3//! # Why this is written down rather than inferred
4//!
5//! Every other failure in the system leaves a mark somebody can point at: a failed run, a timeout,
6//! a help request. A stall leaves none. Every run in the chain *succeeded* — the tester reported,
7//! the reviewer reported — and then the publisher never woke, because the barrier it was waiting
8//! behind could no longer be completed.
9//!
10//! Read back from history, that chain is indistinguishable from one that finished. The runs all
11//! say `succeeded` and nothing says the work never happened. So the moment the Tower gives up on
12//! a rendezvous, it says so in a place that outlives the process, and the dashboard reads it.
13//!
14//! That is the whole reason this type exists: silent permanent stalling is the worst outcome in
15//! this system, and the only thing worse than a factory that stops is one that stops quietly.
16
17use jiff::Timestamp;
18use serde::{Deserialize, Serialize};
19
20use crate::agent::AgentName;
21use crate::flight::ItineraryId;
22
23/// A chain the Tower has given up on, and why.
24#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
25pub struct Stall {
26    /// The chain that will not continue.
27    pub itinerary: ItineraryId,
28    /// The agent that was waiting.
29    pub agent: AgentName,
30    /// Upstreams that never arrived and now never can.
31    pub missing: Vec<AgentName>,
32    /// How many flights were being held.
33    ///
34    /// Work somebody asked for that will not happen. A count rather than the flights themselves,
35    /// because the point is to prompt a person to look, not to replay it.
36    pub stranded: usize,
37    /// When the Tower gave up.
38    pub at: Timestamp,
39}
40
41impl Stall {
42    /// Records a chain that can no longer continue.
43    #[must_use]
44    pub fn new(
45        itinerary: ItineraryId,
46        agent: AgentName,
47        missing: Vec<AgentName>,
48        stranded: usize,
49        at: Timestamp,
50    ) -> Self {
51        Self {
52            itinerary,
53            agent,
54            missing,
55            stranded,
56            at,
57        }
58    }
59
60    /// One line saying what went wrong, written for a person reading a dashboard.
61    #[must_use]
62    pub fn summary(&self) -> String {
63        let missing = self
64            .missing
65            .iter()
66            .map(ToString::to_string)
67            .collect::<Vec<_>>()
68            .join(", ");
69
70        format!(
71            "`{}` never woke: nothing live could still deliver {missing}",
72            self.agent
73        )
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn stall(missing: &[&str]) -> Stall {
82        Stall::new(
83            ItineraryId::generate(),
84            AgentName::new("publisher"),
85            missing.iter().map(|n| AgentName::new(*n)).collect(),
86            1,
87            Timestamp::now(),
88        )
89    }
90
91    #[test]
92    fn a_stall_names_the_agent_that_never_woke() {
93        // The dashboard shows this next to a chain whose runs all say `succeeded`, so it has to
94        // say what did *not* happen rather than what did.
95        let summary = stall(&["reviewer"]).summary();
96
97        assert!(summary.contains("publisher"), "{summary}");
98        assert!(summary.contains("never woke"), "{summary}");
99        assert!(summary.contains("reviewer"), "{summary}");
100    }
101
102    #[test]
103    fn every_missing_upstream_is_named() {
104        let summary = stall(&["reviewer", "tester"]).summary();
105
106        assert!(summary.contains("reviewer"), "{summary}");
107        assert!(summary.contains("tester"), "{summary}");
108    }
109
110    #[test]
111    fn a_stall_survives_a_round_trip_through_json() {
112        // It is written to disk precisely because it has to outlive the process that noticed it.
113        let original = stall(&["reviewer"]);
114        let text = serde_json::to_string(&original).expect("serialises");
115        let read: Stall = serde_json::from_str(&text).expect("deserialises");
116
117        assert_eq!(read, original);
118    }
119}