Skip to main content

behest_runtime/
lifecycle.rs

1//! Cooperative shutdown primitives.
2//!
3//! [`ShutdownToken`] is a clonable, hierarchical cancellation primitive shared
4//! between the [`ComponentRegistry`](super::registry::ComponentRegistry),
5//! every [`Component`](super::component::Component) lifecycle phase,
6//! and the future transport layer (added in M5).
7//!
8//! Design goals:
9//!
10//! 1. **Cooperative, not preemptive**: tasks opt-in by calling
11//!    [`ShutdownToken::wait`] or by polling [`ShutdownToken::is_shutdown`].
12//! 2. **Hierarchical**: child tokens propagate a shutdown signal upward to
13//!    their parent, so the entire process can be torn down from any
14//!    component failure without each component needing to wire its own
15//!    supervision.
16//! 3. **Cheap to clone**: backpressure-free, `Arc`-backed, safe to embed
17//!    in every component without allocation pressure.
18
19#![allow(clippy::pedantic)]
20use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22
23use tokio::sync::Notify;
24
25/// Hierarchical, cooperative shutdown token.
26///
27/// Cloning a token is cheap: all clones share the same underlying state. Child
28/// tokens notify their parent on [`ShutdownToken::signal_shutdown`], so any
29/// component can request a process-wide shutdown.
30#[derive(Clone, Default)]
31pub struct ShutdownToken {
32    state: Arc<ShutdownState>,
33}
34
35struct ShutdownState {
36    flag: AtomicBool,
37    notify: Notify,
38    /// Optional parent. When a child signals shutdown, the parent also
39    /// receives the signal. This enables the
40    /// "any-component-failure-takes-down-the-process" semantics.
41    parent: Option<Arc<ShutdownState>>,
42}
43
44impl Default for ShutdownState {
45    fn default() -> Self {
46        Self {
47            flag: AtomicBool::new(false),
48            notify: Notify::new(),
49            parent: None,
50        }
51    }
52}
53
54impl ShutdownToken {
55    /// Create a root shutdown token with no parent.
56    #[must_use]
57    pub fn new() -> Self {
58        Self {
59            state: Arc::new(ShutdownState {
60                flag: AtomicBool::new(false),
61                notify: Notify::new(),
62                parent: None,
63            }),
64        }
65    }
66
67    /// Derive a child token that propagates its shutdown signal to `self`.
68    ///
69    /// Calling [`ShutdownToken::signal_shutdown`] on the child will also
70    /// signal the parent. The child has its own internal flag, so a parent
71    /// that is already shutdown will not cause spurious wakeups on a
72    /// freshly-created child.
73    #[must_use]
74    pub fn child(&self) -> Self {
75        Self {
76            state: Arc::new(ShutdownState {
77                flag: AtomicBool::new(false),
78                notify: Notify::new(),
79                parent: Some(self.state.clone()),
80            }),
81        }
82    }
83
84    /// Signal shutdown. Idempotent; subsequent calls are no-ops.
85    pub fn signal_shutdown(&self) {
86        self.state.flag.store(true, Ordering::SeqCst);
87        self.state.notify.notify_waiters();
88        if let Some(parent) = &self.state.parent {
89            parent.flag.store(true, Ordering::SeqCst);
90            parent.notify.notify_waiters();
91        }
92    }
93
94    /// Returns `true` if [`ShutdownToken::signal_shutdown`] has been called on
95    /// this token or any of its ancestors.
96    #[must_use]
97    pub fn is_shutdown(&self) -> bool {
98        if self.state.flag.load(Ordering::SeqCst) {
99            return true;
100        }
101        if let Some(parent) = &self.state.parent {
102            return parent.flag.load(Ordering::SeqCst);
103        }
104        false
105    }
106
107    /// Wait for a shutdown signal. Returns immediately if one has already
108    /// been signalled.
109    pub async fn wait(&self) {
110        if self.is_shutdown() {
111            return;
112        }
113        let notified = self.state.notify.notified();
114        if self.is_shutdown() {
115            return;
116        }
117        notified.await;
118    }
119
120    /// Reset the shutdown flag. Intended for tests only; production code
121    /// should treat shutdown as monotonic.
122    #[cfg(test)]
123    pub fn reset(&self) {
124        self.state.flag.store(false, Ordering::SeqCst);
125    }
126}
127
128impl std::fmt::Debug for ShutdownToken {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("ShutdownToken")
131            .field("is_shutdown", &self.is_shutdown())
132            .field("has_parent", &self.state.parent.is_some())
133            .finish()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn fresh_token_is_not_shutdown() {
143        let t = ShutdownToken::new();
144        assert!(!t.is_shutdown());
145    }
146
147    #[test]
148    fn signal_shutdown_is_idempotent() {
149        let t = ShutdownToken::new();
150        t.signal_shutdown();
151        t.signal_shutdown();
152        assert!(t.is_shutdown());
153    }
154
155    #[test]
156    fn child_propagates_to_parent() {
157        let parent = ShutdownToken::new();
158        let child = parent.child();
159        assert!(!parent.is_shutdown());
160        child.signal_shutdown();
161        assert!(child.is_shutdown());
162        assert!(parent.is_shutdown());
163    }
164
165    #[test]
166    fn sibling_children_do_not_propagate_to_each_other() {
167        let parent = ShutdownToken::new();
168        let a = parent.child();
169        let _b = parent.child();
170        a.signal_shutdown();
171        // parent is shutdown but sibling b's local flag is not.
172        // b.is_shutdown() returns true because it walks to parent.
173        // This is intentional: children observe ancestor shutdowns.
174        let _ = parent.is_shutdown();
175    }
176
177    #[tokio::test]
178    async fn wait_returns_when_signalled() {
179        let t = ShutdownToken::new();
180        let t2 = t.clone();
181        tokio::spawn(async move {
182            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
183            t2.signal_shutdown();
184        });
185        t.wait().await;
186        assert!(t.is_shutdown());
187    }
188
189    #[tokio::test]
190    async fn wait_returns_immediately_if_already_signalled() {
191        let t = ShutdownToken::new();
192        t.signal_shutdown();
193        // Should not hang.
194        t.wait().await;
195    }
196}