Skip to main content

hyperactor_mesh/
connect.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//! Actor-based duplex bytestream connections.
10//!
11//! This module provides the equivalent of a `TcpStream` duplex bytestream connection between two actors,
12//! implemented via actor message passing. It allows actors to communicate using familiar `AsyncRead` and
13//! `AsyncWrite` interfaces while leveraging the hyperactor framework's message passing capabilities.
14//!
15//! # Overview
16//!
17//! The connection system consists of:
18//! - [`ActorConnection`]: A duplex connection that implements both `AsyncRead` and `AsyncWrite`
19//! - [`OwnedReadHalf`] and [`OwnedWriteHalf`]: Split halves for independent reading and writing
20//! - [`Connect`] message for establishing connections
21//! - Helper functions [`connect`] and [`accept`] for client and server usage
22//!
23//! # Usage Patterns
24//!
25//! ## Client Side (Initiating Connection)
26//!
27//! Clients use `Connect::allocate()` to create a connection request. This method returns:
28//! 1. A `Connect` message to send to the server to initiate the connection
29//! 2. A `ConnectionCompleter` object that can be awaited for the server to finish connecting,
30//!    returning the `ActorConnection` used by the client.
31//!
32//! The typical pattern is: allocate components, send Connect message to server, await completion.
33//!
34//! ## Server Side (Accepting Connections)
35//!
36//! Servers forward `Connect` messages to the `accept()` helper function to finish setting up the
37//! connection, which returns the `ActorConnection` they can use.
38
39use std::io::Cursor;
40use std::pin::Pin;
41use std::time::Duration;
42
43use anyhow::Result;
44use future::Future;
45use futures::Stream;
46use futures::future;
47use futures::stream::FusedStream;
48use futures::task::Context;
49use futures::task::Poll;
50use hyperactor::ActorAddr;
51use hyperactor::Endpoint as _;
52use hyperactor::OncePortRef;
53use hyperactor::PortRef;
54use hyperactor::context;
55use hyperactor::mailbox::OncePortReceiver;
56use hyperactor::mailbox::PortReceiver;
57use hyperactor::mailbox::open_once_port;
58use hyperactor::mailbox::open_port;
59use pin_project::pin_project;
60use pin_project::pinned_drop;
61use serde::Deserialize;
62use serde::Serialize;
63use tokio::io::AsyncRead;
64use tokio::io::AsyncWrite;
65use tokio_util::io::StreamReader;
66use typeuri::Named;
67
68// Timeout for establishing a connection, used by both client and server.
69const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
70
71/// Messages sent over the "connection" to facilitate communication.
72#[derive(Debug, Serialize, Deserialize, Named, Clone)]
73enum Io {
74    // A data packet.
75    Data(#[serde(with = "serde_bytes")] Vec<u8>),
76    // Signal the end of one side of the connection.
77    Eof,
78}
79wirevalue::register_type!(Io);
80
81struct OwnedReadHalfStream {
82    port: PortReceiver<Io>,
83    exhausted: bool,
84}
85
86/// Wrap a `PortReceiver<IoMsg>` as a `AsyncRead`.
87#[pin_project]
88pub struct OwnedReadHalf<C: context::Actor> {
89    peer: ActorAddr,
90    _caps: C,
91    #[pin]
92    inner: StreamReader<OwnedReadHalfStream, Cursor<Vec<u8>>>,
93}
94
95/// Wrap a `PortRef<IoMsg>` as a `AsyncWrite`.
96#[pin_project(PinnedDrop)]
97pub struct OwnedWriteHalf<C: context::Actor> {
98    peer: ActorAddr,
99    #[pin]
100    caps: C,
101    #[pin]
102    port: PortRef<Io>,
103    #[pin]
104    shutdown: bool,
105}
106
107/// A duplex bytestream connection between two actors.  Can generally be used like a `TcpStream`.
108#[pin_project]
109pub struct ActorConnection<C: context::Actor> {
110    #[pin]
111    reader: OwnedReadHalf<C>,
112    #[pin]
113    writer: OwnedWriteHalf<C>,
114}
115
116impl<C: context::Actor> ActorConnection<C> {
117    pub fn into_split(self) -> (OwnedReadHalf<C>, OwnedWriteHalf<C>) {
118        (self.reader, self.writer)
119    }
120
121    pub fn peer(&self) -> &ActorAddr {
122        self.reader.peer()
123    }
124}
125
126impl<C: context::Actor> OwnedReadHalf<C> {
127    fn new(peer: ActorAddr, caps: C, port: PortReceiver<Io>) -> Self {
128        Self {
129            peer,
130            _caps: caps,
131            inner: StreamReader::new(OwnedReadHalfStream {
132                port,
133                exhausted: false,
134            }),
135        }
136    }
137
138    pub fn peer(&self) -> &ActorAddr {
139        &self.peer
140    }
141
142    pub fn reunited(self, other: OwnedWriteHalf<C>) -> ActorConnection<C> {
143        ActorConnection {
144            reader: self,
145            writer: other,
146        }
147    }
148}
149
150impl<C: context::Actor> OwnedWriteHalf<C> {
151    fn new(peer: ActorAddr, caps: C, port: PortRef<Io>) -> Self {
152        Self {
153            peer,
154            caps,
155            port,
156            shutdown: false,
157        }
158    }
159
160    pub fn peer(&self) -> &ActorAddr {
161        &self.peer
162    }
163
164    pub fn reunited(self, other: OwnedReadHalf<C>) -> ActorConnection<C> {
165        ActorConnection {
166            reader: other,
167            writer: self,
168        }
169    }
170}
171
172#[pinned_drop]
173impl<C: context::Actor> PinnedDrop for OwnedWriteHalf<C> {
174    fn drop(self: Pin<&mut Self>) {
175        let this = self.project();
176        if !*this.shutdown {
177            let _ = this.port.post(&*this.caps, Io::Eof);
178        }
179    }
180}
181
182impl<C: context::Actor> AsyncRead for ActorConnection<C> {
183    fn poll_read(
184        self: Pin<&mut Self>,
185        cx: &mut Context<'_>,
186        buf: &mut tokio::io::ReadBuf<'_>,
187    ) -> Poll<std::io::Result<()>> {
188        // Use project() to get pinned references to fields
189        let this = self.project();
190        this.reader.poll_read(cx, buf)
191    }
192}
193
194impl<C: context::Actor> AsyncWrite for ActorConnection<C> {
195    fn poll_write(
196        self: Pin<&mut Self>,
197        cx: &mut Context<'_>,
198        buf: &[u8],
199    ) -> Poll<Result<usize, std::io::Error>> {
200        // Use project() to get pinned references to fields
201        let this = self.project();
202        this.writer.poll_write(cx, buf)
203    }
204
205    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
206        let this = self.project();
207        this.writer.poll_flush(cx)
208    }
209
210    fn poll_shutdown(
211        self: Pin<&mut Self>,
212        cx: &mut Context<'_>,
213    ) -> Poll<Result<(), std::io::Error>> {
214        let this = self.project();
215        this.writer.poll_shutdown(cx)
216    }
217}
218
219impl Stream for OwnedReadHalfStream {
220    type Item = std::io::Result<Cursor<Vec<u8>>>;
221
222    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
223        // Once exhausted, always return None
224        if self.exhausted {
225            return Poll::Ready(None);
226        }
227
228        let result = futures::ready!(Box::pin(self.port.recv()).as_mut().poll(cx));
229        match result {
230            Err(err) => Poll::Ready(Some(Err(std::io::Error::other(err)))),
231            Ok(Io::Data(buf)) => Poll::Ready(Some(Ok(Cursor::new(buf)))),
232            // Break out of stream when we see EOF.
233            Ok(Io::Eof) => {
234                self.exhausted = true;
235                Poll::Ready(None)
236            }
237        }
238    }
239}
240
241impl FusedStream for OwnedReadHalfStream {
242    fn is_terminated(&self) -> bool {
243        self.exhausted
244    }
245}
246
247impl<C: context::Actor> AsyncRead for OwnedReadHalf<C> {
248    fn poll_read(
249        self: Pin<&mut Self>,
250        cx: &mut Context<'_>,
251        buf: &mut tokio::io::ReadBuf<'_>,
252    ) -> Poll<std::io::Result<()>> {
253        self.project().inner.poll_read(cx, buf)
254    }
255}
256
257impl<C: context::Actor> AsyncWrite for OwnedWriteHalf<C> {
258    fn poll_write(
259        self: Pin<&mut Self>,
260        _cx: &mut Context<'_>,
261        buf: &[u8],
262    ) -> Poll<Result<usize, std::io::Error>> {
263        let this = self.project();
264        if *this.shutdown {
265            return Poll::Ready(Err(std::io::Error::new(
266                std::io::ErrorKind::BrokenPipe,
267                "write after shutdown",
268            )));
269        }
270        this.port.post(&*this.caps, Io::Data(buf.into()));
271        Poll::Ready(Ok(buf.len()))
272    }
273
274    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
275        Poll::Ready(Ok(()))
276    }
277
278    fn poll_shutdown(
279        self: Pin<&mut Self>,
280        _cx: &mut Context<'_>,
281    ) -> Poll<Result<(), std::io::Error>> {
282        // Send EOF on shutdown.
283        self.port.post(&self.caps, Io::Eof);
284        let mut this = self.project();
285        *this.shutdown = true;
286        Poll::Ready(Ok(()))
287    }
288}
289
290/// A helper struct that contains the state needed to complete a connection.
291pub struct ConnectionCompleter<C> {
292    caps: C,
293    conn: PortReceiver<Io>,
294    port: OncePortReceiver<Accept>,
295}
296
297impl<C: context::Actor + Clone> ConnectionCompleter<C> {
298    /// Wait for the server to accept the connection and return the streams that can be used to communicate
299    /// with the server.
300    pub async fn complete(self) -> Result<ActorConnection<C>> {
301        let accept = tokio::time::timeout(CONNECT_TIMEOUT, self.port.recv()).await??;
302        Ok(ActorConnection {
303            reader: OwnedReadHalf::new(accept.id.clone(), self.caps.clone(), self.conn),
304            writer: OwnedWriteHalf::new(accept.id, self.caps, accept.conn),
305        })
306    }
307}
308
309/// A message sent from a client to initiate a connection.
310#[derive(Debug, Serialize, Deserialize, Named, Clone)]
311pub struct Connect {
312    /// The ID of the client initiating the connection.
313    id: ActorAddr,
314    conn: PortRef<Io>,
315    /// The port the server can use to complete the connection.
316    return_conn: OncePortRef<Accept>,
317}
318wirevalue::register_type!(Connect);
319
320impl Connect {
321    /// Allocate a new `Connect` message and return the associated `ConnectionCompleter` that can be used
322    /// to finish setting up the connection.
323    pub fn allocate<C: context::Actor + Clone>(
324        id: ActorAddr,
325        caps: C,
326    ) -> (Self, ConnectionCompleter<C>) {
327        let (conn_tx, conn_rx) = open_port::<Io>(&caps);
328        let (return_tx, return_rx) = open_once_port::<Accept>(&caps);
329        (
330            Self {
331                id,
332                conn: conn_tx.bind(),
333                return_conn: return_tx.bind(),
334            },
335            ConnectionCompleter {
336                caps,
337                conn: conn_rx,
338                port: return_rx,
339            },
340        )
341    }
342}
343
344/// A response message sent from the server back to the client to complete setting
345/// up the connection.
346#[derive(Debug, Serialize, Deserialize, Named, Clone)]
347struct Accept {
348    /// The ID of the server that accepted the connection.
349    id: ActorAddr,
350    /// The port the client will use to send data over the connection to the server.
351    conn: PortRef<Io>,
352}
353wirevalue::register_type!(Accept);
354
355/// Helper used by `Handler<Connect>`s to accept a connection initiated by a `Connect` message and
356/// return `AsyncRead` and `AsyncWrite` streams that can be used to communicate with the other side.
357pub async fn accept<C: context::Actor + Clone>(
358    caps: C,
359    self_id: ActorAddr,
360    message: Connect,
361) -> Result<ActorConnection<C>> {
362    let (tx, rx) = open_port::<Io>(&caps);
363    message.return_conn.post(
364        &caps,
365        Accept {
366            id: self_id,
367            conn: tx.bind(),
368        },
369    );
370    Ok(ActorConnection {
371        reader: OwnedReadHalf::new(message.id.clone(), caps.clone(), rx),
372        writer: OwnedWriteHalf::new(message.id, caps, message.conn),
373    })
374}
375
376#[cfg(test)]
377mod tests {
378    use anyhow::Result;
379    use async_trait::async_trait;
380    use futures::try_join;
381    use hyperactor::Actor;
382    use hyperactor::Context;
383    use hyperactor::Handler;
384    use hyperactor::proc::Proc;
385    use tokio::io::AsyncReadExt;
386    use tokio::io::AsyncWriteExt;
387
388    use super::*;
389
390    #[derive(Debug, Default)]
391    struct EchoActor {}
392
393    impl Actor for EchoActor {}
394
395    #[async_trait]
396    impl Handler<Connect> for EchoActor {
397        async fn handle(
398            &mut self,
399            cx: &Context<Self>,
400            message: Connect,
401        ) -> Result<(), anyhow::Error> {
402            let (mut rd, mut wr) = accept(cx, cx.self_addr().clone(), message)
403                .await?
404                .into_split();
405            tokio::io::copy(&mut rd, &mut wr).await?;
406            wr.shutdown().await?;
407            Ok(())
408        }
409    }
410
411    #[tokio::test]
412    async fn test_simple_connection() -> Result<()> {
413        let proc = Proc::isolated();
414        let client = proc.client("client");
415        let (connect, completer) = Connect::allocate(client.self_addr().clone(), client);
416        let actor = proc.spawn(EchoActor {});
417        actor.post(&completer.caps, connect);
418        let (mut rd, mut wr) = completer.complete().await?.into_split();
419        let send = [3u8, 4u8, 5u8, 6u8];
420        try_join!(
421            async move {
422                wr.write_all(&send).await?;
423                wr.shutdown().await?;
424                anyhow::Ok(())
425            },
426            async {
427                let mut recv = vec![];
428                rd.read_to_end(&mut recv).await?;
429                assert_eq!(&send, recv.as_slice());
430                anyhow::Ok(())
431            },
432        )?;
433        Ok(())
434    }
435
436    #[tokio::test]
437    async fn test_connection_close_on_drop() -> Result<()> {
438        let proc = Proc::isolated();
439        let client = proc.client("client");
440        let client_addr = client.self_addr().clone();
441        let server = proc.client("server");
442        let server_addr = server.self_addr().clone();
443
444        let (connect, completer) = Connect::allocate(client_addr, client);
445        let (mut rd, _) = accept(server, server_addr, connect).await?.into_split();
446        let (_, mut wr) = completer.complete().await?.into_split();
447
448        // Write some data
449        let send = [1u8, 2u8, 3u8];
450        wr.write_all(&send).await?;
451
452        // Drop the writer without explicit shutdown - this should send EOF
453        drop(wr);
454
455        // Reader should receive the data and then EOF (causing read_to_end to complete)
456        let mut recv = vec![];
457        rd.read_to_end(&mut recv).await?;
458        assert_eq!(&send, recv.as_slice());
459
460        Ok(())
461    }
462
463    #[tokio::test]
464    async fn test_no_eof_on_drop_after_shutdown() -> Result<()> {
465        let proc = Proc::isolated();
466        let client = proc.client("client");
467        let client_addr = client.self_addr().clone();
468        let server = proc.client("server");
469        let server_addr = server.self_addr().clone();
470
471        let (connect, completer) = Connect::allocate(client_addr, client);
472        let (mut rd, _) = accept(server, server_addr, connect).await?.into_split();
473        let (_, mut wr) = completer.complete().await?.into_split();
474
475        // Write some data
476        let send = [1u8, 2u8, 3u8];
477        wr.write_all(&send).await?;
478
479        // Explicitly shutdown the writer - this sends EOF and sets shutdown=true
480        wr.shutdown().await?;
481
482        // Reader should receive the data and then EOF (from explicit shutdown, not from drop)
483        let mut recv = vec![];
484        rd.read_to_end(&mut recv).await?;
485        assert_eq!(&send, recv.as_slice());
486
487        // Drop the writer after explicit shutdown - this should NOT send another EOF
488        drop(wr);
489
490        // Verify we didn't see another EOF message.
491        assert!(rd.inner.into_inner().port.try_recv().unwrap().is_none());
492
493        Ok(())
494    }
495}