@@ -1168,6 +1168,7 @@ mod tests {
11681168 init_behavior : InitBehavior ,
11691169 run_behavior : RunBehavior ,
11701170 start_count : Arc < AtomicUsize > ,
1171+ brutal_shutdown : bool ,
11711172 }
11721173
11731174 impl MockWorker {
@@ -1178,6 +1179,7 @@ mod tests {
11781179 init_behavior : InitBehavior :: Instant ,
11791180 run_behavior : RunBehavior :: UntilShutdown ,
11801181 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1182+ brutal_shutdown : false ,
11811183 }
11821184 }
11831185
@@ -1188,6 +1190,7 @@ mod tests {
11881190 init_behavior : InitBehavior :: Instant ,
11891191 run_behavior : RunBehavior :: FailAfter ( delay, "worker failed" ) ,
11901192 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1193+ brutal_shutdown : false ,
11911194 }
11921195 }
11931196
@@ -1198,6 +1201,7 @@ mod tests {
11981201 init_behavior : InitBehavior :: Instant ,
11991202 run_behavior : RunBehavior :: CompleteAfter ( delay) ,
12001203 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1204+ brutal_shutdown : false ,
12011205 }
12021206 }
12031207
@@ -1208,6 +1212,7 @@ mod tests {
12081212 init_behavior : InitBehavior :: Instant ,
12091213 run_behavior : RunBehavior :: SlowShutdown ( delay) ,
12101214 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1215+ brutal_shutdown : false ,
12111216 }
12121217 }
12131218
@@ -1218,6 +1223,7 @@ mod tests {
12181223 init_behavior : InitBehavior :: Instant ,
12191224 run_behavior : RunBehavior :: IgnoreShutdown ,
12201225 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1226+ brutal_shutdown : false ,
12211227 }
12221228 }
12231229
@@ -1228,6 +1234,7 @@ mod tests {
12281234 init_behavior : InitBehavior :: Instant ,
12291235 run_behavior : RunBehavior :: PanicAfter ( delay) ,
12301236 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1237+ brutal_shutdown : false ,
12311238 }
12321239 }
12331240
@@ -1238,6 +1245,7 @@ mod tests {
12381245 init_behavior : InitBehavior :: Fail ( "init failed" ) ,
12391246 run_behavior : RunBehavior :: UntilShutdown ,
12401247 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1248+ brutal_shutdown : false ,
12411249 }
12421250 }
12431251
@@ -1248,13 +1256,20 @@ mod tests {
12481256 init_behavior : InitBehavior :: Slow ( init_delay) ,
12491257 run_behavior : RunBehavior :: UntilShutdown ,
12501258 start_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
1259+ brutal_shutdown : false ,
12511260 }
12521261 }
12531262
12541263 /// Returns a shared handle to the start count for this worker.
12551264 fn start_count ( & self ) -> Arc < AtomicUsize > {
12561265 Arc :: clone ( & self . start_count )
12571266 }
1267+
1268+ /// Configures this worker to use a `Brutal` shutdown strategy (immediate abort, no graceful wait).
1269+ fn with_brutal_shutdown ( mut self ) -> Self {
1270+ self . brutal_shutdown = true ;
1271+ self
1272+ }
12581273 }
12591274
12601275 #[ async_trait]
@@ -1264,7 +1279,11 @@ mod tests {
12641279 }
12651280
12661281 fn shutdown_strategy ( & self ) -> ShutdownStrategy {
1267- ShutdownStrategy :: Graceful ( Duration :: from_millis ( 500 ) )
1282+ if self . brutal_shutdown {
1283+ ShutdownStrategy :: Brutal
1284+ } else {
1285+ ShutdownStrategy :: Graceful ( Duration :: from_millis ( 500 ) )
1286+ }
12681287 }
12691288
12701289 async fn initialize ( & self , process_shutdown : ShutdownHandle ) -> Result < SupervisorFuture , InitializationError > {
@@ -1521,6 +1540,39 @@ mod tests {
15211540 ) ;
15221541 }
15231542
1543+ #[ tokio:: test]
1544+ async fn transient_abnormal_exit_triggers_one_for_all ( ) {
1545+ // A transient child's *own* abnormal exit is restartable, so under one-for-all it triggers a whole-group
1546+ // restart -- the sibling is restarted too, not just the transient.
1547+ let transient = MockWorker :: failing ( "transient-worker" , Duration :: from_millis ( 50 ) ) ;
1548+ let transient_count = transient. start_count ( ) ;
1549+
1550+ let stable = MockWorker :: long_running ( "stable-worker" ) ;
1551+ let stable_count = stable. start_count ( ) ;
1552+
1553+ let mut sup = Supervisor :: new ( "test-sup" ) . unwrap ( ) . with_restart_strategy (
1554+ RestartStrategy :: one_for_all ( ) . with_intensity_and_period ( 20 , Duration :: from_secs ( 10 ) ) ,
1555+ ) ;
1556+ sup. add_worker ( ChildSpecification :: worker ( transient) . with_restart_type ( RestartType :: Transient ) ) ;
1557+ sup. add_worker ( stable) ;
1558+
1559+ let ( tx, handle) = run_supervisor_with_trigger ( sup) . await ;
1560+
1561+ sleep ( Duration :: from_millis ( 300 ) ) . await ;
1562+ let _ = tx. send ( ( ) ) ;
1563+
1564+ let result = timeout ( Duration :: from_secs ( 2 ) , handle) . await . unwrap ( ) . unwrap ( ) ;
1565+ assert ! ( result. is_ok( ) ) ;
1566+ assert ! (
1567+ transient_count. load( Ordering :: SeqCst ) >= 2 ,
1568+ "transient worker must be restarted after its own abnormal exit"
1569+ ) ;
1570+ assert ! (
1571+ stable_count. load( Ordering :: SeqCst ) >= 2 ,
1572+ "the transient's abnormal exit must trigger a one-for-all that also restarts the sibling"
1573+ ) ;
1574+ }
1575+
15241576 #[ tokio:: test]
15251577 async fn restart_limit_exceeded_shuts_down_supervisor ( ) {
15261578 let mut sup = Supervisor :: new ( "test-sup" )
@@ -1686,6 +1738,47 @@ mod tests {
16861738 }
16871739 }
16881740
1741+ #[ tokio:: test]
1742+ async fn transient_clean_exits_do_not_consume_restart_intensity ( ) {
1743+ // With intensity=1, two *restartable* exits within the period would shut the supervisor down. Here several
1744+ // transient workers all complete cleanly. A transient child's clean exit isn't eligible for restart, so it
1745+ // must not consume the restart-intensity budget, and the supervisor must stay up.
1746+ let mut sup = Supervisor :: new ( "test-sup" )
1747+ . unwrap ( )
1748+ . with_restart_strategy ( RestartStrategy :: one_to_one ( ) . with_intensity_and_period ( 1 , Duration :: from_secs ( 10 ) ) ) ;
1749+
1750+ let workers = [
1751+ MockWorker :: completing ( "transient-0" , Duration :: from_millis ( 20 ) ) ,
1752+ MockWorker :: completing ( "transient-1" , Duration :: from_millis ( 20 ) ) ,
1753+ MockWorker :: completing ( "transient-2" , Duration :: from_millis ( 20 ) ) ,
1754+ MockWorker :: completing ( "transient-3" , Duration :: from_millis ( 20 ) ) ,
1755+ MockWorker :: completing ( "transient-4" , Duration :: from_millis ( 20 ) ) ,
1756+ ] ;
1757+ let counts: Vec < _ > = workers. iter ( ) . map ( |w| w. start_count ( ) ) . collect ( ) ;
1758+ for worker in workers {
1759+ sup. add_worker ( ChildSpecification :: worker ( worker) . with_restart_type ( RestartType :: Transient ) ) ;
1760+ }
1761+ // A long-running worker so the supervisor doesn't simply idle once the transients have completed.
1762+ sup. add_worker ( MockWorker :: long_running ( "stable-worker" ) ) ;
1763+
1764+ let ( tx, handle) = run_supervisor_with_trigger ( sup) . await ;
1765+ sleep ( Duration :: from_millis ( 300 ) ) . await ;
1766+ let _ = tx. send ( ( ) ) ;
1767+
1768+ let result = timeout ( Duration :: from_secs ( 2 ) , handle) . await . unwrap ( ) . unwrap ( ) ;
1769+ assert ! (
1770+ result. is_ok( ) ,
1771+ "supervisor must not trip its restart limit on clean transient exits"
1772+ ) ;
1773+ for count in counts {
1774+ assert_eq ! (
1775+ count. load( Ordering :: SeqCst ) ,
1776+ 1 ,
1777+ "each transient worker runs exactly once"
1778+ ) ;
1779+ }
1780+ }
1781+
16891782 #[ tokio:: test]
16901783 async fn supervisor_idles_when_all_temporary_children_exit ( ) {
16911784 // When every child is temporary and they all exit, the worker set drains. The supervisor must not panic or exit
@@ -2146,4 +2239,46 @@ mod tests {
21462239 "stuck child must be aborted at the deadline (took {elapsed:?})"
21472240 ) ;
21482241 }
2242+
2243+ #[ tokio:: test]
2244+ async fn ordered_shutdown_aborts_unresponsive_child ( ) {
2245+ // Under the default `ShutdownMode::Ordered`, a child that never reacts to shutdown must be aborted once its
2246+ // graceful deadline (500ms) elapses, rather than hanging the supervisor.
2247+ let mut sup = Supervisor :: new ( "test-sup" ) . unwrap ( ) ;
2248+ sup. add_worker ( MockWorker :: ignore_shutdown ( "stuck" ) ) ;
2249+
2250+ let ( tx, handle) = run_supervisor_with_trigger ( sup) . await ;
2251+
2252+ let start = std:: time:: Instant :: now ( ) ;
2253+ tx. send ( ( ) ) . unwrap ( ) ;
2254+ let result = timeout ( Duration :: from_secs ( 2 ) , handle) . await . unwrap ( ) . unwrap ( ) ;
2255+ let elapsed = start. elapsed ( ) ;
2256+
2257+ assert ! ( result. is_ok( ) ) ;
2258+ assert ! (
2259+ elapsed < Duration :: from_secs( 1 ) ,
2260+ "unresponsive child must be aborted at its deadline under ordered shutdown (took {elapsed:?})"
2261+ ) ;
2262+ }
2263+
2264+ #[ tokio:: test]
2265+ async fn brutal_shutdown_aborts_child_immediately ( ) {
2266+ // A child with a `Brutal` shutdown strategy is aborted at once on shutdown, with no graceful wait -- so even a
2267+ // child that ignores shutdown is torn down promptly rather than after the graceful deadline.
2268+ let mut sup = Supervisor :: new ( "test-sup" ) . unwrap ( ) ;
2269+ sup. add_worker ( MockWorker :: ignore_shutdown ( "brutal-stuck" ) . with_brutal_shutdown ( ) ) ;
2270+
2271+ let ( tx, handle) = run_supervisor_with_trigger ( sup) . await ;
2272+
2273+ let start = std:: time:: Instant :: now ( ) ;
2274+ tx. send ( ( ) ) . unwrap ( ) ;
2275+ let result = timeout ( Duration :: from_secs ( 2 ) , handle) . await . unwrap ( ) . unwrap ( ) ;
2276+ let elapsed = start. elapsed ( ) ;
2277+
2278+ assert ! ( result. is_ok( ) ) ;
2279+ assert ! (
2280+ elapsed < Duration :: from_millis( 200 ) ,
2281+ "brutal-shutdown child must be aborted immediately, not after a graceful wait (took {elapsed:?})"
2282+ ) ;
2283+ }
21492284}
0 commit comments