Skip to main content

saluki_core/runtime/
builder.rs

1//! Builder for describing children.
2//!
3//! [`ChildSpecification`] is the complete description of a child, and this builder is the only way to configure one:
4//! the specification itself carries no public methods. The builder turns an asynchronous function, or a
5//! [`Supervisable`], into a child with sensible defaults, and uses the typestate pattern to expose only the settings
6//! that make sense for the kind of worker being described.
7//!
8//! The builder has an ambient form and an explicit form, one for each of the two ways to spawn. The free functions in
9//! this module ([`worker`], [`supervisable`]) spawn into the current process. The methods with the same names on
10//! [`SupervisorHandle`] spawn onto the supervisor of that handle. In both forms, [`ChildBuilder::spawn`] starts the
11//! child. [`ChildBuilder::build`] gives the child to [`Supervisor::add_worker`][add_worker], and the supervisor starts
12//! the child when it runs.
13//!
14//! [add_worker]: super::Supervisor::add_worker
15//!
16//! # Restart policy
17//!
18//! You do not choose the restart policy of a child directly. The policy follows from two facts: whether the child can
19//! be started again, and whether its owner can work without it. See the [runtime documentation][restart] for how the
20//! policy is derived, and for the defaults.
21//!
22//! [restart]: super#restart-policy
23//!
24//! # Shutdown
25//!
26//! A subtree shutdown is a _trigger_, not an enforcement. A one-shot child built with [`worker`] never receives the
27//! shutdown signal. It runs until it reaches its own terminal condition, and its owner waits for it. Examples of a
28//! terminal condition are an input channel that closes, a loop that finishes, and a request that completes.
29//!
30//! This behavior lets a set of tasks that are connected by channels drain in dependency order. The tasks do not have to
31//! know that order. Because of this behavior, it is not a problem that a one-shot child cannot see the shutdown signal.
32//!
33//! Each child is held to its own shutdown strategy, whether its owner is a supervisor or a scope. A closure-based child
34//! is graceful, with a timeout of five seconds. When a timeout expires, the process is halted along with everything in
35//! its scope.
36//!
37//! Two cases need something other than the default:
38//!
39//! - Work with no terminal condition holds the drain open until its timeout expires. An endless background loop is an
40//!   example. Give it [`ShutdownStrategy::Brutal`] with [`ChildBuilder::with_shutdown_strategy`] so that it is halted
41//!   at once.
42//! - If work must observe shutdown to know that it must stop, or must run cleanup, build it with
43//!   [`worker_with_shutdown`]. The body of that worker receives the shutdown signal. You can also implement
44//!   [`Supervisable`] directly.
45//!
46//! [`ChildBuilder::with_shutdown_timeout`] sets a timeout other than the default. Use it if a particular child needs
47//! more time to drain, or must be halted sooner than its siblings.
48//!
49//! # Children of the current process
50//!
51//! [`ChildBuilder::spawn`] makes a child a child of the current process, and the [scope][super::scope] of that process
52//! holds it. The process does not finish until its children finish. When the body of the process returns, the scope
53//! stops the children, each according to its own shutdown strategy. [`ChildBuilder::spawn_sibling`] gives the child
54//! to the nearest supervisor above the current process instead.
55//!
56//! # Task naming
57//!
58//! Child task names should generally _not_ contain unique patterns/tokens -- such as monotonic IDs or high-cardinality
59//! values -- as they are used for internal telemetry about the task. Generally, task names should be thought of as a
60//! category label: if a supervisor manages tasks for handling connections, it should prefer to name them like
61//! `conn_handler` instead of `conn_handler_<ID or IP>`.
62
63use std::{future::Future, marker::PhantomData, time::Duration};
64
65use saluki_common::sync::shutdown::ShutdownHandle;
66use tokio::runtime::Handle;
67
68#[cfg(any(test, feature = "test-util"))]
69use super::ExitObserver;
70use super::{
71    scope::{self, Edge, Scope, UnadoptedScope},
72    ChildId, ChildSpecification, FnWorker, IntoWorkerResult, RestartType, ShutdownStrategy, Supervisable, Supervisor,
73    SupervisorHandle, SupervisorSpec, WorkerSpec,
74};
75
76/// Creates a builder for a child task of the current process.
77///
78/// The ambient counterpart to [`SupervisorHandle::worker`].
79///
80/// # Panics
81///
82/// [`ChildBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
83pub fn worker<N, Fut>(name: N, fut: Fut) -> ChildBuilder<'static>
84where
85    N: Into<String>,
86    Fut: Future + Send + 'static,
87    Fut::Output: IntoWorkerResult,
88{
89    ChildBuilder::new_one_shot(BuilderTarget::Ambient, FnWorker::new(name, fut))
90}
91
92/// Creates a builder for a child task of the current process, with a body that receives the shutdown signal.
93///
94/// The ambient counterpart to [`SupervisorHandle::worker_with_shutdown`].
95///
96/// # Panics
97///
98/// [`ChildBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
99pub fn worker_with_shutdown<N, F, Fut>(name: N, f: F) -> ChildBuilder<'static>
100where
101    N: Into<String>,
102    F: FnOnce(ShutdownHandle) -> Fut + Send + 'static,
103    Fut: Future + Send + 'static,
104    Fut::Output: IntoWorkerResult,
105{
106    ChildBuilder::new_one_shot(BuilderTarget::Ambient, FnWorker::with_shutdown(name, f))
107}
108
109/// Creates a builder for a supervisable child task of the current process.
110///
111/// The ambient counterpart to [`SupervisorHandle::supervisable`], which documents what a supervisable task is.
112///
113/// # Panics
114///
115/// [`ChildBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
116pub fn supervisable<T>(worker: T) -> ChildBuilder<'static, Restartable>
117where
118    T: Supervisable + 'static,
119{
120    ChildBuilder::new_restartable(BuilderTarget::Ambient, worker)
121}
122
123impl SupervisorHandle {
124    /// Creates a builder for a child task built from a plain future.
125    ///
126    /// The task runs until it reaches its own terminal condition; it is never handed the shutdown signal. See
127    /// [`FnWorker`] for what that means at shutdown, and for the two cases that need something else.
128    ///
129    /// Use this method when advanced configuration of the underlying task is required. Otherwise, prefer
130    /// [`spawn_worker`][Self::spawn_worker].
131    ///
132    /// # Examples
133    ///
134    /// ```no_run
135    /// # use saluki_core::runtime::SupervisorHandle;
136    /// # async fn encode() {}
137    /// # fn example(supervisor: &SupervisorHandle, pool: tokio::runtime::Handle) {
138    /// supervisor.worker("encoder", encode()).on_runtime(pool).spawn();
139    /// # }
140    /// ```
141    pub fn worker<N, Fut>(&self, name: N, fut: Fut) -> ChildBuilder<'_>
142    where
143        N: Into<String>,
144        Fut: Future + Send + 'static,
145        Fut::Output: IntoWorkerResult,
146    {
147        ChildBuilder::new_one_shot(BuilderTarget::Handle(self), FnWorker::new(name, fut))
148    }
149
150    /// Creates a builder for a child task with a body that receives the shutdown signal.
151    ///
152    /// `f` receives the [`ShutdownHandle`] of the task when the task starts. Use this method for work that must observe
153    /// shutdown to stop in time. Such work stops when it observes shutdown, not when it reaches its own terminal
154    /// condition. An example is a long-lived connection that must close gracefully. See [`FnWorker::with_shutdown`].
155    pub fn worker_with_shutdown<N, F, Fut>(&self, name: N, f: F) -> ChildBuilder<'_>
156    where
157        N: Into<String>,
158        F: FnOnce(ShutdownHandle) -> Fut + Send + 'static,
159        Fut: Future + Send + 'static,
160        Fut::Output: IntoWorkerResult,
161    {
162        ChildBuilder::new_one_shot(BuilderTarget::Handle(self), FnWorker::with_shutdown(name, f))
163    }
164
165    /// Creates a builder for a supervisable child task.
166    ///
167    /// Supervisable tasks are those where the worker already implements [`Supervisable`], which lets the builder serve
168    /// as a consistent control surface for spawning both arbitrary asynchronous functions and more full-fledged
169    /// workers.
170    ///
171    /// A supervisable task can usually be started again, so its owner restarts it when it fails. A task that reports
172    /// through [`can_restart`][Supervisable::can_restart] that it cannot be started again is never restarted. See the
173    /// [runtime documentation][super#restart-policy] for how its restart policy is decided.
174    ///
175    /// Use this method when advanced configuration of the underlying task is required. Otherwise, prefer
176    /// [`spawn_supervisable`][Self::spawn_supervisable].
177    pub fn supervisable<T>(&self, worker: T) -> ChildBuilder<'_, Restartable>
178    where
179        T: Supervisable + 'static,
180    {
181        ChildBuilder::new_restartable(BuilderTarget::Handle(self), worker)
182    }
183
184    /// Spawns a child task built from a plain future.
185    ///
186    /// The task runs until it reaches its own terminal condition; it is never handed the shutdown signal. See
187    /// [`FnWorker`] for what that means at shutdown, and for the two cases that need something else.
188    ///
189    /// Use [`worker`][Self::worker] when advanced configuration of the underlying task is required.
190    pub fn spawn_worker<N, Fut>(&self, name: N, fut: Fut) -> ChildId
191    where
192        N: Into<String>,
193        Fut: Future + Send + 'static,
194        Fut::Output: IntoWorkerResult,
195    {
196        self.worker(name, fut).spawn()
197    }
198
199    /// Spawns a supervisable child task.
200    ///
201    /// Supervisable tasks are those where the worker already implements [`Supervisable`], which lets the builder serve
202    /// as a consistent control surface for spawning both arbitrary asynchronous functions and more full-fledged
203    /// workers.
204    ///
205    /// Use [`supervisable`][Self::supervisable] when advanced configuration of the underlying task is required.
206    pub fn spawn_supervisable<T>(&self, worker: T) -> ChildId
207    where
208        T: Supervisable + 'static,
209    {
210        self.supervisable(worker).spawn()
211    }
212
213    /// Creates a builder for a nested supervisor.
214    ///
215    /// You can give a [`Supervisor`] directly to [`spawn`][Self::spawn] or [`Supervisor::add_worker`]. Most callers
216    /// need only that. This builder is for the settings that are not available in that way: whether the subtree is
217    /// needed or hosted, and whether it [stops with its owner][NestedSupervisorBuilder::stops_with_owner] in a scope.
218    ///
219    /// Unlike [`supervisable`][Self::supervisable], there is no placement or shutdown setting. A nested supervisor runs
220    /// wherever its parent does and its children carry their own placement, and its drain is bounded by its children's
221    /// timeouts and by its owner.
222    pub fn nested_supervisor(&self, supervisor: Supervisor) -> NestedSupervisorBuilder<'_> {
223        NestedSupervisorBuilder::new(BuilderTarget::Handle(self), supervisor)
224    }
225}
226
227/// Creates a builder for a nested supervisor spawned into the current process.
228///
229/// The ambient counterpart to [`SupervisorHandle::nested_supervisor`], which documents what the builder is for.
230///
231/// # Panics
232///
233/// [`NestedSupervisorBuilder::spawn`] panics if it is called outside supervision. See [`spawn`][super::spawn].
234pub fn nested_supervisor(supervisor: Supervisor) -> NestedSupervisorBuilder<'static> {
235    NestedSupervisorBuilder::new(BuilderTarget::Ambient, supervisor)
236}
237
238mod sealed {
239    pub trait Sealed {}
240}
241
242/// The kind of worker a [`ChildBuilder`] is describing.
243///
244/// This trait is sealed. It exists only to mark which configuration a builder makes available. [`OneShot`] and
245/// [`Restartable`] implement it.
246pub trait BuilderState: sealed::Sealed {}
247
248/// Marks a builder whose worker can only be started once.
249///
250/// Closure-based children ([`SupervisorHandle::worker`]) consume their body when they start, so they can never be
251/// restarted.
252pub struct OneShot;
253
254/// Marks a builder whose worker can usually be started more than once.
255///
256/// A [`Supervisable`] builds its work in [`initialize`][Supervisable::initialize] each time it starts, so it can be
257/// restarted. A [`Supervisable`] that consumes something when it starts says so through
258/// [`can_restart`][Supervisable::can_restart], and its owner never restarts it.
259pub struct Restartable;
260
261impl sealed::Sealed for OneShot {}
262impl BuilderState for OneShot {}
263impl sealed::Sealed for Restartable {}
264impl BuilderState for Restartable {}
265
266/// Builder for a yet-to-be-started child task.
267///
268/// This is the only way to configure a child: [`ChildSpecification`], which this produces, carries no settings of its
269/// own. The builder uses the typestate pattern to expose only the properties that apply to the child that it describes.
270/// See [`BuilderState`] for the states.
271#[must_use = "a child is only described until `spawn` or `build` is called"]
272pub struct ChildBuilder<'a, S = OneShot> {
273    target: BuilderTarget<'a>,
274    spec: ChildSpecification<WorkerSpec>,
275    _state: PhantomData<S>,
276}
277
278/// The place where a [`ChildBuilder`] spawns its child.
279#[derive(Clone, Copy)]
280enum BuilderTarget<'a> {
281    /// The scope of the current process, or the ambient supervisor if there is no current scope.
282    Ambient,
283
284    /// This specific supervisor.
285    Handle(&'a SupervisorHandle),
286}
287
288impl<'a, S: BuilderState> ChildBuilder<'a, S> {
289    fn new(target: BuilderTarget<'a>, spec: ChildSpecification<WorkerSpec>) -> Self {
290        Self {
291            target,
292            spec,
293            _state: PhantomData,
294        }
295    }
296
297    fn map_spec<F>(self, f: F) -> Self
298    where
299        F: FnOnce(ChildSpecification<WorkerSpec>) -> ChildSpecification<WorkerSpec>,
300    {
301        let Self { target, spec, .. } = self;
302
303        Self::new(target, f(spec))
304    }
305
306    /// Runs this child task on a specific runtime.
307    ///
308    /// Use this method for compute-heavy work that must not compete with the runtime that drives its owner and its I/O.
309    /// Examples are encoding, serialization, and protocol servers. This method changes only where the task runs. The
310    /// owner of the child still supervises, shuts down, and restarts the child.
311    pub fn on_runtime(self, handle: Handle) -> Self {
312        self.map_spec(|spec| spec.with_runtime(handle))
313    }
314
315    /// Sets an explicit shutdown timeout for this child task.
316    ///
317    /// A closure-based child defaults to five seconds, after which it is halted. Set this if a particular child needs
318    /// more time to drain, or must be halted sooner. A timeout longer than its owner's has no effect, since the child
319    /// is halted along with its owner.
320    ///
321    /// For a [`Supervisable`] child, this overrides the strategy the task reports for itself.
322    pub fn with_shutdown_timeout(self, timeout: Duration) -> Self {
323        self.with_shutdown_strategy(ShutdownStrategy::Graceful(timeout))
324    }
325
326    /// Sets the explicit shutdown strategy used for this child task.
327    pub fn with_shutdown_strategy(self, strategy: ShutdownStrategy) -> Self {
328        self.map_spec(|spec| spec.with_shutdown_strategy(strategy))
329    }
330
331    /// Marks this child as one that its owner cannot work without.
332    ///
333    /// If the child stops without a request to stop and can only be started once, its owner also stops and fails. This
334    /// occurs even when the child exits cleanly. Thus, use this method for a child that its owner cannot work without.
335    /// Do not use it for a child that you expect to finish by itself. If the child can be started again, its owner
336    /// restarts it after any such stop, not only after a failure.
337    ///
338    /// For example, one failed connection is usually not a reason to stop a component that handles client connections.
339    /// But a component that forwards work to a child task can become inoperable if that task stops unexpectedly. This
340    /// occurs if the task cannot be attached again to the necessary channels or state unless the component is created
341    /// again.
342    ///
343    /// See the [runtime documentation][super#restart-policy] for the default.
344    pub fn needed(self) -> Self {
345        self.map_spec(|spec| spec.with_edge(Edge::Needed))
346    }
347
348    /// Marks this child as one that its owner runs but does not need.
349    ///
350    /// If the child stops without a request to stop, and its owner does not restart it, the owner reaps the child and
351    /// continues. The owner does not restart a child that can only be started once, or a child that exited cleanly. If
352    /// the child can be started again, its owner restarts it only if it failed, and accepts a clean exit as final. But
353    /// if the child fails more often than the restart limit of its owner allows, the owner also fails, as for a needed
354    /// child.
355    ///
356    /// See the [runtime documentation][super#restart-policy] for the default.
357    pub fn hosted(self) -> Self {
358        self.map_spec(|spec| spec.with_edge(Edge::Hosted))
359    }
360
361    /// Sets whether the supervisor stops when this child task terminates.
362    ///
363    /// This setting has an effect only under a supervisor that uses the deprecated
364    /// [`with_auto_shutdown`][super::Supervisor::with_auto_shutdown]. It also has no effect on a child that its
365    /// supervisor always restarts, such as a child that can start again and that you declare with
366    /// [`Supervisor::add_worker`] without marking it [hosted][Self::hosted]. The supervisor logs a warning for each
367    /// child where the setting has no effect.
368    #[deprecated(note = "mark the child `needed` instead, which stops its owner when it terminates for good")]
369    pub fn with_significant(self, significant: bool) -> Self {
370        self.map_spec(|spec| spec.with_significant(significant))
371    }
372
373    /// Makes this child adopt `scope` as its own and start what was spawned into it before the child started.
374    pub(crate) fn adopting(self, scope: UnadoptedScope) -> Self {
375        self.map_spec(|spec| spec.adopting(scope))
376    }
377
378    /// Gives this child a resource group of its own, with the same name as the child.
379    ///
380    /// The child then does not count against the resource group of its owner. Use this for a worker that represents
381    /// something with its own resource accounting, such as a topology component.
382    pub(crate) fn with_own_resource_group(self) -> Self {
383        self.map_spec(|spec| spec.with_own_resource_group())
384    }
385
386    /// Makes `observer` receive how this child ended, after its scope closed. See [`ExitObserver`].
387    #[cfg(any(test, feature = "test-util"))]
388    pub(crate) fn observing_exit(self, observer: ExitObserver) -> Self {
389        self.map_spec(|spec| spec.observing_exit(observer))
390    }
391
392    /// Finishes describing the child without starting it, for [`Supervisor::add_worker`].
393    ///
394    /// Use this method to register a configured child on a supervisor that has not started yet. The supervisor starts
395    /// this child when it runs. When the supervisor restarts, the child restarts with it. To start a child now, use
396    /// [`spawn`][Self::spawn].
397    ///
398    /// Whichever supervisor this builder was created against is irrelevant here -- the child belongs to whichever one
399    /// it is handed to.
400    ///
401    /// [`Supervisor::add_worker`]: super::Supervisor::add_worker
402    pub fn build(self) -> ChildSpecification<WorkerSpec> {
403        self.spec
404    }
405
406    /// Spawns the child.
407    ///
408    /// A builder that was created from a [`SupervisorHandle`] spawns the child onto that supervisor. Other builders
409    /// make the child a child of the current process, and the [scope][super::scope] of that process holds it. See
410    /// [`spawn`][super::spawn].
411    ///
412    /// Returns the [`ChildId`] of the child. This method queues the child and does not start it synchronously. Thus,
413    /// when this method returns, the child can still be in the queue. If its owner does not run, or stops before it
414    /// processes the child, the child never runs.
415    ///
416    /// # Panics
417    ///
418    /// Panics if this builder does not target a specific supervisor and code outside supervision calls this method. See
419    /// [`spawn`][super::spawn].
420    pub fn spawn(self) -> ChildId {
421        let Self { target, spec, .. } = self;
422
423        match target {
424            BuilderTarget::Ambient => super::spawn(spec),
425            BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
426        }
427    }
428
429    /// Spawns the child onto the ambient supervisor, the nearest supervisor above the current process.
430    ///
431    /// The child is a sibling of the current process only if a supervisor runs that process directly. For a process
432    /// that runs in a scope, the child goes to the nearest supervisor above it instead. See
433    /// [`spawn_sibling`][super::spawn_sibling].
434    ///
435    /// A builder that was created from a [`SupervisorHandle`] spawns the child onto that supervisor, exactly as
436    /// [`spawn`][Self::spawn] does.
437    ///
438    /// # Panics
439    ///
440    /// Panics if this builder does not target a specific supervisor and there is no ambient supervisor.
441    pub fn spawn_sibling(self) -> ChildId {
442        let Self { target, spec, .. } = self;
443
444        match target {
445            BuilderTarget::Ambient => super::spawn_sibling(spec),
446            BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
447        }
448    }
449
450    /// Spawns the child into `scope`.
451    pub fn spawn_in(self, scope: &Scope) -> ChildId {
452        let Self { spec, .. } = self;
453
454        scope.spawn(spec)
455    }
456
457    /// Spawns the child into the scope of the current process, or as a detached task outside supervision.
458    ///
459    /// Returns the [`ChildId`] of the child, or `None` if there was no current scope and the child started as a
460    /// detached task instead. A detached child never receives a shutdown signal, and nothing restarts it or waits for
461    /// it. This is how the same work runs without supervision at all. Thus, this method is the correct choice for code
462    /// that can be called from any location, such as a primitive that a test can construct.
463    pub fn spawn_or_detached(self) -> Option<ChildId> {
464        let Self { spec, .. } = self;
465
466        match scope::current() {
467            Some(scope) => Some(scope.spawn(spec)),
468            None => {
469                scope::spawn_detached(spec, ShutdownHandle::noop());
470                None
471            }
472        }
473    }
474}
475
476impl<'a> ChildBuilder<'a, OneShot> {
477    fn new_one_shot<T>(target: BuilderTarget<'a>, worker: T) -> Self
478    where
479        T: Supervisable + 'static,
480    {
481        let spec = ChildSpecification::worker(worker);
482
483        Self::new(target, spec)
484    }
485}
486
487impl<'a> ChildBuilder<'a, Restartable> {
488    fn new_restartable<T>(target: BuilderTarget<'a>, worker: T) -> Self
489    where
490        T: Supervisable + 'static,
491    {
492        Self::new(target, ChildSpecification::worker(worker))
493    }
494
495    /// Restarts this child task only when it terminates abnormally.
496    #[deprecated(note = "mark the child `hosted` instead, which restarts it only if it fails")]
497    pub fn transient(self) -> Self {
498        self.map_spec(|spec| spec.with_restart_type(RestartType::Transient))
499    }
500
501    /// Never restarts this child task.
502    #[deprecated(note = "implement `Supervisable::can_restart` instead, if the child can only be started once")]
503    pub fn temporary(self) -> Self {
504        self.map_spec(|spec| spec.with_restart_type(RestartType::Temporary))
505    }
506}
507
508/// Builder for a yet-to-be-started nested supervisor.
509///
510/// The counterpart to [`ChildBuilder`] for a child that is itself a [`Supervisor`], and deliberately a much smaller
511/// surface. A nested supervisor has no placement of its own, because it runs where its parent runs and its children
512/// carry their own placement. It also has no shutdown deadline of its own, because it limits its own drain through its
513/// children. Thus, the settings that remain are whether it is needed or hosted, and whether it stops with its owner.
514#[must_use = "a child is only described until `spawn` or `build` is called"]
515pub struct NestedSupervisorBuilder<'a> {
516    target: BuilderTarget<'a>,
517    spec: ChildSpecification<SupervisorSpec>,
518}
519
520impl<'a> NestedSupervisorBuilder<'a> {
521    fn new(target: BuilderTarget<'a>, supervisor: Supervisor) -> Self {
522        Self {
523            target,
524            spec: ChildSpecification::from(supervisor),
525        }
526    }
527
528    fn map_spec<F>(self, f: F) -> Self
529    where
530        F: FnOnce(ChildSpecification<SupervisorSpec>) -> ChildSpecification<SupervisorSpec>,
531    {
532        let Self { target, spec } = self;
533
534        Self { target, spec: f(spec) }
535    }
536
537    /// Marks this subtree as one that its owner cannot work without.
538    ///
539    /// See [`ChildBuilder::needed`], which this method mirrors.
540    pub fn needed(self) -> Self {
541        self.map_spec(|spec| spec.with_edge(Edge::Needed))
542    }
543
544    /// Marks this subtree as one that its owner runs but does not need.
545    ///
546    /// See [`ChildBuilder::hosted`], which this method mirrors.
547    pub fn hosted(self) -> Self {
548        self.map_spec(|spec| spec.with_edge(Edge::Hosted))
549    }
550
551    /// Tells this subtree to stop as soon as its owner is told to stop, instead of when the owner's scope closes.
552    ///
553    /// Use this for ingress whose output the owner's body consumes, such as a server whose handlers feed a channel that
554    /// the body drains. The server stops accepting work and finishes what it accepted while the body keeps draining.
555    ///
556    /// This takes effect only in the scope of a worker that takes the shutdown signal (see
557    /// [`Supervisable::wants_shutdown_signal`]). A supervisor tells all of its children to stop at once anyway, and a
558    /// worker that takes no signal has nothing to stop the subtree with.
559    ///
560    /// The scope keeps the subtree's specification until the subtree exits, and the specification holds whatever the
561    /// subtree was built with, such as the sender in a request handler. Thus, the channel closes once the subtree exits.
562    /// The body **MUST NOT** hold a sender of that channel itself, or the channel never closes. See the
563    /// [scope documentation][super::scope#nested-supervisors-that-stop-with-their-owner].
564    pub fn stops_with_owner(self) -> Self {
565        self.map_spec(|spec| spec.stopping_with_owner())
566    }
567
568    /// Restarts this subtree only when it terminates abnormally.
569    #[deprecated(note = "mark the subtree `hosted` instead, which restarts it only if it fails")]
570    pub fn transient(self) -> Self {
571        self.map_spec(|spec| spec.with_restart_type(RestartType::Transient))
572    }
573
574    /// Never restarts this subtree.
575    #[deprecated(note = "a supervisor can always be started again; mark it `hosted` or `needed` instead")]
576    pub fn temporary(self) -> Self {
577        self.map_spec(|spec| spec.with_restart_type(RestartType::Temporary))
578    }
579
580    /// Sets whether the parent supervisor stops when this subtree terminates.
581    #[deprecated(note = "mark the subtree `needed` instead, which stops its owner when it terminates for good")]
582    pub fn with_significant(self, significant: bool) -> Self {
583        self.map_spec(|spec| spec.with_significant(significant))
584    }
585
586    /// Finishes describing the child without starting it, for [`Supervisor::add_worker`].
587    ///
588    /// Use this method to register a configured subtree on a supervisor that has not started yet. To start the subtree
589    /// now, use [`spawn`][Self::spawn].
590    ///
591    /// Whichever supervisor this builder was created against is irrelevant here -- the child belongs to whichever one
592    /// it is handed to.
593    pub fn build(self) -> ChildSpecification<SupervisorSpec> {
594        self.spec
595    }
596
597    /// Spawns the nested supervisor.
598    ///
599    /// This method works as [`ChildBuilder::spawn`] does. It spawns onto the target supervisor if the builder has one.
600    /// If not, it spawns the nested supervisor as a child of the current process.
601    ///
602    /// # Panics
603    ///
604    /// Panics if this builder does not target a specific supervisor and code outside supervision calls this method. See
605    /// [`spawn`][super::spawn].
606    pub fn spawn(self) -> ChildId {
607        let Self { target, spec } = self;
608
609        match target {
610            BuilderTarget::Ambient => super::spawn(spec),
611            BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
612        }
613    }
614
615    /// Spawns the nested supervisor onto the ambient supervisor, the nearest supervisor above the current process.
616    ///
617    /// This method works as [`ChildBuilder::spawn_sibling`] does.
618    ///
619    /// # Panics
620    ///
621    /// Panics if this builder does not target a specific supervisor and there is no ambient supervisor.
622    pub fn spawn_sibling(self) -> ChildId {
623        let Self { target, spec } = self;
624
625        match target {
626            BuilderTarget::Ambient => super::spawn_sibling(spec),
627            BuilderTarget::Handle(supervisor) => supervisor.spawn(spec),
628        }
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use std::sync::{
635        atomic::{AtomicUsize, Ordering},
636        Arc, Mutex,
637    };
638    use std::thread::ThreadId;
639
640    use async_trait::async_trait;
641    use saluki_common::sync::shutdown::ShutdownHandle;
642    use saluki_metrics::test::TestRecorder;
643    use tokio::sync::oneshot;
644    use tokio::time::timeout;
645
646    use super::*;
647    use crate::runtime::test_util::TestSupervisor;
648    use crate::runtime::{InitializationError, SupervisorError, SupervisorFuture};
649    use crate::test_support::wait_until;
650
651    /// How long the drain-participating child in these tests takes to finish after its input closes.
652    ///
653    /// Long enough to tell a completed drain apart from a halt.
654    const DRAIN_DURATION: Duration = Duration::from_millis(300);
655
656    /// A hand-written [`Supervisable`] that records where and how often it was initialized, then runs until shutdown.
657    struct CountingWorker {
658        name: &'static str,
659        initializations: Arc<AtomicUsize>,
660        thread_id: Arc<Mutex<Option<ThreadId>>>,
661    }
662
663    impl CountingWorker {
664        fn new(name: &'static str) -> (Self, Arc<AtomicUsize>, Arc<Mutex<Option<ThreadId>>>) {
665            let thread_id = Arc::new(Mutex::new(None));
666            let initializations = Arc::new(AtomicUsize::new(0));
667
668            (
669                Self {
670                    name,
671                    initializations: Arc::clone(&initializations),
672                    thread_id: Arc::clone(&thread_id),
673                },
674                initializations,
675                thread_id,
676            )
677        }
678    }
679
680    #[async_trait]
681    impl Supervisable for CountingWorker {
682        fn name(&self) -> &str {
683            self.name
684        }
685
686        fn shutdown_strategy(&self) -> ShutdownStrategy {
687            ShutdownStrategy::Graceful(Duration::MAX)
688        }
689
690        async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
691            self.initializations.fetch_add(1, Ordering::SeqCst);
692            *self.thread_id.lock().unwrap() = Some(std::thread::current().id());
693
694            Ok(Box::pin(async move {
695                process_shutdown.await;
696                Ok(())
697            }))
698        }
699    }
700
701    /// A [`Supervisable`] that fails its first run and waits for shutdown on every run after.
702    struct FailingOnceWorker {
703        initializations: Arc<AtomicUsize>,
704        restartable: bool,
705    }
706
707    impl FailingOnceWorker {
708        fn new() -> (Self, Arc<AtomicUsize>) {
709            let initializations = Arc::new(AtomicUsize::new(0));
710            (
711                Self {
712                    initializations: Arc::clone(&initializations),
713                    restartable: true,
714                },
715                initializations,
716            )
717        }
718
719        /// Makes this worker report that it cannot be started again, as a worker that consumes its state does.
720        fn one_shot(mut self) -> Self {
721            self.restartable = false;
722            self
723        }
724    }
725
726    #[async_trait]
727    impl Supervisable for FailingOnceWorker {
728        fn name(&self) -> &str {
729            "failing_once"
730        }
731
732        fn shutdown_strategy(&self) -> ShutdownStrategy {
733            ShutdownStrategy::Graceful(Duration::MAX)
734        }
735
736        fn can_restart(&self) -> bool {
737            self.restartable
738        }
739
740        async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
741            let first_run = self.initializations.fetch_add(1, Ordering::SeqCst) == 0;
742
743            Ok(Box::pin(async move {
744                if first_run {
745                    return Err(saluki_error::generic_error!("first run always fails"));
746                }
747
748                process_shutdown.await;
749                Ok(())
750            }))
751        }
752    }
753
754    #[tokio::test]
755    async fn worker_drains_to_its_terminal_condition_before_the_supervisor_stops() {
756        // The central promise of a one-shot worker: shutdown is a trigger, not an enforcement, so a worker that is
757        // still finishing when the drain starts gets to finish. A clean supervisor result is the assertion that
758        // matters -- `ShutdownTimedOut` would mean it was aborted instead.
759        let supervisor = TestSupervisor::start("test_component").await;
760
761        // The child does real work *after* its input closes, which a premature halt would cut short.
762        let drained = Arc::new(AtomicUsize::new(0));
763        let child_drained = Arc::clone(&drained);
764        let (input_tx, input_rx) = oneshot::channel::<()>();
765
766        supervisor.handle().spawn_worker("drainer", async move {
767            let _ = input_rx.await;
768            tokio::time::sleep(DRAIN_DURATION).await;
769            child_drained.fetch_add(1, Ordering::SeqCst);
770        });
771
772        supervisor.wait_for_children(1).await;
773
774        // Close the child's input, then immediately shut down while it is still draining.
775        drop(input_tx);
776        let result = supervisor.shutdown().await;
777        assert!(
778            result.is_ok(),
779            "child should have drained rather than been aborted: {result:?}"
780        );
781        assert_eq!(drained.load(Ordering::SeqCst), 1);
782    }
783
784    #[tokio::test]
785    async fn worker_without_a_terminal_condition_is_halted_at_once_when_marked_brutal() {
786        // The counterpart: work that never ends has to say so, otherwise it holds the drain open until its owner's
787        // timeout. `Brutal` is how it says so, and the shutdown is still clean.
788        let supervisor = TestSupervisor::start_with_worker_timeout("test_component", Duration::from_secs(30)).await;
789
790        supervisor.spawn_worker("component", |shutdown| async move {
791            crate::runtime::worker("endless", std::future::pending::<()>())
792                .with_shutdown_strategy(ShutdownStrategy::Brutal)
793                .spawn();
794            shutdown.await;
795        });
796        supervisor.wait_for_running_descendants(2).await;
797
798        let started = tokio::time::Instant::now();
799        let result = supervisor.shutdown().await;
800        let elapsed = started.elapsed();
801
802        assert!(
803            result.is_ok(),
804            "halting a brutal child is not an unclean shutdown: {result:?}"
805        );
806        assert!(
807            elapsed < Duration::from_secs(5),
808            "the child should have been halted at once rather than held to the component's 30s timeout; took {elapsed:?}"
809        );
810    }
811
812    #[tokio::test]
813    async fn endless_worker_is_halted_with_the_component_it_belongs_to() {
814        // The child keeps its default five-second timeout, but the component's shorter timeout halts it first.
815        let supervisor = TestSupervisor::start_with_worker_timeout("test_component", Duration::from_millis(200)).await;
816
817        supervisor.spawn_worker("component", |shutdown| async move {
818            crate::runtime::worker("stuck", std::future::pending::<()>()).spawn();
819            shutdown.await;
820        });
821        supervisor.wait_for_running_descendants(2).await;
822
823        let started = tokio::time::Instant::now();
824        let result = supervisor.shutdown().await;
825        let elapsed = started.elapsed();
826
827        assert!(
828            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
829            "the stuck child should have been halted with the component, and counted, got {result:?}"
830        );
831        assert!(
832            elapsed < Duration::from_secs(1),
833            "the child should have been halted with the component at 200ms rather than at its own timeout; took {elapsed:?}"
834        );
835    }
836
837    #[tokio::test]
838    async fn on_runtime_places_the_child_on_that_runtime() {
839        // `on_runtime` must actually change where the child's task runs, not just where it was spawned from.
840        let pool = tokio::runtime::Builder::new_multi_thread()
841            .worker_threads(1)
842            .thread_name("builder-pool-test")
843            .enable_all()
844            .build()
845            .expect("should build pool");
846
847        let supervisor = TestSupervisor::start("test_component").await;
848
849        let (thread_tx, thread_rx) = oneshot::channel();
850        supervisor
851            .handle()
852            .worker("pooled", async move {
853                let _ = thread_tx.send(std::thread::current().name().unwrap_or_default().to_string());
854                std::future::pending::<()>().await;
855            })
856            .on_runtime(pool.handle().clone())
857            .with_shutdown_strategy(ShutdownStrategy::Brutal)
858            .spawn();
859
860        let thread_name = timeout(Duration::from_secs(5), thread_rx)
861            .await
862            .expect("child should report its thread promptly")
863            .expect("child should not be dropped before reporting");
864        assert!(
865            thread_name.starts_with("builder-pool-test"),
866            "child must run on the given runtime, but ran on thread {thread_name:?}"
867        );
868
869        assert!(supervisor.shutdown().await.is_ok());
870        pool.shutdown_background();
871    }
872
873    #[tokio::test]
874    async fn child_exiting_does_not_shut_the_supervisor_down() {
875        // Spawned children are hosted. Thus, a child that finishes must not stop the supervisor, because that also
876        // stops the component. During a drain, a child that finishes is the normal case.
877        let supervisor = TestSupervisor::start("test_component").await;
878        let handle = supervisor.handle();
879
880        // Wait for the child to have actually run and exited. `wait_for_children(0)` would be vacuously true before
881        // the supervisor ever picked it up, so the exit this test is about would never be observed.
882        let exited = Arc::new(AtomicUsize::new(0));
883        let child_exited = Arc::clone(&exited);
884        handle.spawn_worker("brief", async move {
885            child_exited.fetch_add(1, Ordering::SeqCst);
886        });
887        wait_until("the child has run and exited", || exited.load(Ordering::SeqCst) == 1).await;
888        supervisor.wait_for_children(0).await;
889
890        // The supervisor survived that exit: it is still running and still accepting work.
891        assert!(handle.is_running());
892        handle
893            .worker("second", std::future::pending::<()>())
894            .with_shutdown_strategy(ShutdownStrategy::Brutal)
895            .spawn();
896
897        assert!(supervisor.shutdown().await.is_ok());
898    }
899
900    #[tokio::test]
901    async fn a_needed_child_exiting_stops_the_supervisor() {
902        // The counterpart: when a child that is marked needed terminates permanently, its supervisor also stops. Here
903        // the child exits cleanly. It is not obvious that a clean exit also stops the supervisor.
904        let supervisor = TestSupervisor::start("test_component").await;
905        let handle = supervisor.handle();
906
907        handle.worker("brief", async {}).needed().spawn();
908
909        // Spawning only queues the child, so wait for the supervisor to actually stop itself rather than racing our
910        // own shutdown against the child ever starting.
911        wait_until("the supervisor has stopped", || !handle.is_running()).await;
912
913        let result = supervisor.shutdown().await;
914        assert!(
915            matches!(result, Err(SupervisorError::SignificantChildExited)),
916            "a needed child's exit should have stopped the supervisor, got {result:?}"
917        );
918    }
919
920    #[tokio::test]
921    async fn spawned_children_record_poll_metrics() {
922        // Every supervised worker's task is timed, and a child spawned through the builder is no exception. The tag is
923        // the child's fully qualified process name, which is what gives one series per name rather than per task.
924        let recorder = TestRecorder::default();
925        let _guard = metrics::set_default_local_recorder(&recorder);
926
927        // The recorder must be installed before the child is spawned: metric handles are resolved once, at spawn.
928        let supervisor = TestSupervisor::start("metrics_component").await;
929
930        // Spawning only queues the child, so wait for it to actually run before shutting down -- otherwise there's no
931        // guarantee it was ever polled, and therefore none that it recorded anything.
932        let (ran_tx, ran_rx) = oneshot::channel();
933        supervisor.handle().spawn_worker("instrumented", async move {
934            let _ = ran_tx.send(());
935        });
936        ran_rx.await.expect("child should have run");
937
938        assert!(supervisor.shutdown().await.is_ok());
939
940        let polls = recorder.counter((
941            "runtime_task_poll_count",
942            &[("task_name", "metrics_component.instrumented")],
943        ));
944        assert!(
945            polls.is_some_and(|polls| polls > 0),
946            "spawned child should have recorded poll metrics, got {polls:?}"
947        );
948    }
949
950    #[tokio::test]
951    async fn supervisable_child_can_be_configured_before_spawning() {
952        // A `Supervisable` child goes through the same builder, including runtime placement.
953        let worker_pool_thread_id = Arc::new(Mutex::new(None));
954        let worker_pool_thread_id2 = Arc::clone(&worker_pool_thread_id);
955
956        let pool = tokio::runtime::Builder::new_multi_thread()
957            .worker_threads(1)
958            .on_thread_start(move || {
959                worker_pool_thread_id2
960                    .lock()
961                    .unwrap()
962                    .replace(std::thread::current().id());
963            })
964            .enable_all()
965            .build()
966            .expect("should build pool");
967
968        let supervisor = TestSupervisor::start("test_component").await;
969
970        let (worker, initializations, worker_thread_id) = CountingWorker::new("counting");
971        supervisor
972            .handle()
973            .supervisable(worker)
974            .on_runtime(pool.handle().clone())
975            .spawn();
976
977        // `spawn` returns once the child is queued, and `initialize` runs inside the child's own task -- on the pool's
978        // runtime here -- so wait for it rather than assuming it has been polled.
979        supervisor.wait_for_children(1).await;
980        wait_until("the worker has initialized once", || {
981            initializations.load(Ordering::SeqCst) == 1
982        })
983        .await;
984
985        // Assert where it actually ran, not just that it ran: without this, `on_runtime` could be a no-op and the test
986        // would still pass.
987        let worker_pool_thread_id = worker_pool_thread_id
988            .lock()
989            .unwrap()
990            .expect("worker pool thread should have recorded its thread ID");
991        let worker_thread_id = worker_thread_id
992            .lock()
993            .unwrap()
994            .expect("worker should have recorded its thread ID");
995        assert_eq!(
996            worker_pool_thread_id, worker_thread_id,
997            "child should have run on the given runtime"
998        );
999
1000        assert!(supervisor.shutdown().await.is_ok());
1001        pool.shutdown_background();
1002    }
1003
1004    #[tokio::test]
1005    async fn supervisable_children_are_restarted_by_default() {
1006        let supervisor = TestSupervisor::start("test_component").await;
1007
1008        let (worker, initializations) = FailingOnceWorker::new();
1009        supervisor.handle().spawn_supervisable(worker);
1010
1011        // The worker fails its first run, but then runs forever after that, so we should observe two initializations
1012        // and no more after that.
1013        wait_until("the worker has initialized twice", || {
1014            initializations.load(Ordering::SeqCst) == 2
1015        })
1016        .await;
1017        assert!(supervisor.shutdown().await.is_ok());
1018    }
1019
1020    #[tokio::test]
1021    #[allow(deprecated)]
1022    async fn a_worker_that_cannot_restart_overrides_an_explicit_restart_policy() {
1023        // A worker that reports that it cannot start again is never restarted by its owner. This is also true if the
1024        // deprecated `transient` gave it an explicit policy that restarts it after a failure.
1025        let supervisor = TestSupervisor::start("test_component").await;
1026
1027        let (worker, initializations) = FailingOnceWorker::new();
1028        supervisor.handle().supervisable(worker.one_shot()).transient().spawn();
1029
1030        // The child starts and fails. If the supervisor restarted it, the child would stay registered, because each
1031        // later run waits for shutdown.
1032        wait_until("the worker has initialized once", || {
1033            initializations.load(Ordering::SeqCst) == 1
1034        })
1035        .await;
1036        supervisor.wait_for_children(0).await;
1037        assert_eq!(initializations.load(Ordering::SeqCst), 1);
1038
1039        assert!(supervisor.shutdown().await.is_ok());
1040    }
1041
1042    #[tokio::test]
1043    #[allow(deprecated)]
1044    async fn a_significant_child_that_is_always_restarted_is_accepted() {
1045        // The deprecated `with_significant` is available on the builder of a child that can start again. Declared in
1046        // advance, such a child is needed and permanent, so the flag has no effect. The supervisor warns about that,
1047        // but must accept the child and run it.
1048        let (worker, initializations, _) = CountingWorker::new("counting");
1049        let mut sup = Supervisor::new("test_sup").expect("supervisor name should be valid");
1050        sup.add_worker(supervisable(worker).with_significant(true).build());
1051
1052        let (tx, rx) = oneshot::channel::<()>();
1053        let run = tokio::spawn(async move {
1054            sup.run_with_shutdown(async move {
1055                let _ = rx.await;
1056            })
1057            .await
1058        });
1059        wait_until("the worker has initialized once", || {
1060            initializations.load(Ordering::SeqCst) == 1
1061        })
1062        .await;
1063
1064        tx.send(()).expect("supervisor should still be running");
1065        let result = run.await.expect("supervisor task should not panic");
1066        assert!(result.is_ok(), "supervisor should stop cleanly: {result:?}");
1067    }
1068
1069    #[tokio::test]
1070    async fn spawning_after_shutdown_never_starts_the_child() {
1071        let supervisor = TestSupervisor::start("test_component").await;
1072        let handle = supervisor.handle();
1073        assert!(supervisor.shutdown().await.is_ok());
1074
1075        let started = Arc::new(AtomicUsize::new(0));
1076        let child_started = Arc::clone(&started);
1077        handle.spawn_worker("late", async move {
1078            child_started.fetch_add(1, Ordering::SeqCst);
1079        });
1080
1081        tokio::time::sleep(Duration::from_millis(50)).await;
1082        assert_eq!(
1083            started.load(Ordering::SeqCst),
1084            0,
1085            "a child spawned against a stopped supervisor must never run"
1086        );
1087    }
1088}