@@ -125,9 +125,17 @@ impl ChildHandle {
125125
126126 /// Block the calling thread until the actor stops and return the exit reason.
127127 ///
128- /// Safe to call from any context (sync or async). For the Watch variant,
129- /// uses the watch channel's built-in blocking recv. For Condvar, blocks
130- /// on the condvar directly.
128+ /// Safe contexts:
129+ /// - Sync code with no active tokio runtime
130+ /// - Multi-thread tokio runtime (uses `block_in_place`)
131+ /// - Threads-mode actors (uses Condvar directly)
132+ ///
133+ /// **Panics** if called from within a current-thread tokio runtime on a
134+ /// tasks-mode handle: blocking the only runtime thread would prevent the
135+ /// actor task from making progress (deadlock). Use [`wait_exit_async`] from
136+ /// async context instead.
137+ ///
138+ /// [`wait_exit_async`]: ChildHandle::wait_exit_async
131139 pub fn wait_exit_blocking ( & self ) -> ExitReason {
132140 match & self . completion {
133141 Completion :: Watch ( rx) => {
@@ -179,16 +187,21 @@ impl ChildHandle {
179187 }
180188}
181189
182- /// Blocking wait on a watch channel. Uses `block_in_place` if inside a tokio
183- /// runtime (safe from multi-threaded runtime), otherwise creates a temporary runtime.
190+ /// Blocking wait on a watch channel.
191+ ///
192+ /// - From a sync context (no tokio runtime): creates a temporary runtime.
193+ /// - From a multi-thread tokio runtime: uses `block_in_place` + `Handle::block_on`.
194+ /// - From a current-thread tokio runtime: **panics**, because blocking the only
195+ /// runtime thread prevents the actor task from making progress (deadlock).
196+ /// Use `wait_exit_async` from async context, or run on a multi-thread runtime.
184197///
185198/// Returns `ExitReason::Kill` if the watch sender is dropped without setting a
186199/// reason — this means the actor task was aborted externally (e.g., runtime
187200/// shutdown) without going through the normal exit path.
188201fn wait_for_exit_watch_blocking (
189202 rx : & spawned_rt:: tasks:: watch:: Receiver < Option < ExitReason > > ,
190203) -> ExitReason {
191- // Fast path: already done
204+ // Fast path: already done — works from any context, no runtime needed
192205 if let Some ( reason) = rx. borrow ( ) . clone ( ) {
193206 return reason;
194207 }
@@ -205,12 +218,24 @@ fn wait_for_exit_watch_blocking(
205218 }
206219 } ;
207220
208- // If inside a tokio runtime, use block_in_place + block_on to avoid
209- // "cannot start a runtime from within a runtime" panic.
210- if let Ok ( handle) = spawned_rt:: tasks:: Handle :: try_current ( ) {
211- spawned_rt:: tasks:: block_in_place ( || handle. block_on ( wait) )
212- } else {
213- spawned_rt:: threads:: block_on ( wait)
221+ match spawned_rt:: tasks:: Handle :: try_current ( ) {
222+ // No active runtime — create a temporary one
223+ Err ( _) => spawned_rt:: threads:: block_on ( wait) ,
224+ // Inside a tokio runtime — check the flavor
225+ Ok ( handle) => match handle. runtime_flavor ( ) {
226+ spawned_rt:: tasks:: RuntimeFlavor :: MultiThread => {
227+ spawned_rt:: tasks:: block_in_place ( || handle. block_on ( wait) )
228+ }
229+ // CurrentThread runtime: blocking here would deadlock the actor task.
230+ // Including future flavors (e.g., MultiThreadAlt) for safety — only
231+ // MultiThread is known to be safe with block_in_place.
232+ _ => panic ! (
233+ "ChildHandle::wait_exit_blocking() cannot be called from within a \
234+ current_thread tokio runtime; doing so would deadlock the actor \
235+ task that this call is waiting for. Use wait_exit_async() from \
236+ async context instead, or run on a multi-thread runtime."
237+ ) ,
238+ } ,
214239 }
215240}
216241
@@ -372,21 +397,68 @@ mod tests {
372397 }
373398
374399 #[ test]
375- fn child_handle_wait_blocking_inside_runtime ( ) {
400+ fn child_handle_wait_blocking_inside_multithread_runtime ( ) {
376401 use crate :: tasks:: actor:: { Actor , ActorStart } ;
377402
378403 struct Idler ;
379404 impl Actor for Idler { }
380405
406+ // Multi-thread runtime — wait_exit_blocking should work via block_in_place
381407 let runtime = spawned_rt:: tasks:: Runtime :: new ( ) . unwrap ( ) ;
382408 runtime. block_on ( async {
383409 let actor = Idler . start ( ) ;
384410 let handle = actor. child_handle ( ) ;
385411 handle. stop ( ) ;
386412
387- // This is the bug-fix test: wait_exit_blocking inside a tokio runtime
388- // should NOT panic (uses block_in_place internally)
389- let reason = spawned_rt:: tasks:: block_in_place ( || handle. wait_exit_blocking ( ) ) ;
413+ let reason = handle. wait_exit_blocking ( ) ;
414+ assert_eq ! ( reason, ExitReason :: Normal ) ;
415+ } ) ;
416+ }
417+
418+ #[ test]
419+ #[ should_panic( expected = "current_thread tokio runtime" ) ]
420+ fn child_handle_wait_blocking_panics_on_current_thread_runtime ( ) {
421+ use crate :: tasks:: actor:: { Actor , ActorStart } ;
422+
423+ struct Idler ;
424+ impl Actor for Idler { }
425+
426+ let runtime = :: tokio:: runtime:: Builder :: new_current_thread ( )
427+ . enable_all ( )
428+ . build ( )
429+ . unwrap ( ) ;
430+
431+ runtime. block_on ( async {
432+ let actor = Idler . start ( ) ;
433+ let handle = actor. child_handle ( ) ;
434+ // Calling wait_exit_blocking from within a current-thread runtime
435+ // would deadlock the actor task — we panic with a clear message instead.
436+ let _ = handle. wait_exit_blocking ( ) ;
437+ } ) ;
438+ }
439+
440+ #[ test]
441+ fn child_handle_wait_blocking_fast_path_on_current_thread_runtime ( ) {
442+ use crate :: tasks:: actor:: { Actor , ActorStart } ;
443+
444+ struct Idler ;
445+ impl Actor for Idler { }
446+
447+ let runtime = :: tokio:: runtime:: Builder :: new_current_thread ( )
448+ . enable_all ( )
449+ . build ( )
450+ . unwrap ( ) ;
451+
452+ // Fast path: if the actor has already exited, wait_exit_blocking is safe
453+ // even on a current-thread runtime (no actual blocking happens).
454+ runtime. block_on ( async {
455+ let actor = Idler . start ( ) ;
456+ let handle = actor. child_handle ( ) ;
457+ handle. stop ( ) ;
458+ // Wait async first so the actor actually exits
459+ let _ = handle. wait_exit_async ( ) . await ;
460+ // Now wait_exit_blocking takes the fast path
461+ let reason = handle. wait_exit_blocking ( ) ;
390462 assert_eq ! ( reason, ExitReason :: Normal ) ;
391463 } ) ;
392464 }
0 commit comments