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}