saluki_core/runtime/
spawn.rs1use std::future::Future;
13
14use tokio::task::futures::TaskLocalFuture;
15
16use super::supervisor::{ChildId, ChildSpecification, ChildState, SupervisorHandle};
17
18tokio::task_local! {
19 pub(super) static CURRENT_SUPERVISOR: SupervisorHandle;
21}
22
23pub fn spawn<S, T>(child: T) -> ChildId
50where
51 S: ChildState,
52 T: Into<ChildSpecification<S>>,
53{
54 CURRENT_SUPERVISOR
55 .try_with(|supervisor| supervisor.spawn(child))
56 .unwrap_or_else(|_| {
57 panic!(
58 "`runtime::spawn` called outside of a supervised process: there is no ambient supervisor to spawn on. \
59 Spawn through a `SupervisorHandle` directly, or establish an ambient supervisor with \
60 `SupervisorHandle::scope`."
61 )
62 })
63}
64
65impl SupervisorHandle {
66 pub fn scope<F>(&self, fut: F) -> TaskLocalFuture<SupervisorHandle, F>
74 where
75 F: Future,
76 {
77 CURRENT_SUPERVISOR.scope(self.clone(), fut)
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use std::sync::{
84 atomic::{AtomicUsize, Ordering},
85 Arc,
86 };
87 use std::time::Duration;
88
89 use async_trait::async_trait;
90 use saluki_common::sync::shutdown::ShutdownHandle;
91 use tokio::sync::oneshot;
92
93 use super::*;
94 use crate::runtime::{
95 ChildSpecification, FnWorker, InitializationError, ShutdownStrategy, Supervisable, Supervisor, SupervisorError,
96 SupervisorFuture,
97 };
98 use crate::test_support::wait_until;
99
100 struct ActionWorker {
105 name: &'static str,
106 during_init: bool,
107 action: std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>>,
108 }
109
110 impl ActionWorker {
111 fn new<F>(name: &'static str, during_init: bool, action: F) -> Self
112 where
113 F: FnOnce() + Send + 'static,
114 {
115 Self {
116 name,
117 during_init,
118 action: std::sync::Mutex::new(Some(Box::new(action))),
119 }
120 }
121
122 fn take_action(&self) -> Box<dyn FnOnce() + Send> {
123 self.action
124 .lock()
125 .expect("action mutex poisoned")
126 .take()
127 .expect("worker should only run once")
128 }
129 }
130
131 #[async_trait]
132 impl Supervisable for ActionWorker {
133 fn name(&self) -> &str {
134 self.name
135 }
136
137 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
138 let action = self.take_action();
139 if self.during_init {
140 action();
141
142 return Ok(Box::pin(async move {
143 process_shutdown.await;
144 Ok(())
145 }));
146 }
147
148 Ok(Box::pin(async move {
149 action();
150 process_shutdown.await;
151 Ok(())
152 }))
153 }
154 }
155
156 fn endless(worker: FnWorker) -> ChildSpecification {
159 ChildSpecification::worker(worker).with_shutdown_strategy(ShutdownStrategy::Brutal)
160 }
161
162 async fn run_worker_that<F>(during_init: bool, action: F) -> Result<(), SupervisorError>
167 where
168 F: FnOnce() + Send + 'static,
169 {
170 let acted = Arc::new(AtomicUsize::new(0));
171 let worker_acted = Arc::clone(&acted);
172
173 let mut sup = Supervisor::new("ambient-sup").expect("supervisor name should be valid");
174 sup.add_worker(ActionWorker::new("actor", during_init, move || {
175 action();
176 worker_acted.fetch_add(1, Ordering::SeqCst);
177 }));
178
179 let (shutdown_tx, shutdown_rx) = oneshot::channel();
180 let handle = sup.handle();
181 let run = tokio::spawn(async move { sup.run_with_shutdown(shutdown_rx).await });
182
183 wait_until("supervisor is running", || handle.is_running()).await;
184 wait_until("the worker has run its action", || acted.load(Ordering::SeqCst) == 1).await;
185
186 let _ = shutdown_tx.send(());
187 tokio::time::timeout(Duration::from_secs(5), run)
188 .await
189 .expect("supervisor should stop promptly")
190 .expect("supervisor task should not panic")
191 }
192
193 #[tokio::test]
194 async fn worker_spawns_onto_its_own_supervisor() {
195 let started = Arc::new(AtomicUsize::new(0));
198 let child_started = Arc::clone(&started);
199
200 let result = run_worker_that(false, move || {
201 spawn(endless(FnWorker::new("sibling", async move {
202 child_started.fetch_add(1, Ordering::SeqCst);
203 std::future::pending::<()>().await;
204 })));
205 })
206 .await;
207
208 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
209 assert_eq!(started.load(Ordering::SeqCst), 1, "the spawned sibling should have run");
210 }
211
212 #[tokio::test]
213 async fn worker_can_spawn_during_initialization() {
214 let started = Arc::new(AtomicUsize::new(0));
217 let child_started = Arc::clone(&started);
218
219 let result = run_worker_that(true, move || {
220 spawn(endless(FnWorker::new("helper", async move {
221 child_started.fetch_add(1, Ordering::SeqCst);
222 std::future::pending::<()>().await;
223 })));
224 })
225 .await;
226
227 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
228 assert_eq!(
229 started.load(Ordering::SeqCst),
230 1,
231 "a child spawned during initialization should have run"
232 );
233 }
234
235 #[tokio::test]
236 #[should_panic(expected = "outside of a supervised process")]
237 async fn spawning_without_an_ambient_supervisor_panics() {
238 spawn(endless(FnWorker::new("orphan", std::future::pending::<()>())));
241 }
242
243 #[tokio::test]
244 async fn ambient_supervisor_is_not_inherited_by_tokio_spawn() {
245 let escaped = Arc::new(AtomicUsize::new(0));
249 let observed = Arc::clone(&escaped);
250
251 let result = run_worker_that(false, move || {
252 tokio::spawn(async move {
253 if CURRENT_SUPERVISOR.try_with(|_| ()).is_err() {
254 observed.fetch_add(1, Ordering::SeqCst);
255 }
256 });
257 })
258 .await;
259
260 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
261 assert_eq!(
262 escaped.load(Ordering::SeqCst),
263 1,
264 "a `tokio::spawn`ed task must not see an ambient supervisor"
265 );
266 }
267
268 #[tokio::test]
269 async fn scope_establishes_an_ambient_supervisor() {
270 let started = Arc::new(AtomicUsize::new(0));
273 let child_started = Arc::clone(&started);
274
275 let mut sup = Supervisor::new("scoped-sup").expect("supervisor name should be valid");
276 let handle = sup.handle();
278 let (shutdown_tx, shutdown_rx) = oneshot::channel();
279 let run = tokio::spawn(async move { sup.run_with_shutdown(shutdown_rx).await });
280 wait_until("supervisor is running", || handle.is_running()).await;
281
282 handle
283 .scope(async {
284 spawn(endless(FnWorker::new("scoped", async move {
285 child_started.fetch_add(1, Ordering::SeqCst);
286 std::future::pending::<()>().await;
287 })));
288 })
289 .await;
290
291 wait_until("the scoped child has started", || started.load(Ordering::SeqCst) == 1).await;
292
293 let _ = shutdown_tx.send(());
294 let result = tokio::time::timeout(Duration::from_secs(5), run)
295 .await
296 .expect("supervisor should stop promptly")
297 .expect("supervisor task should not panic");
298 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
299 }
300}