@@ -168,4 +168,82 @@ impl TaskManager {
168168 pub fn task_count ( & self ) -> usize {
169169 self . task_tracker . len ( )
170170 }
171+
172+ /// Installs custom panic hook and allows app cleanup before dying
173+ pub fn install_panic_hook ( & self ) {
174+ let token = self . cancellation_token ( ) ;
175+ let default_hook = std:: panic:: take_hook ( ) ;
176+ std:: panic:: set_hook ( Box :: new ( move |hook| {
177+ default_hook ( hook) ;
178+ log:: error!( "Thread panicked, shutting down all tasks.." ) ;
179+ token. cancel ( ) ;
180+ } ) ) ;
181+ }
182+ }
183+
184+ #[ cfg( test) ]
185+ mod tests {
186+ use super :: * ;
187+ use std:: sync:: Arc ;
188+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
189+
190+ struct PanicTask ;
191+
192+ #[ async_trait:: async_trait]
193+ impl Task for PanicTask {
194+ fn task_name ( & self ) -> String {
195+ "panic-task" . to_string ( )
196+ }
197+ async fn run ( & self ) -> TaskState {
198+ panic ! ( "simulated papaya panic: assertion failed: len.is_power_of_two()" ) ;
199+ }
200+ }
201+
202+ struct WaitingTask {
203+ cleaned_up : Arc < AtomicBool > ,
204+ }
205+
206+ #[ async_trait:: async_trait]
207+ impl Task for WaitingTask {
208+ fn task_name ( & self ) -> String {
209+ "waiting-task" . to_string ( )
210+ }
211+ async fn run ( & self ) -> TaskState {
212+ // Yield back to the runtime repeatedly, simulating a long-running task
213+ std:: future:: pending :: < ( ) > ( ) . await ;
214+ TaskState :: Continue
215+ }
216+ async fn cleanup ( & self ) {
217+ self . cleaned_up . store ( true , Ordering :: SeqCst ) ;
218+ }
219+ }
220+
221+ #[ tokio:: test]
222+ async fn test_panic_triggers_shutdown_of_all_tasks ( ) {
223+ let manager = TaskManager :: new ( ) ;
224+ manager. install_panic_hook ( ) ;
225+
226+ let cleaned_up = Arc :: new ( AtomicBool :: new ( false ) ) ;
227+
228+ // Spawn a long-running task that should get cancelled when the panic fires
229+ manager
230+ . spawn_task_loop ( WaitingTask {
231+ cleaned_up : cleaned_up. clone ( ) ,
232+ } )
233+ . await ;
234+
235+ // Spawn a task that will panic
236+ manager. spawn_task_loop ( PanicTask ) . await ;
237+
238+ // wait() should return because the panic hook cancels all tasks.
239+ // If the hook doesn't work, this will hang forever — the test runner
240+ // will kill it after its timeout.
241+ manager. wait ( ) . await ;
242+
243+ // The waiting task should have run its cleanup
244+ assert ! (
245+ cleaned_up. load( Ordering :: SeqCst ) ,
246+ "WaitingTask cleanup should have been called during shutdown"
247+ ) ;
248+ }
171249}
0 commit comments