Skip to main content

rightkit_daemon/
shutdown.rs

1//! Ordered shutdown: named phases run in order, each with its own deadline, under a watchdog that
2//! ends the process when a step hangs (rightkit#1122).
3//!
4//! A daemon stops in a fixed order (refuse new work, drain in-flight turns, stop workers, flush
5//! storage). [`OrderedShutdown`] runs the phases the host registered, one after another, each on
6//! its own thread so a hung step can be given up on at its deadline. A failed, panicked or
7//! timed-out phase is recorded and the next phase still runs: a stuck drain must not keep the
8//! storage from flushing. The shutdown is single-flight: the first [`run`](OrderedShutdown::run)
9//! executes the phases, every other call (a signal handler and the window close arriving together)
10//! waits for that run and gets the same [`ShutdownReport`]. The watchdog is a separate thread
11//! started with the run; when the whole shutdown has not finished by its hard deadline it calls
12//! the exit function, by default [`std::process::exit`] with the configured code.
13
14use std::sync::mpsc;
15use std::sync::{Arc, Condvar, Mutex};
16use std::time::{Duration, Instant};
17
18/// How one phase ended.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum PhaseOutcome {
21    Completed,
22    /// The phase returned an error.
23    Failed(String),
24    /// The phase did not finish within its deadline; its thread was left running.
25    TimedOut,
26    /// The phase panicked.
27    Panicked,
28}
29
30/// One phase's record.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct PhaseReport {
33    pub name: String,
34    pub outcome: PhaseOutcome,
35    pub elapsed: Duration,
36}
37
38/// What a shutdown did, in phase order.
39#[derive(Debug, Clone, PartialEq, Eq, Default)]
40pub struct ShutdownReport {
41    pub phases: Vec<PhaseReport>,
42}
43
44impl ShutdownReport {
45    /// Every phase completed.
46    pub fn ok(&self) -> bool {
47        self.phases
48            .iter()
49            .all(|p| p.outcome == PhaseOutcome::Completed)
50    }
51}
52
53type PhaseFn = Box<dyn FnOnce() -> Result<(), String> + Send>;
54type ExitFn = Arc<dyn Fn(i32) + Send + Sync>;
55
56struct Phase {
57    name: String,
58    deadline: Duration,
59    run: PhaseFn,
60}
61
62struct Inner {
63    phases: Mutex<Option<Vec<Phase>>>,
64    watchdog: Mutex<Option<(Duration, i32)>>,
65    exit: Mutex<ExitFn>,
66    report: Mutex<Option<ShutdownReport>>,
67    done: Condvar,
68}
69
70/// The shutdown coordinator. Clones share one shutdown.
71#[derive(Clone)]
72pub struct OrderedShutdown {
73    inner: Arc<Inner>,
74}
75
76impl Default for OrderedShutdown {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl OrderedShutdown {
83    pub fn new() -> Self {
84        Self {
85            inner: Arc::new(Inner {
86                phases: Mutex::new(Some(Vec::new())),
87                watchdog: Mutex::new(None),
88                exit: Mutex::new(Arc::new(|code| std::process::exit(code))),
89                report: Mutex::new(None),
90                done: Condvar::new(),
91            }),
92        }
93    }
94
95    /// Append a phase. Phases run in the order added. `deadline` bounds this phase (a zero
96    /// deadline is raised to 1 ms). Ignored once the shutdown has started.
97    #[must_use]
98    pub fn phase(
99        self,
100        name: impl Into<String>,
101        deadline: Duration,
102        run: impl FnOnce() -> Result<(), String> + Send + 'static,
103    ) -> Self {
104        if let Some(phases) = self
105            .inner
106            .phases
107            .lock()
108            .unwrap_or_else(|e| e.into_inner())
109            .as_mut()
110        {
111            phases.push(Phase {
112                name: name.into(),
113                deadline: deadline.max(Duration::from_millis(1)),
114                run: Box::new(run),
115            });
116        }
117        self
118    }
119
120    /// End the process with `exit_code` when the whole shutdown has not finished `hard_deadline`
121    /// after it started. Without it there is no watchdog.
122    #[must_use]
123    pub fn watchdog(self, hard_deadline: Duration, exit_code: i32) -> Self {
124        *self
125            .inner
126            .watchdog
127            .lock()
128            .unwrap_or_else(|e| e.into_inner()) = Some((hard_deadline, exit_code));
129        self
130    }
131
132    /// Replace what the watchdog does (default: `std::process::exit(code)`), for a host that must
133    /// do its own last write first.
134    #[must_use]
135    pub fn on_watchdog(self, exit: impl Fn(i32) + Send + Sync + 'static) -> Self {
136        *self.inner.exit.lock().unwrap_or_else(|e| e.into_inner()) = Arc::new(exit);
137        self
138    }
139
140    /// Run the shutdown, or wait for the run already in flight and return its report.
141    pub fn run(&self) -> ShutdownReport {
142        let phases = self
143            .inner
144            .phases
145            .lock()
146            .unwrap_or_else(|e| e.into_inner())
147            .take();
148        let Some(phases) = phases else {
149            let mut report = self.inner.report.lock().unwrap_or_else(|e| e.into_inner());
150            while report.is_none() {
151                report = self
152                    .inner
153                    .done
154                    .wait(report)
155                    .unwrap_or_else(|e| e.into_inner());
156            }
157            return report.clone().unwrap_or_default();
158        };
159
160        let finished = Arc::new((Mutex::new(false), Condvar::new()));
161        let watchdog = *self
162            .inner
163            .watchdog
164            .lock()
165            .unwrap_or_else(|e| e.into_inner());
166        if let Some((hard, code)) = watchdog {
167            let finished = finished.clone();
168            let exit = self
169                .inner
170                .exit
171                .lock()
172                .unwrap_or_else(|e| e.into_inner())
173                .clone();
174            let _ = std::thread::Builder::new()
175                .name("rk-shutdown-watchdog".into())
176                .spawn(move || {
177                    let (flag, cv) = &*finished;
178                    let guard = flag.lock().unwrap_or_else(|e| e.into_inner());
179                    let (guard, _) = cv
180                        .wait_timeout_while(guard, hard, |done| !*done)
181                        .unwrap_or_else(|e| e.into_inner());
182                    if !*guard {
183                        exit(code);
184                    }
185                });
186        }
187
188        let mut report = ShutdownReport::default();
189        for phase in phases {
190            let started = Instant::now();
191            let (tx, rx) = mpsc::channel();
192            let run = phase.run;
193            let spawned = std::thread::Builder::new()
194                .name(format!("rk-shutdown-{}", phase.name))
195                .spawn(move || {
196                    let outcome = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(run))
197                    {
198                        Ok(Ok(())) => PhaseOutcome::Completed,
199                        Ok(Err(e)) => PhaseOutcome::Failed(e),
200                        Err(_) => PhaseOutcome::Panicked,
201                    };
202                    let _ = tx.send(outcome);
203                });
204            let outcome = match spawned {
205                Err(e) => PhaseOutcome::Failed(format!("could not start the phase: {e}")),
206                Ok(_) => rx
207                    .recv_timeout(phase.deadline)
208                    .unwrap_or(PhaseOutcome::TimedOut),
209            };
210            report.phases.push(PhaseReport {
211                name: phase.name,
212                outcome,
213                elapsed: started.elapsed(),
214            });
215        }
216
217        {
218            let (flag, cv) = &*finished;
219            *flag.lock().unwrap_or_else(|e| e.into_inner()) = true;
220            cv.notify_all();
221        }
222        *self.inner.report.lock().unwrap_or_else(|e| e.into_inner()) = Some(report.clone());
223        self.inner.done.notify_all();
224        report
225    }
226}