saluki_core/runtime/
supervisor.rs

1use std::{
2    future::Future,
3    pin::Pin,
4    sync::{
5        atomic::{AtomicU64, AtomicUsize, Ordering},
6        Arc, Mutex,
7    },
8    time::Duration,
9};
10
11use async_trait::async_trait;
12use saluki_common::collections::FastHashMap;
13use saluki_common::sync::shutdown::ShutdownHandle;
14use saluki_error::GenericError;
15use snafu::{OptionExt as _, Snafu};
16use tokio::{pin, runtime::Handle, select, sync::mpsc};
17use tracing::{debug, error, warn};
18
19use super::{
20    dedicated::{spawn_dedicated_runtime, RuntimeConfiguration, RuntimeMode},
21    restart::{RestartAction, RestartMode, RestartState, RestartStrategy, RestartType},
22    worker_state::WorkerState,
23};
24use crate::runtime::{
25    process::{Process, ProcessExt as _},
26    state::DataspaceRegistry,
27};
28
29/// Process name segment used for a child whose own name can't be turned into a valid process name.
30///
31/// See [`SupervisedChild::create_process`].
32const UNNAMED_CHILD: &str = "unnamed";
33
34/// A `Future` that represents the execution of a supervised process.
35pub type SupervisorFuture = Pin<Box<dyn Future<Output = Result<(), GenericError>> + Send>>;
36
37/// A `Future` that represents the full lifecycle of a worker, including initialization.
38///
39/// Unlike [`SupervisorFuture`], which only represents the runtime phase, this future first performs async
40/// initialization and then runs the worker. This allows initialization to happen concurrently when multiple workers are
41/// spawned, and keeps the supervisor loop responsive to shutdown signals during initialization.
42pub(super) type WorkerFuture = Pin<Box<dyn Future<Output = Result<(), WorkerError>> + Send>>;
43
44/// Worker lifecycle errors.
45///
46/// Distinguishes between initialization failures (which shouldn't trigger restart logic) and runtime failures (which
47/// are eligible for restart).
48#[derive(Debug)]
49pub(super) enum WorkerError {
50    /// The worker failed during async initialization.
51    ///
52    /// The optional `child_name` carries the name of the original failing child when the error originates from a
53    /// nested supervisor. This allows the parent to include it in its own `FailedToInitialize` error for better
54    /// diagnostics across supervision tree levels.
55    Initialization {
56        child_name: Option<String>,
57        source: InitializationError,
58    },
59
60    /// The worker failed during runtime execution.
61    Runtime(GenericError),
62
63    /// The worker was a nested supervisor that completed a requested shutdown after forcefully aborting one or more of
64    /// its own workers.
65    ///
66    /// Carried as a distinct variant (rather than collapsed into [`Runtime`][WorkerError::Runtime]) so the parent's
67    /// shutdown drain can recover the structured count and merge it into its own tally, aggregating forced aborts up
68    /// the supervision tree.
69    ShutdownTimedOut {
70        /// The number of workers the nested supervisor forcefully aborted, summed across its own supervision tree.
71        aborted: usize,
72    },
73}
74
75impl From<SupervisorError> for WorkerError {
76    fn from(err: SupervisorError) -> Self {
77        match err {
78            // Propagate initialization failures so the parent supervisor does NOT attempt to restart.
79            // Preserve the original child name so the parent can include it in diagnostics.
80            SupervisorError::FailedToInitialize { child_name, source } => WorkerError::Initialization {
81                child_name: Some(child_name),
82                source,
83            },
84            // Preserve the structured abort count so the parent can merge it into its own shutdown tally.
85            SupervisorError::ShutdownTimedOut { aborted } => WorkerError::ShutdownTimedOut { aborted },
86            // All other supervisor errors (shutdown, no children, invalid name) are runtime-level.
87            other => WorkerError::Runtime(other.into()),
88        }
89    }
90}
91
92/// Process errors.
93#[derive(Debug, Snafu)]
94pub enum ProcessError {
95    /// The child process was aborted by the supervisor.
96    #[snafu(display("Child process was aborted by the supervisor."))]
97    Aborted,
98
99    /// The child process panicked.
100    #[snafu(display("Child process panicked."))]
101    Panicked,
102
103    /// The child process terminated with an error.
104    #[snafu(display("Child process terminated with an error: {}", source))]
105    Terminated {
106        /// The error that caused the termination.
107        source: GenericError,
108    },
109}
110
111/// Initialization errors.
112///
113/// Initialization errors are distinct from runtime errors: they indicate that a process couldn't be started at all
114/// (for example, failed to bind a port, missing configuration). These errors don't trigger restart logic; instead, they
115/// immediately propagate up and fail the supervisor.
116#[derive(Debug, Snafu)]
117#[snafu(context(suffix(false)))]
118pub enum InitializationError {
119    /// The process couldn't be initialized due to an error.
120    #[snafu(display("Process failed to initialize: {}", source))]
121    Failed {
122        /// The underlying error that caused initialization to fail.
123        source: GenericError,
124    },
125}
126
127impl From<GenericError> for InitializationError {
128    fn from(source: GenericError) -> Self {
129        Self::Failed { source }
130    }
131}
132
133/// Strategy for shutting down a process.
134#[derive(Clone, Copy, Debug)]
135pub enum ShutdownStrategy {
136    /// Waits for the configured duration for the process to exit, and then forcefully aborts it otherwise.
137    Graceful(Duration),
138
139    /// Forcefully aborts the process without waiting.
140    Brutal,
141}
142
143/// Policy for automatically shutting a supervisor down based on the termination of its _significant_ children.
144///
145/// A significant child (see [`ChildBuilder::with_significant`][crate::runtime::ChildBuilder::with_significant]) is one whose termination -- when it isn't restarted -- can
146/// drive the supervisor to shut down. This mirrors Erlang/OTP's `auto_shutdown` supervisor flag, and is how an
147/// unexpected (or intentional) child exit cascades into the supervisor stopping, and thus propagating up the tree,
148/// without that child being restarted.
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
150pub enum AutoShutdown {
151    /// Never shut down automatically; significant children have no special effect. This is the default.
152    #[default]
153    Never,
154
155    /// Shut down as soon as _any_ significant child terminates without being restarted.
156    AnySignificant,
157
158    /// Shut down once _all_ significant children have terminated without being restarted.
159    AllSignificant,
160}
161
162/// A supervisable process.
163#[async_trait]
164pub trait Supervisable: Send + Sync {
165    /// Returns the name of the process.
166    fn name(&self) -> &str;
167
168    /// Returns the shutdown strategy for the process.
169    fn shutdown_strategy(&self) -> ShutdownStrategy {
170        ShutdownStrategy::Graceful(Duration::from_secs(5))
171    }
172
173    /// Returns whether this process observes the shutdown signal it is given.
174    ///
175    /// Shutting a subtree down is a _trigger_, not an enforcement: many workers ignore the signal entirely and stop
176    /// only when they reach their own terminal condition, such as an input channel closing. Reporting `false` lets the
177    /// supervisor skip creating a shutdown coordinator it would never usefully fire, and hand the process a
178    /// [`ShutdownHandle::noop`] instead.
179    ///
180    /// This says nothing about _whether_ the supervisor waits for the process -- that's
181    /// [`shutdown_strategy`][Self::shutdown_strategy]. A process that ignores the signal is still waited for, up to
182    /// whatever deadline applies to it.
183    ///
184    /// Defaults to `true`.
185    fn wants_shutdown_signal(&self) -> bool {
186        true
187    }
188
189    /// Initializes the process asynchronously.
190    ///
191    /// During initialization, any resources or configuration for the process can be created asynchronously, and the
192    /// same runtime that's used for running the process is used for initialization. The resulting future is expected to
193    /// complete as soon as reasonably possible after `shutdown` resolves.
194    ///
195    /// **Important:** The `process_shutdown` signal must be moved into the returned [`SupervisorFuture`] so the worker
196    /// can respond to supervisor-initiated shutdown. If `process_shutdown` is dropped during initialization, the worker
197    /// will be unable to shut down gracefully and will be forcefully aborted after the shutdown timeout.
198    ///
199    /// # Errors
200    ///
201    /// If the process can't be initialized, an error is returned.
202    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError>;
203}
204
205/// Supervisor errors.
206#[derive(Debug, Snafu)]
207#[snafu(context(suffix(false)))]
208pub enum SupervisorError {
209    /// Supervisor or worker name is invalid.
210    #[snafu(display("Invalid name for supervisor or worker: '{}'", name))]
211    InvalidName {
212        /// The name of the supervisor is invalid.
213        name: String,
214    },
215
216    /// A child process failed to initialize.
217    ///
218    /// This error indicates that a child couldn't complete its async initialization. This is distinct from runtime
219    /// failures and doesn't trigger restart logic.
220    #[snafu(display("Child process '{}' failed to initialize: {}", child_name, source))]
221    FailedToInitialize {
222        /// The name of the child that failed to initialize.
223        child_name: String,
224
225        /// The underlying initialization error.
226        source: InitializationError,
227    },
228
229    /// The supervisor exceeded its restart limits and was forced to shutdown.
230    #[snafu(display("Supervisor has exceeded restart limits and was forced to shutdown."))]
231    Shutdown,
232
233    /// The supervisor shut down because a significant child terminated.
234    ///
235    /// See [`AutoShutdown`] and [`ChildBuilder::with_significant`][crate::runtime::ChildBuilder::with_significant]. The supervisor stopped, and drained its remaining
236    /// children, because a child marked significant terminated without being restarted.
237    #[snafu(display("Supervisor shut down after a significant child terminated."))]
238    SignificantChildExited,
239
240    /// The supervisor completed a requested shutdown, but one or more workers ignored graceful shutdown and had to be
241    /// forcefully aborted after exceeding their shutdown timeout.
242    ///
243    /// The shutdown itself was requested and otherwise orderly; this variant exists so that having to forcefully stop a
244    /// worker is surfaced as a failure rather than reported as a clean shutdown. The count aggregates forced aborts
245    /// across the entire supervision tree: a parent merges in the counts reported by any child supervisors that also
246    /// timed out, so the value observed at the root supervisor is the total number of workers that had to be
247    /// force-stopped tree-wide.
248    #[snafu(display(
249        "Shutdown completed uncleanly: {} worker(s) were forcefully aborted after exceeding their shutdown timeout.",
250        aborted
251    ))]
252    ShutdownTimedOut {
253        /// The number of workers that had to be forcefully aborted.
254        aborted: usize,
255    },
256}
257
258/// A specification for a process to be added to a [`Supervisor`].
259///
260/// A child specification describes how the supervisor should create and manage a child: the underlying future that
261/// represents the process, along with metadata such as its name and shutdown strategy. All processes in a supervisor,
262/// whether a worker or a (nested) supervisor, are represented by a [`ChildSpecification`].
263///
264/// A specification is a description, not a control surface: it carries no public methods of its own. There are two
265/// ways to obtain one, matching the two levels of control:
266///
267/// - Pass a worker or supervisor directly to [`add_worker`][Supervisor::add_worker], [`spawn`][crate::runtime::spawn],
268///   or [`SupervisorHandle::spawn`], all of which accept a [`Supervisor`] or any [`Supervisable`] and convert it for
269///   you, applying the defaults appropriate to that kind of child -- including the shutdown strategy that lets a
270///   nested supervisor drain its whole subtree.
271/// - Configure one with [`ChildBuilder`][crate::runtime::ChildBuilder], which is the only way to set a restart policy,
272///   significance, placement, or a shutdown deadline. The builder exposes only the settings that make sense for the
273///   kind of child being described, and [`build`][crate::runtime::ChildBuilder::build] hands the result to
274///   [`add_worker`][Supervisor::add_worker].
275///
276/// Supervisors have no per-child settings of their own, so there is nothing to configure for a nested supervisor.
277pub struct ChildSpecification<S = WorkerSpec> {
278    spec_inner: S,
279}
280
281/// Child specification state for a worker.
282pub struct WorkerSpec {
283    worker: Arc<dyn Supervisable>,
284    options: ChildOptions,
285}
286
287/// Child specification state for a supervisor.
288pub struct SupervisorSpec {
289    supervisor: Supervisor,
290    options: ChildOptions,
291}
292
293// The configuration surface below is deliberately crate-internal: `ChildBuilder` is the public front end for all of
294// it, and is what decides which settings are offered for which kind of child. Keeping these methods off the public API
295// means a combination the builder refuses to express -- a permanent child marked significant, say -- can't be reached
296// by going around it. In-crate callers use them directly where the builder would be a layering inversion: this
297// module's own tests, which exercise the lowering these methods feed.
298impl ChildSpecification<WorkerSpec> {
299    /// Creates a specification for the given worker.
300    pub(crate) fn worker<T: Supervisable + 'static>(worker: T) -> Self {
301        Self {
302            spec_inner: WorkerSpec {
303                worker: Arc::new(worker),
304                options: ChildOptions::default(),
305            },
306        }
307    }
308
309    /// Creates a specification for a worker that can only run once.
310    ///
311    /// This function is shorthand for calling [`worker`][Self::worker] followed by
312    /// [`with_restart_type`][Self::with_restart_type] set to [`RestartType::Temporary`][RestartType::Temporary].
313    pub(crate) fn one_shot_worker<T: Supervisable + 'static>(worker: T) -> Self {
314        Self::worker(worker).with_restart_type(RestartType::Temporary)
315    }
316
317    /// Sets the restart policy for this worker.
318    ///
319    /// When left unset, the policy depends on how the child is registered: a child added up front with
320    /// [`Supervisor::add_worker`] defaults to [`RestartType::Permanent`], while one spawned dynamically with
321    /// [`SupervisorHandle::spawn`] defaults to [`RestartType::Temporary`].
322    #[must_use]
323    pub(crate) fn with_restart_type(mut self, restart_type: RestartType) -> Self {
324        self.spec_inner.options.restart = Some(restart_type);
325        self
326    }
327
328    /// Sets whether this worker is _significant_.
329    ///
330    /// A significant worker's termination (when it isn't restarted) can drive the supervisor to shut down, per the
331    /// supervisor's [`AutoShutdown`] policy. Only meaningful for non-permanent workers, since a permanent worker is
332    /// always restarted and so never terminates without being restarted.
333    #[must_use]
334    pub(crate) fn with_significant(mut self, significant: bool) -> Self {
335        self.spec_inner.options.significant = significant;
336        self
337    }
338
339    /// Runs this worker on the given Tokio runtime rather than the supervisor's own runtime.
340    ///
341    /// By default, a worker runs on whatever runtime its supervisor runs on. Use this for compute-heavy workers that
342    /// shouldn't contend with the supervisor's runtime -- for example, a topology component offloading encoding work
343    /// onto a shared worker pool.
344    ///
345    /// Note that this only affects where the worker's task is spawned. Supervision itself -- shutdown signalling,
346    /// restart handling, and abort-on-timeout -- is unchanged, and is still driven from the supervisor's runtime.
347    #[must_use]
348    pub(crate) fn with_runtime(mut self, handle: Handle) -> Self {
349        self.spec_inner.options.runtime = Some(handle);
350        self
351    }
352
353    /// Overrides the shutdown strategy for this worker.
354    ///
355    /// By default, a worker's strategy comes from [`Supervisable::shutdown_strategy`], which itself defaults to
356    /// `Graceful(5s)`. Use this when the grace period depends on where the worker is used rather than on the worker
357    /// type: a worker that a component drains during its own shutdown needs at least as long as the component itself,
358    /// otherwise it is forcefully aborted while the component is still waiting on it.
359    #[must_use]
360    pub(crate) fn with_shutdown_strategy(mut self, strategy: ShutdownStrategy) -> Self {
361        self.spec_inner.options.shutdown = ChildShutdown::Explicit(strategy);
362        self
363    }
364
365    /// Gives this worker no shutdown deadline of its own, leaving it bounded solely by its supervisor's shutdown
366    /// budget.
367    ///
368    /// Use this for a worker whose acceptable drain time is a property of the subtree it belongs to rather than of the
369    /// worker itself -- a task spawned by a topology component, for instance, where what matters is that the component
370    /// as a whole stops in time. See [`Supervisor::with_shutdown_budget`].
371    ///
372    /// A supervisor with no budget has nothing to bound the worker with, so in that case the worker falls back to the
373    /// strategy it reports through [`Supervisable::shutdown_strategy`] rather than being left to stall the drain
374    /// indefinitely.
375    #[must_use]
376    pub(crate) fn with_budget_bounded_shutdown(mut self) -> Self {
377        self.spec_inner.options.shutdown = ChildShutdown::BudgetBounded;
378        self
379    }
380}
381
382// Crate-internal for the same reason as the worker surface above: `NestedSupervisorBuilder` is the public front end,
383// and going around it would allow combinations the builder refuses to express.
384//
385// Deliberately narrower than the worker surface, though. A nested supervisor bounds its own subtree through its
386// children's deadlines: `SupervisedChild::shutdown_strategy` reports `Graceful(Duration::MAX)` for one, and
387// `WorkerState::add_worker` exempts it from the parent's budget. Offering a shutdown setting here would let a caller
388// truncate a drain the subtree is already responsible for, so there isn't one. Placement is likewise absent: a nested
389// supervisor runs wherever its parent does, and its children carry their own placement.
390impl ChildSpecification<SupervisorSpec> {
391    /// Sets the restart policy for this nested supervisor.
392    ///
393    /// When left unset, the policy depends on how the child is registered: a child added up front with
394    /// [`Supervisor::add_worker`] defaults to [`RestartType::Permanent`], while one spawned dynamically with
395    /// [`SupervisorHandle::spawn`] defaults to [`RestartType::Temporary`].
396    #[must_use]
397    pub(crate) fn with_restart_type(mut self, restart_type: RestartType) -> Self {
398        self.spec_inner.options.restart = Some(restart_type);
399        self
400    }
401
402    /// Sets whether this nested supervisor is _significant_.
403    ///
404    /// A significant child's termination (when it isn't restarted) can drive the parent supervisor to shut down, per
405    /// the parent's [`AutoShutdown`] policy.
406    #[must_use]
407    pub(crate) fn with_significant(mut self, significant: bool) -> Self {
408        self.spec_inner.options.significant = significant;
409        self
410    }
411}
412
413impl<T> From<T> for ChildSpecification<WorkerSpec>
414where
415    T: Supervisable + 'static,
416{
417    fn from(worker: T) -> Self {
418        Self::worker(worker)
419    }
420}
421
422impl From<Supervisor> for ChildSpecification<SupervisorSpec> {
423    fn from(supervisor: Supervisor) -> Self {
424        Self {
425            spec_inner: SupervisorSpec {
426                supervisor,
427                options: ChildOptions::default(),
428            },
429        }
430    }
431}
432
433mod sealed {
434    pub trait Sealed {}
435}
436
437impl sealed::Sealed for WorkerSpec {}
438impl sealed::Sealed for SupervisorSpec {}
439
440/// Child specification state.
441///
442/// This trait is sealed -- it cannot be implemented outside of this crate -- and is implemented only for
443/// [`WorkerSpec`] and [`SupervisorSpec`]. It exists so that [`Supervisor::add_worker`] and
444/// [`SupervisorHandle::spawn`] can both accept a [`ChildSpecification`] in either state (as well as bare workers and
445/// supervisors) while lowering each into the supervisor's internal representation.
446pub trait ChildState: sealed::Sealed + Sized {
447    /// Lowers a specification into the supervisor's internal representation of a child.
448    ///
449    /// `default_restart` supplies the restart policy for a specification that didn't set one, which differs by
450    /// registration path: children added up front are permanent, dynamically spawned children are temporary.
451    #[doc(hidden)]
452    fn into_child_parts(spec: ChildSpecification<Self>, default_restart: RestartType) -> LoweredChild;
453}
454
455/// A child specification lowered into the supervisor's internal representation.
456///
457/// Opaque to callers: it exists only to carry the output of [`ChildState::into_child_parts`] to the supervisor that
458/// registers the child, and is public only because [`ChildState`] is.
459pub struct LoweredChild {
460    spec: SupervisedChild,
461    config: ChildConfig,
462}
463
464impl ChildState for WorkerSpec {
465    fn into_child_parts(spec: ChildSpecification<Self>, default_restart: RestartType) -> LoweredChild {
466        let WorkerSpec { worker, options } = spec.spec_inner;
467        LoweredChild {
468            spec: SupervisedChild::Worker(worker),
469            config: options.resolve(default_restart),
470        }
471    }
472}
473
474impl ChildState for SupervisorSpec {
475    fn into_child_parts(spec: ChildSpecification<Self>, default_restart: RestartType) -> LoweredChild {
476        let SupervisorSpec { supervisor, options } = spec.spec_inner;
477        LoweredChild {
478            spec: SupervisedChild::Supervisor(supervisor),
479            config: options.resolve(default_restart),
480        }
481    }
482}
483
484/// The type-erased, runnable form of a child: either a worker or a nested supervisor.
485pub(super) enum SupervisedChild {
486    Worker(Arc<dyn Supervisable>),
487    Supervisor(Supervisor),
488}
489
490impl SupervisedChild {
491    /// Returns whether this child is a nested supervisor rather than a leaf worker.
492    pub(super) fn is_supervisor(&self) -> bool {
493        matches!(self, Self::Supervisor(_))
494    }
495
496    fn process_type(&self) -> &'static str {
497        match self {
498            Self::Worker(_) => "worker",
499            Self::Supervisor(_) => "supervisor",
500        }
501    }
502
503    fn name(&self) -> &str {
504        match self {
505            Self::Worker(worker) => worker.name(),
506            Self::Supervisor(supervisor) => &supervisor.supervisor_id,
507        }
508    }
509
510    /// Returns whether this child observes the shutdown signal it is given.
511    ///
512    /// Always true for a nested supervisor: the signal is how it learns to drain its own subtree, and a supervisor on
513    /// a dedicated runtime receives it across the thread boundary through `spawn_dedicated_runtime`, where aborting
514    /// the awaiting future wouldn't stop the runtime thread anyway.
515    pub(super) fn wants_shutdown_signal(&self) -> bool {
516        match self {
517            Self::Worker(worker) => worker.wants_shutdown_signal(),
518            Self::Supervisor(_) => true,
519        }
520    }
521
522    pub(super) fn shutdown_strategy(&self) -> ShutdownStrategy {
523        match self {
524            Self::Worker(worker) => worker.shutdown_strategy(),
525
526            // Supervisors should always be given as much time as necessary shutdown down gracefully to ensure that the
527            // entire supervision subtree can be shutdown cleanly.
528            Self::Supervisor(_) => ShutdownStrategy::Graceful(Duration::MAX),
529        }
530    }
531
532    /// Creates the process for this child under `parent_process`.
533    ///
534    /// A name that sanitizes to nothing at all (an empty string, or one made up entirely of separators) can't be used
535    /// as a process name. Rather than refuse to start the child -- which for a dynamically spawned child would mean
536    /// silently losing work that the caller was told had been accepted -- the child runs under
537    /// [`UNNAMED_CHILD`] instead, and the substitution is logged.
538    pub(super) fn create_process(&self, parent_process: &Process) -> Process {
539        let name = self.name();
540        let process = match self {
541            Self::Worker(_) => Process::worker(name, parent_process),
542            Self::Supervisor(_) => Process::supervisor(name, Some(parent_process)),
543        };
544
545        process.unwrap_or_else(|| {
546            warn!(
547                parent_process = parent_process.name(),
548                child_name = name,
549                "Child process name is not usable as a process name; falling back to '{}'.",
550                UNNAMED_CHILD
551            );
552
553            match self {
554                Self::Worker(_) => Process::worker(UNNAMED_CHILD, parent_process),
555                Self::Supervisor(_) => Process::supervisor(UNNAMED_CHILD, Some(parent_process)),
556            }
557            .expect("placeholder child name is always a valid process name")
558        })
559    }
560
561    pub(super) fn create_worker_future(
562        &self, process: Process, process_shutdown: ShutdownHandle,
563    ) -> Result<WorkerFuture, SupervisorError> {
564        match self {
565            Self::Worker(worker) => {
566                let worker = Arc::clone(worker);
567                Ok(Box::pin(async move {
568                    let run_future =
569                        worker
570                            .initialize(process_shutdown)
571                            .await
572                            .map_err(|source| WorkerError::Initialization {
573                                child_name: None,
574                                source,
575                            })?;
576                    run_future.await.map_err(WorkerError::Runtime)
577                }))
578            }
579            Self::Supervisor(sup) => {
580                match sup.runtime_mode() {
581                    RuntimeMode::Ambient => {
582                        // Run on the parent's ambient runtime.
583                        Ok(sup.as_nested_process(process, process_shutdown))
584                    }
585                    RuntimeMode::Dedicated(config) => {
586                        // Spawn in a dedicated runtime on a new OS thread, passing the parent's
587                        // dataspace so the nested supervisor inherits it across the thread boundary.
588                        let child_name = sup.supervisor_id.to_string();
589                        let dataspace = process.dataspace().clone();
590                        let handle =
591                            spawn_dedicated_runtime(sup.inner_clone(), config.clone(), process_shutdown, dataspace)
592                                .map_err(|e| SupervisorError::FailedToInitialize {
593                                    child_name,
594                                    source: e.into(),
595                                })?;
596
597                        Ok(Box::pin(async move { handle.await.map_err(WorkerError::from) }))
598                    }
599                }
600            }
601        }
602    }
603}
604
605impl Clone for SupervisedChild {
606    fn clone(&self) -> Self {
607        match self {
608            Self::Worker(worker) => Self::Worker(Arc::clone(worker)),
609            Self::Supervisor(supervisor) => Self::Supervisor(supervisor.inner_clone()),
610        }
611    }
612}
613
614/// How a child's shutdown strategy is determined.
615#[derive(Clone, Copy, Debug, Default)]
616pub(super) enum ChildShutdown {
617    /// Use whatever the worker reports through [`Supervisable::shutdown_strategy`]. This is the default.
618    #[default]
619    Worker,
620
621    /// Use this strategy, overriding whatever the worker reports.
622    Explicit(ShutdownStrategy),
623
624    /// The child carries no deadline of its own and is bounded solely by its supervisor's shutdown budget.
625    ///
626    /// A supervisor with no budget has nothing to bound the child with, so this falls back to the worker's own
627    /// strategy rather than leaving the child free to stall the drain indefinitely.
628    BudgetBounded,
629}
630
631/// Per-child settings as configured on a [`ChildSpecification`], before they are resolved for a specific registration
632/// path.
633///
634/// Separate from [`ChildConfig`] because the restart policy has no single default: a child registered up front with
635/// [`Supervisor::add_worker`] is permanent, while one spawned dynamically with [`SupervisorHandle::spawn`] is
636/// temporary. Leaving the policy unset here is what lets both paths share one specification type.
637#[derive(Clone, Debug, Default)]
638pub(super) struct ChildOptions {
639    restart: Option<RestartType>,
640    significant: bool,
641
642    /// Runtime to spawn the child on. `None` means the supervisor's own runtime.
643    runtime: Option<Handle>,
644
645    shutdown: ChildShutdown,
646}
647
648impl ChildOptions {
649    /// Resolves these options into a concrete configuration, applying `default_restart` if no policy was set.
650    fn resolve(self, default_restart: RestartType) -> ChildConfig {
651        ChildConfig {
652            restart: self.restart.unwrap_or(default_restart),
653            significant: self.significant,
654            runtime: self.runtime,
655            shutdown: self.shutdown,
656        }
657    }
658}
659
660/// Per-child configuration: its [`RestartType`], whether it is _significant_ (see [`AutoShutdown`]), the runtime it
661/// runs on, and how its shutdown strategy is decided.
662#[derive(Clone, Debug)]
663pub(super) struct ChildConfig {
664    restart: RestartType,
665    significant: bool,
666    runtime: Option<Handle>,
667    shutdown: ChildShutdown,
668}
669
670impl ChildConfig {
671    /// Returns the runtime the child should be spawned on, if it isn't the supervisor's own.
672    pub(super) fn runtime(&self) -> Option<&Handle> {
673        self.runtime.as_ref()
674    }
675
676    /// Returns how the child's shutdown strategy should be determined.
677    pub(super) fn shutdown(&self) -> ChildShutdown {
678        self.shutdown
679    }
680}
681
682/// A registered child: its specification together with the configuration chosen at registration time.
683#[derive(Clone)]
684struct ChildEntry {
685    spec: SupervisedChild,
686    config: ChildConfig,
687    /// Whether this child was added dynamically (via [`SupervisorHandle`]) rather than statically before the run. Used
688    /// to maintain the dynamic-children gauge.
689    dynamic: bool,
690}
691
692/// Identifier for a child managed by a [`Supervisor`].
693///
694/// Returned by [`SupervisorHandle::spawn`] for dynamically spawned children. Unique within a single process for the
695/// lifetime of a supervisor run.
696#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
697pub struct ChildId(u64);
698
699impl ChildId {
700    /// Returns the raw numeric value of this identifier.
701    pub const fn as_u64(self) -> u64 {
702        self.0
703    }
704}
705
706/// A dynamic spawn request handed from a [`SupervisorHandle`] to the running supervisor.
707struct PendingSpawn {
708    id: u64,
709    spec: SupervisedChild,
710    config: ChildConfig,
711}
712
713/// Number of queued spawn requests the supervisor takes in one go before returning to its loop.
714///
715/// Draining in batches keeps a burst of spawns to a single wake-up rather than one per child, while still bounding how
716/// long the supervisor can spend registering children before it re-checks the rest of its loop (most importantly,
717/// shutdown).
718const SPAWN_DRAIN_BATCH: usize = 64;
719
720/// A handle for spawning dynamic children on a running [`Supervisor`].
721///
722/// Obtained from [`Supervisor::handle`]. Handles are cheap to clone and can be shared across tasks.
723///
724/// Spawning is synchronous and infallible, in the spirit of [`tokio::spawn`]: the child is queued for the running
725/// supervisor and the call returns immediately with the child's [`ChildId`]. Also as with [`tokio::spawn`], being
726/// accepted is not a promise of being run -- if the supervisor isn't running, or shuts down before it gets to the
727/// queued child, the child is never started at all.
728///
729/// # Ambient spawning
730///
731/// Code running under supervision usually doesn't need a handle at all: [`spawn`][crate::runtime::spawn] targets the
732/// supervisor of whatever process is currently running. Use a handle when spawning from outside supervision, or when
733/// targeting a supervisor other than the ambient one. [`scope`][Self::scope] bridges the two by making a handle the
734/// ambient supervisor for a future.
735#[derive(Clone)]
736pub struct SupervisorHandle {
737    name: Arc<str>,
738    // The currently running supervisor publishes its spawn queue here so handles can reach the live run; it's cleared
739    // when no run is active, at which point spawns are accepted and dropped.
740    current_tx: Arc<Mutex<Option<mpsc::UnboundedSender<PendingSpawn>>>>,
741    id_counter: Arc<AtomicU64>,
742    active: Arc<AtomicUsize>,
743}
744
745impl SupervisorHandle {
746    /// Returns the name of the supervisor this handle refers to.
747    pub fn name(&self) -> &str {
748        &self.name
749    }
750
751    /// Spawns a new dynamic child.
752    ///
753    /// Accepts anything [`Supervisor::add_worker`] accepts: a bare [`Supervisable`], a [`Supervisor`] to run as a
754    /// nested supervision subtree, or a [`ChildSpecification`] configured in detail.
755    ///
756    /// Unless [`ChildBuilder`][crate::runtime::ChildBuilder] says otherwise, dynamic children are
757    /// [`temporary`][RestartType::Temporary]: they
758    /// aren't restarted when they die, and they aren't restored when the supervisor itself restarts. That suits
759    /// short-lived, non-critical work that still wants structured concurrency -- the child is stopped when the
760    /// supervisor is restarted or terminated.
761    ///
762    /// The returned [`ChildId`] identifies the child for the lifetime of the supervisor run. The child is queued
763    /// rather than started synchronously, so it may not have begun running by the time this returns; if the supervisor
764    /// isn't running, or shuts down before reaching the child, it never runs at all.
765    pub fn spawn<S, T>(&self, child: T) -> ChildId
766    where
767        S: ChildState,
768        T: Into<ChildSpecification<S>>,
769    {
770        let LoweredChild { spec, config } = S::into_child_parts(child.into(), RestartType::Temporary);
771
772        // Take the id before we try to enqueue: the caller gets a stable identifier either way, and ids are only
773        // meaningful within a run.
774        let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
775        let pending = PendingSpawn { id, spec, config };
776
777        // Clone the sender out from under the lock rather than sending while holding the guard.
778        //
779        // The queue behind it is unbounded on purpose: a queued child is a child that will be started, so the only
780        // thing a depth limit could buy is discarding work the caller was told had been accepted. A backlog only forms
781        // while the supervisor can't drain -- mid-restart, or mid-drain -- and holding it until it can is the whole
782        // point.
783        let tx = self.current_tx.lock().unwrap().clone();
784        match tx {
785            // Racing a teardown is normal rather than exceptional -- a source that spawns a child per connection will
786            // do it every time it is shut down mid-accept -- so this stays at debug level.
787            Some(tx) => {
788                if let Err(e) = tx.send(pending) {
789                    debug!(
790                        supervisor_id = %self.name,
791                        child_name = e.0.spec.name(),
792                        "Supervisor is shutting down; dynamic child will not be started."
793                    );
794                }
795            }
796            // Spawning against a supervisor that never ran, on the other hand, is a wiring mistake: nothing about the
797            // program's normal operation produces it, and the child is silently lost.
798            None => warn!(
799                supervisor_id = %self.name,
800                child_name = pending.spec.name(),
801                "Supervisor is not running; dynamic child will not be started."
802            ),
803        }
804
805        ChildId(id)
806    }
807
808    /// Returns whether the supervisor is currently running.
809    pub fn is_running(&self) -> bool {
810        self.current_tx.lock().unwrap().is_some()
811    }
812
813    /// Returns the number of dynamic children currently running under the supervisor.
814    ///
815    /// Counts children the supervisor has actually started, so a child that has been spawned but not yet picked up
816    /// isn't included yet.
817    pub fn active_children(&self) -> usize {
818        self.active.load(Ordering::Relaxed)
819    }
820}
821
822/// Supervises a set of workers.
823///
824/// # Workers
825///
826/// All workers are defined through implementation of the [`Supervisable`] trait, which provides the logic for both
827/// creating the underlying worker future that's spawned, as well as other metadata, such as the worker's name, how the
828/// worker should be shutdown, and so on.
829///
830/// Supervisors also (indirectly) implement the [`Supervisable`] trait, allowing them to be supervised by other
831/// supervisors in order to construct _supervision trees_.
832///
833/// # Instrumentation
834///
835/// Supervisors automatically create their own allocation group
836/// ([`TrackingAllocator`][saluki_common::resource_tracking::TrackingAllocator]), which is used to track both the memory
837/// usage of the supervisor itself and its children. Additionally, individual worker processes are wrapped in a
838/// dedicated [`tracing::Span`] to allow tracing the causal relationship between arbitrary code and the worker executing
839/// it, and statistics about task polls (poll count, poll duration) are collected.
840///
841/// # Restart Strategies
842///
843/// As the main purpose of a supervisor, restart behavior is fully configurable. A number of restart strategies are
844/// available, which generally relate to the purpose of the supervisor: whether the workers being managed are
845/// independent or interdependent.
846///
847/// All restart strategies are configured through [`RestartStrategy`], which has more information on the available
848/// strategies and configuration settings.
849pub struct Supervisor {
850    supervisor_id: Arc<str>,
851    child_specs: Vec<ChildEntry>,
852    restart_strategy: RestartStrategy,
853    auto_shutdown: AutoShutdown,
854    shutdown_budget: Option<Duration>,
855    runtime_mode: RuntimeMode,
856    // Shared across clones (a nested supervisor is cloned each time it runs) and across all handles. While a run is
857    // active it holds that run's spawn queue so handles can reach the live supervisor; it's `None` whenever no run is
858    // active, at which point spawned children are dropped rather than queued. Doubles as the `is_running` signal.
859    current_tx: Arc<Mutex<Option<mpsc::UnboundedSender<PendingSpawn>>>>,
860    id_counter: Arc<AtomicU64>,
861    // Number of dynamic children currently running, shared with handles so it can be surfaced as a gauge.
862    active: Arc<AtomicUsize>,
863}
864
865impl Supervisor {
866    /// Creates an empty `Supervisor` with the default restart strategy.
867    pub fn new<S: AsRef<str>>(supervisor_id: S) -> Result<Self, SupervisorError> {
868        // We try to throw an error about invalid names as early as possible. This is a manual check, so we might still
869        // encounter an error later when actually running the supervisor, but this is a good first step to catch the
870        // bulk of invalid names.
871        if supervisor_id.as_ref().is_empty() {
872            return Err(SupervisorError::InvalidName {
873                name: supervisor_id.as_ref().to_string(),
874            });
875        }
876
877        Ok(Self {
878            supervisor_id: supervisor_id.as_ref().into(),
879            child_specs: Vec::new(),
880            restart_strategy: RestartStrategy::default(),
881            auto_shutdown: AutoShutdown::default(),
882            shutdown_budget: None,
883            runtime_mode: RuntimeMode::default(),
884            current_tx: Arc::new(Mutex::new(None)),
885            id_counter: Arc::new(AtomicU64::new(0)),
886            active: Arc::new(AtomicUsize::new(0)),
887        })
888    }
889
890    /// Returns the supervisor's ID.
891    pub fn id(&self) -> &str {
892        &self.supervisor_id
893    }
894
895    /// Sets the restart strategy for the supervisor.
896    pub fn with_restart_strategy(mut self, strategy: RestartStrategy) -> Self {
897        self.restart_strategy = strategy;
898        self
899    }
900
901    /// Sets the supervisor's automatic-shutdown policy.
902    ///
903    /// Controls whether the termination of _significant_ children (see [`ChildBuilder::with_significant`][crate::runtime::ChildBuilder::with_significant]) drives the
904    /// supervisor to shut down. Defaults to [`AutoShutdown::Never`].
905    pub fn with_auto_shutdown(mut self, auto_shutdown: AutoShutdown) -> Self {
906        self.auto_shutdown = auto_shutdown;
907        self
908    }
909
910    /// Bounds how long this supervisor waits for its worker children during shutdown.
911    ///
912    /// Without a budget, a supervisor waits as long as each child's own [`ShutdownStrategy`] allows, and waits
913    /// indefinitely for any child that has no finite deadline of its own. A budget makes the supervisor responsible for
914    /// the deadline instead: children need no individual timeouts, and whatever is still running when the budget
915    /// elapses is forcefully aborted -- each one named in the logs, and counted in the resulting
916    /// [`SupervisorError::ShutdownTimedOut`].
917    ///
918    /// The budget is a ceiling, not a replacement: a child that also carries its own finite deadline is still held to
919    /// whichever elapses first.
920    ///
921    /// Since children are always drained concurrently, the budget bounds the drain as a whole rather than accruing
922    /// per child: it is measured from the moment shutdown begins, and every child is held to it simultaneously.
923    ///
924    /// Two kinds of child are outside it. A nested supervisor is never cut off by its parent's budget -- it bounds its
925    /// own subtree, and aborting it would both truncate that drain and, for a supervisor running on a dedicated
926    /// runtime, fail to stop it at all. A [`ShutdownStrategy::Brutal`] child is aborted up front and never waited on.
927    /// Neither can a budget bound work that ignores cancellation, since an abort only takes effect at an await point.
928    ///
929    /// Use this where one deadline for a whole subtree is more meaningful than a guess per worker -- a topology
930    /// component and its background tasks, for instance, where what matters is that the component as a whole stops in
931    /// time.
932    #[must_use]
933    pub fn with_shutdown_budget(mut self, budget: Duration) -> Self {
934        self.shutdown_budget = Some(budget);
935        self
936    }
937
938    /// Returns a handle for spawning dynamic children on this supervisor while it runs.
939    ///
940    /// The handle can be created before the supervisor starts and cloned freely. Spawning through it always succeeds,
941    /// but a child is only ever started while the supervisor is actually running: one spawned before the supervisor
942    /// starts, or after it has shut down, is accepted and then dropped.
943    pub fn handle(&self) -> SupervisorHandle {
944        SupervisorHandle {
945            name: Arc::clone(&self.supervisor_id),
946            current_tx: Arc::clone(&self.current_tx),
947            id_counter: Arc::clone(&self.id_counter),
948            active: Arc::clone(&self.active),
949        }
950    }
951
952    /// Configures this supervisor to run in a dedicated runtime.
953    ///
954    /// When this supervisor is added as a child to another supervisor, it will spawn its own OS threads and Tokio
955    /// runtime instead of running on the parent's ambient runtime.
956    ///
957    /// This provides runtime isolation, which can be useful for:
958    /// - CPU-bound work that shouldn't block the parent's runtime
959    /// - Isolating failures in one part of the system
960    /// - Using different runtime configurations (for example, single-threaded vs multi-threaded)
961    pub fn with_dedicated_runtime(mut self, config: RuntimeConfiguration) -> Self {
962        self.runtime_mode = RuntimeMode::Dedicated(config);
963        self
964    }
965
966    /// Returns the runtime mode for this supervisor.
967    pub(crate) fn runtime_mode(&self) -> &RuntimeMode {
968        &self.runtime_mode
969    }
970
971    /// Adds a worker (or nested supervisor) to the supervisor.
972    ///
973    /// A worker can be anything that implements the [`Supervisable`] trait. A [`Supervisor`] can also be added as a
974    /// worker and managed in a nested fashion, known as a supervision tree.
975    ///
976    /// Anything that needs configuring -- a restart policy, significance, placement, a shutdown deadline -- is
977    /// described with [`ChildBuilder`][crate::runtime::ChildBuilder] and handed over via
978    /// [`build`][crate::runtime::ChildBuilder::build]. See [`ChildSpecification`] for how children are represented
979    /// internally.
980    pub fn add_worker<S, T>(&mut self, child: T)
981    where
982        S: ChildState,
983        T: Into<ChildSpecification<S>>,
984    {
985        let LoweredChild { spec, config } = S::into_child_parts(child.into(), RestartType::Permanent);
986        self.push_child(ChildEntry {
987            spec,
988            config,
989            dynamic: false,
990        });
991    }
992
993    /// Warns when a child was marked significant but nothing will act on it.
994    ///
995    /// Significance only has an effect for a child that can terminate without being restarted, under a supervisor
996    /// whose [`AutoShutdown`] policy isn't [`Never`][AutoShutdown::Never]. Either mismatch makes the flag inert, which
997    /// is worth saying out loud: a caller who marked a child significant is asserting that its termination matters,
998    /// and silently ignoring that is how a supervisor ends up outliving something it can't work without.
999    ///
1000    /// Called as children are started rather than as they are registered, because the policy half of the question
1001    /// isn't answerable any earlier: [`with_auto_shutdown`][Self::with_auto_shutdown] consumes the supervisor while
1002    /// [`add_worker`][Self::add_worker] borrows it, so a caller is free to add children first and set the policy
1003    /// afterwards. Checking at registration time would flag that -- entirely correct -- ordering as a mistake.
1004    ///
1005    /// Warn-only: the child still starts, since an inert flag is useless rather than unsafe.
1006    fn warn_if_significance_is_inert(&self, config: &ChildConfig, child_name: &str) {
1007        if !config.significant {
1008            return;
1009        }
1010
1011        if config.restart == RestartType::Permanent {
1012            warn!(
1013                supervisor_id = %self.supervisor_id,
1014                child_name,
1015                "Child is marked significant but is permanent, so it is always restarted and the flag has no effect."
1016            );
1017        }
1018
1019        if self.auto_shutdown == AutoShutdown::Never {
1020            warn!(
1021                supervisor_id = %self.supervisor_id,
1022                child_name,
1023                "Child is marked significant but the supervisor's auto-shutdown policy is `Never`, so the flag has \
1024                 no effect."
1025            );
1026        }
1027    }
1028
1029    fn push_child(&mut self, entry: ChildEntry) {
1030        debug!(
1031            supervisor_id = %self.supervisor_id,
1032            "Adding new static child process #{}. ({}, {}, {:?})",
1033            self.child_specs.len(),
1034            entry.spec.process_type(),
1035            entry.spec.name(),
1036            entry.config,
1037        );
1038
1039        // The policy half of the inert-significance check has to wait until the supervisor runs (see
1040        // `warn_if_significance_is_inert`), but this half doesn't depend on anything but the child itself, and here we
1041        // are still in the caller's frame. `ChildBuilder` makes the combination unreachable from outside the crate, so
1042        // this guards in-crate construction.
1043        debug_assert!(
1044            !(entry.config.significant && entry.config.restart == RestartType::Permanent),
1045            "child '{}' was marked significant but is permanent, so it is always restarted and its termination can \
1046             never drive auto-shutdown",
1047            entry.spec.name()
1048        );
1049
1050        self.child_specs.push(entry);
1051    }
1052
1053    fn spawn_static_children(
1054        &self, children: &mut FastHashMap<u64, ChildEntry>, worker_state: &mut WorkerState,
1055    ) -> Result<(), SupervisorError> {
1056        debug!(supervisor_id = %self.supervisor_id, "Spawning all static child processes.");
1057        for entry in &self.child_specs {
1058            self.warn_if_significance_is_inert(&entry.config, entry.spec.name());
1059
1060            let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
1061            worker_state.add_worker(id, &entry.spec, &entry.config)?;
1062            children.insert(id, entry.clone());
1063        }
1064
1065        Ok(())
1066    }
1067
1068    /// Respawns children after a one-for-all restart, honoring each child's [`RestartType`].
1069    ///
1070    /// Every child except [`RestartType::Temporary`] is restarted, matching Erlang/OTP: a group restart restarts all
1071    /// permanent and transient children -- regardless of how they last exited, including a transient child that had
1072    /// already exited cleanly -- but never temporary children, which are shut down with the group and not brought back.
1073    /// A transient child's "restart only on abnormal exit" rule governs its _own_ termination, not a group restart
1074    /// driven by a sibling. Dynamic children are not restored (they are lost on a supervisor-level restart).
1075    fn respawn_children_one_for_all(
1076        &self, children: &mut FastHashMap<u64, ChildEntry>, worker_state: &mut WorkerState,
1077    ) -> Result<(), SupervisorError> {
1078        debug!(supervisor_id = %self.supervisor_id, "Restarting all eligible static child processes.");
1079        for entry in &self.child_specs {
1080            // Temporary children are never restarted by a group restart (matching OTP): they are shut down with the
1081            // group but not brought back.
1082            if entry.config.restart == RestartType::Temporary {
1083                continue;
1084            }
1085            let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
1086            worker_state.add_worker(id, &entry.spec, &entry.config)?;
1087            children.insert(id, entry.clone());
1088        }
1089
1090        Ok(())
1091    }
1092
1093    /// Registers one dynamic child into the running supervisor's worker set and roster.
1094    fn spawn_dynamic_child(
1095        &self, spawn: PendingSpawn, worker_state: &mut WorkerState, children: &mut FastHashMap<u64, ChildEntry>,
1096        significant_remaining: &mut usize,
1097    ) {
1098        let PendingSpawn { id, spec, config } = spawn;
1099        let entry = ChildEntry {
1100            spec,
1101            config,
1102            dynamic: true,
1103        };
1104        self.warn_if_significance_is_inert(&entry.config, entry.spec.name());
1105
1106        match worker_state.add_worker(id, &entry.spec, &entry.config) {
1107            Ok(()) => {
1108                if entry.config.significant {
1109                    *significant_remaining += 1;
1110                }
1111                self.active.fetch_add(1, Ordering::Relaxed);
1112                children.insert(id, entry);
1113            }
1114            Err(e) => {
1115                // The only way registration fails now that child names always resolve is a nested supervisor on a
1116                // dedicated runtime failing to get an OS thread. There's no caller left to report it to -- spawning is
1117                // infallible -- so the child is dropped and the failure is logged here.
1118                error!(
1119                    supervisor_id = %self.supervisor_id,
1120                    child_name = entry.spec.name(),
1121                    error = %e,
1122                    "Failed to start dynamic child."
1123                );
1124            }
1125        }
1126    }
1127
1128    async fn run_inner(&self, process: Process, process_shutdown: ShutdownHandle) -> Result<(), SupervisorError> {
1129        // Publish a fresh spawn queue for this run so handles can spawn dynamic children into it; while it's set,
1130        // handles observe us as running.
1131        let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
1132        *self.current_tx.lock().unwrap() = Some(cmd_tx);
1133
1134        let result = self.supervise(process, process_shutdown, cmd_rx).await;
1135
1136        // The run is over. Clear the sender so later spawns are dropped rather than queued, and reset the
1137        // dynamic-children gauge. Dropping the receiver (owned by `supervise`) already discarded anything in flight.
1138        *self.current_tx.lock().unwrap() = None;
1139        self.active.store(0, Ordering::Relaxed);
1140        result
1141    }
1142
1143    async fn supervise(
1144        &self, process: Process, process_shutdown: ShutdownHandle, mut cmd_rx: mpsc::UnboundedReceiver<PendingSpawn>,
1145    ) -> Result<(), SupervisorError> {
1146        let mut restart_state = RestartState::new(self.restart_strategy);
1147        let mut worker_state = WorkerState::new(process, self.handle(), self.shutdown_budget);
1148
1149        // The live roster of children -- both static (seeded below) and dynamic (added via the handle) -- keyed by a
1150        // stable id. A restart re-runs a child by id; a child that isn't restarted is removed from the roster.
1151        let mut children: FastHashMap<u64, ChildEntry> = FastHashMap::default();
1152
1153        // Spawn the static children. Initialization is folded into each worker's task, so this returns immediately --
1154        // children initialize concurrently in the background.
1155        self.spawn_static_children(&mut children, &mut worker_state)?;
1156
1157        // Track how many significant children are still running, for `AutoShutdown` evaluation.
1158        let mut significant_remaining = children.values().filter(|entry| entry.config.significant).count();
1159
1160        // Scratch space reused across every batched drain of the spawn queue.
1161        let mut spawn_batch = Vec::with_capacity(SPAWN_DRAIN_BATCH);
1162
1163        // Now we supervise.
1164        pin!(process_shutdown);
1165
1166        let outcome = loop {
1167            select! {
1168                // Shutdown first, then reaping, then taking on new work -- so neither a flood of spawns nor a stream
1169                // of exiting children can starve anything ranked above it.
1170                biased;
1171
1172                // Shutdown has been triggered; break out of the loop with a clean outcome and tear down below. (We
1173                // can't touch `cmd_rx` in any arm's handler -- the `recv_many` arm below borrows it for the whole
1174                // `select!` -- so all teardown happens after the loop.)
1175                _ = &mut process_shutdown => break Ok(()),
1176
1177                // Reaping outranks taking on new work: it is the only place children leave the join set, the roster
1178                // and the `active` gauge, so a steady stream of spawns must not be able to starve it. It parks
1179                // whenever there are no children, so it can't starve spawning in return.
1180                (child_id, worker_result) = worker_state.wait_for_next_worker() => {
1181                    // Pull out what we need from the roster before we mutate it.
1182                    let (child_name, config, dynamic) = {
1183                        let entry = children.get(&child_id).expect("completed worker must be present in the roster");
1184                        (entry.spec.name().to_string(), entry.config.clone(), entry.dynamic)
1185                    };
1186
1187                    // Initialization failures are not eligible for restart -- they propagate immediately.
1188                    if let Err(WorkerError::Initialization { child_name: inner, source }) = worker_result {
1189                        // If the error came from a nested supervisor, include the original child name to make the error
1190                        // chain more informative (e.g., "ctrl-pln/privileged-api").
1191                        let full_name = match inner {
1192                            Some(inner) => format!("{}/{}", child_name, inner),
1193                            None => child_name.clone(),
1194                        };
1195
1196                        error!(supervisor_id = %self.supervisor_id, worker_name = full_name, "Child process failed to initialize: {}", source);
1197                        break Err(SupervisorError::FailedToInitialize { child_name: full_name, source });
1198                    }
1199
1200                    // A worker exited abnormally if it returned an error, panicked, or was aborted; a clean exit is
1201                    // `Ok(())`. Together with the worker's restart policy, this determines whether we restart it.
1202                    let abnormal = worker_result.is_err();
1203                    let worker_result = worker_result.map_err(|e| match e {
1204                        WorkerError::Runtime(e) => ProcessError::Terminated { source: e },
1205                        WorkerError::Initialization { .. } => unreachable!("handled above"),
1206                        // A nested supervisor only reports `ShutdownTimedOut` while draining, which is driven by its own
1207                        // `process_shutdown` -- and that fires only when *this* supervisor is itself draining it, i.e.
1208                        // from `shutdown_workers` below, never from this main-loop arm. Treat it as a runtime
1209                        // termination defensively rather than asserting unreachable.
1210                        WorkerError::ShutdownTimedOut { aborted } => ProcessError::Terminated {
1211                            source: SupervisorError::ShutdownTimedOut { aborted }.into(),
1212                        },
1213                    });
1214
1215                    if !config.restart.should_restart(abnormal) {
1216                        // Not eligible for restart given how it exited. Drop it from the roster, and free its slot/gauge
1217                        // if it was dynamic. Crucially, we do NOT consult `evaluate_restart` here: non-restarts must not
1218                        // consume the restart-intensity budget, otherwise a steady stream of terminating temporary
1219                        // children would eventually trip the limit and tear the supervisor (and its siblings) down.
1220                        //
1221                        // An abnormal exit is reported at `warn` rather than `debug`: a child that isn't restarted --
1222                        // every dynamically-spawned child, in practice -- has no other path back to its owner, so this
1223                        // is the only place its failure is surfaced.
1224                        if abnormal {
1225                            warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, restart = ?config.restart, ?worker_result, "Child process exited with an error and is not eligible for restart.");
1226                        } else {
1227                            debug!(supervisor_id = %self.supervisor_id, worker_name = %child_name, restart = ?config.restart, "Child process exited and is not eligible for restart.");
1228                        }
1229                        children.remove(&child_id);
1230                        if dynamic {
1231                            self.active.fetch_sub(1, Ordering::Relaxed);
1232                        }
1233
1234                        // A significant child terminating without restart can drive the supervisor to shut down, per its
1235                        // `AutoShutdown` policy -- cascading an unexpected (or intentional) child exit into the
1236                        // supervisor stopping and propagating up the tree.
1237                        if config.significant {
1238                            significant_remaining = significant_remaining.saturating_sub(1);
1239                            let auto_shutdown = match self.auto_shutdown {
1240                                AutoShutdown::Never => false,
1241                                AutoShutdown::AnySignificant => true,
1242                                AutoShutdown::AllSignificant => significant_remaining == 0,
1243                            };
1244                            if auto_shutdown {
1245                                warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Significant child terminated; shutting down supervisor.");
1246                                break Err(SupervisorError::SignificantChildExited);
1247                            }
1248                        }
1249                    } else {
1250                        match restart_state.evaluate_restart() {
1251                            RestartAction::Restart(mode) => match mode {
1252                                RestartMode::OneForOne => {
1253                                    warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting.");
1254                                    let spec = children.get(&child_id).expect("present for restart").spec.clone();
1255                                    if let Err(e) = worker_state.add_worker(child_id, &spec, &config) {
1256                                        break Err(e);
1257                                    }
1258                                }
1259                                RestartMode::OneForAll => {
1260                                    warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting all processes.");
1261                                    // This drain is part of a restart, not a shutdown: any forced aborts here are
1262                                    // already logged per-worker, and the supervisor keeps running, so the count does
1263                                    // not feed the unclean-shutdown signal.
1264                                    let _ = worker_state.shutdown_workers().await;
1265                                    // A one-for-all restart resets to the static roster; dynamic children are not
1266                                    // restored (they're lost on a supervisor-level restart, matching Erlang/OTP), and
1267                                    // temporary children are not restarted.
1268                                    children.clear();
1269                                    self.active.store(0, Ordering::Relaxed);
1270                                    let respawn = self.respawn_children_one_for_all(&mut children, &mut worker_state);
1271                                    if let Err(e) = respawn {
1272                                        break Err(e);
1273                                    }
1274                                    significant_remaining =
1275                                        children.values().filter(|entry| entry.config.significant).count();
1276                                }
1277                            },
1278                            RestartAction::Shutdown => {
1279                                error!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Supervisor shutting down due to restart limits.");
1280                                break Err(SupervisorError::Shutdown);
1281                            }
1282                        }
1283                    }
1284                }
1285
1286                // A handle asked us to spawn one or more dynamic children. The published sender keeps the queue open
1287                // for the whole run, so this only yields zero once we close it during teardown. Draining in batches
1288                // keeps a burst of spawns to a single wake-up.
1289                _ = cmd_rx.recv_many(&mut spawn_batch, SPAWN_DRAIN_BATCH) => {
1290                    for spawn in spawn_batch.drain(..) {
1291                        self.spawn_dynamic_child(spawn, &mut worker_state, &mut children, &mut significant_remaining);
1292                    }
1293                }
1294            }
1295        };
1296
1297        // The run is ending -- either cleanly (shutdown was signalled) or with an error (a child failed to initialize
1298        // or restart, the restart limit was exceeded, or a significant child exited). On every path: stop accepting
1299        // spawns and discard anything still queued, rather than starting children only to tear them down immediately,
1300        // and then shut down all children.
1301        cmd_rx.close();
1302        let mut discarded = 0;
1303        while cmd_rx.try_recv().is_ok() {
1304            discarded += 1;
1305        }
1306        if discarded > 0 {
1307            debug!(
1308                supervisor_id = %self.supervisor_id,
1309                discarded,
1310                "Discarded queued dynamic children during shutdown."
1311            );
1312        }
1313        let aborted = worker_state.shutdown_workers().await;
1314
1315        // A requested shutdown that nonetheless had to forcefully abort one or more workers (here or anywhere in the
1316        // subtree below us) is surfaced as an unclean shutdown so it propagates up the tree rather than being reported
1317        // as success. An outcome that already carries an error (initialization, restart limit, significant child)
1318        // takes precedence -- that's the root cause -- and the forced aborts are left to the per-worker warnings.
1319        match outcome {
1320            Ok(()) if aborted > 0 => {
1321                warn!(supervisor_id = %self.supervisor_id, aborted, "Shutdown completed uncleanly; workers were forcefully aborted.");
1322                Err(SupervisorError::ShutdownTimedOut { aborted })
1323            }
1324            outcome => outcome,
1325        }
1326    }
1327
1328    fn as_nested_process(&self, process: Process, process_shutdown: ShutdownHandle) -> WorkerFuture {
1329        // Simple wrapper around `run_inner` to satisfy the return type signature needed when running the supervisor as
1330        // a nested child process in another supervisor.
1331        debug!(supervisor_id = %self.supervisor_id, "Nested supervisor starting.");
1332
1333        // Create a standalone clone of ourselves so we can fulfill the future signature.
1334        let sup = self.inner_clone();
1335
1336        Box::pin(async move {
1337            sup.run_inner(process, process_shutdown)
1338                .await
1339                .map_err(WorkerError::from)
1340        })
1341    }
1342
1343    /// Runs the supervisor forever.
1344    ///
1345    /// # Errors
1346    ///
1347    /// If the supervisor exceeds its restart limits, or fails to initialize a child process, an error is returned.
1348    pub async fn run(&mut self) -> Result<(), SupervisorError> {
1349        // Create a no-op `ShutdownHandle` to satisfy the `run_inner` function. This is never used since we want to run
1350        // forever, but we need to satisfy the signature.
1351        let process_shutdown = ShutdownHandle::noop();
1352        let process = Process::supervisor(&self.supervisor_id, None).context(InvalidName {
1353            name: self.supervisor_id.to_string(),
1354        })?;
1355
1356        debug!(supervisor_id = %self.supervisor_id, "Supervisor starting.");
1357        self.run_inner(process.clone(), process_shutdown)
1358            .into_process_future(process)
1359            .await
1360    }
1361
1362    /// Runs the supervisor until shutdown is triggered.
1363    ///
1364    /// When `shutdown` resolves, the supervisor will shutdown all child processes according to their shutdown strategy,
1365    /// and then return.
1366    ///
1367    /// # Errors
1368    ///
1369    /// If the supervisor exceeds its restart limits, or fails to initialize a child process, an error is returned.
1370    pub async fn run_with_shutdown<F: Future + Send + 'static>(&mut self, shutdown: F) -> Result<(), SupervisorError> {
1371        // Drive the caller-provided shutdown future into a trigger so the supervisor can begin shutting down its
1372        // children once `shutdown` resolves. The trigger fires at most once (guarded), and otherwise fires on drop if
1373        // the supervisor returns on its own first.
1374        let (shutdown_coordinator, shutdown_handle) = ShutdownHandle::paired();
1375        let run = self.run_with_shutdown_inner(shutdown_handle, None);
1376        pin!(run, shutdown);
1377
1378        let mut shutdown_coordinator = Some(shutdown_coordinator);
1379        loop {
1380            select! {
1381                result = &mut run => return result,
1382                _ = &mut shutdown, if shutdown_coordinator.is_some() => {
1383                    shutdown_coordinator.take().expect("coordinator present per select guard").shutdown();
1384                }
1385            }
1386        }
1387    }
1388
1389    /// Runs the supervisor until the given `ShutdownHandle` signal is received.
1390    ///
1391    /// This is an internal variant of `run_with_shutdown` that takes a `ShutdownHandle` directly, used when spawning
1392    /// supervisors in dedicated runtimes where the shutdown signal is already wrapped in a `ShutdownHandle`.
1393    ///
1394    /// If `dataspace` is provided, the supervisor will use it instead of creating a new one. This is used to propagate
1395    /// the parent's dataspace across OS thread boundaries for dedicated runtimes.
1396    ///
1397    /// # Errors
1398    ///
1399    /// If the supervisor exceeds its restart limits, or fails to initialize a child process, an error is returned.
1400    pub(crate) async fn run_with_shutdown_inner(
1401        &mut self, process_shutdown: ShutdownHandle, dataspace: Option<DataspaceRegistry>,
1402    ) -> Result<(), SupervisorError> {
1403        let process =
1404            Process::supervisor_with_dataspace(&self.supervisor_id, None, dataspace).context(InvalidName {
1405                name: self.supervisor_id.to_string(),
1406            })?;
1407
1408        debug!(supervisor_id = %self.supervisor_id, "Supervisor starting.");
1409        self.run_inner(process.clone(), process_shutdown)
1410            .into_process_future(process)
1411            .await
1412    }
1413
1414    fn inner_clone(&self) -> Self {
1415        // This is no different than if we just implemented `Clone` directly, but it allows us to avoid exposing a
1416        // _public_ implementation of `Clone`, which we don't want normal users to be able to do. We only need this
1417        // internally to support nested supervisors.
1418        Self {
1419            supervisor_id: Arc::clone(&self.supervisor_id),
1420            child_specs: self.child_specs.clone(),
1421            restart_strategy: self.restart_strategy,
1422            auto_shutdown: self.auto_shutdown,
1423            shutdown_budget: self.shutdown_budget,
1424            runtime_mode: self.runtime_mode.clone(),
1425            current_tx: Arc::clone(&self.current_tx),
1426            id_counter: Arc::clone(&self.id_counter),
1427            active: Arc::clone(&self.active),
1428        }
1429    }
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434    use std::{
1435        future::pending,
1436        sync::atomic::{AtomicBool, AtomicUsize, Ordering},
1437    };
1438
1439    use async_trait::async_trait;
1440    use saluki_common::sync::shutdown::ShutdownCoordinator;
1441    use saluki_metrics::test::TestRecorder;
1442    use tokio::{
1443        sync::oneshot,
1444        task::JoinHandle,
1445        time::{sleep, timeout},
1446    };
1447
1448    use super::*;
1449    use crate::runtime::{self, FnWorker};
1450    use crate::test_support::wait_until;
1451
1452    /// Behavior for a mock worker during initialization.
1453    #[derive(Clone)]
1454    enum InitBehavior {
1455        /// Initialization succeeds immediately.
1456        Instant,
1457
1458        /// Initialization takes the given duration before succeeding.
1459        Slow(Duration),
1460
1461        /// Initialization fails with the given message.
1462        Fail(&'static str),
1463    }
1464
1465    /// Behavior for a mock worker during runtime (after initialization).
1466    #[derive(Clone)]
1467    enum RunBehavior {
1468        /// Runs until shutdown is received.
1469        UntilShutdown,
1470
1471        /// Fails with the given error message after the given delay.
1472        FailAfter(Duration, &'static str),
1473
1474        /// Completes successfully after the given delay.
1475        CompleteAfter(Duration),
1476
1477        /// On shutdown, sleeps for the given duration before exiting (to exercise concurrent draining).
1478        SlowShutdown(Duration),
1479
1480        /// Ignores shutdown entirely and runs forever (to exercise abort-at-deadline).
1481        IgnoreShutdown,
1482
1483        /// Panics after the given delay, unless shutdown arrives first.
1484        PanicAfter(Duration),
1485    }
1486
1487    /// A configurable mock worker for testing supervisor behavior.
1488    struct MockWorker {
1489        name: &'static str,
1490        init_behavior: InitBehavior,
1491        run_behavior: RunBehavior,
1492        start_count: Arc<AtomicUsize>,
1493        finish_count: Arc<AtomicUsize>,
1494        brutal_shutdown: bool,
1495        graceful_timeout: Duration,
1496    }
1497
1498    impl MockWorker {
1499        /// Creates a worker that runs until shutdown.
1500        fn long_running(name: &'static str) -> Self {
1501            Self {
1502                name,
1503                init_behavior: InitBehavior::Instant,
1504                run_behavior: RunBehavior::UntilShutdown,
1505                start_count: Arc::new(AtomicUsize::new(0)),
1506                finish_count: Arc::new(AtomicUsize::new(0)),
1507                brutal_shutdown: false,
1508                graceful_timeout: Duration::from_millis(500),
1509            }
1510        }
1511
1512        /// Creates a worker that fails after the given delay.
1513        fn failing(name: &'static str, delay: Duration) -> Self {
1514            Self {
1515                name,
1516                init_behavior: InitBehavior::Instant,
1517                run_behavior: RunBehavior::FailAfter(delay, "worker failed"),
1518                start_count: Arc::new(AtomicUsize::new(0)),
1519                finish_count: Arc::new(AtomicUsize::new(0)),
1520                brutal_shutdown: false,
1521                graceful_timeout: Duration::from_millis(500),
1522            }
1523        }
1524
1525        /// Creates a worker that completes successfully after the given delay.
1526        fn completing(name: &'static str, delay: Duration) -> Self {
1527            Self {
1528                name,
1529                init_behavior: InitBehavior::Instant,
1530                run_behavior: RunBehavior::CompleteAfter(delay),
1531                start_count: Arc::new(AtomicUsize::new(0)),
1532                finish_count: Arc::new(AtomicUsize::new(0)),
1533                brutal_shutdown: false,
1534                graceful_timeout: Duration::from_millis(500),
1535            }
1536        }
1537
1538        /// Creates a worker that sleeps for `delay` after observing shutdown before exiting.
1539        fn slow_shutdown(name: &'static str, delay: Duration) -> Self {
1540            Self {
1541                name,
1542                init_behavior: InitBehavior::Instant,
1543                run_behavior: RunBehavior::SlowShutdown(delay),
1544                start_count: Arc::new(AtomicUsize::new(0)),
1545                finish_count: Arc::new(AtomicUsize::new(0)),
1546                brutal_shutdown: false,
1547                graceful_timeout: Duration::from_millis(500),
1548            }
1549        }
1550
1551        /// Creates a worker that never reacts to shutdown.
1552        fn ignore_shutdown(name: &'static str) -> Self {
1553            Self {
1554                name,
1555                init_behavior: InitBehavior::Instant,
1556                run_behavior: RunBehavior::IgnoreShutdown,
1557                start_count: Arc::new(AtomicUsize::new(0)),
1558                finish_count: Arc::new(AtomicUsize::new(0)),
1559                brutal_shutdown: false,
1560                graceful_timeout: Duration::from_millis(500),
1561            }
1562        }
1563
1564        /// Creates a worker that panics after the given delay.
1565        fn panicking(name: &'static str, delay: Duration) -> Self {
1566            Self {
1567                name,
1568                init_behavior: InitBehavior::Instant,
1569                run_behavior: RunBehavior::PanicAfter(delay),
1570                start_count: Arc::new(AtomicUsize::new(0)),
1571                finish_count: Arc::new(AtomicUsize::new(0)),
1572                brutal_shutdown: false,
1573                graceful_timeout: Duration::from_millis(500),
1574            }
1575        }
1576
1577        /// Creates a worker that fails during initialization.
1578        fn init_failure(name: &'static str) -> Self {
1579            Self {
1580                name,
1581                init_behavior: InitBehavior::Fail("init failed"),
1582                run_behavior: RunBehavior::UntilShutdown,
1583                start_count: Arc::new(AtomicUsize::new(0)),
1584                finish_count: Arc::new(AtomicUsize::new(0)),
1585                brutal_shutdown: false,
1586                graceful_timeout: Duration::from_millis(500),
1587            }
1588        }
1589
1590        /// Creates a worker with slow initialization.
1591        fn slow_init(name: &'static str, init_delay: Duration) -> Self {
1592            Self {
1593                name,
1594                init_behavior: InitBehavior::Slow(init_delay),
1595                run_behavior: RunBehavior::UntilShutdown,
1596                start_count: Arc::new(AtomicUsize::new(0)),
1597                finish_count: Arc::new(AtomicUsize::new(0)),
1598                brutal_shutdown: false,
1599                graceful_timeout: Duration::from_millis(500),
1600            }
1601        }
1602
1603        /// Returns a shared handle to the start count for this worker.
1604        ///
1605        /// The start count ticks up the instant the worker's run future begins executing, which is *before* any
1606        /// programmed delay elapses. It records that the worker started (or was restarted), not that it ran to any
1607        /// particular outcome.
1608        fn start_count(&self) -> Arc<AtomicUsize> {
1609            Arc::clone(&self.start_count)
1610        }
1611
1612        /// Returns a shared handle to the finish count for this worker.
1613        ///
1614        /// The finish count ticks up only when the worker runs to its *own* programmed terminal state -- a
1615        /// [`RunBehavior::FailAfter`] failure, a [`RunBehavior::CompleteAfter`] completion, or a
1616        /// [`RunBehavior::SlowShutdown`] drain that finished -- and not when it is cut short by an abort. Tests use it
1617        /// to wait for a worker to actually fail or complete (rather than merely start) before asserting on restart
1618        /// behavior, so the failure/completion path is genuinely exercised.
1619        fn finish_count(&self) -> Arc<AtomicUsize> {
1620            Arc::clone(&self.finish_count)
1621        }
1622
1623        /// Configures this worker to use a `Brutal` shutdown strategy (immediate abort, no graceful wait).
1624        fn with_brutal_shutdown(mut self) -> Self {
1625            self.brutal_shutdown = true;
1626            self
1627        }
1628
1629        /// Overrides the worker's graceful shutdown timeout (defaults to 500 milliseconds).
1630        fn with_graceful_timeout(mut self, timeout: Duration) -> Self {
1631            self.graceful_timeout = timeout;
1632            self
1633        }
1634    }
1635
1636    #[async_trait]
1637    impl Supervisable for MockWorker {
1638        fn name(&self) -> &str {
1639            self.name
1640        }
1641
1642        fn shutdown_strategy(&self) -> ShutdownStrategy {
1643            if self.brutal_shutdown {
1644                ShutdownStrategy::Brutal
1645            } else {
1646                ShutdownStrategy::Graceful(self.graceful_timeout)
1647            }
1648        }
1649
1650        async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
1651            match &self.init_behavior {
1652                InitBehavior::Instant => {}
1653                InitBehavior::Slow(delay) => {
1654                    sleep(*delay).await;
1655                }
1656                InitBehavior::Fail(msg) => {
1657                    return Err(InitializationError::Failed {
1658                        source: GenericError::msg(*msg),
1659                    });
1660                }
1661            }
1662
1663            let start_count = Arc::clone(&self.start_count);
1664            let finish_count = Arc::clone(&self.finish_count);
1665            let run_behavior = self.run_behavior.clone();
1666
1667            Ok(Box::pin(async move {
1668                start_count.fetch_add(1, Ordering::SeqCst);
1669
1670                match run_behavior {
1671                    RunBehavior::UntilShutdown => {
1672                        process_shutdown.await;
1673                        Ok(())
1674                    }
1675                    RunBehavior::FailAfter(delay, msg) => {
1676                        select! {
1677                            _ = sleep(delay) => {
1678                                // Ran to our own programmed failure rather than being cut short by shutdown; record
1679                                // it so tests can wait for the failure to actually happen before asserting.
1680                                finish_count.fetch_add(1, Ordering::SeqCst);
1681                                Err(GenericError::msg(msg))
1682                            }
1683                            _ = process_shutdown => {
1684                                Ok(())
1685                            }
1686                        }
1687                    }
1688                    RunBehavior::CompleteAfter(delay) => {
1689                        select! {
1690                            _ = sleep(delay) => {
1691                                // Ran to our own programmed completion rather than being cut short by shutdown.
1692                                finish_count.fetch_add(1, Ordering::SeqCst);
1693                                Ok(())
1694                            }
1695                            _ = process_shutdown => Ok(()),
1696                        }
1697                    }
1698                    RunBehavior::SlowShutdown(delay) => {
1699                        process_shutdown.await;
1700                        sleep(delay).await;
1701                        // Finished draining rather than being aborted partway through it.
1702                        finish_count.fetch_add(1, Ordering::SeqCst);
1703                        Ok(())
1704                    }
1705                    RunBehavior::IgnoreShutdown => {
1706                        // Hold the handle (so the supervisor counts us as outstanding) but never react to it.
1707                        let _hold = process_shutdown;
1708                        pending().await
1709                    }
1710                    RunBehavior::PanicAfter(delay) => {
1711                        select! {
1712                            _ = sleep(delay) => panic!("worker panicked"),
1713                            _ = process_shutdown => Ok(()),
1714                        }
1715                    }
1716                }
1717            }))
1718        }
1719    }
1720
1721    /// Helper: run a supervisor with a oneshot-based shutdown trigger.
1722    ///
1723    /// Returns the shutdown sender and a join handle for the run. The supervisor is polled to a running state (its
1724    /// static children spawned) via readiness polling rather than a blind startup sleep, so callers can rely on it
1725    /// being live on return.
1726    async fn run_supervisor_with_trigger(
1727        supervisor: Supervisor,
1728    ) -> (oneshot::Sender<()>, JoinHandle<Result<(), SupervisorError>>) {
1729        // Grab a handle before moving the supervisor into the run task so we can observe when it actually starts.
1730        let sup_handle = supervisor.handle();
1731        let mut supervisor = supervisor;
1732
1733        let (tx, rx) = oneshot::channel();
1734        let handle = tokio::spawn(async move { supervisor.run_with_shutdown(rx).await });
1735
1736        wait_until("supervisor is running", || sup_handle.is_running()).await;
1737        (tx, handle)
1738    }
1739
1740    /// Helper: awaits a spawned supervisor run to completion under a bounded timeout, unwrapping the join.
1741    ///
1742    /// Collapses the `timeout(..).await.unwrap().unwrap()` suffix repeated across the restart/shutdown tests into one
1743    /// call with useful panic messages.
1744    async fn join_supervisor(handle: JoinHandle<Result<(), SupervisorError>>) -> Result<(), SupervisorError> {
1745        timeout(Duration::from_secs(2), handle)
1746            .await
1747            .expect("supervisor should exit promptly")
1748            .expect("supervisor task should not panic")
1749    }
1750
1751    // -- Supervisor run mode tests ---------------------------------------------------------
1752
1753    #[tokio::test]
1754    async fn standalone_supervisor_shuts_down_cleanly() {
1755        let mut sup = Supervisor::new("test-sup").unwrap();
1756        sup.add_worker(MockWorker::long_running("worker1"));
1757        sup.add_worker(MockWorker::long_running("worker2"));
1758
1759        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1760        tx.send(()).unwrap();
1761
1762        let result = join_supervisor(handle).await;
1763        assert!(result.is_ok());
1764    }
1765
1766    #[tokio::test]
1767    async fn nested_supervisor_shuts_down_cleanly() {
1768        let mut child_sup = Supervisor::new("child-sup").unwrap();
1769        child_sup.add_worker(MockWorker::long_running("inner-worker"));
1770
1771        let mut parent_sup = Supervisor::new("parent-sup").unwrap();
1772        parent_sup.add_worker(MockWorker::long_running("outer-worker"));
1773        parent_sup.add_worker(child_sup);
1774
1775        let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
1776        tx.send(()).unwrap();
1777
1778        let result = join_supervisor(handle).await;
1779        assert!(result.is_ok());
1780    }
1781
1782    #[tokio::test]
1783    async fn empty_supervisor_idles_until_shutdown() {
1784        // A supervisor with no static children is valid: it idles, waiting for dynamic children, and shuts down
1785        // cleanly when signalled. (Before dynamic children were folded in, this returned a `NoChildren` error.)
1786        let sup = Supervisor::new("empty-sup").unwrap();
1787
1788        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1789        assert!(!handle.is_finished(), "an empty supervisor must idle rather than exit");
1790
1791        tx.send(()).unwrap();
1792        let result = join_supervisor(handle).await;
1793        assert!(result.is_ok());
1794    }
1795
1796    // -- Child restart behavior tests ------------------------------------------------------
1797
1798    #[tokio::test]
1799    async fn one_for_one_restarts_only_failed_child() {
1800        let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1801        let failing_count = failing.start_count();
1802
1803        let stable = MockWorker::long_running("stable-worker");
1804        let stable_count = stable.start_count();
1805
1806        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1807            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1808        );
1809        sup.add_worker(stable);
1810        sup.add_worker(failing);
1811
1812        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1813
1814        // Wait until the failing worker has actually been restarted (its second start), then shut down.
1815        wait_until("the failing worker has been restarted", || {
1816            failing_count.load(Ordering::SeqCst) >= 2
1817        })
1818        .await;
1819        let _ = tx.send(());
1820
1821        let result = join_supervisor(handle).await;
1822        assert!(result.is_ok());
1823
1824        // The failing worker should have been started multiple times.
1825        assert!(
1826            failing_count.load(Ordering::SeqCst) >= 2,
1827            "failing worker should have been restarted"
1828        );
1829        // The stable worker should only have been started once (never restarted).
1830        assert_eq!(
1831            stable_count.load(Ordering::SeqCst),
1832            1,
1833            "stable worker should not have been restarted"
1834        );
1835    }
1836
1837    #[tokio::test]
1838    async fn one_for_all_restarts_all_children() {
1839        let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1840        let failing_count = failing.start_count();
1841
1842        let stable = MockWorker::long_running("stable-worker");
1843        let stable_count = stable.start_count();
1844
1845        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1846            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1847        );
1848        sup.add_worker(stable);
1849        sup.add_worker(failing);
1850
1851        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1852
1853        // Wait until a one-for-all cycle has restarted both workers (each on its second start), then shut down.
1854        wait_until("both workers have been restarted", || {
1855            failing_count.load(Ordering::SeqCst) >= 2 && stable_count.load(Ordering::SeqCst) >= 2
1856        })
1857        .await;
1858        let _ = tx.send(());
1859
1860        let result = join_supervisor(handle).await;
1861        assert!(result.is_ok());
1862
1863        // Both workers should have been started multiple times.
1864        assert!(
1865            failing_count.load(Ordering::SeqCst) >= 2,
1866            "failing worker should have been restarted"
1867        );
1868        assert!(
1869            stable_count.load(Ordering::SeqCst) >= 2,
1870            "stable worker should also have been restarted"
1871        );
1872    }
1873
1874    #[tokio::test]
1875    async fn one_for_all_does_not_restart_temporary_children() {
1876        // A permanent worker that fails repeatedly drives one-for-all restarts; a temporary sibling is shut down with
1877        // the group on each cycle but, per OTP semantics, must never be brought back.
1878        let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1879        let failing_count = failing.start_count();
1880
1881        let temp = MockWorker::long_running("temp-worker");
1882        let temp_count = temp.start_count();
1883
1884        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1885            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1886        );
1887        sup.add_worker(ChildSpecification::worker(temp).with_restart_type(RestartType::Temporary));
1888        sup.add_worker(failing);
1889
1890        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1891
1892        // Wait until the permanent worker has driven at least one one-for-all restart, then shut down.
1893        wait_until("the permanent worker has been restarted", || {
1894            failing_count.load(Ordering::SeqCst) >= 2
1895        })
1896        .await;
1897        let _ = tx.send(());
1898
1899        let result = join_supervisor(handle).await;
1900        assert!(result.is_ok());
1901        assert!(
1902            failing_count.load(Ordering::SeqCst) >= 2,
1903            "permanent worker should have been restarted by one-for-all"
1904        );
1905        assert_eq!(
1906            temp_count.load(Ordering::SeqCst),
1907            1,
1908            "temporary child must not be restarted by a one-for-all group restart"
1909        );
1910    }
1911
1912    #[tokio::test]
1913    async fn one_for_all_restarts_transient_children() {
1914        // A transient child that exits cleanly is not restarted on its own, but a one-for-all restart triggered by a
1915        // sibling restarts it anyway -- matching OTP, where only temporary children are exempt from group restarts.
1916        let transient = MockWorker::completing("transient-worker", Duration::from_millis(30));
1917        let transient_count = transient.start_count();
1918
1919        // Fails after the transient has already exited cleanly, so the group restart is what brings the transient back.
1920        let failing = MockWorker::failing("failing-worker", Duration::from_millis(80));
1921
1922        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1923            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1924        );
1925        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1926        sup.add_worker(failing);
1927
1928        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1929
1930        wait_until("the transient worker has been restarted by the group", || {
1931            transient_count.load(Ordering::SeqCst) >= 2
1932        })
1933        .await;
1934        let _ = tx.send(());
1935
1936        let result = join_supervisor(handle).await;
1937        assert!(result.is_ok());
1938        assert!(
1939            transient_count.load(Ordering::SeqCst) >= 2,
1940            "transient child must be restarted by a one-for-all group restart, even after a clean exit"
1941        );
1942    }
1943
1944    #[tokio::test]
1945    async fn transient_abnormal_exit_triggers_one_for_all() {
1946        // A transient child's *own* abnormal exit is restartable, so under one-for-all it triggers a whole-group
1947        // restart -- the sibling is restarted too, not just the transient.
1948        let transient = MockWorker::failing("transient-worker", Duration::from_millis(50));
1949        let transient_count = transient.start_count();
1950
1951        let stable = MockWorker::long_running("stable-worker");
1952        let stable_count = stable.start_count();
1953
1954        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1955            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1956        );
1957        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1958        sup.add_worker(stable);
1959
1960        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1961
1962        wait_until("the abnormal exit has restarted both workers", || {
1963            transient_count.load(Ordering::SeqCst) >= 2 && stable_count.load(Ordering::SeqCst) >= 2
1964        })
1965        .await;
1966        let _ = tx.send(());
1967
1968        let result = join_supervisor(handle).await;
1969        assert!(result.is_ok());
1970        assert!(
1971            transient_count.load(Ordering::SeqCst) >= 2,
1972            "transient worker must be restarted after its own abnormal exit"
1973        );
1974        assert!(
1975            stable_count.load(Ordering::SeqCst) >= 2,
1976            "the transient's abnormal exit must trigger a one-for-all that also restarts the sibling"
1977        );
1978    }
1979
1980    #[tokio::test]
1981    async fn restart_limit_exceeded_shuts_down_supervisor() {
1982        let mut sup = Supervisor::new("test-sup")
1983            .unwrap()
1984            .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
1985        // This worker fails immediately, which will exhaust the restart budget quickly.
1986        sup.add_worker(MockWorker::failing("fast-fail", Duration::ZERO));
1987
1988        let (tx, rx) = oneshot::channel::<()>();
1989        let handle = tokio::spawn(async move { sup.run_with_shutdown(rx).await });
1990
1991        let result = join_supervisor(handle).await;
1992        drop(tx);
1993
1994        assert!(matches!(result, Err(SupervisorError::Shutdown)));
1995    }
1996
1997    // -- Restart type tests ----------------------------------------------------------------
1998
1999    #[tokio::test]
2000    async fn temporary_child_is_not_restarted() {
2001        // A temporary worker that fails quickly, alongside a long-running worker that keeps the supervisor alive.
2002        let temp = MockWorker::failing("temp-worker", Duration::from_millis(50));
2003        let temp_started = temp.start_count();
2004        let temp_failed = temp.finish_count();
2005
2006        let stable = MockWorker::long_running("stable-worker");
2007
2008        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
2009            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
2010        );
2011        sup.add_worker(stable);
2012        sup.add_worker(ChildSpecification::worker(temp).with_restart_type(RestartType::Temporary));
2013
2014        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2015
2016        // Wait for the worker to *actually fail*, not merely start. `start_count` ticks up the instant the worker
2017        // begins running -- well before its 50ms failure -- so shutting down as soon as it reached 1 would tear the
2018        // supervisor down before the failure -> no-restart path ever ran, hiding a regression that restarted a
2019        // temporary child (or charged the failure against restart intensity). `finish_count` ticks only once the
2020        // worker runs to its own failure, so waiting on it genuinely exercises that path before we shut down.
2021        wait_until("the temporary worker has failed once", || {
2022            temp_failed.load(Ordering::SeqCst) == 1
2023        })
2024        .await;
2025        let _ = tx.send(());
2026
2027        let result = join_supervisor(handle).await;
2028        assert!(result.is_ok());
2029        assert_eq!(
2030            temp_started.load(Ordering::SeqCst),
2031            1,
2032            "temporary worker must not be restarted after it fails"
2033        );
2034    }
2035
2036    #[tokio::test]
2037    async fn transient_child_is_not_restarted_on_clean_exit() {
2038        let transient = MockWorker::completing("transient-worker", Duration::from_millis(50));
2039        let transient_started = transient.start_count();
2040        let transient_finished = transient.finish_count();
2041
2042        let stable = MockWorker::long_running("stable-worker");
2043
2044        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
2045            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
2046        );
2047        sup.add_worker(stable);
2048        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
2049
2050        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2051
2052        // Wait for the worker to *actually complete*, not merely start: `start_count` ticks the instant it begins
2053        // running, so shutting down as soon as it reached 1 would drive the supervisor's teardown before the clean
2054        // exit -> no-restart path ran, hiding a regression that restarted a transient child after a clean exit.
2055        // `finish_count` ticks only once the worker runs to its own completion.
2056        wait_until("the transient worker has completed once", || {
2057            transient_finished.load(Ordering::SeqCst) == 1
2058        })
2059        .await;
2060        let _ = tx.send(());
2061
2062        let result = join_supervisor(handle).await;
2063        assert!(result.is_ok());
2064        assert_eq!(
2065            transient_started.load(Ordering::SeqCst),
2066            1,
2067            "transient worker must not be restarted after a clean exit"
2068        );
2069    }
2070
2071    #[tokio::test]
2072    async fn transient_child_is_restarted_on_failure() {
2073        let transient = MockWorker::failing("transient-worker", Duration::from_millis(50));
2074        let transient_count = transient.start_count();
2075
2076        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
2077            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
2078        );
2079        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
2080
2081        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2082
2083        wait_until("the transient worker has been restarted", || {
2084            transient_count.load(Ordering::SeqCst) >= 2
2085        })
2086        .await;
2087        let _ = tx.send(());
2088
2089        let result = join_supervisor(handle).await;
2090        assert!(result.is_ok());
2091        assert!(
2092            transient_count.load(Ordering::SeqCst) >= 2,
2093            "transient worker must be restarted after an abnormal exit"
2094        );
2095    }
2096
2097    #[tokio::test]
2098    async fn permanent_child_is_restarted_on_clean_exit() {
2099        // A permanent worker that completes cleanly must still be restarted -- this is what distinguishes
2100        // `Permanent` from `Transient`, which is left stopped after a clean exit.
2101        let permanent = MockWorker::completing("permanent-worker", Duration::from_millis(50));
2102        let permanent_count = permanent.start_count();
2103
2104        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
2105            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
2106        );
2107        // Added with the default restart policy, which is `Permanent`.
2108        sup.add_worker(permanent);
2109
2110        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2111
2112        wait_until("the permanent worker has been restarted", || {
2113            permanent_count.load(Ordering::SeqCst) >= 2
2114        })
2115        .await;
2116        let _ = tx.send(());
2117
2118        let result = join_supervisor(handle).await;
2119        assert!(result.is_ok());
2120        assert!(
2121            permanent_count.load(Ordering::SeqCst) >= 2,
2122            "permanent worker must be restarted even after a clean exit"
2123        );
2124    }
2125
2126    #[tokio::test]
2127    async fn temporary_failures_do_not_consume_restart_intensity() {
2128        // With intensity=1, two *restartable* failures within the period would shut the supervisor down. Here several
2129        // temporary workers all fail quickly. Because temporary exits aren't eligible for restart, they must not consume
2130        // the restart-intensity budget, and the supervisor must stay up.
2131        let mut sup = Supervisor::new("test-sup")
2132            .unwrap()
2133            .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
2134
2135        let workers = [
2136            MockWorker::failing("temp-0", Duration::from_millis(20)),
2137            MockWorker::failing("temp-1", Duration::from_millis(20)),
2138            MockWorker::failing("temp-2", Duration::from_millis(20)),
2139            MockWorker::failing("temp-3", Duration::from_millis(20)),
2140            MockWorker::failing("temp-4", Duration::from_millis(20)),
2141        ];
2142        let started: Vec<_> = workers.iter().map(|w| w.start_count()).collect();
2143        let failed: Vec<_> = workers.iter().map(|w| w.finish_count()).collect();
2144        for worker in workers {
2145            sup.add_worker(ChildSpecification::worker(worker).with_restart_type(RestartType::Temporary));
2146        }
2147        // A long-running worker so the supervisor doesn't simply idle once the temporaries are gone.
2148        sup.add_worker(MockWorker::long_running("stable-worker"));
2149
2150        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2151        // Wait for every temporary worker to *actually fail* on its own. Keying off `start_count` would let shutdown
2152        // cut them short before their failures ran, so the supervisor would never get the chance to (mis)charge those
2153        // failures against its intensity=1 budget -- hiding the very regression this guards against.
2154        wait_until("every temporary worker has failed once", || {
2155            failed.iter().all(|c| c.load(Ordering::SeqCst) == 1)
2156        })
2157        .await;
2158        let _ = tx.send(());
2159
2160        let result = join_supervisor(handle).await;
2161        assert!(
2162            result.is_ok(),
2163            "supervisor must not trip its restart limit on temporary exits"
2164        );
2165        for count in started {
2166            assert_eq!(
2167                count.load(Ordering::SeqCst),
2168                1,
2169                "each temporary worker runs exactly once"
2170            );
2171        }
2172    }
2173
2174    #[tokio::test]
2175    async fn transient_clean_exits_do_not_consume_restart_intensity() {
2176        // With intensity=1, two *restartable* exits within the period would shut the supervisor down. Here several
2177        // transient workers all complete cleanly. A transient child's clean exit isn't eligible for restart, so it
2178        // must not consume the restart-intensity budget, and the supervisor must stay up.
2179        let mut sup = Supervisor::new("test-sup")
2180            .unwrap()
2181            .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
2182
2183        let workers = [
2184            MockWorker::completing("transient-0", Duration::from_millis(20)),
2185            MockWorker::completing("transient-1", Duration::from_millis(20)),
2186            MockWorker::completing("transient-2", Duration::from_millis(20)),
2187            MockWorker::completing("transient-3", Duration::from_millis(20)),
2188            MockWorker::completing("transient-4", Duration::from_millis(20)),
2189        ];
2190        let started: Vec<_> = workers.iter().map(|w| w.start_count()).collect();
2191        let finished: Vec<_> = workers.iter().map(|w| w.finish_count()).collect();
2192        for worker in workers {
2193            sup.add_worker(ChildSpecification::worker(worker).with_restart_type(RestartType::Transient));
2194        }
2195        // A long-running worker so the supervisor doesn't simply idle once the transients have completed.
2196        sup.add_worker(MockWorker::long_running("stable-worker"));
2197
2198        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2199        // Wait for every transient to *actually complete* on its own. Keying off `start_count` would let shutdown cut
2200        // the workers short before their clean exits ran, so the supervisor would never get the chance to (mis)charge
2201        // those exits against its intensity=1 budget -- hiding the very regression this guards against.
2202        wait_until("every transient worker has completed once", || {
2203            finished.iter().all(|c| c.load(Ordering::SeqCst) == 1)
2204        })
2205        .await;
2206        let _ = tx.send(());
2207
2208        let result = join_supervisor(handle).await;
2209        assert!(
2210            result.is_ok(),
2211            "supervisor must not trip its restart limit on clean transient exits"
2212        );
2213        for count in started {
2214            assert_eq!(
2215                count.load(Ordering::SeqCst),
2216                1,
2217                "each transient worker runs exactly once"
2218            );
2219        }
2220    }
2221
2222    #[tokio::test]
2223    async fn supervisor_idles_when_all_temporary_children_exit() {
2224        // When every static child is temporary and they all exit, the worker set drains. The supervisor must not panic
2225        // or exit on its own; it must keep running and remain able to accept new (dynamic) work until shutdown is
2226        // triggered.
2227        let temp_a = MockWorker::completing("temp-a", Duration::from_millis(10));
2228        let a_finished = temp_a.finish_count();
2229        let temp_b = MockWorker::completing("temp-b", Duration::from_millis(10));
2230        let b_finished = temp_b.finish_count();
2231
2232        let mut sup = Supervisor::new("test-sup").unwrap();
2233        let handle = sup.handle();
2234        sup.add_worker(ChildSpecification::worker(temp_a).with_restart_type(RestartType::Temporary));
2235        sup.add_worker(ChildSpecification::worker(temp_b).with_restart_type(RestartType::Temporary));
2236
2237        let (tx, run) = run_supervisor_with_trigger(sup).await;
2238
2239        // Wait for both temporary children to actually complete -- draining the worker set to empty -- before probing.
2240        // Keying off `start_count` could spawn the probe child before the set ever emptied, letting a supervisor that
2241        // (wrongly) exited once its last child left slip through.
2242        wait_until("both temporary children have completed", || {
2243            a_finished.load(Ordering::SeqCst) == 1 && b_finished.load(Ordering::SeqCst) == 1
2244        })
2245        .await;
2246
2247        // The supervisor must still be alive after its worker set empties: spawning a new dynamic child succeeds and
2248        // runs, which is only possible if the supervise loop kept running rather than exiting when the last child left.
2249        let dynamic = MockWorker::long_running("late-comer");
2250        let dynamic_count = dynamic.start_count();
2251        handle.spawn(dynamic);
2252        wait_until("the late dynamic child has started", || {
2253            dynamic_count.load(Ordering::SeqCst) == 1
2254        })
2255        .await;
2256        assert!(
2257            handle.is_running(),
2258            "supervisor must keep running after all temporary children exit"
2259        );
2260
2261        tx.send(()).unwrap();
2262        let result = join_supervisor(run).await;
2263        assert!(result.is_ok());
2264    }
2265
2266    // -- Significant child / auto-shutdown tests -------------------------------------------
2267
2268    #[tokio::test]
2269    async fn significant_child_drives_auto_shutdown() {
2270        // With `AnySignificant`, a significant child terminating (even cleanly, and without being restarted) must
2271        // shut the supervisor down, surfacing the significant-exit error.
2272        let mut sup = Supervisor::new("test-sup")
2273            .unwrap()
2274            .with_auto_shutdown(AutoShutdown::AnySignificant);
2275        sup.add_worker(MockWorker::long_running("stable"));
2276        sup.add_worker(
2277            ChildSpecification::worker(MockWorker::completing("significant", Duration::from_millis(50)))
2278                .with_restart_type(RestartType::Temporary)
2279                .with_significant(true),
2280        );
2281
2282        // Hold the shutdown sender so the only thing that can stop the supervisor is the significant child.
2283        let (_tx, rx) = oneshot::channel::<()>();
2284        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2285            .await
2286            .unwrap();
2287        assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
2288    }
2289
2290    #[tokio::test]
2291    async fn significant_child_added_before_the_auto_shutdown_policy_still_drives_it() {
2292        // `with_auto_shutdown` consumes the supervisor while `add_worker` borrows it, so adding children first and
2293        // setting the policy afterwards is a perfectly good way to build one up. Nothing about registration may
2294        // assume the policy is already final -- an earlier version of the inert-significance check read it at
2295        // registration time and flagged this ordering as a mistake.
2296        let mut sup = Supervisor::new("test-sup").unwrap();
2297        sup.add_worker(MockWorker::long_running("stable"));
2298        sup.add_worker(
2299            runtime::supervisable(MockWorker::completing("significant", Duration::from_millis(50)))
2300                .temporary()
2301                .with_significant(true)
2302                .build(),
2303        );
2304        let mut sup = sup.with_auto_shutdown(AutoShutdown::AnySignificant);
2305
2306        let (_tx, rx) = oneshot::channel::<()>();
2307        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2308            .await
2309            .unwrap();
2310        assert!(
2311            matches!(result, Err(SupervisorError::SignificantChildExited)),
2312            "the policy set after registration should still have applied, got {result:?}"
2313        );
2314    }
2315
2316    #[tokio::test]
2317    async fn non_significant_exit_does_not_auto_shutdown() {
2318        // Even with `AnySignificant` set, a non-significant child exiting must not shut the supervisor down.
2319        let plain = MockWorker::completing("plain", Duration::from_millis(10));
2320        let plain_finished = plain.finish_count();
2321
2322        let mut sup = Supervisor::new("test-sup")
2323            .unwrap()
2324            .with_auto_shutdown(AutoShutdown::AnySignificant);
2325        let handle = sup.handle();
2326        sup.add_worker(MockWorker::long_running("stable"));
2327        sup.add_worker(ChildSpecification::worker(plain).with_restart_type(RestartType::Temporary));
2328
2329        let (tx, run) = run_supervisor_with_trigger(sup).await;
2330
2331        // Let the non-significant child actually run to completion -- not merely start. Its completion is what could
2332        // (wrongly) trip `AnySignificant`, so we must observe the real exit before probing liveness; keying off
2333        // `start_count` could assert before the completion was ever processed.
2334        wait_until("the non-significant child has completed", || {
2335            plain_finished.load(Ordering::SeqCst) == 1
2336        })
2337        .await;
2338
2339        // The supervisor must still be alive after the non-significant child exits (had it been treated as
2340        // significant, `AnySignificant` would have torn the supervisor down). Spawning a dynamic child and observing
2341        // it start proves the supervise loop is still running.
2342        let dynamic = MockWorker::long_running("late-comer");
2343        let dynamic_count = dynamic.start_count();
2344        handle.spawn(dynamic);
2345        wait_until("the late dynamic child has started", || {
2346            dynamic_count.load(Ordering::SeqCst) == 1
2347        })
2348        .await;
2349        assert!(
2350            handle.is_running(),
2351            "a non-significant child exiting must not trigger auto-shutdown"
2352        );
2353
2354        tx.send(()).unwrap();
2355        let result = join_supervisor(run).await;
2356        assert!(result.is_ok());
2357    }
2358
2359    #[tokio::test]
2360    async fn all_significant_waits_for_last() {
2361        // With `AllSignificant`, the supervisor shuts down only once *all* significant children have terminated.
2362        let mut sup = Supervisor::new("test-sup")
2363            .unwrap()
2364            .with_auto_shutdown(AutoShutdown::AllSignificant);
2365        sup.add_worker(
2366            ChildSpecification::worker(MockWorker::completing("sig-a", Duration::from_millis(50)))
2367                .with_restart_type(RestartType::Temporary)
2368                .with_significant(true),
2369        );
2370        sup.add_worker(
2371            ChildSpecification::worker(MockWorker::completing("sig-b", Duration::from_millis(250)))
2372                .with_restart_type(RestartType::Temporary)
2373                .with_significant(true),
2374        );
2375
2376        let (_tx, rx) = oneshot::channel::<()>();
2377        let start = std::time::Instant::now();
2378        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2379            .await
2380            .unwrap();
2381        let elapsed = start.elapsed();
2382
2383        assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
2384        // The first significant child exits at ~50ms but must NOT trigger shutdown; only the second (~250ms) does.
2385        assert!(
2386            elapsed >= Duration::from_millis(200),
2387            "auto-shutdown must wait for all significant children (took {elapsed:?})"
2388        );
2389    }
2390
2391    // -- Initialization failure tests ------------------------------------------------------
2392
2393    #[tokio::test]
2394    async fn init_failure_propagates_with_child_name() {
2395        let mut sup = Supervisor::new("test-sup").unwrap();
2396        sup.add_worker(MockWorker::long_running("good-worker"));
2397        sup.add_worker(MockWorker::init_failure("bad-worker"));
2398
2399        let (_tx, rx) = oneshot::channel::<()>();
2400        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2401            .await
2402            .unwrap();
2403
2404        match result {
2405            Err(SupervisorError::FailedToInitialize { child_name, .. }) => {
2406                assert_eq!(child_name, "bad-worker");
2407            }
2408            other => panic!("expected FailedToInitialize, got: {:?}", other),
2409        }
2410    }
2411
2412    #[tokio::test]
2413    async fn init_failure_does_not_trigger_restart() {
2414        let init_fail = MockWorker::init_failure("bad-worker");
2415        let start_count = init_fail.start_count();
2416
2417        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
2418            RestartStrategy::one_to_one().with_intensity_and_period(10, Duration::from_secs(10)),
2419        );
2420        sup.add_worker(init_fail);
2421
2422        let (_tx, rx) = oneshot::channel::<()>();
2423        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2424            .await
2425            .unwrap();
2426
2427        assert!(matches!(result, Err(SupervisorError::FailedToInitialize { .. })));
2428        // The worker never got past init, so start_count should be 0.
2429        assert_eq!(start_count.load(Ordering::SeqCst), 0);
2430    }
2431
2432    // -- Shutdown responsiveness tests -----------------------------------------------------
2433
2434    #[tokio::test]
2435    async fn shutdown_completes_promptly_in_steady_state() {
2436        let mut sup = Supervisor::new("test-sup").unwrap();
2437        sup.add_worker(MockWorker::long_running("worker1"));
2438        sup.add_worker(MockWorker::long_running("worker2"));
2439
2440        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2441        tx.send(()).unwrap();
2442
2443        // Shutdown should complete well within 1 second (workers respond to shutdown signal immediately).
2444        let result = timeout(Duration::from_secs(1), handle).await;
2445        assert!(result.is_ok(), "shutdown should complete promptly");
2446    }
2447
2448    #[tokio::test]
2449    async fn shutdown_during_slow_init_completes_promptly() {
2450        let mut sup = Supervisor::new("test-sup").unwrap();
2451        // This worker takes 30 seconds to initialize — but we'll trigger shutdown immediately.
2452        sup.add_worker(MockWorker::slow_init("slow-worker", Duration::from_secs(30)));
2453
2454        let (tx, rx) = oneshot::channel();
2455        let handle = tokio::spawn(async move { sup.run_with_shutdown(rx).await });
2456
2457        // Give the supervisor just enough time to spawn the task, then trigger shutdown.
2458        sleep(Duration::from_millis(20)).await;
2459        tx.send(()).unwrap();
2460
2461        // Shutdown should complete quickly even though the worker hasn't finished initializing.
2462        // The supervisor loop sees the shutdown signal and aborts the still-initializing task.
2463        let result = timeout(Duration::from_secs(2), handle).await;
2464        assert!(result.is_ok(), "shutdown during slow init should complete promptly");
2465    }
2466
2467    // -- Dynamic children tests ------------------------------------------------------------
2468
2469    #[tokio::test]
2470    async fn dynamic_children_spawn_after_start() {
2471        let sup = Supervisor::new("dyn-sup").unwrap();
2472        let handle = sup.handle();
2473        let (tx, run) = run_supervisor_with_trigger(sup).await;
2474        wait_until("supervisor is running", || handle.is_running()).await;
2475
2476        let c1 = MockWorker::long_running("c1");
2477        let c2 = MockWorker::long_running("c2");
2478        let c1_count = c1.start_count();
2479        let c2_count = c2.start_count();
2480        handle.spawn(c1);
2481        handle.spawn(c2);
2482
2483        wait_until("both dynamic children have started", || {
2484            c1_count.load(Ordering::SeqCst) == 1 && c2_count.load(Ordering::SeqCst) == 1
2485        })
2486        .await;
2487        assert_eq!(handle.active_children(), 2);
2488
2489        tx.send(()).unwrap();
2490        let result = join_supervisor(run).await;
2491        assert!(result.is_ok());
2492        assert_eq!(
2493            handle.active_children(),
2494            0,
2495            "all dynamic children must be drained on shutdown"
2496        );
2497    }
2498
2499    #[tokio::test]
2500    async fn temporary_dynamic_child_failure_is_isolated() {
2501        // A dynamic child added with the default config (temporary, not significant) is fault-isolated: its failure is
2502        // reaped and removed without restarting it or disturbing the supervisor or its siblings.
2503        let sup = Supervisor::new("dyn-sup").unwrap();
2504        let handle = sup.handle();
2505        let (tx, run) = run_supervisor_with_trigger(sup).await;
2506        wait_until("supervisor is running", || handle.is_running()).await;
2507
2508        let failing = MockWorker::failing("boom", Duration::from_millis(20));
2509        let failing_count = failing.start_count();
2510        handle.spawn(failing);
2511        wait_until("the failing dynamic child has run once", || {
2512            failing_count.load(Ordering::SeqCst) == 1
2513        })
2514        .await;
2515        wait_until("all dynamic children have drained", || handle.active_children() == 0).await;
2516
2517        sleep(Duration::from_millis(50)).await;
2518        assert!(
2519            handle.is_running(),
2520            "supervisor stays up after an isolated child failure"
2521        );
2522        assert_eq!(
2523            failing_count.load(Ordering::SeqCst),
2524            1,
2525            "a temporary child is never restarted"
2526        );
2527
2528        // It still accepts new children.
2529        handle.spawn(MockWorker::long_running("c2"));
2530        wait_until("one dynamic child is running", || handle.active_children() == 1).await;
2531
2532        tx.send(()).unwrap();
2533        let result = join_supervisor(run).await;
2534        assert!(result.is_ok());
2535    }
2536
2537    #[tokio::test]
2538    async fn temporary_dynamic_child_panic_is_isolated() {
2539        // A panicking temporary, non-significant child is isolated exactly like an error exit.
2540        let sup = Supervisor::new("dyn-sup").unwrap();
2541        let handle = sup.handle();
2542        let (tx, run) = run_supervisor_with_trigger(sup).await;
2543        wait_until("supervisor is running", || handle.is_running()).await;
2544
2545        handle.spawn(MockWorker::panicking("boom", Duration::from_millis(20)));
2546        wait_until("all dynamic children have drained", || handle.active_children() == 0).await;
2547
2548        sleep(Duration::from_millis(50)).await;
2549        assert!(handle.is_running(), "supervisor stays up after an isolated child panic");
2550
2551        tx.send(()).unwrap();
2552        let result = join_supervisor(run).await;
2553        assert!(result.is_ok());
2554    }
2555
2556    #[tokio::test]
2557    async fn significant_dynamic_child_failure_shuts_down_supervisor() {
2558        // A dynamic child added as significant, under `AutoShutdown::AnySignificant`, drives the supervisor to shut
2559        // down when it terminates -- the opt-in mechanism that replaces the old escalate-on-error behavior.
2560        let sup = Supervisor::new("dyn-sup")
2561            .unwrap()
2562            .with_auto_shutdown(AutoShutdown::AnySignificant);
2563        let handle = sup.handle();
2564        let (_tx, run) = run_supervisor_with_trigger(sup).await;
2565        wait_until("supervisor is running", || handle.is_running()).await;
2566
2567        handle.spawn(
2568            ChildSpecification::worker(MockWorker::failing("boom", Duration::from_millis(20))).with_significant(true),
2569        );
2570
2571        let result = join_supervisor(run).await;
2572        assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
2573    }
2574
2575    #[tokio::test]
2576    async fn dynamic_spawn_outside_a_run_is_accepted_and_dropped() {
2577        // Spawning is infallible in the same sense `tokio::spawn` is: the child is always accepted, but a child handed
2578        // to a supervisor that isn't running is never started. That holds both before a run and after one, and a child
2579        // spawned before the run must not be held over and started by it -- children belong to a run, not to the
2580        // supervisor across runs.
2581        let sup = Supervisor::new("dyn-sup").unwrap();
2582        let handle = sup.handle();
2583
2584        assert!(!handle.is_running());
2585        let before = MockWorker::long_running("before-start");
2586        let before_count = before.start_count();
2587        handle.spawn(before);
2588
2589        // Once it's running, spawns do start children.
2590        let (tx, run) = run_supervisor_with_trigger(sup).await;
2591        wait_until("supervisor is running", || handle.is_running()).await;
2592        let worker = MockWorker::long_running("after-start");
2593        let started = worker.start_count();
2594        handle.spawn(worker);
2595        wait_until("the dynamic child has started", || started.load(Ordering::SeqCst) == 1).await;
2596        assert_eq!(
2597            before_count.load(Ordering::SeqCst),
2598            0,
2599            "a child spawned before the run must not be started by it"
2600        );
2601
2602        tx.send(()).unwrap();
2603        let result = join_supervisor(run).await;
2604        assert!(result.is_ok());
2605
2606        // And once it has shut down there is nothing left to start children either.
2607        wait_until("the supervisor has stopped", || !handle.is_running()).await;
2608        let after = MockWorker::long_running("after-shutdown");
2609        let after_count = after.start_count();
2610        handle.spawn(after);
2611
2612        sleep(Duration::from_millis(50)).await;
2613        assert_eq!(
2614            after_count.load(Ordering::SeqCst),
2615            0,
2616            "a child spawned after shutdown must never start"
2617        );
2618    }
2619
2620    #[tokio::test]
2621    async fn dynamic_spawn_allocates_an_id_eagerly() {
2622        // The id comes back synchronously, before the supervisor has picked the child up, so it can't depend on
2623        // registration having happened. Ids come from the supervisor's shared counter, so with no static children the
2624        // first dynamic child takes id 0.
2625        let sup = Supervisor::new("dyn-sup").unwrap();
2626        let handle = sup.handle();
2627        let (tx, run) = run_supervisor_with_trigger(sup).await;
2628        wait_until("supervisor is running", || handle.is_running()).await;
2629
2630        let worker = MockWorker::long_running("c");
2631        let started = worker.start_count();
2632        let id = handle.spawn(worker);
2633        assert_eq!(id.as_u64(), 0);
2634        wait_until("the dynamic child has started", || started.load(Ordering::SeqCst) == 1).await;
2635
2636        tx.send(()).unwrap();
2637        let result = join_supervisor(run).await;
2638        assert!(result.is_ok());
2639    }
2640
2641    #[tokio::test]
2642    async fn dynamic_child_with_an_unusable_name_runs_under_a_placeholder() {
2643        // A name that sanitizes to nothing can't be used as a process name. Spawning is infallible, so rather than
2644        // quietly discarding work the caller was told had been accepted, the child runs under a placeholder segment.
2645        // The poll metric is what proves the substituted name is what the child actually ran as, rather than the child
2646        // merely having started somehow.
2647        let recorder = TestRecorder::default();
2648        let _guard = metrics::set_default_local_recorder(&recorder);
2649
2650        let sup = Supervisor::new("dyn-sup").unwrap();
2651        let handle = sup.handle();
2652        let (tx, run) = run_supervisor_with_trigger(sup).await;
2653        wait_until("supervisor is running", || handle.is_running()).await;
2654
2655        let worker = MockWorker::long_running("");
2656        let started = worker.start_count();
2657        handle.spawn(worker);
2658        wait_until("the unnamed dynamic child has started", || {
2659            started.load(Ordering::SeqCst) == 1
2660        })
2661        .await;
2662
2663        // The supervisor stays up and still accepts normally-named children.
2664        assert!(handle.is_running());
2665        handle.spawn(MockWorker::long_running("ok"));
2666        wait_until("both dynamic children are running", || handle.active_children() == 2).await;
2667
2668        tx.send(()).unwrap();
2669        let result = join_supervisor(run).await;
2670        assert!(result.is_ok());
2671
2672        let polls = recorder.counter(("runtime_task_poll_count", &[("task_name", "dyn_sup.unnamed")]));
2673        assert!(
2674            polls.is_some_and(|polls| polls > 0),
2675            "the child should have run under the placeholder name, got {polls:?}"
2676        );
2677    }
2678
2679    #[tokio::test]
2680    async fn dynamic_spawns_are_not_capped() {
2681        // Spawn requests are queued without a bound, so a burst well past what any fixed-capacity channel would hold
2682        // still starts every child. Being accepted means being started -- a depth limit could only deliver that by
2683        // discarding work the caller was already told had been taken.
2684        const CHILDREN: usize = 2048;
2685
2686        let sup = Supervisor::new("dyn-sup").unwrap();
2687        let handle = sup.handle();
2688        let (tx, run) = run_supervisor_with_trigger(sup).await;
2689        wait_until("supervisor is running", || handle.is_running()).await;
2690
2691        for _ in 0..CHILDREN {
2692            // A generous deadline: this test is about every child starting, not about how fast a couple of thousand
2693            // of them can be reaped, which is slow enough in a debug build to trip a short per-child timeout.
2694            handle.spawn(MockWorker::long_running("burst").with_graceful_timeout(Duration::from_secs(30)));
2695        }
2696
2697        wait_until("every child in the burst has started", || {
2698            handle.active_children() == CHILDREN
2699        })
2700        .await;
2701
2702        tx.send(()).unwrap();
2703        let result = timeout(Duration::from_secs(30), run)
2704            .await
2705            .expect("supervisor should stop")
2706            .expect("supervisor task should not panic");
2707        assert!(result.is_ok(), "the burst should have drained cleanly: {result:?}");
2708    }
2709
2710    #[tokio::test]
2711    async fn dynamically_spawned_supervisor_runs_and_drains() {
2712        // A dynamic child can be a whole supervision subtree, not just a worker: spawning a `Supervisor` runs it
2713        // nested, and shutting the parent down drains it along with everything under it.
2714        let child_worker = MockWorker::long_running("nested-child");
2715        let child_started = child_worker.start_count();
2716        let mut nested = Supervisor::new("nested-sup").unwrap();
2717        nested.add_worker(child_worker);
2718
2719        let sup = Supervisor::new("dyn-sup").unwrap();
2720        let handle = sup.handle();
2721        let (tx, run) = run_supervisor_with_trigger(sup).await;
2722        wait_until("supervisor is running", || handle.is_running()).await;
2723
2724        handle.spawn(nested);
2725        wait_until("the nested supervisor's own child has started", || {
2726            child_started.load(Ordering::SeqCst) == 1
2727        })
2728        .await;
2729
2730        tx.send(()).unwrap();
2731        let result = join_supervisor(run).await;
2732        assert!(
2733            result.is_ok(),
2734            "the nested subtree should have drained cleanly: {result:?}"
2735        );
2736    }
2737
2738    /// Counts how many times a nested subtree is started, by counting starts of its sole child.
2739    ///
2740    /// The child fails immediately and the subtree has a restart intensity of zero, so the subtree gives up the first
2741    /// time it fails. That makes the child's start count equal to the number of times the *subtree* ran, which is what
2742    /// these tests are actually asserting on -- without the zero intensity the subtree's own one-for-one restart would
2743    /// be indistinguishable from the parent restarting the subtree.
2744    fn failing_subtree(name: &'static str) -> (Supervisor, Arc<AtomicUsize>) {
2745        let worker = MockWorker::failing("nested-child", Duration::from_millis(5));
2746        let started = worker.start_count();
2747        let mut nested = Supervisor::new(name)
2748            .unwrap()
2749            .with_restart_strategy(RestartStrategy::new(RestartMode::OneForOne, 0, Duration::from_secs(30)));
2750        nested.add_worker(worker);
2751
2752        (nested, started)
2753    }
2754
2755    #[tokio::test]
2756    async fn dynamic_nested_supervisor_defaults_to_temporary() {
2757        // Spawning a bare `Supervisor` takes the dynamic default, so a subtree that gives up stays gone. For a
2758        // listener that means it silently disappears while whatever owns it keeps reporting healthy -- which is the
2759        // reason `nested_supervisor` exists.
2760        let (nested, started) = failing_subtree("nested-temp");
2761
2762        let sup = Supervisor::new("dyn-temp-sup").unwrap();
2763        let handle = sup.handle();
2764        let (tx, run) = run_supervisor_with_trigger(sup).await;
2765
2766        handle.spawn(nested);
2767        wait_until("the subtree has started once", || started.load(Ordering::SeqCst) == 1).await;
2768
2769        // Give the subtree time to fail and be reaped. Nothing brings it back.
2770        sleep(Duration::from_millis(200)).await;
2771        assert_eq!(
2772            started.load(Ordering::SeqCst),
2773            1,
2774            "a temporary subtree must not be restarted"
2775        );
2776
2777        tx.send(()).unwrap();
2778        assert!(join_supervisor(run).await.is_ok());
2779    }
2780
2781    #[tokio::test]
2782    async fn dynamic_nested_supervisor_can_be_made_permanent() {
2783        // What `nested_supervisor` buys: the same subtree, spawned permanent, is brought back when it terminates.
2784        let (nested, started) = failing_subtree("nested-perm");
2785
2786        let sup = Supervisor::new("dyn-perm-sup")
2787            .unwrap()
2788            .with_restart_strategy(RestartStrategy::new(
2789                RestartMode::OneForOne,
2790                100,
2791                Duration::from_secs(30),
2792            ));
2793        let handle = sup.handle();
2794        let (tx, run) = run_supervisor_with_trigger(sup).await;
2795
2796        handle.nested_supervisor(nested).spawn();
2797        wait_until("the subtree has been restarted", || started.load(Ordering::SeqCst) >= 2).await;
2798
2799        tx.send(()).unwrap();
2800        assert!(join_supervisor(run).await.is_ok());
2801    }
2802
2803    #[tokio::test]
2804    async fn dynamic_nested_supervisor_can_be_significant() {
2805        // The other half: a subtree its parent can't function without takes the parent with it when it terminates,
2806        // rather than leaving it running with nothing behind it.
2807        let (nested, _started) = failing_subtree("nested-sig");
2808
2809        let sup = Supervisor::new("dyn-sig-sup")
2810            .unwrap()
2811            .with_auto_shutdown(AutoShutdown::AnySignificant);
2812        let handle = sup.handle();
2813        let (tx, run) = run_supervisor_with_trigger(sup).await;
2814
2815        handle
2816            .nested_supervisor(nested)
2817            .temporary()
2818            .with_significant(true)
2819            .spawn();
2820
2821        let result = timeout(Duration::from_secs(5), run)
2822            .await
2823            .expect("supervisor should stop once the significant subtree terminates")
2824            .expect("supervisor task should not panic");
2825        assert!(
2826            matches!(result, Err(SupervisorError::SignificantChildExited)),
2827            "the significant subtree's termination should have stopped the parent, got {result:?}"
2828        );
2829
2830        // The run already ended; the trigger is redundant but keeps the sender alive to the end of the test.
2831        let _ = tx.send(());
2832    }
2833
2834    #[tokio::test]
2835    async fn budget_of_duration_max_does_not_leave_a_budget_bounded_child_unbounded() {
2836        // `Duration::MAX` is the natural spelling of "no ceiling", and a budget too large to become a deadline bounds
2837        // nothing at all. A budget-bounded child under one must therefore fall back to its own deadline: without that,
2838        // setting `MAX` would be strictly *worse* than setting no budget, since the child would never be abandoned and
2839        // the drain would hang.
2840        let mut sup = Supervisor::new("test-sup").unwrap().with_shutdown_budget(Duration::MAX);
2841        sup.add_worker(
2842            ChildSpecification::one_shot_worker(
2843                MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(100)),
2844            )
2845            .with_budget_bounded_shutdown(),
2846        );
2847
2848        let (tx, run) = run_supervisor_with_trigger(sup).await;
2849        tx.send(()).unwrap();
2850
2851        let started = tokio::time::Instant::now();
2852        let result = join_supervisor(run).await;
2853        let elapsed = started.elapsed();
2854
2855        assert!(
2856            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2857            "the worker's own deadline should have aborted it, got {result:?}"
2858        );
2859        assert!(
2860            elapsed < Duration::from_secs(1),
2861            "the child should have been bounded by its own 100ms deadline; took {elapsed:?}"
2862        );
2863    }
2864
2865    #[tokio::test]
2866    async fn budget_bounded_child_falls_back_to_its_own_deadline_without_a_budget() {
2867        // A one-shot child asks to be bounded by its supervisor's budget rather than carrying a deadline of its own.
2868        // On a supervisor with no budget there'd be nothing bounding it at all, so it falls back to the strategy the
2869        // worker reports -- here a short one, which is what lets this test finish rather than hang.
2870        let mut sup = Supervisor::new("test-sup").unwrap();
2871        sup.add_worker(
2872            ChildSpecification::one_shot_worker(
2873                MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(100)),
2874            )
2875            .with_budget_bounded_shutdown(),
2876        );
2877
2878        let (tx, run) = run_supervisor_with_trigger(sup).await;
2879        tx.send(()).unwrap();
2880
2881        let started = tokio::time::Instant::now();
2882        let result = join_supervisor(run).await;
2883        let elapsed = started.elapsed();
2884
2885        assert!(
2886            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2887            "the worker's own deadline should have aborted it, got {result:?}"
2888        );
2889        assert!(
2890            elapsed < Duration::from_secs(1),
2891            "the child should have been bounded by its own 100ms deadline; took {elapsed:?}"
2892        );
2893    }
2894
2895    #[tokio::test]
2896    async fn concurrent_shutdown_drains_many_children_quickly() {
2897        const CHILDREN: usize = 500;
2898        const SHUTDOWN_DELAY: Duration = Duration::from_millis(50);
2899
2900        let sup = Supervisor::new("dyn-sup").unwrap();
2901        let handle = sup.handle();
2902        let (tx, run) = run_supervisor_with_trigger(sup).await;
2903        wait_until("supervisor is running", || handle.is_running()).await;
2904
2905        for _ in 0..CHILDREN {
2906            handle.spawn(MockWorker::slow_shutdown("conn", SHUTDOWN_DELAY));
2907        }
2908        wait_until("all dynamic children are running", || {
2909            handle.active_children() == CHILDREN
2910        })
2911        .await;
2912
2913        // Each child sleeps after observing shutdown. Concurrent shutdown drains them all in roughly one delay; an
2914        // ordered shutdown would take CHILDREN * delay (25s here). Assert it finishes well under that.
2915        let start = std::time::Instant::now();
2916        tx.send(()).unwrap();
2917        let result = timeout(Duration::from_secs(5), run).await.unwrap().unwrap();
2918        let elapsed = start.elapsed();
2919
2920        assert!(result.is_ok());
2921        assert_eq!(handle.active_children(), 0, "active count must return to zero");
2922        assert!(
2923            elapsed < Duration::from_secs(2),
2924            "shutdown must be concurrent (took {elapsed:?})"
2925        );
2926    }
2927
2928    #[tokio::test]
2929    async fn concurrent_shutdown_aborts_unresponsive_children() {
2930        let sup = Supervisor::new("dyn-sup").unwrap();
2931        let handle = sup.handle();
2932        let (tx, run) = run_supervisor_with_trigger(sup).await;
2933        wait_until("supervisor is running", || handle.is_running()).await;
2934
2935        handle.spawn(MockWorker::ignore_shutdown("stuck"));
2936        wait_until("one dynamic child is running", || handle.active_children() == 1).await;
2937
2938        // The child never reacts to shutdown, so it must be aborted once its graceful deadline (500ms) elapses rather
2939        // than hanging the supervisor.
2940        let start = std::time::Instant::now();
2941        tx.send(()).unwrap();
2942        let result = join_supervisor(run).await;
2943        let elapsed = start.elapsed();
2944
2945        // Forcefully aborting an unresponsive child is surfaced as an unclean shutdown rather than reported as success.
2946        assert!(
2947            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2948            "aborting a stuck child must surface as an unclean shutdown, got {result:?}"
2949        );
2950        assert_eq!(handle.active_children(), 0);
2951        assert!(
2952            elapsed < Duration::from_secs(1),
2953            "stuck child must be aborted at the deadline (took {elapsed:?})"
2954        );
2955    }
2956
2957    #[tokio::test]
2958    async fn concurrent_shutdown_honors_per_child_deadline() {
2959        // Each child must be aborted at its OWN graceful deadline, not a single shared one. A responsive child with an
2960        // effectively-infinite timeout (modeling a nested supervisor, which uses `Graceful(Duration::MAX)`) coexists
2961        // with an unresponsive child with a short timeout. Under a shared `max` deadline the short-timeout child would
2962        // never be aborted (the shared deadline would be `MAX`) and shutdown would hang.
2963        let sup = Supervisor::new("dyn-sup").unwrap();
2964        let handle = sup.handle();
2965        let (tx, run) = run_supervisor_with_trigger(sup).await;
2966        wait_until("supervisor is running", || handle.is_running()).await;
2967
2968        // Responds to shutdown promptly, but its deadline is effectively infinite.
2969        handle.spawn(MockWorker::long_running("responsive").with_graceful_timeout(Duration::MAX));
2970        // Never responds; must be aborted at its own short deadline.
2971        handle.spawn(MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(200)));
2972        wait_until("both dynamic children are running", || handle.active_children() == 2).await;
2973
2974        let start = std::time::Instant::now();
2975        tx.send(()).unwrap();
2976        let result = join_supervisor(run).await;
2977        let elapsed = start.elapsed();
2978
2979        // Only the stuck child is aborted (the responsive one exits cleanly), so the unclean-shutdown tally is exactly 1.
2980        assert!(
2981            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2982            "aborting the stuck child must surface as an unclean shutdown with a count of 1, got {result:?}"
2983        );
2984        assert_eq!(handle.active_children(), 0);
2985        assert!(
2986            elapsed < Duration::from_secs(1),
2987            "stuck child must be aborted at its own deadline despite an infinite-timeout sibling (took {elapsed:?})"
2988        );
2989    }
2990
2991    #[tokio::test]
2992    async fn unresponsive_child_is_aborted_at_its_deadline() {
2993        // A child that never reacts to shutdown must be aborted once its graceful deadline (500ms) elapses, rather
2994        // than hanging the supervisor indefinitely.
2995        let mut sup = Supervisor::new("test-sup").unwrap();
2996        sup.add_worker(MockWorker::ignore_shutdown("stuck"));
2997
2998        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2999
3000        let start = std::time::Instant::now();
3001        tx.send(()).unwrap();
3002        let result = join_supervisor(handle).await;
3003        let elapsed = start.elapsed();
3004
3005        assert!(
3006            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
3007            "aborting a stuck child must surface as an unclean shutdown, got {result:?}"
3008        );
3009        assert!(
3010            elapsed < Duration::from_secs(1),
3011            "unresponsive child must be aborted at its deadline (took {elapsed:?})"
3012        );
3013    }
3014
3015    #[tokio::test]
3016    async fn brutal_shutdown_aborts_child_immediately() {
3017        // A child with a `Brutal` shutdown strategy is aborted at once on shutdown, with no graceful wait -- so even a
3018        // child that ignores shutdown is torn down promptly rather than after the graceful deadline.
3019        let mut sup = Supervisor::new("test-sup").unwrap();
3020        sup.add_worker(MockWorker::ignore_shutdown("brutal-stuck").with_brutal_shutdown());
3021
3022        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3023
3024        let start = std::time::Instant::now();
3025        tx.send(()).unwrap();
3026        let result = join_supervisor(handle).await;
3027        let elapsed = start.elapsed();
3028
3029        // A brutal abort is the configured, expected way to stop this child -- not a graceful-timeout overrun -- so it
3030        // is NOT counted toward the unclean-shutdown tally, and the shutdown reports success.
3031        assert!(result.is_ok());
3032        assert!(
3033            elapsed < Duration::from_millis(200),
3034            "brutal-shutdown child must be aborted immediately, not after a graceful wait (took {elapsed:?})"
3035        );
3036    }
3037
3038    #[tokio::test]
3039    async fn shutdown_timeout_aborts_aggregate_to_root() {
3040        // Forced aborts must surface as an unclean shutdown and aggregate up the tree: a supervisor adds the workers it
3041        // aborts directly to the counts reported by any child supervisors that also timed out. Here the parent aborts
3042        // one direct child and a nested supervisor aborts one of its own, so the root observes a total of 2.
3043        let mut child_sup = Supervisor::new("child-sup").unwrap();
3044        child_sup
3045            .add_worker(MockWorker::ignore_shutdown("child-stuck").with_graceful_timeout(Duration::from_millis(200)));
3046
3047        let mut parent_sup = Supervisor::new("parent-sup").unwrap();
3048        parent_sup
3049            .add_worker(MockWorker::ignore_shutdown("parent-stuck").with_graceful_timeout(Duration::from_millis(200)));
3050        parent_sup.add_worker(MockWorker::long_running("parent-clean"));
3051        parent_sup.add_worker(child_sup);
3052
3053        let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
3054        tx.send(()).unwrap();
3055
3056        let result = join_supervisor(handle).await;
3057        assert!(
3058            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 2 })),
3059            "forced aborts must aggregate across the tree (1 direct + 1 nested), got {result:?}"
3060        );
3061    }
3062
3063    // -- Restart-policy edge cases ---------------------------------------------------------
3064
3065    #[tokio::test]
3066    async fn restart_intensity_zero_shuts_down_on_first_failure() {
3067        // A restart intensity of zero means the supervisor gives up the moment any restartable child fails: it shuts
3068        // down on the very first failure without ever restarting the worker. (See `RestartState::evaluate_restart`,
3069        // which short-circuits to `Shutdown` when intensity is zero.)
3070        let worker = MockWorker::failing("boom", Duration::from_millis(20));
3071        let start_count = worker.start_count();
3072
3073        let mut sup = Supervisor::new("test-sup")
3074            .unwrap()
3075            .with_restart_strategy(RestartStrategy::new(RestartMode::OneForOne, 0, Duration::from_secs(5)));
3076        sup.add_worker(worker);
3077
3078        let (_tx, rx) = oneshot::channel::<()>();
3079        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
3080            .await
3081            .unwrap();
3082
3083        assert!(matches!(result, Err(SupervisorError::Shutdown)));
3084        assert_eq!(
3085            start_count.load(Ordering::SeqCst),
3086            1,
3087            "with intensity zero the worker must run exactly once and never be restarted"
3088        );
3089    }
3090
3091    #[tokio::test]
3092    async fn one_for_all_restart_loses_dynamic_children() {
3093        // Documented one-for-all semantics: a group restart resets to the static roster only -- dynamic children are
3094        // NOT restored (they're lost on a supervisor-level restart, matching Erlang/OTP). A permanent static worker
3095        // that keeps failing drives repeated one-for-all restarts; a dynamic child spawned before the first restart
3096        // must be torn down and never brought back.
3097        let failing = MockWorker::failing("failing-static", Duration::from_millis(50));
3098        let failing_count = failing.start_count();
3099
3100        let sup = Supervisor::new("dyn-sup").unwrap().with_restart_strategy(
3101            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
3102        );
3103        let handle = sup.handle();
3104        let mut sup = sup;
3105        sup.add_worker(failing);
3106
3107        let (tx, run) = run_supervisor_with_trigger(sup).await;
3108
3109        // Spawn a long-running dynamic child and wait for it to be running.
3110        let dynamic = MockWorker::long_running("dynamic");
3111        let dynamic_count = dynamic.start_count();
3112        handle.spawn(dynamic);
3113        wait_until("the dynamic child is running", || handle.active_children() == 1).await;
3114
3115        // Let the static worker drive at least one one-for-all restart (its second start).
3116        wait_until("the static worker has been restarted", || {
3117            failing_count.load(Ordering::SeqCst) >= 2
3118        })
3119        .await;
3120
3121        // The one-for-all restart must have discarded the dynamic child: the active count returns to zero, and the
3122        // dynamic child ran exactly once (it was never restored).
3123        wait_until("the dynamic child has been discarded", || handle.active_children() == 0).await;
3124        assert_eq!(
3125            dynamic_count.load(Ordering::SeqCst),
3126            1,
3127            "a dynamic child must be lost -- not restored -- across a one-for-all restart"
3128        );
3129
3130        tx.send(()).unwrap();
3131        let result = join_supervisor(run).await;
3132        assert!(result.is_ok());
3133    }
3134
3135    // -- Dedicated-runtime tests -----------------------------------------------------------
3136
3137    #[tokio::test]
3138    async fn dedicated_single_threaded_runtime_runs_nested_worker_and_shuts_down_cleanly() {
3139        // A nested supervisor configured with a dedicated single-threaded runtime spawns its own OS thread and Tokio
3140        // runtime (via `spawn_dedicated_runtime`). Its worker must run there, and a shutdown signalled by the parent
3141        // must propagate across the thread boundary and tear it down cleanly.
3142        let worker = MockWorker::long_running("dedicated-worker");
3143        let worker_count = worker.start_count();
3144
3145        let mut child_sup = Supervisor::new("child-sup")
3146            .unwrap()
3147            .with_dedicated_runtime(RuntimeConfiguration::single_threaded());
3148        child_sup.add_worker(worker);
3149
3150        let mut parent_sup = Supervisor::new("parent-sup").unwrap();
3151        parent_sup.add_worker(child_sup);
3152
3153        let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
3154
3155        // The worker starts on the dedicated runtime's own thread.
3156        wait_until("the dedicated worker has started", || {
3157            worker_count.load(Ordering::SeqCst) == 1
3158        })
3159        .await;
3160
3161        tx.send(()).unwrap();
3162        let result = join_supervisor(handle).await;
3163        assert!(
3164            result.is_ok(),
3165            "dedicated-runtime supervisor should shut down cleanly, got {result:?}"
3166        );
3167    }
3168
3169    #[tokio::test]
3170    async fn dedicated_multi_threaded_runtime_runs_nested_worker() {
3171        // The same nested-dedicated flow, but exercising the multi-threaded dedicated runtime builder path.
3172        let worker = MockWorker::long_running("dedicated-worker");
3173        let worker_count = worker.start_count();
3174
3175        let mut child_sup = Supervisor::new("child-sup")
3176            .unwrap()
3177            .with_dedicated_runtime(RuntimeConfiguration::multi_threaded(2));
3178        child_sup.add_worker(worker);
3179
3180        let mut parent_sup = Supervisor::new("parent-sup").unwrap();
3181        parent_sup.add_worker(child_sup);
3182
3183        let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
3184        wait_until("the dedicated worker has started", || {
3185            worker_count.load(Ordering::SeqCst) == 1
3186        })
3187        .await;
3188
3189        tx.send(()).unwrap();
3190        let result = join_supervisor(handle).await;
3191        assert!(
3192            result.is_ok(),
3193            "multi-threaded dedicated-runtime supervisor should shut down cleanly, got {result:?}"
3194        );
3195    }
3196
3197    #[tokio::test]
3198    async fn dedicated_runtime_forced_abort_aggregates_to_root() {
3199        // A worker inside a dedicated-runtime nested supervisor that ignores shutdown must be forcefully aborted at its
3200        // deadline, and that abort tally must survive the OS-thread boundary (`DedicatedRuntimeHandle` -> `WorkerError`)
3201        // and be observed by the root supervisor as `ShutdownTimedOut`.
3202        let stuck = MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(200));
3203        let stuck_count = stuck.start_count();
3204
3205        let mut child_sup = Supervisor::new("child-sup")
3206            .unwrap()
3207            .with_dedicated_runtime(RuntimeConfiguration::single_threaded());
3208        child_sup.add_worker(stuck);
3209
3210        let mut parent_sup = Supervisor::new("parent-sup").unwrap();
3211        parent_sup.add_worker(child_sup);
3212
3213        let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
3214
3215        // Make sure the stuck worker is actually running on the dedicated runtime before signalling shutdown, so the
3216        // forced-abort path (rather than an early exit) is what we exercise.
3217        wait_until("the stuck worker has started", || {
3218            stuck_count.load(Ordering::SeqCst) == 1
3219        })
3220        .await;
3221
3222        tx.send(()).unwrap();
3223        let result = join_supervisor(handle).await;
3224        assert!(
3225            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
3226            "a stuck worker in a dedicated runtime must surface as an unclean shutdown aggregated to the root, got {result:?}"
3227        );
3228    }
3229
3230    // -- Per-child override tests ----------------------------------------------------------
3231
3232    #[tokio::test]
3233    async fn child_with_runtime_override_runs_on_that_runtime() {
3234        // `with_runtime` places an individual child's task on a caller-provided runtime instead of the supervisor's
3235        // own. The child reports the name of the thread it's actually running on, which must belong to that runtime.
3236        let child_runtime = tokio::runtime::Builder::new_multi_thread()
3237            .worker_threads(1)
3238            .thread_name("child-rt-test")
3239            .enable_all()
3240            .build()
3241            .expect("should build child runtime");
3242
3243        let (thread_tx, thread_rx) = oneshot::channel();
3244        let worker = FnWorker::new("placed", async move {
3245            let thread_name = std::thread::current().name().unwrap_or_default().to_string();
3246            let _ = thread_tx.send(thread_name);
3247            pending::<()>().await;
3248        });
3249
3250        // The worker only has to stay alive long enough to report where it ran, so it's aborted at shutdown rather
3251        // than given a terminal condition to reach.
3252        let mut sup = Supervisor::new("test-sup").unwrap();
3253        sup.add_worker(
3254            ChildSpecification::worker(worker)
3255                .with_restart_type(RestartType::Temporary)
3256                .with_runtime(child_runtime.handle().clone())
3257                .with_shutdown_strategy(ShutdownStrategy::Brutal),
3258        );
3259
3260        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3261
3262        let thread_name = timeout(Duration::from_secs(2), thread_rx)
3263            .await
3264            .expect("child should report its thread promptly")
3265            .expect("child should not be dropped before reporting");
3266        assert!(
3267            thread_name.starts_with("child-rt-test"),
3268            "child must run on the runtime given to `with_runtime`, but ran on thread {thread_name:?}"
3269        );
3270
3271        tx.send(()).unwrap();
3272        assert!(join_supervisor(handle).await.is_ok());
3273
3274        // Dropping a `Runtime` from within an async context panics, so tear it down without blocking.
3275        child_runtime.shutdown_background();
3276    }
3277
3278    #[tokio::test]
3279    async fn child_shutdown_strategy_override_takes_precedence_over_worker() {
3280        // `with_shutdown_strategy` overrides what the worker reports for itself. The worker below asks for a 30-second
3281        // grace period and then ignores shutdown entirely; the override cuts that to 50ms, so the supervisor must
3282        // abort it and report an unclean shutdown well inside `join_supervisor`'s two-second bound. Without the
3283        // override taking precedence, this test times out.
3284        let worker = MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_secs(30));
3285
3286        let mut sup = Supervisor::new("test-sup").unwrap();
3287        sup.add_worker(
3288            ChildSpecification::worker(worker)
3289                .with_restart_type(RestartType::Temporary)
3290                .with_shutdown_strategy(ShutdownStrategy::Graceful(Duration::from_millis(50))),
3291        );
3292
3293        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3294        tx.send(()).unwrap();
3295
3296        let result = join_supervisor(handle).await;
3297        assert!(
3298            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
3299            "the overridden 50ms deadline should have aborted the stuck child, got {result:?}"
3300        );
3301    }
3302
3303    /// A worker that waits for shutdown and then drains a [`ShutdownCoordinator`] before exiting.
3304    ///
3305    /// Stands in for a component that owns background work and waits for it during its own shutdown. Unlike a
3306    /// closure-based worker it genuinely needs the shutdown signal, which is exactly the case [`Supervisable`] exists
3307    /// for.
3308    struct DrainWaiter {
3309        coordinator: Mutex<Option<ShutdownCoordinator>>,
3310        finished: Arc<AtomicBool>,
3311    }
3312
3313    #[async_trait]
3314    impl Supervisable for DrainWaiter {
3315        fn name(&self) -> &str {
3316            "waiter"
3317        }
3318
3319        async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
3320            let coordinator = self
3321                .coordinator
3322                .lock()
3323                .expect("drain waiter mutex poisoned")
3324                .take()
3325                .expect("drain waiter runs once");
3326            let finished = Arc::clone(&self.finished);
3327
3328            Ok(Box::pin(async move {
3329                process_shutdown.await;
3330                coordinator.shutdown_and_wait().await;
3331                finished.store(true, Ordering::SeqCst);
3332                Ok(())
3333            }))
3334        }
3335    }
3336
3337    /// Builds two children modelling an owner that drains a background task during shutdown.
3338    ///
3339    /// `stuck` holds a shutdown handle and never releases it voluntarily, so the only way it goes away is a forced
3340    /// abort. `waiter` blocks on that handle being dropped, standing in for a component's `shutdown_and_wait`. The
3341    /// returned flag records whether `waiter` ran to completion rather than being aborted itself.
3342    fn build_drain_pair(
3343        stuck_strategy: ShutdownStrategy, waiter_strategy: ShutdownStrategy,
3344    ) -> (Supervisor, Arc<AtomicBool>) {
3345        let mut coordinator = ShutdownCoordinator::default();
3346        let held_handle = coordinator.register();
3347
3348        let stuck = FnWorker::new("stuck", async move {
3349            // Hold the handle for as long as this future lives, and ignore shutdown entirely.
3350            let _held = held_handle;
3351            pending::<()>().await;
3352        });
3353
3354        let waiter_finished = Arc::new(AtomicBool::new(false));
3355        let waiter = DrainWaiter {
3356            coordinator: Mutex::new(Some(coordinator)),
3357            finished: Arc::clone(&waiter_finished),
3358        };
3359
3360        let mut sup = Supervisor::new("test-sup").unwrap();
3361        sup.add_worker(
3362            ChildSpecification::worker(stuck)
3363                .with_restart_type(RestartType::Temporary)
3364                .with_shutdown_strategy(stuck_strategy),
3365        );
3366        sup.add_worker(
3367            ChildSpecification::worker(waiter)
3368                .with_restart_type(RestartType::Temporary)
3369                .with_shutdown_strategy(waiter_strategy),
3370        );
3371
3372        (sup, waiter_finished)
3373    }
3374
3375    #[tokio::test]
3376    async fn shorter_child_deadline_releases_a_waiting_sibling() {
3377        // Aborting a stuck child drops the shutdown handle it was holding, which is what releases anything waiting on
3378        // it. A child bounded more tightly than its waiter therefore stays recoverable: the child is aborted, the
3379        // waiter unblocks and finishes cleanly, and only one abort is reported.
3380        let (sup, waiter_finished) = build_drain_pair(
3381            ShutdownStrategy::Graceful(Duration::from_millis(100)),
3382            ShutdownStrategy::Graceful(Duration::from_secs(1)),
3383        );
3384
3385        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3386        tx.send(()).unwrap();
3387
3388        let result = join_supervisor(handle).await;
3389        assert!(
3390            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
3391            "only the stuck child should have been aborted, got {result:?}"
3392        );
3393        assert!(
3394            waiter_finished.load(Ordering::SeqCst),
3395            "the waiter should have been released by the stuck child's abort and run to completion"
3396        );
3397    }
3398
3399    #[tokio::test]
3400    async fn equal_child_deadlines_abort_the_waiter_too() {
3401        // The counterpart: concurrent shutdown computes every deadline from one shared instant, so identical timeouts
3402        // elapse in the same pass and the waiter is aborted alongside the child it was waiting on. This is also what a
3403        // shutdown budget does to a whole subtree, which is why the budget is set at a level where losing the entire
3404        // group at once is the intended outcome.
3405        let (sup, waiter_finished) = build_drain_pair(
3406            ShutdownStrategy::Graceful(Duration::from_millis(100)),
3407            ShutdownStrategy::Graceful(Duration::from_millis(100)),
3408        );
3409
3410        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3411        tx.send(()).unwrap();
3412
3413        let result = join_supervisor(handle).await;
3414        assert!(
3415            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 2 })),
3416            "both children should have been aborted together, got {result:?}"
3417        );
3418        assert!(
3419            !waiter_finished.load(Ordering::SeqCst),
3420            "the waiter should have been aborted mid-wait, not completed"
3421        );
3422    }
3423
3424    // -- Shutdown budget tests -------------------------------------------------------------
3425
3426    #[tokio::test]
3427    async fn budget_bounds_children_that_have_no_deadline_of_their_own() {
3428        // Without a budget, `Graceful(Duration::MAX)` children are waited on indefinitely and a stuck one hangs
3429        // shutdown forever. A budget makes the supervisor responsible for the deadline instead, and each child it has
3430        // to abort is still counted individually -- so an overrun says how many tasks were responsible, not merely
3431        // that the group as a whole overran.
3432        let mut sup = Supervisor::new("test-sup")
3433            .unwrap()
3434            .with_shutdown_budget(Duration::from_millis(100));
3435
3436        for name in ["stuck_one", "stuck_two"] {
3437            sup.add_worker(
3438                ChildSpecification::worker(FnWorker::new(name, pending::<()>()))
3439                    .with_restart_type(RestartType::Temporary)
3440                    .with_shutdown_strategy(ShutdownStrategy::Graceful(Duration::MAX)),
3441            );
3442        }
3443
3444        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3445        tx.send(()).unwrap();
3446
3447        let result = join_supervisor(handle).await;
3448        assert!(
3449            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 2 })),
3450            "the budget should have aborted both deadline-less children, got {result:?}"
3451        );
3452    }
3453
3454    #[tokio::test]
3455    async fn budget_does_not_delay_children_that_stop_on_their_own() {
3456        // The budget is a ceiling, not a wait. Asserting on elapsed time rather than merely on success is what makes
3457        // this meaningful: a supervisor that waited out its budget regardless would still report `Ok`.
3458        let mut sup = Supervisor::new("test-sup")
3459            .unwrap()
3460            .with_shutdown_budget(Duration::from_secs(30));
3461        sup.add_worker(
3462            ChildSpecification::one_shot_worker(MockWorker::long_running("prompt"))
3463                .with_shutdown_strategy(ShutdownStrategy::Graceful(Duration::MAX)),
3464        );
3465
3466        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3467        let started = tokio::time::Instant::now();
3468        tx.send(()).unwrap();
3469
3470        assert!(join_supervisor(handle).await.is_ok());
3471        let elapsed = started.elapsed();
3472        assert!(
3473            elapsed < Duration::from_millis(500),
3474            "shutdown should finish as soon as the child does, not burn the budget; took {elapsed:?}"
3475        );
3476    }
3477
3478    #[tokio::test]
3479    async fn child_deadline_shorter_than_budget_still_wins() {
3480        // A child that carries its own finite deadline is held to whichever elapses first, so a component can still
3481        // bound one particular task more tightly than the budget covering the rest.
3482        let mut sup = Supervisor::new("test-sup")
3483            .unwrap()
3484            .with_shutdown_budget(Duration::from_secs(30));
3485        sup.add_worker(
3486            ChildSpecification::worker(FnWorker::new("stuck", pending::<()>()))
3487                .with_restart_type(RestartType::Temporary)
3488                .with_shutdown_strategy(ShutdownStrategy::Graceful(Duration::from_millis(100))),
3489        );
3490
3491        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3492        tx.send(()).unwrap();
3493
3494        // Again bounded at two seconds: if the 30-second budget had won, this would time out instead.
3495        let result = join_supervisor(handle).await;
3496        assert!(
3497            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
3498            "the child's own 100ms deadline should have won over the budget, got {result:?}"
3499        );
3500    }
3501
3502    #[tokio::test]
3503    async fn every_worker_records_poll_metrics() {
3504        // Poll timing is a property of being supervised, not something a child opts into, so a plain statically
3505        // registered worker gets it too -- tagged with its fully qualified process name.
3506        let recorder = TestRecorder::default();
3507        let _guard = metrics::set_default_local_recorder(&recorder);
3508
3509        // The recorder has to be installed before the worker spawns: its metric handles are resolved once, at spawn.
3510        let mut sup = Supervisor::new("metrics_sup").unwrap();
3511        sup.add_worker(ChildSpecification::one_shot_worker(MockWorker::long_running("timed")));
3512
3513        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3514        tx.send(()).unwrap();
3515        assert!(join_supervisor(handle).await.is_ok());
3516
3517        let polls = recorder.counter(("runtime_task_poll_count", &[("task_name", "metrics_sup.timed")]));
3518        assert!(
3519            polls.is_some_and(|polls| polls > 0),
3520            "a supervised worker should have recorded poll metrics, got {polls:?}"
3521        );
3522    }
3523
3524    #[tokio::test]
3525    async fn budget_bounds_the_whole_drain_rather_than_each_child() {
3526        // The budget is measured once, from the start of the drain, and every child is held to that same instant --
3527        // it does not reset per child. Three children that each ignore a 10-second deadline must all be aborted at
3528        // the shared 150ms budget, not 30 seconds later.
3529        let mut sup = Supervisor::new("test-sup")
3530            .unwrap()
3531            .with_shutdown_budget(Duration::from_millis(150));
3532
3533        for name in ["stuck_one", "stuck_two", "stuck_three"] {
3534            sup.add_worker(
3535                ChildSpecification::one_shot_worker(FnWorker::new(name, pending::<()>()))
3536                    .with_shutdown_strategy(ShutdownStrategy::Graceful(Duration::from_secs(10))),
3537            );
3538        }
3539
3540        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3541        tx.send(()).unwrap();
3542
3543        let result = join_supervisor(handle).await;
3544        assert!(
3545            matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 3 })),
3546            "the budget should have bounded the whole drain, got {result:?}"
3547        );
3548    }
3549
3550    #[tokio::test]
3551    async fn budget_of_duration_max_is_treated_as_no_budget() {
3552        // `Duration::MAX` is the natural spelling of "no ceiling" and used to panic the supervisor task on an instant
3553        // overflow.
3554        let mut sup = Supervisor::new("test-sup").unwrap().with_shutdown_budget(Duration::MAX);
3555        sup.add_worker(ChildSpecification::one_shot_worker(MockWorker::long_running("prompt")));
3556
3557        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3558        tx.send(()).unwrap();
3559        assert!(join_supervisor(handle).await.is_ok());
3560    }
3561
3562    #[tokio::test]
3563    async fn near_max_child_timeout_does_not_panic() {
3564        // Resolving a graceful timeout to an instant makes anything just under `Duration::MAX` overflow unless it is
3565        // added with `checked_add`. See `resolve_abort_deadline`.
3566        let mut sup = Supervisor::new("test-sup").unwrap();
3567        sup.add_worker(
3568            ChildSpecification::one_shot_worker(MockWorker::long_running("prompt"))
3569                .with_shutdown_strategy(ShutdownStrategy::Graceful(Duration::MAX - Duration::from_nanos(1))),
3570        );
3571
3572        let (tx, handle) = run_supervisor_with_trigger(sup).await;
3573        tx.send(()).unwrap();
3574        assert!(join_supervisor(handle).await.is_ok());
3575    }
3576
3577    #[tokio::test]
3578    async fn budget_does_not_cut_off_a_nested_supervisor_mid_drain() {
3579        // A nested supervisor bounds its own subtree, so a parent's budget must not abort it: doing so truncates the
3580        // subtree's drain, discards its abort tally, and -- for a supervisor on a dedicated runtime, whose work is on
3581        // another OS thread -- reports it as stopped without actually stopping it.
3582        let slow = MockWorker::slow_shutdown("slow", Duration::from_millis(300));
3583        let drained = slow.finish_count();
3584
3585        let mut nested = Supervisor::new("nested").unwrap();
3586        nested.add_worker(
3587            ChildSpecification::one_shot_worker(slow)
3588                .with_shutdown_strategy(ShutdownStrategy::Graceful(Duration::from_secs(10))),
3589        );
3590
3591        let mut parent = Supervisor::new("parent")
3592            .unwrap()
3593            .with_shutdown_budget(Duration::from_millis(50));
3594        parent.add_worker(nested);
3595
3596        let (tx, handle) = run_supervisor_with_trigger(parent).await;
3597        tx.send(()).unwrap();
3598
3599        let result = join_supervisor(handle).await;
3600        assert!(
3601            drained.load(Ordering::SeqCst) == 1,
3602            "the nested subtree should have drained rather than being cut off by the parent's budget: {result:?}"
3603        );
3604        assert!(
3605            result.is_ok(),
3606            "the nested drain finished in time, so shutdown was clean: {result:?}"
3607        );
3608    }
3609}