瀏覽代碼

Wrote tests to verify delivery of exit notifications.

Matthew Carr 1 年之前
父節點
當前提交
875060adac
共有 4 個文件被更改,包括 399 次插入114 次删除
  1. 323 60
      crates/btrun/src/kernel.rs
  2. 66 47
      crates/btrun/src/lib.rs
  3. 4 4
      crates/btrun/src/model.rs
  4. 6 3
      crates/btrun/tests/runtime_tests.rs

+ 323 - 60
crates/btrun/src/kernel.rs

@@ -1,76 +1,56 @@
 //! This module contains the code necessary to run and supervise actors.
 
-use std::{future::Future, pin::Pin};
+use std::time::Duration;
 
 use log::{debug, error, warn};
-use tokio::{
-    select,
-    sync::{mpsc, oneshot},
-    task::{AbortHandle, JoinSet},
-};
+use tokio::time::timeout;
 
-use crate::{ActorExit, ActorFault, ActorId, ControlMsg, Runtime};
+use crate::{ActorExit, ActorFault, ActorId, ControlMsg, Runtime, RuntimeError};
 
+/// The type that spawned actors complete with.
+///
+/// It's primary responsibility is conveying the [ActorId] to the [kernel] regardless of whether
+/// an actor exited normally, with an error, or panicked.
 pub(super) type FaultResult = std::result::Result<ActorId, ActorFault>;
-pub(super) type BoxedFuture = Pin<Box<dyn Send + Future<Output = FaultResult>>>;
-
-pub struct SpawnReq {
-    task: BoxedFuture,
-    sender: oneshot::Sender<AbortHandle>,
-}
-
-impl SpawnReq {
-    pub(super) fn new<Fut>(task: Fut) -> (Self, oneshot::Receiver<AbortHandle>)
-    where
-        Fut: 'static + Send + Future<Output = FaultResult>,
-    {
-        let (sender, receiver) = oneshot::channel();
-        let req = Self {
-            task: Box::pin(task),
-            sender,
-        };
-        (req, receiver)
-    }
-}
 
 /// The kernel is responsible for spawning and supervising actors.
-pub(super) async fn kernel(runtime: &Runtime, mut tasks: mpsc::Receiver<SpawnReq>) {
-    let mut running = JoinSet::<FaultResult>::new();
+pub(super) async fn kernel(runtime: &Runtime) {
+    const EXIT_WAIT: Duration = Duration::from_millis(10);
 
     loop {
-        select! {
-            option = tasks.recv() => {
-                match option {
-                    Some(SpawnReq { task, sender }) => {
-                        let handle = running.spawn(task);
-                        if sender.send(handle).is_err() {
-                            error!("SpawnReq sender dropped the channel. Aborting actor.");
-                        }
-                    }
-                    None => panic!("The Runtime was dropped!")
-                };
+        let option = {
+            let mut tasks = runtime.tasks.lock().await;
+            if let Ok(option) = timeout(EXIT_WAIT, tasks.join_next()).await {
+                option
+            } else {
+                // Timed out waiting for a task to exit, releasing lock to prevent starvation of
+                // other tasks.
+                continue;
             }
-            Some(result) = running.join_next() => {
-                match result {
-                    Ok(actor_result) => {
-                        handle_actor_exit(actor_result, runtime).await;
-                    }
-                    Err(join_error) => {
-                        match join_error.try_into_panic() {
-                            Ok(panic_obj) => {
-                                if let Some(message) = panic_obj.downcast_ref::<String>() {
-                                    error!("An actor panic was uncaught: {message}");
-                                } else {
-                                    error!("An actor panic was uncaught.");
-                                }
-                            }
-                            Err(join_error) => {
-                                debug!("Actor was aborted: {join_error}");
+        };
+        if let Some(join_result) = option {
+            match join_result {
+                Ok(actor_result) => {
+                    handle_actor_exit(actor_result, runtime).await;
+                }
+                Err(join_error) => {
+                    match join_error.try_into_panic() {
+                        Ok(panic_obj) => {
+                            if let Some(message) = panic_obj.downcast_ref::<String>() {
+                                error!("An actor panic was uncaught: {message}");
+                            } else {
+                                error!("An actor panic was uncaught.");
                             }
-                        };
-                    }
+                        }
+                        Err(join_error) => {
+                            debug!("Actor was aborted: {join_error}");
+                        }
+                    };
                 }
             }
+        } else {
+            // The join set is currently empty. We just loop again.
+            continue;
         }
     }
 }
@@ -81,7 +61,7 @@ async fn handle_actor_exit(actor_result: FaultResult, runtime: &Runtime) {
     let (actor_id, actor_exit) = match actor_result {
         Ok(actor_id) => {
             debug!("Actor {actor_id} exited normally.");
-            (actor_id, ActorExit::Ok)
+            (actor_id, ActorExit::Ok(actor_id))
         }
         Err(err) => {
             error!("{err}");
@@ -89,6 +69,7 @@ async fn handle_actor_exit(actor_result: FaultResult, runtime: &Runtime) {
         }
     };
 
+    // Take ownership of the exited actor's handle.
     let mut handle = {
         let mut handles = runtime.handles.write().await;
         if let Some(handle) = handles.remove(&actor_id) {
@@ -99,6 +80,7 @@ async fn handle_actor_exit(actor_result: FaultResult, runtime: &Runtime) {
         }
     };
 
+    // Send notification to its owner.
     if let Some(owner) = handle.take_owner() {
         let handle_name = handle.name();
         let msg = ControlMsg::OwnedExited {
@@ -107,12 +89,19 @@ async fn handle_actor_exit(actor_result: FaultResult, runtime: &Runtime) {
         };
         let result = runtime.send_control(owner.clone(), msg).await;
         if let Err(err) = result {
-            error!("Failed to notify owner {owner} that {handle_name} exited: {err}");
+            if let Some(RuntimeError::BadActorName(_)) = err.downcast_ref::<RuntimeError>() {
+                // There's nothing to do in this case because the owner has already exited. We just
+                // happened to handle the exit of one of its owned actors first.
+            } else {
+                error!("Failed to notify owner {owner} that {handle_name} exited: {err}");
+            }
         }
     }
 
+    // Notify all of its owned actors.
     let handle_name = handle.name().clone();
     for owned in handle.owns_mut().drain(0..) {
+        debug!("sending owner exit notification to {owned}");
         let msg = ControlMsg::OwnerExited(actor_exit.clone());
         let result = runtime.send_control(owned.clone(), msg).await;
         if let Err(err) = result {
@@ -120,3 +109,277 @@ async fn handle_actor_exit(actor_result: FaultResult, runtime: &Runtime) {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::{future::Future, sync::Arc};
+
+    use btlib::bterr;
+    use tokio::sync::oneshot;
+
+    use super::*;
+
+    use crate::{
+        tests::{EchoMsg, ASYNC_RT, RUNTIME},
+        ActorError, ActorErrorCommon, ActorName, ActorResult, Envelope, Mailbox, Named, TransKind,
+    };
+
+    #[derive(Debug)]
+    struct ExitTestResult {
+        expected_name: ActorName,
+        actual_name: ActorName,
+        actual_exit: ActorExit,
+    }
+
+    /// Actor used to test the receipt of a notification when one of its owned actors exits.
+    async fn spawn_owning_exit_server<F, Fut>(
+        runtime: &'static Runtime,
+        client: F,
+        sender: oneshot::Sender<ExitTestResult>,
+    ) -> ActorName
+    where
+        Fut: 'static + Send + Future<Output = ActorResult>,
+        F: 'static + Send + FnOnce(Mailbox<EchoMsg>, ActorId, &'static Runtime) -> Fut,
+    {
+        let server = move |mut mailbox: Mailbox<EchoMsg>, actor_id, runtime: &'static Runtime| async move {
+            let owner = runtime.actor_name(actor_id);
+            let mut expected_name = Some(runtime.spawn(Some(owner), client).await.unwrap());
+            let mut sender = Some(sender);
+            while let Some(envelope) = mailbox.recv().await {
+                match envelope {
+                    Envelope::Control(msg) => match msg {
+                        ControlMsg::OwnedExited {
+                            name: actual_name,
+                            exit,
+                        } => {
+                            let test_result = ExitTestResult {
+                                actual_name,
+                                expected_name: expected_name.take().unwrap(),
+                                actual_exit: exit,
+                            };
+                            sender.take().unwrap().send(test_result).unwrap();
+                        }
+                        msg => panic!("Unexpected control message: {}", msg.name()),
+                    },
+                    envelope => panic!("Unexpected envelope type: {}", envelope.name()),
+                }
+            }
+            ActorResult::Ok(actor_id)
+        };
+        runtime.spawn(None, server).await.unwrap()
+    }
+
+    #[test]
+    fn owner_notification_when_owned_exit_ok() {
+        ASYNC_RT.block_on(async {
+            let client = |_: Mailbox<EchoMsg>, actor_id, _| async move {
+                ActorResult::Ok(actor_id)
+            };
+            let (sender, receiver) = oneshot::channel();
+
+            spawn_owning_exit_server(&RUNTIME, client, sender).await;
+            let test_result = timeout(Duration::from_millis(500), receiver).await.unwrap().unwrap();
+
+            assert_eq!(test_result.expected_name, test_result.actual_name);
+            let expected_id = test_result.expected_name.act_id();
+            assert!(matches!(test_result.actual_exit, ActorExit::Ok(actor_id) if actor_id == expected_id));
+        });
+    }
+
+    #[test]
+    fn owner_notification_when_owned_exit_err() {
+        ASYNC_RT.block_on(async {
+            const EXPECTED_ERR: &str = "Expected error text.";
+            let expected_actor_impl = Arc::new(String::from("notification_client"));
+            let expected_state = Arc::new(String::from("Init"));
+            let expected_message = Arc::new(String::from("None"));
+            const EXPECTED_KIND: TransKind = TransKind::Receive;
+
+            let client = {
+                let expected_actor_impl = expected_actor_impl.clone();
+                let expected_state = expected_state.clone();
+                let expected_message = expected_message.clone();
+                move |_: Mailbox<EchoMsg>, actor_id, _| async move {
+                    ActorResult::Err(ActorError::new(
+                        bterr!(EXPECTED_ERR),
+                        ActorErrorCommon {
+                            actor_id,
+                            actor_impl: expected_actor_impl,
+                            state: expected_state,
+                            message: expected_message,
+                            kind: EXPECTED_KIND,
+                        },
+                    ))
+                }
+            };
+            let (sender, receiver) = oneshot::channel();
+
+            spawn_owning_exit_server(&RUNTIME, client, sender).await;
+            let test_result = timeout(Duration::from_millis(500), receiver)
+                .await
+                .unwrap()
+                .unwrap();
+
+            assert_eq!(test_result.expected_name, test_result.actual_name);
+            let expected_id = test_result.expected_name.act_id();
+            if let ActorExit::Error(actual_error) = test_result.actual_exit {
+                assert_eq!(expected_id, actual_error.actor_id());
+                assert_eq!(EXPECTED_ERR, actual_error.err().as_ref());
+                assert_eq!(&expected_actor_impl, actual_error.actor_impl());
+                assert_eq!(&expected_state, actual_error.state());
+                assert_eq!(&expected_message, actual_error.message());
+                assert_eq!(EXPECTED_KIND, actual_error.kind());
+            } else {
+                panic!("Actor exit wasn't the right variant.");
+            }
+        });
+    }
+
+    #[test]
+    fn owner_notification_when_owned_exit_panic() {
+        ASYNC_RT.block_on(async {
+            const EXPECTED_ERR: &str = "Expected panic text.";
+            let client = |_: Mailbox<EchoMsg>, _, _| async move {
+                panic!("{EXPECTED_ERR}");
+            };
+            let (sender, receiver) = oneshot::channel();
+
+            spawn_owning_exit_server(&RUNTIME, client, sender).await;
+            let test_result = timeout(Duration::from_millis(500), receiver)
+                .await
+                .unwrap()
+                .unwrap();
+
+            assert_eq!(test_result.expected_name, test_result.actual_name);
+            let expected_id = test_result.expected_name.act_id();
+            if let ActorExit::Panic(actual_panic) = test_result.actual_exit {
+                assert_eq!(expected_id, actual_panic.actor_id());
+                assert_eq!(EXPECTED_ERR, actual_panic.err().unwrap());
+            } else {
+                panic!("Actor exit wasn't the right variant.");
+            }
+        });
+    }
+
+    /// Actor used to test the receipt of a notification when its owner exits.
+    async fn spawn_owned_exit_server(
+        runtime: &'static Runtime,
+        owner: ActorName,
+        sender: oneshot::Sender<ActorExit>,
+    ) {
+        let owned = |mut mailbox: Mailbox<EchoMsg>, owned_id, _| async move {
+            let mut sender = Some(sender);
+            while let Some(envelope) = mailbox.recv().await {
+                match envelope {
+                    Envelope::Control(msg) => match msg {
+                        ControlMsg::OwnerExited(actor_exit) => {
+                            sender.take().unwrap().send(actor_exit).unwrap();
+                            break;
+                        }
+                        msg => panic!("Unexpected control message: {}", msg.name()),
+                    },
+                    envelope => panic!("Unexpected envelope kind: {}", envelope.name()),
+                }
+            }
+            ActorResult::Ok(owned_id)
+        };
+
+        runtime.spawn(Some(owner), owned).await.unwrap();
+    }
+
+    #[test]
+    fn owned_notification_when_owner_exit_ok() {
+        ASYNC_RT.block_on(async {
+            let (sender, receiver) = oneshot::channel();
+            let owner = |_: Mailbox<EchoMsg>, owner_id, runtime: &'static Runtime| async move {
+                let owner_name = runtime.actor_name(owner_id);
+                spawn_owned_exit_server(runtime, owner_name, sender).await;
+                ActorResult::Ok(owner_id)
+            };
+
+            let expected_name = RUNTIME.spawn(None, owner).await.unwrap();
+
+            let actual = timeout(Duration::from_millis(500), receiver)
+                .await
+                .unwrap()
+                .unwrap();
+            let expected_id = expected_name.act_id();
+            assert!(matches!(actual, ActorExit::Ok(actual_id) if actual_id == expected_id));
+        });
+    }
+
+    #[test]
+    fn owned_notification_when_owner_exit_error() {
+        ASYNC_RT.block_on(async {
+            const EXPECTED_ERR: &str = "Expected error text.";
+            let expected_actor_impl = Arc::new(String::from("notification_client"));
+            let expected_state = Arc::new(String::from("Init"));
+            let expected_message = Arc::new(String::from("None"));
+            const EXPECTED_KIND: TransKind = TransKind::Receive;
+
+            let (sender, receiver) = oneshot::channel();
+            let owner = {
+                let expected_actor_impl = expected_actor_impl.clone();
+                let expected_state = expected_state.clone();
+                let expected_message = expected_message.clone();
+                move |_: Mailbox<EchoMsg>, owner_id, runtime: &'static Runtime| async move {
+                    let owner_name = runtime.actor_name(owner_id);
+                    spawn_owned_exit_server(runtime, owner_name, sender).await;
+                    ActorResult::Err(ActorError::new(
+                        bterr!(EXPECTED_ERR),
+                        ActorErrorCommon {
+                            actor_id: owner_id,
+                            actor_impl: expected_actor_impl,
+                            state: expected_state,
+                            message: expected_message,
+                            kind: EXPECTED_KIND,
+                        },
+                    ))
+                }
+            };
+
+            let expected_name = RUNTIME.spawn(None, owner).await.unwrap();
+
+            let actual = timeout(Duration::from_millis(500), receiver)
+                .await
+                .unwrap()
+                .unwrap();
+            if let ActorExit::Error(actual_error) = actual {
+                assert_eq!(expected_name.act_id(), actual_error.actor_id());
+                assert_eq!(EXPECTED_ERR, actual_error.err().as_ref());
+                assert_eq!(&expected_actor_impl, actual_error.actor_impl());
+                assert_eq!(&expected_state, actual_error.state());
+                assert_eq!(&expected_message, actual_error.message());
+                assert_eq!(EXPECTED_KIND, actual_error.kind());
+            } else {
+                panic!("Actor exit wasn't the right variant.");
+            }
+        });
+    }
+
+    #[test]
+    fn owned_notification_when_owner_exit_panic() {
+        ASYNC_RT.block_on(async {
+            const EXPECTED_ERR: &str = "Expected panic text.";
+            let (sender, receiver) = oneshot::channel();
+            let owner = |_: Mailbox<EchoMsg>, owner_id, runtime: &'static Runtime| async move {
+                let owner_name = runtime.actor_name(owner_id);
+                spawn_owned_exit_server(runtime, owner_name, sender).await;
+                panic!("{EXPECTED_ERR}");
+            };
+
+            let expected_name = RUNTIME.spawn(None, owner).await.unwrap();
+
+            let actual = timeout(Duration::from_millis(500), receiver)
+                .await
+                .unwrap()
+                .unwrap();
+            if let ActorExit::Panic(panic) = actual {
+                assert_eq!(expected_name.act_id(), panic.actor_id());
+                assert_eq!(EXPECTED_ERR, panic.err().unwrap());
+            } else {
+                panic!("Actor exit wasn't the right variant.");
+            }
+        });
+    }
+}

+ 66 - 47
crates/btrun/src/lib.rs

@@ -9,7 +9,7 @@ use std::{
     ops::DerefMut,
     panic::AssertUnwindSafe,
     pin::Pin,
-    sync::{Arc, Mutex as SyncMutex},
+    sync::Arc,
 };
 
 use btlib::{bterr, crypto::Creds, error::StringError, BlockPath, Result};
@@ -17,11 +17,13 @@ use btserde::{from_slice, to_vec, write_to};
 use bttp::{DeserCallback, MsgCallback, Replier, Transmitter};
 use futures::FutureExt;
 use serde::{Deserialize, Serialize};
-use tokio::sync::{mpsc, Mutex, RwLock};
+use tokio::{
+    sync::{mpsc, Mutex, RwLock},
+    task::JoinSet,
+};
 
 pub use bttp::Receiver;
 mod kernel;
-pub use kernel::SpawnReq;
 use kernel::{kernel, FaultResult};
 pub mod model;
 use model::*;
@@ -32,17 +34,18 @@ use model::*;
 #[macro_export]
 macro_rules! declare_runtime {
     ($name:ident, $ip_addr:expr, $creds:expr) => {
-        static $name: $crate::model::Lazy<&'static $crate::Runtime> = $crate::model::Lazy::new(|| {
-            use $crate::{Runtime, Receiver, model::Lazy};
-            static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::_new($creds).unwrap());
-            static RECEIVER: Lazy<Receiver> =
-                Lazy::new(|| _new_receiver($ip_addr, $creds, &*RUNTIME));
-            // Start the kernel task.
-            RUNTIME._spawn_kernel();
-            // By dereferencing RECEIVER we ensure it's started.
-            let _ = &*RECEIVER;
-            &*RUNTIME
-        });
+        pub static $name: $crate::model::Lazy<&'static $crate::Runtime> =
+            $crate::model::Lazy::new(|| {
+                use $crate::{model::Lazy, Receiver, Runtime};
+                static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::_new($creds).unwrap());
+                static RECEIVER: Lazy<Receiver> =
+                    Lazy::new(|| _new_receiver($ip_addr, $creds, &*RUNTIME));
+                // Start the kernel task.
+                RUNTIME._spawn_kernel();
+                // By dereferencing RECEIVER we ensure it's started.
+                let _ = &*RECEIVER;
+                &*RUNTIME
+            });
     };
 }
 
@@ -70,30 +73,22 @@ pub struct Runtime {
     handles: RwLock<HashMap<ActorId, ActorHandle>>,
     peers: RwLock<HashMap<Arc<BlockPath>, Transmitter>>,
     registry: RwLock<HashMap<ServiceId, ServiceRecord>>,
-    kernel_sender: mpsc::Sender<SpawnReq>,
-    kernel_receiver: SyncMutex<Option<mpsc::Receiver<SpawnReq>>>,
+    tasks: Mutex<JoinSet<FaultResult>>,
 }
 
 impl Runtime {
-    /// The size of the buffer to use for the channel between [Runtime] and [kernel] used for
-    /// spawning tasks.
-    const SPAWN_REQ_BUF_SZ: usize = 16;
-
     ///  This method is not intended to be called directly by downstream crates. Use the macro
     /// [declare_runtime] to create a [Runtime].
     ///
     /// If you create a non-static [Runtime], your process will panic when it is dropped.
     #[doc(hidden)]
     pub fn _new<C: 'static + Send + Sync + Creds>(creds: Arc<C>) -> Result<Runtime> {
-        let path = Arc::new(creds.bind_path()?);
-        let (sender, receiver) = mpsc::channel(Self::SPAWN_REQ_BUF_SZ);
         Ok(Runtime {
-            path,
+            path: Arc::new(creds.bind_path()?),
             handles: RwLock::new(HashMap::new()),
             peers: RwLock::new(HashMap::new()),
             registry: RwLock::new(HashMap::new()),
-            kernel_sender: sender,
-            kernel_receiver: SyncMutex::new(Some(receiver)),
+            tasks: Mutex::new(JoinSet::new()),
         })
     }
 
@@ -102,8 +97,7 @@ impl Runtime {
     /// it will panic.
     #[doc(hidden)]
     pub fn _spawn_kernel(&'static self) {
-        let receiver = self.kernel_receiver.lock().unwrap().take().unwrap();
-        tokio::task::spawn(kernel(self, receiver));
+        tokio::task::spawn(kernel(self));
     }
 
     pub fn path(&self) -> &Arc<BlockPath> {
@@ -274,13 +268,28 @@ impl Runtime {
     }
 
     /// Spawns a new actor using the given activator function and returns a handle to it.
-    pub async fn spawn<Msg, F, Fut>(&'static self, owner: Option<ActorName>, actor: F) -> ActorName
+    ///
+    /// This method will return an error of type [RuntimeError::BadOwnerName] if `owner`
+    /// isn't the name of an actor in this runtime.
+    pub async fn spawn<Msg, F, Fut>(
+        &'static self,
+        mut owner: Option<ActorName>,
+        actor: F,
+    ) -> Result<ActorName>
     where
         Msg: 'static + MsgEnum,
         Fut: 'static + Send + Future<Output = ActorResult>,
         F: FnOnce(Mailbox<Msg>, ActorId, &'static Runtime) -> Fut,
     {
         let mut handles = self.handles.write().await;
+
+        if let Some(owner_ref) = owner.as_ref() {
+            if !handles.contains_key(&owner_ref.act_id()) {
+                let owner = owner.take().unwrap();
+                return Err(RuntimeError::BadOwnerName(owner).into());
+            }
+        }
+
         let act_id = {
             let mut act_id = ActorId::new();
             while handles.contains_key(&act_id) {
@@ -288,6 +297,7 @@ impl Runtime {
             }
             act_id
         };
+
         let act_name = self.actor_name(act_id);
         let (tx, rx) = mpsc::channel::<Envelope<Msg>>(MAILBOX_LIMIT);
         // The deliverer closure is responsible for deserializing messages received over the wire
@@ -330,6 +340,7 @@ impl Runtime {
                 fut
             }
         };
+
         // ctrl_deliverer is responsible for delivering ControlMsgs to the actor.
         let ctrl_deliverer = {
             let tx = tx.clone();
@@ -343,15 +354,17 @@ impl Runtime {
                 fut
             }
         };
-        let actor = actor(rx, act_id, self);
-        let (req, receiver) = SpawnReq::new(Self::catch_unwind(act_id, actor));
-        self.kernel_sender
-            .send(req)
-            .await
-            .unwrap_or_else(|err| panic!("The kernel has panicked: {err}"));
-        let handle = receiver
-            .await
-            .unwrap_or_else(|err| panic!("Kernel failed to send abort handle: {err}"));
+
+        if let Some(owner) = owner.as_ref() {
+            let owner_handle = handles.get_mut(&owner.act_id()).unwrap();
+            owner_handle.owns_mut().push(act_name.clone());
+        }
+
+        let task = Self::catch_unwind(act_id, actor(rx, act_id, self));
+        let handle = {
+            let mut tasks = self.tasks.lock().await;
+            tasks.spawn(task)
+        };
         let actor_handle = ActorHandle::new(
             act_name.clone(),
             handle,
@@ -361,7 +374,8 @@ impl Runtime {
             owner,
         );
         handles.insert(act_id, actor_handle);
-        act_name
+
+        Ok(act_name)
     }
 
     async fn catch_unwind<Fut>(actor_id: ActorId, fut: Fut) -> FaultResult
@@ -495,6 +509,7 @@ impl ServiceRecord {
 pub enum RuntimeError {
     BadActorName(ActorName),
     BadServiceId(ServiceId),
+    BadOwnerName(ActorName),
 }
 
 impl Display for RuntimeError {
@@ -504,6 +519,10 @@ impl Display for RuntimeError {
             Self::BadServiceId(service_id) => {
                 write!(f, "service ID is not registered: {service_id}")
             }
+            Self::BadOwnerName(name) => write!(
+                f,
+                "Non-existent name {name} can't be used as an actor owner."
+            ),
         }
     }
 }
@@ -690,7 +709,7 @@ macro_rules! test_setup {
         ///
         /// By creating a single async runtime which is used by all of the tests, we can avoid this
         /// problem.
-        static ASYNC_RT: $crate::model::Lazy<::tokio::runtime::Runtime> =
+        pub(crate) static ASYNC_RT: $crate::model::Lazy<::tokio::runtime::Runtime> =
             $crate::model::Lazy::new(|| {
                 ::tokio::runtime::Builder::new_current_thread()
                     .enable_all()
@@ -698,9 +717,9 @@ macro_rules! test_setup {
                     .unwrap()
             });
 
-        const RUNTIME_ADDR: ::std::net::IpAddr =
+        pub(crate) const RUNTIME_ADDR: ::std::net::IpAddr =
             ::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127, 0, 0, 1));
-        static RUNTIME_CREDS: $crate::model::Lazy<
+        pub(crate) static RUNTIME_CREDS: $crate::model::Lazy<
             ::std::sync::Arc<::btlib::crypto::ConcreteCreds>,
         > = $crate::model::Lazy::new(|| {
             let test_store = &::btlib_tests::TEST_STORE;
@@ -709,7 +728,7 @@ macro_rules! test_setup {
         declare_runtime!(RUNTIME, RUNTIME_ADDR, RUNTIME_CREDS.clone());
 
         /// The log level to use when running tests.
-        const LOG_LEVEL: &str = "warn";
+        pub(crate) const LOG_LEVEL: &str = "warn";
 
         #[::ctor::ctor]
         fn ctor() {
@@ -721,7 +740,7 @@ macro_rules! test_setup {
 }
 
 #[cfg(test)]
-pub mod test {
+pub(crate) mod tests {
     use super::*;
 
     use btlib::crypto::{CredStore, CredsPriv};
@@ -734,7 +753,7 @@ pub mod test {
     test_setup!();
 
     #[derive(Serialize, Deserialize)]
-    struct EchoMsg(String);
+    pub(crate) struct EchoMsg(String);
 
     impl CallMsg for EchoMsg {
         type Reply = EchoMsg;
@@ -786,7 +805,7 @@ pub mod test {
     fn local_call() {
         ASYNC_RT.block_on(async {
             const EXPECTED: &str = "hello";
-            let name = RUNTIME.spawn(None, echo).await;
+            let name = RUNTIME.spawn(None, echo).await.unwrap();
             let from = ActorName::new(name.path().clone(), ActorId::new());
 
             let reply = RUNTIME
@@ -812,7 +831,7 @@ pub mod test {
             TEST_STORE.node_creds().unwrap()
         );
         assert_eq!(0, LOCAL_RT.num_running().await);
-        let name = LOCAL_RT.spawn(None, echo).await;
+        let name = LOCAL_RT.spawn(None, echo).await.unwrap();
         assert_eq!(1, LOCAL_RT.num_running().await);
         LOCAL_RT.take(&name).await.unwrap();
         assert_eq!(0, LOCAL_RT.num_running().await);
@@ -822,7 +841,7 @@ pub mod test {
     fn remote_call() {
         ASYNC_RT.block_on(async {
             const EXPECTED: &str = "hello";
-            let actor_name = RUNTIME.spawn(None, echo).await;
+            let actor_name = RUNTIME.spawn(None, echo).await.unwrap();
             let bind_path = Arc::new(RUNTIME_CREDS.bind_path().unwrap());
             let block_addr = Arc::new(BlockAddr::new(RUNTIME_ADDR, bind_path));
             let transmitter = Transmitter::new(block_addr, RUNTIME_CREDS.clone())

+ 4 - 4
crates/btrun/src/model.rs

@@ -80,7 +80,7 @@ impl Display for ActorErrorCommon {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(
             f,
-            "Actor {}, with implementation '{}', panicked in state {} while {} a message of type {}",
+            "Actor {}, with implementation '{}', erred in state {} while {} a message of type {}",
             self.actor_id,
             self.actor_impl,
             self.state,
@@ -131,7 +131,7 @@ impl Display for ActorError {
     }
 }
 
-#[derive(Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct ActorErrorStr {
     common: ActorErrorCommon,
     #[serde(with = "smart_ptr")]
@@ -265,11 +265,11 @@ impl Display for ActorFault {
     }
 }
 
-#[derive(Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 /// Represents the ways an actor can exit.
 pub enum ActorExit {
     /// The actor exited normally.
-    Ok,
+    Ok(ActorId),
     /// The actor exited with an error.
     Error(ActorErrorStr),
     /// The actor panicked.

+ 6 - 3
crates/btrun/tests/runtime_tests.rs

@@ -218,7 +218,8 @@ mod ping_pong {
                     .spawn(None, move |mailbox, act_id, runtime| {
                         server_loop(runtime, make_init, mailbox, act_id)
                     })
-                    .await;
+                    .await
+                    .unwrap();
                 Ok(actor_impl)
             };
             Box::pin(fut)
@@ -561,7 +562,7 @@ mod client_callback {
                     *guard = Some(new_state);
                 }
                 Ok(actor_id)
-            }).await
+            }).await.unwrap()
         };
         ClientHandle {
             runtime,
@@ -668,7 +669,8 @@ mod client_callback {
                         .spawn(None, move |mailbox, act_id, runtime| {
                             server_loop(runtime, make_init, mailbox, act_id)
                         })
-                        .await;
+                        .await
+                        .unwrap();
                     Ok(actor_impl)
                 };
                 Box::pin(fut)
@@ -724,6 +726,7 @@ mod client_callback {
                 Ok(actor_id)
             })
             .await
+            .unwrap()
     }
 
     #[test]