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
62impl From<SupervisorError> for WorkerError {
63 fn from(err: SupervisorError) -> Self {
64 match err {
65 SupervisorError::FailedToInitialize { child_name, source } => WorkerError::Initialization {
68 child_name: Some(child_name),
69 source,
70 },
71 other => WorkerError::Runtime(other.into()),
73 }
74 }
75}
76
77#[derive(Debug, Snafu)]
79pub enum ProcessError {
80 #[snafu(display("Child process was aborted by the supervisor."))]
82 Aborted,
83
84 #[snafu(display("Child process panicked."))]
86 Panicked,
87
88 #[snafu(display("Child process terminated with an error: {}", source))]
90 Terminated {
91 source: GenericError,
93 },
94}
95
96#[derive(Debug, Snafu)]
102#[snafu(context(suffix(false)))]
103pub enum InitializationError {
104 #[snafu(display("Process failed to initialize: {}", source))]
106 Failed {
107 source: GenericError,
109 },
110}
111
112impl From<GenericError> for InitializationError {
113 fn from(source: GenericError) -> Self {
114 Self::Failed { source }
115 }
116}
117
118pub enum ShutdownStrategy {
120 Graceful(Duration),
122
123 Brutal,
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
134pub enum AutoShutdown {
135 #[default]
137 Never,
138
139 AnySignificant,
141
142 AllSignificant,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
148pub enum ShutdownMode {
149 #[default]
154 Ordered,
155
156 Concurrent,
161}
162
163#[async_trait]
165pub trait Supervisable: Send + Sync {
166 fn name(&self) -> &str;
168
169 fn shutdown_strategy(&self) -> ShutdownStrategy {
171 ShutdownStrategy::Graceful(Duration::from_secs(5))
172 }
173
174 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError>;
188}
189
190#[derive(Debug, Snafu)]
192#[snafu(context(suffix(false)))]
193pub enum SupervisorError {
194 #[snafu(display("Invalid name for supervisor or worker: '{}'", name))]
196 InvalidName {
197 name: String,
199 },
200
201 #[snafu(display("Child process '{}' failed to initialize: {}", child_name, source))]
206 FailedToInitialize {
207 child_name: String,
209
210 source: InitializationError,
212 },
213
214 #[snafu(display("Supervisor has exceeded restart limits and was forced to shutdown."))]
216 Shutdown,
217
218 #[snafu(display("Supervisor shut down after a significant child terminated."))]
223 SignificantChildExited,
224}
225
226pub struct ChildSpecification<S = WorkerSpec> {
242 spec_inner: S,
243}
244
245pub struct WorkerSpec {
247 worker: Arc<dyn Supervisable>,
248 config: ChildConfig,
249}
250
251pub struct SupervisorSpec {
253 supervisor: Supervisor,
254}
255
256impl ChildSpecification<WorkerSpec> {
257 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 #[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 #[must_use]
282 pub fn with_significant(mut self, significant: bool) -> Self {
283 self.spec_inner.config.significant = significant;
284 self
285 }
286
287 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
317pub 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
349pub(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 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 Ok(sup.as_nested_process(process, process_shutdown))
416 }
417 RuntimeMode::Dedicated(config) => {
418 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#[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#[derive(Clone)]
467struct ChildEntry {
468 spec: SupervisedChild,
469 config: ChildConfig,
470 dynamic: bool,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
480pub struct ChildId(u64);
481
482impl ChildId {
483 pub const fn as_u64(self) -> u64 {
485 self.0
486 }
487}
488
489#[derive(Debug, Snafu)]
491pub enum SpawnError {
492 #[snafu(display("supervisor is gone"))]
498 SupervisorGone,
499
500 #[snafu(display("supervisor rejected the spawn: {}", source))]
505 Rejected {
506 source: GenericError,
508 },
509}
510
511struct PendingSpawn {
513 id: u64,
514 spec: SupervisedChild,
515 config: ChildConfig,
516 ack: oneshot::Sender<Result<(), SpawnError>>,
517}
518
519const DYNAMIC_SPAWN_CHANNEL_CAPACITY: usize = 1024;
524
525#[derive(Clone)]
531pub struct SupervisorHandle {
532 name: Arc<str>,
533 current_tx: Arc<Mutex<Option<mpsc::Sender<PendingSpawn>>>>,
536 id_counter: Arc<AtomicU64>,
537 active: Arc<AtomicUsize>,
538}
539
540impl SupervisorHandle {
541 pub fn name(&self) -> &str {
543 &self.name
544 }
545
546 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 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 ack_rx
591 .await
592 .map_err(|_| SpawnError::SupervisorGone)?
593 .map(|()| ChildId(id))
594 }
595
596 pub fn is_running(&self) -> bool {
598 self.current_tx.lock().unwrap().is_some()
599 }
600
601 pub fn active_children(&self) -> usize {
603 self.active.load(Ordering::Relaxed)
604 }
605
606 async fn send(&self, spawn: PendingSpawn) -> Result<(), SpawnError> {
610 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
619pub 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 current_tx: Arc<Mutex<Option<mpsc::Sender<PendingSpawn>>>>,
656 id_counter: Arc<AtomicU64>,
657 active: Arc<AtomicUsize>,
659}
660
661impl Supervisor {
662 pub fn new<S: AsRef<str>>(supervisor_id: S) -> Result<Self, SupervisorError> {
664 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 pub fn id(&self) -> &str {
688 &self.supervisor_id
689 }
690
691 pub fn with_restart_strategy(mut self, strategy: RestartStrategy) -> Self {
693 self.restart_strategy = strategy;
694 self
695 }
696
697 pub fn with_auto_shutdown(mut self, auto_shutdown: AutoShutdown) -> Self {
702 self.auto_shutdown = auto_shutdown;
703 self
704 }
705
706 pub fn with_shutdown_mode(mut self, mode: ShutdownMode) -> Self {
708 self.shutdown_mode = mode;
709 self
710 }
711
712 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 pub fn with_dedicated_runtime(mut self, config: RuntimeConfiguration) -> Self {
736 self.runtime_mode = RuntimeMode::Dedicated(config);
737 self
738 }
739
740 pub(crate) fn runtime_mode(&self) -> &RuntimeMode {
742 &self.runtime_mode
743 }
744
745 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 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 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 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 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 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 *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 let mut children: FastHashMap<u64, ChildEntry> = FastHashMap::default();
863
864 self.spawn_static_children(&mut children, &mut worker_state)?;
867
868 let mut significant_remaining = children.values().filter(|entry| entry.config.significant).count();
870
871 pin!(process_shutdown);
873
874 let outcome = loop {
875 select! {
876 biased;
878
879 _ = &mut process_shutdown => break Ok(()),
883
884 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 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 if let Err(WorkerError::Initialization { child_name: inner, source }) = worker_result {
901 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 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 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 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 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 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 debug!(supervisor_id = %self.supervisor_id, "Nested supervisor starting.");
1001
1002 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 pub async fn run(&mut self) -> Result<(), SupervisorError> {
1018 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 pub async fn run_with_shutdown<F: Future + Send + 'static>(&mut self, shutdown: F) -> Result<(), SupervisorError> {
1040 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 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 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 #[derive(Clone)]
1119 enum InitBehavior {
1120 Instant,
1122
1123 Slow(Duration),
1125
1126 Fail(&'static str),
1128 }
1129
1130 #[derive(Clone)]
1132 enum RunBehavior {
1133 UntilShutdown,
1135
1136 FailAfter(Duration, &'static str),
1138
1139 CompleteAfter(Duration),
1141
1142 SlowShutdown(Duration),
1144
1145 IgnoreShutdown,
1147
1148 PanicAfter(Duration),
1150 }
1151
1152 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 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 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 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 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 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 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 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 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 fn start_count(&self) -> Arc<AtomicUsize> {
1261 Arc::clone(&self.start_count)
1262 }
1263
1264 fn with_brutal_shutdown(mut self) -> Self {
1266 self.brutal_shutdown = true;
1267 self
1268 }
1269
1270 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 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 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 sleep(Duration::from_millis(50)).await;
1361 (tx, handle)
1362 }
1363
1364 #[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 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 #[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 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 assert!(
1436 failing_count.load(Ordering::SeqCst) >= 2,
1437 "failing worker should have been restarted"
1438 );
1439 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 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 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 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 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 let transient = MockWorker::completing("transient-worker", Duration::from_millis(30));
1521 let transient_count = transient.start_count();
1522
1523 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 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 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 #[tokio::test]
1598 async fn temporary_child_is_not_restarted() {
1599 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 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 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 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 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 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 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 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 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 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 #[tokio::test]
1815 async fn significant_child_drives_auto_shutdown() {
1816 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 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 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 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 assert!(
1887 elapsed >= Duration::from_millis(200),
1888 "auto-shutdown must wait for all significant children (took {elapsed:?})"
1889 );
1890 }
1891
1892 #[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 assert_eq!(start_count.load(Ordering::SeqCst), 0);
1931 }
1932
1933 #[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 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 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 sleep(Duration::from_millis(20)).await;
1960 tx.send(()).unwrap();
1961
1962 let result = timeout(Duration::from_secs(2), handle).await;
1965 assert!(result.is_ok(), "shutdown during slow init should complete promptly");
1966 }
1967
1968 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 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 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 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 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 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 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 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 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 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 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 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 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 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 handle
2251 .spawn(MockWorker::long_running("responsive").with_graceful_timeout(Duration::MAX))
2252 .await
2253 .unwrap();
2254 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 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 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}