Skip to main content

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::{
17    pin, select,
18    sync::{mpsc, oneshot},
19};
20use tracing::{debug, error, warn};
21
22use super::{
23    dedicated::{spawn_dedicated_runtime, RuntimeConfiguration, RuntimeMode},
24    restart::{RestartAction, RestartMode, RestartState, RestartStrategy, RestartType},
25    worker_state::WorkerState,
26};
27use crate::runtime::{
28    process::{Process, ProcessExt as _},
29    state::DataspaceRegistry,
30};
31
32/// A `Future` that represents the execution of a supervised process.
33pub type SupervisorFuture = Pin<Box<dyn Future<Output = Result<(), GenericError>> + Send>>;
34
35/// A `Future` that represents the full lifecycle of a worker, including initialization.
36///
37/// Unlike [`SupervisorFuture`], which only represents the runtime phase, this future first performs async
38/// initialization and then runs the worker. This allows initialization to happen concurrently when multiple workers are
39/// spawned, and keeps the supervisor loop responsive to shutdown signals during initialization.
40pub(super) type WorkerFuture = Pin<Box<dyn Future<Output = Result<(), WorkerError>> + Send>>;
41
42/// Worker lifecycle errors.
43///
44/// Distinguishes between initialization failures (which shouldn't trigger restart logic) and runtime failures (which
45/// are eligible for restart).
46#[derive(Debug)]
47pub(super) enum WorkerError {
48    /// The worker failed during async initialization.
49    ///
50    /// The optional `child_name` carries the name of the original failing child when the error originates from a
51    /// nested supervisor. This allows the parent to include it in its own `FailedToInitialize` error for better
52    /// diagnostics across supervision tree levels.
53    Initialization {
54        child_name: Option<String>,
55        source: InitializationError,
56    },
57
58    /// The worker failed during runtime execution.
59    Runtime(GenericError),
60}
61
62impl From<SupervisorError> for WorkerError {
63    fn from(err: SupervisorError) -> Self {
64        match err {
65            // Propagate initialization failures so the parent supervisor does NOT attempt to restart.
66            // Preserve the original child name so the parent can include it in diagnostics.
67            SupervisorError::FailedToInitialize { child_name, source } => WorkerError::Initialization {
68                child_name: Some(child_name),
69                source,
70            },
71            // All other supervisor errors (shutdown, no children, invalid name) are runtime-level.
72            other => WorkerError::Runtime(other.into()),
73        }
74    }
75}
76
77/// Process errors.
78#[derive(Debug, Snafu)]
79pub enum ProcessError {
80    /// The child process was aborted by the supervisor.
81    #[snafu(display("Child process was aborted by the supervisor."))]
82    Aborted,
83
84    /// The child process panicked.
85    #[snafu(display("Child process panicked."))]
86    Panicked,
87
88    /// The child process terminated with an error.
89    #[snafu(display("Child process terminated with an error: {}", source))]
90    Terminated {
91        /// The error that caused the termination.
92        source: GenericError,
93    },
94}
95
96/// Initialization errors.
97///
98/// Initialization errors are distinct from runtime errors: they indicate that a process couldn't be started at all
99/// (for example, failed to bind a port, missing configuration). These errors don't trigger restart logic; instead, they
100/// immediately propagate up and fail the supervisor.
101#[derive(Debug, Snafu)]
102#[snafu(context(suffix(false)))]
103pub enum InitializationError {
104    /// The process couldn't be initialized due to an error.
105    #[snafu(display("Process failed to initialize: {}", source))]
106    Failed {
107        /// The underlying error that caused initialization to fail.
108        source: GenericError,
109    },
110}
111
112impl From<GenericError> for InitializationError {
113    fn from(source: GenericError) -> Self {
114        Self::Failed { source }
115    }
116}
117
118/// Strategy for shutting down a process.
119pub enum ShutdownStrategy {
120    /// Waits for the configured duration for the process to exit, and then forcefully aborts it otherwise.
121    Graceful(Duration),
122
123    /// Forcefully aborts the process without waiting.
124    Brutal,
125}
126
127/// Policy for automatically shutting a supervisor down based on the termination of its _significant_ children.
128///
129/// A significant child (see [`ChildSpecification::with_significant`]) is one whose termination -- when it isn't restarted -- can
130/// drive the supervisor to shut down. This mirrors Erlang/OTP's `auto_shutdown` supervisor flag, and is how an
131/// unexpected (or intentional) child exit cascades into the supervisor stopping, and thus propagating up the tree,
132/// without that child being restarted.
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
134pub enum AutoShutdown {
135    /// Never shut down automatically; significant children have no special effect. This is the default.
136    #[default]
137    Never,
138
139    /// Shut down as soon as _any_ significant child terminates without being restarted.
140    AnySignificant,
141
142    /// Shut down once _all_ significant children have terminated without being restarted.
143    AllSignificant,
144}
145
146/// How a supervisor shuts its children down.
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
148pub enum ShutdownMode {
149    /// Shut children down one at a time, in reverse order of starting (last-started first).
150    ///
151    /// This is the default, and is appropriate when later children may depend on earlier ones: each child is fully
152    /// stopped before the next is signalled.
153    #[default]
154    Ordered,
155
156    /// Shut all children down at once and wait for them concurrently.
157    ///
158    /// Total shutdown time is bounded by the slowest child rather than the sum of all children, which suits large,
159    /// independent child sets -- for example, one task per network connection.
160    Concurrent,
161}
162
163/// A supervisable process.
164#[async_trait]
165pub trait Supervisable: Send + Sync {
166    /// Returns the name of the process.
167    fn name(&self) -> &str;
168
169    /// Returns the shutdown strategy for the process.
170    fn shutdown_strategy(&self) -> ShutdownStrategy {
171        ShutdownStrategy::Graceful(Duration::from_secs(5))
172    }
173
174    /// Initializes the process asynchronously.
175    ///
176    /// During initialization, any resources or configuration for the process can be created asynchronously, and the
177    /// same runtime that's used for running the process is used for initialization. The resulting future is expected to
178    /// complete as soon as reasonably possible after `shutdown` resolves.
179    ///
180    /// **Important:** The `process_shutdown` signal must be moved into the returned [`SupervisorFuture`] so the worker
181    /// can respond to supervisor-initiated shutdown. If `process_shutdown` is dropped during initialization, the worker
182    /// will be unable to shut down gracefully and will be forcefully aborted after the shutdown timeout.
183    ///
184    /// # Errors
185    ///
186    /// If the process can't be initialized, an error is returned.
187    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError>;
188}
189
190/// Supervisor errors.
191#[derive(Debug, Snafu)]
192#[snafu(context(suffix(false)))]
193pub enum SupervisorError {
194    /// Supervisor or worker name is invalid.
195    #[snafu(display("Invalid name for supervisor or worker: '{}'", name))]
196    InvalidName {
197        /// The name of the supervisor is invalid.
198        name: String,
199    },
200
201    /// A child process failed to initialize.
202    ///
203    /// This error indicates that a child couldn't complete its async initialization. This is distinct from runtime
204    /// failures and doesn't trigger restart logic.
205    #[snafu(display("Child process '{}' failed to initialize: {}", child_name, source))]
206    FailedToInitialize {
207        /// The name of the child that failed to initialize.
208        child_name: String,
209
210        /// The underlying initialization error.
211        source: InitializationError,
212    },
213
214    /// The supervisor exceeded its restart limits and was forced to shutdown.
215    #[snafu(display("Supervisor has exceeded restart limits and was forced to shutdown."))]
216    Shutdown,
217
218    /// The supervisor shut down because a significant child terminated.
219    ///
220    /// See [`AutoShutdown`] and [`ChildSpecification::with_significant`]. The supervisor stopped, and drained its remaining
221    /// children, because a child marked significant terminated without being restarted.
222    #[snafu(display("Supervisor shut down after a significant child terminated."))]
223    SignificantChildExited,
224}
225
226/// A specification for a process to be added to a [`Supervisor`].
227///
228/// A child specification describes how the supervisor should create and manage a child: the underlying future that
229/// represents the process, along with metadata such as its name and shutdown strategy. All processes in a supervisor,
230/// whether a worker or a (nested) supervisor, are represented by a [`ChildSpecification`].
231///
232/// Generally, callers should prefer to use [`add_worker`][Supervisor::add_worker] directly, which can accept either
233/// [`Supervisor`] or any value that implements [`Supervisable`], without needing to explicitly create a
234/// [`ChildSpecification`]. This is preferred as it is more concise but also will ensure that relevant settings are
235/// configured properly for the given worker type, such as using the proper shutdown strategy for supervisors to allow
236/// for complete, graceful shutdown.
237///
238/// If more control is needed, [`ChildSpecification::worker`] can be used to create a specification directly, allowing
239/// access to configuring those more advanced settings. This is currently only valid for worker processes, as
240/// supervisors have no additional user-configurable settings.
241pub struct ChildSpecification<S = WorkerSpec> {
242    spec_inner: S,
243}
244
245/// Child specification state for a worker.
246pub struct WorkerSpec {
247    worker: Arc<dyn Supervisable>,
248    config: ChildConfig,
249}
250
251/// Child specification state for a supervisor.
252pub struct SupervisorSpec {
253    supervisor: Supervisor,
254}
255
256impl ChildSpecification<WorkerSpec> {
257    /// Creates a specification for the given worker.
258    pub fn worker<T: Supervisable + 'static>(worker: T) -> Self {
259        Self {
260            spec_inner: WorkerSpec {
261                worker: Arc::new(worker),
262                config: ChildConfig::default(),
263            },
264        }
265    }
266
267    /// Sets the restart policy for this worker.
268    ///
269    /// Defaults to [`RestartType::Permanent`].
270    #[must_use]
271    pub fn with_restart_type(mut self, restart_type: RestartType) -> Self {
272        self.spec_inner.config.restart = restart_type;
273        self
274    }
275
276    /// Sets whether this worker is _significant_.
277    ///
278    /// A significant worker's termination (when it isn't restarted) can drive the supervisor to shut down, per the
279    /// supervisor's [`AutoShutdown`] policy. Only meaningful for non-permanent workers, since a permanent worker is
280    /// always restarted and so never terminates without being restarted.
281    #[must_use]
282    pub fn with_significant(mut self, significant: bool) -> Self {
283        self.spec_inner.config.significant = significant;
284        self
285    }
286
287    /// Lowers this worker specification into its type-erased child and configuration.
288    fn into_worker_parts(self) -> (SupervisedChild, ChildConfig) {
289        (SupervisedChild::Worker(self.spec_inner.worker), self.spec_inner.config)
290    }
291}
292
293impl<T> From<T> for ChildSpecification<WorkerSpec>
294where
295    T: Supervisable + 'static,
296{
297    fn from(worker: T) -> Self {
298        Self::worker(worker)
299    }
300}
301
302impl From<Supervisor> for ChildSpecification<SupervisorSpec> {
303    fn from(supervisor: Supervisor) -> Self {
304        Self {
305            spec_inner: SupervisorSpec { supervisor },
306        }
307    }
308}
309
310mod sealed {
311    pub trait Sealed {}
312}
313
314impl sealed::Sealed for WorkerSpec {}
315impl sealed::Sealed for SupervisorSpec {}
316
317/// Child specification state.
318///
319/// This trait is sealed -- it cannot be implemented outside of this crate -- and is implemented only for
320/// [`WorkerSpec`] and [`SupervisorSpec`]. It exists so that [`Supervisor::add_worker`] can accept a
321/// [`ChildSpecification`] in either state (as well as bare workers and supervisors) while lowering each into the
322/// supervisor's internal representation.
323pub trait ChildState: sealed::Sealed + Sized {
324    #[doc(hidden)]
325    fn register(spec: ChildSpecification<Self>, supervisor: &mut Supervisor);
326}
327
328impl ChildState for WorkerSpec {
329    fn register(spec: ChildSpecification<Self>, supervisor: &mut Supervisor) {
330        let (child, config) = spec.into_worker_parts();
331        supervisor.push_child(ChildEntry {
332            spec: child,
333            config,
334            dynamic: false,
335        });
336    }
337}
338
339impl ChildState for SupervisorSpec {
340    fn register(spec: ChildSpecification<Self>, supervisor: &mut Supervisor) {
341        supervisor.push_child(ChildEntry {
342            spec: SupervisedChild::Supervisor(spec.spec_inner.supervisor),
343            config: ChildConfig::default(),
344            dynamic: false,
345        });
346    }
347}
348
349/// The type-erased, runnable form of a child: either a worker or a nested supervisor.
350pub(super) enum SupervisedChild {
351    Worker(Arc<dyn Supervisable>),
352    Supervisor(Supervisor),
353}
354
355impl SupervisedChild {
356    fn process_type(&self) -> &'static str {
357        match self {
358            Self::Worker(_) => "worker",
359            Self::Supervisor(_) => "supervisor",
360        }
361    }
362
363    fn name(&self) -> &str {
364        match self {
365            Self::Worker(worker) => worker.name(),
366            Self::Supervisor(supervisor) => &supervisor.supervisor_id,
367        }
368    }
369
370    pub(super) fn shutdown_strategy(&self) -> ShutdownStrategy {
371        match self {
372            Self::Worker(worker) => worker.shutdown_strategy(),
373
374            // Supervisors should always be given as much time as necessary shutdown down gracefully to ensure that the
375            // entire supervision subtree can be shutdown cleanly.
376            Self::Supervisor(_) => ShutdownStrategy::Graceful(Duration::MAX),
377        }
378    }
379
380    pub(super) fn create_process(&self, parent_process: &Process) -> Result<Process, SupervisorError> {
381        match self {
382            Self::Worker(worker) => Process::worker(worker.name(), parent_process).context(InvalidName {
383                name: worker.name().to_string(),
384            }),
385            Self::Supervisor(sup) => {
386                Process::supervisor(&sup.supervisor_id, Some(parent_process)).context(InvalidName {
387                    name: sup.supervisor_id.to_string(),
388                })
389            }
390        }
391    }
392
393    pub(super) fn create_worker_future(
394        &self, process: Process, process_shutdown: ShutdownHandle,
395    ) -> Result<WorkerFuture, SupervisorError> {
396        match self {
397            Self::Worker(worker) => {
398                let worker = Arc::clone(worker);
399                Ok(Box::pin(async move {
400                    let run_future =
401                        worker
402                            .initialize(process_shutdown)
403                            .await
404                            .map_err(|source| WorkerError::Initialization {
405                                child_name: None,
406                                source,
407                            })?;
408                    run_future.await.map_err(WorkerError::Runtime)
409                }))
410            }
411            Self::Supervisor(sup) => {
412                match sup.runtime_mode() {
413                    RuntimeMode::Ambient => {
414                        // Run on the parent's ambient runtime.
415                        Ok(sup.as_nested_process(process, process_shutdown))
416                    }
417                    RuntimeMode::Dedicated(config) => {
418                        // Spawn in a dedicated runtime on a new OS thread, passing the parent's
419                        // dataspace so the nested supervisor inherits it across the thread boundary.
420                        let child_name = sup.supervisor_id.to_string();
421                        let dataspace = process.dataspace().clone();
422                        let handle =
423                            spawn_dedicated_runtime(sup.inner_clone(), config.clone(), process_shutdown, dataspace)
424                                .map_err(|e| SupervisorError::FailedToInitialize {
425                                    child_name,
426                                    source: e.into(),
427                                })?;
428
429                        Ok(Box::pin(async move { handle.await.map_err(WorkerError::from) }))
430                    }
431                }
432            }
433        }
434    }
435}
436
437impl Clone for SupervisedChild {
438    fn clone(&self) -> Self {
439        match self {
440            Self::Worker(worker) => Self::Worker(Arc::clone(worker)),
441            Self::Supervisor(supervisor) => Self::Supervisor(supervisor.inner_clone()),
442        }
443    }
444}
445
446/// Per-child configuration: its [`RestartType`] and whether it is _significant_ (see [`AutoShutdown`]).
447///
448/// Defaults to a permanent, non-significant child. On a worker, this is set through
449/// [`ChildSpecification::with_restart_type`] and [`ChildSpecification::with_significant`].
450#[derive(Clone, Copy, Debug)]
451struct ChildConfig {
452    restart: RestartType,
453    significant: bool,
454}
455
456impl Default for ChildConfig {
457    fn default() -> Self {
458        Self {
459            restart: RestartType::Permanent,
460            significant: false,
461        }
462    }
463}
464
465/// A registered child: its specification together with the configuration chosen at registration time.
466#[derive(Clone)]
467struct ChildEntry {
468    spec: SupervisedChild,
469    config: ChildConfig,
470    /// Whether this child was added dynamically (via [`SupervisorHandle`]) rather than statically before the run. Used
471    /// to maintain the dynamic-children gauge.
472    dynamic: bool,
473}
474
475/// Identifier for a child managed by a [`Supervisor`].
476///
477/// Returned by [`SupervisorHandle::spawn`] for dynamically spawned children. Unique within a single process for the
478/// lifetime of a supervisor run.
479#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
480pub struct ChildId(u64);
481
482impl ChildId {
483    /// Returns the raw numeric value of this identifier.
484    pub const fn as_u64(self) -> u64 {
485        self.0
486    }
487}
488
489/// Error returned when spawning a dynamic child on a [`Supervisor`] fails.
490#[derive(Debug, Snafu)]
491pub enum SpawnError {
492    /// The supervisor isn't currently running, so it can't accept the spawn.
493    ///
494    /// Returned when the supervisor hasn't started yet, is between restarts, or has shut down -- and also if the run
495    /// ends after the request is accepted but before the child is started. To add children before the supervisor
496    /// starts, configure them statically with [`Supervisor::add_worker`] instead.
497    #[snafu(display("supervisor is gone"))]
498    SupervisorGone,
499
500    /// The supervisor was running but rejected the spawn (for example, an invalid child name).
501    ///
502    /// Unlike [`SupervisorGone`](Self::SupervisorGone), the supervisor accepted the request and then couldn't start the
503    /// child; the underlying error is preserved as the source.
504    #[snafu(display("supervisor rejected the spawn: {}", source))]
505    Rejected {
506        /// The underlying error that caused the spawn to be rejected.
507        source: GenericError,
508    },
509}
510
511/// A dynamic spawn request sent from a [`SupervisorHandle`] to the running supervisor.
512struct PendingSpawn {
513    id: u64,
514    spec: SupervisedChild,
515    config: ChildConfig,
516    ack: oneshot::Sender<Result<(), SpawnError>>,
517}
518
519/// Capacity of the per-run channel that carries dynamic spawn requests from handles to the running supervisor.
520///
521/// Each request is short-lived -- the supervisor processes it and signals the waiting caller promptly -- so this only
522/// bounds how many spawns can be in flight before a caller's send applies backpressure.
523const DYNAMIC_SPAWN_CHANNEL_CAPACITY: usize = 1024;
524
525/// A handle for spawning dynamic children on a running [`Supervisor`].
526///
527/// Obtained from [`Supervisor::handle`]. Handles are cheap to clone and can be shared across tasks. Spawning is async:
528/// the request is handed to the running supervisor and the call returns once the child has been started. If the
529/// supervisor isn't currently running, spawning returns [`SpawnError::SupervisorGone`].
530#[derive(Clone)]
531pub struct SupervisorHandle {
532    name: Arc<str>,
533    // The currently running supervisor publishes its command sender here so handles can reach the live run; it's
534    // cleared when no run is active, at which point spawns observe `SupervisorGone`.
535    current_tx: Arc<Mutex<Option<mpsc::Sender<PendingSpawn>>>>,
536    id_counter: Arc<AtomicU64>,
537    active: Arc<AtomicUsize>,
538}
539
540impl SupervisorHandle {
541    /// Returns the name of the supervisor this handle refers to.
542    pub fn name(&self) -> &str {
543        &self.name
544    }
545
546    /// Spawns a new dynamic worker.
547    ///
548    /// Dynamic workers are temporary children that are not restarted by the supervisor when they die or when the
549    /// supervisor itself is restarted. They are useful for short-lived, non-critical background tasks that require
550    /// structured concurrency: the process should be cancelled when the supervisor itself is restarted or terminated,
551    /// and so on.
552    ///
553    /// Use [`spawn_with`](Self::spawn_with) to configure the child's restart policy or significance.
554    ///
555    /// # Errors
556    ///
557    /// If the supervisor isn't current running, or if the child specification is invalid, an error is returned.
558    pub async fn spawn<T: Supervisable + 'static>(&self, worker: T) -> Result<ChildId, SpawnError> {
559        self.spawn_with(ChildSpecification::worker(worker).with_restart_type(RestartType::Temporary))
560            .await
561    }
562
563    /// Spawns a new dynamic child from a fully configured [`ChildSpecification`].
564    ///
565    /// Dynamic workers are temporary children that are not restarted by the supervisor when they die or when the
566    /// supervisor itself is restarted. They are useful for short-lived, non-critical background tasks that require
567    /// structured concurrency: the process should be cancelled when the supervisor itself is restarted or terminated,
568    /// and so on.
569    ///
570    /// This method allows for configuring more advanced aspects of the child process, such as its restart type and
571    /// significance.
572    ///
573    /// # Errors
574    ///
575    /// If the supervisor isn't current running, or if the child specification is invalid, an error is returned.
576    pub async fn spawn_with(&self, spec: ChildSpecification<WorkerSpec>) -> Result<ChildId, SpawnError> {
577        let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
578        let (spec, config) = spec.into_worker_parts();
579        let (ack_tx, ack_rx) = oneshot::channel();
580        self.send(PendingSpawn {
581            id,
582            spec,
583            config,
584            ack: ack_tx,
585        })
586        .await?;
587
588        // Wait for the supervisor to start (or reject) the child. A dropped ack channel means the run ended before it
589        // got to us, which is indistinguishable from `SupervisorGone` to the caller.
590        ack_rx
591            .await
592            .map_err(|_| SpawnError::SupervisorGone)?
593            .map(|()| ChildId(id))
594    }
595
596    /// Returns whether the supervisor is currently running.
597    pub fn is_running(&self) -> bool {
598        self.current_tx.lock().unwrap().is_some()
599    }
600
601    /// Returns the number of dynamic children currently running under the supervisor.
602    pub fn active_children(&self) -> usize {
603        self.active.load(Ordering::Relaxed)
604    }
605
606    /// Hands a spawn request to the currently running supervisor, applying backpressure if its channel is full.
607    ///
608    /// Returns [`SpawnError::SupervisorGone`] if no run is active, or if the run ends before the request is accepted.
609    async fn send(&self, spawn: PendingSpawn) -> Result<(), SpawnError> {
610        // Clone the sender out from under the lock so we don't hold the (synchronous) mutex guard across the await.
611        let tx = self.current_tx.lock().unwrap().clone();
612        match tx {
613            Some(tx) => tx.send(spawn).await.map_err(|_| SpawnError::SupervisorGone),
614            None => Err(SpawnError::SupervisorGone),
615        }
616    }
617}
618
619/// Supervises a set of workers.
620///
621/// # Workers
622///
623/// All workers are defined through implementation of the [`Supervisable`] trait, which provides the logic for both
624/// creating the underlying worker future that's spawned, as well as other metadata, such as the worker's name, how the
625/// worker should be shutdown, and so on.
626///
627/// Supervisors also (indirectly) implement the [`Supervisable`] trait, allowing them to be supervised by other
628/// supervisors in order to construct _supervision trees_.
629///
630/// # Instrumentation
631///
632/// Supervisors automatically create their own allocation group
633/// ([`TrackingAllocator`][resource_accounting::TrackingAllocator]), which is used to track both the memory usage of the
634/// supervisor itself and its children. Additionally, individual worker processes are wrapped in a dedicated
635/// [`tracing::Span`] to allow tracing the causal relationship between arbitrary code and the worker executing it.
636///
637/// # Restart Strategies
638///
639/// As the main purpose of a supervisor, restart behavior is fully configurable. A number of restart strategies are
640/// available, which generally relate to the purpose of the supervisor: whether the workers being managed are
641/// independent or interdependent.
642///
643/// All restart strategies are configured through [`RestartStrategy`], which has more information on the available
644/// strategies and configuration settings.
645pub struct Supervisor {
646    supervisor_id: Arc<str>,
647    child_specs: Vec<ChildEntry>,
648    restart_strategy: RestartStrategy,
649    auto_shutdown: AutoShutdown,
650    shutdown_mode: ShutdownMode,
651    runtime_mode: RuntimeMode,
652    // Shared across clones (a nested supervisor is cloned each time it runs) and across all handles. While a run is
653    // active it holds that run's spawn-command sender so handles can reach the live supervisor; it's `None` whenever no
654    // run is active, at which point spawns observe `SupervisorGone`. Doubles as the `is_running` signal.
655    current_tx: Arc<Mutex<Option<mpsc::Sender<PendingSpawn>>>>,
656    id_counter: Arc<AtomicU64>,
657    // Number of dynamic children currently running, shared with handles so it can be surfaced as a gauge.
658    active: Arc<AtomicUsize>,
659}
660
661impl Supervisor {
662    /// Creates an empty `Supervisor` with the default restart strategy.
663    pub fn new<S: AsRef<str>>(supervisor_id: S) -> Result<Self, SupervisorError> {
664        // We try to throw an error about invalid names as early as possible. This is a manual check, so we might still
665        // encounter an error later when actually running the supervisor, but this is a good first step to catch the
666        // bulk of invalid names.
667        if supervisor_id.as_ref().is_empty() {
668            return Err(SupervisorError::InvalidName {
669                name: supervisor_id.as_ref().to_string(),
670            });
671        }
672
673        Ok(Self {
674            supervisor_id: supervisor_id.as_ref().into(),
675            child_specs: Vec::new(),
676            restart_strategy: RestartStrategy::default(),
677            auto_shutdown: AutoShutdown::default(),
678            shutdown_mode: ShutdownMode::default(),
679            runtime_mode: RuntimeMode::default(),
680            current_tx: Arc::new(Mutex::new(None)),
681            id_counter: Arc::new(AtomicU64::new(0)),
682            active: Arc::new(AtomicUsize::new(0)),
683        })
684    }
685
686    /// Returns the supervisor's ID.
687    pub fn id(&self) -> &str {
688        &self.supervisor_id
689    }
690
691    /// Sets the restart strategy for the supervisor.
692    pub fn with_restart_strategy(mut self, strategy: RestartStrategy) -> Self {
693        self.restart_strategy = strategy;
694        self
695    }
696
697    /// Sets the supervisor's automatic-shutdown policy.
698    ///
699    /// Controls whether the termination of _significant_ children (see [`ChildSpecification::with_significant`]) drives the
700    /// supervisor to shut down. Defaults to [`AutoShutdown::Never`].
701    pub fn with_auto_shutdown(mut self, auto_shutdown: AutoShutdown) -> Self {
702        self.auto_shutdown = auto_shutdown;
703        self
704    }
705
706    /// Sets the supervisor's shutdown mode. See [`ShutdownMode`]. Defaults to [`ShutdownMode::Ordered`].
707    pub fn with_shutdown_mode(mut self, mode: ShutdownMode) -> Self {
708        self.shutdown_mode = mode;
709        self
710    }
711
712    /// Returns a handle for spawning dynamic children on this supervisor while it runs.
713    ///
714    /// The handle can be created before the supervisor starts and cloned freely. Spawns only succeed while the
715    /// supervisor is actually running; if it hasn't started yet, is between restarts, or has shut down, they return
716    /// [`SpawnError::SupervisorGone`].
717    pub fn handle(&self) -> SupervisorHandle {
718        SupervisorHandle {
719            name: Arc::clone(&self.supervisor_id),
720            current_tx: Arc::clone(&self.current_tx),
721            id_counter: Arc::clone(&self.id_counter),
722            active: Arc::clone(&self.active),
723        }
724    }
725
726    /// Configures this supervisor to run in a dedicated runtime.
727    ///
728    /// When this supervisor is added as a child to another supervisor, it will spawn its own OS threads and Tokio
729    /// runtime instead of running on the parent's ambient runtime.
730    ///
731    /// This provides runtime isolation, which can be useful for:
732    /// - CPU-bound work that shouldn't block the parent's runtime
733    /// - Isolating failures in one part of the system
734    /// - Using different runtime configurations (for example, single-threaded vs multi-threaded)
735    pub fn with_dedicated_runtime(mut self, config: RuntimeConfiguration) -> Self {
736        self.runtime_mode = RuntimeMode::Dedicated(config);
737        self
738    }
739
740    /// Returns the runtime mode for this supervisor.
741    pub(crate) fn runtime_mode(&self) -> &RuntimeMode {
742        &self.runtime_mode
743    }
744
745    /// Adds a worker (or nested supervisor) to the supervisor.
746    ///
747    /// A worker can be anything that implements the [`Supervisable`] trait. A [`Supervisor`] can also be added as a
748    /// worker and managed in a nested fashion, known as a supervision tree.
749    ///
750    /// See [`ChildSpecification`] for more details on how workers are represented internally and what options are
751    /// available to configure.
752    pub fn add_worker<S, T>(&mut self, child: T)
753    where
754        S: ChildState,
755        T: Into<ChildSpecification<S>>,
756    {
757        S::register(child.into(), self);
758    }
759
760    fn push_child(&mut self, entry: ChildEntry) {
761        debug!(
762            supervisor_id = %self.supervisor_id,
763            "Adding new static child process #{}. ({}, {}, {:?})",
764            self.child_specs.len(),
765            entry.spec.process_type(),
766            entry.spec.name(),
767            entry.config,
768        );
769        self.child_specs.push(entry);
770    }
771
772    fn spawn_static_children(
773        &self, children: &mut FastHashMap<u64, ChildEntry>, worker_state: &mut WorkerState,
774    ) -> Result<(), SupervisorError> {
775        debug!(supervisor_id = %self.supervisor_id, "Spawning all static child processes.");
776        for entry in &self.child_specs {
777            let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
778            worker_state.add_worker(id, &entry.spec)?;
779            children.insert(id, entry.clone());
780        }
781
782        Ok(())
783    }
784
785    /// Respawns children after a one-for-all restart, honoring each child's [`RestartType`].
786    ///
787    /// Every child except [`RestartType::Temporary`] is restarted, matching Erlang/OTP: a group restart restarts all
788    /// permanent and transient children -- regardless of how they last exited, including a transient child that had
789    /// already exited cleanly -- but never temporary children, which are shut down with the group and not brought back.
790    /// A transient child's "restart only on abnormal exit" rule governs its _own_ termination, not a group restart
791    /// driven by a sibling. Dynamic children are not restored (they are lost on a supervisor-level restart).
792    fn respawn_children_one_for_all(
793        &self, children: &mut FastHashMap<u64, ChildEntry>, worker_state: &mut WorkerState,
794    ) -> Result<(), SupervisorError> {
795        debug!(supervisor_id = %self.supervisor_id, "Restarting all eligible static child processes.");
796        for entry in &self.child_specs {
797            // Temporary children are never restarted by a group restart (matching OTP): they are shut down with the
798            // group but not brought back.
799            if entry.config.restart == RestartType::Temporary {
800                continue;
801            }
802            let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
803            worker_state.add_worker(id, &entry.spec)?;
804            children.insert(id, entry.clone());
805        }
806
807        Ok(())
808    }
809
810    /// Spawns one dynamic child into the running supervisor's worker set and roster, signaling the requesting handle.
811    fn spawn_dynamic_child(
812        &self, spawn: PendingSpawn, worker_state: &mut WorkerState, children: &mut FastHashMap<u64, ChildEntry>,
813        significant_remaining: &mut usize,
814    ) {
815        let PendingSpawn { id, spec, config, ack } = spawn;
816        let entry = ChildEntry {
817            spec,
818            config,
819            dynamic: true,
820        };
821        match worker_state.add_worker(id, &entry.spec) {
822            Ok(()) => {
823                if config.significant {
824                    *significant_remaining += 1;
825                }
826                self.active.fetch_add(1, Ordering::Relaxed);
827                children.insert(id, entry);
828                let _ = ack.send(Ok(()));
829            }
830            Err(e) => {
831                // Registration failed (e.g. an invalid child name). Report it to the waiting caller as `Rejected` --
832                // distinct from `SupervisorGone` -- so the underlying cause isn't lost.
833                error!(supervisor_id = %self.supervisor_id, error = %e, "Failed to spawn dynamic child.");
834                let _ = ack.send(Err(SpawnError::Rejected { source: e.into() }));
835            }
836        }
837    }
838
839    async fn run_inner(&self, process: Process, process_shutdown: ShutdownHandle) -> Result<(), SupervisorError> {
840        // Publish a fresh command channel for this run so handles can spawn dynamic children into it; while it's set,
841        // handles observe us as running.
842        let (cmd_tx, cmd_rx) = mpsc::channel(DYNAMIC_SPAWN_CHANNEL_CAPACITY);
843        *self.current_tx.lock().unwrap() = Some(cmd_tx);
844
845        let result = self.supervise(process, process_shutdown, cmd_rx).await;
846
847        // The run is over. Clear the sender so later spawns observe `SupervisorGone`, and reset the dynamic-children
848        // gauge. Dropping the receiver (owned by `supervise`) already rejected anything still in flight.
849        *self.current_tx.lock().unwrap() = None;
850        self.active.store(0, Ordering::Relaxed);
851        result
852    }
853
854    async fn supervise(
855        &self, process: Process, process_shutdown: ShutdownHandle, mut cmd_rx: mpsc::Receiver<PendingSpawn>,
856    ) -> Result<(), SupervisorError> {
857        let mut restart_state = RestartState::new(self.restart_strategy);
858        let mut worker_state = WorkerState::new(process, self.shutdown_mode);
859
860        // The live roster of children -- both static (seeded below) and dynamic (added via the handle) -- keyed by a
861        // stable id. A restart re-runs a child by id; a child that isn't restarted is removed from the roster.
862        let mut children: FastHashMap<u64, ChildEntry> = FastHashMap::default();
863
864        // Spawn the static children. Initialization is folded into each worker's task, so this returns immediately --
865        // children initialize concurrently in the background.
866        self.spawn_static_children(&mut children, &mut worker_state)?;
867
868        // Track how many significant children are still running, for `AutoShutdown` evaluation.
869        let mut significant_remaining = children.values().filter(|entry| entry.config.significant).count();
870
871        // Now we supervise.
872        pin!(process_shutdown);
873
874        let outcome = loop {
875            select! {
876                // Shutdown takes priority so a flood of dynamic spawns can't starve it.
877                biased;
878
879                // Shutdown has been triggered; break out of the loop with a clean outcome and tear down below. (We
880                // can't touch `cmd_rx` in any arm's handler -- the `recv` arm below borrows it for the whole
881                // `select!` -- so all teardown happens after the loop.)
882                _ = &mut process_shutdown => break Ok(()),
883
884                // A handle asked us to spawn a dynamic child. The published sender keeps the channel open for the whole
885                // run, so `recv` only yields `None` once we close it during teardown.
886                spawn = cmd_rx.recv() => {
887                    if let Some(spawn) = spawn {
888                        self.spawn_dynamic_child(spawn, &mut worker_state, &mut children, &mut significant_remaining);
889                    }
890                }
891
892                (child_id, worker_result) = worker_state.wait_for_next_worker() => {
893                    // Pull out what we need from the roster before we mutate it.
894                    let (child_name, config, dynamic) = {
895                        let entry = children.get(&child_id).expect("completed worker must be present in the roster");
896                        (entry.spec.name().to_string(), entry.config, entry.dynamic)
897                    };
898
899                    // Initialization failures are not eligible for restart -- they propagate immediately.
900                    if let Err(WorkerError::Initialization { child_name: inner, source }) = worker_result {
901                        // If the error came from a nested supervisor, include the original child name to make the error
902                        // chain more informative (e.g., "ctrl-pln/privileged-api").
903                        let full_name = match inner {
904                            Some(inner) => format!("{}/{}", child_name, inner),
905                            None => child_name.clone(),
906                        };
907
908                        error!(supervisor_id = %self.supervisor_id, worker_name = full_name, "Child process failed to initialize: {}", source);
909                        break Err(SupervisorError::FailedToInitialize { child_name: full_name, source });
910                    }
911
912                    // A worker exited abnormally if it returned an error, panicked, or was aborted; a clean exit is
913                    // `Ok(())`. Together with the worker's restart policy, this determines whether we restart it.
914                    let abnormal = worker_result.is_err();
915                    let worker_result = worker_result.map_err(|e| match e {
916                        WorkerError::Runtime(e) => ProcessError::Terminated { source: e },
917                        WorkerError::Initialization { .. } => unreachable!("handled above"),
918                    });
919
920                    if !config.restart.should_restart(abnormal) {
921                        // Not eligible for restart given how it exited. Drop it from the roster, and free its slot/gauge
922                        // if it was dynamic. Crucially, we do NOT consult `evaluate_restart` here: non-restarts must not
923                        // consume the restart-intensity budget, otherwise a steady stream of terminating temporary
924                        // children would eventually trip the limit and tear the supervisor (and its siblings) down.
925                        debug!(supervisor_id = %self.supervisor_id, worker_name = %child_name, restart = ?config.restart, ?worker_result, "Child process exited and is not eligible for restart.");
926                        children.remove(&child_id);
927                        if dynamic {
928                            self.active.fetch_sub(1, Ordering::Relaxed);
929                        }
930
931                        // A significant child terminating without restart can drive the supervisor to shut down, per its
932                        // `AutoShutdown` policy -- cascading an unexpected (or intentional) child exit into the
933                        // supervisor stopping and propagating up the tree.
934                        if config.significant {
935                            significant_remaining = significant_remaining.saturating_sub(1);
936                            let auto_shutdown = match self.auto_shutdown {
937                                AutoShutdown::Never => false,
938                                AutoShutdown::AnySignificant => true,
939                                AutoShutdown::AllSignificant => significant_remaining == 0,
940                            };
941                            if auto_shutdown {
942                                warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Significant child terminated; shutting down supervisor.");
943                                break Err(SupervisorError::SignificantChildExited);
944                            }
945                        }
946                    } else {
947                        match restart_state.evaluate_restart() {
948                            RestartAction::Restart(mode) => match mode {
949                                RestartMode::OneForOne => {
950                                    warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting.");
951                                    let spec = children.get(&child_id).expect("present for restart").spec.clone();
952                                    if let Err(e) = worker_state.add_worker(child_id, &spec) {
953                                        break Err(e);
954                                    }
955                                }
956                                RestartMode::OneForAll => {
957                                    warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting all processes.");
958                                    worker_state.shutdown_workers().await;
959                                    // A one-for-all restart resets to the static roster; dynamic children are not
960                                    // restored (they're lost on a supervisor-level restart, matching Erlang/OTP), and
961                                    // temporary children are not restarted.
962                                    children.clear();
963                                    self.active.store(0, Ordering::Relaxed);
964                                    let respawn = self.respawn_children_one_for_all(&mut children, &mut worker_state);
965                                    if let Err(e) = respawn {
966                                        break Err(e);
967                                    }
968                                    significant_remaining =
969                                        children.values().filter(|entry| entry.config.significant).count();
970                                }
971                            },
972                            RestartAction::Shutdown => {
973                                error!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Supervisor shutting down due to restart limits.");
974                                break Err(SupervisorError::Shutdown);
975                            }
976                        }
977                    }
978                }
979            }
980        };
981
982        // The run is ending -- either cleanly (shutdown was signalled) or with an error (a child failed to initialize
983        // or restart, the restart limit was exceeded, or a significant child exited). On every path: stop accepting
984        // spawns and reject anything still queued -- rather
985        // than starting children only to tear them down immediately -- then shut down all children. Closing the channel
986        // before the (possibly slow) shutdown also unblocks any handle parked on a full channel, so a spawn racing the
987        // teardown observes `SupervisorGone` promptly instead of hanging until shutdown finishes.
988        cmd_rx.close();
989        while let Ok(spawn) = cmd_rx.try_recv() {
990            let _ = spawn.ack.send(Err(SpawnError::SupervisorGone));
991        }
992        worker_state.shutdown_workers().await;
993
994        outcome
995    }
996
997    fn as_nested_process(&self, process: Process, process_shutdown: ShutdownHandle) -> WorkerFuture {
998        // Simple wrapper around `run_inner` to satisfy the return type signature needed when running the supervisor as
999        // a nested child process in another supervisor.
1000        debug!(supervisor_id = %self.supervisor_id, "Nested supervisor starting.");
1001
1002        // Create a standalone clone of ourselves so we can fulfill the future signature.
1003        let sup = self.inner_clone();
1004
1005        Box::pin(async move {
1006            sup.run_inner(process, process_shutdown)
1007                .await
1008                .map_err(WorkerError::from)
1009        })
1010    }
1011
1012    /// Runs the supervisor forever.
1013    ///
1014    /// # Errors
1015    ///
1016    /// If the supervisor exceeds its restart limits, or fails to initialize a child process, an error is returned.
1017    pub async fn run(&mut self) -> Result<(), SupervisorError> {
1018        // Create a no-op `ShutdownHandle` to satisfy the `run_inner` function. This is never used since we want to run
1019        // forever, but we need to satisfy the signature.
1020        let process_shutdown = ShutdownHandle::noop();
1021        let process = Process::supervisor(&self.supervisor_id, None).context(InvalidName {
1022            name: self.supervisor_id.to_string(),
1023        })?;
1024
1025        debug!(supervisor_id = %self.supervisor_id, "Supervisor starting.");
1026        self.run_inner(process.clone(), process_shutdown)
1027            .into_process_future(process)
1028            .await
1029    }
1030
1031    /// Runs the supervisor until shutdown is triggered.
1032    ///
1033    /// When `shutdown` resolves, the supervisor will shutdown all child processes according to their shutdown strategy,
1034    /// and then return.
1035    ///
1036    /// # Errors
1037    ///
1038    /// If the supervisor exceeds its restart limits, or fails to initialize a child process, an error is returned.
1039    pub async fn run_with_shutdown<F: Future + Send + 'static>(&mut self, shutdown: F) -> Result<(), SupervisorError> {
1040        // Drive the caller-provided shutdown future into a trigger so the supervisor can begin shutting down its
1041        // children once `shutdown` resolves. The trigger fires at most once (guarded), and otherwise fires on drop if
1042        // the supervisor returns on its own first.
1043        let (shutdown_coordinator, shutdown_handle) = ShutdownHandle::paired();
1044        let run = self.run_with_shutdown_inner(shutdown_handle, None);
1045        pin!(run, shutdown);
1046
1047        let mut shutdown_coordinator = Some(shutdown_coordinator);
1048        loop {
1049            select! {
1050                result = &mut run => return result,
1051                _ = &mut shutdown, if shutdown_coordinator.is_some() => {
1052                    shutdown_coordinator.take().expect("coordinator present per select guard").shutdown();
1053                }
1054            }
1055        }
1056    }
1057
1058    /// Runs the supervisor until the given `ShutdownHandle` signal is received.
1059    ///
1060    /// This is an internal variant of `run_with_shutdown` that takes a `ShutdownHandle` directly, used when spawning
1061    /// supervisors in dedicated runtimes where the shutdown signal is already wrapped in a `ShutdownHandle`.
1062    ///
1063    /// If `dataspace` is provided, the supervisor will use it instead of creating a new one. This is used to propagate
1064    /// the parent's dataspace across OS thread boundaries for dedicated runtimes.
1065    ///
1066    /// # Errors
1067    ///
1068    /// If the supervisor exceeds its restart limits, or fails to initialize a child process, an error is returned.
1069    pub(crate) async fn run_with_shutdown_inner(
1070        &mut self, process_shutdown: ShutdownHandle, dataspace: Option<DataspaceRegistry>,
1071    ) -> Result<(), SupervisorError> {
1072        let process =
1073            Process::supervisor_with_dataspace(&self.supervisor_id, None, dataspace).context(InvalidName {
1074                name: self.supervisor_id.to_string(),
1075            })?;
1076
1077        debug!(supervisor_id = %self.supervisor_id, "Supervisor starting.");
1078        self.run_inner(process.clone(), process_shutdown)
1079            .into_process_future(process)
1080            .await
1081    }
1082
1083    fn inner_clone(&self) -> Self {
1084        // This is no different than if we just implemented `Clone` directly, but it allows us to avoid exposing a
1085        // _public_ implementation of `Clone`, which we don't want normal users to be able to do. We only need this
1086        // internally to support nested supervisors.
1087        Self {
1088            supervisor_id: Arc::clone(&self.supervisor_id),
1089            child_specs: self.child_specs.clone(),
1090            restart_strategy: self.restart_strategy,
1091            auto_shutdown: self.auto_shutdown,
1092            shutdown_mode: self.shutdown_mode,
1093            runtime_mode: self.runtime_mode.clone(),
1094            current_tx: Arc::clone(&self.current_tx),
1095            id_counter: Arc::clone(&self.id_counter),
1096            active: Arc::clone(&self.active),
1097        }
1098    }
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103    use std::{
1104        future::pending,
1105        sync::atomic::{AtomicUsize, Ordering},
1106    };
1107
1108    use async_trait::async_trait;
1109    use tokio::{
1110        sync::oneshot,
1111        task::JoinHandle,
1112        time::{sleep, timeout},
1113    };
1114
1115    use super::*;
1116
1117    /// Behavior for a mock worker during initialization.
1118    #[derive(Clone)]
1119    enum InitBehavior {
1120        /// Initialization succeeds immediately.
1121        Instant,
1122
1123        /// Initialization takes the given duration before succeeding.
1124        Slow(Duration),
1125
1126        /// Initialization fails with the given message.
1127        Fail(&'static str),
1128    }
1129
1130    /// Behavior for a mock worker during runtime (after initialization).
1131    #[derive(Clone)]
1132    enum RunBehavior {
1133        /// Runs until shutdown is received.
1134        UntilShutdown,
1135
1136        /// Fails with the given error message after the given delay.
1137        FailAfter(Duration, &'static str),
1138
1139        /// Completes successfully after the given delay.
1140        CompleteAfter(Duration),
1141
1142        /// On shutdown, sleeps for the given duration before exiting (to exercise concurrent draining).
1143        SlowShutdown(Duration),
1144
1145        /// Ignores shutdown entirely and runs forever (to exercise abort-at-deadline).
1146        IgnoreShutdown,
1147
1148        /// Panics after the given delay, unless shutdown arrives first.
1149        PanicAfter(Duration),
1150    }
1151
1152    /// A configurable mock worker for testing supervisor behavior.
1153    struct MockWorker {
1154        name: &'static str,
1155        init_behavior: InitBehavior,
1156        run_behavior: RunBehavior,
1157        start_count: Arc<AtomicUsize>,
1158        brutal_shutdown: bool,
1159        graceful_timeout: Duration,
1160    }
1161
1162    impl MockWorker {
1163        /// Creates a worker that runs until shutdown.
1164        fn long_running(name: &'static str) -> Self {
1165            Self {
1166                name,
1167                init_behavior: InitBehavior::Instant,
1168                run_behavior: RunBehavior::UntilShutdown,
1169                start_count: Arc::new(AtomicUsize::new(0)),
1170                brutal_shutdown: false,
1171                graceful_timeout: Duration::from_millis(500),
1172            }
1173        }
1174
1175        /// Creates a worker that fails after the given delay.
1176        fn failing(name: &'static str, delay: Duration) -> Self {
1177            Self {
1178                name,
1179                init_behavior: InitBehavior::Instant,
1180                run_behavior: RunBehavior::FailAfter(delay, "worker failed"),
1181                start_count: Arc::new(AtomicUsize::new(0)),
1182                brutal_shutdown: false,
1183                graceful_timeout: Duration::from_millis(500),
1184            }
1185        }
1186
1187        /// Creates a worker that completes successfully after the given delay.
1188        fn completing(name: &'static str, delay: Duration) -> Self {
1189            Self {
1190                name,
1191                init_behavior: InitBehavior::Instant,
1192                run_behavior: RunBehavior::CompleteAfter(delay),
1193                start_count: Arc::new(AtomicUsize::new(0)),
1194                brutal_shutdown: false,
1195                graceful_timeout: Duration::from_millis(500),
1196            }
1197        }
1198
1199        /// Creates a worker that sleeps for `delay` after observing shutdown before exiting.
1200        fn slow_shutdown(name: &'static str, delay: Duration) -> Self {
1201            Self {
1202                name,
1203                init_behavior: InitBehavior::Instant,
1204                run_behavior: RunBehavior::SlowShutdown(delay),
1205                start_count: Arc::new(AtomicUsize::new(0)),
1206                brutal_shutdown: false,
1207                graceful_timeout: Duration::from_millis(500),
1208            }
1209        }
1210
1211        /// Creates a worker that never reacts to shutdown.
1212        fn ignore_shutdown(name: &'static str) -> Self {
1213            Self {
1214                name,
1215                init_behavior: InitBehavior::Instant,
1216                run_behavior: RunBehavior::IgnoreShutdown,
1217                start_count: Arc::new(AtomicUsize::new(0)),
1218                brutal_shutdown: false,
1219                graceful_timeout: Duration::from_millis(500),
1220            }
1221        }
1222
1223        /// Creates a worker that panics after the given delay.
1224        fn panicking(name: &'static str, delay: Duration) -> Self {
1225            Self {
1226                name,
1227                init_behavior: InitBehavior::Instant,
1228                run_behavior: RunBehavior::PanicAfter(delay),
1229                start_count: Arc::new(AtomicUsize::new(0)),
1230                brutal_shutdown: false,
1231                graceful_timeout: Duration::from_millis(500),
1232            }
1233        }
1234
1235        /// Creates a worker that fails during initialization.
1236        fn init_failure(name: &'static str) -> Self {
1237            Self {
1238                name,
1239                init_behavior: InitBehavior::Fail("init failed"),
1240                run_behavior: RunBehavior::UntilShutdown,
1241                start_count: Arc::new(AtomicUsize::new(0)),
1242                brutal_shutdown: false,
1243                graceful_timeout: Duration::from_millis(500),
1244            }
1245        }
1246
1247        /// Creates a worker with slow initialization.
1248        fn slow_init(name: &'static str, init_delay: Duration) -> Self {
1249            Self {
1250                name,
1251                init_behavior: InitBehavior::Slow(init_delay),
1252                run_behavior: RunBehavior::UntilShutdown,
1253                start_count: Arc::new(AtomicUsize::new(0)),
1254                brutal_shutdown: false,
1255                graceful_timeout: Duration::from_millis(500),
1256            }
1257        }
1258
1259        /// Returns a shared handle to the start count for this worker.
1260        fn start_count(&self) -> Arc<AtomicUsize> {
1261            Arc::clone(&self.start_count)
1262        }
1263
1264        /// Configures this worker to use a `Brutal` shutdown strategy (immediate abort, no graceful wait).
1265        fn with_brutal_shutdown(mut self) -> Self {
1266            self.brutal_shutdown = true;
1267            self
1268        }
1269
1270        /// Overrides the worker's graceful shutdown timeout (defaults to 500 milliseconds).
1271        fn with_graceful_timeout(mut self, timeout: Duration) -> Self {
1272            self.graceful_timeout = timeout;
1273            self
1274        }
1275    }
1276
1277    #[async_trait]
1278    impl Supervisable for MockWorker {
1279        fn name(&self) -> &str {
1280            self.name
1281        }
1282
1283        fn shutdown_strategy(&self) -> ShutdownStrategy {
1284            if self.brutal_shutdown {
1285                ShutdownStrategy::Brutal
1286            } else {
1287                ShutdownStrategy::Graceful(self.graceful_timeout)
1288            }
1289        }
1290
1291        async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
1292            match &self.init_behavior {
1293                InitBehavior::Instant => {}
1294                InitBehavior::Slow(delay) => {
1295                    sleep(*delay).await;
1296                }
1297                InitBehavior::Fail(msg) => {
1298                    return Err(InitializationError::Failed {
1299                        source: GenericError::msg(*msg),
1300                    });
1301                }
1302            }
1303
1304            let start_count = Arc::clone(&self.start_count);
1305            let run_behavior = self.run_behavior.clone();
1306
1307            Ok(Box::pin(async move {
1308                start_count.fetch_add(1, Ordering::SeqCst);
1309
1310                match run_behavior {
1311                    RunBehavior::UntilShutdown => {
1312                        process_shutdown.await;
1313                        Ok(())
1314                    }
1315                    RunBehavior::FailAfter(delay, msg) => {
1316                        select! {
1317                            _ = sleep(delay) => {
1318                                Err(GenericError::msg(msg))
1319                            }
1320                            _ = process_shutdown => {
1321                                Ok(())
1322                            }
1323                        }
1324                    }
1325                    RunBehavior::CompleteAfter(delay) => {
1326                        select! {
1327                            _ = sleep(delay) => Ok(()),
1328                            _ = process_shutdown => Ok(()),
1329                        }
1330                    }
1331                    RunBehavior::SlowShutdown(delay) => {
1332                        process_shutdown.await;
1333                        sleep(delay).await;
1334                        Ok(())
1335                    }
1336                    RunBehavior::IgnoreShutdown => {
1337                        // Hold the handle (so the supervisor counts us as outstanding) but never react to it.
1338                        let _hold = process_shutdown;
1339                        pending().await
1340                    }
1341                    RunBehavior::PanicAfter(delay) => {
1342                        select! {
1343                            _ = sleep(delay) => panic!("worker panicked"),
1344                            _ = process_shutdown => Ok(()),
1345                        }
1346                    }
1347                }
1348            }))
1349        }
1350    }
1351
1352    /// Helper: run a supervisor with a oneshot-based shutdown trigger.
1353    /// Returns the supervisor result and provides the shutdown sender.
1354    async fn run_supervisor_with_trigger(
1355        mut supervisor: Supervisor,
1356    ) -> (oneshot::Sender<()>, JoinHandle<Result<(), SupervisorError>>) {
1357        let (tx, rx) = oneshot::channel();
1358        let handle = tokio::spawn(async move { supervisor.run_with_shutdown(rx).await });
1359        // Give the supervisor a moment to start and spawn children.
1360        sleep(Duration::from_millis(50)).await;
1361        (tx, handle)
1362    }
1363
1364    // -- Supervisor run mode tests ---------------------------------------------------------
1365
1366    #[tokio::test]
1367    async fn standalone_supervisor_shuts_down_cleanly() {
1368        let mut sup = Supervisor::new("test-sup").unwrap();
1369        sup.add_worker(MockWorker::long_running("worker1"));
1370        sup.add_worker(MockWorker::long_running("worker2"));
1371
1372        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1373        tx.send(()).unwrap();
1374
1375        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1376        assert!(result.is_ok());
1377    }
1378
1379    #[tokio::test]
1380    async fn nested_supervisor_shuts_down_cleanly() {
1381        let mut child_sup = Supervisor::new("child-sup").unwrap();
1382        child_sup.add_worker(MockWorker::long_running("inner-worker"));
1383
1384        let mut parent_sup = Supervisor::new("parent-sup").unwrap();
1385        parent_sup.add_worker(MockWorker::long_running("outer-worker"));
1386        parent_sup.add_worker(child_sup);
1387
1388        let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
1389        tx.send(()).unwrap();
1390
1391        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1392        assert!(result.is_ok());
1393    }
1394
1395    #[tokio::test]
1396    async fn empty_supervisor_idles_until_shutdown() {
1397        // A supervisor with no static children is valid: it idles, waiting for dynamic children, and shuts down
1398        // cleanly when signalled. (Before dynamic children were folded in, this returned a `NoChildren` error.)
1399        let sup = Supervisor::new("empty-sup").unwrap();
1400
1401        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1402        assert!(!handle.is_finished(), "an empty supervisor must idle rather than exit");
1403
1404        tx.send(()).unwrap();
1405        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1406        assert!(result.is_ok());
1407    }
1408
1409    // -- Child restart behavior tests ------------------------------------------------------
1410
1411    #[tokio::test]
1412    async fn one_for_one_restarts_only_failed_child() {
1413        let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1414        let failing_count = failing.start_count();
1415
1416        let stable = MockWorker::long_running("stable-worker");
1417        let stable_count = stable.start_count();
1418
1419        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1420            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1421        );
1422        sup.add_worker(stable);
1423        sup.add_worker(failing);
1424
1425        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1426
1427        // Wait for a few restarts to happen.
1428        sleep(Duration::from_millis(300)).await;
1429        let _ = tx.send(());
1430
1431        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1432        assert!(result.is_ok());
1433
1434        // The failing worker should have been started multiple times.
1435        assert!(
1436            failing_count.load(Ordering::SeqCst) >= 2,
1437            "failing worker should have been restarted"
1438        );
1439        // The stable worker should only have been started once (never restarted).
1440        assert_eq!(
1441            stable_count.load(Ordering::SeqCst),
1442            1,
1443            "stable worker should not have been restarted"
1444        );
1445    }
1446
1447    #[tokio::test]
1448    async fn one_for_all_restarts_all_children() {
1449        let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1450        let failing_count = failing.start_count();
1451
1452        let stable = MockWorker::long_running("stable-worker");
1453        let stable_count = stable.start_count();
1454
1455        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1456            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1457        );
1458        sup.add_worker(stable);
1459        sup.add_worker(failing);
1460
1461        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1462
1463        // Wait for at least one restart cycle.
1464        sleep(Duration::from_millis(300)).await;
1465        let _ = tx.send(());
1466
1467        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1468        assert!(result.is_ok());
1469
1470        // Both workers should have been started multiple times.
1471        assert!(
1472            failing_count.load(Ordering::SeqCst) >= 2,
1473            "failing worker should have been restarted"
1474        );
1475        assert!(
1476            stable_count.load(Ordering::SeqCst) >= 2,
1477            "stable worker should also have been restarted"
1478        );
1479    }
1480
1481    #[tokio::test]
1482    async fn one_for_all_does_not_restart_temporary_children() {
1483        // A permanent worker that fails repeatedly drives one-for-all restarts; a temporary sibling is shut down with
1484        // the group on each cycle but, per OTP semantics, must never be brought back.
1485        let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1486        let failing_count = failing.start_count();
1487
1488        let temp = MockWorker::long_running("temp-worker");
1489        let temp_count = temp.start_count();
1490
1491        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1492            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1493        );
1494        sup.add_worker(ChildSpecification::worker(temp).with_restart_type(RestartType::Temporary));
1495        sup.add_worker(failing);
1496
1497        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1498
1499        // Let several one-for-all cycles occur.
1500        sleep(Duration::from_millis(300)).await;
1501        let _ = tx.send(());
1502
1503        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1504        assert!(result.is_ok());
1505        assert!(
1506            failing_count.load(Ordering::SeqCst) >= 2,
1507            "permanent worker should have been restarted by one-for-all"
1508        );
1509        assert_eq!(
1510            temp_count.load(Ordering::SeqCst),
1511            1,
1512            "temporary child must not be restarted by a one-for-all group restart"
1513        );
1514    }
1515
1516    #[tokio::test]
1517    async fn one_for_all_restarts_transient_children() {
1518        // A transient child that exits cleanly is not restarted on its own, but a one-for-all restart triggered by a
1519        // sibling restarts it anyway -- matching OTP, where only temporary children are exempt from group restarts.
1520        let transient = MockWorker::completing("transient-worker", Duration::from_millis(30));
1521        let transient_count = transient.start_count();
1522
1523        // Fails after the transient has already exited cleanly, so the group restart is what brings the transient back.
1524        let failing = MockWorker::failing("failing-worker", Duration::from_millis(80));
1525
1526        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1527            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1528        );
1529        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1530        sup.add_worker(failing);
1531
1532        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1533
1534        sleep(Duration::from_millis(300)).await;
1535        let _ = tx.send(());
1536
1537        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1538        assert!(result.is_ok());
1539        assert!(
1540            transient_count.load(Ordering::SeqCst) >= 2,
1541            "transient child must be restarted by a one-for-all group restart, even after a clean exit"
1542        );
1543    }
1544
1545    #[tokio::test]
1546    async fn transient_abnormal_exit_triggers_one_for_all() {
1547        // A transient child's *own* abnormal exit is restartable, so under one-for-all it triggers a whole-group
1548        // restart -- the sibling is restarted too, not just the transient.
1549        let transient = MockWorker::failing("transient-worker", Duration::from_millis(50));
1550        let transient_count = transient.start_count();
1551
1552        let stable = MockWorker::long_running("stable-worker");
1553        let stable_count = stable.start_count();
1554
1555        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1556            RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1557        );
1558        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1559        sup.add_worker(stable);
1560
1561        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1562
1563        sleep(Duration::from_millis(300)).await;
1564        let _ = tx.send(());
1565
1566        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1567        assert!(result.is_ok());
1568        assert!(
1569            transient_count.load(Ordering::SeqCst) >= 2,
1570            "transient worker must be restarted after its own abnormal exit"
1571        );
1572        assert!(
1573            stable_count.load(Ordering::SeqCst) >= 2,
1574            "the transient's abnormal exit must trigger a one-for-all that also restarts the sibling"
1575        );
1576    }
1577
1578    #[tokio::test]
1579    async fn restart_limit_exceeded_shuts_down_supervisor() {
1580        let mut sup = Supervisor::new("test-sup")
1581            .unwrap()
1582            .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
1583        // This worker fails immediately, which will exhaust the restart budget quickly.
1584        sup.add_worker(MockWorker::failing("fast-fail", Duration::ZERO));
1585
1586        let (tx, rx) = oneshot::channel::<()>();
1587        let handle = tokio::spawn(async move { sup.run_with_shutdown(rx).await });
1588
1589        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1590        drop(tx);
1591
1592        assert!(matches!(result, Err(SupervisorError::Shutdown)));
1593    }
1594
1595    // -- Restart type tests ----------------------------------------------------------------
1596
1597    #[tokio::test]
1598    async fn temporary_child_is_not_restarted() {
1599        // A temporary worker that fails quickly, alongside a long-running worker that keeps the supervisor alive.
1600        let temp = MockWorker::failing("temp-worker", Duration::from_millis(50));
1601        let temp_count = temp.start_count();
1602
1603        let stable = MockWorker::long_running("stable-worker");
1604
1605        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1606            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1607        );
1608        sup.add_worker(stable);
1609        sup.add_worker(ChildSpecification::worker(temp).with_restart_type(RestartType::Temporary));
1610
1611        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1612
1613        // Give the temporary worker time to fail; it must not be restarted.
1614        sleep(Duration::from_millis(300)).await;
1615        let _ = tx.send(());
1616
1617        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1618        assert!(result.is_ok());
1619        assert_eq!(
1620            temp_count.load(Ordering::SeqCst),
1621            1,
1622            "temporary worker must not be restarted"
1623        );
1624    }
1625
1626    #[tokio::test]
1627    async fn transient_child_is_not_restarted_on_clean_exit() {
1628        let transient = MockWorker::completing("transient-worker", Duration::from_millis(50));
1629        let transient_count = transient.start_count();
1630
1631        let stable = MockWorker::long_running("stable-worker");
1632
1633        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1634            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1635        );
1636        sup.add_worker(stable);
1637        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1638
1639        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1640
1641        sleep(Duration::from_millis(300)).await;
1642        let _ = tx.send(());
1643
1644        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1645        assert!(result.is_ok());
1646        assert_eq!(
1647            transient_count.load(Ordering::SeqCst),
1648            1,
1649            "transient worker must not be restarted after a clean exit"
1650        );
1651    }
1652
1653    #[tokio::test]
1654    async fn transient_child_is_restarted_on_failure() {
1655        let transient = MockWorker::failing("transient-worker", Duration::from_millis(50));
1656        let transient_count = transient.start_count();
1657
1658        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1659            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1660        );
1661        sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1662
1663        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1664
1665        sleep(Duration::from_millis(300)).await;
1666        let _ = tx.send(());
1667
1668        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1669        assert!(result.is_ok());
1670        assert!(
1671            transient_count.load(Ordering::SeqCst) >= 2,
1672            "transient worker must be restarted after an abnormal exit"
1673        );
1674    }
1675
1676    #[tokio::test]
1677    async fn permanent_child_is_restarted_on_clean_exit() {
1678        // A permanent worker that completes cleanly must still be restarted -- this is what distinguishes
1679        // `Permanent` from `Transient`, which is left stopped after a clean exit.
1680        let permanent = MockWorker::completing("permanent-worker", Duration::from_millis(50));
1681        let permanent_count = permanent.start_count();
1682
1683        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1684            RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1685        );
1686        // Added with the default restart policy, which is `Permanent`.
1687        sup.add_worker(permanent);
1688
1689        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1690
1691        sleep(Duration::from_millis(300)).await;
1692        let _ = tx.send(());
1693
1694        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1695        assert!(result.is_ok());
1696        assert!(
1697            permanent_count.load(Ordering::SeqCst) >= 2,
1698            "permanent worker must be restarted even after a clean exit"
1699        );
1700    }
1701
1702    #[tokio::test]
1703    async fn temporary_failures_do_not_consume_restart_intensity() {
1704        // With intensity=1, two *restartable* failures within the period would shut the supervisor down. Here several
1705        // temporary workers all fail quickly. Because temporary exits aren't eligible for restart, they must not consume
1706        // the restart-intensity budget, and the supervisor must stay up.
1707        let mut sup = Supervisor::new("test-sup")
1708            .unwrap()
1709            .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
1710
1711        let workers = [
1712            MockWorker::failing("temp-0", Duration::from_millis(20)),
1713            MockWorker::failing("temp-1", Duration::from_millis(20)),
1714            MockWorker::failing("temp-2", Duration::from_millis(20)),
1715            MockWorker::failing("temp-3", Duration::from_millis(20)),
1716            MockWorker::failing("temp-4", Duration::from_millis(20)),
1717        ];
1718        let counts: Vec<_> = workers.iter().map(|w| w.start_count()).collect();
1719        for worker in workers {
1720            sup.add_worker(ChildSpecification::worker(worker).with_restart_type(RestartType::Temporary));
1721        }
1722        // A long-running worker so the supervisor doesn't simply idle once the temporaries are gone.
1723        sup.add_worker(MockWorker::long_running("stable-worker"));
1724
1725        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1726        sleep(Duration::from_millis(300)).await;
1727        let _ = tx.send(());
1728
1729        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1730        assert!(
1731            result.is_ok(),
1732            "supervisor must not trip its restart limit on temporary exits"
1733        );
1734        for count in counts {
1735            assert_eq!(
1736                count.load(Ordering::SeqCst),
1737                1,
1738                "each temporary worker runs exactly once"
1739            );
1740        }
1741    }
1742
1743    #[tokio::test]
1744    async fn transient_clean_exits_do_not_consume_restart_intensity() {
1745        // With intensity=1, two *restartable* exits within the period would shut the supervisor down. Here several
1746        // transient workers all complete cleanly. A transient child's clean exit isn't eligible for restart, so it
1747        // must not consume the restart-intensity budget, and the supervisor must stay up.
1748        let mut sup = Supervisor::new("test-sup")
1749            .unwrap()
1750            .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
1751
1752        let workers = [
1753            MockWorker::completing("transient-0", Duration::from_millis(20)),
1754            MockWorker::completing("transient-1", Duration::from_millis(20)),
1755            MockWorker::completing("transient-2", Duration::from_millis(20)),
1756            MockWorker::completing("transient-3", Duration::from_millis(20)),
1757            MockWorker::completing("transient-4", Duration::from_millis(20)),
1758        ];
1759        let counts: Vec<_> = workers.iter().map(|w| w.start_count()).collect();
1760        for worker in workers {
1761            sup.add_worker(ChildSpecification::worker(worker).with_restart_type(RestartType::Transient));
1762        }
1763        // A long-running worker so the supervisor doesn't simply idle once the transients have completed.
1764        sup.add_worker(MockWorker::long_running("stable-worker"));
1765
1766        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1767        sleep(Duration::from_millis(300)).await;
1768        let _ = tx.send(());
1769
1770        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1771        assert!(
1772            result.is_ok(),
1773            "supervisor must not trip its restart limit on clean transient exits"
1774        );
1775        for count in counts {
1776            assert_eq!(
1777                count.load(Ordering::SeqCst),
1778                1,
1779                "each transient worker runs exactly once"
1780            );
1781        }
1782    }
1783
1784    #[tokio::test]
1785    async fn supervisor_idles_when_all_temporary_children_exit() {
1786        // When every child is temporary and they all exit, the worker set drains. The supervisor must not panic or exit
1787        // on its own; it should keep waiting until shutdown is triggered.
1788        let mut sup = Supervisor::new("test-sup").unwrap();
1789        sup.add_worker(
1790            ChildSpecification::worker(MockWorker::completing("temp-a", Duration::from_millis(30)))
1791                .with_restart_type(RestartType::Temporary),
1792        );
1793        sup.add_worker(
1794            ChildSpecification::worker(MockWorker::completing("temp-b", Duration::from_millis(30)))
1795                .with_restart_type(RestartType::Temporary),
1796        );
1797
1798        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1799
1800        // Both children complete well within this window; the supervisor should still be running (idling).
1801        sleep(Duration::from_millis(200)).await;
1802        assert!(
1803            !handle.is_finished(),
1804            "supervisor must keep running after all children exit"
1805        );
1806
1807        let _ = tx.send(());
1808        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1809        assert!(result.is_ok());
1810    }
1811
1812    // -- Significant child / auto-shutdown tests -------------------------------------------
1813
1814    #[tokio::test]
1815    async fn significant_child_drives_auto_shutdown() {
1816        // With `AnySignificant`, a significant child terminating (even cleanly, and without being restarted) must
1817        // shut the supervisor down, surfacing the significant-exit error.
1818        let mut sup = Supervisor::new("test-sup")
1819            .unwrap()
1820            .with_auto_shutdown(AutoShutdown::AnySignificant);
1821        sup.add_worker(MockWorker::long_running("stable"));
1822        sup.add_worker(
1823            ChildSpecification::worker(MockWorker::completing("significant", Duration::from_millis(50)))
1824                .with_restart_type(RestartType::Temporary)
1825                .with_significant(true),
1826        );
1827
1828        // Hold the shutdown sender so the only thing that can stop the supervisor is the significant child.
1829        let (_tx, rx) = oneshot::channel::<()>();
1830        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
1831            .await
1832            .unwrap();
1833        assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
1834    }
1835
1836    #[tokio::test]
1837    async fn non_significant_exit_does_not_auto_shutdown() {
1838        // Even with `AnySignificant` set, a non-significant child exiting must not shut the supervisor down.
1839        let mut sup = Supervisor::new("test-sup")
1840            .unwrap()
1841            .with_auto_shutdown(AutoShutdown::AnySignificant);
1842        sup.add_worker(MockWorker::long_running("stable"));
1843        sup.add_worker(
1844            ChildSpecification::worker(MockWorker::completing("plain", Duration::from_millis(50)))
1845                .with_restart_type(RestartType::Temporary),
1846        );
1847
1848        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1849        sleep(Duration::from_millis(200)).await;
1850        assert!(
1851            !handle.is_finished(),
1852            "a non-significant child exiting must not trigger auto-shutdown"
1853        );
1854
1855        tx.send(()).unwrap();
1856        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
1857        assert!(result.is_ok());
1858    }
1859
1860    #[tokio::test]
1861    async fn all_significant_waits_for_last() {
1862        // With `AllSignificant`, the supervisor shuts down only once *all* significant children have terminated.
1863        let mut sup = Supervisor::new("test-sup")
1864            .unwrap()
1865            .with_auto_shutdown(AutoShutdown::AllSignificant);
1866        sup.add_worker(
1867            ChildSpecification::worker(MockWorker::completing("sig-a", Duration::from_millis(50)))
1868                .with_restart_type(RestartType::Temporary)
1869                .with_significant(true),
1870        );
1871        sup.add_worker(
1872            ChildSpecification::worker(MockWorker::completing("sig-b", Duration::from_millis(250)))
1873                .with_restart_type(RestartType::Temporary)
1874                .with_significant(true),
1875        );
1876
1877        let (_tx, rx) = oneshot::channel::<()>();
1878        let start = std::time::Instant::now();
1879        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
1880            .await
1881            .unwrap();
1882        let elapsed = start.elapsed();
1883
1884        assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
1885        // The first significant child exits at ~50ms but must NOT trigger shutdown; only the second (~250ms) does.
1886        assert!(
1887            elapsed >= Duration::from_millis(200),
1888            "auto-shutdown must wait for all significant children (took {elapsed:?})"
1889        );
1890    }
1891
1892    // -- Initialization failure tests ------------------------------------------------------
1893
1894    #[tokio::test]
1895    async fn init_failure_propagates_with_child_name() {
1896        let mut sup = Supervisor::new("test-sup").unwrap();
1897        sup.add_worker(MockWorker::long_running("good-worker"));
1898        sup.add_worker(MockWorker::init_failure("bad-worker"));
1899
1900        let (_tx, rx) = oneshot::channel::<()>();
1901        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
1902            .await
1903            .unwrap();
1904
1905        match result {
1906            Err(SupervisorError::FailedToInitialize { child_name, .. }) => {
1907                assert_eq!(child_name, "bad-worker");
1908            }
1909            other => panic!("expected FailedToInitialize, got: {:?}", other),
1910        }
1911    }
1912
1913    #[tokio::test]
1914    async fn init_failure_does_not_trigger_restart() {
1915        let init_fail = MockWorker::init_failure("bad-worker");
1916        let start_count = init_fail.start_count();
1917
1918        let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1919            RestartStrategy::one_to_one().with_intensity_and_period(10, Duration::from_secs(10)),
1920        );
1921        sup.add_worker(init_fail);
1922
1923        let (_tx, rx) = oneshot::channel::<()>();
1924        let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
1925            .await
1926            .unwrap();
1927
1928        assert!(matches!(result, Err(SupervisorError::FailedToInitialize { .. })));
1929        // The worker never got past init, so start_count should be 0.
1930        assert_eq!(start_count.load(Ordering::SeqCst), 0);
1931    }
1932
1933    // -- Shutdown responsiveness tests -----------------------------------------------------
1934
1935    #[tokio::test]
1936    async fn shutdown_completes_promptly_in_steady_state() {
1937        let mut sup = Supervisor::new("test-sup").unwrap();
1938        sup.add_worker(MockWorker::long_running("worker1"));
1939        sup.add_worker(MockWorker::long_running("worker2"));
1940
1941        let (tx, handle) = run_supervisor_with_trigger(sup).await;
1942        tx.send(()).unwrap();
1943
1944        // Shutdown should complete well within 1 second (workers respond to shutdown signal immediately).
1945        let result = timeout(Duration::from_secs(1), handle).await;
1946        assert!(result.is_ok(), "shutdown should complete promptly");
1947    }
1948
1949    #[tokio::test]
1950    async fn shutdown_during_slow_init_completes_promptly() {
1951        let mut sup = Supervisor::new("test-sup").unwrap();
1952        // This worker takes 30 seconds to initialize — but we'll trigger shutdown immediately.
1953        sup.add_worker(MockWorker::slow_init("slow-worker", Duration::from_secs(30)));
1954
1955        let (tx, rx) = oneshot::channel();
1956        let handle = tokio::spawn(async move { sup.run_with_shutdown(rx).await });
1957
1958        // Give the supervisor just enough time to spawn the task, then trigger shutdown.
1959        sleep(Duration::from_millis(20)).await;
1960        tx.send(()).unwrap();
1961
1962        // Shutdown should complete quickly even though the worker hasn't finished initializing.
1963        // The supervisor loop sees the shutdown signal and aborts the still-initializing task.
1964        let result = timeout(Duration::from_secs(2), handle).await;
1965        assert!(result.is_ok(), "shutdown during slow init should complete promptly");
1966    }
1967
1968    // -- Dynamic children tests ------------------------------------------------------------
1969
1970    async fn wait_running(handle: &SupervisorHandle) {
1971        for _ in 0..200 {
1972            if handle.is_running() {
1973                return;
1974            }
1975            sleep(Duration::from_millis(5)).await;
1976        }
1977        panic!("supervisor did not start in time");
1978    }
1979
1980    async fn wait_until(condition: impl Fn() -> bool) {
1981        for _ in 0..200 {
1982            if condition() {
1983                return;
1984            }
1985            sleep(Duration::from_millis(5)).await;
1986        }
1987        panic!("condition not met in time");
1988    }
1989
1990    #[tokio::test]
1991    async fn dynamic_children_spawn_after_start() {
1992        let sup = Supervisor::new("dyn-sup").unwrap();
1993        let handle = sup.handle();
1994        let (tx, run) = run_supervisor_with_trigger(sup).await;
1995        wait_running(&handle).await;
1996
1997        let c1 = MockWorker::long_running("c1");
1998        let c2 = MockWorker::long_running("c2");
1999        let c1_count = c1.start_count();
2000        let c2_count = c2.start_count();
2001        handle.spawn(c1).await.unwrap();
2002        handle.spawn(c2).await.unwrap();
2003
2004        wait_until(|| c1_count.load(Ordering::SeqCst) == 1 && c2_count.load(Ordering::SeqCst) == 1).await;
2005        assert_eq!(handle.active_children(), 2);
2006
2007        tx.send(()).unwrap();
2008        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2009        assert!(result.is_ok());
2010        assert_eq!(
2011            handle.active_children(),
2012            0,
2013            "all dynamic children must be drained on shutdown"
2014        );
2015    }
2016
2017    #[tokio::test]
2018    async fn temporary_dynamic_child_failure_is_isolated() {
2019        // A dynamic child added with the default config (temporary, not significant) is fault-isolated: its failure is
2020        // reaped and removed without restarting it or disturbing the supervisor or its siblings.
2021        let sup = Supervisor::new("dyn-sup").unwrap();
2022        let handle = sup.handle();
2023        let (tx, run) = run_supervisor_with_trigger(sup).await;
2024        wait_running(&handle).await;
2025
2026        let failing = MockWorker::failing("boom", Duration::from_millis(20));
2027        let failing_count = failing.start_count();
2028        handle.spawn(failing).await.unwrap();
2029        wait_until(|| failing_count.load(Ordering::SeqCst) == 1).await;
2030        wait_until(|| handle.active_children() == 0).await;
2031
2032        sleep(Duration::from_millis(50)).await;
2033        assert!(
2034            handle.is_running(),
2035            "supervisor stays up after an isolated child failure"
2036        );
2037        assert_eq!(
2038            failing_count.load(Ordering::SeqCst),
2039            1,
2040            "a temporary child is never restarted"
2041        );
2042
2043        // It still accepts new children.
2044        handle.spawn(MockWorker::long_running("c2")).await.unwrap();
2045        wait_until(|| handle.active_children() == 1).await;
2046
2047        tx.send(()).unwrap();
2048        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2049        assert!(result.is_ok());
2050    }
2051
2052    #[tokio::test]
2053    async fn temporary_dynamic_child_panic_is_isolated() {
2054        // A panicking temporary, non-significant child is isolated exactly like an error exit.
2055        let sup = Supervisor::new("dyn-sup").unwrap();
2056        let handle = sup.handle();
2057        let (tx, run) = run_supervisor_with_trigger(sup).await;
2058        wait_running(&handle).await;
2059
2060        handle
2061            .spawn(MockWorker::panicking("boom", Duration::from_millis(20)))
2062            .await
2063            .unwrap();
2064        wait_until(|| handle.active_children() == 0).await;
2065
2066        sleep(Duration::from_millis(50)).await;
2067        assert!(handle.is_running(), "supervisor stays up after an isolated child panic");
2068
2069        tx.send(()).unwrap();
2070        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2071        assert!(result.is_ok());
2072    }
2073
2074    #[tokio::test]
2075    async fn significant_dynamic_child_failure_shuts_down_supervisor() {
2076        // A dynamic child added as significant, under `AutoShutdown::AnySignificant`, drives the supervisor to shut
2077        // down when it terminates -- the opt-in mechanism that replaces the old escalate-on-error behavior.
2078        let sup = Supervisor::new("dyn-sup")
2079            .unwrap()
2080            .with_auto_shutdown(AutoShutdown::AnySignificant);
2081        let handle = sup.handle();
2082        let (_tx, run) = run_supervisor_with_trigger(sup).await;
2083        wait_running(&handle).await;
2084
2085        handle
2086            .spawn_with(
2087                ChildSpecification::worker(MockWorker::failing("boom", Duration::from_millis(20)))
2088                    .with_restart_type(RestartType::Temporary)
2089                    .with_significant(true),
2090            )
2091            .await
2092            .unwrap();
2093
2094        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2095        assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
2096    }
2097
2098    #[tokio::test]
2099    async fn dynamic_spawn_fails_before_start_and_after_shutdown() {
2100        let sup = Supervisor::new("dyn-sup").unwrap();
2101        let handle = sup.handle();
2102
2103        // Before the supervisor is running there's nothing to accept the spawn, so it's rejected outright (static
2104        // children should be configured up front via `add_worker` instead).
2105        assert!(!handle.is_running());
2106        let err = handle
2107            .spawn(MockWorker::long_running("before-start"))
2108            .await
2109            .unwrap_err();
2110        assert!(matches!(err, SpawnError::SupervisorGone));
2111
2112        // Once it's running, spawns succeed.
2113        let (tx, run) = run_supervisor_with_trigger(sup).await;
2114        wait_running(&handle).await;
2115        let worker = MockWorker::long_running("after-start");
2116        let started = worker.start_count();
2117        handle.spawn(worker).await.unwrap();
2118        wait_until(|| started.load(Ordering::SeqCst) == 1).await;
2119
2120        tx.send(()).unwrap();
2121        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2122        assert!(result.is_ok());
2123
2124        // Once the supervisor has shut down, the run is gone and spawns are rejected again.
2125        wait_until(|| !handle.is_running()).await;
2126        let err = handle
2127            .spawn(MockWorker::long_running("after-shutdown"))
2128            .await
2129            .unwrap_err();
2130        assert!(matches!(err, SpawnError::SupervisorGone));
2131    }
2132
2133    #[tokio::test]
2134    async fn dynamic_spawn_returns_after_registration() {
2135        let sup = Supervisor::new("dyn-sup").unwrap();
2136        let handle = sup.handle();
2137        let (tx, run) = run_supervisor_with_trigger(sup).await;
2138        wait_running(&handle).await;
2139
2140        let worker = MockWorker::long_running("c");
2141        let started = worker.start_count();
2142        let id = handle.spawn(worker).await.unwrap();
2143        // No static children, so the first dynamic child takes id 0.
2144        assert_eq!(id.as_u64(), 0);
2145        wait_until(|| started.load(Ordering::SeqCst) == 1).await;
2146
2147        tx.send(()).unwrap();
2148        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2149        assert!(result.is_ok());
2150    }
2151
2152    #[tokio::test]
2153    async fn dynamic_spawn_rejects_invalid_child_name() {
2154        // While running, a spawn that fails registration (here, an empty/invalid child name) is reported as
2155        // `Rejected` with the underlying cause -- not `SupervisorGone`, which means the supervisor isn't running.
2156        let sup = Supervisor::new("dyn-sup").unwrap();
2157        let handle = sup.handle();
2158        let (tx, run) = run_supervisor_with_trigger(sup).await;
2159        wait_running(&handle).await;
2160
2161        let err = handle.spawn(MockWorker::long_running("")).await.unwrap_err();
2162        assert!(matches!(err, SpawnError::Rejected { .. }), "got {err:?}");
2163
2164        // The supervisor stays up and still accepts valid children.
2165        assert!(handle.is_running());
2166        handle.spawn(MockWorker::long_running("ok")).await.unwrap();
2167        wait_until(|| handle.active_children() == 1).await;
2168
2169        tx.send(()).unwrap();
2170        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2171        assert!(result.is_ok());
2172    }
2173
2174    #[tokio::test]
2175    async fn concurrent_shutdown_drains_many_children_quickly() {
2176        const CHILDREN: usize = 500;
2177        const SHUTDOWN_DELAY: Duration = Duration::from_millis(50);
2178
2179        let sup = Supervisor::new("dyn-sup")
2180            .unwrap()
2181            .with_shutdown_mode(ShutdownMode::Concurrent);
2182        let handle = sup.handle();
2183        let (tx, run) = run_supervisor_with_trigger(sup).await;
2184        wait_running(&handle).await;
2185
2186        for _ in 0..CHILDREN {
2187            handle
2188                .spawn(MockWorker::slow_shutdown("conn", SHUTDOWN_DELAY))
2189                .await
2190                .unwrap();
2191        }
2192        wait_until(|| handle.active_children() == CHILDREN).await;
2193
2194        // Each child sleeps after observing shutdown. Concurrent shutdown drains them all in roughly one delay; an
2195        // ordered shutdown would take CHILDREN * delay (25s here). Assert it finishes well under that.
2196        let start = std::time::Instant::now();
2197        tx.send(()).unwrap();
2198        let result = timeout(Duration::from_secs(5), run).await.unwrap().unwrap();
2199        let elapsed = start.elapsed();
2200
2201        assert!(result.is_ok());
2202        assert_eq!(handle.active_children(), 0, "active count must return to zero");
2203        assert!(
2204            elapsed < Duration::from_secs(2),
2205            "shutdown must be concurrent (took {elapsed:?})"
2206        );
2207    }
2208
2209    #[tokio::test]
2210    async fn concurrent_shutdown_aborts_unresponsive_children() {
2211        let sup = Supervisor::new("dyn-sup")
2212            .unwrap()
2213            .with_shutdown_mode(ShutdownMode::Concurrent);
2214        let handle = sup.handle();
2215        let (tx, run) = run_supervisor_with_trigger(sup).await;
2216        wait_running(&handle).await;
2217
2218        handle.spawn(MockWorker::ignore_shutdown("stuck")).await.unwrap();
2219        wait_until(|| handle.active_children() == 1).await;
2220
2221        // The child never reacts to shutdown, so it must be aborted once its graceful deadline (500ms) elapses rather
2222        // than hanging the supervisor.
2223        let start = std::time::Instant::now();
2224        tx.send(()).unwrap();
2225        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2226        let elapsed = start.elapsed();
2227
2228        assert!(result.is_ok());
2229        assert_eq!(handle.active_children(), 0);
2230        assert!(
2231            elapsed < Duration::from_secs(1),
2232            "stuck child must be aborted at the deadline (took {elapsed:?})"
2233        );
2234    }
2235
2236    #[tokio::test]
2237    async fn concurrent_shutdown_honors_per_child_deadline() {
2238        // Each child must be aborted at its OWN graceful deadline, not a single shared one. A responsive child with an
2239        // effectively-infinite timeout (modeling a nested supervisor, which uses `Graceful(Duration::MAX)`) coexists
2240        // with an unresponsive child with a short timeout. Under a shared `max` deadline the short-timeout child would
2241        // never be aborted (the shared deadline would be `MAX`) and shutdown would hang.
2242        let sup = Supervisor::new("dyn-sup")
2243            .unwrap()
2244            .with_shutdown_mode(ShutdownMode::Concurrent);
2245        let handle = sup.handle();
2246        let (tx, run) = run_supervisor_with_trigger(sup).await;
2247        wait_running(&handle).await;
2248
2249        // Responds to shutdown promptly, but its deadline is effectively infinite.
2250        handle
2251            .spawn(MockWorker::long_running("responsive").with_graceful_timeout(Duration::MAX))
2252            .await
2253            .unwrap();
2254        // Never responds; must be aborted at its own short deadline.
2255        handle
2256            .spawn(MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(200)))
2257            .await
2258            .unwrap();
2259        wait_until(|| handle.active_children() == 2).await;
2260
2261        let start = std::time::Instant::now();
2262        tx.send(()).unwrap();
2263        let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2264        let elapsed = start.elapsed();
2265
2266        assert!(result.is_ok());
2267        assert_eq!(handle.active_children(), 0);
2268        assert!(
2269            elapsed < Duration::from_secs(1),
2270            "stuck child must be aborted at its own deadline despite an infinite-timeout sibling (took {elapsed:?})"
2271        );
2272    }
2273
2274    #[tokio::test]
2275    async fn ordered_shutdown_aborts_unresponsive_child() {
2276        // Under the default `ShutdownMode::Ordered`, a child that never reacts to shutdown must be aborted once its
2277        // graceful deadline (500ms) elapses, rather than hanging the supervisor.
2278        let mut sup = Supervisor::new("test-sup").unwrap();
2279        sup.add_worker(MockWorker::ignore_shutdown("stuck"));
2280
2281        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2282
2283        let start = std::time::Instant::now();
2284        tx.send(()).unwrap();
2285        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
2286        let elapsed = start.elapsed();
2287
2288        assert!(result.is_ok());
2289        assert!(
2290            elapsed < Duration::from_secs(1),
2291            "unresponsive child must be aborted at its deadline under ordered shutdown (took {elapsed:?})"
2292        );
2293    }
2294
2295    #[tokio::test]
2296    async fn brutal_shutdown_aborts_child_immediately() {
2297        // A child with a `Brutal` shutdown strategy is aborted at once on shutdown, with no graceful wait -- so even a
2298        // child that ignores shutdown is torn down promptly rather than after the graceful deadline.
2299        let mut sup = Supervisor::new("test-sup").unwrap();
2300        sup.add_worker(MockWorker::ignore_shutdown("brutal-stuck").with_brutal_shutdown());
2301
2302        let (tx, handle) = run_supervisor_with_trigger(sup).await;
2303
2304        let start = std::time::Instant::now();
2305        tx.send(()).unwrap();
2306        let result = timeout(Duration::from_secs(2), handle).await.unwrap().unwrap();
2307        let elapsed = start.elapsed();
2308
2309        assert!(result.is_ok());
2310        assert!(
2311            elapsed < Duration::from_millis(200),
2312            "brutal-shutdown child must be aborted immediately, not after a graceful wait (took {elapsed:?})"
2313        );
2314    }
2315}