@@ -525,6 +525,24 @@ impl Supervisor {
525525 Ok ( ( ) )
526526 }
527527
528+ /// Respawns children after a one-for-all restart, honoring each child's [`RestartType`].
529+ ///
530+ /// Every child except [`RestartType::Temporary`] is restarted, matching Erlang/OTP: a group restart restarts all
531+ /// permanent and transient children -- regardless of how they last exited, including a transient child that had
532+ /// already exited cleanly -- but never temporary children, which are shut down with the group and not brought back.
533+ /// A transient child's "restart only on abnormal exit" rule governs its _own_ termination, not a group restart
534+ /// driven by a sibling.
535+ fn respawn_children_one_for_all ( & self , worker_state : & mut WorkerState ) -> Result < ( ) , SupervisorError > {
536+ debug ! ( supervisor_id = %self . supervisor_id, "Restarting all eligible static child processes." ) ;
537+ for child_spec_idx in 0 ..self . child_specs . len ( ) {
538+ if self . get_restart_type ( child_spec_idx) != RestartType :: Temporary {
539+ self . spawn_child ( child_spec_idx, worker_state) ?;
540+ }
541+ }
542+
543+ Ok ( ( ) )
544+ }
545+
528546 async fn run_inner ( & self , process : Process , process_shutdown : ShutdownHandle ) -> Result < ( ) , SupervisorError > {
529547 if self . child_specs . is_empty ( ) {
530548 return Err ( SupervisorError :: NoChildren ) ;
@@ -598,7 +616,7 @@ impl Supervisor {
598616 RestartMode :: OneForAll => {
599617 warn!( supervisor_id = %self . supervisor_id, worker_name = child_spec. name( ) , ?worker_result, "Child process terminated, restarting all processes." ) ;
600618 worker_state. shutdown_workers( ) . await ;
601- self . spawn_all_children ( & mut worker_state) ?;
619+ self . respawn_children_one_for_all ( & mut worker_state) ?;
602620 }
603621 } ,
604622 RestartAction :: Shutdown => {
@@ -1162,6 +1180,70 @@ mod tests {
11621180 ) ;
11631181 }
11641182
1183+ #[ tokio:: test]
1184+ async fn one_for_all_does_not_restart_temporary_children ( ) {
1185+ // A permanent worker that fails repeatedly drives one-for-all restarts; a temporary sibling is shut down with
1186+ // the group on each cycle but, per OTP semantics, must never be brought back.
1187+ let failing = MockWorker :: failing ( "failing-worker" , Duration :: from_millis ( 50 ) ) ;
1188+ let failing_count = failing. start_count ( ) ;
1189+
1190+ let temp = MockWorker :: long_running ( "temp-worker" ) ;
1191+ let temp_count = temp. start_count ( ) ;
1192+
1193+ let mut sup = Supervisor :: new ( "test-sup" ) . unwrap ( ) . with_restart_strategy (
1194+ RestartStrategy :: one_for_all ( ) . with_intensity_and_period ( 20 , Duration :: from_secs ( 10 ) ) ,
1195+ ) ;
1196+ sup. add_worker ( ChildSpecification :: worker ( temp) . with_restart_type ( RestartType :: Temporary ) ) ;
1197+ sup. add_worker ( failing) ;
1198+
1199+ let ( tx, handle) = run_supervisor_with_trigger ( sup) . await ;
1200+
1201+ // Let several one-for-all cycles occur.
1202+ sleep ( Duration :: from_millis ( 300 ) ) . await ;
1203+ let _ = tx. send ( ( ) ) ;
1204+
1205+ let result = timeout ( Duration :: from_secs ( 2 ) , handle) . await . unwrap ( ) . unwrap ( ) ;
1206+ assert ! ( result. is_ok( ) ) ;
1207+ assert ! (
1208+ failing_count. load( Ordering :: SeqCst ) >= 2 ,
1209+ "permanent worker should have been restarted by one-for-all"
1210+ ) ;
1211+ assert_eq ! (
1212+ temp_count. load( Ordering :: SeqCst ) ,
1213+ 1 ,
1214+ "temporary child must not be restarted by a one-for-all group restart"
1215+ ) ;
1216+ }
1217+
1218+ #[ tokio:: test]
1219+ async fn one_for_all_restarts_transient_children ( ) {
1220+ // A transient child that exits cleanly is not restarted on its own, but a one-for-all restart triggered by a
1221+ // sibling restarts it anyway -- matching OTP, where only temporary children are exempt from group restarts.
1222+ let transient = MockWorker :: completing ( "transient-worker" , Duration :: from_millis ( 30 ) ) ;
1223+ let transient_count = transient. start_count ( ) ;
1224+
1225+ // Fails after the transient has already exited cleanly, so the group restart is what brings the transient back.
1226+ let failing = MockWorker :: failing ( "failing-worker" , Duration :: from_millis ( 80 ) ) ;
1227+
1228+ let mut sup = Supervisor :: new ( "test-sup" ) . unwrap ( ) . with_restart_strategy (
1229+ RestartStrategy :: one_for_all ( ) . with_intensity_and_period ( 20 , Duration :: from_secs ( 10 ) ) ,
1230+ ) ;
1231+ sup. add_worker ( ChildSpecification :: worker ( transient) . with_restart_type ( RestartType :: Transient ) ) ;
1232+ sup. add_worker ( failing) ;
1233+
1234+ let ( tx, handle) = run_supervisor_with_trigger ( sup) . await ;
1235+
1236+ sleep ( Duration :: from_millis ( 300 ) ) . await ;
1237+ let _ = tx. send ( ( ) ) ;
1238+
1239+ let result = timeout ( Duration :: from_secs ( 2 ) , handle) . await . unwrap ( ) . unwrap ( ) ;
1240+ assert ! ( result. is_ok( ) ) ;
1241+ assert ! (
1242+ transient_count. load( Ordering :: SeqCst ) >= 2 ,
1243+ "transient child must be restarted by a one-for-all group restart, even after a clean exit"
1244+ ) ;
1245+ }
1246+
11651247 #[ tokio:: test]
11661248 async fn restart_limit_exceeded_shuts_down_supervisor ( ) {
11671249 let mut sup = Supervisor :: new ( "test-sup" )
0 commit comments