behest_runtime/
lifecycle.rs1#![allow(clippy::pedantic)]
20use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22
23use tokio::sync::Notify;
24
25#[derive(Clone, Default)]
31pub struct ShutdownToken {
32 state: Arc<ShutdownState>,
33}
34
35struct ShutdownState {
36 flag: AtomicBool,
37 notify: Notify,
38 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 #[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 #[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 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 #[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 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 #[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 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 t.wait().await;
195 }
196}