Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/books/hyperactor-book/src/actors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This chapter introduces the actor system in hyperactor. We'll cover:

- The [`Actor`](./actor.md) trait and its lifecycle hooks
- The [`Handler`](./handler.md) trait for defining message-handling behavior
- The [`RemotableActor`](./remotable_actor.md) trait for enabling remote spawning
- The [`RemoteSpawn`](./remotable_actor.md) trait for enabling remote spawning
- The [`Checkpointable`](./checkpointable.md) trait for supporting actor persistence and recovery
- The [`Referable`](./remote_actor.md) marker trait for remotely referencable types
- The [`Binds`](./binds.md) trait for wiring exported ports to reference types
Expand Down
53 changes: 12 additions & 41 deletions docs/source/books/hyperactor-book/src/actors/remotable_actor.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
# The `RemoteableActor` Trait

```rust
pub trait RemotableActor: Actor
where
Self::Params: RemoteMessage,
{
pub trait RemoteSpawn: Actor + Referable + Binds<Self> {
/// The type of parameters used to instantiate the actor remotely.
type Params: RemoteMessage;

/// Creates a new actor instance given its instantiation parameters.
async fn new(params: Self::Params) -> anyhow::Result<Self>;

fn gspawn(
proc: &Proc,
name: &str,
serialized_params: Data,
) -> Pin<Box<dyn Future<Output = Result<ActorId, anyhow::Error>> + Send>>;
) -> Pin<Box<dyn Future<Output = Result<ActorId, anyhow::Error>> + Send>> { /* default impl. */}

fn get_type_id() -> TypeId {
TypeId::of::<Self>()
}
}
```
The `RemotableActor` trait marks an actor type as spawnable across process boundaries. It enables hyperactor's remote spawning and registration system, allowing actors to be created from serialized parameters in a different `Proc`.
The `RemoteSpawn` trait marks an actor type as spawnable across process boundaries. It enables hyperactor's remote spawning and registration system, allowing actors to be created from serialized parameters in a different `Proc`.

## Requirements
- The actor type must also implement `Actor`.
- Its `Params` type (used in `Actor::new`) must implement `RemoteMessage`, so it can be serialized and transmitted over the network.
- Its `Params` type (used in `RemoteSpawn::new`) must implement `RemoteMessage`, so it can be serialized and transmitted over the network.
- `new` creates a new instance of the actor given its parameters

## `gspawn`
```rust
Expand All @@ -39,41 +43,8 @@ The method deserializes the parameters, creates the actor, and returns its `Acto

This is used internally by hyperactor's remote actor registry and `spawn` services. Ordinary users generally don't call this directly.

> **Note:** This is not an `async fn` because `RemotableActor` must be object-safe.
> **Note:** This is not an `async fn` because `RemoteSpawn` must be object-safe.

## `get_type_id`

Returns a stable `TypeId` for the actor type. Used to identify actor types at runtime—e.g., in registration tables or type-based routing logic.

## Blanket Implementation

The RemotableActor trait is automatically implemented for any actor type `A` that:
- implements `Actor` and `Referable`,
- and whose `Params` type implements `RemoteMessage`.

This allows `A` to be remotely registered and instantiated from serialized data, typically via the runtime's registration mechanism.

```rust
impl<A> RemotableActor for A
where
A: Actor + Referable,
A: Binds<A>,
A::Params: RemoteMessage,
{
fn gspawn(
proc: &Proc,
name: &str,
serialized_params: Data,
) -> Pin<Box<dyn Future<Output = Result<ActorId, anyhow::Error>> + Send>> {
let proc = proc.clone();
let name = name.to_string();
Box::pin(async move {
let handle = proc
.spawn::<A>(&name, bincode::deserialize(&serialized_params)?)
.await?;
Ok(handle.bind::<A>().actor_id)
})
}
}
```
Note the `Binds<A>` bound: this trait specifies how an actor's ports are wired determining which message types the actor can receive remotely. The resulting `ActorId` corresponds to a port-bound, remotely callable version of the actor.
4 changes: 2 additions & 2 deletions docs/source/books/hyperactor-book/src/macros/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The macro expands to include:
- A `Binds<Self>` implementation that registers supported message types
- Implementations of `RemoteHandles<T>` for each type in the `handlers = [...]` list
- A `Referable` marker implementation
- If `spawn = true`, a `RemotableActor` implementation and an inventory registration of the `spawn` function.
- If `spawn = true`, the actor's `RemoteSpawn` implementation is registered in the remote actor inventory.

This enables the actor to be:
- Spawned dynamically by name
Expand Down Expand Up @@ -46,7 +46,7 @@ impl Named for ShoppingListActor {
```
If `spawn = true`, the macro also emits:
```rust
impl RemotableActor for ShoppingListActor {}
impl RemoteSpawn for ShoppingListActor {}
```
This enables remote spawning via the default `gspawn` provided by a blanket implementation.

Expand Down
7 changes: 5 additions & 2 deletions hyper/src/commands/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use hyperactor::HandleClient;
use hyperactor::Handler;
use hyperactor::Named;
use hyperactor::RefClient;
use hyperactor::RemoteSpawn;
use hyperactor::channel::ChannelAddr;
use hyperactor::forward;
use hyperactor::id;
Expand Down Expand Up @@ -210,7 +211,7 @@ enum DemoMessage {
Error(String, #[reply] OncePortRef<()>),
}

#[derive(Debug, Default, Actor)]
#[derive(Debug, Default)]
#[hyperactor::export(
spawn = true,
handlers = [
Expand All @@ -219,6 +220,8 @@ enum DemoMessage {
)]
struct DemoActor;

impl Actor for DemoActor {}

#[async_trait]
#[forward(DemoMessage)]
impl DemoMessageHandler for DemoActor {
Expand All @@ -243,7 +246,7 @@ impl DemoMessageHandler for DemoActor {

async fn spawn_child(&mut self, cx: &Context<Self>) -> Result<ActorRef<Self>, anyhow::Error> {
tracing::info!("demo: spawn child");
Ok(Self::spawn(cx, ()).await?.bind())
Ok(Self.spawn(cx).await?.bind())
}

async fn error(&mut self, _cx: &Context<Self>, message: String) -> Result<(), anyhow::Error> {
Expand Down
14 changes: 4 additions & 10 deletions hyperactor/example/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,11 @@ struct GetItemCount<C> {
}

// Define an actor.
#[derive(Debug, Actor, Default)]
#[hyperactor::export(
spawn = true,
handlers = [
ShoppingList,
ClearList,
GetItemCount<usize>,
],
)]
#[derive(Debug, Default)]
struct ShoppingListActor(HashSet<String>);

impl Actor for ShoppingListActor {}

// ShoppingListHandler is the trait generated by derive(Handler) above.
// We implement the trait here for the actor, defining a handler for
// each ShoppingList message.
Expand Down Expand Up @@ -140,7 +134,7 @@ async fn main() -> Result<(), anyhow::Error> {

// Spawn our actor, and get a handle for rank 0.
let shopping_list_actor: hyperactor::ActorHandle<ShoppingListActor> =
proc.spawn("shopping", ()).await?;
proc.spawn("shopping", ShoppingListActor::default()).await?;
let shopping_api: hyperactor::ActorRef<ShoppingApi> = shopping_list_actor.bind();
// We join the system, so that we can send messages to actors.
let (client, _) = proc.instance("client").unwrap();
Expand Down
28 changes: 18 additions & 10 deletions hyperactor/example/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@ use hyperactor::Handler;
use hyperactor::Instance;
use hyperactor::Named;
use hyperactor::PortRef;
use hyperactor::RemoteSpawn;
use hyperactor::proc::Proc;
use serde::Deserialize;
use serde::Serialize;

#[derive(Debug, Actor, Default)]
#[derive(Debug, Default)]
struct CounterActor {
subscribers: Vec<PortRef<u64>>,
n: u64,
}

impl Actor for CounterActor {}

#[derive(Serialize, Deserialize, Debug, Named)]
struct Subscribe(PortRef<u64>);

Expand All @@ -52,15 +55,14 @@ struct CountClient {
counter: PortRef<Subscribe>,
}

#[async_trait]
impl Actor for CountClient {
// Where to send subscribe messages.
type Params = PortRef<Subscribe>;

async fn new(counter: PortRef<Subscribe>) -> Result<Self, anyhow::Error> {
Ok(Self { counter })
impl CountClient {
fn new(counter: PortRef<Subscribe>) -> Self {
Self { counter }
}
}

#[async_trait]
impl Actor for CountClient {
async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
// Subscribe to the counter on initialization. We give it our u64 port to report
// messages back to.
Expand All @@ -81,13 +83,19 @@ impl Handler<u64> for CountClient {
async fn main() {
let proc = Proc::local();

let counter_actor: ActorHandle<CounterActor> = proc.spawn("counter", ()).await.unwrap();
let counter_actor: ActorHandle<CounterActor> = proc
.spawn("counter", CounterActor::default())
.await
.unwrap();

for i in 0..10 {
// Spawn new "countees". Every time each subscribes, the counter broadcasts
// the count to everyone.
let _countee_actor: ActorHandle<CountClient> = proc
.spawn(&format!("countee_{}", i), counter_actor.port().bind())
.spawn(
&format!("countee_{}", i),
CountClient::new(counter_actor.port().bind()),
)
.await
.unwrap();
#[allow(clippy::disallowed_methods)]
Expand Down
Loading
Loading