@@ -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