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}