graceful_worker/
shutdown.rs1use std::time::Duration;
33
34use tokio_util::sync::CancellationToken;
35
36#[derive(Debug, Clone, Default)]
48pub struct Shutdown {
49 token: CancellationToken,
51}
52
53impl Shutdown {
54 #[must_use]
56 pub fn new() -> Self {
57 Self {
58 token: CancellationToken::new(),
59 }
60 }
61
62 #[must_use]
64 pub fn watcher(&self) -> Watcher {
65 Watcher {
66 token: self.token.clone(),
67 }
68 }
69
70 pub fn stop(&self) {
76 if !self.token.is_cancelled() {
77 log_requested();
78 }
79 self.token.cancel();
80 }
81
82 #[must_use]
84 pub fn is_stopping(&self) -> bool {
85 self.token.is_cancelled()
86 }
87
88 pub fn listen_for_signals(&self) {
104 let shutdown = self.clone();
105 tokio::spawn(async move {
106 wait_for_signal().await;
107 shutdown.stop();
108 });
109 }
110}
111
112#[derive(Debug, Clone)]
116pub struct Watcher {
117 token: CancellationToken,
119}
120
121impl Watcher {
122 #[must_use]
126 pub fn is_stopping(&self) -> bool {
127 self.token.is_cancelled()
128 }
129
130 #[must_use]
135 pub fn is_running(&self) -> bool {
136 !self.token.is_cancelled()
137 }
138
139 pub async fn wait(&self) {
144 self.token.cancelled().await;
145 }
146
147 pub async fn sleep(&self, duration: Duration) -> bool {
168 tokio::select! {
169 () = tokio::time::sleep(duration) => true,
170 () = self.token.cancelled() => false,
171 }
172 }
173}
174
175#[cfg(unix)]
177async fn wait_for_signal() {
178 use tokio::signal::unix::{SignalKind, signal};
179
180 let mut terminate = match signal(SignalKind::terminate()) {
181 Ok(stream) => stream,
182 Err(error) => {
183 log_no_handler("SIGTERM", &error);
184 return;
185 }
186 };
187 let mut interrupt = match signal(SignalKind::interrupt()) {
188 Ok(stream) => stream,
189 Err(error) => {
190 log_no_handler("SIGINT", &error);
191 return;
192 }
193 };
194
195 tokio::select! {
196 _ = terminate.recv() => log_signal("SIGTERM"),
197 _ = interrupt.recv() => log_signal("SIGINT"),
198 }
199}
200
201#[cfg(not(unix))]
203async fn wait_for_signal() {
204 if let Err(error) = tokio::signal::ctrl_c().await {
205 log_no_handler("Ctrl-C", &error);
206 }
207}
208
209#[cfg(feature = "tracing")]
210fn log_requested() {
211 tracing::info!("shutdown requested");
212}
213#[cfg(not(feature = "tracing"))]
214fn log_requested() {}
215
216#[cfg(feature = "tracing")]
217fn log_signal(name: &str) {
218 tracing::info!("{name} received");
219}
220#[cfg(not(feature = "tracing"))]
221fn log_signal(_name: &str) {}
222
223#[cfg(feature = "tracing")]
224fn log_no_handler(name: &str, error: &std::io::Error) {
225 tracing::error!(%error, "could not listen for {name}");
226}
227#[cfg(not(feature = "tracing"))]
228fn log_no_handler(_name: &str, _error: &std::io::Error) {}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[tokio::test]
235 async fn a_new_shutdown_is_not_stopping() {
236 let shutdown = Shutdown::new();
237 assert!(!shutdown.is_stopping());
238 assert!(shutdown.watcher().is_running());
239 }
240
241 #[tokio::test]
242 async fn stopping_is_visible_to_every_watcher() {
243 let shutdown = Shutdown::new();
244 let first = shutdown.watcher();
245 let second = shutdown.watcher();
246
247 shutdown.stop();
248
249 assert!(first.is_stopping());
250 assert!(second.is_stopping());
251 assert!(!first.is_running());
252 }
253
254 #[tokio::test]
255 async fn stopping_twice_is_harmless() {
256 let shutdown = Shutdown::new();
258 shutdown.stop();
259 shutdown.stop();
260 assert!(shutdown.is_stopping());
261 }
262
263 #[tokio::test]
264 async fn dropping_a_shutdown_does_not_stop_anything() {
265 let watcher = {
267 let shutdown = Shutdown::new();
268 shutdown.watcher()
269 };
270 assert!(watcher.is_running());
271 }
272
273 #[tokio::test(start_paused = true)]
274 async fn a_sleep_runs_to_completion_when_nothing_stops_it() {
275 let shutdown = Shutdown::new();
276 let watcher = shutdown.watcher();
277 assert!(watcher.sleep(Duration::from_secs(900)).await);
278 }
279
280 #[tokio::test(start_paused = true)]
281 async fn a_sleep_is_cut_short_by_a_stop() {
282 let shutdown = Shutdown::new();
283 let watcher = shutdown.watcher();
284
285 let sleeping = tokio::spawn(async move { watcher.sleep(Duration::from_secs(900)).await });
286
287 tokio::task::yield_now().await;
288 shutdown.stop();
289
290 assert!(
291 !sleeping.await.expect("the sleeping task"),
292 "the sleep should report having been cut short"
293 );
294 }
295
296 #[tokio::test]
297 async fn waiting_returns_at_once_when_already_stopping() {
298 let shutdown = Shutdown::new();
299 let watcher = shutdown.watcher();
300 shutdown.stop();
301 watcher.wait().await;
302 }
303
304 #[tokio::test(start_paused = true)]
305 async fn waiting_resolves_when_the_stop_arrives() {
306 let shutdown = Shutdown::new();
307 let watcher = shutdown.watcher();
308
309 let waiting = tokio::spawn(async move { watcher.wait().await });
310 tokio::task::yield_now().await;
311 shutdown.stop();
312
313 waiting.await.expect("the waiting task");
314 }
315
316 #[tokio::test]
317 async fn installing_signal_handlers_does_not_stop_anything_by_itself() {
318 let shutdown = Shutdown::new();
319 shutdown.listen_for_signals();
320 tokio::task::yield_now().await;
321 assert!(!shutdown.is_stopping());
322 }
323}