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
32pub type SupervisorFuture = Pin<Box<dyn Future<Output = Result<(), GenericError>> + Send>>;
34
35pub(super) type WorkerFuture = Pin<Box<dyn Future<Output = Result<(), WorkerError>> + Send>>;
41
42#[derive(Debug)]
47pub(super) enum WorkerError {
48 Initialization {
54 child_name: Option<String>,
55 source: InitializationError,
56 },
57
58 Runtime(GenericError),
60
61 ShutdownTimedOut {
68 aborted: usize,
70 },
71}
72
73impl From<SupervisorError> for WorkerError {
74 fn from(err: SupervisorError) -> Self {
75 match err {
76 SupervisorError::FailedToInitialize { child_name, source } => WorkerError::Initialization {
79 child_name: Some(child_name),
80 source,
81 },
82 SupervisorError::ShutdownTimedOut { aborted } => WorkerError::ShutdownTimedOut { aborted },
84 other => WorkerError::Runtime(other.into()),
86 }
87 }
88}
89
90#[derive(Debug, Snafu)]
92pub enum ProcessError {
93 #[snafu(display("Child process was aborted by the supervisor."))]
95 Aborted,
96
97 #[snafu(display("Child process panicked."))]
99 Panicked,
100
101 #[snafu(display("Child process terminated with an error: {}", source))]
103 Terminated {
104 source: GenericError,
106 },
107}
108
109#[derive(Debug, Snafu)]
115#[snafu(context(suffix(false)))]
116pub enum InitializationError {
117 #[snafu(display("Process failed to initialize: {}", source))]
119 Failed {
120 source: GenericError,
122 },
123}
124
125impl From<GenericError> for InitializationError {
126 fn from(source: GenericError) -> Self {
127 Self::Failed { source }
128 }
129}
130
131pub enum ShutdownStrategy {
133 Graceful(Duration),
135
136 Brutal,
138}
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
147pub enum AutoShutdown {
148 #[default]
150 Never,
151
152 AnySignificant,
154
155 AllSignificant,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
161pub enum ShutdownMode {
162 #[default]
167 Ordered,
168
169 Concurrent,
174}
175
176#[async_trait]
178pub trait Supervisable: Send + Sync {
179 fn name(&self) -> &str;
181
182 fn shutdown_strategy(&self) -> ShutdownStrategy {
184 ShutdownStrategy::Graceful(Duration::from_secs(5))
185 }
186
187 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError>;
201}
202
203#[derive(Debug, Snafu)]
205#[snafu(context(suffix(false)))]
206pub enum SupervisorError {
207 #[snafu(display("Invalid name for supervisor or worker: '{}'", name))]
209 InvalidName {
210 name: String,
212 },
213
214 #[snafu(display("Child process '{}' failed to initialize: {}", child_name, source))]
219 FailedToInitialize {
220 child_name: String,
222
223 source: InitializationError,
225 },
226
227 #[snafu(display("Supervisor has exceeded restart limits and was forced to shutdown."))]
229 Shutdown,
230
231 #[snafu(display("Supervisor shut down after a significant child terminated."))]
236 SignificantChildExited,
237
238 #[snafu(display(
247 "Shutdown completed uncleanly: {} worker(s) were forcefully aborted after exceeding their shutdown timeout.",
248 aborted
249 ))]
250 ShutdownTimedOut {
251 aborted: usize,
253 },
254}
255
256pub struct ChildSpecification<S = WorkerSpec> {
272 spec_inner: S,
273}
274
275pub struct WorkerSpec {
277 worker: Arc<dyn Supervisable>,
278 config: ChildConfig,
279}
280
281pub struct SupervisorSpec {
283 supervisor: Supervisor,
284}
285
286impl ChildSpecification<WorkerSpec> {
287 pub fn worker<T: Supervisable + 'static>(worker: T) -> Self {
289 Self {
290 spec_inner: WorkerSpec {
291 worker: Arc::new(worker),
292 config: ChildConfig::default(),
293 },
294 }
295 }
296
297 #[must_use]
301 pub fn with_restart_type(mut self, restart_type: RestartType) -> Self {
302 self.spec_inner.config.restart = restart_type;
303 self
304 }
305
306 #[must_use]
312 pub fn with_significant(mut self, significant: bool) -> Self {
313 self.spec_inner.config.significant = significant;
314 self
315 }
316
317 fn into_worker_parts(self) -> (SupervisedChild, ChildConfig) {
319 (SupervisedChild::Worker(self.spec_inner.worker), self.spec_inner.config)
320 }
321}
322
323impl<T> From<T> for ChildSpecification<WorkerSpec>
324where
325 T: Supervisable + 'static,
326{
327 fn from(worker: T) -> Self {
328 Self::worker(worker)
329 }
330}
331
332impl From<Supervisor> for ChildSpecification<SupervisorSpec> {
333 fn from(supervisor: Supervisor) -> Self {
334 Self {
335 spec_inner: SupervisorSpec { supervisor },
336 }
337 }
338}
339
340mod sealed {
341 pub trait Sealed {}
342}
343
344impl sealed::Sealed for WorkerSpec {}
345impl sealed::Sealed for SupervisorSpec {}
346
347pub trait ChildState: sealed::Sealed + Sized {
354 #[doc(hidden)]
355 fn register(spec: ChildSpecification<Self>, supervisor: &mut Supervisor);
356}
357
358impl ChildState for WorkerSpec {
359 fn register(spec: ChildSpecification<Self>, supervisor: &mut Supervisor) {
360 let (child, config) = spec.into_worker_parts();
361 supervisor.push_child(ChildEntry {
362 spec: child,
363 config,
364 dynamic: false,
365 });
366 }
367}
368
369impl ChildState for SupervisorSpec {
370 fn register(spec: ChildSpecification<Self>, supervisor: &mut Supervisor) {
371 supervisor.push_child(ChildEntry {
372 spec: SupervisedChild::Supervisor(spec.spec_inner.supervisor),
373 config: ChildConfig::default(),
374 dynamic: false,
375 });
376 }
377}
378
379pub(super) enum SupervisedChild {
381 Worker(Arc<dyn Supervisable>),
382 Supervisor(Supervisor),
383}
384
385impl SupervisedChild {
386 fn process_type(&self) -> &'static str {
387 match self {
388 Self::Worker(_) => "worker",
389 Self::Supervisor(_) => "supervisor",
390 }
391 }
392
393 fn name(&self) -> &str {
394 match self {
395 Self::Worker(worker) => worker.name(),
396 Self::Supervisor(supervisor) => &supervisor.supervisor_id,
397 }
398 }
399
400 pub(super) fn shutdown_strategy(&self) -> ShutdownStrategy {
401 match self {
402 Self::Worker(worker) => worker.shutdown_strategy(),
403
404 Self::Supervisor(_) => ShutdownStrategy::Graceful(Duration::MAX),
407 }
408 }
409
410 pub(super) fn create_process(&self, parent_process: &Process) -> Result<Process, SupervisorError> {
411 match self {
412 Self::Worker(worker) => Process::worker(worker.name(), parent_process).context(InvalidName {
413 name: worker.name().to_string(),
414 }),
415 Self::Supervisor(sup) => {
416 Process::supervisor(&sup.supervisor_id, Some(parent_process)).context(InvalidName {
417 name: sup.supervisor_id.to_string(),
418 })
419 }
420 }
421 }
422
423 pub(super) fn create_worker_future(
424 &self, process: Process, process_shutdown: ShutdownHandle,
425 ) -> Result<WorkerFuture, SupervisorError> {
426 match self {
427 Self::Worker(worker) => {
428 let worker = Arc::clone(worker);
429 Ok(Box::pin(async move {
430 let run_future =
431 worker
432 .initialize(process_shutdown)
433 .await
434 .map_err(|source| WorkerError::Initialization {
435 child_name: None,
436 source,
437 })?;
438 run_future.await.map_err(WorkerError::Runtime)
439 }))
440 }
441 Self::Supervisor(sup) => {
442 match sup.runtime_mode() {
443 RuntimeMode::Ambient => {
444 Ok(sup.as_nested_process(process, process_shutdown))
446 }
447 RuntimeMode::Dedicated(config) => {
448 let child_name = sup.supervisor_id.to_string();
451 let dataspace = process.dataspace().clone();
452 let handle =
453 spawn_dedicated_runtime(sup.inner_clone(), config.clone(), process_shutdown, dataspace)
454 .map_err(|e| SupervisorError::FailedToInitialize {
455 child_name,
456 source: e.into(),
457 })?;
458
459 Ok(Box::pin(async move { handle.await.map_err(WorkerError::from) }))
460 }
461 }
462 }
463 }
464 }
465}
466
467impl Clone for SupervisedChild {
468 fn clone(&self) -> Self {
469 match self {
470 Self::Worker(worker) => Self::Worker(Arc::clone(worker)),
471 Self::Supervisor(supervisor) => Self::Supervisor(supervisor.inner_clone()),
472 }
473 }
474}
475
476#[derive(Clone, Copy, Debug)]
481struct ChildConfig {
482 restart: RestartType,
483 significant: bool,
484}
485
486impl Default for ChildConfig {
487 fn default() -> Self {
488 Self {
489 restart: RestartType::Permanent,
490 significant: false,
491 }
492 }
493}
494
495#[derive(Clone)]
497struct ChildEntry {
498 spec: SupervisedChild,
499 config: ChildConfig,
500 dynamic: bool,
503}
504
505#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
510pub struct ChildId(u64);
511
512impl ChildId {
513 pub const fn as_u64(self) -> u64 {
515 self.0
516 }
517}
518
519#[derive(Debug, Snafu)]
521pub enum SpawnError {
522 #[snafu(display("supervisor is gone"))]
528 SupervisorGone,
529
530 #[snafu(display("supervisor rejected the spawn: {}", source))]
535 Rejected {
536 source: GenericError,
538 },
539}
540
541struct PendingSpawn {
543 id: u64,
544 spec: SupervisedChild,
545 config: ChildConfig,
546 ack: oneshot::Sender<Result<(), SpawnError>>,
547}
548
549const DYNAMIC_SPAWN_CHANNEL_CAPACITY: usize = 1024;
554
555#[derive(Clone)]
561pub struct SupervisorHandle {
562 name: Arc<str>,
563 current_tx: Arc<Mutex<Option<mpsc::Sender<PendingSpawn>>>>,
566 id_counter: Arc<AtomicU64>,
567 active: Arc<AtomicUsize>,
568}
569
570impl SupervisorHandle {
571 pub fn name(&self) -> &str {
573 &self.name
574 }
575
576 pub async fn spawn<T: Supervisable + 'static>(&self, worker: T) -> Result<ChildId, SpawnError> {
589 self.spawn_with(ChildSpecification::worker(worker).with_restart_type(RestartType::Temporary))
590 .await
591 }
592
593 pub async fn spawn_with(&self, spec: ChildSpecification<WorkerSpec>) -> Result<ChildId, SpawnError> {
607 let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
608 let (spec, config) = spec.into_worker_parts();
609 let (ack_tx, ack_rx) = oneshot::channel();
610 self.send(PendingSpawn {
611 id,
612 spec,
613 config,
614 ack: ack_tx,
615 })
616 .await?;
617
618 ack_rx
621 .await
622 .map_err(|_| SpawnError::SupervisorGone)?
623 .map(|()| ChildId(id))
624 }
625
626 pub fn is_running(&self) -> bool {
628 self.current_tx.lock().unwrap().is_some()
629 }
630
631 pub fn active_children(&self) -> usize {
633 self.active.load(Ordering::Relaxed)
634 }
635
636 async fn send(&self, spawn: PendingSpawn) -> Result<(), SpawnError> {
640 let tx = self.current_tx.lock().unwrap().clone();
642 match tx {
643 Some(tx) => tx.send(spawn).await.map_err(|_| SpawnError::SupervisorGone),
644 None => Err(SpawnError::SupervisorGone),
645 }
646 }
647}
648
649pub struct Supervisor {
676 supervisor_id: Arc<str>,
677 child_specs: Vec<ChildEntry>,
678 restart_strategy: RestartStrategy,
679 auto_shutdown: AutoShutdown,
680 shutdown_mode: ShutdownMode,
681 runtime_mode: RuntimeMode,
682 current_tx: Arc<Mutex<Option<mpsc::Sender<PendingSpawn>>>>,
686 id_counter: Arc<AtomicU64>,
687 active: Arc<AtomicUsize>,
689}
690
691impl Supervisor {
692 pub fn new<S: AsRef<str>>(supervisor_id: S) -> Result<Self, SupervisorError> {
694 if supervisor_id.as_ref().is_empty() {
698 return Err(SupervisorError::InvalidName {
699 name: supervisor_id.as_ref().to_string(),
700 });
701 }
702
703 Ok(Self {
704 supervisor_id: supervisor_id.as_ref().into(),
705 child_specs: Vec::new(),
706 restart_strategy: RestartStrategy::default(),
707 auto_shutdown: AutoShutdown::default(),
708 shutdown_mode: ShutdownMode::default(),
709 runtime_mode: RuntimeMode::default(),
710 current_tx: Arc::new(Mutex::new(None)),
711 id_counter: Arc::new(AtomicU64::new(0)),
712 active: Arc::new(AtomicUsize::new(0)),
713 })
714 }
715
716 pub fn id(&self) -> &str {
718 &self.supervisor_id
719 }
720
721 pub fn with_restart_strategy(mut self, strategy: RestartStrategy) -> Self {
723 self.restart_strategy = strategy;
724 self
725 }
726
727 pub fn with_auto_shutdown(mut self, auto_shutdown: AutoShutdown) -> Self {
732 self.auto_shutdown = auto_shutdown;
733 self
734 }
735
736 pub fn with_shutdown_mode(mut self, mode: ShutdownMode) -> Self {
738 self.shutdown_mode = mode;
739 self
740 }
741
742 pub fn handle(&self) -> SupervisorHandle {
748 SupervisorHandle {
749 name: Arc::clone(&self.supervisor_id),
750 current_tx: Arc::clone(&self.current_tx),
751 id_counter: Arc::clone(&self.id_counter),
752 active: Arc::clone(&self.active),
753 }
754 }
755
756 pub fn with_dedicated_runtime(mut self, config: RuntimeConfiguration) -> Self {
766 self.runtime_mode = RuntimeMode::Dedicated(config);
767 self
768 }
769
770 pub(crate) fn runtime_mode(&self) -> &RuntimeMode {
772 &self.runtime_mode
773 }
774
775 pub fn add_worker<S, T>(&mut self, child: T)
783 where
784 S: ChildState,
785 T: Into<ChildSpecification<S>>,
786 {
787 S::register(child.into(), self);
788 }
789
790 fn push_child(&mut self, entry: ChildEntry) {
791 debug!(
792 supervisor_id = %self.supervisor_id,
793 "Adding new static child process #{}. ({}, {}, {:?})",
794 self.child_specs.len(),
795 entry.spec.process_type(),
796 entry.spec.name(),
797 entry.config,
798 );
799 self.child_specs.push(entry);
800 }
801
802 fn spawn_static_children(
803 &self, children: &mut FastHashMap<u64, ChildEntry>, worker_state: &mut WorkerState,
804 ) -> Result<(), SupervisorError> {
805 debug!(supervisor_id = %self.supervisor_id, "Spawning all static child processes.");
806 for entry in &self.child_specs {
807 let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
808 worker_state.add_worker(id, &entry.spec)?;
809 children.insert(id, entry.clone());
810 }
811
812 Ok(())
813 }
814
815 fn respawn_children_one_for_all(
823 &self, children: &mut FastHashMap<u64, ChildEntry>, worker_state: &mut WorkerState,
824 ) -> Result<(), SupervisorError> {
825 debug!(supervisor_id = %self.supervisor_id, "Restarting all eligible static child processes.");
826 for entry in &self.child_specs {
827 if entry.config.restart == RestartType::Temporary {
830 continue;
831 }
832 let id = self.id_counter.fetch_add(1, Ordering::Relaxed);
833 worker_state.add_worker(id, &entry.spec)?;
834 children.insert(id, entry.clone());
835 }
836
837 Ok(())
838 }
839
840 fn spawn_dynamic_child(
842 &self, spawn: PendingSpawn, worker_state: &mut WorkerState, children: &mut FastHashMap<u64, ChildEntry>,
843 significant_remaining: &mut usize,
844 ) {
845 let PendingSpawn { id, spec, config, ack } = spawn;
846 let entry = ChildEntry {
847 spec,
848 config,
849 dynamic: true,
850 };
851 match worker_state.add_worker(id, &entry.spec) {
852 Ok(()) => {
853 if config.significant {
854 *significant_remaining += 1;
855 }
856 self.active.fetch_add(1, Ordering::Relaxed);
857 children.insert(id, entry);
858 let _ = ack.send(Ok(()));
859 }
860 Err(e) => {
861 error!(supervisor_id = %self.supervisor_id, error = %e, "Failed to spawn dynamic child.");
864 let _ = ack.send(Err(SpawnError::Rejected { source: e.into() }));
865 }
866 }
867 }
868
869 async fn run_inner(&self, process: Process, process_shutdown: ShutdownHandle) -> Result<(), SupervisorError> {
870 let (cmd_tx, cmd_rx) = mpsc::channel(DYNAMIC_SPAWN_CHANNEL_CAPACITY);
873 *self.current_tx.lock().unwrap() = Some(cmd_tx);
874
875 let result = self.supervise(process, process_shutdown, cmd_rx).await;
876
877 *self.current_tx.lock().unwrap() = None;
880 self.active.store(0, Ordering::Relaxed);
881 result
882 }
883
884 async fn supervise(
885 &self, process: Process, process_shutdown: ShutdownHandle, mut cmd_rx: mpsc::Receiver<PendingSpawn>,
886 ) -> Result<(), SupervisorError> {
887 let mut restart_state = RestartState::new(self.restart_strategy);
888 let mut worker_state = WorkerState::new(process, self.shutdown_mode);
889
890 let mut children: FastHashMap<u64, ChildEntry> = FastHashMap::default();
893
894 self.spawn_static_children(&mut children, &mut worker_state)?;
897
898 let mut significant_remaining = children.values().filter(|entry| entry.config.significant).count();
900
901 pin!(process_shutdown);
903
904 let outcome = loop {
905 select! {
906 biased;
908
909 _ = &mut process_shutdown => break Ok(()),
913
914 spawn = cmd_rx.recv() => {
917 if let Some(spawn) = spawn {
918 self.spawn_dynamic_child(spawn, &mut worker_state, &mut children, &mut significant_remaining);
919 }
920 }
921
922 (child_id, worker_result) = worker_state.wait_for_next_worker() => {
923 let (child_name, config, dynamic) = {
925 let entry = children.get(&child_id).expect("completed worker must be present in the roster");
926 (entry.spec.name().to_string(), entry.config, entry.dynamic)
927 };
928
929 if let Err(WorkerError::Initialization { child_name: inner, source }) = worker_result {
931 let full_name = match inner {
934 Some(inner) => format!("{}/{}", child_name, inner),
935 None => child_name.clone(),
936 };
937
938 error!(supervisor_id = %self.supervisor_id, worker_name = full_name, "Child process failed to initialize: {}", source);
939 break Err(SupervisorError::FailedToInitialize { child_name: full_name, source });
940 }
941
942 let abnormal = worker_result.is_err();
945 let worker_result = worker_result.map_err(|e| match e {
946 WorkerError::Runtime(e) => ProcessError::Terminated { source: e },
947 WorkerError::Initialization { .. } => unreachable!("handled above"),
948 WorkerError::ShutdownTimedOut { aborted } => ProcessError::Terminated {
953 source: SupervisorError::ShutdownTimedOut { aborted }.into(),
954 },
955 });
956
957 if !config.restart.should_restart(abnormal) {
958 debug!(supervisor_id = %self.supervisor_id, worker_name = %child_name, restart = ?config.restart, ?worker_result, "Child process exited and is not eligible for restart.");
963 children.remove(&child_id);
964 if dynamic {
965 self.active.fetch_sub(1, Ordering::Relaxed);
966 }
967
968 if config.significant {
972 significant_remaining = significant_remaining.saturating_sub(1);
973 let auto_shutdown = match self.auto_shutdown {
974 AutoShutdown::Never => false,
975 AutoShutdown::AnySignificant => true,
976 AutoShutdown::AllSignificant => significant_remaining == 0,
977 };
978 if auto_shutdown {
979 warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Significant child terminated; shutting down supervisor.");
980 break Err(SupervisorError::SignificantChildExited);
981 }
982 }
983 } else {
984 match restart_state.evaluate_restart() {
985 RestartAction::Restart(mode) => match mode {
986 RestartMode::OneForOne => {
987 warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting.");
988 let spec = children.get(&child_id).expect("present for restart").spec.clone();
989 if let Err(e) = worker_state.add_worker(child_id, &spec) {
990 break Err(e);
991 }
992 }
993 RestartMode::OneForAll => {
994 warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting all processes.");
995 let _ = worker_state.shutdown_workers().await;
999 children.clear();
1003 self.active.store(0, Ordering::Relaxed);
1004 let respawn = self.respawn_children_one_for_all(&mut children, &mut worker_state);
1005 if let Err(e) = respawn {
1006 break Err(e);
1007 }
1008 significant_remaining =
1009 children.values().filter(|entry| entry.config.significant).count();
1010 }
1011 },
1012 RestartAction::Shutdown => {
1013 error!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Supervisor shutting down due to restart limits.");
1014 break Err(SupervisorError::Shutdown);
1015 }
1016 }
1017 }
1018 }
1019 }
1020 };
1021
1022 cmd_rx.close();
1029 while let Ok(spawn) = cmd_rx.try_recv() {
1030 let _ = spawn.ack.send(Err(SpawnError::SupervisorGone));
1031 }
1032 let aborted = worker_state.shutdown_workers().await;
1033
1034 match outcome {
1039 Ok(()) if aborted > 0 => {
1040 warn!(supervisor_id = %self.supervisor_id, aborted, "Shutdown completed uncleanly; workers were forcefully aborted.");
1041 Err(SupervisorError::ShutdownTimedOut { aborted })
1042 }
1043 outcome => outcome,
1044 }
1045 }
1046
1047 fn as_nested_process(&self, process: Process, process_shutdown: ShutdownHandle) -> WorkerFuture {
1048 debug!(supervisor_id = %self.supervisor_id, "Nested supervisor starting.");
1051
1052 let sup = self.inner_clone();
1054
1055 Box::pin(async move {
1056 sup.run_inner(process, process_shutdown)
1057 .await
1058 .map_err(WorkerError::from)
1059 })
1060 }
1061
1062 pub async fn run(&mut self) -> Result<(), SupervisorError> {
1068 let process_shutdown = ShutdownHandle::noop();
1071 let process = Process::supervisor(&self.supervisor_id, None).context(InvalidName {
1072 name: self.supervisor_id.to_string(),
1073 })?;
1074
1075 debug!(supervisor_id = %self.supervisor_id, "Supervisor starting.");
1076 self.run_inner(process.clone(), process_shutdown)
1077 .into_process_future(process)
1078 .await
1079 }
1080
1081 pub async fn run_with_shutdown<F: Future + Send + 'static>(&mut self, shutdown: F) -> Result<(), SupervisorError> {
1090 let (shutdown_coordinator, shutdown_handle) = ShutdownHandle::paired();
1094 let run = self.run_with_shutdown_inner(shutdown_handle, None);
1095 pin!(run, shutdown);
1096
1097 let mut shutdown_coordinator = Some(shutdown_coordinator);
1098 loop {
1099 select! {
1100 result = &mut run => return result,
1101 _ = &mut shutdown, if shutdown_coordinator.is_some() => {
1102 shutdown_coordinator.take().expect("coordinator present per select guard").shutdown();
1103 }
1104 }
1105 }
1106 }
1107
1108 pub(crate) async fn run_with_shutdown_inner(
1120 &mut self, process_shutdown: ShutdownHandle, dataspace: Option<DataspaceRegistry>,
1121 ) -> Result<(), SupervisorError> {
1122 let process =
1123 Process::supervisor_with_dataspace(&self.supervisor_id, None, dataspace).context(InvalidName {
1124 name: self.supervisor_id.to_string(),
1125 })?;
1126
1127 debug!(supervisor_id = %self.supervisor_id, "Supervisor starting.");
1128 self.run_inner(process.clone(), process_shutdown)
1129 .into_process_future(process)
1130 .await
1131 }
1132
1133 fn inner_clone(&self) -> Self {
1134 Self {
1138 supervisor_id: Arc::clone(&self.supervisor_id),
1139 child_specs: self.child_specs.clone(),
1140 restart_strategy: self.restart_strategy,
1141 auto_shutdown: self.auto_shutdown,
1142 shutdown_mode: self.shutdown_mode,
1143 runtime_mode: self.runtime_mode.clone(),
1144 current_tx: Arc::clone(&self.current_tx),
1145 id_counter: Arc::clone(&self.id_counter),
1146 active: Arc::clone(&self.active),
1147 }
1148 }
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use std::{
1154 future::pending,
1155 sync::atomic::{AtomicUsize, Ordering},
1156 };
1157
1158 use async_trait::async_trait;
1159 use tokio::{
1160 sync::oneshot,
1161 task::JoinHandle,
1162 time::{sleep, timeout},
1163 };
1164
1165 use super::*;
1166 use crate::test_support::wait_until;
1167
1168 #[derive(Clone)]
1170 enum InitBehavior {
1171 Instant,
1173
1174 Slow(Duration),
1176
1177 Fail(&'static str),
1179 }
1180
1181 #[derive(Clone)]
1183 enum RunBehavior {
1184 UntilShutdown,
1186
1187 FailAfter(Duration, &'static str),
1189
1190 CompleteAfter(Duration),
1192
1193 SlowShutdown(Duration),
1195
1196 IgnoreShutdown,
1198
1199 PanicAfter(Duration),
1201 }
1202
1203 struct MockWorker {
1205 name: &'static str,
1206 init_behavior: InitBehavior,
1207 run_behavior: RunBehavior,
1208 start_count: Arc<AtomicUsize>,
1209 finish_count: Arc<AtomicUsize>,
1210 brutal_shutdown: bool,
1211 graceful_timeout: Duration,
1212 }
1213
1214 impl MockWorker {
1215 fn long_running(name: &'static str) -> Self {
1217 Self {
1218 name,
1219 init_behavior: InitBehavior::Instant,
1220 run_behavior: RunBehavior::UntilShutdown,
1221 start_count: Arc::new(AtomicUsize::new(0)),
1222 finish_count: Arc::new(AtomicUsize::new(0)),
1223 brutal_shutdown: false,
1224 graceful_timeout: Duration::from_millis(500),
1225 }
1226 }
1227
1228 fn failing(name: &'static str, delay: Duration) -> Self {
1230 Self {
1231 name,
1232 init_behavior: InitBehavior::Instant,
1233 run_behavior: RunBehavior::FailAfter(delay, "worker failed"),
1234 start_count: Arc::new(AtomicUsize::new(0)),
1235 finish_count: Arc::new(AtomicUsize::new(0)),
1236 brutal_shutdown: false,
1237 graceful_timeout: Duration::from_millis(500),
1238 }
1239 }
1240
1241 fn completing(name: &'static str, delay: Duration) -> Self {
1243 Self {
1244 name,
1245 init_behavior: InitBehavior::Instant,
1246 run_behavior: RunBehavior::CompleteAfter(delay),
1247 start_count: Arc::new(AtomicUsize::new(0)),
1248 finish_count: Arc::new(AtomicUsize::new(0)),
1249 brutal_shutdown: false,
1250 graceful_timeout: Duration::from_millis(500),
1251 }
1252 }
1253
1254 fn slow_shutdown(name: &'static str, delay: Duration) -> Self {
1256 Self {
1257 name,
1258 init_behavior: InitBehavior::Instant,
1259 run_behavior: RunBehavior::SlowShutdown(delay),
1260 start_count: Arc::new(AtomicUsize::new(0)),
1261 finish_count: Arc::new(AtomicUsize::new(0)),
1262 brutal_shutdown: false,
1263 graceful_timeout: Duration::from_millis(500),
1264 }
1265 }
1266
1267 fn ignore_shutdown(name: &'static str) -> Self {
1269 Self {
1270 name,
1271 init_behavior: InitBehavior::Instant,
1272 run_behavior: RunBehavior::IgnoreShutdown,
1273 start_count: Arc::new(AtomicUsize::new(0)),
1274 finish_count: Arc::new(AtomicUsize::new(0)),
1275 brutal_shutdown: false,
1276 graceful_timeout: Duration::from_millis(500),
1277 }
1278 }
1279
1280 fn panicking(name: &'static str, delay: Duration) -> Self {
1282 Self {
1283 name,
1284 init_behavior: InitBehavior::Instant,
1285 run_behavior: RunBehavior::PanicAfter(delay),
1286 start_count: Arc::new(AtomicUsize::new(0)),
1287 finish_count: Arc::new(AtomicUsize::new(0)),
1288 brutal_shutdown: false,
1289 graceful_timeout: Duration::from_millis(500),
1290 }
1291 }
1292
1293 fn init_failure(name: &'static str) -> Self {
1295 Self {
1296 name,
1297 init_behavior: InitBehavior::Fail("init failed"),
1298 run_behavior: RunBehavior::UntilShutdown,
1299 start_count: Arc::new(AtomicUsize::new(0)),
1300 finish_count: Arc::new(AtomicUsize::new(0)),
1301 brutal_shutdown: false,
1302 graceful_timeout: Duration::from_millis(500),
1303 }
1304 }
1305
1306 fn slow_init(name: &'static str, init_delay: Duration) -> Self {
1308 Self {
1309 name,
1310 init_behavior: InitBehavior::Slow(init_delay),
1311 run_behavior: RunBehavior::UntilShutdown,
1312 start_count: Arc::new(AtomicUsize::new(0)),
1313 finish_count: Arc::new(AtomicUsize::new(0)),
1314 brutal_shutdown: false,
1315 graceful_timeout: Duration::from_millis(500),
1316 }
1317 }
1318
1319 fn start_count(&self) -> Arc<AtomicUsize> {
1325 Arc::clone(&self.start_count)
1326 }
1327
1328 fn finish_count(&self) -> Arc<AtomicUsize> {
1335 Arc::clone(&self.finish_count)
1336 }
1337
1338 fn with_brutal_shutdown(mut self) -> Self {
1340 self.brutal_shutdown = true;
1341 self
1342 }
1343
1344 fn with_graceful_timeout(mut self, timeout: Duration) -> Self {
1346 self.graceful_timeout = timeout;
1347 self
1348 }
1349 }
1350
1351 #[async_trait]
1352 impl Supervisable for MockWorker {
1353 fn name(&self) -> &str {
1354 self.name
1355 }
1356
1357 fn shutdown_strategy(&self) -> ShutdownStrategy {
1358 if self.brutal_shutdown {
1359 ShutdownStrategy::Brutal
1360 } else {
1361 ShutdownStrategy::Graceful(self.graceful_timeout)
1362 }
1363 }
1364
1365 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
1366 match &self.init_behavior {
1367 InitBehavior::Instant => {}
1368 InitBehavior::Slow(delay) => {
1369 sleep(*delay).await;
1370 }
1371 InitBehavior::Fail(msg) => {
1372 return Err(InitializationError::Failed {
1373 source: GenericError::msg(*msg),
1374 });
1375 }
1376 }
1377
1378 let start_count = Arc::clone(&self.start_count);
1379 let finish_count = Arc::clone(&self.finish_count);
1380 let run_behavior = self.run_behavior.clone();
1381
1382 Ok(Box::pin(async move {
1383 start_count.fetch_add(1, Ordering::SeqCst);
1384
1385 match run_behavior {
1386 RunBehavior::UntilShutdown => {
1387 process_shutdown.await;
1388 Ok(())
1389 }
1390 RunBehavior::FailAfter(delay, msg) => {
1391 select! {
1392 _ = sleep(delay) => {
1393 finish_count.fetch_add(1, Ordering::SeqCst);
1396 Err(GenericError::msg(msg))
1397 }
1398 _ = process_shutdown => {
1399 Ok(())
1400 }
1401 }
1402 }
1403 RunBehavior::CompleteAfter(delay) => {
1404 select! {
1405 _ = sleep(delay) => {
1406 finish_count.fetch_add(1, Ordering::SeqCst);
1408 Ok(())
1409 }
1410 _ = process_shutdown => Ok(()),
1411 }
1412 }
1413 RunBehavior::SlowShutdown(delay) => {
1414 process_shutdown.await;
1415 sleep(delay).await;
1416 Ok(())
1417 }
1418 RunBehavior::IgnoreShutdown => {
1419 let _hold = process_shutdown;
1421 pending().await
1422 }
1423 RunBehavior::PanicAfter(delay) => {
1424 select! {
1425 _ = sleep(delay) => panic!("worker panicked"),
1426 _ = process_shutdown => Ok(()),
1427 }
1428 }
1429 }
1430 }))
1431 }
1432 }
1433
1434 async fn run_supervisor_with_trigger(
1440 supervisor: Supervisor,
1441 ) -> (oneshot::Sender<()>, JoinHandle<Result<(), SupervisorError>>) {
1442 let sup_handle = supervisor.handle();
1444 let mut supervisor = supervisor;
1445
1446 let (tx, rx) = oneshot::channel();
1447 let handle = tokio::spawn(async move { supervisor.run_with_shutdown(rx).await });
1448
1449 wait_until("supervisor is running", || sup_handle.is_running()).await;
1450 (tx, handle)
1451 }
1452
1453 async fn join_supervisor(handle: JoinHandle<Result<(), SupervisorError>>) -> Result<(), SupervisorError> {
1458 timeout(Duration::from_secs(2), handle)
1459 .await
1460 .expect("supervisor should exit promptly")
1461 .expect("supervisor task should not panic")
1462 }
1463
1464 #[tokio::test]
1467 async fn standalone_supervisor_shuts_down_cleanly() {
1468 let mut sup = Supervisor::new("test-sup").unwrap();
1469 sup.add_worker(MockWorker::long_running("worker1"));
1470 sup.add_worker(MockWorker::long_running("worker2"));
1471
1472 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1473 tx.send(()).unwrap();
1474
1475 let result = join_supervisor(handle).await;
1476 assert!(result.is_ok());
1477 }
1478
1479 #[tokio::test]
1480 async fn nested_supervisor_shuts_down_cleanly() {
1481 let mut child_sup = Supervisor::new("child-sup").unwrap();
1482 child_sup.add_worker(MockWorker::long_running("inner-worker"));
1483
1484 let mut parent_sup = Supervisor::new("parent-sup").unwrap();
1485 parent_sup.add_worker(MockWorker::long_running("outer-worker"));
1486 parent_sup.add_worker(child_sup);
1487
1488 let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
1489 tx.send(()).unwrap();
1490
1491 let result = join_supervisor(handle).await;
1492 assert!(result.is_ok());
1493 }
1494
1495 #[tokio::test]
1496 async fn empty_supervisor_idles_until_shutdown() {
1497 let sup = Supervisor::new("empty-sup").unwrap();
1500
1501 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1502 assert!(!handle.is_finished(), "an empty supervisor must idle rather than exit");
1503
1504 tx.send(()).unwrap();
1505 let result = join_supervisor(handle).await;
1506 assert!(result.is_ok());
1507 }
1508
1509 #[tokio::test]
1512 async fn one_for_one_restarts_only_failed_child() {
1513 let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1514 let failing_count = failing.start_count();
1515
1516 let stable = MockWorker::long_running("stable-worker");
1517 let stable_count = stable.start_count();
1518
1519 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1520 RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1521 );
1522 sup.add_worker(stable);
1523 sup.add_worker(failing);
1524
1525 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1526
1527 wait_until("the failing worker has been restarted", || {
1529 failing_count.load(Ordering::SeqCst) >= 2
1530 })
1531 .await;
1532 let _ = tx.send(());
1533
1534 let result = join_supervisor(handle).await;
1535 assert!(result.is_ok());
1536
1537 assert!(
1539 failing_count.load(Ordering::SeqCst) >= 2,
1540 "failing worker should have been restarted"
1541 );
1542 assert_eq!(
1544 stable_count.load(Ordering::SeqCst),
1545 1,
1546 "stable worker should not have been restarted"
1547 );
1548 }
1549
1550 #[tokio::test]
1551 async fn one_for_all_restarts_all_children() {
1552 let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1553 let failing_count = failing.start_count();
1554
1555 let stable = MockWorker::long_running("stable-worker");
1556 let stable_count = stable.start_count();
1557
1558 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1559 RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1560 );
1561 sup.add_worker(stable);
1562 sup.add_worker(failing);
1563
1564 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1565
1566 wait_until("both workers have been restarted", || {
1568 failing_count.load(Ordering::SeqCst) >= 2 && stable_count.load(Ordering::SeqCst) >= 2
1569 })
1570 .await;
1571 let _ = tx.send(());
1572
1573 let result = join_supervisor(handle).await;
1574 assert!(result.is_ok());
1575
1576 assert!(
1578 failing_count.load(Ordering::SeqCst) >= 2,
1579 "failing worker should have been restarted"
1580 );
1581 assert!(
1582 stable_count.load(Ordering::SeqCst) >= 2,
1583 "stable worker should also have been restarted"
1584 );
1585 }
1586
1587 #[tokio::test]
1588 async fn one_for_all_does_not_restart_temporary_children() {
1589 let failing = MockWorker::failing("failing-worker", Duration::from_millis(50));
1592 let failing_count = failing.start_count();
1593
1594 let temp = MockWorker::long_running("temp-worker");
1595 let temp_count = temp.start_count();
1596
1597 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1598 RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1599 );
1600 sup.add_worker(ChildSpecification::worker(temp).with_restart_type(RestartType::Temporary));
1601 sup.add_worker(failing);
1602
1603 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1604
1605 wait_until("the permanent worker has been restarted", || {
1607 failing_count.load(Ordering::SeqCst) >= 2
1608 })
1609 .await;
1610 let _ = tx.send(());
1611
1612 let result = join_supervisor(handle).await;
1613 assert!(result.is_ok());
1614 assert!(
1615 failing_count.load(Ordering::SeqCst) >= 2,
1616 "permanent worker should have been restarted by one-for-all"
1617 );
1618 assert_eq!(
1619 temp_count.load(Ordering::SeqCst),
1620 1,
1621 "temporary child must not be restarted by a one-for-all group restart"
1622 );
1623 }
1624
1625 #[tokio::test]
1626 async fn one_for_all_restarts_transient_children() {
1627 let transient = MockWorker::completing("transient-worker", Duration::from_millis(30));
1630 let transient_count = transient.start_count();
1631
1632 let failing = MockWorker::failing("failing-worker", Duration::from_millis(80));
1634
1635 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1636 RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1637 );
1638 sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1639 sup.add_worker(failing);
1640
1641 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1642
1643 wait_until("the transient worker has been restarted by the group", || {
1644 transient_count.load(Ordering::SeqCst) >= 2
1645 })
1646 .await;
1647 let _ = tx.send(());
1648
1649 let result = join_supervisor(handle).await;
1650 assert!(result.is_ok());
1651 assert!(
1652 transient_count.load(Ordering::SeqCst) >= 2,
1653 "transient child must be restarted by a one-for-all group restart, even after a clean exit"
1654 );
1655 }
1656
1657 #[tokio::test]
1658 async fn transient_abnormal_exit_triggers_one_for_all() {
1659 let transient = MockWorker::failing("transient-worker", Duration::from_millis(50));
1662 let transient_count = transient.start_count();
1663
1664 let stable = MockWorker::long_running("stable-worker");
1665 let stable_count = stable.start_count();
1666
1667 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1668 RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
1669 );
1670 sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1671 sup.add_worker(stable);
1672
1673 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1674
1675 wait_until("the abnormal exit has restarted both workers", || {
1676 transient_count.load(Ordering::SeqCst) >= 2 && stable_count.load(Ordering::SeqCst) >= 2
1677 })
1678 .await;
1679 let _ = tx.send(());
1680
1681 let result = join_supervisor(handle).await;
1682 assert!(result.is_ok());
1683 assert!(
1684 transient_count.load(Ordering::SeqCst) >= 2,
1685 "transient worker must be restarted after its own abnormal exit"
1686 );
1687 assert!(
1688 stable_count.load(Ordering::SeqCst) >= 2,
1689 "the transient's abnormal exit must trigger a one-for-all that also restarts the sibling"
1690 );
1691 }
1692
1693 #[tokio::test]
1694 async fn restart_limit_exceeded_shuts_down_supervisor() {
1695 let mut sup = Supervisor::new("test-sup")
1696 .unwrap()
1697 .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
1698 sup.add_worker(MockWorker::failing("fast-fail", Duration::ZERO));
1700
1701 let (tx, rx) = oneshot::channel::<()>();
1702 let handle = tokio::spawn(async move { sup.run_with_shutdown(rx).await });
1703
1704 let result = join_supervisor(handle).await;
1705 drop(tx);
1706
1707 assert!(matches!(result, Err(SupervisorError::Shutdown)));
1708 }
1709
1710 #[tokio::test]
1713 async fn temporary_child_is_not_restarted() {
1714 let temp = MockWorker::failing("temp-worker", Duration::from_millis(50));
1716 let temp_started = temp.start_count();
1717 let temp_failed = temp.finish_count();
1718
1719 let stable = MockWorker::long_running("stable-worker");
1720
1721 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1722 RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1723 );
1724 sup.add_worker(stable);
1725 sup.add_worker(ChildSpecification::worker(temp).with_restart_type(RestartType::Temporary));
1726
1727 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1728
1729 wait_until("the temporary worker has failed once", || {
1735 temp_failed.load(Ordering::SeqCst) == 1
1736 })
1737 .await;
1738 let _ = tx.send(());
1739
1740 let result = join_supervisor(handle).await;
1741 assert!(result.is_ok());
1742 assert_eq!(
1743 temp_started.load(Ordering::SeqCst),
1744 1,
1745 "temporary worker must not be restarted after it fails"
1746 );
1747 }
1748
1749 #[tokio::test]
1750 async fn transient_child_is_not_restarted_on_clean_exit() {
1751 let transient = MockWorker::completing("transient-worker", Duration::from_millis(50));
1752 let transient_started = transient.start_count();
1753 let transient_finished = transient.finish_count();
1754
1755 let stable = MockWorker::long_running("stable-worker");
1756
1757 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1758 RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1759 );
1760 sup.add_worker(stable);
1761 sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1762
1763 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1764
1765 wait_until("the transient worker has completed once", || {
1770 transient_finished.load(Ordering::SeqCst) == 1
1771 })
1772 .await;
1773 let _ = tx.send(());
1774
1775 let result = join_supervisor(handle).await;
1776 assert!(result.is_ok());
1777 assert_eq!(
1778 transient_started.load(Ordering::SeqCst),
1779 1,
1780 "transient worker must not be restarted after a clean exit"
1781 );
1782 }
1783
1784 #[tokio::test]
1785 async fn transient_child_is_restarted_on_failure() {
1786 let transient = MockWorker::failing("transient-worker", Duration::from_millis(50));
1787 let transient_count = transient.start_count();
1788
1789 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1790 RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1791 );
1792 sup.add_worker(ChildSpecification::worker(transient).with_restart_type(RestartType::Transient));
1793
1794 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1795
1796 wait_until("the transient worker has been restarted", || {
1797 transient_count.load(Ordering::SeqCst) >= 2
1798 })
1799 .await;
1800 let _ = tx.send(());
1801
1802 let result = join_supervisor(handle).await;
1803 assert!(result.is_ok());
1804 assert!(
1805 transient_count.load(Ordering::SeqCst) >= 2,
1806 "transient worker must be restarted after an abnormal exit"
1807 );
1808 }
1809
1810 #[tokio::test]
1811 async fn permanent_child_is_restarted_on_clean_exit() {
1812 let permanent = MockWorker::completing("permanent-worker", Duration::from_millis(50));
1815 let permanent_count = permanent.start_count();
1816
1817 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
1818 RestartStrategy::one_to_one().with_intensity_and_period(20, Duration::from_secs(10)),
1819 );
1820 sup.add_worker(permanent);
1822
1823 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1824
1825 wait_until("the permanent worker has been restarted", || {
1826 permanent_count.load(Ordering::SeqCst) >= 2
1827 })
1828 .await;
1829 let _ = tx.send(());
1830
1831 let result = join_supervisor(handle).await;
1832 assert!(result.is_ok());
1833 assert!(
1834 permanent_count.load(Ordering::SeqCst) >= 2,
1835 "permanent worker must be restarted even after a clean exit"
1836 );
1837 }
1838
1839 #[tokio::test]
1840 async fn temporary_failures_do_not_consume_restart_intensity() {
1841 let mut sup = Supervisor::new("test-sup")
1845 .unwrap()
1846 .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
1847
1848 let workers = [
1849 MockWorker::failing("temp-0", Duration::from_millis(20)),
1850 MockWorker::failing("temp-1", Duration::from_millis(20)),
1851 MockWorker::failing("temp-2", Duration::from_millis(20)),
1852 MockWorker::failing("temp-3", Duration::from_millis(20)),
1853 MockWorker::failing("temp-4", Duration::from_millis(20)),
1854 ];
1855 let started: Vec<_> = workers.iter().map(|w| w.start_count()).collect();
1856 let failed: Vec<_> = workers.iter().map(|w| w.finish_count()).collect();
1857 for worker in workers {
1858 sup.add_worker(ChildSpecification::worker(worker).with_restart_type(RestartType::Temporary));
1859 }
1860 sup.add_worker(MockWorker::long_running("stable-worker"));
1862
1863 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1864 wait_until("every temporary worker has failed once", || {
1868 failed.iter().all(|c| c.load(Ordering::SeqCst) == 1)
1869 })
1870 .await;
1871 let _ = tx.send(());
1872
1873 let result = join_supervisor(handle).await;
1874 assert!(
1875 result.is_ok(),
1876 "supervisor must not trip its restart limit on temporary exits"
1877 );
1878 for count in started {
1879 assert_eq!(
1880 count.load(Ordering::SeqCst),
1881 1,
1882 "each temporary worker runs exactly once"
1883 );
1884 }
1885 }
1886
1887 #[tokio::test]
1888 async fn transient_clean_exits_do_not_consume_restart_intensity() {
1889 let mut sup = Supervisor::new("test-sup")
1893 .unwrap()
1894 .with_restart_strategy(RestartStrategy::one_to_one().with_intensity_and_period(1, Duration::from_secs(10)));
1895
1896 let workers = [
1897 MockWorker::completing("transient-0", Duration::from_millis(20)),
1898 MockWorker::completing("transient-1", Duration::from_millis(20)),
1899 MockWorker::completing("transient-2", Duration::from_millis(20)),
1900 MockWorker::completing("transient-3", Duration::from_millis(20)),
1901 MockWorker::completing("transient-4", Duration::from_millis(20)),
1902 ];
1903 let started: Vec<_> = workers.iter().map(|w| w.start_count()).collect();
1904 let finished: Vec<_> = workers.iter().map(|w| w.finish_count()).collect();
1905 for worker in workers {
1906 sup.add_worker(ChildSpecification::worker(worker).with_restart_type(RestartType::Transient));
1907 }
1908 sup.add_worker(MockWorker::long_running("stable-worker"));
1910
1911 let (tx, handle) = run_supervisor_with_trigger(sup).await;
1912 wait_until("every transient worker has completed once", || {
1916 finished.iter().all(|c| c.load(Ordering::SeqCst) == 1)
1917 })
1918 .await;
1919 let _ = tx.send(());
1920
1921 let result = join_supervisor(handle).await;
1922 assert!(
1923 result.is_ok(),
1924 "supervisor must not trip its restart limit on clean transient exits"
1925 );
1926 for count in started {
1927 assert_eq!(
1928 count.load(Ordering::SeqCst),
1929 1,
1930 "each transient worker runs exactly once"
1931 );
1932 }
1933 }
1934
1935 #[tokio::test]
1936 async fn supervisor_idles_when_all_temporary_children_exit() {
1937 let temp_a = MockWorker::completing("temp-a", Duration::from_millis(10));
1941 let a_finished = temp_a.finish_count();
1942 let temp_b = MockWorker::completing("temp-b", Duration::from_millis(10));
1943 let b_finished = temp_b.finish_count();
1944
1945 let mut sup = Supervisor::new("test-sup").unwrap();
1946 let handle = sup.handle();
1947 sup.add_worker(ChildSpecification::worker(temp_a).with_restart_type(RestartType::Temporary));
1948 sup.add_worker(ChildSpecification::worker(temp_b).with_restart_type(RestartType::Temporary));
1949
1950 let (tx, run) = run_supervisor_with_trigger(sup).await;
1951
1952 wait_until("both temporary children have completed", || {
1956 a_finished.load(Ordering::SeqCst) == 1 && b_finished.load(Ordering::SeqCst) == 1
1957 })
1958 .await;
1959
1960 let dynamic = MockWorker::long_running("late-comer");
1963 let dynamic_count = dynamic.start_count();
1964 handle
1965 .spawn(dynamic)
1966 .await
1967 .expect("supervisor must still accept work after its children drain");
1968 wait_until("the late dynamic child has started", || {
1969 dynamic_count.load(Ordering::SeqCst) == 1
1970 })
1971 .await;
1972 assert!(
1973 handle.is_running(),
1974 "supervisor must keep running after all temporary children exit"
1975 );
1976
1977 tx.send(()).unwrap();
1978 let result = join_supervisor(run).await;
1979 assert!(result.is_ok());
1980 }
1981
1982 #[tokio::test]
1985 async fn significant_child_drives_auto_shutdown() {
1986 let mut sup = Supervisor::new("test-sup")
1989 .unwrap()
1990 .with_auto_shutdown(AutoShutdown::AnySignificant);
1991 sup.add_worker(MockWorker::long_running("stable"));
1992 sup.add_worker(
1993 ChildSpecification::worker(MockWorker::completing("significant", Duration::from_millis(50)))
1994 .with_restart_type(RestartType::Temporary)
1995 .with_significant(true),
1996 );
1997
1998 let (_tx, rx) = oneshot::channel::<()>();
2000 let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2001 .await
2002 .unwrap();
2003 assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
2004 }
2005
2006 #[tokio::test]
2007 async fn non_significant_exit_does_not_auto_shutdown() {
2008 let plain = MockWorker::completing("plain", Duration::from_millis(10));
2010 let plain_finished = plain.finish_count();
2011
2012 let mut sup = Supervisor::new("test-sup")
2013 .unwrap()
2014 .with_auto_shutdown(AutoShutdown::AnySignificant);
2015 let handle = sup.handle();
2016 sup.add_worker(MockWorker::long_running("stable"));
2017 sup.add_worker(ChildSpecification::worker(plain).with_restart_type(RestartType::Temporary));
2018
2019 let (tx, run) = run_supervisor_with_trigger(sup).await;
2020
2021 wait_until("the non-significant child has completed", || {
2025 plain_finished.load(Ordering::SeqCst) == 1
2026 })
2027 .await;
2028
2029 let dynamic = MockWorker::long_running("late-comer");
2033 let dynamic_count = dynamic.start_count();
2034 handle
2035 .spawn(dynamic)
2036 .await
2037 .expect("supervisor must still accept work after a non-significant child exits");
2038 wait_until("the late dynamic child has started", || {
2039 dynamic_count.load(Ordering::SeqCst) == 1
2040 })
2041 .await;
2042 assert!(
2043 handle.is_running(),
2044 "a non-significant child exiting must not trigger auto-shutdown"
2045 );
2046
2047 tx.send(()).unwrap();
2048 let result = join_supervisor(run).await;
2049 assert!(result.is_ok());
2050 }
2051
2052 #[tokio::test]
2053 async fn all_significant_waits_for_last() {
2054 let mut sup = Supervisor::new("test-sup")
2056 .unwrap()
2057 .with_auto_shutdown(AutoShutdown::AllSignificant);
2058 sup.add_worker(
2059 ChildSpecification::worker(MockWorker::completing("sig-a", Duration::from_millis(50)))
2060 .with_restart_type(RestartType::Temporary)
2061 .with_significant(true),
2062 );
2063 sup.add_worker(
2064 ChildSpecification::worker(MockWorker::completing("sig-b", Duration::from_millis(250)))
2065 .with_restart_type(RestartType::Temporary)
2066 .with_significant(true),
2067 );
2068
2069 let (_tx, rx) = oneshot::channel::<()>();
2070 let start = std::time::Instant::now();
2071 let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2072 .await
2073 .unwrap();
2074 let elapsed = start.elapsed();
2075
2076 assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
2077 assert!(
2079 elapsed >= Duration::from_millis(200),
2080 "auto-shutdown must wait for all significant children (took {elapsed:?})"
2081 );
2082 }
2083
2084 #[tokio::test]
2087 async fn init_failure_propagates_with_child_name() {
2088 let mut sup = Supervisor::new("test-sup").unwrap();
2089 sup.add_worker(MockWorker::long_running("good-worker"));
2090 sup.add_worker(MockWorker::init_failure("bad-worker"));
2091
2092 let (_tx, rx) = oneshot::channel::<()>();
2093 let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2094 .await
2095 .unwrap();
2096
2097 match result {
2098 Err(SupervisorError::FailedToInitialize { child_name, .. }) => {
2099 assert_eq!(child_name, "bad-worker");
2100 }
2101 other => panic!("expected FailedToInitialize, got: {:?}", other),
2102 }
2103 }
2104
2105 #[tokio::test]
2106 async fn init_failure_does_not_trigger_restart() {
2107 let init_fail = MockWorker::init_failure("bad-worker");
2108 let start_count = init_fail.start_count();
2109
2110 let mut sup = Supervisor::new("test-sup").unwrap().with_restart_strategy(
2111 RestartStrategy::one_to_one().with_intensity_and_period(10, Duration::from_secs(10)),
2112 );
2113 sup.add_worker(init_fail);
2114
2115 let (_tx, rx) = oneshot::channel::<()>();
2116 let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2117 .await
2118 .unwrap();
2119
2120 assert!(matches!(result, Err(SupervisorError::FailedToInitialize { .. })));
2121 assert_eq!(start_count.load(Ordering::SeqCst), 0);
2123 }
2124
2125 #[tokio::test]
2128 async fn shutdown_completes_promptly_in_steady_state() {
2129 let mut sup = Supervisor::new("test-sup").unwrap();
2130 sup.add_worker(MockWorker::long_running("worker1"));
2131 sup.add_worker(MockWorker::long_running("worker2"));
2132
2133 let (tx, handle) = run_supervisor_with_trigger(sup).await;
2134 tx.send(()).unwrap();
2135
2136 let result = timeout(Duration::from_secs(1), handle).await;
2138 assert!(result.is_ok(), "shutdown should complete promptly");
2139 }
2140
2141 #[tokio::test]
2142 async fn shutdown_during_slow_init_completes_promptly() {
2143 let mut sup = Supervisor::new("test-sup").unwrap();
2144 sup.add_worker(MockWorker::slow_init("slow-worker", Duration::from_secs(30)));
2146
2147 let (tx, rx) = oneshot::channel();
2148 let handle = tokio::spawn(async move { sup.run_with_shutdown(rx).await });
2149
2150 sleep(Duration::from_millis(20)).await;
2152 tx.send(()).unwrap();
2153
2154 let result = timeout(Duration::from_secs(2), handle).await;
2157 assert!(result.is_ok(), "shutdown during slow init should complete promptly");
2158 }
2159
2160 #[tokio::test]
2163 async fn dynamic_children_spawn_after_start() {
2164 let sup = Supervisor::new("dyn-sup").unwrap();
2165 let handle = sup.handle();
2166 let (tx, run) = run_supervisor_with_trigger(sup).await;
2167 wait_until("supervisor is running", || handle.is_running()).await;
2168
2169 let c1 = MockWorker::long_running("c1");
2170 let c2 = MockWorker::long_running("c2");
2171 let c1_count = c1.start_count();
2172 let c2_count = c2.start_count();
2173 handle.spawn(c1).await.unwrap();
2174 handle.spawn(c2).await.unwrap();
2175
2176 wait_until("both dynamic children have started", || {
2177 c1_count.load(Ordering::SeqCst) == 1 && c2_count.load(Ordering::SeqCst) == 1
2178 })
2179 .await;
2180 assert_eq!(handle.active_children(), 2);
2181
2182 tx.send(()).unwrap();
2183 let result = join_supervisor(run).await;
2184 assert!(result.is_ok());
2185 assert_eq!(
2186 handle.active_children(),
2187 0,
2188 "all dynamic children must be drained on shutdown"
2189 );
2190 }
2191
2192 #[tokio::test]
2193 async fn temporary_dynamic_child_failure_is_isolated() {
2194 let sup = Supervisor::new("dyn-sup").unwrap();
2197 let handle = sup.handle();
2198 let (tx, run) = run_supervisor_with_trigger(sup).await;
2199 wait_until("supervisor is running", || handle.is_running()).await;
2200
2201 let failing = MockWorker::failing("boom", Duration::from_millis(20));
2202 let failing_count = failing.start_count();
2203 handle.spawn(failing).await.unwrap();
2204 wait_until("the failing dynamic child has run once", || {
2205 failing_count.load(Ordering::SeqCst) == 1
2206 })
2207 .await;
2208 wait_until("all dynamic children have drained", || handle.active_children() == 0).await;
2209
2210 sleep(Duration::from_millis(50)).await;
2211 assert!(
2212 handle.is_running(),
2213 "supervisor stays up after an isolated child failure"
2214 );
2215 assert_eq!(
2216 failing_count.load(Ordering::SeqCst),
2217 1,
2218 "a temporary child is never restarted"
2219 );
2220
2221 handle.spawn(MockWorker::long_running("c2")).await.unwrap();
2223 wait_until("one dynamic child is running", || handle.active_children() == 1).await;
2224
2225 tx.send(()).unwrap();
2226 let result = join_supervisor(run).await;
2227 assert!(result.is_ok());
2228 }
2229
2230 #[tokio::test]
2231 async fn temporary_dynamic_child_panic_is_isolated() {
2232 let sup = Supervisor::new("dyn-sup").unwrap();
2234 let handle = sup.handle();
2235 let (tx, run) = run_supervisor_with_trigger(sup).await;
2236 wait_until("supervisor is running", || handle.is_running()).await;
2237
2238 handle
2239 .spawn(MockWorker::panicking("boom", Duration::from_millis(20)))
2240 .await
2241 .unwrap();
2242 wait_until("all dynamic children have drained", || handle.active_children() == 0).await;
2243
2244 sleep(Duration::from_millis(50)).await;
2245 assert!(handle.is_running(), "supervisor stays up after an isolated child panic");
2246
2247 tx.send(()).unwrap();
2248 let result = join_supervisor(run).await;
2249 assert!(result.is_ok());
2250 }
2251
2252 #[tokio::test]
2253 async fn significant_dynamic_child_failure_shuts_down_supervisor() {
2254 let sup = Supervisor::new("dyn-sup")
2257 .unwrap()
2258 .with_auto_shutdown(AutoShutdown::AnySignificant);
2259 let handle = sup.handle();
2260 let (_tx, run) = run_supervisor_with_trigger(sup).await;
2261 wait_until("supervisor is running", || handle.is_running()).await;
2262
2263 handle
2264 .spawn_with(
2265 ChildSpecification::worker(MockWorker::failing("boom", Duration::from_millis(20)))
2266 .with_restart_type(RestartType::Temporary)
2267 .with_significant(true),
2268 )
2269 .await
2270 .unwrap();
2271
2272 let result = join_supervisor(run).await;
2273 assert!(matches!(result, Err(SupervisorError::SignificantChildExited)));
2274 }
2275
2276 #[tokio::test]
2277 async fn dynamic_spawn_fails_before_start_and_after_shutdown() {
2278 let sup = Supervisor::new("dyn-sup").unwrap();
2279 let handle = sup.handle();
2280
2281 assert!(!handle.is_running());
2284 let err = handle
2285 .spawn(MockWorker::long_running("before-start"))
2286 .await
2287 .unwrap_err();
2288 assert!(matches!(err, SpawnError::SupervisorGone));
2289
2290 let (tx, run) = run_supervisor_with_trigger(sup).await;
2292 wait_until("supervisor is running", || handle.is_running()).await;
2293 let worker = MockWorker::long_running("after-start");
2294 let started = worker.start_count();
2295 handle.spawn(worker).await.unwrap();
2296 wait_until("the dynamic child has started", || started.load(Ordering::SeqCst) == 1).await;
2297
2298 tx.send(()).unwrap();
2299 let result = join_supervisor(run).await;
2300 assert!(result.is_ok());
2301
2302 wait_until("the supervisor has stopped", || !handle.is_running()).await;
2304 let err = handle
2305 .spawn(MockWorker::long_running("after-shutdown"))
2306 .await
2307 .unwrap_err();
2308 assert!(matches!(err, SpawnError::SupervisorGone));
2309 }
2310
2311 #[tokio::test]
2312 async fn dynamic_spawn_returns_after_registration() {
2313 let sup = Supervisor::new("dyn-sup").unwrap();
2314 let handle = sup.handle();
2315 let (tx, run) = run_supervisor_with_trigger(sup).await;
2316 wait_until("supervisor is running", || handle.is_running()).await;
2317
2318 let worker = MockWorker::long_running("c");
2319 let started = worker.start_count();
2320 let id = handle.spawn(worker).await.unwrap();
2321 assert_eq!(id.as_u64(), 0);
2323 wait_until("the dynamic child has started", || started.load(Ordering::SeqCst) == 1).await;
2324
2325 tx.send(()).unwrap();
2326 let result = join_supervisor(run).await;
2327 assert!(result.is_ok());
2328 }
2329
2330 #[tokio::test]
2331 async fn dynamic_spawn_rejects_invalid_child_name() {
2332 let sup = Supervisor::new("dyn-sup").unwrap();
2335 let handle = sup.handle();
2336 let (tx, run) = run_supervisor_with_trigger(sup).await;
2337 wait_until("supervisor is running", || handle.is_running()).await;
2338
2339 let err = handle.spawn(MockWorker::long_running("")).await.unwrap_err();
2340 assert!(matches!(err, SpawnError::Rejected { .. }), "got {err:?}");
2341
2342 assert!(handle.is_running());
2344 handle.spawn(MockWorker::long_running("ok")).await.unwrap();
2345 wait_until("one dynamic child is running", || handle.active_children() == 1).await;
2346
2347 tx.send(()).unwrap();
2348 let result = join_supervisor(run).await;
2349 assert!(result.is_ok());
2350 }
2351
2352 #[tokio::test]
2353 async fn concurrent_shutdown_drains_many_children_quickly() {
2354 const CHILDREN: usize = 500;
2355 const SHUTDOWN_DELAY: Duration = Duration::from_millis(50);
2356
2357 let sup = Supervisor::new("dyn-sup")
2358 .unwrap()
2359 .with_shutdown_mode(ShutdownMode::Concurrent);
2360 let handle = sup.handle();
2361 let (tx, run) = run_supervisor_with_trigger(sup).await;
2362 wait_until("supervisor is running", || handle.is_running()).await;
2363
2364 for _ in 0..CHILDREN {
2365 handle
2366 .spawn(MockWorker::slow_shutdown("conn", SHUTDOWN_DELAY))
2367 .await
2368 .unwrap();
2369 }
2370 wait_until("all dynamic children are running", || {
2371 handle.active_children() == CHILDREN
2372 })
2373 .await;
2374
2375 let start = std::time::Instant::now();
2378 tx.send(()).unwrap();
2379 let result = timeout(Duration::from_secs(5), run).await.unwrap().unwrap();
2380 let elapsed = start.elapsed();
2381
2382 assert!(result.is_ok());
2383 assert_eq!(handle.active_children(), 0, "active count must return to zero");
2384 assert!(
2385 elapsed < Duration::from_secs(2),
2386 "shutdown must be concurrent (took {elapsed:?})"
2387 );
2388 }
2389
2390 #[tokio::test]
2391 async fn concurrent_shutdown_aborts_unresponsive_children() {
2392 let sup = Supervisor::new("dyn-sup")
2393 .unwrap()
2394 .with_shutdown_mode(ShutdownMode::Concurrent);
2395 let handle = sup.handle();
2396 let (tx, run) = run_supervisor_with_trigger(sup).await;
2397 wait_until("supervisor is running", || handle.is_running()).await;
2398
2399 handle.spawn(MockWorker::ignore_shutdown("stuck")).await.unwrap();
2400 wait_until("one dynamic child is running", || handle.active_children() == 1).await;
2401
2402 let start = std::time::Instant::now();
2405 tx.send(()).unwrap();
2406 let result = join_supervisor(run).await;
2407 let elapsed = start.elapsed();
2408
2409 assert!(
2411 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2412 "aborting a stuck child must surface as an unclean shutdown, got {result:?}"
2413 );
2414 assert_eq!(handle.active_children(), 0);
2415 assert!(
2416 elapsed < Duration::from_secs(1),
2417 "stuck child must be aborted at the deadline (took {elapsed:?})"
2418 );
2419 }
2420
2421 #[tokio::test]
2422 async fn concurrent_shutdown_honors_per_child_deadline() {
2423 let sup = Supervisor::new("dyn-sup")
2428 .unwrap()
2429 .with_shutdown_mode(ShutdownMode::Concurrent);
2430 let handle = sup.handle();
2431 let (tx, run) = run_supervisor_with_trigger(sup).await;
2432 wait_until("supervisor is running", || handle.is_running()).await;
2433
2434 handle
2436 .spawn(MockWorker::long_running("responsive").with_graceful_timeout(Duration::MAX))
2437 .await
2438 .unwrap();
2439 handle
2441 .spawn(MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(200)))
2442 .await
2443 .unwrap();
2444 wait_until("both dynamic children are running", || handle.active_children() == 2).await;
2445
2446 let start = std::time::Instant::now();
2447 tx.send(()).unwrap();
2448 let result = join_supervisor(run).await;
2449 let elapsed = start.elapsed();
2450
2451 assert!(
2453 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2454 "aborting the stuck child must surface as an unclean shutdown with a count of 1, got {result:?}"
2455 );
2456 assert_eq!(handle.active_children(), 0);
2457 assert!(
2458 elapsed < Duration::from_secs(1),
2459 "stuck child must be aborted at its own deadline despite an infinite-timeout sibling (took {elapsed:?})"
2460 );
2461 }
2462
2463 #[tokio::test]
2464 async fn ordered_shutdown_aborts_unresponsive_child() {
2465 let mut sup = Supervisor::new("test-sup").unwrap();
2468 sup.add_worker(MockWorker::ignore_shutdown("stuck"));
2469
2470 let (tx, handle) = run_supervisor_with_trigger(sup).await;
2471
2472 let start = std::time::Instant::now();
2473 tx.send(()).unwrap();
2474 let result = join_supervisor(handle).await;
2475 let elapsed = start.elapsed();
2476
2477 assert!(
2478 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2479 "aborting a stuck child under ordered shutdown must surface as an unclean shutdown, got {result:?}"
2480 );
2481 assert!(
2482 elapsed < Duration::from_secs(1),
2483 "unresponsive child must be aborted at its deadline under ordered shutdown (took {elapsed:?})"
2484 );
2485 }
2486
2487 #[tokio::test]
2488 async fn brutal_shutdown_aborts_child_immediately() {
2489 let mut sup = Supervisor::new("test-sup").unwrap();
2492 sup.add_worker(MockWorker::ignore_shutdown("brutal-stuck").with_brutal_shutdown());
2493
2494 let (tx, handle) = run_supervisor_with_trigger(sup).await;
2495
2496 let start = std::time::Instant::now();
2497 tx.send(()).unwrap();
2498 let result = join_supervisor(handle).await;
2499 let elapsed = start.elapsed();
2500
2501 assert!(result.is_ok());
2504 assert!(
2505 elapsed < Duration::from_millis(200),
2506 "brutal-shutdown child must be aborted immediately, not after a graceful wait (took {elapsed:?})"
2507 );
2508 }
2509
2510 #[tokio::test]
2511 async fn shutdown_timeout_aborts_aggregate_to_root() {
2512 let mut child_sup = Supervisor::new("child-sup")
2516 .unwrap()
2517 .with_shutdown_mode(ShutdownMode::Concurrent);
2518 child_sup
2519 .add_worker(MockWorker::ignore_shutdown("child-stuck").with_graceful_timeout(Duration::from_millis(200)));
2520
2521 let mut parent_sup = Supervisor::new("parent-sup")
2522 .unwrap()
2523 .with_shutdown_mode(ShutdownMode::Concurrent);
2524 parent_sup
2525 .add_worker(MockWorker::ignore_shutdown("parent-stuck").with_graceful_timeout(Duration::from_millis(200)));
2526 parent_sup.add_worker(MockWorker::long_running("parent-clean"));
2527 parent_sup.add_worker(child_sup);
2528
2529 let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
2530 tx.send(()).unwrap();
2531
2532 let result = join_supervisor(handle).await;
2533 assert!(
2534 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 2 })),
2535 "forced aborts must aggregate across the tree (1 direct + 1 nested), got {result:?}"
2536 );
2537 }
2538
2539 #[tokio::test]
2542 async fn restart_intensity_zero_shuts_down_on_first_failure() {
2543 let worker = MockWorker::failing("boom", Duration::from_millis(20));
2547 let start_count = worker.start_count();
2548
2549 let mut sup = Supervisor::new("test-sup")
2550 .unwrap()
2551 .with_restart_strategy(RestartStrategy::new(RestartMode::OneForOne, 0, Duration::from_secs(5)));
2552 sup.add_worker(worker);
2553
2554 let (_tx, rx) = oneshot::channel::<()>();
2555 let result = timeout(Duration::from_secs(2), sup.run_with_shutdown(rx))
2556 .await
2557 .unwrap();
2558
2559 assert!(matches!(result, Err(SupervisorError::Shutdown)));
2560 assert_eq!(
2561 start_count.load(Ordering::SeqCst),
2562 1,
2563 "with intensity zero the worker must run exactly once and never be restarted"
2564 );
2565 }
2566
2567 #[tokio::test]
2568 async fn one_for_all_restart_loses_dynamic_children() {
2569 let failing = MockWorker::failing("failing-static", Duration::from_millis(50));
2574 let failing_count = failing.start_count();
2575
2576 let sup = Supervisor::new("dyn-sup").unwrap().with_restart_strategy(
2577 RestartStrategy::one_for_all().with_intensity_and_period(20, Duration::from_secs(10)),
2578 );
2579 let handle = sup.handle();
2580 let mut sup = sup;
2581 sup.add_worker(failing);
2582
2583 let (tx, run) = run_supervisor_with_trigger(sup).await;
2584
2585 let dynamic = MockWorker::long_running("dynamic");
2587 let dynamic_count = dynamic.start_count();
2588 handle.spawn(dynamic).await.expect("should spawn dynamic child");
2589 wait_until("the dynamic child is running", || handle.active_children() == 1).await;
2590
2591 wait_until("the static worker has been restarted", || {
2593 failing_count.load(Ordering::SeqCst) >= 2
2594 })
2595 .await;
2596
2597 wait_until("the dynamic child has been discarded", || handle.active_children() == 0).await;
2600 assert_eq!(
2601 dynamic_count.load(Ordering::SeqCst),
2602 1,
2603 "a dynamic child must be lost -- not restored -- across a one-for-all restart"
2604 );
2605
2606 tx.send(()).unwrap();
2607 let result = join_supervisor(run).await;
2608 assert!(result.is_ok());
2609 }
2610
2611 #[tokio::test]
2614 async fn dedicated_single_threaded_runtime_runs_nested_worker_and_shuts_down_cleanly() {
2615 let worker = MockWorker::long_running("dedicated-worker");
2619 let worker_count = worker.start_count();
2620
2621 let mut child_sup = Supervisor::new("child-sup")
2622 .unwrap()
2623 .with_dedicated_runtime(RuntimeConfiguration::single_threaded());
2624 child_sup.add_worker(worker);
2625
2626 let mut parent_sup = Supervisor::new("parent-sup").unwrap();
2627 parent_sup.add_worker(child_sup);
2628
2629 let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
2630
2631 wait_until("the dedicated worker has started", || {
2633 worker_count.load(Ordering::SeqCst) == 1
2634 })
2635 .await;
2636
2637 tx.send(()).unwrap();
2638 let result = join_supervisor(handle).await;
2639 assert!(
2640 result.is_ok(),
2641 "dedicated-runtime supervisor should shut down cleanly, got {result:?}"
2642 );
2643 }
2644
2645 #[tokio::test]
2646 async fn dedicated_multi_threaded_runtime_runs_nested_worker() {
2647 let worker = MockWorker::long_running("dedicated-worker");
2649 let worker_count = worker.start_count();
2650
2651 let mut child_sup = Supervisor::new("child-sup")
2652 .unwrap()
2653 .with_dedicated_runtime(RuntimeConfiguration::multi_threaded(2));
2654 child_sup.add_worker(worker);
2655
2656 let mut parent_sup = Supervisor::new("parent-sup").unwrap();
2657 parent_sup.add_worker(child_sup);
2658
2659 let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
2660 wait_until("the dedicated worker has started", || {
2661 worker_count.load(Ordering::SeqCst) == 1
2662 })
2663 .await;
2664
2665 tx.send(()).unwrap();
2666 let result = join_supervisor(handle).await;
2667 assert!(
2668 result.is_ok(),
2669 "multi-threaded dedicated-runtime supervisor should shut down cleanly, got {result:?}"
2670 );
2671 }
2672
2673 #[tokio::test]
2674 async fn dedicated_runtime_forced_abort_aggregates_to_root() {
2675 let stuck = MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(200));
2679 let stuck_count = stuck.start_count();
2680
2681 let mut child_sup = Supervisor::new("child-sup")
2682 .unwrap()
2683 .with_dedicated_runtime(RuntimeConfiguration::single_threaded())
2684 .with_shutdown_mode(ShutdownMode::Concurrent);
2685 child_sup.add_worker(stuck);
2686
2687 let mut parent_sup = Supervisor::new("parent-sup").unwrap();
2688 parent_sup.add_worker(child_sup);
2689
2690 let (tx, handle) = run_supervisor_with_trigger(parent_sup).await;
2691
2692 wait_until("the stuck worker has started", || {
2695 stuck_count.load(Ordering::SeqCst) == 1
2696 })
2697 .await;
2698
2699 tx.send(()).unwrap();
2700 let result = join_supervisor(handle).await;
2701 assert!(
2702 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
2703 "a stuck worker in a dedicated runtime must surface as an unclean shutdown aggregated to the root, got {result:?}"
2704 );
2705 }
2706}