Skip to main content

hyperactor/
client.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//! Client caller contexts.
10
11use std::fmt;
12use std::sync::Arc;
13
14use crate as hyperactor;
15use crate::Actor;
16use crate::ActorAddr;
17use crate::ActorRef;
18use crate::Data;
19use crate::Message;
20use crate::RemoteMessage;
21use crate::actor::ActorHandle;
22use crate::actor::ActorStatus;
23use crate::actor::AnyActorHandle;
24use crate::actor::Binds;
25use crate::context;
26use crate::context::Mailbox as MailboxContext;
27use crate::id::Uid;
28use crate::mailbox::Mailbox;
29use crate::mailbox::MessageEnvelope;
30use crate::mailbox::OncePortHandle;
31use crate::mailbox::OncePortReceiver;
32use crate::mailbox::PortHandle;
33use crate::mailbox::PortReceiver;
34use crate::mailbox::Undeliverable;
35use crate::ordering::Sequencer;
36use crate::proc::HandlerPorts;
37use crate::proc::Instance;
38use crate::proc::Proc;
39
40/// Actor marker type used for client caller contexts.
41#[derive(Debug, Default)]
42#[hyperactor::export]
43pub struct ClientActor;
44
45impl Actor for ClientActor {}
46
47impl Binds<ClientActor> for () {
48    fn bind(_ports: &HandlerPorts<ClientActor>) {}
49}
50
51/// A scoped caller context.
52///
53/// Dropping the last clone of a client closes its mailbox. Messages already
54/// accepted into port receivers remain available; later deliveries to the
55/// client's ports fail as ordinary closed-mailbox deliveries.
56pub struct Client {
57    instance: Instance<ClientActor>,
58    lifecycle: Arc<ClientLifecycle>,
59}
60
61struct ClientLifecycle {
62    instance: Instance<ClientActor>,
63}
64
65impl Drop for ClientLifecycle {
66    fn drop(&mut self) {
67        self.instance.close_client("client dropped");
68    }
69}
70
71impl fmt::Debug for Client {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("Client")
74            .field("self_addr", self.self_addr())
75            .finish()
76    }
77}
78
79impl Client {
80    pub(crate) fn new(instance: Instance<ClientActor>) -> Self {
81        let actor_id = instance.self_addr().clone();
82        instance.bind_handler_enqueue_port::<Undeliverable<MessageEnvelope>>(move |_, message| {
83            tracing::error!(
84                actor_id = %actor_id,
85                undeliverable = ?message,
86                "client received undeliverable message: crashing"
87            );
88            std::process::exit(1);
89        });
90
91        Self {
92            lifecycle: Arc::new(ClientLifecycle {
93                instance: instance.clone_for_py(),
94            }),
95            instance,
96        }
97    }
98
99    /// This client's actor address.
100    pub fn self_addr(&self) -> &ActorAddr {
101        self.instance.self_addr()
102    }
103
104    /// The proc that owns this client.
105    pub fn proc(&self) -> &Proc {
106        self.instance.proc()
107    }
108
109    /// Open a new port that accepts `M`-typed messages.
110    pub fn open_port<M: Message>(&self) -> (PortHandle<M>, PortReceiver<M>) {
111        self.instance.open_port()
112    }
113
114    /// Open a new one-shot port that accepts an `M`-typed message.
115    pub fn open_once_port<M: Message>(&self) -> (OncePortHandle<M>, OncePortReceiver<M>) {
116        self.instance.open_once_port()
117    }
118
119    /// Bind a handler port to this client.
120    pub fn bind_handler_port<M: RemoteMessage>(&self) -> (PortHandle<M>, PortReceiver<M>) {
121        self.instance.mailbox().bind_handler_port()
122    }
123
124    /// Bind this client as an actor ref.
125    pub fn bind<R: Binds<ClientActor>>(&self) -> ActorRef<R> {
126        self.instance.bind()
127    }
128
129    /// Create a new direct child client.
130    pub fn child(&self) -> Client {
131        self.instance.child_client()
132    }
133
134    /// Spawn a child actor with a fresh uid labeled from the actor type.
135    pub fn spawn<A: Actor>(&self, actor: A) -> ActorHandle<A> {
136        self.instance.spawn(actor)
137    }
138
139    /// Spawn a child actor with a fresh uid carrying a display label.
140    pub fn spawn_with_label<A: Actor>(&self, label: &str, actor: A) -> ActorHandle<A> {
141        self.instance.spawn_with_label(label, actor)
142    }
143
144    /// Spawn a child actor using an explicit uid.
145    pub fn spawn_with_uid<A: Actor>(&self, uid: Uid, actor: A) -> anyhow::Result<ActorHandle<A>> {
146        self.instance.spawn_with_uid(uid, actor)
147    }
148
149    /// Spawn a registered actor as this client's child.
150    pub async fn gspawn(&self, actor_type: &str, params: Data) -> anyhow::Result<AnyActorHandle> {
151        self.instance.gspawn(actor_type, params).await
152    }
153
154    /// Spawn a registered actor as this client's child using an explicit uid.
155    pub async fn gspawn_uid(
156        &self,
157        actor_type: &str,
158        uid: Uid,
159        params: Data,
160    ) -> anyhow::Result<AnyActorHandle> {
161        self.instance.gspawn_uid(actor_type, uid, params).await
162    }
163
164    /// Return this client's sequencer.
165    pub fn sequencer(&self) -> &Sequencer {
166        self.instance.sequencer()
167    }
168
169    /// The client's lifecycle status.
170    pub fn status(&self) -> tokio::sync::watch::Receiver<ActorStatus> {
171        self.instance.status()
172    }
173}
174
175impl Clone for Client {
176    fn clone(&self) -> Self {
177        Self {
178            instance: self.instance.clone_for_py(),
179            lifecycle: Arc::clone(&self.lifecycle),
180        }
181    }
182}
183
184impl context::Mailbox for Client {
185    fn mailbox(&self) -> &Mailbox {
186        MailboxContext::mailbox(&self.instance)
187    }
188}
189
190impl context::Mailbox for &Client {
191    fn mailbox(&self) -> &Mailbox {
192        MailboxContext::mailbox(&self.instance)
193    }
194}
195
196impl context::Actor for Client {
197    type A = ClientActor;
198
199    fn instance(&self) -> &Instance<ClientActor> {
200        &self.instance
201    }
202}
203
204impl context::Actor for &Client {
205    type A = ClientActor;
206
207    fn instance(&self) -> &Instance<ClientActor> {
208        &self.instance
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use crate::Proc;
215    use crate::context;
216
217    #[test]
218    fn client_ids_are_fresh_instances() {
219        let proc = Proc::isolated();
220
221        let first = proc.client("caller");
222        let second = proc.client("caller");
223        let anonymous = proc.client("");
224
225        assert!(first.self_addr().id().uid().is_instance());
226        assert!(second.self_addr().id().uid().is_instance());
227        assert!(anonymous.self_addr().id().uid().is_instance());
228        assert_eq!(
229            first.self_addr().id().label().map(|label| label.as_str()),
230            Some("caller")
231        );
232        assert_eq!(anonymous.self_addr().id().label(), None);
233        assert_ne!(first.self_addr().id(), second.self_addr().id());
234    }
235
236    #[test]
237    fn client_binds_return_handler_without_actor_loop() {
238        let proc = Proc::isolated();
239        let client = proc.client("client");
240
241        assert!(
242            context::Mailbox::mailbox(&client)
243                .bound_return_handle()
244                .is_some(),
245            "taskless clients should install a return handler"
246        );
247    }
248
249    #[tokio::test]
250    async fn dropping_client_closes_mailbox_but_preserves_accepted_messages() {
251        let proc = Proc::isolated();
252        let receiver = proc.client("receiver");
253        let sender = proc.client("sender");
254        let (port, mut rx) = receiver.open_port::<u64>();
255
256        port.try_post(&sender, 1).unwrap();
257        drop(receiver);
258
259        assert_eq!(rx.recv().await.unwrap(), 1);
260        assert!(port.try_post(&sender, 2).is_err());
261        assert_eq!(rx.try_recv().unwrap(), None);
262    }
263}