saluki_core/runtime/spawn.rs
1//! Spawn functions that use the ambient scope and supervisor.
2//!
3//! The runtime installs two task-local values for every supervised process:
4//!
5//! - its [scope][crate::runtime::scope], which holds the children that it spawns as its own
6//! - a handle to the supervisor that supervises it
7//!
8//! These values make [`spawn`] possible. Any code in a supervised process can start a child of that process, without a
9//! handle. This code includes the body of the worker, its [`initialize`][crate::runtime::Supervisable::initialize], and
10//! any helper that it calls. [`spawn_sibling`] gives the child to the nearest supervisor above the process instead.
11//!
12//! The runtime installs both values separately for each supervised task. Thus, tasks started with [`tokio::spawn`] do
13//! not get them. These tasks run outside supervision, and [`spawn`] panics in them, so that it does not silently attach
14//! the child to an unexpected owner. Use [`SupervisorHandle::scope`] to deliberately establish an ambient supervisor
15//! for such a task.
16
17use std::future::Future;
18
19use tokio::task::futures::TaskLocalFuture;
20
21use super::scope;
22use super::supervisor::{ChildId, ChildSpecification, ChildState, SupervisorHandle};
23
24tokio::task_local! {
25 /// Handle to the supervisor supervising the currently running process.
26 pub(super) static CURRENT_SUPERVISOR: SupervisorHandle;
27}
28
29/// Spawns a child of the current process.
30///
31/// The [scope][crate::runtime::scope] of the current process holds the child. The process does not finish until the
32/// child finishes, and the child is told to stop when the body of the process returns.
33///
34/// Some code does not run as a supervised process, but has an ambient supervisor from [`SupervisorHandle::scope`]. An
35/// example is a test that drives a component directly. That code spawns onto the ambient supervisor instead.
36///
37/// Accepts anything [`Supervisor::add_worker`][crate::runtime::Supervisor::add_worker] accepts: a bare
38/// [`Supervisable`][crate::runtime::Supervisable], a [`Supervisor`][crate::runtime::Supervisor] to run as a nested
39/// supervision subtree, or a [`ChildSpecification`] configured in detail. The child is hosted, unless the specification
40/// says otherwise. See [`ChildBuilder`][crate::runtime::ChildBuilder] for what that means.
41///
42/// This function has the same shape and guarantees as [`tokio::spawn`]. It always succeeds, but success means only that
43/// the child was accepted. It does not guarantee that the child runs. If an owner stops before it gets to the queued
44/// child, it never starts that child.
45///
46/// # Panics
47///
48/// Panics if there is no current scope and no ambient supervisor. This means that the caller does not run as a
49/// supervised process, or in a supervised process. Establish an ambient supervisor with [`SupervisorHandle::scope`], or
50/// spawn through a [`SupervisorHandle`] directly.
51///
52/// # Examples
53///
54/// ```no_run
55/// # use saluki_core::runtime::{spawn, FnWorker};
56/// # async fn refresh() {}
57/// spawn(FnWorker::new("refresher", refresh()));
58/// ```
59pub fn spawn<S, T>(child: T) -> ChildId
60where
61 S: ChildState,
62 T: Into<ChildSpecification<S>>,
63{
64 match scope::current() {
65 Some(scope) => scope.spawn(child),
66 None => spawn_sibling(child),
67 }
68}
69
70/// Spawns a child onto the ambient supervisor, the nearest supervisor above the current process.
71///
72/// The child belongs to that supervisor, not to the current process. The process does not wait for the child, and does
73/// not stop it.
74///
75/// The child is a sibling of the current process only if a supervisor runs that process directly. A process in a scope
76/// has no supervisor of its own. For such a process, the ambient supervisor is the nearest supervisor above it, and the
77/// child runs beside the worker of that supervisor that the process descends from. For example, everything that a
78/// topology component spawns runs in the scope of the component. Thus, a child that a task of the component spawns with
79/// this function runs under the topology supervisor, beside the components. That supervisor does not restart children,
80/// so if such a child fails and must be restarted, the whole topology stops. Use [`spawn`] unless the child must
81/// outlive the current process.
82///
83/// The child is hosted, unless the specification says otherwise. The child is not restored when the supervisor itself
84/// restarts. See [`SupervisorHandle::spawn`].
85///
86/// # Panics
87///
88/// Panics if there is no ambient supervisor. This means that the caller does not run as a supervised process, or in a
89/// supervised process. Establish an ambient supervisor with [`SupervisorHandle::scope`], or spawn through a
90/// [`SupervisorHandle`] directly.
91pub fn spawn_sibling<S, T>(child: T) -> ChildId
92where
93 S: ChildState,
94 T: Into<ChildSpecification<S>>,
95{
96 CURRENT_SUPERVISOR
97 .try_with(|supervisor| supervisor.spawn(child))
98 .unwrap_or_else(|_| {
99 panic!(
100 "spawned outside of a supervised process: there is no ambient supervisor to spawn on. Spawn through a \
101 `SupervisorHandle` directly, or establish an ambient supervisor with `SupervisorHandle::scope`."
102 )
103 })
104}
105
106impl SupervisorHandle {
107 /// Runs `fut` with this supervisor installed as the ambient supervisor.
108 ///
109 /// Anything `fut` spawns through [`spawn_sibling`] becomes a child of this supervisor. So does anything that `fut`
110 /// spawns through [`spawn`], but only if `fut` runs outside supervision. Code in a supervised process has a current
111 /// scope, and [`spawn`] uses that scope instead of the ambient supervisor. Thus, this function is for code that
112 /// runs outside supervision -- a test driving a component directly, or a task started with [`tokio::spawn`] that
113 /// needs to attach children to a known supervisor.
114 ///
115 /// The ambient supervisor applies only for the duration of `fut`, and shadows any supervisor already installed.
116 pub fn scope<F>(&self, fut: F) -> TaskLocalFuture<SupervisorHandle, F>
117 where
118 F: Future,
119 {
120 CURRENT_SUPERVISOR.scope(self.clone(), fut)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use std::sync::{
127 atomic::{AtomicUsize, Ordering},
128 Arc,
129 };
130 use std::time::Duration;
131
132 use async_trait::async_trait;
133 use saluki_common::sync::shutdown::ShutdownHandle;
134 use tokio::sync::oneshot;
135
136 use super::*;
137 use crate::runtime::{
138 ChildSpecification, FnWorker, InitializationError, ShutdownStrategy, Supervisable, Supervisor, SupervisorError,
139 SupervisorFuture,
140 };
141 use crate::test_support::wait_until;
142
143 /// A worker that runs a caller-supplied action and then waits for shutdown.
144 ///
145 /// `when` decides whether the action runs during initialization or once the worker is running, which is the
146 /// distinction these tests care about: the ambient supervisor has to be in place for both.
147 struct ActionWorker {
148 name: &'static str,
149 during_init: bool,
150 action: std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>>,
151 }
152
153 impl ActionWorker {
154 fn new<F>(name: &'static str, during_init: bool, action: F) -> Self
155 where
156 F: FnOnce() + Send + 'static,
157 {
158 Self {
159 name,
160 during_init,
161 action: std::sync::Mutex::new(Some(Box::new(action))),
162 }
163 }
164
165 fn take_action(&self) -> Box<dyn FnOnce() + Send> {
166 self.action
167 .lock()
168 .expect("action mutex poisoned")
169 .take()
170 .expect("worker should only run once")
171 }
172 }
173
174 #[async_trait]
175 impl Supervisable for ActionWorker {
176 fn name(&self) -> &str {
177 self.name
178 }
179
180 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
181 let action = self.take_action();
182 if self.during_init {
183 action();
184
185 return Ok(Box::pin(async move {
186 process_shutdown.await;
187 Ok(())
188 }));
189 }
190
191 Ok(Box::pin(async move {
192 action();
193 process_shutdown.await;
194 Ok(())
195 }))
196 }
197 }
198
199 /// Wraps an endless worker so the supervisor aborts it at shutdown instead of waiting for a terminal condition
200 /// it doesn't have.
201 fn endless(worker: FnWorker) -> ChildSpecification {
202 ChildSpecification::worker(worker).with_shutdown_strategy(ShutdownStrategy::Brutal)
203 }
204
205 /// Runs a supervisor holding a single worker that performs `action`, then shuts it down and returns the result.
206 ///
207 /// Waits for `action` to have actually run before signalling shutdown. A fixed sleep would work most of the time
208 /// and fail on a loaded runner, which is the whole reason [`wait_until`] exists.
209 async fn run_worker_that<F>(during_init: bool, action: F) -> Result<(), SupervisorError>
210 where
211 F: FnOnce() + Send + 'static,
212 {
213 let acted = Arc::new(AtomicUsize::new(0));
214 let worker_acted = Arc::clone(&acted);
215
216 let mut sup = Supervisor::new("ambient-sup").expect("supervisor name should be valid");
217 sup.add_worker(ActionWorker::new("actor", during_init, move || {
218 action();
219 worker_acted.fetch_add(1, Ordering::SeqCst);
220 }));
221
222 let (shutdown_tx, shutdown_rx) = oneshot::channel();
223 let handle = sup.handle();
224 let run = tokio::spawn(async move { sup.run_with_shutdown(shutdown_rx).await });
225
226 wait_until("supervisor is running", || handle.is_running()).await;
227 wait_until("the worker has run its action", || acted.load(Ordering::SeqCst) == 1).await;
228
229 let _ = shutdown_tx.send(());
230 tokio::time::timeout(Duration::from_secs(5), run)
231 .await
232 .expect("supervisor should stop promptly")
233 .expect("supervisor task should not panic")
234 }
235
236 #[tokio::test]
237 async fn worker_spawns_onto_its_own_supervisor() {
238 // The ambient supervisor of a running worker is the one supervising it, so what it spawns becomes its sibling
239 // -- and is drained when that supervisor stops.
240 let started = Arc::new(AtomicUsize::new(0));
241 let child_started = Arc::clone(&started);
242
243 let result = run_worker_that(false, move || {
244 spawn(endless(FnWorker::new("sibling", async move {
245 child_started.fetch_add(1, Ordering::SeqCst);
246 std::future::pending::<()>().await;
247 })));
248 })
249 .await;
250
251 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
252 assert_eq!(started.load(Ordering::SeqCst), 1, "the spawned sibling should have run");
253 }
254
255 #[tokio::test]
256 async fn worker_can_spawn_during_initialization() {
257 // Initialization runs inside the worker's own task, so the ambient supervisor is already in place there. A
258 // worker that sets up helpers before it starts running shouldn't have to defer them until after.
259 let started = Arc::new(AtomicUsize::new(0));
260 let child_started = Arc::clone(&started);
261
262 let result = run_worker_that(true, move || {
263 spawn(endless(FnWorker::new("helper", async move {
264 child_started.fetch_add(1, Ordering::SeqCst);
265 std::future::pending::<()>().await;
266 })));
267 })
268 .await;
269
270 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
271 assert_eq!(
272 started.load(Ordering::SeqCst),
273 1,
274 "a child spawned during initialization should have run"
275 );
276 }
277
278 #[tokio::test]
279 #[should_panic(expected = "outside of a supervised process")]
280 async fn spawning_without_an_ambient_supervisor_panics() {
281 // Nothing sensible can be done with a child here, and silently dropping it would hide the mistake until
282 // whatever was spawned turned out never to have run.
283 spawn(endless(FnWorker::new("orphan", std::future::pending::<()>())));
284 }
285
286 #[tokio::test]
287 async fn ambient_supervisor_is_not_inherited_by_tokio_spawn() {
288 // A task started with `tokio::spawn` is outside supervision entirely, even when its parent was supervised.
289 // Inheriting the ambient supervisor there would quietly attach children to a supervisor that has no
290 // relationship to the task's actual lifetime.
291 let escaped = Arc::new(AtomicUsize::new(0));
292 let observed = Arc::clone(&escaped);
293
294 let result = run_worker_that(false, move || {
295 tokio::spawn(async move {
296 if CURRENT_SUPERVISOR.try_with(|_| ()).is_err() {
297 observed.fetch_add(1, Ordering::SeqCst);
298 }
299 });
300 })
301 .await;
302
303 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
304 assert_eq!(
305 escaped.load(Ordering::SeqCst),
306 1,
307 "a `tokio::spawn`ed task must not see an ambient supervisor"
308 );
309 }
310
311 #[tokio::test]
312 async fn scope_establishes_an_ambient_supervisor() {
313 // The escape hatch for code that isn't running under supervision: tests, and tasks bridging back into a known
314 // supervisor.
315 let started = Arc::new(AtomicUsize::new(0));
316 let child_started = Arc::clone(&started);
317
318 let mut sup = Supervisor::new("scoped-sup").expect("supervisor name should be valid");
319 // A supervisor with no children at all still idles until shutdown, so nothing else is needed here.
320 let handle = sup.handle();
321 let (shutdown_tx, shutdown_rx) = oneshot::channel();
322 let run = tokio::spawn(async move { sup.run_with_shutdown(shutdown_rx).await });
323 wait_until("supervisor is running", || handle.is_running()).await;
324
325 handle
326 .scope(async {
327 spawn(endless(FnWorker::new("scoped", async move {
328 child_started.fetch_add(1, Ordering::SeqCst);
329 std::future::pending::<()>().await;
330 })));
331 })
332 .await;
333
334 wait_until("the scoped child has started", || started.load(Ordering::SeqCst) == 1).await;
335
336 let _ = shutdown_tx.send(());
337 let result = tokio::time::timeout(Duration::from_secs(5), run)
338 .await
339 .expect("supervisor should stop promptly")
340 .expect("supervisor task should not panic");
341 assert!(result.is_ok(), "supervisor should have stopped cleanly: {result:?}");
342 }
343}