Skip to main content

hyperactor_remote/
link.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//! Link implementation actors.
10//!
11//! A link is represented by a registered actor spawned as a supervised child
12//! of the actor that owns the remote supervision proxy. The actor's ordinary
13//! lifecycle is the link lifecycle: if the link actor fails, local supervision
14//! observes that failure; if the parent stops, the link actor stops with it.
15
16use hyperactor::Actor;
17use hyperactor::AnyActorHandle;
18use hyperactor::Data;
19#[cfg(test)]
20use hyperactor::Instance;
21use hyperactor::Uid;
22use hyperactor::context;
23use serde::Deserialize;
24use serde::Serialize;
25use typeuri::Named;
26
27/// Serializable worker-side link actor spawn specification.
28///
29/// A supervisor-side link factory returns this after it starts the local
30/// supervisor link actor. The worker sends this spec to the remote worker,
31/// which calls [`LinkSpec::spawn_worker`] to instantiate its side of the link.
32#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
33pub struct LinkSpec {
34    actor_type: String,
35    uid: Uid,
36    params: Data,
37}
38wirevalue::register_type!(LinkSpec);
39
40impl LinkSpec {
41    /// Create a worker-side link spec for the registered actor type with a fresh uid.
42    pub fn for_actor<A: Actor + Named>(params: Data) -> Self {
43        Self::new(A::typename(), params)
44    }
45
46    /// Create a worker-side link spec for the registered actor type with an explicit uid.
47    pub fn for_actor_uid<A: Actor + Named>(uid: Uid, params: Data) -> Self {
48        Self::with_uid(A::typename(), uid, params)
49    }
50
51    /// Create a worker-side link spec with a fresh uid.
52    pub fn new(actor_type: impl Into<String>, params: Data) -> Self {
53        Self::with_uid(actor_type, Uid::anonymous(), params)
54    }
55
56    /// Create a worker-side link spec with an explicit uid.
57    pub fn with_uid(actor_type: impl Into<String>, uid: Uid, params: Data) -> Self {
58        Self {
59            actor_type: actor_type.into(),
60            uid,
61            params,
62        }
63    }
64
65    /// Registered name of the worker-side link implementation actor.
66    pub fn actor_type(&self) -> &str {
67        &self.actor_type
68    }
69
70    /// The uid that will identify this link actor.
71    pub fn uid(&self) -> &Uid {
72        &self.uid
73    }
74
75    /// The serialized parameters passed to the link actor.
76    pub fn params(&self) -> &[u8] {
77        &self.params
78    }
79
80    /// Spawn the worker-side link actor as a supervised child of `parent`.
81    pub async fn spawn_worker<C: context::Actor>(
82        self,
83        parent: &C,
84    ) -> anyhow::Result<AnyActorHandle> {
85        parent
86            .instance()
87            .gspawn_uid(&self.actor_type, self.uid, self.params)
88            .await
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use async_trait::async_trait;
95    use hyperactor::Actor;
96    use hyperactor::ActorEnvironment;
97    use hyperactor::Context;
98    use hyperactor::Endpoint as _;
99    use hyperactor::Handler;
100    use hyperactor::Label;
101    use hyperactor::PortRef;
102    use hyperactor::Proc;
103    use hyperactor::RemoteSpawn;
104    use hyperactor::Uid;
105    use hyperactor::actor::ActorErrorKind;
106    use hyperactor::actor::ActorStatus;
107    use hyperactor::supervision::ActorSupervisionEvent;
108
109    use super::*;
110
111    #[derive(Debug)]
112    #[hyperactor::export(())]
113    struct TestLinkActor;
114
115    #[async_trait]
116    impl Actor for TestLinkActor {}
117
118    #[async_trait]
119    impl RemoteSpawn for TestLinkActor {
120        type Params = ();
121
122        async fn new(_params: (), _environment: &ActorEnvironment) -> anyhow::Result<Self> {
123            Ok(Self)
124        }
125    }
126
127    #[async_trait]
128    impl Handler<()> for TestLinkActor {
129        async fn handle(&mut self, _cx: &Context<Self>, _message: ()) -> anyhow::Result<()> {
130            Ok(())
131        }
132    }
133
134    hyperactor::register_spawnable!(TestLinkActor);
135
136    #[derive(Debug)]
137    struct TestParentActor {
138        link: Option<LinkSpec>,
139        events: PortRef<ActorSupervisionEvent>,
140    }
141
142    #[async_trait]
143    impl Actor for TestParentActor {
144        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
145            let link = self.link.take().unwrap();
146            let handle = link.spawn_worker(this).await?;
147            handle.kill("link failed")?;
148            Ok(())
149        }
150
151        async fn handle_supervision_event(
152            &mut self,
153            this: &Instance<Self>,
154            event: &ActorSupervisionEvent,
155        ) -> anyhow::Result<bool> {
156            self.events.post(this, event.clone());
157            Ok(true)
158        }
159    }
160
161    #[tokio::test]
162    async fn test_link_spec_spawns_supervised_child() {
163        let proc = Proc::isolated();
164        let parent = proc.client("parent");
165        let uid = Uid::instance(Label::new("link").unwrap());
166
167        let link = LinkSpec::for_actor_uid::<TestLinkActor>(
168            uid.clone(),
169            bincode::serde::encode_to_vec((), bincode::config::legacy()).unwrap(),
170        );
171
172        let handle = link.spawn_worker(&parent).await.unwrap();
173
174        assert_eq!(handle.actor_id().uid(), &uid);
175        assert!(handle.downcast::<TestLinkActor>().is_some());
176
177        handle.stop("test").unwrap();
178        handle.await;
179    }
180
181    #[tokio::test]
182    async fn test_link_actor_failure_propagates_to_parent() {
183        let proc = Proc::isolated();
184        let client = proc.client("client");
185        let (events, mut event_rx) = client.open_port::<ActorSupervisionEvent>();
186        let uid = Uid::instance(Label::new("link").unwrap());
187        let link = LinkSpec::for_actor_uid::<TestLinkActor>(
188            uid.clone(),
189            bincode::serde::encode_to_vec((), bincode::config::legacy()).unwrap(),
190        );
191        let parent = proc.spawn(TestParentActor {
192            link: Some(link),
193            events: events.bind(),
194        });
195
196        let event = event_rx.recv().await.unwrap();
197
198        assert_eq!(event.actor_id.uid(), &uid);
199        assert!(matches!(
200            event.actor_status,
201            ActorStatus::Failed(ActorErrorKind::Generic(ref reason))
202                if reason == "actor explicitly aborted due to: link failed"
203        ));
204        assert!(!event.actor_id.is_root());
205
206        parent.stop("test").unwrap();
207        parent.await;
208    }
209}