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