|
| 1 | +# The `Actor` Trait |
| 2 | + |
| 3 | +The `Actor` trait defines the core behavior of all actors in the hyperactor runtime. |
| 4 | + |
| 5 | +Every actor type must implement this trait to participate in the system. It defines how an actor is constructed, initialized, and supervised. |
| 6 | + |
| 7 | +```rust |
| 8 | +#[async_trait] |
| 9 | +pub trait Actor: Sized + Send + Debug + 'static { |
| 10 | + type Params: Send + 'static; |
| 11 | + |
| 12 | + async fn new(params: Self::Params) -> Result<Self, anyhow::Error>; |
| 13 | + |
| 14 | + async fn init(&mut self, _this: &Instance<Self>) -> Result<(), anyhow::Error> { |
| 15 | + Ok(()) |
| 16 | + } |
| 17 | + |
| 18 | + async fn spawn( |
| 19 | + cap: &impl cap::CanSpawn, |
| 20 | + params: Self::Params, |
| 21 | + ) -> anyhow::Result<ActorHandle<Self>> { |
| 22 | + cap.spawn(params).await |
| 23 | + } |
| 24 | + |
| 25 | + async fn spawn_detached(params: Self::Params) -> Result<ActorHandle<Self>, anyhow::Error> { |
| 26 | + Proc::local().spawn("anon", params).await |
| 27 | + } |
| 28 | + |
| 29 | + fn spawn_server_task<F>(future: F) -> JoinHandle<F::Output> |
| 30 | + where |
| 31 | + F: Future + Send + 'static, |
| 32 | + F::Output: Send + 'static, |
| 33 | + { |
| 34 | + tokio::spawn(future) |
| 35 | + } |
| 36 | + |
| 37 | + async fn handle_supervision_event( |
| 38 | + &mut self, |
| 39 | + _this: &Instance<Self>, |
| 40 | + _event: &ActorSupervisionEvent, |
| 41 | + ) -> Result<bool, anyhow::Error> { |
| 42 | + Ok(false) |
| 43 | + } |
| 44 | + |
| 45 | + async fn handle_undeliverable_message( |
| 46 | + &mut self, |
| 47 | + this: &Instance<Self>, |
| 48 | + Undeliverable(envelope): Undeliverable<MessageEnvelope>, |
| 49 | + ) -> Result<(), anyhow::Error> { |
| 50 | + assert_eq!(envelope.sender(), this.self_id()); |
| 51 | + |
| 52 | + anyhow::bail!(UndeliverableMessageError::delivery_failure(&envelope)); |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +## Construction: `Params` and `new` |
| 58 | + |
| 59 | +Each actor must define a `Params` type: |
| 60 | + |
| 61 | +```rust |
| 62 | +type Params: Send + 'static; |
| 63 | +``` |
| 64 | + |
| 65 | +This associated type defines the data required to instantiate the actor. |
| 66 | + |
| 67 | +The actor is constructed by the runtime using: |
| 68 | +```rust |
| 69 | +async fn new(params: Self::Params) -> Result<Self, anyhow::Error>; |
| 70 | +``` |
| 71 | + |
| 72 | +This method returns the actor's internal state. At this point, the actor has not yet been connected to the runtime; it has no mailbox and cannot yet send or receive messages. `new` is typically used to construct the actor's fields from its input parameters. |
| 73 | + |
| 74 | +## Initialization: `init` |
| 75 | + |
| 76 | +```rust |
| 77 | +async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> |
| 78 | +``` |
| 79 | + |
| 80 | +The `init` method is called after the actor has been constructed with `new` and registered with the runtime. It is passed a reference to the actor's `Instance`, allowing access to runtime services such as: |
| 81 | +- The actor’s ID and status |
| 82 | +- The mailbox and port system |
| 83 | +- Capabilities for spawning or sending messages |
| 84 | + |
| 85 | +The default implementation does nothing and returns `Ok(())`. |
| 86 | + |
| 87 | +If `init` returns an error, the actor is considered failed and will not proceed to handle any messages. |
| 88 | + |
| 89 | +Use `init` to perform startup logic that depends on the actor being fully integrated into the system. |
| 90 | + |
| 91 | +## Spawning: `spawn` |
| 92 | + |
| 93 | +The `spawn` method provides a default implementation for creating a new actor from an existing one: |
| 94 | + |
| 95 | +```rust |
| 96 | +async fn spawn( |
| 97 | + cap: &impl cap::CanSpawn, |
| 98 | + params: Self::Params, |
| 99 | +) -> anyhow::Result<ActorHandle<Self>> { |
| 100 | + cap.spawn(params).await |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +In practice, `CanSpawn` is only implemented for `Instance<A>`, which represents a running actor. As a result, `Actor::spawn(...)` always constructs a child actor: the new actor receives a child ID and is linked to its parent through the runtime. |
| 105 | + |
| 106 | +## Detached Spawning: `spawn_detached` |
| 107 | + |
| 108 | +```rust |
| 109 | +async fn spawn_detached(params: Self::Params) -> Result<ActorHandle<Self>, anyhow::Error> { |
| 110 | + Proc::local().spawn("anon", params).await |
| 111 | +} |
| 112 | +``` |
| 113 | +This method creates a root actor on a fresh, isolated proc. |
| 114 | +- The proc is local-only and cannot forward messages externally. |
| 115 | +- The actor receives a unique root `ActorId` with no parent. |
| 116 | +- No supervision or linkage is established. |
| 117 | +- The actor is named `"anon"`. |
| 118 | + |
| 119 | +## Background Tasks: `spawn_server_task` |
| 120 | + |
| 121 | +```rust |
| 122 | +fn spawn_server_task<F>(future: F) -> JoinHandle<F::Output> |
| 123 | +where |
| 124 | + F: Future + Send + 'static, |
| 125 | + F::Output: Send + 'static, |
| 126 | +{ |
| 127 | + tokio::spawn(future) |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +This method provides a hook point for customizing how the runtime spawns background tasks. |
| 132 | + |
| 133 | +By default, it simply calls `tokio::spawn(...)` to run the given future on the Tokio executor. |
| 134 | + |
| 135 | +# Supervision Events: `handle_supervision_event` |
| 136 | + |
| 137 | +```rust |
| 138 | +async fn handle_supervision_event( |
| 139 | + &mut self, |
| 140 | + _this: &Instance<Self>, |
| 141 | + _event: &ActorSupervisionEvent, |
| 142 | +) -> Result<bool, anyhow::Error> { |
| 143 | + Ok(false) |
| 144 | +} |
| 145 | +``` |
| 146 | +This method is invoked when the runtime delivers an `ActorSupervisionEvent` to the actor — for example, when a child crashes or exits. |
| 147 | + |
| 148 | +By default, it returns `Ok(false)`, which indicates that the event was not handled by the actor. This allows the runtime to fall back on default behavior (e.g., escalation). |
| 149 | + |
| 150 | +Actors may override this to implement custom supervision logic. |
| 151 | + |
| 152 | +## Undeliverables: `handle_undeliverable_message` |
| 153 | + |
| 154 | +```rust |
| 155 | +async fn handle_undeliverable_message( |
| 156 | + &mut self, |
| 157 | + this: &Instance<Self>, |
| 158 | + Undeliverable(envelope): Undeliverable<MessageEnvelope>, |
| 159 | +) -> Result<(), anyhow::Error> { |
| 160 | + assert_eq!(envelope.sender(), this.self_id()); |
| 161 | + |
| 162 | + anyhow::bail!(UndeliverableMessageError::delivery_failure(&envelope)); |
| 163 | +} |
| 164 | +``` |
| 165 | +This method is called when a message sent by this actor fails to be delivered. |
| 166 | +- It asserts that the message was indeed sent by this actor. |
| 167 | +- Then it returns an error: `Err(UndeliverableMessageError::DeliveryFailure(...))` |
| 168 | + |
| 169 | +This signals that the actor considers this delivery failure to be a fatal error. You may override this method to suppress the failure or to implement custom fallback behavior. |
0 commit comments