Repository: slawlor/ractor
Pull request #1 (2023-01-04 20:59:48)

Initial actor with a starting supervision tree. 
There are likely many things missing in this implementation, but it's a starting point. This also sets up a CI which runs a basic set of tests

There's also a "sample" app which eventually will get expanded upon, for now it's just a ping-pong example

```
$ cargo run
pre_start called
post_start called
ping..pong..ping..pong..ping..pong..ping..pong..ping..pong..ping..pong..
Sending stop...
Stopping
post_stop called
```


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..fc5e8ff
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,72 @@
+name: CI
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    types: [opened, reopened, synchronize]
+
+jobs:
+  test:
+    name: ${{matrix.name}}
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - name: Run the default tests
+            package: ractor
+            # flags: 
+            
+    steps:
+      - uses: actions/checkout@main
+
+      - name: Install rust
+        uses: actions-rs/toolchain@v1
+        with:
+          toolchain: stable
+          override: true
+
+      - name: Run test
+        uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: --package ${{matrix.package}} ${{matrix.flags}}
+
+  clippy:
+    name: Clippy
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@main
+      - name: Install minimal stable with clippy
+        uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          components: clippy
+          override: true
+
+      - name: Run Clippy
+        uses: actions-rs/cargo@v1
+        with:
+          command: clippy
+          args: --all -- -D clippy::all -D warnings
+
+  rustfmt:
+    name: rustfmt
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@main
+      - name: Install minimal stable with rustfmt
+        uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          components: rustfmt
+          override: true
+
+      - name: Run rustfmt
+        uses: actions-rs/cargo@v1
+        with:
+          command: fmt
+          args: --all -- --check
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 088ba6b..e356625 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ Cargo.lock
 
 # These are backup files generated by rustfmt
 **/*.rs.bk
+
+# Remove all target compilation folders from all sub-folders as well
+**/target/
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..f1ab7f4
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,6 @@
+[workspace]
+
+members = [
+    "ractor",
+    "ractor-repl"
+]
diff --git a/README.md b/README.md
index 0394d14..0f6b4bb 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,18 @@
 # ractor
-Rust actor framework
+
+A Rust actor framework.
+
+## About
+
+Ractor tries to solve the problem of building and maintaing an Erlang-like actor framework in Rust. It gives
+a set of generic primitives and helps automate the supervision tree. It's built *heavily* on `tokio` which is a
+hard requirement for `ractor`. 
+
+## Installation
+
+Install `ractor` by adding the following to your Cargo.toml dependencies
+
+```toml
+[dependencies]
+ractor = "0.1"
+```
diff --git a/ractor-repl/Cargo.toml b/ractor-repl/Cargo.toml
new file mode 100644
index 0000000..dbaf9f8
--- /dev/null
+++ b/ractor-repl/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "ractor-repl"
+default-run = "ractor-repl"
+version = "0.0.0"
+authors = ["Sean Lawlor <seanlawlor@fb.com>"]
+description = "A actor framework for Rust - REPL"
+license = "MIT"
+edition = "2018"
+keywords = ["actor", "ractor"]
+repository = "https://github.com/slawlor/ractor"
+readme = "../README.md"
+publish = false
+
+[dependencies]
+async-trait = "0.1"
+ractor = { path="../ractor" }
+rustyrepl = "0.1"
+tokio = { version = "1.23", features = ["full"]}
\ No newline at end of file
diff --git a/ractor-repl/src/main.rs b/ractor-repl/src/main.rs
new file mode 100644
index 0000000..d7955ca
--- /dev/null
+++ b/ractor-repl/src/main.rs
@@ -0,0 +1,15 @@
+// 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.
+
+mod ping_pong;
+
+// MAIN //
+#[tokio::main]
+async fn main() {
+    let actor_handler = ping_pong::PingPong;
+    let (agent, ports) = ractor::Actor::new(None, actor_handler);
+    let (_actor_ref, actor_handle) = agent.start(ports, None).await.unwrap();
+    actor_handle.await.unwrap();
+}
diff --git a/ractor-repl/src/ping_pong.rs b/ractor-repl/src/ping_pong.rs
new file mode 100644
index 0000000..fb56a58
--- /dev/null
+++ b/ractor-repl/src/ping_pong.rs
@@ -0,0 +1,88 @@
+// 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.
+
+//! A ping-pong actor implementation
+
+use ractor::actor::*;
+use ractor::ActorCell;
+
+pub struct PingPong;
+
+#[derive(Debug, Clone)]
+pub enum Message {
+    Ping,
+    Pong,
+    Stop,
+}
+
+impl Message {
+    fn next(&self) -> Self {
+        match self {
+            Self::Ping => Self::Pong,
+            Self::Pong => Self::Ping,
+            Self::Stop => Self::Stop,
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl ActorHandler for PingPong {
+    type Msg = Message;
+
+    type State = u8;
+
+    async fn pre_start(&self, myself: ActorCell) -> Self::State {
+        println!("pre_start called");
+        // startup the event processing
+        let _ = myself.send_message_t(Message::Ping);
+        // create the initial state
+        0u8
+    }
+
+    async fn post_start(
+        &self,
+        _this_actor: ActorCell,
+        _state: &Self::State,
+    ) -> Option<Self::State> {
+        println!("post_start called");
+        None
+    }
+
+    /// Invoked after an actor has been stopped.
+    async fn post_stop(&self, _this_actor: ActorCell, _state: &Self::State) {
+        println!("post_stop called");
+    }
+
+    async fn handle(
+        &self,
+        myself: ractor::ActorCell,
+        message: Self::Msg,
+        state: &Self::State,
+    ) -> Option<Self::State> {
+        let result = match message {
+            Message::Ping => {
+                print!("ping..");
+                Some(*state + 1u8)
+            }
+            Message::Pong => {
+                print!("pong..");
+                Some(*state + 1u8)
+            }
+            Message::Stop => {
+                println!("Stopping");
+                let _ = myself.stop().await;
+                return None;
+            }
+        };
+        if *state > 10u8 {
+            println!("\nSending stop...");
+            let _ = myself.send_message_t(Message::Stop);
+        } else {
+            let next = message.next();
+            let _ = myself.send_message_t(next);
+        }
+        result
+    }
+}
diff --git a/ractor/Cargo.toml b/ractor/Cargo.toml
new file mode 100644
index 0000000..570117b
--- /dev/null
+++ b/ractor/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "ractor"
+version = "0.1.0"
+authors = ["Sean Lawlor <seanlawlor@fb.com>"]
+description = "A actor framework for Rust"
+license = "MIT"
+edition = "2018"
+keywords = ["actor", "ractor"]
+repository = "https://github.com/slawlor/ractor"
+readme = "../README.md"
+
+[dependencies]
+async-recursion = "1"
+async-trait = "0.1"
+tokio = { version = "1.23", features = ["rt", "time", "sync", "macros"]}
\ No newline at end of file
diff --git a/ractor/src/actor/actor_cell.rs b/ractor/src/actor/actor_cell.rs
new file mode 100644
index 0000000..87a7d6d
--- /dev/null
+++ b/ractor/src/actor/actor_cell.rs
@@ -0,0 +1,242 @@
+// 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.
+
+//! A reference counted actor which can be passed around as needed
+
+use std::sync::atomic::{AtomicU8, Ordering};
+use std::sync::Arc;
+
+use tokio::sync::mpsc;
+
+use super::messages::{BoxedMessage, Signal};
+use super::supervision::SupervisionTree;
+use super::SupervisionEvent;
+use crate::port::input::InputPortReceiver;
+use crate::ActorId;
+
+/// The status of the agent
+#[derive(Debug, Clone, Eq, PartialEq, Copy)]
+#[repr(u8)]
+pub enum ActorStatus {
+    /// Created, but not yet started
+    Unstarted = 0u8,
+    /// Starting
+    Starting = 1u8,
+    /// Executing (or waiting on messages)
+    Running = 2u8,
+    /// Upgrading
+    Upgrading = 3u8,
+    /// Stopping
+    Stopping = 4u8,
+    /// Dead
+    Stopped = 5u8,
+}
+
+/// The collection of ports an agent needs to listen to
+pub struct ActorPortSet {
+    pub(crate) signal_rx: mpsc::Receiver<Signal>,
+    pub(crate) supervisor_rx: mpsc::UnboundedReceiver<SupervisionEvent>,
+    pub(crate) message_rx: InputPortReceiver<BoxedMessage>,
+}
+
+/// The inner-properties of an Actor
+struct ActorProperties {
+    id: ActorId,
+    name: Option<String>,
+    status: Arc<AtomicU8>,
+    signal: mpsc::Sender<Signal>,
+    supervision: mpsc::UnboundedSender<SupervisionEvent>,
+    message: mpsc::UnboundedSender<BoxedMessage>,
+    tree: SupervisionTree,
+}
+impl ActorProperties {
+    pub fn new(
+        name: Option<String>,
+    ) -> (
+        Self,
+        mpsc::Receiver<Signal>,
+        mpsc::UnboundedReceiver<SupervisionEvent>,
+        mpsc::UnboundedReceiver<BoxedMessage>,
+    ) {
+        let (tx, rx) = mpsc::channel(2);
+        let (tx2, rx2) = mpsc::unbounded_channel();
+        let (tx3, rx3) = mpsc::unbounded_channel();
+        (
+            Self {
+                id: crate::AGENT_ID_ALLOCATOR.fetch_add(1u64, Ordering::Relaxed),
+                name,
+                status: Arc::new(AtomicU8::new(ActorStatus::Unstarted as u8)),
+                signal: tx,
+                supervision: tx2,
+                message: tx3,
+                tree: SupervisionTree::default(),
+            },
+            rx,
+            rx2,
+            rx3,
+        )
+    }
+
+    pub fn get_status(&self) -> ActorStatus {
+        match self.status.load(Ordering::Relaxed) {
+            0u8 => ActorStatus::Unstarted,
+            1u8 => ActorStatus::Starting,
+            2u8 => ActorStatus::Running,
+            3u8 => ActorStatus::Upgrading,
+            4u8 => ActorStatus::Stopping,
+            _ => ActorStatus::Stopped,
+        }
+    }
+
+    pub fn set_status(&self, status: ActorStatus) {
+        self.status.store(status as u8, Ordering::Relaxed);
+    }
+
+    pub async fn send_signal(&self, signal: Signal) -> Result<(), mpsc::error::SendError<Signal>> {
+        self.signal.send(signal).await
+    }
+
+    pub fn send_supervisor_evt(
+        &self,
+        message: SupervisionEvent,
+    ) -> Result<(), mpsc::error::SendError<SupervisionEvent>> {
+        self.supervision.send(message)
+    }
+
+    pub fn send_message(
+        &self,
+        message: BoxedMessage,
+    ) -> Result<(), mpsc::error::SendError<BoxedMessage>> {
+        self.message.send(message)
+    }
+}
+
+/// A handy-dandy reference to actor's and their inner properties
+#[derive(Clone)]
+pub struct ActorCell {
+    inner: Arc<ActorProperties>,
+}
+
+impl std::fmt::Debug for ActorCell {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Agent {}", self.get_id())
+    }
+}
+
+impl ActorCell {
+    /// Construct a new actor cell and return the message reception channels
+    pub fn new(name: Option<String>) -> (Self, ActorPortSet) {
+        let (props, rx1, rx2, rx3) = ActorProperties::new(name);
+        (
+            Self {
+                inner: Arc::new(props),
+            },
+            ActorPortSet {
+                signal_rx: rx1,
+                supervisor_rx: rx2,
+                message_rx: rx3,
+            },
+        )
+    }
+
+    /// Retrieve the agent's id
+    pub fn get_id(&self) -> ActorId {
+        self.inner.id
+    }
+
+    /// Retrieve the agent's name
+    pub fn get_name(&self) -> Option<String> {
+        self.inner.name.clone()
+    }
+
+    /// Retrieve the status of an actor
+    pub fn get_status(&self) -> ActorStatus {
+        self.inner.get_status()
+    }
+
+    /// Set the status of the actor
+    pub fn set_status(&self, status: ActorStatus) {
+        self.inner.set_status(status)
+    }
+
+    // /// Returns the state object, only in the event of a dead actor. Otherwise
+    // /// returns None. We cannot read the state of a live actor (for thread safety)
+    // pub fn get_state(&self) -> Option<Box<dyn std::any::Any>> {
+    //     None
+    // }
+
+    /// Terminate yourself and all children beneath you
+    pub async fn terminate(&self) {
+        // we don't need to nofity of exit if we're already stopping or stopped
+        if self.get_status() as u8 <= ActorStatus::Upgrading as u8 {
+            // kill myself immediately. Ignores failures, as a failure means either
+            // 1. we're already dead or
+            // 2. the channel is full of "signals"
+            let _ = self.send_signal(Signal::Exit).await;
+        }
+
+        // notify children they should die. They will unlink themselves from the supervisor
+        self.inner.tree.terminate_children().await;
+    }
+
+    /// Link another actor to the supervised list
+    pub async fn link(&self, other: ActorCell) {
+        other.inner.tree.insert_parent(self.clone()).await;
+        self.inner.tree.insert_child(other).await;
+    }
+
+    /// Unlink another actor from the supervised list
+    pub async fn unlink(&self, other: ActorCell) {
+        other.inner.tree.remove_parent(self.clone()).await;
+        self.inner.tree.remove_child(other).await;
+    }
+
+    /// Send a signal event to the signal port
+    pub async fn send_signal(&self, signal: Signal) -> Result<(), mpsc::error::SendError<Signal>> {
+        self.inner.send_signal(signal).await
+    }
+
+    /// Stop this agent, but sending Signal::Exit
+    pub async fn stop(&self) -> Result<(), mpsc::error::SendError<Signal>> {
+        self.send_signal(Signal::Exit).await
+    }
+
+    /// Send a supervisor event to the supervisory port
+    pub fn send_supervisor_evt(
+        &self,
+        message: SupervisionEvent,
+    ) -> Result<(), mpsc::error::SendError<SupervisionEvent>> {
+        self.inner.send_supervisor_evt(message)
+    }
+
+    /// Send a message to the regular message handling port
+    pub fn send_message(
+        &self,
+        message: BoxedMessage,
+    ) -> Result<(), mpsc::error::SendError<BoxedMessage>> {
+        self.inner.send_message(message)
+    }
+
+    /// Send a strongly-typed message, constructing the boxed message on the fly
+    pub fn send_message_t<TMsg: crate::Message>(
+        &self,
+        message: TMsg,
+    ) -> Result<(), mpsc::error::SendError<BoxedMessage>> {
+        self.inner.send_message(BoxedMessage::new(message, true))
+    }
+
+    /// Notify the supervisors that a supervision event occurred
+    pub async fn notify_supervisors(
+        &self,
+        message: SupervisionEvent,
+    ) -> Result<(), mpsc::error::SendError<SupervisionEvent>> {
+        self.inner.tree.notify_supervisors(message).await
+    }
+
+    #[cfg(test)]
+    pub fn get_tree(&self) -> SupervisionTree {
+        self.inner.tree.clone()
+    }
+}
diff --git a/ractor/src/actor/errors.rs b/ractor/src/actor/errors.rs
new file mode 100644
index 0000000..15294a2
--- /dev/null
+++ b/ractor/src/actor/errors.rs
@@ -0,0 +1,60 @@
+// 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.
+
+//! Actor error types
+
+use std::fmt::Display;
+
+/// Spawn errors starting an actor
+#[derive(Debug)]
+pub enum SpawnErr {
+    /// Actor panic'd during startup
+    StartupPanic(String),
+    /// Actor failed to startup because the startup task was cancelled
+    StartupCancelled,
+    /// An agent cannot be started > 1 time
+    ActorAlreadyStarted,
+}
+
+impl Display for SpawnErr {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::StartupPanic(panic_msg) => {
+                write!(f, "Actor panicked during startup '{}'", panic_msg)
+            }
+            Self::StartupCancelled => {
+                write!(
+                    f,
+                    "Actor failed to startup due to cancelled processing task"
+                )
+            }
+            Self::ActorAlreadyStarted => {
+                write!(f, "Actor cannot be started more than once")
+            }
+        }
+    }
+}
+
+/// Actor processing loop errors
+#[derive(Debug)]
+pub enum ActorProcessingErr {
+    /// Actor had a task cancelled internally during processing
+    Cancelled,
+    /// Actor had an internal panic
+    Panic(String),
+}
+
+impl Display for ActorProcessingErr {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Panic(panic_msg) => {
+                write!(f, "Actor panicked '{}'", panic_msg)
+            }
+            Self::Cancelled => {
+                write!(f, "Actor operation cancelled")
+            }
+        }
+    }
+}
diff --git a/ractor/src/actor/messages.rs b/ractor/src/actor/messages.rs
new file mode 100644
index 0000000..225fc64
--- /dev/null
+++ b/ractor/src/actor/messages.rs
@@ -0,0 +1,95 @@
+// 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.
+
+//! Messages which are built-in for the agent
+
+use std::any::Any;
+use std::fmt::Debug;
+
+use crate::Message;
+
+/// A "boxed" message denoting a strong-type message
+/// but generic so it can be passed around without type
+/// constraints
+pub struct BoxedMessage {
+    /// Flag denoting if message can be consumed 1 time
+    pub one_time: bool,
+    /// The message value
+    pub msg: Option<Box<dyn Any + Send>>,
+}
+
+/// An error downcasting a boxed message to a strong type
+pub struct DowncastBoxedMessageErr;
+
+impl BoxedMessage {
+    /// Create a new [BoxedMessage] from a strongly-typed message
+    /// and a flag denoting if the message can be consumed 1 time
+    /// or can be cloned and re-consumed
+    pub fn new<T>(msg: T, one_time: bool) -> Self
+    where
+        T: Any + Message,
+    {
+        Self {
+            one_time,
+            msg: Some(Box::new(msg)),
+        }
+    }
+
+    /// Try and take the resulting message as a specific type, consumes
+    /// the boxed message if one_time = true
+    pub fn take<T>(&mut self) -> Result<T, DowncastBoxedMessageErr>
+    where
+        T: Any + Message,
+    {
+        if self.one_time {
+            match self.msg.take() {
+                Some(m) => {
+                    if m.is::<T>() {
+                        Ok(*m.downcast::<T>().unwrap())
+                    } else {
+                        Err(DowncastBoxedMessageErr)
+                    }
+                }
+                None => Err(DowncastBoxedMessageErr),
+            }
+        } else {
+            match self.msg.as_ref() {
+                Some(m) if m.is::<T>() => Ok(m.downcast_ref::<T>().cloned().unwrap()),
+                Some(_) => Err(DowncastBoxedMessageErr),
+                None => Err(DowncastBoxedMessageErr),
+            }
+        }
+    }
+}
+
+impl Clone for BoxedMessage {
+    fn clone(&self) -> Self {
+        panic!("Cloning of an `BoxedMessage` type is not supported");
+    }
+}
+
+impl Debug for BoxedMessage {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        f.write_str("BoxedMessage")
+    }
+}
+
+/// A supervision event from the supervision tree
+#[derive(Debug, Clone)]
+pub enum SupervisionEvent {
+    /// An actor was started
+    ActorStarted(super::actor_cell::ActorCell),
+    /// An actor terminated
+    ActorTerminated(super::actor_cell::ActorCell),
+    /// An actor panicked
+    ActorPanicked(super::actor_cell::ActorCell, String),
+}
+
+/// A signal message which takes priority above all else
+#[derive(Clone, Debug)]
+pub enum Signal {
+    /// Exit, cancelling all async work immediately
+    Exit,
+}
diff --git a/ractor/src/actor/mod.rs b/ractor/src/actor/mod.rs
new file mode 100644
index 0000000..7264b75
--- /dev/null
+++ b/ractor/src/actor/mod.rs
@@ -0,0 +1,411 @@
+// 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.
+
+//! Holds the logic to a basic actor building-block
+
+use std::sync::Arc;
+
+use tokio::task::JoinHandle;
+
+pub mod messages;
+use messages::*;
+
+pub mod actor_cell;
+pub mod errors;
+pub mod supervision;
+
+#[cfg(test)]
+mod tests;
+
+use crate::{Message, State};
+use actor_cell::{ActorCell, ActorPortSet, ActorStatus};
+use errors::{ActorProcessingErr, SpawnErr};
+
+pub(crate) fn get_panic_string(e: Box<dyn std::any::Any + Send>) -> String {
+    match e.downcast::<String>() {
+        Ok(v) => *v,
+        Err(e) => match e.downcast::<&str>() {
+            Ok(v) => v.to_string(),
+            _ => "Unknown panic occurred which couldn't be coerced to a string".to_string(),
+        },
+    }
+}
+
+enum PanickableResult<T> {
+    Ok(T),
+    Cancelled,
+    Panic(String),
+}
+
+async fn handle_panickable<TResult>(
+    handle: tokio::task::JoinHandle<TResult>,
+) -> PanickableResult<TResult> {
+    match handle.await {
+        Ok(t) => PanickableResult::Ok(t),
+        Err(maybe_panic) => {
+            if maybe_panic.is_panic() {
+                let panic_message = get_panic_string(maybe_panic.into_panic());
+                PanickableResult::Panic(panic_message)
+            } else {
+                // task cancelled, just notify of exit
+                PanickableResult::Cancelled
+            }
+        }
+    }
+}
+
+/// The message handling implementation for an actor with a specific type of input message and state
+#[async_trait::async_trait]
+pub trait ActorHandler: Sync + Send + 'static {
+    /// The message type for this handler
+    type Msg: Message;
+    /// The type of state this actor deals with
+    type State: State;
+
+    /// Invoked when an actor is being started by the system.
+    ///
+    /// Any initialization inherent to the actor's role should be
+    /// performed here hence why it returns the initial state.
+    ///
+    /// Panics in `pre_start` do not invoke the
+    /// supervision strategy and the actor will be terminated.
+    async fn pre_start(&self, _this_actor: ActorCell) -> Self::State;
+
+    /// Invoked after an actor has started.
+    ///
+    /// Any post initialization can be performed here, such as writing
+    /// to a log file, emmitting metrics.
+    ///
+    /// Panics in `post_start` follow the supervision strategy.
+    async fn post_start(
+        &self,
+        _this_actor: ActorCell,
+        _state: &Self::State,
+    ) -> Option<Self::State> {
+        None
+    }
+
+    /// Invoked after an actor has been stopped.
+    async fn post_stop(&self, _this_actor: ActorCell, _state: &Self::State) {}
+
+    /// Handle the incoming message in a basic event processing loop. Unhandled panic's will be captured and
+    /// treated as agent death in the supervision tree
+    async fn handle(
+        &self,
+        _this_actor: ActorCell,
+        _message: Self::Msg,
+        _state: &Self::State,
+    ) -> Option<Self::State> {
+        None
+    }
+
+    /// Handle the incoming supervision event. Unhandled panic's will captured and treated as agent death in
+    /// the supervision tree
+    async fn handle_supervisor_evt(
+        &self,
+        _this_actor: ActorCell,
+        _message: SupervisionEvent,
+        _state: &Self::State,
+    ) -> Option<Self::State> {
+        None
+    }
+}
+
+/// The basic actor with a
+/// 1. input message type
+/// 2. state
+/// 3. message handling implementation
+pub struct Actor<TMsg, TState, THandler>
+where
+    TMsg: Message,
+    TState: State,
+    THandler: ActorHandler<Msg = TMsg, State = TState>,
+{
+    base: ActorCell,
+    handler: Arc<THandler>,
+}
+
+impl<TMsg, TState, THandler> Actor<TMsg, TState, THandler>
+where
+    TMsg: Message,
+    TState: State,
+    THandler: ActorHandler<Msg = TMsg, State = TState>,
+{
+    /// Create a new 10-port agent with some handler implementation and initial state
+    pub fn new(name: Option<String>, handler: THandler) -> (Self, ActorPortSet) {
+        let (actor_cell, ports) = ActorCell::new(name);
+        (
+            Self {
+                base: actor_cell,
+                handler: Arc::new(handler),
+            },
+            ports,
+        )
+    }
+
+    /// Spawn an actor, which is unsupervised
+    pub async fn spawn(
+        name: Option<String>,
+        handler: THandler,
+    ) -> Result<(ActorCell, JoinHandle<()>), SpawnErr> {
+        let (actor, ports) = Self::new(name, handler);
+        actor.start(ports, None).await
+    }
+
+    /// Spawn an actor with a supervisor
+    pub async fn spawn_linked(
+        name: Option<String>,
+        handler: THandler,
+        supervisor: ActorCell,
+    ) -> Result<(ActorCell, JoinHandle<()>), SpawnErr> {
+        let (actor, ports) = Self::new(name, handler);
+        actor.start(ports, Some(supervisor)).await
+    }
+
+    /// Start the actor immediately, optionally linking to a parent actor (supervision tree)
+    ///
+    /// NOTE: This returned [tokio::task::JoinHandle] is guaranteed to not panic (unless the runtime is shutting down perhaps).
+    /// An inner join handle is capturing panic results from any part of the inner tasks, so therefore
+    /// we can safely ignore it, or wait on it to block on the actor's progress
+    pub async fn start(
+        self,
+        ports: ActorPortSet,
+        supervisor: Option<ActorCell>,
+    ) -> Result<(ActorCell, JoinHandle<()>), SpawnErr> {
+        // cannot start an actor more than once
+        if self.base.get_status() != ActorStatus::Unstarted {
+            return Err(SpawnErr::ActorAlreadyStarted);
+        }
+
+        self.base.set_status(ActorStatus::Starting);
+
+        // Perform the pre-start routine, crashing immediately if we fail to start
+        let state = Self::do_pre_start(self.base.clone(), self.handler.clone()).await?;
+
+        // setup supervision
+        if let Some(sup) = &supervisor {
+            sup.link(self.base.clone()).await;
+        }
+
+        // run the processing loop, capturing panic's
+        let myself = self.base.clone();
+        let myself_ret = self.base.clone();
+        let handle = tokio::spawn(async move {
+            let evt =
+                match Self::processing_loop(ports, state, self.handler.clone(), self.base.clone())
+                    .await
+                {
+                    Ok(_) => SupervisionEvent::ActorTerminated(myself.clone()),
+                    Err(actor_err) => match actor_err {
+                        ActorProcessingErr::Cancelled => {
+                            SupervisionEvent::ActorTerminated(myself.clone())
+                        }
+                        ActorProcessingErr::Panic(msg) => {
+                            SupervisionEvent::ActorPanicked(myself.clone(), msg)
+                        }
+                    },
+                };
+
+            // terminate children
+            myself.terminate().await;
+
+            // notify supervisors of the actor's death
+            let _ = myself.notify_supervisors(evt).await;
+
+            myself.set_status(ActorStatus::Stopped);
+            // signal received or process exited cleanly, we should already have "handled" the signal, so we can just terminate
+            if let Some(sup) = supervisor {
+                sup.unlink(myself.clone()).await;
+            }
+        });
+
+        Ok((myself_ret, handle))
+    }
+
+    async fn processing_loop(
+        ports: ActorPortSet,
+        state: TState,
+        handler: Arc<THandler>,
+        myself: ActorCell,
+    ) -> Result<(), ActorProcessingErr> {
+        // perform the post-start, with supervision enabled
+        let mut state = Self::do_post_start(myself.clone(), handler.clone(), state).await?;
+
+        myself.set_status(ActorStatus::Running);
+        let _ = myself
+            .notify_supervisors(SupervisionEvent::ActorStarted(myself.clone()))
+            .await;
+
+        // let mut last_state = state.clone();
+        let myself_clone = myself.clone();
+        let handler_clone = handler.clone();
+
+        let last_state = Arc::new(tokio::sync::RwLock::new(state.clone()));
+
+        let last_state_clone = last_state.clone();
+        let handle = tokio::spawn(async move {
+            let mut ports = ports;
+            while let (n_state, None) = Self::process_message(
+                myself_clone.clone(),
+                state,
+                handler_clone.clone(),
+                &mut ports,
+            )
+            .await
+            {
+                state = n_state;
+                *(last_state_clone.write().await) = state.clone();
+            }
+        });
+
+        match handle_panickable(handle).await {
+            PanickableResult::Ok(_) => (),
+            PanickableResult::Cancelled => return Err(ActorProcessingErr::Cancelled),
+            PanickableResult::Panic(msg) => return Err(ActorProcessingErr::Panic(msg)),
+        }
+
+        myself.set_status(ActorStatus::Stopping);
+
+        let deref_state = last_state.read().await;
+        Self::do_post_stop(myself, handler, deref_state.clone()).await
+    }
+
+    /// Process a message, returning the "new" state (if changed)
+    /// along with optionally whether we were signaled mid-processing or not
+    async fn process_message(
+        myself: ActorCell,
+        state: TState,
+        handler: Arc<THandler>,
+        ports: &mut ActorPortSet,
+    ) -> (TState, Option<Signal>) {
+        tokio::select! {
+            signal = ports.signal_rx.recv() => {
+                (state, Some(Self::handle_signal(myself, signal.unwrap_or(Signal::Exit)).await))
+            },
+            supervision = ports.supervisor_rx.recv() => {
+                let state = Self::handle_supervision_message(myself.clone(), &state, handler.clone(), supervision).await.unwrap_or(state);
+                (state, None)
+            }
+            message = ports.message_rx.recv() => {
+                Self::handle_message(myself, ports, state, handler, message).await
+            }
+        }
+    }
+
+    async fn handle_message(
+        myself: ActorCell,
+        ports: &mut ActorPortSet,
+        state: TState,
+        handler: Arc<THandler>,
+        message: Option<BoxedMessage>,
+    ) -> (TState, Option<Signal>) {
+        if let Some(mut msg) = message {
+            let typed_msg = match msg.take() {
+                Ok(m) => m,
+                Err(_) => {
+                    panic!("Failed to convert message from `BoxedMessage` to `TMsg`")
+                }
+            };
+            // NOTE: We listen for the signal port again during the processing of async work in order
+            // to "cancel" any pending work should a signal be received immediately
+            tokio::select! {
+                signal = ports.signal_rx.recv() => {
+                    (state, Some(Self::handle_signal(myself, signal.unwrap_or(Signal::Exit)).await))
+                }
+                maybe_new_state = handler.handle(myself.clone(), typed_msg, &state) => {
+                    if let Some(new_state) = maybe_new_state {
+                        (new_state, None)
+                    } else {
+                        (state, None)
+                    }
+                }
+            }
+        } else {
+            (state, Some(Self::handle_signal(myself, Signal::Exit).await))
+        }
+    }
+
+    async fn handle_signal(myself: ActorCell, signal: Signal) -> Signal {
+        match &signal {
+            Signal::Exit => {
+                myself.terminate().await;
+            }
+        }
+        // signal's always bubble up
+        signal
+    }
+
+    async fn handle_supervision_message(
+        myself: ActorCell,
+        state: &TState,
+        handler: Arc<THandler>,
+        message: Option<SupervisionEvent>,
+    ) -> Option<TState> {
+        // TODO: process the specific supervision logic (pass to handler?)
+        if let Some(evt) = message {
+            let maybe_new_state = handler.handle_supervisor_evt(myself, evt, state).await;
+            maybe_new_state
+        } else {
+            None
+        }
+    }
+
+    async fn do_pre_start(myself: ActorCell, handler: Arc<THandler>) -> Result<TState, SpawnErr> {
+        let handle = tokio::spawn(async move { handler.pre_start(myself).await });
+        let start_result = handle_panickable(handle).await;
+        // let start_result = panic::catch_unwind(AssertUnwindSafe(|| handler.pre_start(myself)));
+        match start_result {
+            PanickableResult::Ok(state) => {
+                // intitialize the state
+                Ok(state)
+            }
+            PanickableResult::Cancelled => Err(SpawnErr::StartupCancelled),
+            PanickableResult::Panic(panic_information) => Err(SpawnErr::StartupPanic(format!(
+                "Actor panicked during pre_start with '{}'",
+                panic_information
+            ))),
+        }
+    }
+
+    async fn do_post_start(
+        myself: ActorCell,
+        handler: Arc<THandler>,
+        state: TState,
+    ) -> Result<TState, ActorProcessingErr> {
+        // TODO: ugh i hate cloning here :/ but the async move moves ownership of "state" into
+        // the async block
+        let original_state = state.clone();
+        let handle = tokio::spawn(async move { handler.post_start(myself, &state).await });
+        let post_start_result = handle_panickable(handle).await;
+        match post_start_result {
+            PanickableResult::Ok(Some(new_state)) => Ok(new_state),
+            PanickableResult::Ok(None) => Ok(original_state),
+            PanickableResult::Cancelled => Err(ActorProcessingErr::Cancelled),
+            PanickableResult::Panic(panic_information) => Err(ActorProcessingErr::Panic(format!(
+                "Actor panicked in post_start with '{}'",
+                panic_information
+            ))),
+        }
+    }
+
+    async fn do_post_stop(
+        myself: ActorCell,
+        handler: Arc<THandler>,
+        state: TState,
+    ) -> Result<(), ActorProcessingErr> {
+        let post_stop_result = handle_panickable(tokio::spawn(async move {
+            handler.post_stop(myself, &state).await
+        }))
+        .await;
+        match post_stop_result {
+            PanickableResult::Ok(_) => Ok(()),
+            PanickableResult::Cancelled => Err(ActorProcessingErr::Cancelled),
+            PanickableResult::Panic(panic_information) => Err(ActorProcessingErr::Panic(format!(
+                "Actor panicked in post_start with '{}'",
+                panic_information
+            ))),
+        }
+    }
+}
diff --git a/ractor/src/actor/supervision.rs b/ractor/src/actor/supervision.rs
new file mode 100644
index 0000000..bd94724
--- /dev/null
+++ b/ractor/src/actor/supervision.rs
@@ -0,0 +1,93 @@
+// 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.
+
+//! Supervision management logic
+
+use std::{collections::HashMap, sync::Arc};
+
+use tokio::sync::RwLock;
+
+use super::{actor_cell::ActorCell, messages::SupervisionEvent};
+use crate::ActorId;
+
+/// A supervision tree
+#[derive(Clone, Default)]
+pub struct SupervisionTree {
+    children: Arc<RwLock<HashMap<ActorId, ActorCell>>>,
+    parents: Arc<RwLock<HashMap<ActorId, ActorCell>>>,
+}
+
+impl SupervisionTree {
+    /// Push a child into the tere
+    pub async fn insert_child(&self, child: ActorCell) {
+        let mut guard = self.children.write().await;
+        guard.insert(child.get_id(), child.clone());
+    }
+
+    /// Remove a specific actor from the supervision tree (e.g. actor died)
+    pub async fn remove_child(&self, child: ActorCell) {
+        let mut guard = self.children.write().await;
+        let id = child.get_id();
+        if guard.contains_key(&id) {
+            guard.remove(&id);
+        }
+    }
+
+    /// Push a parent into the tere
+    pub async fn insert_parent(&self, parent: ActorCell) {
+        let mut guard = self.parents.write().await;
+        guard.insert(parent.get_id(), parent.clone());
+    }
+
+    /// Remove a specific actor from the supervision tree (e.g. actor died)
+    pub async fn remove_parent(&self, parent: ActorCell) {
+        let mut guard = self.parents.write().await;
+        let id = parent.get_id();
+        if guard.contains_key(&id) {
+            guard.remove(&id);
+        }
+    }
+
+    /// Terminate all your supervised children
+    #[async_recursion::async_recursion]
+    pub async fn terminate_children(&self) {
+        let guard = self.children.read().await;
+
+        for (_, child) in guard.iter() {
+            child.terminate().await;
+        }
+    }
+
+    /// Determine if the specified actor is a member of this supervision tree
+    pub async fn is_supervisor_of(&self, id: ActorId) -> bool {
+        self.children.read().await.contains_key(&id)
+    }
+
+    /// Determine if the specified actor is a parent of this actor
+    pub async fn is_child_of(&self, id: ActorId) -> bool {
+        self.parents.read().await.contains_key(&id)
+    }
+
+    /// Send a notification to all supervisors
+    pub async fn notify_supervisors(
+        &self,
+        evt: SupervisionEvent,
+    ) -> Result<(), tokio::sync::mpsc::error::SendError<SupervisionEvent>> {
+        for (_, parent) in self.parents.read().await.iter() {
+            parent.send_supervisor_evt(evt.clone())?;
+        }
+        Ok(())
+    }
+
+    /// Retrieve the number of supervised children
+    pub async fn get_num_children(&self) -> usize {
+        self.children.read().await.len()
+    }
+
+    /// Retrieve the number of supervised children
+    pub async fn get_num_parents(&self) -> usize {
+        self.parents.read().await.len()
+    }
+}
diff --git a/ractor/src/lib.rs b/ractor/src/lib.rs
new file mode 100644
index 0000000..44847f7
--- /dev/null
+++ b/ractor/src/lib.rs
@@ -0,0 +1,35 @@
+// 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.
+
+//! An actor framework for Rust
+
+#![warn(missing_docs)]
+#![warn(unused_crate_dependencies)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+
+use core::fmt::Debug;
+use std::sync::atomic::AtomicU64;
+
+/// An agent's identifier
+pub type ActorId = u64;
+/// The global id allocator for agents
+pub static AGENT_ID_ALLOCATOR: AtomicU64 = AtomicU64::new(0u64);
+
+pub mod actor;
+pub mod port;
+
+// re-exports
+pub use actor::actor_cell::{ActorCell, ActorStatus};
+pub use actor::errors::{ActorProcessingErr, SpawnErr};
+pub use actor::messages::{Signal, SupervisionEvent};
+pub use actor::{Actor, ActorHandler};
+
+/// Message type for an agent
+pub trait Message: Debug + Clone + Send + 'static {}
+impl<T: Debug + Clone + Send + 'static> Message for T {}
+
+/// An agent's state
+pub trait State: Clone + Sync + Send + std::any::Any {}
+impl<T: Clone + Sync + Send + std::any::Any> State for T {}
diff --git a/ractor/src/port/input.rs b/ractor/src/port/input.rs
new file mode 100644
index 0000000..812d942
--- /dev/null
+++ b/ractor/src/port/input.rs
@@ -0,0 +1,11 @@
+// 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.
+
+//! Input port implementations for various sizes
+
+/// An import-port
+pub type InputPort<TMsg> = tokio::sync::mpsc::UnboundedSender<TMsg>;
+
+pub(crate) type InputPortReceiver<TMsg> = tokio::sync::mpsc::UnboundedReceiver<TMsg>;
diff --git a/ractor/src/port/mod.rs b/ractor/src/port/mod.rs
new file mode 100644
index 0000000..eb845c8
--- /dev/null
+++ b/ractor/src/port/mod.rs
@@ -0,0 +1,33 @@
+// 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.
+
+//! Port implementations for signaling and reception of messages in the Ractor environment
+
+pub mod input;
+// pub mod output;
+
+// /// A muxed value over the wire sent through a [MuxedPort]
+// pub enum MuxedValue<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> {
+//     /// Type 1
+//     T1(T1),
+//     /// Type 2
+//     T2(T2),
+//     /// Type 3
+//     T3(T3),
+//     /// Type 4
+//     T4(T4),
+//     /// Type 5
+//     T5(T5),
+//     /// Type 6
+//     T6(T6),
+//     /// Type 7
+//     T7(T7),
+//     /// Type 8
+//     T8(T8),
+//     /// Type 9
+//     T9(T9),
+//     /// Type 10
+//     T10(T10),
+// }
diff --git a/ractor/src/port/output.rs b/ractor/src/port/output.rs
new file mode 100644
index 0000000..777d91a
--- /dev/null
+++ b/ractor/src/port/output.rs
@@ -0,0 +1,27 @@
+// 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.
+
+//! Output port implementations for actors
+
+use super::MuxedValue;
+
+/// A mutex'd port which represents up to 10 different types of messaged on the same port
+pub struct MuxedOutputPort<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> {
+    port: tokio::sync::mpsc::Receiver<MuxedValue<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>>,
+    subscriptions: Vec<()>,
+}
+
+// TODO: setup subscriptions?
+
+// impl<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10> MuxedOutputPort<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10> {
+//     fn subscribe(&mut self, subscription: )
+// }
+
+/// An output-port (broadcast)
+pub type OutputPort<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> =
+    MuxedOutputPort<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>;
+
+pub(crate) type OutputPortSender<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> =
+    tokio::sync::mpsc::Sender<MuxedOutputPort<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>>;


Test patch:
diff --git a/ractor/src/actor/tests/mod.rs b/ractor/src/actor/tests/mod.rs
new file mode 100644
index 0000000..6b19ba1
--- /dev/null
+++ b/ractor/src/actor/tests/mod.rs
@@ -0,0 +1,31 @@
+// 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.
+
+//! General tests, more logic-specific tests are contained in sub-modules
+
+use crate::{Actor, ActorHandler, SpawnErr};
+
+mod supervisor;
+
+#[tokio::test]
+async fn test_panic_on_start_captured() {
+    struct TestActor;
+
+    #[async_trait::async_trait]
+    impl ActorHandler for TestActor {
+        type Msg = ();
+
+        type State = ();
+
+        async fn pre_start(&self, _this_actor: crate::ActorCell) -> Self::State {
+            panic!("Boom!");
+        }
+    }
+
+    let (actor, ports) = Actor::new(None, TestActor);
+    let actor_output = actor.start(ports, None).await;
+
+    assert!(matches!(actor_output, Err(SpawnErr::StartupPanic(_))));
+}
diff --git a/ractor/src/actor/tests/supervisor.rs b/ractor/src/actor/tests/supervisor.rs
new file mode 100644
index 0000000..67101fe
--- /dev/null
+++ b/ractor/src/actor/tests/supervisor.rs
@@ -0,0 +1,381 @@
+// 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 crate::{Actor, ActorCell, ActorHandler, SupervisionEvent};
+
+#[tokio::test]
+async fn test_supervision_panic_in_post_startup() {
+    struct Child;
+    struct Supervisor {
+        flag: Arc<AtomicU64>,
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Child {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {
+            ()
+        }
+        async fn post_start(
+            &self,
+            _this_actor: ActorCell,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            panic!("Boom");
+        }
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Supervisor {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {}
+        async fn handle(
+            &self,
+            _this_actor: ActorCell,
+            _message: Self::Msg,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            None
+        }
+
+        async fn handle_supervisor_evt(
+            &self,
+            this_actor: ActorCell,
+            message: SupervisionEvent,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            println!("Supervisor event received {:?}", message);
+
+            // check that the panic was captured
+            if let SupervisionEvent::ActorPanicked(dead_actor, _panic_msg) = message {
+                self.flag.store(dead_actor.get_id(), Ordering::Relaxed);
+                let _ = this_actor.stop().await;
+            }
+
+            None
+        }
+    }
+
+    let flag = Arc::new(AtomicU64::new(0));
+
+    let (supervisor, supervisor_ports) = Actor::new(None, Supervisor { flag: flag.clone() });
+    let (supervisor_ref, s_handle) = supervisor
+        .start(supervisor_ports, None)
+        .await
+        .expect("Supervisor panicked on startup");
+
+    let supervisor_tree = supervisor_ref.get_tree();
+
+    let (child, child_ports) = Actor::new(None, Child);
+    let (child_ref, c_handle) = child
+        .start(child_ports, Some(supervisor_ref))
+        .await
+        .expect("Child panicked on startup");
+
+    let (_, _) = tokio::join!(s_handle, c_handle);
+
+    assert_eq!(child_ref.get_id(), flag.load(Ordering::Relaxed));
+
+    // supervisor relationship cleaned up correctly
+    assert_eq!(0, supervisor_tree.get_num_children().await);
+}
+
+#[tokio::test]
+async fn test_supervision_panic_in_handle() {
+    struct Child;
+    struct Supervisor {
+        flag: Arc<AtomicU64>,
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Child {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {}
+        async fn handle(
+            &self,
+            _this_actor: ActorCell,
+            _message: Self::Msg,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            panic!("Boom");
+        }
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Supervisor {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {}
+        async fn handle(
+            &self,
+            _this_actor: ActorCell,
+            _message: Self::Msg,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            None
+        }
+
+        async fn handle_supervisor_evt(
+            &self,
+            this_actor: ActorCell,
+            message: SupervisionEvent,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            println!("Supervisor event received {:?}", message);
+
+            // check that the panic was captured
+            if let SupervisionEvent::ActorPanicked(dead_actor, _panic_msg) = message {
+                self.flag.store(dead_actor.get_id(), Ordering::Relaxed);
+                let _ = this_actor.stop().await;
+            }
+
+            None
+        }
+    }
+
+    let flag = Arc::new(AtomicU64::new(0));
+
+    let (supervisor, supervisor_ports) = Actor::new(None, Supervisor { flag: flag.clone() });
+    let (supervisor_ref, s_handle) = supervisor
+        .start(supervisor_ports, None)
+        .await
+        .expect("Supervisor panicked on startup");
+
+    let supervisor_tree = supervisor_ref.get_tree();
+
+    let (child, child_ports) = Actor::new(None, Child);
+    let (child_ref, c_handle) = child
+        .start(child_ports, Some(supervisor_ref))
+        .await
+        .expect("Child panicked on startup");
+
+    // check that the supervision is wired up correctly
+    assert_eq!(1, supervisor_tree.get_num_children().await);
+    assert_eq!(1, child_ref.get_tree().get_num_parents().await);
+
+    // trigger the child failure
+    child_ref
+        .send_message_t(())
+        .expect("Failed to send message to child");
+
+    let (_, _) = tokio::join!(s_handle, c_handle);
+
+    assert_eq!(child_ref.get_id(), flag.load(Ordering::Relaxed));
+
+    // supervisor relationship cleaned up correctly
+    assert_eq!(0, supervisor_tree.get_num_children().await);
+}
+
+#[tokio::test]
+async fn test_supervision_panic_in_post_stop() {
+    struct Child;
+    struct Supervisor {
+        flag: Arc<AtomicU64>,
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Child {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, this_actor: ActorCell) -> Self::State {
+            // trigger stop, which starts shutdown
+            let _ = this_actor.stop().await;
+            ()
+        }
+        async fn post_stop(&self, _this_actor: ActorCell, _state: &Self::State) {
+            panic!("Boom");
+        }
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Supervisor {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {}
+        async fn handle(
+            &self,
+            _this_actor: ActorCell,
+            _message: Self::Msg,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            None
+        }
+
+        async fn handle_supervisor_evt(
+            &self,
+            this_actor: ActorCell,
+            message: SupervisionEvent,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            println!("Supervisor event received {:?}", message);
+
+            // check that the panic was captured
+            if let SupervisionEvent::ActorPanicked(dead_actor, _panic_msg) = message {
+                self.flag.store(dead_actor.get_id(), Ordering::Relaxed);
+                let _ = this_actor.stop().await;
+            }
+
+            None
+        }
+    }
+
+    let flag = Arc::new(AtomicU64::new(0));
+
+    let (supervisor, supervisor_ports) = Actor::new(None, Supervisor { flag: flag.clone() });
+    let (supervisor_ref, s_handle) = supervisor
+        .start(supervisor_ports, None)
+        .await
+        .expect("Supervisor panicked on startup");
+
+    let supervisor_tree = supervisor_ref.get_tree();
+
+    let (child, child_ports) = Actor::new(None, Child);
+    let (child_ref, c_handle) = child
+        .start(child_ports, Some(supervisor_ref))
+        .await
+        .expect("Child panicked on startup");
+
+    let (_, _) = tokio::join!(s_handle, c_handle);
+
+    assert_eq!(child_ref.get_id(), flag.load(Ordering::Relaxed));
+
+    // supervisor relationship cleaned up correctly
+    assert_eq!(0, supervisor_tree.get_num_children().await);
+}
+
+/// Test that a panic in the supervisor's handling propogates to
+/// the supervisor's supervisor
+#[tokio::test]
+async fn test_supervision_panic_in_supervisor_handle() {
+    struct Child;
+    struct Midpoint;
+    struct Supervisor {
+        flag: Arc<AtomicU64>,
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Child {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {}
+        async fn handle(
+            &self,
+            _this_actor: ActorCell,
+            _message: Self::Msg,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            panic!("Boom!");
+        }
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Midpoint {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {}
+        async fn handle_supervisor_evt(
+            &self,
+            _this_actor: ActorCell,
+            _message: SupervisionEvent,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            if let SupervisionEvent::ActorPanicked(_child, _msg) = _message {
+                panic!("Boom again!");
+            }
+            None
+        }
+    }
+
+    #[async_trait::async_trait]
+    impl ActorHandler for Supervisor {
+        type Msg = ();
+        type State = ();
+        async fn pre_start(&self, _this_actor: ActorCell) -> Self::State {}
+        async fn handle(
+            &self,
+            _this_actor: ActorCell,
+            _message: Self::Msg,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            None
+        }
+
+        async fn handle_supervisor_evt(
+            &self,
+            this_actor: ActorCell,
+            message: SupervisionEvent,
+            _state: &Self::State,
+        ) -> Option<Self::State> {
+            println!("Supervisor event received {:?}", message);
+
+            // check that the panic was captured
+            if let SupervisionEvent::ActorPanicked(dead_actor, _panic_msg) = message {
+                self.flag.store(dead_actor.get_id(), Ordering::Relaxed);
+                let _ = this_actor.stop().await;
+            }
+
+            None
+        }
+    }
+
+    let flag = Arc::new(AtomicU64::new(0));
+
+    let (supervisor, supervisor_ports) = Actor::new(None, Supervisor { flag: flag.clone() });
+    let (supervisor_ref, s_handle) = supervisor
+        .start(supervisor_ports, None)
+        .await
+        .expect("Supervisor panicked on startup");
+
+    let supervisor_tree = supervisor_ref.get_tree();
+
+    let (midpoint, midpoint_ports) = Actor::new(None, Midpoint);
+    let (midpoint_ref, m_handle) = midpoint
+        .start(midpoint_ports, Some(supervisor_ref))
+        .await
+        .expect("Midpoint actor failed to startup");
+
+    let midpoint_tree = midpoint_ref.get_tree();
+    let midpoint_ref_clone = midpoint_ref.clone();
+
+    let (child, child_ports) = Actor::new(None, Child);
+    let (child_ref, c_handle) = child
+        .start(child_ports, Some(midpoint_ref))
+        .await
+        .expect("Child panicked on startup");
+
+    // check that the supervision is wired up correctly
+    assert_eq!(1, supervisor_tree.get_num_children().await);
+    assert_eq!(1, midpoint_tree.get_num_children().await);
+    assert_eq!(1, midpoint_tree.get_num_parents().await);
+    assert_eq!(1, child_ref.get_tree().get_num_parents().await);
+
+    // trigger the child failure
+    child_ref
+        .send_message_t(())
+        .expect("Failed to send message to child");
+
+    // which triggers the handler in the midpoint, which panic's in supervision
+
+    // wait for all agents to die in the test
+    let _ = s_handle.await;
+    let _ = c_handle.await;
+    let _ = m_handle.await;
+
+    // check that we got the midpoint's ref id
+    assert_eq!(midpoint_ref_clone.get_id(), flag.load(Ordering::Relaxed));
+
+    // supervisor relationship cleaned up correctly
+    assert_eq!(0, supervisor_tree.get_num_children().await);
+}