Skip to main content

webserver_base/webserver/
shutdown.rs

1//! Coordinated graceful shutdown.
2
3use std::future::Future;
4use std::sync::Arc;
5use std::time::Duration;
6
7use tokio::sync::watch;
8use tracing::{info, instrument, warn};
9
10/// The longest in-flight work may take once shutdown begins.
11///
12/// A ceiling, never a wait: a server holding no connection exits the instant it
13/// is signalled. The ceiling is what makes graceful shutdown terminate at all —
14/// `with_graceful_shutdown` waits for every connection, and a WebSocket never
15/// closes on its own, so without a deadline a socket-holding server hangs until
16/// it is killed.
17pub const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_secs(10);
18
19/// Application cleanup that must finish before the process exits.
20///
21/// [`WebServer::run`](super::WebServer::run) requires it of every state it
22/// carries, so a project cannot add app state and quietly forget that it needs
23/// draining. A server with no state gets the no-op implementation below and
24/// writes nothing.
25///
26/// The library owns the sequencing. This runs the moment the shutdown signal
27/// arrives, *concurrently* with the connection drain and under the same
28/// [`DEFAULT_DRAIN_TIMEOUT`], so a slow drain never eats the cleanup's budget
29/// and the two together cannot exceed one window. Overrunning is reported at
30/// `error!` by the caller — do not try to bound this yourself for that reason.
31///
32/// Being cut off at the ceiling is ordinary cancellation: the future is
33/// dropped at its last await point. Anything that must not be interrupted
34/// mid-way needs to be atomic on its own, because no shutdown design can
35/// cancel a synchronously blocking call.
36///
37/// # Idempotency
38///
39/// **This runs once per server holding the state, so it must be idempotent.** A
40/// binary running several servers off one `Arc<AppState>` calls it once per
41/// server. The library cannot deduplicate that — each [`WebServer`](super::WebServer)
42/// builds its own [`WebServerState`](super::WebServerState), and only the
43/// application knows what those share. Guard anything that would misbehave
44/// twice behind a `OnceCell` in your own state.
45///
46/// ```no_run
47/// use std::future::Future;
48/// use webserver_base::webserver::AppShutdown;
49///
50/// struct AppState {
51///     telegram: SomeNotifier,
52/// }
53/// # struct SomeNotifier;
54/// # impl SomeNotifier {
55/// #     async fn flush(&self, _: std::time::Duration) -> bool { true }
56/// # }
57///
58/// impl AppShutdown for AppState {
59///     async fn on_shutdown(&self) {
60///         self.telegram.flush(std::time::Duration::from_secs(5)).await;
61///     }
62/// }
63/// ```
64pub trait AppShutdown {
65    /// Drains whatever would otherwise be lost when the process exits.
66    ///
67    /// Returns `impl Future` rather than being an `async fn` because the
68    /// server's own future must stay `Send`, and an `async fn` in a trait
69    /// cannot promise that to its callers.
70    fn on_shutdown(&self) -> impl Future<Output = ()> + Send;
71}
72
73/// A server carrying no application state has nothing to drain.
74///
75/// Deliberately the only blanket implementation: a project that adds state has
76/// to say what draining means for it, and that is the whole point of the bound.
77impl AppShutdown for () {
78    async fn on_shutdown(&self) {}
79}
80
81/// A cloneable handle that resolves when the process should stop.
82///
83/// One listener, many holders — which is what lets a binary run several servers
84/// and drain them together.
85#[derive(Clone, Debug)]
86pub struct Shutdown {
87    sender: Arc<watch::Sender<bool>>,
88    receiver: watch::Receiver<bool>,
89}
90
91impl Shutdown {
92    /// A handle fired by hand, for tests and custom signal handling.
93    #[must_use]
94    pub fn manual() -> Self {
95        let (sender, receiver) = watch::channel(false);
96        Self {
97            sender: Arc::new(sender),
98            receiver,
99        }
100    }
101
102    /// A handle wired to this platform's termination signals: `SIGTERM`,
103    /// `SIGINT` and `SIGQUIT` on unix, ctrl-c elsewhere.
104    ///
105    /// The first signal starts the drain; a second exits immediately. Must be
106    /// called from inside a Tokio runtime.
107    #[must_use]
108    #[instrument(skip_all)]
109    pub fn listen() -> Self {
110        let shutdown: Self = Self::manual();
111        let trigger: Self = shutdown.clone();
112
113        tokio::spawn(async move {
114            wait_for_signal().await;
115            info!("shutdown signal received; draining");
116            trigger.trigger();
117
118            wait_for_signal().await;
119            warn!("second shutdown signal received; exiting immediately");
120            std::process::exit(130);
121        });
122
123        shutdown
124    }
125
126    /// Starts the drain.
127    pub fn trigger(&self) {
128        // A send failure means every receiver is already gone.
129        let _ = self.sender.send(true);
130    }
131
132    /// Whether the drain has started.
133    #[must_use]
134    pub fn is_shutting_down(&self) -> bool {
135        *self.receiver.borrow()
136    }
137
138    /// Resolves when the drain starts, immediately if it already has.
139    ///
140    /// Consumes the handle so the future is `'static`; clone first if the
141    /// handle is still needed.
142    pub async fn recv(mut self) {
143        if *self.receiver.borrow_and_update() {
144            return;
145        }
146        let _ = self.receiver.changed().await;
147    }
148}
149
150/// Waits for whichever termination signal arrives first.
151#[cfg(unix)]
152async fn wait_for_signal() {
153    use tokio::signal::unix::{Signal, SignalKind, signal};
154
155    let mut terminate: Signal =
156        signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");
157    let mut interrupt: Signal =
158        signal(SignalKind::interrupt()).expect("failed to install SIGINT handler");
159    let mut quit: Signal = signal(SignalKind::quit()).expect("failed to install SIGQUIT handler");
160
161    tokio::select! {
162        _ = terminate.recv() => {}
163        _ = interrupt.recv() => {}
164        _ = quit.recv() => {}
165    }
166}
167
168/// Waits for whichever termination signal arrives first.
169#[cfg(not(unix))]
170async fn wait_for_signal() {
171    let _ = tokio::signal::ctrl_c().await;
172}
173
174#[cfg(test)]
175mod tests {
176    use std::time::Duration;
177
178    use tokio::time::timeout;
179
180    use super::Shutdown;
181
182    #[tokio::test]
183    async fn a_fresh_handle_is_not_shutting_down_and_does_not_resolve() {
184        let shutdown: Shutdown = Shutdown::manual();
185
186        let expected: bool = false;
187        let actual: bool = shutdown.is_shutting_down();
188        assert_eq!(expected, actual);
189
190        let resolved: bool = timeout(Duration::from_millis(20), shutdown.clone().recv())
191            .await
192            .is_ok();
193        assert!(!resolved, "recv resolved before anything triggered it");
194    }
195
196    #[tokio::test]
197    async fn every_clone_hears_one_trigger() {
198        let shutdown: Shutdown = Shutdown::manual();
199        let first: Shutdown = shutdown.clone();
200        let second: Shutdown = shutdown.clone();
201
202        shutdown.trigger();
203
204        timeout(Duration::from_millis(200), first.recv())
205            .await
206            .expect("the first clone resolved");
207        timeout(Duration::from_millis(200), second.recv())
208            .await
209            .expect("the second clone resolved");
210    }
211
212    #[tokio::test]
213    async fn recv_after_the_fact_resolves_immediately() {
214        let shutdown: Shutdown = Shutdown::manual();
215        shutdown.trigger();
216
217        let expected: bool = true;
218        let actual: bool = shutdown.is_shutting_down();
219        assert_eq!(expected, actual);
220
221        timeout(Duration::from_millis(50), shutdown.clone().recv())
222            .await
223            .expect("a handle created before the trigger still resolves after it");
224    }
225
226    #[tokio::test]
227    async fn a_handle_cloned_after_the_trigger_still_resolves() {
228        let shutdown: Shutdown = Shutdown::manual();
229        shutdown.trigger();
230
231        let late: Shutdown = shutdown.clone();
232        timeout(Duration::from_millis(50), late.recv())
233            .await
234            .expect("a late clone sees the state, not just the transition");
235    }
236}