Skip to content

Commit 2960002

Browse files
books: hyperactor-book: actor traits (#418)
Summary: Pull Request resolved: #418 new section: actor traits. Reviewed By: mariusae Differential Revision: D77690728 fbshipit-source-id: 46c46730ab493e4a4a0ea40d8d5c73e2d63d70ff
1 parent 4c5b0a3 commit 2960002

File tree

10 files changed

+468
-12
lines changed

10 files changed

+468
-12
lines changed

books/hyperactor-book/src/SUMMARY.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
# Summary
22

33
- [Introduction](./introduction.md)
4-
- [Macros](macros/index.md)
5-
- [`#[derive(Handler)]`](macros/handler.md)
6-
- [`#[derive(HandleClient)]`](macros/handle_client.md)
7-
- [`#[derive(RefClient)]`](macros/ref_client.md)
8-
- [`#[derive(Named)]`](macros/named.md)
9-
- [`#[export]`](macros/export.md)
10-
- [`#[forward]`](macros/forward.md)
114
- [References](references/index.md)
125
- [Syntax](references/syntax.md)
136
- [WorldId](references/world_id.md)
@@ -29,3 +22,18 @@
2922
- [Delivery Semantics](mailboxes/delivery.md)
3023
- [Multiplexers](mailboxes/multiplexer.md)
3124
- [Routers](mailboxes/routers.md)
25+
- [Actors](actors/index.md)
26+
- [Actor](actors/actor.md)
27+
- [Handler](actors/handler.md)
28+
- [RemoteableActor](actors/remotable_actor.md)
29+
- [Checkpointable](actors/checkpointable.md)
30+
- [RemoteActor](actors/remote_actor.md)
31+
- [Binds](actors/binds.md)
32+
- [RemoteHandles](actors/remote_handles.md)
33+
- [Macros](macros/index.md)
34+
- [`#[derive(Handler)]`](macros/handler.md)
35+
- [`#[derive(HandleClient)]`](macros/handle_client.md)
36+
- [`#[derive(RefClient)]`](macros/ref_client.md)
37+
- [`#[derive(Named)]`](macros/named.md)
38+
- [`#[export]`](macros/export.md)
39+
- [`#[forward]`](macros/forward.md)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 actors 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.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Binds
2+
3+
The `Binds` trait defines how an actor's ports are associated with the message types it can receive remotely.
4+
```rust
5+
pub trait Binds<A: Actor>: RemoteActor {
6+
fn bind(ports: &Ports<A>);
7+
}
8+
```
9+
Implementing `Binds<A>` allows the system to determine which messages can be routed to an actor instance of type `A`.
10+
11+
## Code Generation
12+
13+
In most cases, you do not implement this trait manually. Instead, the `#[export]` macro generates the appropriate `Binds<A>` implementation by registering the actor's supported message types.
14+
15+
For example:
16+
```rust
17+
#[hyperactor::export(
18+
spawn = true,
19+
handlers = [ShoppingList],
20+
)]
21+
struct ShoppingListActor;
22+
```
23+
Expands to:
24+
```rust
25+
impl Binds<ShoppingListActor> for ShoppingListActor {
26+
fn bind(ports: &Ports<Self>) {
27+
ports.bind::<ShoppingList>();
28+
}
29+
}
30+
```
31+
This ensures that the actor is correctly wired to handle messages of type `ShoppingList` when used in a remote messaging context.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Checkpointable
2+
3+
The `Checkpointable` trait enables an actor to define how its internal state can be saved and restored. This allows actors to participate in checkpointing and recovery mechanisms when supported by the surrounding system.
4+
5+
## Trait definition
6+
```rust
7+
#[async_trait]
8+
pub trait Checkpointable: Send + Sync + Sized {
9+
type State: RemoteMessage;
10+
11+
async fn save(&self) -> Result<Self::State, CheckpointError>;
12+
async fn load(state: Self::State) -> Result<Self, CheckpointError>;
13+
}
14+
```
15+
16+
## Associated Type
17+
18+
- `type State`: A serializable type representing the object's saved state. This must implement `RemoteMessage` so it can serialized and transmitted.
19+
20+
## `save`
21+
22+
Persists the current state of the component. Returns the Returns a `Self::State` value. If the operation fails, returns `CheckpointError::Save`.
23+
24+
## `load`
25+
26+
Reconstructs a new instance from a previously saved `Self::State`. If deserialization or reconstruction fails, returns `CheckpointError::Load`.
27+
28+
## `CheckpointError`
29+
30+
Errors returned by save and load operations:
31+
```rust
32+
pub enum CheckpointError {
33+
Save(anyhow::Error),
34+
Load(SeqId, anyhow::Error),
35+
}
36+
```
37+
38+
## Blanket Implementation
39+
40+
Any type `T` that implements `RemoteMessage` and `Clone` automatically satisfies `Checkpointable`:
41+
```rust
42+
#[async_trait]
43+
impl<T> Checkpointable for T
44+
where
45+
T: RemoteMessage + Clone,
46+
{
47+
type State = T;
48+
49+
async fn save(&self) -> Result<Self::State, CheckpointError> {
50+
Ok(self.clone())
51+
}
52+
53+
async fn load(state: Self::State) -> Result<Self, CheckpointError> {
54+
Ok(state)
55+
}
56+
}
57+
```
58+
This implementation uses `clone()` to produce a checkpoint and simply returns the cloned state in load.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# The `Handler` Trait
2+
3+
The `Handler` trait defines how an actor receives and responds to messages of a specific type.
4+
5+
Each message type that an actor can handle must be declared by implementing this trait. The runtime invokes the `handle` method when such a message is delivered.
6+
7+
```rust
8+
#[async_trait]
9+
pub trait Handler<M>: Actor {
10+
async fn handle(&mut self, this: &Context<Self>, message: M) -> Result<(), anyhow::Error>;
11+
}
12+
```
13+
14+
## Message Dispatch: `handle`
15+
16+
The `handle` method is invoked by the runtime whenever a message of type `M` arrives at a matching port on the actor.
17+
- message is the received payload.
18+
- this gives access to the actor's runtime context, including its identity, mailbox, and and any capabilities exposed by the `Instance` type (such as spawning or reference resolution).
19+
- The return value indicates whether the message was handled successfully.
20+
21+
An actor may implement `Handler<M>` multiple times — once for each message type `M` it supports.
22+
23+
## Built-in Handlers
24+
25+
The runtime provides implementations of `Handler<M>` for a few internal message types:
26+
27+
### `Handler<Signal>`
28+
29+
This is a marker implementation indicating that all actors can receive `Signal`. The handler is not expected to be invoked directly — its real behavior is implemented inside the runtime.
30+
```rust
31+
#[async_trait]
32+
impl<A: Actor> Handler<Signal> for A {
33+
async fn handle(
34+
&mut self,
35+
_this: &Context<Self>,
36+
_message: Signal,
37+
) -> Result<(), anyhow::Error> {
38+
unimplemented!("signal handler should not be called directly")
39+
}
40+
}
41+
```
42+
43+
### `Handler<Undeliverable<MessageEnvelope>>`
44+
45+
```rust
46+
#[async_trait]
47+
impl<A, M> Handler<IndexedErasedUnbound<M>> for A
48+
where
49+
A: Handler<M>,
50+
M: Castable,
51+
{
52+
async fn handle(
53+
&mut self,
54+
this: &Context<Self>,
55+
msg: IndexedErasedUnbound<M>,
56+
) -> anyhow::Result<()> {
57+
let message = msg.downcast()?.bind()?;
58+
Handler::handle(self, this, message).await
59+
}
60+
}
61+
```
62+
This implementation allows an actor to transparently handle erased, rebound messages of type `M`, provided it already implements `Handler<M>`.
63+
64+
This construct is used in the implementation of **accumulation**, a communication pattern where a message is multicast to multiple recipients and their replies are gathered—possibly through intermediate actors—before being sent back to the original sender.
65+
66+
To enable this, messages are unbound at the sender: reply ports (`PortRef`s) are extracted into a `Bindings` object, allowing intermediate nodes to rewrite those ports to point back to themselves. This ensures that replies from downstream actors are routed through the intermediate, enabling reply collection and reduction.
67+
68+
Once a message reaches its destination, it is rebound by merging the updated bindings back into the message. The `Handler<IndexedErasedUnbound<M>>` implementation automates this by recovering the typed message `M` and dispatching it to the actor's existing `Handler<M>` implementation.
69+
70+
This allows actors to remain unaware of accumulation mechanics—they can just implement `Handler<M>` as usual.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Actors
2+
3+
Hyperactor programs are structured around actors: isolated state machines that process messages asynchronously.
4+
5+
Each actor runs in isolation, and maintains private internal state. Actors interact with the outside world through typed message ports and follow strict lifecycle semantics managed by the runtime.
6+
7+
This chapter introduces the actor system in hyperactor. We'll cover:
8+
9+
- The [`Actor`](./actor.md) trait and its lifecycle hooks
10+
- The [`Handler`](./handler.md) trait for defining message-handling behavior
11+
- The [`RemotableActor`](./remotable_actor.md) trait for enabling remote spawning
12+
- The [`Checkpointable`](./checkpointable.md) trait for supporting actor persistence and recovery
13+
- The [`RemoteActor`](./remote_actor.md) marker trait for remotely referencable types
14+
- The [`Binds`](./binds.md) trait for wiring exported ports to reference types
15+
- The [`RemoteHandles`](./remote_handles.md) trait for associating message types with a reference
16+
17+
Actors are always instantiated with parameters and bound to a mailbox, enabling them to participate in reliable message-passing systems. Supervision, checkpointing, and references all build upon this core abstraction.

0 commit comments

Comments
 (0)