Skip to main content

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}