Skip to content

Commit 4226c32

Browse files
shayne-fletchermeta-codesync[bot]
authored andcommitted
remote actor registry docs
Differential Revision: D88089606
1 parent 52b0f4b commit 4226c32

File tree

2 files changed

+230
-3
lines changed

2 files changed

+230
-3
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,228 @@
11
# Remote Registry
2+
3+
The `hyperactor::actor::remote` module provides the process-local registry for remote-spawnable actors. It is the counterpart to `RemoteSpawn`: given actor types that implement `RemoteSpawn` and are registered with `remote!`, this module discovers them at runtime and allows actors to be spawned by their global type name. The implementation uses the [`inventory`](https://docs.rs/inventory/0.3.21/inventory/index.html) crate to collect registrations contributed from any crate linked into the application.
4+
5+
## Registration model and the `remote!` macro
6+
7+
Remote-spawnable actors are registered using the `remote!` macro. Given an actor type that implements [`RemoteSpawn`](./remote_spawn.md) and `Named`, `remote!(MyActor)` arranges for a `SpawnableActor` record to be submitted to a global registry using the [`inventory`](https://docs.rs/inventory/0.3.21/inventory/index.html) crate.
8+
9+
In idiomatic use:
10+
```rust
11+
#[derive(Debug)]
12+
#[hyperactor::export(handlers = [()])]
13+
struct MyActor;
14+
15+
#[async_trait::async_trait]
16+
impl Actor for MyActor {}
17+
18+
#[async_trait::async_trait]
19+
impl RemoteSpawn for MyActor {
20+
type Params = bool;
21+
22+
async fn new(params: bool) -> anyhow::Result<Self> {
23+
if params {
24+
Ok(MyActor)
25+
} else {
26+
Err(anyhow::anyhow!("some failure"))
27+
}
28+
}
29+
}
30+
31+
remote!(MyActor);
32+
```
33+
34+
Conceptually, the `remote!` invocation expands to something like:
35+
```rust
36+
static MY_ACTOR_NAME: std::sync::LazyLock<&'static str> =
37+
std::sync::LazyLock::new(|| <MyActor as hyperactor::data::Named>::typename());
38+
39+
inventory::submit! {
40+
hyperactor::actor::remote::SpawnableActor {
41+
name: &MY_ACTOR_NAME,
42+
gspawn: <MyActor as hyperactor::actor::RemoteSpawn>::gspawn,
43+
get_type_id: <MyActor as hyperactor::actor::RemoteSpawn>::get_type_id,
44+
}
45+
}
46+
```
47+
The real macro uses `paste!` to synthesize the `MY_ACTOR_NAME` identifier and the crate-local paths, but the effect is the same:
48+
- compute a **global type name** for `MyActor` via `Named::typename()`,
49+
- build a `SpawnableActor` record that points at `MyActor`s `RemoteSpawn` implementation, and
50+
- submit that record into the inventory of `SpawnableActor` entries.
51+
52+
At runtime, the `Remote` registry (described below) discovers all such submissions via `inventory::iter::<SpawnableActor>` and makes them available for lookup and spawning by global type name.
53+
54+
## `SpawnableActor`: registration records
55+
56+
A `SpawnableActor` is the type-erased registration record produced by `remote!`. Each remotely spawnable actor type contributes exactly one of these records to the process. The registry discovers them at runtime and uses them to look up actors by global type name and to invoke their type-erased constructor.
57+
```rust
58+
#[derive(Debug)]
59+
pub struct SpawnableActor {
60+
/// A URI that globally identifies an actor.
61+
pub name: &'static LazyLock<&'static str>,
62+
63+
pub gspawn: fn(
64+
&Proc,
65+
&str,
66+
Data,
67+
) -> Pin<Box<dyn Future<Output = Result<ActorId, anyhow::Error>> + Send>>,
68+
69+
pub get_type_id: fn() -> TypeId,
70+
}
71+
```
72+
- `name` is the actor's global type name, obtained from `Named::typename()`. This is the string that appears on the wire in a remote-spawn request.
73+
- `gspawn` is the type-erased entry point for constructing the actor on a remote `Proc`. It is backed by the actor's `RemoteSpawn::gspawn` implementation and handles deserializing parameters and invoking `RemoteSpawn::new(...).await`.
74+
- `get_type_id` returns the actor's `TypeId`, allowing the registry to map a concrete Rust type back to it's registration entry.
75+
76+
Users never construct a `SpawnableActor` manually; these records are generated automatically by the `remote!` macro.
77+
78+
## The `Remote` registry
79+
80+
The `Remote` type is the process-local registry of remote-spawnable actors. It is built from all `SpawnableActor` records submitted via `remote!` and exposed through two lookups: by global type name and by `TypeId`.
81+
```rust
82+
#[derive(Debug)]
83+
pub struct Remote {
84+
by_name: HashMap<&'static str, &'static SpawnableActor>,
85+
by_type_id: HashMap<TypeId, &'static SpawnableActor>,
86+
}
87+
```
88+
89+
- `by_type_id` is used by `Remote::name_of::<A>()`, which starts from a concrete type `A: Actor` and looks up its `SpawnableActor` in order to read the registered name.
90+
- `by_name` is used by `Remote::gspawn`, which starts from a global type name string received over the wire and looks up the corresponding `SpawnableActor` in order to call its `gspawn` function.
91+
92+
This is why the registry maintains two maps: one keyed by `TypeId` for caller-side APIs that start from a Rust type, and one keyed by string name for remote services that start from a serialized request.
93+
94+
### Building the registry: `Remote::collect`
95+
96+
```rust
97+
impl Remote {
98+
pub fn collect() -> Self {
99+
let mut by_name = HashMap::new();
100+
let mut by_type_id = HashMap::new();
101+
for entry in inventory::iter::<SpawnableActor> {
102+
if by_name.insert(**entry.name, entry).is_some() {
103+
panic!("actor name {} registered multiple times", **entry.name);
104+
}
105+
let type_id = (entry.get_type_id)();
106+
if by_type_id.insert(type_id, entry).is_some() {
107+
panic!(
108+
"type id {:?} ({}) registered multiple times",
109+
type_id, **entry.name
110+
);
111+
}
112+
}
113+
Self { by_name, by_type_id }
114+
}
115+
}
116+
```
117+
`Remote::collect` walks `inventory::iter::<SpawnableActor>` and builds two maps:
118+
- `by_name` for lookup up actors by their global type name (the string that appears on the wire), and
119+
- `by_type_id` for looking up the registration associated with a concrete Rust type.
120+
121+
It enforces that no two actors register the same global name or `TypeId` in a single binary.
122+
123+
The result is a process-local view of all remote-spawnable actors; callers are free to construct this registry once and reuse it or to rebuild it on demand, depending on their needs.
124+
125+
### Looking up names: `Remote::name_of`
126+
127+
```rust
128+
impl Remote {
129+
pub fn name_of<A: Actor>(&self) -> Option<&'static str> {
130+
self.by_type_id
131+
.get(&TypeId::of::<A>())
132+
.map(|entry| **entry.name)
133+
}
134+
}
135+
```
136+
`name_of` resolves a concrete `A: RemoteSpawn` to its registered global type name string.
137+
138+
Given a concrete `A: Actor`, `name_of` returns the string name that was registered via `remote!`. This is used by caller-side APIs that *start from a Rust type* and need to put a string type name on the wire for a remote spawn request.
139+
140+
For example, `spawn_with_name_inner` constructs an `ActorSpec` by first resolving the type `A` to its global name:
141+
```rust
142+
impl ProcMeshRef {
143+
async fn spawn_with_name_inner<A: RemoteSpawn>(
144+
&self,
145+
cx: &impl context::Actor,
146+
name: Name,
147+
params: &A::Params,
148+
) -> v1::Result<ActorMesh<A>>
149+
{
150+
let remote = Remote::collect();
151+
152+
// Caller starts from the Rust type `A` → resolve to a global type name.
153+
let actor_type = remote
154+
.name_of::<A>()
155+
.ok_or(Error::ActorTypeNotRegistered(type_name::<A>().to_string()))?
156+
.to_string();
157+
158+
let serialized_params = bincode::serialize(params)?;
159+
let agent_mesh = self.agent_mesh();
160+
161+
agent_mesh.cast(
162+
cx,
163+
resource::CreateOrUpdate::<mesh_agent::ActorSpec> {
164+
name: name.clone(),
165+
rank: Default::default(),
166+
spec: mesh_agent::ActorSpec {
167+
actor_type: actor_type.clone(), // ← string name sent over the wire
168+
params_data: serialized_params.clone(),
169+
},
170+
},
171+
)?;
172+
173+
...
174+
}
175+
```
176+
Here the caller begins with the Rust type `A` and uses `name_of::<A>()` to obtain the global name that will be sent to the remote `Proc`. On the receiving side, the registry takes the global type name string, resolves it to a `SpawnableActor`, and then invokes that entry's `gspawn` function to construct the actor.
177+
178+
### Spawning by name: `Remote::gspawn`
179+
```rust
180+
impl Remote {
181+
pub async fn gspawn(
182+
&self,
183+
proc: &Proc,
184+
actor_type: &str,
185+
actor_name: &str,
186+
params: Data,
187+
) -> Result<ActorId, anyhow::Error> {
188+
let entry = self
189+
.by_name
190+
.get(actor_type)
191+
.ok_or_else(|| anyhow::anyhow!("actor type {} not registered", actor_type))?;
192+
(entry.gspawn)(proc, actor_name, params).await
193+
}
194+
}
195+
```
196+
`gspawn` is the **name -> spawn** path. It starts from a a global type name string (`actor_type`), looks up the corresponding `SpawnableActor` in `by_name`, and invokes its `gspawn` function. That function is the type-erased adapter provided by the actor's `RemoteSpawn` implementation: it deserializes `params` into `RemoteSpawn::Params`, calls `RemoteSpawn::new`, wires the actor into the given `Proc`, and returns the resulting `ActorId`.
197+
198+
In a typical setup, higher-level code in a separate crate starts from a generic `A: RemoteSpawn`, uses `Remote::name_of::<A>()` to obtain the global type name, serializes `A::Params`, and sends a request containing:
199+
- `actor_type`: that global type name, and
200+
- `params_data`: serialized `A::Params`.
201+
202+
On the receiving side, a control-plane or management actor calls:
203+
```rust
204+
self.remote.gspawn(&self.proc, &actor_type, &actor_name, params_data).await
205+
```
206+
to look up the corresponding `SpawnableActor` by `actor_type` and invoke its `gspawn` entry point. That call deserializes `params_data`, constructs the actor, wires it into `self.proc` and returns the new `ActorId`.
207+
208+
## Putting it together
209+
210+
Remote spawning in hyperactor involves two complementary pieces:
211+
1. **Type-level registration** Each `A: RemoteSpawn` contributes a `SpawnableActor` record when the user writes `remote!(A)`. These records are collected at runtime by `Remote::collect()`.
212+
2. **Data-level spawn requests** Higher-level code starts from a concrete actor type (e.g. `A: RemoteSpawn`), uses `Remote::name_of::<A>()` to obtain it's global type name, serializes `A::Params`, and sends a request containing those two pieces of data.
213+
214+
On the receiving side, a management component reconstructs the actor by calling:
215+
```rust
216+
remote.gspawn(&proc, &actor_type, &actor_name, params_data).await
217+
```
218+
219+
`Remote::gspawn` uses the global type name to locate the correct `SpawnableActor` and invokes its type-erased `gspawn` function, which:
220+
- deserializes `params_data`,
221+
- calls `A::new(params).await`, and
222+
- installs the actor into the provided `Proc`.
223+
224+
The `Remote` registry is thus the bridge between:
225+
- **Rust types** implementing `RemoteSpawn` (which define how to construct the actor), and
226+
- **runtime identifiers** (global type names) used in serialized remote-spawn requests.
227+
228+
This decoupling allows remote spawning to work across processes without requiring shared type information at compile time: all that crosses the wire is a global name and a parameter payload, and the receiving process uses its local registry to handle construction.

hyperactor/src/actor/remote.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ pub struct SpawnableActor {
5858
/// multiple actors with the same name.
5959
///
6060
/// This is a LazyLock because the names are provided through a trait
61-
/// implemetnation, which can not yet be `const`.
61+
/// implementation, which can not yet be `const`.
6262
pub name: &'static LazyLock<&'static str>,
6363

64-
/// Type-erased spawn function. This is the type's [`Actor::gspawn`].
64+
/// Type-erased spawn function. This is the type's [`RemoteSpawn::gspawn`].
6565
pub gspawn: fn(
6666
&Proc,
6767
&str,
@@ -76,7 +76,7 @@ pub struct SpawnableActor {
7676
inventory::collect!(SpawnableActor);
7777

7878
/// Registry of actors linked into this image and registered by way of
79-
/// [`crate::register`].
79+
/// [`crate::remote`].
8080
#[derive(Debug)]
8181
pub struct Remote {
8282
by_name: HashMap<&'static str, &'static SpawnableActor>,

0 commit comments

Comments
 (0)