Skip to content

Commit 944fd29

Browse files
committed
unify the shutdown/cleanup logic for all restart strategies
1 parent 7f0ee11 commit 944fd29

2 files changed

Lines changed: 169 additions & 50 deletions

File tree

lib/saluki-core/src/runtime/supervisor.rs

Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,16 @@ pub enum SpawnError {
496496
/// starts, configure them statically with [`Supervisor::add_worker`] instead.
497497
#[snafu(display("supervisor is gone"))]
498498
SupervisorGone,
499+
500+
/// The supervisor was running but rejected the spawn (for example, an invalid child name).
501+
///
502+
/// Unlike [`SupervisorGone`](Self::SupervisorGone), the supervisor accepted the request and then couldn't start the
503+
/// child; the underlying error is preserved as the source.
504+
#[snafu(display("supervisor rejected the spawn: {}", source))]
505+
Rejected {
506+
/// The underlying error that caused the spawn to be rejected.
507+
source: GenericError,
508+
},
499509
}
500510

501511
/// A dynamic spawn request sent from a [`SupervisorHandle`] to the running supervisor.
@@ -541,7 +551,8 @@ impl SupervisorHandle {
541551
/// # Errors
542552
///
543553
/// Returns [`SpawnError::SupervisorGone`] if the supervisor isn't currently running (it hasn't started yet, is
544-
/// between restarts, or has shut down) and so can't accept the spawn.
554+
/// between restarts, or has shut down) and so can't accept the spawn, or [`SpawnError::Rejected`] if the supervisor
555+
/// accepts the request but can't start the child (for example, an invalid child name).
545556
pub async fn spawn<T: Supervisable + 'static>(&self, worker: T) -> Result<ChildId, SpawnError> {
546557
self.spawn_with(ChildSpecification::worker(worker).with_restart_type(RestartType::Temporary))
547558
.await
@@ -811,9 +822,10 @@ impl Supervisor {
811822
let _ = ack.send(Ok(()));
812823
}
813824
Err(e) => {
814-
// Registration failed (e.g. an invalid name). Dropping the ack lets the waiting caller observe the
815-
// failure as `SupervisorGone`.
825+
// Registration failed (e.g. an invalid child name). Report it to the waiting caller as `Rejected` --
826+
// distinct from `SupervisorGone` -- so the underlying cause isn't lost.
816827
error!(supervisor_id = %self.supervisor_id, error = %e, "Failed to spawn dynamic child.");
828+
let _ = ack.send(Err(SpawnError::Rejected { source: e.into() }));
817829
}
818830
}
819831
}
@@ -853,14 +865,15 @@ impl Supervisor {
853865
// Now we supervise.
854866
pin!(process_shutdown);
855867

856-
loop {
868+
let outcome = loop {
857869
select! {
858870
// Shutdown takes priority so a flood of dynamic spawns can't starve it.
859871
biased;
860872

861-
// Shutdown has been triggered; break out of the loop and tear down below. (We can't touch `cmd_rx`
862-
// here, as the `recv` arm below borrows it for the duration of the `select!`.)
863-
_ = &mut process_shutdown => break,
873+
// Shutdown has been triggered; break out of the loop with a clean outcome and tear down below. (We
874+
// can't touch `cmd_rx` in any arm's handler -- the `recv` arm below borrows it for the whole
875+
// `select!` -- so all teardown happens after the loop.)
876+
_ = &mut process_shutdown => break Ok(()),
864877

865878
// A handle asked us to spawn a dynamic child. The published sender keeps the channel open for the whole
866879
// run, so `recv` only yields `None` once we close it during teardown.
@@ -887,8 +900,7 @@ impl Supervisor {
887900
};
888901

889902
error!(supervisor_id = %self.supervisor_id, worker_name = full_name, "Child process failed to initialize: {}", source);
890-
worker_state.shutdown_workers().await;
891-
return Err(SupervisorError::FailedToInitialize { child_name: full_name, source });
903+
break Err(SupervisorError::FailedToInitialize { child_name: full_name, source });
892904
}
893905

894906
// A worker exited abnormally if it returned an error, panicked, or was aborted; a clean exit is
@@ -922,8 +934,7 @@ impl Supervisor {
922934
};
923935
if auto_shutdown {
924936
warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Significant child terminated; shutting down supervisor.");
925-
worker_state.shutdown_workers().await;
926-
return Err(SupervisorError::SignificantChildExited);
937+
break Err(SupervisorError::SignificantChildExited);
927938
}
928939
}
929940
} else {
@@ -932,7 +943,9 @@ impl Supervisor {
932943
RestartMode::OneForOne => {
933944
warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting.");
934945
let spec = children.get(&child_id).expect("present for restart").spec.clone();
935-
worker_state.add_worker(child_id, &spec)?;
946+
if let Err(e) = worker_state.add_worker(child_id, &spec) {
947+
break Err(e);
948+
}
936949
}
937950
RestartMode::OneForAll => {
938951
warn!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Child process terminated, restarting all processes.");
@@ -942,32 +955,37 @@ impl Supervisor {
942955
// temporary children are not restarted.
943956
children.clear();
944957
self.active.store(0, Ordering::Relaxed);
945-
self.respawn_children_one_for_all(&mut children, &mut worker_state)?;
958+
let respawn = self.respawn_children_one_for_all(&mut children, &mut worker_state);
959+
if let Err(e) = respawn {
960+
break Err(e);
961+
}
946962
significant_remaining =
947963
children.values().filter(|entry| entry.config.significant).count();
948964
}
949965
},
950966
RestartAction::Shutdown => {
951967
error!(supervisor_id = %self.supervisor_id, worker_name = %child_name, ?worker_result, "Supervisor shutting down due to restart limits.");
952-
worker_state.shutdown_workers().await;
953-
return Err(SupervisorError::Shutdown);
968+
break Err(SupervisorError::Shutdown);
954969
}
955970
}
956971
}
957972
}
958973
}
959-
}
974+
};
960975

961-
// Shutdown was triggered. Stop accepting spawns and reject anything still queued -- rather than starting
962-
// children only to tear them down immediately -- then shut down all children. Closing the channel also unblocks
963-
// any handle parked on a full channel so it observes `SupervisorGone` instead of hanging into the deadline.
976+
// The run is ending -- either cleanly (shutdown was signalled) or with an error (a child failed to initialize
977+
// or restart, the restart limit was exceeded, or a significant child exited). On every path: stop accepting
978+
// spawns and reject anything still queued -- rather
979+
// than starting children only to tear them down immediately -- then shut down all children. Closing the channel
980+
// before the (possibly slow) shutdown also unblocks any handle parked on a full channel, so a spawn racing the
981+
// teardown observes `SupervisorGone` promptly instead of hanging until shutdown finishes.
964982
cmd_rx.close();
965983
while let Ok(spawn) = cmd_rx.try_recv() {
966984
let _ = spawn.ack.send(Err(SpawnError::SupervisorGone));
967985
}
968986
worker_state.shutdown_workers().await;
969987

970-
Ok(())
988+
outcome
971989
}
972990

973991
fn as_nested_process(&self, process: Process, process_shutdown: ShutdownHandle) -> WorkerFuture {
@@ -1132,6 +1150,7 @@ mod tests {
11321150
run_behavior: RunBehavior,
11331151
start_count: Arc<AtomicUsize>,
11341152
brutal_shutdown: bool,
1153+
graceful_timeout: Duration,
11351154
}
11361155

11371156
impl MockWorker {
@@ -1143,6 +1162,7 @@ mod tests {
11431162
run_behavior: RunBehavior::UntilShutdown,
11441163
start_count: Arc::new(AtomicUsize::new(0)),
11451164
brutal_shutdown: false,
1165+
graceful_timeout: Duration::from_millis(500),
11461166
}
11471167
}
11481168

@@ -1154,6 +1174,7 @@ mod tests {
11541174
run_behavior: RunBehavior::FailAfter(delay, "worker failed"),
11551175
start_count: Arc::new(AtomicUsize::new(0)),
11561176
brutal_shutdown: false,
1177+
graceful_timeout: Duration::from_millis(500),
11571178
}
11581179
}
11591180

@@ -1165,6 +1186,7 @@ mod tests {
11651186
run_behavior: RunBehavior::CompleteAfter(delay),
11661187
start_count: Arc::new(AtomicUsize::new(0)),
11671188
brutal_shutdown: false,
1189+
graceful_timeout: Duration::from_millis(500),
11681190
}
11691191
}
11701192

@@ -1176,6 +1198,7 @@ mod tests {
11761198
run_behavior: RunBehavior::SlowShutdown(delay),
11771199
start_count: Arc::new(AtomicUsize::new(0)),
11781200
brutal_shutdown: false,
1201+
graceful_timeout: Duration::from_millis(500),
11791202
}
11801203
}
11811204

@@ -1187,6 +1210,7 @@ mod tests {
11871210
run_behavior: RunBehavior::IgnoreShutdown,
11881211
start_count: Arc::new(AtomicUsize::new(0)),
11891212
brutal_shutdown: false,
1213+
graceful_timeout: Duration::from_millis(500),
11901214
}
11911215
}
11921216

@@ -1198,6 +1222,7 @@ mod tests {
11981222
run_behavior: RunBehavior::PanicAfter(delay),
11991223
start_count: Arc::new(AtomicUsize::new(0)),
12001224
brutal_shutdown: false,
1225+
graceful_timeout: Duration::from_millis(500),
12011226
}
12021227
}
12031228

@@ -1209,6 +1234,7 @@ mod tests {
12091234
run_behavior: RunBehavior::UntilShutdown,
12101235
start_count: Arc::new(AtomicUsize::new(0)),
12111236
brutal_shutdown: false,
1237+
graceful_timeout: Duration::from_millis(500),
12121238
}
12131239
}
12141240

@@ -1220,6 +1246,7 @@ mod tests {
12201246
run_behavior: RunBehavior::UntilShutdown,
12211247
start_count: Arc::new(AtomicUsize::new(0)),
12221248
brutal_shutdown: false,
1249+
graceful_timeout: Duration::from_millis(500),
12231250
}
12241251
}
12251252

@@ -1233,6 +1260,12 @@ mod tests {
12331260
self.brutal_shutdown = true;
12341261
self
12351262
}
1263+
1264+
/// Overrides the worker's graceful shutdown timeout (defaults to 500 milliseconds).
1265+
fn with_graceful_timeout(mut self, timeout: Duration) -> Self {
1266+
self.graceful_timeout = timeout;
1267+
self
1268+
}
12361269
}
12371270

12381271
#[async_trait]
@@ -1245,7 +1278,7 @@ mod tests {
12451278
if self.brutal_shutdown {
12461279
ShutdownStrategy::Brutal
12471280
} else {
1248-
ShutdownStrategy::Graceful(Duration::from_millis(500))
1281+
ShutdownStrategy::Graceful(self.graceful_timeout)
12491282
}
12501283
}
12511284

@@ -2110,6 +2143,28 @@ mod tests {
21102143
assert!(result.is_ok());
21112144
}
21122145

2146+
#[tokio::test]
2147+
async fn dynamic_spawn_rejects_invalid_child_name() {
2148+
// While running, a spawn that fails registration (here, an empty/invalid child name) is reported as
2149+
// `Rejected` with the underlying cause -- not `SupervisorGone`, which means the supervisor isn't running.
2150+
let sup = Supervisor::new("dyn-sup").unwrap();
2151+
let handle = sup.handle();
2152+
let (tx, run) = run_supervisor_with_trigger(sup).await;
2153+
wait_running(&handle).await;
2154+
2155+
let err = handle.spawn(MockWorker::long_running("")).await.unwrap_err();
2156+
assert!(matches!(err, SpawnError::Rejected { .. }), "got {err:?}");
2157+
2158+
// The supervisor stays up and still accepts valid children.
2159+
assert!(handle.is_running());
2160+
handle.spawn(MockWorker::long_running("ok")).await.unwrap();
2161+
wait_until(|| handle.active_children() == 1).await;
2162+
2163+
tx.send(()).unwrap();
2164+
let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2165+
assert!(result.is_ok());
2166+
}
2167+
21132168
#[tokio::test]
21142169
async fn concurrent_shutdown_drains_many_children_quickly() {
21152170
const CHILDREN: usize = 500;
@@ -2172,6 +2227,44 @@ mod tests {
21722227
);
21732228
}
21742229

2230+
#[tokio::test]
2231+
async fn concurrent_shutdown_honors_per_child_deadline() {
2232+
// Each child must be aborted at its OWN graceful deadline, not a single shared one. A responsive child with an
2233+
// effectively-infinite timeout (modeling a nested supervisor, which uses `Graceful(Duration::MAX)`) coexists
2234+
// with an unresponsive child with a short timeout. Under a shared `max` deadline the short-timeout child would
2235+
// never be aborted (the shared deadline would be `MAX`) and shutdown would hang.
2236+
let sup = Supervisor::new("dyn-sup")
2237+
.unwrap()
2238+
.with_shutdown_mode(ShutdownMode::Concurrent);
2239+
let handle = sup.handle();
2240+
let (tx, run) = run_supervisor_with_trigger(sup).await;
2241+
wait_running(&handle).await;
2242+
2243+
// Responds to shutdown promptly, but its deadline is effectively infinite.
2244+
handle
2245+
.spawn(MockWorker::long_running("responsive").with_graceful_timeout(Duration::MAX))
2246+
.await
2247+
.unwrap();
2248+
// Never responds; must be aborted at its own short deadline.
2249+
handle
2250+
.spawn(MockWorker::ignore_shutdown("stuck").with_graceful_timeout(Duration::from_millis(200)))
2251+
.await
2252+
.unwrap();
2253+
wait_until(|| handle.active_children() == 2).await;
2254+
2255+
let start = std::time::Instant::now();
2256+
tx.send(()).unwrap();
2257+
let result = timeout(Duration::from_secs(2), run).await.unwrap().unwrap();
2258+
let elapsed = start.elapsed();
2259+
2260+
assert!(result.is_ok());
2261+
assert_eq!(handle.active_children(), 0);
2262+
assert!(
2263+
elapsed < Duration::from_secs(1),
2264+
"stuck child must be aborted at its own deadline despite an infinite-timeout sibling (took {elapsed:?})"
2265+
);
2266+
}
2267+
21752268
#[tokio::test]
21762269
async fn ordered_shutdown_aborts_unresponsive_child() {
21772270
// Under the default `ShutdownMode::Ordered`, a child that never reacts to shutdown must be aborted once its

0 commit comments

Comments
 (0)