@@ -34,9 +34,9 @@ impl std::fmt::Display for ActorId {
3434#[ derive( Clone ) ]
3535pub ( crate ) enum Completion {
3636 /// Tasks mode: watch channel carrying Option<ExitReason>.
37- Watch ( spawned_rt:: tasks:: watch:: Receiver < Option < ExitReason > > ) ,
37+ Tasks ( spawned_rt:: tasks:: watch:: Receiver < Option < ExitReason > > ) ,
3838 /// Threads mode: Mutex + Condvar carrying Option<ExitReason>.
39- Condvar ( Arc < ( Mutex < Option < ExitReason > > , Condvar ) > ) ,
39+ Threads ( Arc < ( Mutex < Option < ExitReason > > , Condvar ) > ) ,
4040}
4141
4242// ---------------------------------------------------------------------------
@@ -78,7 +78,7 @@ impl ChildHandle {
7878 Self {
7979 id,
8080 cancel : Arc :: new ( move || cancellation_token. cancel ( ) ) ,
81- completion : Completion :: Watch ( completion_rx) ,
81+ completion : Completion :: Tasks ( completion_rx) ,
8282 }
8383 }
8484
@@ -91,7 +91,7 @@ impl ChildHandle {
9191 Self {
9292 id,
9393 cancel : Arc :: new ( move || cancellation_token. cancel ( ) ) ,
94- completion : Completion :: Condvar ( completion) ,
94+ completion : Completion :: Threads ( completion) ,
9595 }
9696 }
9797
@@ -114,8 +114,8 @@ impl ChildHandle {
114114 /// Poll the exit reason. Returns `None` if the actor is still running.
115115 pub fn exit_reason ( & self ) -> Option < ExitReason > {
116116 match & self . completion {
117- Completion :: Watch ( rx) => rx. borrow ( ) . clone ( ) ,
118- Completion :: Condvar ( completion) => {
117+ Completion :: Tasks ( rx) => rx. borrow ( ) . clone ( ) ,
118+ Completion :: Threads ( completion) => {
119119 let ( lock, _) = & * * completion;
120120 let guard = lock. lock ( ) . unwrap_or_else ( |p| p. into_inner ( ) ) ;
121121 guard. clone ( )
@@ -138,12 +138,11 @@ impl ChildHandle {
138138 /// [`wait_exit_async`]: ChildHandle::wait_exit_async
139139 pub fn wait_exit_blocking ( & self ) -> ExitReason {
140140 match & self . completion {
141- Completion :: Watch ( rx) => {
142- // wait_for_exit_watch_blocking is extracted to avoid holding the
143- // borrow on self across the loop
144- wait_for_exit_watch_blocking ( & rx. clone ( ) )
141+ Completion :: Tasks ( rx) => {
142+ // Extracted to avoid holding the borrow on self across the loop
143+ wait_for_tasks_exit_blocking ( & rx. clone ( ) )
145144 }
146- Completion :: Condvar ( completion) => {
145+ Completion :: Threads ( completion) => {
147146 let ( lock, cvar) = & * * completion;
148147 let mut guard = lock. lock ( ) . unwrap_or_else ( |p| p. into_inner ( ) ) ;
149148 loop {
@@ -158,15 +157,15 @@ impl ChildHandle {
158157
159158 /// Async wait until the actor stops and return the exit reason.
160159 ///
161- /// Works with both execution modes. For Watch ( tasks-mode handles) , awaits
162- /// the watch channel directly. For Condvar ( threads-mode handles) , delegates
163- /// to a blocking task via `spawn_blocking` to avoid blocking the async runtime.
160+ /// Works with both execution modes. For tasks-mode handles, awaits the
161+ /// watch channel directly. For threads-mode handles, delegates to a
162+ /// blocking task via `spawn_blocking` to avoid blocking the async runtime.
164163 ///
165164 /// **Note:** When used with threads-mode handles, this consumes a thread from
166165 /// tokio's blocking pool for the duration of the wait.
167166 pub async fn wait_exit_async ( & self ) -> ExitReason {
168167 match & self . completion {
169- Completion :: Watch ( rx) => {
168+ Completion :: Tasks ( rx) => {
170169 let mut rx = rx. clone ( ) ;
171170 loop {
172171 if let Some ( reason) = rx. borrow_and_update ( ) . clone ( ) {
@@ -177,7 +176,7 @@ impl ChildHandle {
177176 }
178177 }
179178 }
180- Completion :: Condvar ( _) => {
179+ Completion :: Threads ( _) => {
181180 let handle = self . clone ( ) ;
182181 spawned_rt:: tasks:: spawn_blocking ( move || handle. wait_exit_blocking ( ) )
183182 . await
@@ -187,7 +186,7 @@ impl ChildHandle {
187186 }
188187}
189188
190- /// Blocking wait on a watch channel.
189+ /// Blocking wait on a tasks-mode watch channel.
191190///
192191/// - From a sync context (no tokio runtime): creates a temporary runtime.
193192/// - From a multi-thread tokio runtime: uses `block_in_place` + `Handle::block_on`.
@@ -198,7 +197,7 @@ impl ChildHandle {
198197/// Returns `ExitReason::Kill` if the watch sender is dropped without setting a
199198/// reason — this means the actor task was aborted externally (e.g., runtime
200199/// shutdown) without going through the normal exit path.
201- fn wait_for_exit_watch_blocking (
200+ fn wait_for_tasks_exit_blocking (
202201 rx : & spawned_rt:: tasks:: watch:: Receiver < Option < ExitReason > > ,
203202) -> ExitReason {
204203 // Fast path: already done — works from any context, no runtime needed
@@ -462,4 +461,97 @@ mod tests {
462461 assert_eq ! ( reason, ExitReason :: Normal ) ;
463462 } ) ;
464463 }
464+
465+ // --- Panic observation tests ---
466+
467+ #[ test]
468+ fn child_handle_observes_panic_in_threads_actor ( ) {
469+ use crate :: message:: Message ;
470+ use crate :: threads:: actor:: { Actor , ActorStart , Context , Handler } ;
471+
472+ struct Boomer ;
473+ struct Boom ;
474+ impl Message for Boom {
475+ type Result = ( ) ;
476+ }
477+ impl Actor for Boomer { }
478+ impl Handler < Boom > for Boomer {
479+ fn handle ( & mut self , _msg : Boom , _ctx : & Context < Self > ) {
480+ panic ! ( "intentional panic in handler" ) ;
481+ }
482+ }
483+
484+ let actor = Boomer . start ( ) ;
485+ let handle = actor. child_handle ( ) ;
486+ let _ = actor. send ( Boom ) ;
487+
488+ let reason = handle. wait_exit_blocking ( ) ;
489+ match reason {
490+ ExitReason :: Panic ( msg) => {
491+ assert ! ( msg. contains( "intentional panic in handler" ) , "got: {msg}" ) ;
492+ }
493+ other => panic ! ( "expected Panic, got {other:?}" ) ,
494+ }
495+ assert ! ( !handle. is_alive( ) ) ;
496+ }
497+
498+ #[ test]
499+ fn child_handle_observes_panic_in_tasks_actor ( ) {
500+ use crate :: message:: Message ;
501+ use crate :: tasks:: actor:: { Actor , ActorStart , Context , Handler } ;
502+
503+ struct Boomer ;
504+ struct Boom ;
505+ impl Message for Boom {
506+ type Result = ( ) ;
507+ }
508+ impl Actor for Boomer { }
509+ impl Handler < Boom > for Boomer {
510+ async fn handle ( & mut self , _msg : Boom , _ctx : & Context < Self > ) {
511+ panic ! ( "intentional panic in handler" ) ;
512+ }
513+ }
514+
515+ let runtime = spawned_rt:: tasks:: Runtime :: new ( ) . unwrap ( ) ;
516+ runtime. block_on ( async {
517+ let actor = Boomer . start ( ) ;
518+ let handle = actor. child_handle ( ) ;
519+ let _ = actor. send ( Boom ) ;
520+
521+ let reason = handle. wait_exit_async ( ) . await ;
522+ match reason {
523+ ExitReason :: Panic ( msg) => {
524+ assert ! ( msg. contains( "intentional panic in handler" ) , "got: {msg}" ) ;
525+ }
526+ other => panic ! ( "expected Panic, got {other:?}" ) ,
527+ }
528+ assert ! ( !handle. is_alive( ) ) ;
529+ } ) ;
530+ }
531+
532+ #[ test]
533+ fn child_handle_observes_panic_in_started_callback ( ) {
534+ use crate :: tasks:: actor:: { Actor , ActorStart , Context } ;
535+
536+ struct PanicStart ;
537+ impl Actor for PanicStart {
538+ async fn started ( & mut self , _ctx : & Context < Self > ) {
539+ panic ! ( "intentional panic in started" ) ;
540+ }
541+ }
542+
543+ let runtime = spawned_rt:: tasks:: Runtime :: new ( ) . unwrap ( ) ;
544+ runtime. block_on ( async {
545+ let actor = PanicStart . start ( ) ;
546+ let handle = actor. child_handle ( ) ;
547+
548+ let reason = handle. wait_exit_async ( ) . await ;
549+ match reason {
550+ ExitReason :: Panic ( msg) => {
551+ assert ! ( msg. contains( "intentional panic in started" ) , "got: {msg}" ) ;
552+ }
553+ other => panic ! ( "expected Panic, got {other:?}" ) ,
554+ }
555+ } ) ;
556+ }
465557}
0 commit comments