Skip to content

Commit 8836c3b

Browse files
committed
refactor: rename Completion variants to Tasks/Threads, add panic observation tests
1 parent cee6ae7 commit 8836c3b

1 file changed

Lines changed: 110 additions & 18 deletions

File tree

concurrency/src/child_handle.rs

Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ impl std::fmt::Display for ActorId {
3434
#[derive(Clone)]
3535
pub(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

Comments
 (0)