Skip to main content

stratum_apps/utils/
shutdown.rs

1//! Process shutdown signalling.
2
3use std::{fmt, io};
4
5use tokio::signal::unix::{Signal, SignalKind, signal};
6
7/// A signal asking the process to stop.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum StopSignal {
10    /// `SIGINT`, sent by an interactive terminal on Ctrl+C.
11    Interrupt,
12    /// `SIGTERM`, sent by service managers and container runtimes.
13    Terminate,
14}
15
16impl StopSignal {
17    /// The exit status conventionally associated with this signal.
18    ///
19    /// That status is `128 + the signal number` (`SIGINT` is 2, `SIGTERM` is 15), which is what a
20    /// shell or a container runtime reports when a signal terminates a process. Exiting with it
21    /// is not the same outcome as being terminated by the signal, which a supervisor can tell
22    /// apart, but it is the number tooling already reads as "stopped by an operator".
23    pub fn exit_code(self) -> i32 {
24        match self {
25            Self::Interrupt => 130, // 128 + 2
26            Self::Terminate => 143, // 128 + 15
27        }
28    }
29}
30
31impl fmt::Display for StopSignal {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        f.write_str(match self {
34            Self::Interrupt => "SIGINT",
35            Self::Terminate => "SIGTERM",
36        })
37    }
38}
39
40/// Listener for the signals that ask the process to stop.
41pub struct ShutdownListener {
42    sigint: Signal,
43    sigterm: Signal,
44}
45
46impl ShutdownListener {
47    /// Starts listening for `SIGINT` and `SIGTERM`.
48    ///
49    /// Listening begins here rather than at the first [`ShutdownListener::wait`] call, so a caller
50    /// that builds the listener before it starts serving cannot miss a signal that arrives
51    /// during startup.
52    ///
53    /// An error means the process has no way to observe either signal, so callers treat it as
54    /// fatal rather than running on without a shutdown path.
55    pub fn new() -> Result<Self, io::Error> {
56        Ok(Self {
57            sigint: signal(SignalKind::interrupt())?,
58            sigterm: signal(SignalKind::terminate())?,
59        })
60    }
61
62    /// Waits for the next stop signal.
63    ///
64    /// The listeners stay alive across calls, so a caller that has already begun shutting down
65    /// can wait again to learn that the operator is no longer willing to wait for the graceful
66    /// path, and exit with [`StopSignal::exit_code`].
67    ///
68    /// Signals are coalesced, so two that arrive close enough together can be reported as one.
69    pub async fn wait(&mut self) -> StopSignal {
70        tokio::select! {
71            _ = self.sigint.recv() => StopSignal::Interrupt,
72            _ = self.sigterm.recv() => StopSignal::Terminate,
73        }
74    }
75}