Skip to content

Commit cee6ae7

Browse files
committed
fix: detect current_thread runtime in wait_exit_blocking and panic with clear message
1 parent 260cc35 commit cee6ae7

3 files changed

Lines changed: 90 additions & 18 deletions

File tree

concurrency/src/child_handle.rs

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
188201
fn 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
}

rt/src/tasks/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub use crate::tasks::tokio::timeout;
1818
pub use crate::tasks::tokio::watch;
1919
pub use crate::tasks::tokio::CancellationToken;
2020
pub use crate::tasks::tokio::{
21-
block_in_place, spawn, spawn_blocking, task_id, Handle, JoinHandle, Runtime,
21+
block_in_place, spawn, spawn_blocking, task_id, Handle, JoinHandle, Runtime, RuntimeFlavor,
2222
};
2323
pub use crate::tasks::tokio::{BroadcastStream, ReceiverStream};
2424
use std::future::Future;

rt/src/tasks/tokio/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pub mod oneshot;
44
pub use tokio::sync::watch;
55

66
pub use tokio::{
7-
runtime::{Handle, Runtime},
7+
runtime::{Handle, Runtime, RuntimeFlavor},
88
task::{block_in_place, id as task_id, spawn, spawn_blocking, JoinHandle},
99
time::{sleep, timeout},
1010
};

0 commit comments

Comments
 (0)