Repository: slawlor/ractor
Pull request #329 (2025-02-05 15:17:38)

Add utility functions for spawning actors
Utilities added on

* The ActorRef for spawning children (instance method of `spawn_linked`) 
* The crate root for spawning unmanaged actors

Tests updated to cover functionality.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/slawlor/ractor/pull/329?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 81.64%. Comparing base [(`f9ccc48`)](https://app.codecov.io/gh/slawlor/ractor/commit/f9ccc48d16dcddb9ff87f55987e2089bfb9fbb47?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sean+Lawlor) to head [(`bebc427`)](https://app.codecov.io/gh/slawlor/ractor/commit/bebc4277a89fb0b90d8298474dece6be1a9183d6?dropdown=coverage&el=desc&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     #329      +/-   ##
==========================================
+ Coverage   81.61%   81.64%   +0.03%     
==========================================
  Files          61       62       +1     
  Lines       12298    12319      +21     
==========================================
+ Hits        10037    10058      +21     
  Misses       2261     2261              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/slawlor/ractor/pull/329?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).


Patch:
diff --git a/ractor/src/actor/actor_cell.rs b/ractor/src/actor/actor_cell.rs
index bae9d47..6d76f8a 100644
--- a/ractor/src/actor/actor_cell.rs
+++ b/ractor/src/actor/actor_cell.rs
@@ -19,13 +19,13 @@ use super::actor_properties::MuxedMessage;
 use super::messages::{Signal, StopMessage};
 use super::SupervisionEvent;
 use crate::actor::actor_properties::ActorProperties;
-use crate::concurrency::{MpscUnboundedReceiver as InputPortReceiver, OneshotReceiver};
+use crate::concurrency::{JoinHandle, MpscUnboundedReceiver as InputPortReceiver, OneshotReceiver};
 use crate::errors::MessagingErr;
 #[cfg(feature = "cluster")]
 use crate::message::SerializedMessage;
-use crate::RactorErr;
 use crate::{Actor, ActorName, SpawnErr};
 use crate::{ActorId, Message};
+use crate::{ActorRef, RactorErr};
 
 /// [ActorStatus] represents the status of an actor's lifecycle
 #[derive(Debug, Clone, Eq, PartialEq, Copy, PartialOrd, Ord)]
@@ -655,6 +655,26 @@ impl ActorCell {
         }
     }
 
+    /// Spawn an actor of the given type as a 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_linked<T: Actor>(
+        &self,
+        name: Option<String>,
+        handler: T,
+        startup_args: T::Arguments,
+    ) -> Result<(ActorRef<T::Msg>, JoinHandle<()>), SpawnErr> {
+        crate::actor::ActorRuntime::spawn_linked(name, handler, startup_args, self.clone()).await
+    }
+
     // ================== Test Utilities ================== //
 
     #[cfg(test)]
diff --git a/ractor/src/lib.rs b/ractor/src/lib.rs
index cc69bc7..9d5670b 100644
--- a/ractor/src/lib.rs
+++ b/ractor/src/lib.rs
@@ -179,6 +179,7 @@ pub mod rpc;
 pub mod serialization;
 pub mod time;
 
+use concurrency::JoinHandle;
 #[cfg(not(feature = "async-trait"))]
 use strum as _;
 
@@ -228,3 +229,33 @@ pub type ScopeName = String;
 /// to send between threads (same bounds as a [Message])
 pub trait State: std::any::Any + Send + 'static {}
 impl<T: std::any::Any + Send + 'static> State for T {}
+
+// ======================== Helper Functionality ======================== //
+
+/// Perform a background-spawn of an actor. This is a utility wrapper over [Actor::spawn] which drops
+/// the [crate::concurrency::JoinHandle] for convenience and assumes the actor implementation implements
+/// [Default].
+///
+/// * `args` - The arguments to start the actor
+///
+/// Returns [Ok((ActorRef, JoinHandle<()>))] upon successful actor startup, [Err(SpawnErr)] otherwise
+pub async fn spawn<T: Actor + Default>(
+    args: T::Arguments,
+) -> Result<(ActorRef<T::Msg>, JoinHandle<()>), SpawnErr> {
+    T::spawn(None, T::default(), args).await
+}
+
+/// Perform a background-spawn of an actor with the provided name. This is a utility wrapper
+/// over [Actor::spawn] which drops the [crate::concurrency::JoinHandle] for convenience and
+/// assumes the actor implementation implements [Default].
+///
+/// * `name` - The name for the actor to spawn
+/// * `args` - The arguments to start the actor
+///
+/// Returns [Ok((ActorRef, JoinHandle<()>))] upon successful actor startup, [Err(SpawnErr)] otherwise
+pub async fn spawn_named<T: Actor + Default>(
+    name: crate::ActorName,
+    args: T::Arguments,
+) -> Result<(ActorRef<T::Msg>, JoinHandle<()>), SpawnErr> {
+    T::spawn(Some(name), T::default(), args).await
+}


Test patch:
diff --git a/ractor/src/actor/tests/mod.rs b/ractor/src/actor/tests/mod.rs
index 225df0e..0697e80 100644
--- a/ractor/src/actor/tests/mod.rs
+++ b/ractor/src/actor/tests/mod.rs
@@ -30,6 +30,7 @@ impl crate::Message for EmptyMessage {}
 #[crate::concurrency::test]
 #[tracing_test::traced_test]
 async fn test_panic_on_start_captured() {
+    #[derive(Default)]
     struct TestActor;
 
     #[cfg_attr(feature = "async-trait", crate::async_trait)]
@@ -47,7 +48,7 @@ async fn test_panic_on_start_captured() {
         }
     }
 
-    let actor_output = Actor::spawn(None, TestActor, ()).await;
+    let actor_output = crate::spawn::<TestActor>(()).await;
     assert!(matches!(actor_output, Err(SpawnErr::StartupFailed(_))));
 }
 
@@ -364,6 +365,7 @@ async fn test_sending_message_to_invalid_actor_type() {
 #[crate::concurrency::test]
 #[tracing_test::traced_test]
 async fn test_sending_message_to_dead_actor() {
+    #[derive(Default)]
     struct TestActor;
 
     #[cfg_attr(feature = "async-trait", crate::async_trait)]
@@ -381,13 +383,15 @@ async fn test_sending_message_to_dead_actor() {
         }
     }
 
-    let (actor, handle) = Actor::spawn(None, TestActor, ())
+    let (actor, _) = crate::spawn::<TestActor>(())
         .await
         .expect("Actor failed to start");
 
     // Stop the actor and wait for it to die
-    actor.stop(None);
-    handle.await.unwrap();
+    actor
+        .stop_and_wait(None, None)
+        .await
+        .expect("Failed to stop");
 
     // assert that if we send a message, it doesn't transmit since
     // the receiver is dropped
@@ -968,6 +972,7 @@ fn returns_actor_references() {
 #[crate::concurrency::test]
 #[tracing_test::traced_test]
 async fn actor_failing_in_spawn_err_doesnt_poison_registries() {
+    #[derive(Default)]
     struct Test;
 
     #[cfg_attr(feature = "async-trait", crate::async_trait)]
@@ -981,6 +986,7 @@ async fn actor_failing_in_spawn_err_doesnt_poison_registries() {
         }
     }
 
+    #[derive(Default)]
     struct Test2;
 
     #[cfg_attr(feature = "async-trait", crate::async_trait)]
@@ -994,7 +1000,7 @@ async fn actor_failing_in_spawn_err_doesnt_poison_registries() {
         }
     }
 
-    let a = Actor::spawn(Some("test".to_owned()), Test, ()).await;
+    let a = crate::spawn_named::<Test>("test".to_owned(), ()).await;
     assert!(a.is_err());
     drop(a);
 
diff --git a/ractor/src/actor/tests/supervisor.rs b/ractor/src/actor/tests/supervisor.rs
index b5723f0..246f8f8 100644
--- a/ractor/src/actor/tests/supervisor.rs
+++ b/ractor/src/actor/tests/supervisor.rs
@@ -87,9 +87,8 @@ async fn test_supervision_panic_in_post_startup() {
         .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)
+    let (child_ref, c_handle) = supervisor_ref
+        .spawn_linked(None, Child, ())
         .await
         .expect("Child panicked on startup");
 
@@ -1088,9 +1087,8 @@ async fn test_supervisor_captures_dead_childs_state() {
         .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)
+    let (child_ref, c_handle) = supervisor_ref
+        .spawn_linked(None, Child, ())
         .await
         .expect("Child panicked on startup");
 
diff --git a/ractor/src/registry/tests.rs b/ractor/src/registry/tests.rs
index 9e0857f..5a5ae86 100644
--- a/ractor/src/registry/tests.rs
+++ b/ractor/src/registry/tests.rs
@@ -12,6 +12,7 @@ use crate::{Actor, ActorProcessingErr, SpawnErr};
 #[crate::concurrency::test]
 #[tracing_test::traced_test]
 async fn test_basic_registation() {
+    #[derive(Default)]
     struct EmptyActor;
 
     #[cfg_attr(feature = "async-trait", crate::async_trait)]
@@ -29,7 +30,7 @@ async fn test_basic_registation() {
         }
     }
 
-    let (actor, handle) = Actor::spawn(Some("my_actor".to_string()), EmptyActor, ())
+    let (actor, _) = crate::spawn_named::<EmptyActor>("my_actor".to_string(), ())
         .await
         .expect("Actor failed to start");
 
@@ -38,8 +39,10 @@ async fn test_basic_registation() {
     // Coverage for Issue #70
     assert!(crate::ActorRef::<()>::where_is("my_actor".to_string()).is_some());
 
-    actor.stop(None);
-    handle.await.expect("Failed to clean stop the actor");
+    actor
+        .stop_and_wait(None, None)
+        .await
+        .expect("Failed to wait for stop");
 }
 
 #[crate::concurrency::test]