Skip to main content

hyperactor/actor/
remote.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Management of actor registration for remote spawning.
10
11use std::any::TypeId;
12use std::collections::HashMap;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::LazyLock;
16
17use crate::Actor;
18use crate::ActorEnvironment;
19use crate::AnyActorHandle;
20use crate::Data;
21use crate::id::Uid;
22use crate::proc::InstanceCell;
23use crate::proc::Proc;
24
25/// The offset of user-defined ports (i.e., arbitrarily bound).
26pub const USER_PORT_OFFSET: u64 = 1024;
27
28/// Register an actor type so that it can be spawned remotely. The actor
29/// type must implement [`typeuri::Named`], which will be used to identify
30/// the actor globally.
31///
32/// Example:
33///
34/// ```ignore
35/// struct MyActor { ... }
36///
37/// register_spawnable!(MyActor);
38/// ```
39#[macro_export]
40macro_rules! register_spawnable {
41    ($actor:ty) => {
42        const _: () = {
43            static NAME: std::sync::LazyLock<&'static str> = std::sync::LazyLock::new(|| {
44                <$actor as $crate::internal_macro_support::typeuri::Named>::typename()
45            });
46
47            $crate::internal_macro_support::inventory::submit! {
48                $crate::actor::remote::SpawnableActor {
49                    name: &NAME,
50                    gspawn_root_bind: <$actor as $crate::actor::RemoteSpawn>::gspawn_root_bind,
51                    gspawn_child: <$actor as $crate::actor::RemoteSpawn>::gspawn_child,
52                    get_type_id: <$actor as $crate::actor::RemoteSpawn>::get_type_id,
53                }
54            }
55        };
56    };
57}
58
59/// A type-erased actor registration entry. These are constructed via
60/// [`crate::register_spawnable`].
61#[derive(Debug)]
62pub struct SpawnableActor {
63    /// A URI that globally identifies an actor. It is an error to register
64    /// multiple actors with the same name.
65    ///
66    /// This is a LazyLock because the names are provided through a trait
67    /// implementation, which can not yet be `const`.
68    pub name: &'static LazyLock<&'static str>,
69
70    /// Type-erased root spawn function. This is the type's
71    /// [`RemoteSpawn::gspawn_root_bind`]. The `ActorEnvironment` is passed to
72    /// the constructor and stored unchanged on the new instance.
73    pub gspawn_root_bind: fn(
74        &Proc,
75        Uid,
76        Data,
77        ActorEnvironment,
78    ) -> Pin<
79        Box<dyn Future<Output = Result<crate::ActorAddr, anyhow::Error>> + Send>,
80    >,
81
82    /// Type-erased child spawn function. This is the type's
83    /// [`RemoteSpawn::gspawn_child`]. The `ActorEnvironment` is passed to the
84    /// constructor and stored unchanged on the new instance.
85    pub gspawn_child:
86        fn(
87            &Proc,
88            InstanceCell,
89            Uid,
90            Data,
91            ActorEnvironment,
92        ) -> Pin<Box<dyn Future<Output = Result<AnyActorHandle, anyhow::Error>> + Send>>,
93
94    /// A function to retrieve the type id of the actor itself. This is
95    /// used to translate a concrete type to a global name.
96    pub get_type_id: fn() -> TypeId,
97}
98
99inventory::collect!(SpawnableActor);
100
101/// Registry of actors linked into this image and registered by way of
102/// [`crate::register_spawnable`].
103#[derive(Debug)]
104pub struct Remote {
105    by_name: HashMap<&'static str, &'static SpawnableActor>,
106    by_type_id: HashMap<TypeId, &'static SpawnableActor>,
107}
108
109impl Remote {
110    /// Construct a registry. Panics if there are conflicting registrations.
111    pub fn collect() -> Self {
112        let mut by_name = HashMap::new();
113        let mut by_type_id = HashMap::new();
114        for entry in inventory::iter::<SpawnableActor> {
115            if by_name.insert(**entry.name, entry).is_some() {
116                panic!("actor name {} registered multiple times", **entry.name);
117            }
118            let type_id = (entry.get_type_id)();
119            if by_type_id.insert(type_id, entry).is_some() {
120                panic!(
121                    "type id {:?} ({}) registered multiple times",
122                    type_id, **entry.name
123                );
124            }
125        }
126        Self {
127            by_name,
128            by_type_id,
129        }
130    }
131
132    /// Return the process-wide remote spawn registry.
133    pub fn global() -> &'static Self {
134        static REMOTE: LazyLock<Remote> = LazyLock::new(Remote::collect);
135        &REMOTE
136    }
137
138    /// Returns the name of the provided actor, if registered.
139    pub fn name_of<A: Actor>(&self) -> Option<&'static str> {
140        self.by_type_id
141            .get(&TypeId::of::<A>())
142            .map(|entry| **entry.name)
143    }
144
145    /// Spawns the actor with the provided sender, actor uid,
146    /// and serialized parameters. Returns an error if the actor is not
147    /// registered, or if the actor's spawn fails.
148    pub async fn gspawn(
149        &self,
150        proc: &Proc,
151        actor_type: &str,
152        actor_uid: Uid,
153        params: Data,
154        environment: ActorEnvironment,
155    ) -> Result<crate::ActorAddr, anyhow::Error> {
156        let entry = self
157            .by_name
158            .get(actor_type)
159            .ok_or_else(|| anyhow::anyhow!("actor type {} not registered", actor_type))?;
160        (entry.gspawn_root_bind)(proc, actor_uid, params, environment).await
161    }
162
163    /// Spawns the actor as a child of the provided parent. Returns an
164    /// erased lifecycle handle.
165    pub async fn gspawn_child(
166        &self,
167        proc: &Proc,
168        parent: InstanceCell,
169        actor_type: &str,
170        actor_uid: Uid,
171        params: Data,
172    ) -> Result<AnyActorHandle, anyhow::Error> {
173        let environment = parent.actor_environment().clone();
174        self.gspawn_child_in_environment(proc, parent, actor_type, actor_uid, params, environment)
175            .await
176    }
177
178    /// Spawn a child with an explicit environment instead of inheriting from
179    /// its physical parent.
180    pub(crate) async fn gspawn_child_in_environment(
181        &self,
182        proc: &Proc,
183        parent: InstanceCell,
184        actor_type: &str,
185        actor_uid: Uid,
186        params: Data,
187        environment: ActorEnvironment,
188    ) -> Result<AnyActorHandle, anyhow::Error> {
189        let entry = self
190            .by_name
191            .get(actor_type)
192            .ok_or_else(|| anyhow::anyhow!("actor type {} not registered", actor_type))?;
193        (entry.gspawn_child)(proc, parent, actor_uid, params, environment).await
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use std::assert_matches;
200
201    use async_trait::async_trait;
202
203    use super::*;
204    use crate as hyperactor; // for macros
205    use crate::Context;
206    use crate::Handler;
207    use crate::RemoteSpawn;
208    use crate::id::Label;
209
210    #[derive(Debug)]
211    #[hyperactor::export(())]
212    struct MyActor;
213
214    #[async_trait]
215    impl Actor for MyActor {}
216
217    #[async_trait]
218    impl RemoteSpawn for MyActor {
219        type Params = bool;
220
221        async fn new(params: bool, _environment: &ActorEnvironment) -> Result<Self, anyhow::Error> {
222            if params {
223                Ok(MyActor)
224            } else {
225                Err(anyhow::anyhow!("some failure"))
226            }
227        }
228    }
229
230    #[async_trait]
231    impl Handler<()> for MyActor {
232        async fn handle(&mut self, _cx: &Context<Self>, _message: ()) -> anyhow::Result<()> {
233            unimplemented!()
234        }
235    }
236
237    register_spawnable!(MyActor);
238
239    #[derive(Debug, Default)]
240    #[hyperactor::export(())]
241    struct GenericActor<T>(std::marker::PhantomData<T>);
242
243    #[async_trait]
244    impl<T: Send + 'static> Actor for GenericActor<T> {}
245
246    #[async_trait]
247    impl<T: Send + Sync + 'static> Handler<()> for GenericActor<T> {
248        async fn handle(&mut self, _cx: &Context<Self>, _message: ()) -> anyhow::Result<()> {
249            unimplemented!()
250        }
251    }
252
253    register_spawnable!(GenericActor<u64>);
254    register_spawnable!(GenericActor<bool>);
255
256    #[tokio::test]
257    async fn test_registry() {
258        let remote = Remote::collect();
259        assert_matches!(
260            remote.name_of::<MyActor>(),
261            Some("hyperactor::actor::remote::tests::MyActor")
262        );
263        assert_matches!(
264            remote.name_of::<GenericActor<u64>>(),
265            Some("hyperactor::actor::remote::tests::GenericActor<u64>")
266        );
267        assert_matches!(
268            remote.name_of::<GenericActor<bool>>(),
269            Some("hyperactor::actor::remote::tests::GenericActor<bool>")
270        );
271        assert_ne!(
272            <GenericActor<u64> as typeuri::Named>::typename(),
273            <GenericActor<bool> as typeuri::Named>::typename()
274        );
275
276        let _ = remote
277            .gspawn(
278                &Proc::isolated(),
279                "hyperactor::actor::remote::tests::MyActor",
280                Uid::instance(Label::new("actor").unwrap()),
281                bincode::serde::encode_to_vec(true, bincode::config::legacy()).unwrap(),
282                ActorEnvironment::default(),
283            )
284            .await
285            .unwrap();
286
287        let err = remote
288            .gspawn(
289                &Proc::isolated(),
290                "hyperactor::actor::remote::tests::MyActor",
291                Uid::instance(Label::new("actor").unwrap()),
292                bincode::serde::encode_to_vec(false, bincode::config::legacy()).unwrap(),
293                ActorEnvironment::default(),
294            )
295            .await
296            .unwrap_err();
297
298        assert_eq!(err.to_string().as_str(), "some failure");
299    }
300
301    #[tokio::test]
302    async fn test_instance_gspawn_child_returns_erased_handle() {
303        let proc = Proc::isolated();
304        let parent = proc.client("parent");
305
306        let child = parent
307            .gspawn(
308                "hyperactor::actor::remote::tests::MyActor",
309                bincode::serde::encode_to_vec(true, bincode::config::legacy()).unwrap(),
310            )
311            .await
312            .unwrap();
313
314        assert!(!child.actor_id().is_root());
315        assert!(child.downcast::<MyActor>().is_some());
316        assert!(child.downcast::<GenericActor<u64>>().is_none());
317
318        child.stop("test").unwrap();
319        child.await;
320    }
321
322    #[tokio::test]
323    async fn test_instance_gspawn_uid_uses_explicit_uid() {
324        let proc = Proc::isolated();
325        let parent = proc.client("parent");
326        let uid = Uid::instance(Label::new("child").unwrap());
327
328        let child = parent
329            .gspawn_uid(
330                "hyperactor::actor::remote::tests::MyActor",
331                uid.clone(),
332                bincode::serde::encode_to_vec(true, bincode::config::legacy()).unwrap(),
333            )
334            .await
335            .unwrap();
336
337        assert_eq!(child.actor_id().uid(), &uid);
338
339        child.stop("test").unwrap();
340        child.await;
341    }
342
343    #[tokio::test]
344    async fn test_instance_gspawn_uid_rejects_duplicate_uid() {
345        let proc = Proc::isolated();
346        let parent = proc.client("parent");
347        let uid = Uid::instance(Label::new("child").unwrap());
348
349        let child = parent
350            .gspawn_uid(
351                "hyperactor::actor::remote::tests::MyActor",
352                uid.clone(),
353                bincode::serde::encode_to_vec(true, bincode::config::legacy()).unwrap(),
354            )
355            .await
356            .unwrap();
357
358        let err = parent
359            .gspawn_uid(
360                "hyperactor::actor::remote::tests::MyActor",
361                uid,
362                bincode::serde::encode_to_vec(true, bincode::config::legacy()).unwrap(),
363            )
364            .await
365            .unwrap_err();
366
367        assert!(err.to_string().contains("has already been spawned"));
368
369        child.stop("test").unwrap();
370        child.await;
371    }
372}