Repository: slawlor/ractor
Pull request #350 (2025-05-02 02:08:01)

[cleanup] Cleanup docs and other stuff on new thread-local actors

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/slawlor/ractor/pull/350?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor) Report
Attention: Patch coverage is `85.58559%` with `32 lines` in your changes missing coverage. Please review.
> Project coverage is 80.95%. Comparing base [(`56e12a6`)](https://app.codecov.io/gh/slawlor/ractor/commit/56e12a68fae1d88f6eef86e2a98f9b9f67d234f4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor) to head [(`6d13f77`)](https://app.codecov.io/gh/slawlor/ractor/commit/6d13f7769eb6ec2252db90f21722f4b6be47d821?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor).

| [Files with missing lines](https://app.codecov.io/gh/slawlor/ractor/pull/350?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor) | Patch % | Lines |
|---|---|---|
| [ractor/src/thread\_local/supervision\_tests.rs](https://app.codecov.io/gh/slawlor/ractor/pull/350?src=pr&el=tree&filepath=ractor%2Fsrc%2Fthread_local%2Fsupervision_tests.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor#diff-cmFjdG9yL3NyYy90aHJlYWRfbG9jYWwvc3VwZXJ2aXNpb25fdGVzdHMucnM=) | 85.56% | [27 Missing :warning: ](https://app.codecov.io/gh/slawlor/ractor/pull/350?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor) |
| [ractor/src/thread\_local.rs](https://app.codecov.io/gh/slawlor/ractor/pull/350?src=pr&el=tree&filepath=ractor%2Fsrc%2Fthread_local.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor#diff-cmFjdG9yL3NyYy90aHJlYWRfbG9jYWwucnM=) | 84.84% | [5 Missing :warning: ](https://app.codecov.io/gh/slawlor/ractor/pull/350?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #350      +/-   ##
==========================================
+ Coverage   80.60%   80.95%   +0.34%     
==========================================
  Files          69       70       +1     
  Lines       14156    14367     +211     
==========================================
+ Hits        11411    11631     +220     
+ Misses       2745     2736       -9     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/slawlor/ractor/pull/350?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/ractor/Cargo.toml b/ractor/Cargo.toml
index 5a2ae79..0edb86e 100644
--- a/ractor/Cargo.toml
+++ b/ractor/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ractor"
-version = "0.15.4"
+version = "0.15.5"
 authors = ["Sean Lawlor", "Evan Au", "Dillon George"]
 description = "A actor framework for Rust"
 documentation = "https://docs.rs/ractor"
diff --git a/ractor/src/actor.rs b/ractor/src/actor.rs
index 346ba27..27c7448 100644
--- a/ractor/src/actor.rs
+++ b/ractor/src/actor.rs
@@ -73,6 +73,8 @@ pub mod actor_ref;
 pub mod derived_actor;
 mod supervision;
 
+#[cfg(test)]
+mod supervision_tests;
 #[cfg(test)]
 mod tests;
 
diff --git a/ractor/src/thread_local.rs b/ractor/src/thread_local.rs
index 7d559ca..ad74471 100644
--- a/ractor/src/thread_local.rs
+++ b/ractor/src/thread_local.rs
@@ -12,7 +12,7 @@
 use std::future::Future;
 
 use crate::concurrency::JoinHandle;
-use crate::Actor as TraditionalActor;
+use crate::Actor as SendActor;
 use crate::ActorCell;
 use crate::ActorName;
 use crate::ActorProcessingErr;
@@ -25,15 +25,10 @@ use crate::SupervisionEvent;
 
 mod inner;
 #[cfg(test)]
+mod supervision_tests;
+#[cfg(test)]
 mod tests;
 
-/// Represents the state of an thread-local actor. Separate
-/// from the traditional [State] trait, this does NOT require
-/// that the state be [Send] and therefore does NOT need to be
-/// safe to send between threads.
-pub trait ThreadLocalState: std::any::Any + 'static {}
-impl<T: std::any::Any + 'static> ThreadLocalState for T {}
-
 /// [ThreadLocalActor] defines the behavior of an Actor. It specifies the
 /// Message type, State type, and all processing logic for the actor
 ///
@@ -56,13 +51,15 @@ pub trait ThreadLocalActor: Default + Sized + 'static {
     /// The message type for this actor
     type Msg: Message;
 
-    /// The type of state this actor manages internally
-    type State: ThreadLocalState;
+    /// The type of state this actor manages internally. This type
+    /// has no bound requirements, and needs to neither be [Send] nor
+    /// [Sync] when used in a [ThreadLocalActor] context.
+    type State;
 
     /// Initialization arguments. These must be [Send] as they are
     /// sent to the pinned thread in order to startup the actor.
-    /// However the actor's local `State` does NOT need to be
-    /// [Send] and neither does the actor instance.
+    /// However the actor's local [ThreadLocalActor::State] does
+    /// NOT need to be [Send] and neither does the actor instance.
     type Arguments: State;
 
     /// Invoked when an actor is being started by the system.
@@ -71,14 +68,14 @@ pub trait ThreadLocalActor: Default + Sized + 'static {
     /// performed here hence why it returns the initial state.
     ///
     /// Panics in `pre_start` do not invoke the
-    /// supervision strategy and the actor won't be started. [Actor]::`spawn`
+    /// supervision strategy and the actor won't be started. [ThreadLocalActor]::`spawn`
     /// will return an error to the caller
     ///
     /// * `myself` - A handle to the [ActorCell] representing this actor
     /// * `args` - Arguments that are passed in the spawning of the actor which might
     ///   be necessary to construct the initial state
     ///
-    /// Returns an initial [Actor::State] to bootstrap the actor
+    /// Returns an initial [ThreadLocalActor::State] to bootstrap the actor
     fn pre_start(
         &self,
         myself: ActorRef<Self::Msg>,
@@ -104,7 +101,7 @@ pub trait ThreadLocalActor: Default + Sized + 'static {
     }
 
     /// Invoked after an actor has been stopped to perform final cleanup. In the
-    /// event the actor is terminated with [Signal::Kill] or has self-panicked,
+    /// event the actor is terminated with `Signal::Kill` or has self-panicked,
     /// `post_stop` won't be called.
     ///
     /// Panics in `post_stop` follow the supervision strategy.
@@ -183,7 +180,7 @@ pub trait ThreadLocalActor: Default + Sized + 'static {
     ///
     /// * `name`: A name to give the actor. Useful for global referencing or debug printing
     /// * `handler` The implementation of Self
-    /// * `startup_args`: Arguments passed to the `pre_start` call of the [Actor] to facilitate startup and
+    /// * `startup_args`: Arguments passed to the `pre_start` call of the [ThreadLocalActor] to facilitate startup and
     ///   initial state creation
     ///
     /// Returns a [Ok((ActorRef, JoinHandle<()>))] upon successful start, denoting the actor reference
@@ -200,8 +197,8 @@ pub trait ThreadLocalActor: Default + Sized + 'static {
     ///
     /// * `name`: A name to give the actor. Useful for global referencing or debug printing
     /// * `handler` The implementation of Self
-    /// * `startup_args`: Arguments passed to the `pre_start` call of the [Actor] to facilitate startup and
-    ///   initial state creation
+    /// * `startup_args`: Arguments passed to the `pre_start` call of the [ThreadLocalActor] to
+    ///   facilitate startup and initial state creation
     /// * `supervisor`: The [ActorCell] which is to become the supervisor (parent) of this actor
     ///
     /// Returns a [Ok((ActorRef, JoinHandle<()>))] upon successful start, denoting the actor reference
@@ -224,18 +221,18 @@ pub trait ThreadLocalActor: Default + Sized + 'static {
 
 impl<T> ThreadLocalActor for T
 where
-    T: TraditionalActor + Default,
+    T: SendActor + Default,
 {
-    type Msg = <T as TraditionalActor>::Msg;
-    type State = <T as TraditionalActor>::State;
-    type Arguments = <T as TraditionalActor>::Arguments;
+    type Msg = <T as SendActor>::Msg;
+    type State = <T as SendActor>::State;
+    type Arguments = <T as SendActor>::Arguments;
 
     async fn pre_start(
         &self,
         myself: ActorRef<Self::Msg>,
         args: Self::Arguments,
     ) -> Result<Self::State, ActorProcessingErr> {
-        <Self as TraditionalActor>::pre_start(self, myself, args).await
+        <Self as SendActor>::pre_start(self, myself, args).await
     }
 
     async fn post_start(
@@ -243,7 +240,7 @@ where
         myself: ActorRef<Self::Msg>,
         state: &mut Self::State,
     ) -> Result<(), ActorProcessingErr> {
-        <Self as TraditionalActor>::post_start(self, myself, state).await
+        <Self as SendActor>::post_start(self, myself, state).await
     }
 
     async fn handle(
@@ -252,7 +249,7 @@ where
         message: Self::Msg,
         state: &mut Self::State,
     ) -> Result<(), ActorProcessingErr> {
-        <Self as TraditionalActor>::handle(self, myself, message, state).await
+        <Self as SendActor>::handle(self, myself, message, state).await
     }
 
     #[cfg(feature = "cluster")]
@@ -262,7 +259,7 @@ where
         message: crate::message::SerializedMessage,
         state: &mut Self::State,
     ) -> Result<(), ActorProcessingErr> {
-        <Self as TraditionalActor>::handle_serialized(self, myself, message, state).await
+        <Self as SendActor>::handle_serialized(self, myself, message, state).await
     }
 
     async fn handle_supervisor_evt(
@@ -271,7 +268,7 @@ where
         message: SupervisionEvent,
         state: &mut Self::State,
     ) -> Result<(), ActorProcessingErr> {
-        <Self as TraditionalActor>::handle_supervisor_evt(self, myself, message, state).await
+        <Self as SendActor>::handle_supervisor_evt(self, myself, message, state).await
     }
 }
 
@@ -282,6 +279,7 @@ struct SpawnArgs {
             + Send,
     >,
     reply: RpcReplyPort<JoinHandle<Result<JoinHandle<()>, SpawnErr>>>,
+    name: Option<String>,
 }
 
 /// The [ThreadLocalActorSpawner] is responsible for spawning [ThreadLocalActor] instances
@@ -320,10 +318,27 @@ impl ThreadLocalActorSpawner {
 
             // TODO (seanlawlor): Supported named spawn
             local.spawn_local(async move {
-                while let Some(SpawnArgs { builder, reply }) = recv.recv().await {
+                while let Some(SpawnArgs {
+                    builder,
+                    reply,
+                    name,
+                }) = recv.recv().await
+                {
                     let fut = builder();
-                    let handle = tokio::task::spawn_local(fut);
-                    _ = reply.send(handle);
+                    #[cfg(tokio_unstable)]
+                    {
+                        let mut builder = tokio::task::Builder::new();
+                        if let Some(name) = name {
+                            builder = builder.name(name);
+                        }
+                        builder.spawn_local(fut).expect("Tokio task spawn failed")
+                    }
+                    #[cfg(not(tokio_unstable))]
+                    {
+                        _ = name;
+                        let handle = tokio::task::spawn_local(fut);
+                        _ = reply.send(handle);
+                    }
                 }
                 // If the while loop returns, then all the LocalSpawner
                 // objects have been dropped.
@@ -345,11 +360,13 @@ impl ThreadLocalActorSpawner {
                     -> std::pin::Pin<Box<dyn Future<Output = Result<JoinHandle<()>, SpawnErr>>>>
                 + Send,
         >,
+        name: Option<String>,
     ) -> Result<JoinHandle<()>, SpawnErr> {
         let (tx, rx) = crate::concurrency::oneshot();
         let args = SpawnArgs {
             builder,
             reply: tx.into(),
+            name,
         };
 
         if self.send.send(args).is_err() {
@@ -362,3 +379,25 @@ impl ThreadLocalActorSpawner {
             .map_err(|joinerr| SpawnErr::StartupFailed(joinerr.into()))?
     }
 }
+
+impl ActorCell {
+    /// Spawn an actor of the given type as a thread-local child of this actor, automatically starting the actor.
+    /// This [ActorCell] becomes the supervisor of the child actor.
+    ///
+    /// * `name`: A name to give the actor. Useful for global referencing or debug printing
+    /// * `handler` The implementation of Self
+    /// * `startup_args`: Arguments passed to the `pre_start` call of the [Actor] to facilitate startup and
+    ///   initial state creation
+    ///
+    /// Returns a [Ok((ActorRef, JoinHandle<()>))] upon successful start, denoting the actor reference
+    /// along with the join handle which will complete when the actor terminates. Returns [Err(SpawnErr)] if
+    /// the actor failed to start
+    pub async fn spawn_local_linked<T: ThreadLocalActor>(
+        &self,
+        name: Option<String>,
+        startup_args: T::Arguments,
+        spawner: ThreadLocalActorSpawner,
+    ) -> Result<(ActorRef<T::Msg>, JoinHandle<()>), SpawnErr> {
+        T::spawn_linked(name, startup_args, self.clone(), spawner).await
+    }
+}
diff --git a/ractor/src/thread_local/inner.rs b/ractor/src/thread_local/inner.rs
index b09c5fc..d11aa85 100644
--- a/ractor/src/thread_local/inner.rs
+++ b/ractor/src/thread_local/inner.rs
@@ -251,6 +251,7 @@ impl<TActor: ThreadLocalActor> ThreadLocalActorRuntime<TActor> {
         actor_ref.set_status(ActorStatus::Starting);
 
         // Generate the ActorRef which will be returned
+        let spawn_name = name.clone();
         let myself_ret = actor_ref.clone();
 
         // run the processing loop, backgrounding the work
@@ -311,7 +312,7 @@ impl<TActor: ThreadLocalActor> ThreadLocalActorRuntime<TActor> {
             .boxed_local()
         });
         let handle = spawner
-            .spawn(builder)
+            .spawn(builder, spawn_name)
             .await
             .map_err(|e| SpawnErr::StartupFailed(e.into()))?;
 
diff --git a/ractor_cluster/Cargo.toml b/ractor_cluster/Cargo.toml
index 67c7355..1a61390 100644
--- a/ractor_cluster/Cargo.toml
+++ b/ractor_cluster/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ractor_cluster"
-version = "0.15.4"
+version = "0.15.5"
 authors = ["Sean Lawlor <slawlor>"]
 description = "Distributed cluster environment of Ractor actors"
 documentation = "https://docs.rs/ractor"
diff --git a/ractor_cluster_derive/Cargo.toml b/ractor_cluster_derive/Cargo.toml
index 9af3a01..ac2301e 100644
--- a/ractor_cluster_derive/Cargo.toml
+++ b/ractor_cluster_derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "ractor_cluster_derive"
-version = "0.15.4"
+version = "0.15.5"
 authors = ["Sean Lawlor <slawlor>"]
 description = "Derives for ractor_cluster"
 license = "MIT"


Test patch:
diff --git a/ractor/src/actor/tests/supervisor.rs b/ractor/src/actor/supervision_tests.rs
similarity index 99%
rename from ractor/src/actor/tests/supervisor.rs
rename to ractor/src/actor/supervision_tests.rs
index 8894aa8..272043f 100644
--- a/ractor/src/actor/tests/supervisor.rs
+++ b/ractor/src/actor/supervision_tests.rs
@@ -1112,9 +1112,9 @@ async fn test_supervisor_captures_dead_childs_state() {
         }
     }
 
-    let mut temp_state = super::super::messages::BoxedState::new(123u64);
+    let mut temp_state = super::messages::BoxedState::new(123u64);
     assert_eq!(Err(BoxedDowncastErr), temp_state.take::<u32>());
-    let mut temp_state = super::super::messages::BoxedState::new(123u64);
+    let mut temp_state = super::messages::BoxedState::new(123u64);
     assert_eq!(Ok(123u64), temp_state.take::<u64>());
     assert_eq!(Err(BoxedDowncastErr), temp_state.take::<u64>());
 
diff --git a/ractor/src/actor/tests.rs b/ractor/src/actor/tests.rs
index 08e32ec..ad1273c 100644
--- a/ractor/src/actor/tests.rs
+++ b/ractor/src/actor/tests.rs
@@ -21,8 +21,6 @@ use crate::{
     Actor, ActorCell, ActorProcessingErr, ActorRef, ActorStatus, SpawnErr, SupervisionEvent,
 };
 
-mod supervisor;
-
 struct EmptyMessage;
 #[cfg(feature = "cluster")]
 impl crate::Message for EmptyMessage {}
diff --git a/ractor/src/thread_local/supervision_tests.rs b/ractor/src/thread_local/supervision_tests.rs
new file mode 100644
index 0000000..daffa16
--- /dev/null
+++ b/ractor/src/thread_local/supervision_tests.rs
@@ -0,0 +1,302 @@
+// Copyright (c) Sean Lawlor
+//
+// This source code is licensed under both the MIT license found in the
+// LICENSE-MIT file in the root directory of this source tree.
+
+//! Supervisor tests
+
+use std::sync::{
+    atomic::{AtomicU64, Ordering},
+    Arc,
+};
+
+use once_cell::sync::OnceCell;
+
+use crate::thread_local::ThreadLocalActorSpawner;
+use crate::{thread_local::ThreadLocalActor, ActorProcessingErr};
+
+use crate::{Actor, ActorCell, ActorRef, SupervisionEvent};
+
+static LOCAL_SPAWNER: OnceCell<ThreadLocalActorSpawner> = OnceCell::new();
+
+fn get_spawner() -> ThreadLocalActorSpawner {
+    LOCAL_SPAWNER
+        .get_or_init(ThreadLocalActorSpawner::new)
+        .clone()
+}
+
+#[crate::concurrency::test]
+#[tracing_test::traced_test]
+#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+async fn test_thread_local_child() {
+    use crate::thread_local::ThreadLocalActor;
+
+    #[derive(Default)]
+    struct Child;
+
+    struct Supervisor {
+        flag: Arc<AtomicU64>,
+    }
+
+    impl ThreadLocalActor for Child {
+        type Msg = ();
+        type State = ();
+        type Arguments = ();
+        async fn pre_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _: (),
+        ) -> Result<Self::State, ActorProcessingErr> {
+            Ok(())
+        }
+        async fn post_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            panic!("Boom");
+        }
+    }
+
+    #[cfg_attr(feature = "async-trait", crate::async_trait)]
+    impl Actor for Supervisor {
+        type Msg = ();
+        type State = ();
+        type Arguments = ();
+        async fn pre_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _: (),
+        ) -> Result<Self::State, ActorProcessingErr> {
+            Ok(())
+        }
+        async fn handle(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _message: Self::Msg,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            Ok(())
+        }
+
+        async fn handle_supervisor_evt(
+            &self,
+            this_actor: ActorRef<Self::Msg>,
+            message: SupervisionEvent,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            println!("Supervisor event received {message:?}");
+
+            // check that the panic was captured
+            if let SupervisionEvent::ActorFailed(dead_actor, _panic_msg) = message {
+                self.flag.store(dead_actor.get_id().pid(), Ordering::SeqCst);
+                this_actor.stop(None);
+            }
+            Ok(())
+        }
+    }
+
+    let flag = Arc::new(AtomicU64::new(0));
+
+    let (supervisor_ref, s_handle) = Actor::spawn(None, Supervisor { flag: flag.clone() }, ())
+        .await
+        .expect("Supervisor panicked on startup");
+
+    let (child_ref, c_handle) = supervisor_ref
+        .spawn_local_linked::<Child>(None, (), get_spawner())
+        .await
+        .expect("Child panicked on startup");
+
+    let maybe_sup = child_ref.try_get_supervisor();
+    assert!(maybe_sup.is_some());
+    assert_eq!(maybe_sup.map(|a| a.get_id()), Some(supervisor_ref.get_id()));
+
+    let (_, _) = tokio::join!(s_handle, c_handle);
+
+    assert_eq!(child_ref.get_id().pid(), flag.load(Ordering::SeqCst));
+
+    // supervisor relationship cleaned up correctly
+    assert_eq!(0, supervisor_ref.get_num_children());
+}
+
+#[crate::concurrency::test]
+#[tracing_test::traced_test]
+async fn test_thread_local_supervisor() {
+    struct Child;
+    #[derive(Default)]
+    struct Supervisor;
+
+    #[cfg_attr(feature = "async-trait", crate::async_trait)]
+    impl Actor for Child {
+        type Msg = ();
+        type State = ();
+        type Arguments = ();
+        async fn pre_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _: (),
+        ) -> Result<Self::State, ActorProcessingErr> {
+            Ok(())
+        }
+        async fn post_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            Err(From::from("boom"))
+        }
+    }
+
+    impl ThreadLocalActor for Supervisor {
+        type Msg = ();
+        type State = Arc<AtomicU64>;
+        type Arguments = Arc<AtomicU64>;
+        async fn pre_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            args: Arc<AtomicU64>,
+        ) -> Result<Self::State, ActorProcessingErr> {
+            Ok(args)
+        }
+        async fn handle(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _message: Self::Msg,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            Ok(())
+        }
+
+        async fn handle_supervisor_evt(
+            &self,
+            this_actor: ActorRef<Self::Msg>,
+            message: SupervisionEvent,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            println!("Supervisor event received {message:?}");
+
+            // check that the panic was captured
+            if let SupervisionEvent::ActorFailed(dead_actor, _panic_msg) = message {
+                _state.store(dead_actor.get_id().pid(), Ordering::SeqCst);
+                this_actor.stop(None);
+            }
+            Ok(())
+        }
+    }
+
+    let flag = Arc::new(AtomicU64::new(0));
+
+    let (supervisor_ref, s_handle) = Supervisor::spawn(None, flag.clone(), get_spawner())
+        .await
+        .expect("Supervisor panicked on startup");
+
+    let supervisor_cell: ActorCell = supervisor_ref.clone().into();
+
+    let (child_ref, c_handle) = Actor::spawn_linked(None, Child, (), supervisor_cell)
+        .await
+        .expect("Child panicked on startup");
+
+    let (_, _) = tokio::join!(s_handle, c_handle);
+
+    assert_eq!(child_ref.get_id().pid(), flag.load(Ordering::SeqCst));
+
+    // supervisor relationship cleaned up correctly
+    assert_eq!(0, supervisor_ref.get_num_children());
+}
+
+#[crate::concurrency::test]
+#[tracing_test::traced_test]
+async fn test_thread_local_child_panic_handle() {
+    #[derive(Default)]
+    struct Child;
+    struct Supervisor {
+        flag: Arc<AtomicU64>,
+    }
+
+    impl ThreadLocalActor for Child {
+        type Msg = ();
+        type State = ();
+        type Arguments = ();
+        async fn pre_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _: (),
+        ) -> Result<Self::State, ActorProcessingErr> {
+            Ok(())
+        }
+        async fn handle(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _message: Self::Msg,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            panic!("Boom");
+        }
+    }
+
+    #[cfg_attr(feature = "async-trait", crate::async_trait)]
+    impl Actor for Supervisor {
+        type Msg = ();
+        type State = ();
+        type Arguments = ();
+        async fn pre_start(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _: (),
+        ) -> Result<Self::State, ActorProcessingErr> {
+            Ok(())
+        }
+        async fn handle(
+            &self,
+            _this_actor: ActorRef<Self::Msg>,
+            _message: Self::Msg,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            Ok(())
+        }
+
+        async fn handle_supervisor_evt(
+            &self,
+            this_actor: ActorRef<Self::Msg>,
+            message: SupervisionEvent,
+            _state: &mut Self::State,
+        ) -> Result<(), ActorProcessingErr> {
+            println!("Supervisor event received {message:?}");
+
+            // check that the panic was captured
+            if let SupervisionEvent::ActorFailed(dead_actor, _panic_msg) = message {
+                self.flag.store(dead_actor.get_id().pid(), Ordering::SeqCst);
+                this_actor.stop(None);
+            }
+            Ok(())
+        }
+    }
+
+    let flag = Arc::new(AtomicU64::new(0));
+
+    let (supervisor_ref, s_handle) = Actor::spawn(None, Supervisor { flag: flag.clone() }, ())
+        .await
+        .expect("Supervisor panicked on startup");
+
+    let supervisor_cell: ActorCell = supervisor_ref.clone().into();
+
+    let (child_ref, c_handle) = supervisor_cell
+        .spawn_local_linked::<Child>(None, (), get_spawner())
+        .await
+        .expect("Child panicked on startup");
+
+    // check that the supervision is wired up correctly
+    assert_eq!(1, supervisor_ref.get_num_children());
+    assert_eq!(1, child_ref.get_num_parents());
+
+    // trigger the child failure
+    child_ref.send_message(()).expect("Failed to send message");
+
+    let _ = s_handle.await;
+    let _ = c_handle.await;
+
+    assert_eq!(child_ref.get_id().pid(), flag.load(Ordering::SeqCst));
+
+    // supervisor relationship cleaned up correctly
+    assert_eq!(0, supervisor_ref.get_num_children());
+}