ados_protocol/shutdown.rs
1//! A latching, clone-able shutdown signal for a service's run loops.
2//!
3//! `tokio::sync::Notify::notify_waiters` stores no permit: it wakes only the
4//! futures already waiting at the instant it runs. A loop that is inside its
5//! body at that instant (building a snapshot, writing a frame) re-enters its
6//! `select!` with a fresh `notified()` that nothing will ever wake, so a
7//! shutdown that joins its tasks hangs until the service manager kills it.
8//!
9//! [`Shutdown`] latches instead: [`Shutdown::trigger`] sets a flag and wakes
10//! every current waiter, and [`Shutdown::wait`] returns at once when the flag is
11//! already set, so a loop that reaches its `select!` after the trigger still
12//! stops.
13
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::Arc;
16
17use tokio::sync::Notify;
18
19/// A cancellation handle every run loop awaits and the service triggers on
20/// SIGTERM / SIGINT. Cheap to clone (shared `Arc`s).
21#[derive(Clone, Debug)]
22pub struct Shutdown {
23 fired: Arc<AtomicBool>,
24 notify: Arc<Notify>,
25}
26
27impl Shutdown {
28 /// A fresh, un-fired shutdown handle.
29 pub fn new() -> Self {
30 Self {
31 fired: Arc::new(AtomicBool::new(false)),
32 notify: Arc::new(Notify::new()),
33 }
34 }
35
36 /// Fire the signal. Idempotent; wakes every current and future waiter.
37 pub fn trigger(&self) {
38 self.fired.store(true, Ordering::SeqCst);
39 self.notify.notify_waiters();
40 }
41
42 /// Whether the signal has fired.
43 pub fn is_fired(&self) -> bool {
44 self.fired.load(Ordering::SeqCst)
45 }
46
47 /// Resolve once the signal has fired. Returns immediately when it fired
48 /// before this call.
49 pub async fn wait(&self) {
50 if self.is_fired() {
51 return;
52 }
53 // Register for the wakeup, then re-check, closing the race where
54 // trigger() ran between the flag check and the registration.
55 let notified = self.notify.notified();
56 if self.is_fired() {
57 return;
58 }
59 notified.await;
60 }
61}
62
63impl Default for Shutdown {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use std::time::Duration;
73
74 #[tokio::test]
75 async fn a_parked_waiter_wakes_on_trigger() {
76 let s = Shutdown::new();
77 let waiter = s.clone();
78 let handle = tokio::spawn(async move { waiter.wait().await });
79 tokio::time::sleep(Duration::from_millis(20)).await;
80 s.trigger();
81 tokio::time::timeout(Duration::from_secs(1), handle)
82 .await
83 .expect("a parked waiter must wake on trigger")
84 .unwrap();
85 }
86
87 /// The property `Notify::notify_waiters` lacks: a loop that was busy when
88 /// the signal fired, and only reaches its wait afterwards, still stops.
89 #[tokio::test]
90 async fn a_waiter_that_arrives_after_the_trigger_returns_at_once() {
91 let s = Shutdown::new();
92 s.trigger();
93 tokio::time::timeout(Duration::from_millis(100), s.wait())
94 .await
95 .expect("a fired signal must not block a later waiter");
96 assert!(s.is_fired());
97 }
98}