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::Data;
19use crate::proc::Proc;
20use crate::reference::ActorId;
21
22/// The offset of user-defined ports (i.e., arbitrarily bound).
23pub const USER_PORT_OFFSET: u64 = 1024;
24
25/// Register an actor type so that it can be spawned remotely. The actor
26/// type must implement [`typeuri::Named`], which will be used to identify
27/// the actor globally.
28///
29/// Example:
30///
31/// ```ignore
32/// struct MyActor { ... }
33///
34/// remote!(MyActor);
35/// ```
36#[macro_export]
37macro_rules! remote {
38    ($actor:ty) => {
39        $crate::internal_macro_support::paste! {
40            static [<$actor:snake:upper _NAME>]: std::sync::LazyLock<&'static str> =
41              std::sync::LazyLock::new(|| <$actor as $crate::internal_macro_support::typeuri::Named>::typename());
42            $crate::internal_macro_support::inventory::submit! {
43                $crate::actor::remote::SpawnableActor {
44                    name: &[<$actor:snake:upper _NAME>],
45                    gspawn: <$actor as $crate::actor::RemoteSpawn>::gspawn,
46                    get_type_id: <$actor as $crate::actor::RemoteSpawn>::get_type_id,
47                }
48            }
49        }
50    };
51}
52
53/// A type-erased actor registration entry. These are constructed via
54/// [`crate::remote`].
55#[derive(Debug)]
56pub struct SpawnableActor {
57    /// A URI that globally identifies an actor. It is an error to register
58    /// multiple actors with the same name.
59    ///
60    /// This is a LazyLock because the names are provided through a trait
61    /// implementation, which can not yet be `const`.
62    pub name: &'static LazyLock<&'static str>,
63
64    /// Type-erased spawn function. This is the type's [`RemoteSpawn::gspawn`].
65    pub gspawn: fn(
66        &Proc,
67        &str,
68        Data,
69    ) -> Pin<Box<dyn Future<Output = Result<ActorId, anyhow::Error>> + Send>>,
70
71    /// A function to retrieve the type id of the actor itself. This is
72    /// used to translate a concrete type to a global name.
73    pub get_type_id: fn() -> TypeId,
74}
75
76inventory::collect!(SpawnableActor);
77
78/// Registry of actors linked into this image and registered by way of
79/// [`crate::remote`].
80#[derive(Debug)]
81pub struct Remote {
82    by_name: HashMap<&'static str, &'static SpawnableActor>,
83    by_type_id: HashMap<TypeId, &'static SpawnableActor>,
84}
85
86impl Remote {
87    /// Construct a registry. Panics if there are conflicting registrations.
88    pub fn collect() -> Self {
89        let mut by_name = HashMap::new();
90        let mut by_type_id = HashMap::new();
91        for entry in inventory::iter::<SpawnableActor> {
92            if by_name.insert(**entry.name, entry).is_some() {
93                panic!("actor name {} registered multiple times", **entry.name);
94            }
95            let type_id = (entry.get_type_id)();
96            if by_type_id.insert(type_id, entry).is_some() {
97                panic!(
98                    "type id {:?} ({}) registered multiple times",
99                    type_id, **entry.name
100                );
101            }
102        }
103        Self {
104            by_name,
105            by_type_id,
106        }
107    }
108
109    /// Returns the name of the provided actor, if registered.
110    pub fn name_of<A: Actor>(&self) -> Option<&'static str> {
111        self.by_type_id
112            .get(&TypeId::of::<A>())
113            .map(|entry| **entry.name)
114    }
115
116    /// Spawns the named actor with the provided sender, actor id,
117    /// and serialized parameters. Returns an error if the actor is not
118    /// registered, or if the actor's spawn fails.
119    pub async fn gspawn(
120        &self,
121        proc: &Proc,
122        actor_type: &str,
123        actor_name: &str,
124        params: Data,
125    ) -> Result<ActorId, anyhow::Error> {
126        let entry = self
127            .by_name
128            .get(actor_type)
129            .ok_or_else(|| anyhow::anyhow!("actor type {} not registered", actor_type))?;
130        (entry.gspawn)(proc, actor_name, params).await
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use std::assert_matches::assert_matches;
137
138    use async_trait::async_trait;
139
140    use super::*;
141    use crate as hyperactor; // for macros
142    use crate::Context;
143    use crate::Handler;
144    use crate::RemoteSpawn;
145
146    #[derive(Debug)]
147    #[hyperactor::export(handlers = [()])]
148    struct MyActor;
149
150    #[async_trait]
151    impl Actor for MyActor {}
152
153    #[async_trait]
154    impl RemoteSpawn for MyActor {
155        type Params = bool;
156
157        async fn new(params: bool) -> Result<Self, anyhow::Error> {
158            if params {
159                Ok(MyActor)
160            } else {
161                Err(anyhow::anyhow!("some failure"))
162            }
163        }
164    }
165
166    #[async_trait]
167    impl Handler<()> for MyActor {
168        async fn handle(&mut self, _cx: &Context<Self>, _message: ()) -> anyhow::Result<()> {
169            unimplemented!()
170        }
171    }
172
173    remote!(MyActor);
174
175    #[tokio::test]
176    async fn test_registry() {
177        let remote = Remote::collect();
178        assert_matches!(
179            remote.name_of::<MyActor>(),
180            Some("hyperactor::actor::remote::tests::MyActor")
181        );
182
183        let _ = remote
184            .gspawn(
185                &Proc::local(),
186                "hyperactor::actor::remote::tests::MyActor",
187                "actor",
188                bincode::serialize(&true).unwrap(),
189            )
190            .await
191            .unwrap();
192
193        let err = remote
194            .gspawn(
195                &Proc::local(),
196                "hyperactor::actor::remote::tests::MyActor",
197                "actor",
198                bincode::serialize(&false).unwrap(),
199            )
200            .await
201            .unwrap_err();
202
203        assert_eq!(err.to_string().as_str(), "some failure");
204    }
205}