Skip to main content

saluki_common/sync/
shutdown.rs

1//! Primitives for signalling shutdown.
2
3use std::{
4    future::Future,
5    pin::Pin,
6    sync::{
7        atomic::{
8            AtomicBool, AtomicUsize,
9            Ordering::{AcqRel, Acquire, Release},
10        },
11        Arc,
12    },
13    task::{Context, Poll},
14};
15
16use pin_project::{pin_project, pinned_drop};
17use tokio::sync::{futures::OwnedNotified, Notify};
18
19#[derive(Default)]
20struct ShutdownInner {
21    flag: AtomicBool,
22    notify: Arc<Notify>,
23    outstanding: AtomicUsize,
24    all_dropped: Notify,
25}
26
27impl ShutdownInner {
28    /// Records a newly created handle.
29    fn register(&self) {
30        self.outstanding.fetch_add(1, Release);
31    }
32
33    /// Records a dropped handle, releasing completion waiters once the last handle goes away.
34    fn deregister(&self) {
35        if self.outstanding.fetch_sub(1, AcqRel) == 1 {
36            // Use `notify_waiters` (rather than storing a permit): the last handle may drop _before_ shutdown is
37            // signaled, and an emitter only cares about this once it's actually waiting in `signal_and_await_handles`.
38            self.all_dropped.notify_waiters();
39        }
40    }
41
42    /// Signals shutdown, releasing any blocked waiters.
43    fn signal(&self) {
44        if !self.flag.swap(true, AcqRel) {
45            self.notify.notify_waiters();
46        }
47    }
48
49    /// Signals shutdown and waits until every outstanding handle has been dropped.
50    ///
51    /// Returns immediately if there are no outstanding handles.
52    async fn signal_and_await_handles(&self) {
53        // Snapshot the completion notification _before_ signaling, so the last handle dropping can't slip through the
54        // gap between signaling and parking.
55        let all_dropped = self.all_dropped.notified();
56
57        self.signal();
58
59        if self.outstanding.load(Acquire) == 0 {
60            return;
61        }
62
63        all_dropped.await;
64    }
65}
66
67/// A shutdown coordinator for triggering, and optionally waiting for, tasks to shut down.
68///
69/// Callers register a [`ShutdownHandle`], which can be awaited to observe shutdown being triggered, or checked
70/// synchronously via [`ShutdownHandle::is_triggered`]. When a handle is dropped, it reports completion back to the
71/// coordinator; this is what allows [`shutdown_and_wait`][Self::shutdown_and_wait] to block until every outstanding
72/// handle has finished.
73///
74/// Shutdown is triggered either fire-and-forget, via [`shutdown`][Self::shutdown], or blocking, via
75/// [`shutdown_and_wait`][Self::shutdown_and_wait].
76///
77/// # Shutdown on drop
78///
79/// Shutdown is also triggered implicitly on drop if it hasn't already been triggered, to enforce that shutdown always
80/// happens even in exceptional circumstances. Shutdown on drop is fire-and-forget: when the coordinator's drop logic
81/// has executed, there is no guarantee that all outstanding handles have also been dropped.
82#[derive(Default)]
83pub struct ShutdownCoordinator {
84    state: Arc<ShutdownInner>,
85}
86
87impl ShutdownCoordinator {
88    /// Registers and returns a new shutdown handle.
89    pub fn register(&mut self) -> ShutdownHandle {
90        ShutdownHandle::registered(Arc::clone(&self.state))
91    }
92
93    /// Signals shutdown to all outstanding handles without waiting.
94    ///
95    /// When it is desirable to wait until all outstanding handles have completed (been dropped), use
96    /// [`shutdown_and_wait`][Self::shutdown_and_wait].
97    pub fn shutdown(self) {
98        self.state.signal();
99    }
100
101    /// Signals shutdown to all outstanding handles, waiting until all handles have completed (been dropped).
102    ///
103    /// If there are no outstanding handles, this returns immediately.
104    pub async fn shutdown_and_wait(self) {
105        self.state.signal_and_await_handles().await;
106    }
107}
108
109impl Drop for ShutdownCoordinator {
110    fn drop(&mut self) {
111        self.state.signal();
112    }
113}
114
115/// A handle for observing shutdown from a [`ShutdownCoordinator`].
116///
117/// The handle is a [`Future`] that resolves once shutdown is triggered, and can also be checked synchronously via
118/// [`is_triggered`][Self::is_triggered]. Dropping the handle reports completion back to its coordinator, which is how
119/// [`ShutdownCoordinator::shutdown_and_wait`] knows when all outstanding work has finished.
120#[pin_project(PinnedDrop)]
121pub struct ShutdownHandle {
122    state: Arc<ShutdownInner>,
123
124    #[pin]
125    notified: OwnedNotified,
126}
127
128impl ShutdownHandle {
129    /// Creates a handle sharing the given state, registering it as an outstanding handle.
130    fn registered(state: Arc<ShutdownInner>) -> Self {
131        state.register();
132
133        // Capture our `OwnedNotified` during registration, which ensures that when we eventually poll it, it will be
134        // guaranteed that we observe any notifications sent _after_ this point.
135        //
136        // We couple that invariant with first checking the atomic flag to ensure we never miss wakeups.
137        let notify = Arc::clone(&state.notify);
138        let notified = notify.notified_owned();
139
140        Self { state, notified }
141    }
142
143    /// Creates a shutdown handle that is never triggered.
144    ///
145    /// This can be useful when a callee requires a handle but no shutdown signal is expected or available to provide.
146    pub fn noop() -> Self {
147        Self::registered(Arc::new(ShutdownInner::default()))
148    }
149
150    /// Creates a coordinator/handle pair.
151    ///
152    /// This is equivalent to creating [`ShutdownCoordinator`] and calling [`ShutdownCoordinator::register`]. The
153    /// resulting coordinator can still be used to register further handles if need be.
154    pub fn paired() -> (ShutdownCoordinator, Self) {
155        let mut coordinator = ShutdownCoordinator::default();
156        let handle = coordinator.register();
157        (coordinator, handle)
158    }
159
160    /// Returns `true` if shutdown has been triggered.
161    pub fn is_triggered(&self) -> bool {
162        self.state.flag.load(Acquire)
163    }
164
165    /// Returns a read-only view of this handle's shutdown signal.
166    ///
167    /// Clone the view to observe the signal from other tasks.
168    pub fn view(&self) -> ShutdownView {
169        ShutdownView {
170            state: Arc::clone(&self.state),
171        }
172    }
173}
174
175/// A read-only view of the shutdown signal of a [`ShutdownHandle`].
176///
177/// A view reports the same state as the handle that it came from. But a view is not a handle: it does not count as an
178/// outstanding handle, so [`ShutdownCoordinator::shutdown_and_wait`] does not wait for it, and it cannot be awaited.
179#[derive(Clone)]
180pub struct ShutdownView {
181    state: Arc<ShutdownInner>,
182}
183
184impl ShutdownView {
185    /// Returns `true` if shutdown has been triggered.
186    pub fn is_triggered(&self) -> bool {
187        self.state.flag.load(Acquire)
188    }
189}
190
191impl Future for ShutdownHandle {
192    type Output = ();
193
194    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
195        let this = self.project();
196
197        // Fast path: if shutdown has already been triggered, return immediately.
198        if this.state.flag.load(Acquire) {
199            return Poll::Ready(());
200        }
201
202        // Wait to be notified that shutdown has been triggered.
203        //
204        // We don't need to recheck the flag because we know that by creating `OwnedNotified` before
205        // checking the flag, any subsequent poll we do that returns as ready can only be the result
206        // of the notify happening after setting the flag.
207        this.notified.poll(cx)
208    }
209}
210
211#[pinned_drop]
212impl PinnedDrop for ShutdownHandle {
213    fn drop(self: Pin<&mut Self>) {
214        self.state.deregister();
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use tokio_test::{assert_pending, assert_ready, task::spawn};
221
222    use super::*;
223
224    #[test]
225    fn handle_starts_untriggered() {
226        let (_coordinator, handle) = ShutdownHandle::paired();
227
228        assert!(!handle.is_triggered());
229    }
230
231    #[test]
232    fn handle_triggered_on_coordinator_shutdown() {
233        let (coordinator, handle) = ShutdownHandle::paired();
234        assert!(!handle.is_triggered());
235
236        coordinator.shutdown();
237        assert!(handle.is_triggered());
238    }
239
240    #[test]
241    fn handle_triggered_on_coordinator_drop() {
242        let (coordinator, handle) = ShutdownHandle::paired();
243        assert!(!handle.is_triggered());
244
245        drop(coordinator);
246        assert!(handle.is_triggered());
247    }
248
249    #[test]
250    fn view_reports_the_state_of_its_handle() {
251        let (coordinator, handle) = ShutdownHandle::paired();
252        let view = handle.view();
253        assert!(!view.is_triggered());
254
255        coordinator.shutdown();
256        assert!(view.is_triggered());
257    }
258
259    #[tokio::test]
260    async fn view_is_not_waited_for() {
261        let (coordinator, handle) = ShutdownHandle::paired();
262        let view = handle.view();
263
264        // The view is not an outstanding handle. Thus, after the handle is dropped, a coordinator that waits for its
265        // handles does not wait for the view.
266        drop(handle);
267        tokio::time::timeout(std::time::Duration::from_secs(1), coordinator.shutdown_and_wait())
268            .await
269            .expect("the view must not hold up the coordinator");
270        assert!(view.is_triggered());
271    }
272
273    #[test]
274    fn handle_wait_returns_immediately_when_already_triggered() {
275        let (coordinator, handle) = ShutdownHandle::paired();
276        coordinator.shutdown();
277
278        let mut wait = spawn(handle);
279        assert_ready!(wait.poll());
280    }
281
282    #[test]
283    fn handle_completes_after_coordinator_shutdown() {
284        let (coordinator, handle) = ShutdownHandle::paired();
285
286        let mut wait = spawn(handle);
287        assert_pending!(wait.poll());
288        assert!(!wait.is_woken());
289
290        // Triggering shutdown wakes the parked handle and allows it to resolve:
291        coordinator.shutdown();
292        assert!(wait.is_woken());
293        assert_ready!(wait.poll());
294    }
295
296    #[test]
297    fn handle_completes_after_coordinator_dropped() {
298        let (coordinator, handle) = ShutdownHandle::paired();
299
300        let mut wait = spawn(handle);
301        assert_pending!(wait.poll());
302        assert!(!wait.is_woken());
303
304        // Dropping the coordinator is equivalent to triggering shutdown and should wake the parked handle and allow it
305        // to resolve:
306        drop(coordinator);
307        assert!(wait.is_woken());
308        assert_ready!(wait.poll());
309    }
310
311    #[test]
312    fn handle_is_fused_after_coordinator_shutdown() {
313        let (coordinator, handle) = ShutdownHandle::paired();
314        coordinator.shutdown();
315
316        // The flag is latched, so every subsequent wait observes it and resolves immediately:
317        let mut wait = spawn(handle);
318        assert_ready!(wait.poll());
319        assert_ready!(wait.poll());
320    }
321
322    #[test]
323    fn multiple_handles_all_wake_on_shutdown() {
324        let mut coordinator = ShutdownCoordinator::default();
325        let handle1 = coordinator.register();
326        let handle2 = coordinator.register();
327        let handle3 = coordinator.register();
328
329        let mut wait1 = spawn(handle1);
330        let mut wait2 = spawn(handle2);
331        let mut wait3 = spawn(handle3);
332
333        assert_pending!(wait1.poll());
334        assert_pending!(wait2.poll());
335        assert_pending!(wait3.poll());
336
337        // Triggering shutdown should immediately wake all parked handles and allow them to resolve:
338        coordinator.shutdown();
339        assert!(wait1.is_woken());
340        assert!(wait2.is_woken());
341        assert!(wait3.is_woken());
342
343        assert_ready!(wait1.poll());
344        assert_ready!(wait2.poll());
345        assert_ready!(wait3.poll());
346    }
347
348    #[test]
349    fn noop_handle_never_completes() {
350        let handle = ShutdownHandle::noop();
351
352        // A no-op handle has no trigger, so it reports as untriggered and never resolves:
353        assert!(!handle.is_triggered());
354
355        let mut wait = spawn(handle);
356        assert_pending!(wait.poll());
357        assert_pending!(wait.poll());
358        assert!(!wait.is_woken());
359    }
360
361    #[test]
362    fn coordinator_shutdown_and_wait_returns_immediately_with_no_handles() {
363        let coordinator = ShutdownCoordinator::default();
364
365        // With no registered handles, `shutdown_and_wait` resolves on the first poll:
366        let mut shutdown = spawn(coordinator.shutdown_and_wait());
367        assert_ready!(shutdown.poll());
368    }
369
370    #[test]
371    fn coordinator_shutdown_and_wait_ignores_handle_dropped_before_shutdown() {
372        let mut coordinator = ShutdownCoordinator::default();
373
374        // A handle registered and then dropped _before_ shutdown deregisters itself, so it doesn't count toward
375        // outstanding handles:
376        let handle = coordinator.register();
377        drop(handle);
378
379        // With no outstanding handles remaining, `shutdown_and_wait` resolves on the first poll:
380        let mut shutdown = spawn(coordinator.shutdown_and_wait());
381        assert_ready!(shutdown.poll());
382    }
383
384    #[test]
385    fn coordinator_shutdown_waits_for_outstanding_handle_to_drop_single() {
386        let mut coordinator = ShutdownCoordinator::default();
387        let handle = coordinator.register();
388
389        let mut wait = spawn(handle);
390        assert!(!wait.is_woken());
391        assert_pending!(wait.poll());
392
393        // The first poll signals the outstanding handle and then parks until that handle is dropped:
394        let mut shutdown = spawn(coordinator.shutdown_and_wait());
395        assert!(!shutdown.is_woken());
396        assert_pending!(shutdown.poll());
397        assert!(!shutdown.is_woken());
398
399        // Our handle should now be woken up and able to complete, but the coordinator should _not_ be woken up until
400        // the handle is actually dropped:
401        assert!(wait.is_woken());
402        assert_ready!(wait.poll());
403
404        assert_pending!(shutdown.poll());
405        assert!(!shutdown.is_woken());
406
407        drop(wait);
408
409        assert!(shutdown.is_woken());
410        assert_ready!(shutdown.poll());
411    }
412
413    #[test]
414    fn coordinator_shutdown_waits_for_outstanding_handle_to_drop_multiple() {
415        let mut coordinator = ShutdownCoordinator::default();
416        let handle1 = coordinator.register();
417        let handle2 = coordinator.register();
418
419        let mut wait1 = spawn(handle1);
420        let mut wait2 = spawn(handle2);
421
422        assert!(!wait1.is_woken());
423        assert!(!wait2.is_woken());
424        assert_pending!(wait1.poll());
425        assert_pending!(wait2.poll());
426
427        // The first poll signals the outstanding handles and then parks until those handles are dropped:
428        let mut shutdown = spawn(coordinator.shutdown_and_wait());
429        assert!(!shutdown.is_woken());
430        assert_pending!(shutdown.poll());
431        assert!(!shutdown.is_woken());
432
433        // Our handles should now be woken up and able to complete, but the coordinator should _not_ be woken up until
434        // the handles are actually dropped:
435        assert!(wait1.is_woken());
436        assert_ready!(wait1.poll());
437
438        assert!(wait2.is_woken());
439        assert_ready!(wait2.poll());
440
441        assert!(!shutdown.is_woken());
442        assert_pending!(shutdown.poll());
443
444        // Drop the first handle, which should not resolve the shutdown yet:
445        drop(wait1);
446
447        assert!(!shutdown.is_woken());
448        assert_pending!(shutdown.poll());
449
450        // Drop the second handle, which should resolve the shutdown:
451        drop(wait2);
452
453        assert!(shutdown.is_woken());
454        assert_ready!(shutdown.poll());
455    }
456}