Skip to main content

hyperactor/channel/
net.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//! A simple socket channel implementation using a single-stream
10//! framing protocol. Each frame is encoded as an 8-byte
11//! **big-endian** length prefix (u64), followed by exactly that many
12//! bytes of payload.
13//!
14//! Message frames carry a `serde_multipart::Message` (not raw
15//! bincode). In compat mode (current default), this is encoded as a
16//! sentinel `u64::MAX` followed by a single bincode payload. Response frames
17//! are a bincode-serialized NetRxResponse enum, containing either the acked
18//! sequence number, or the Reject value indicating that the server rejected
19//! the connection.
20//!
21//! Message frame (compat/unipart) example:
22//! ```text
23//! +------------------ len: u64 (BE) ------------------+----------------------- data -----------------------+
24//! | \x00\x00\x00\x00\x00\x00\x00\x10                  | \xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF | <bincode bytes> |
25//! |                       16                          |           u64::MAX             |                   |
26//! +---------------------------------------------------+-----------------------------------------------------+
27//! ```
28//!
29//! Response frame (wire format):
30//! ```text
31//! +------------------ len: u64 (BE) ------------------+---------------- data ------------------+
32//! | \x00\x00\x00\x00\x00\x00\x00\x??                  | <bincode acked sequence num or reject> |
33//! +---------------------------------------------------+----------------------------------------+
34//! ```
35//!
36//! I/O is handled by `FrameReader`/`FrameWrite`, which are
37//! cancellation-safe and avoid extra copies. Helper fns
38//! `serialize_response(NetRxResponse) -> Result<Bytes, bincode::Error>`
39//! and `deserialize_response(Bytes) -> Result<NetRxResponse, bincode::Error>`
40//! convert to/from the response payload.
41//!
42//! ### Limits & EOF semantics
43//! * **Max frame size:** frames larger than
44//!   `config::CODEC_MAX_FRAME_LENGTH` are rejected with
45//!   `io::ErrorKind::InvalidData`.
46//! * **EOF handling:** `FrameReader::next()` returns `Ok(None)` only
47//!   when EOF occurs exactly on a frame boundary. If EOF happens
48//!   mid-frame, it returns `Err(io::ErrorKind::UnexpectedEof)`.
49
50use std::fmt;
51use std::fmt::Debug;
52use std::net::ToSocketAddrs;
53use std::time::Duration;
54
55use backoff::ExponentialBackoffBuilder;
56use backoff::backoff::Backoff;
57use bytes::Bytes;
58use enum_as_inner::EnumAsInner;
59use serde::Deserialize;
60use serde::Serialize;
61use serde::de::Error;
62use tokio::io::AsyncRead;
63use tokio::io::AsyncReadExt;
64use tokio::io::AsyncWrite;
65use tokio::io::AsyncWriteExt;
66use tokio::sync::watch;
67use tracing::Instrument;
68
69use super::*;
70use crate::RemoteMessage;
71
72pub mod duplex;
73mod framed;
74pub(super) mod mux;
75pub(crate) mod quic;
76pub(super) mod server;
77pub(super) mod session;
78pub use server::ServerHandle;
79
80pub(crate) trait Stream:
81    AsyncRead + AsyncWrite + Unpin + Send + Sync + Debug + 'static
82{
83}
84impl<S: AsyncRead + AsyncWrite + Unpin + Send + Sync + Debug + 'static> Stream for S {}
85
86/// Opaque identifier for a session. Generated by the client,
87/// sent to the server on each connect so the server can correlate
88/// reconnections to the same logical session.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90pub(crate) struct SessionId(u64);
91
92impl SessionId {
93    /// Generate a new random session ID.
94    pub fn random() -> Self {
95        Self(rand::random())
96    }
97}
98
99impl fmt::Display for SessionId {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "{:016x}", self.0)
102    }
103}
104
105/// Logical channel tag for initiator→acceptor traffic.
106pub(crate) const INITIATOR_TO_ACCEPTOR: u8 = 0;
107
108/// Logical channel tag for acceptor→initiator traffic.
109pub(crate) const ACCEPTOR_TO_INITIATOR: u8 = 1;
110
111/// Wire-level protocol kind carried by [`ProtocolKind`] in the
112/// [`LinkInit`](write_link_init) header. Distinguishes simplex
113/// (one-direction byte stream carrying tag `0x00` frames) from
114/// duplex (bidirectional byte stream carrying both `0x00` and
115/// `0x01` frames) so a server listening on a single address can
116/// demultiplex both styles without peeking at application payloads.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub(crate) enum ProtocolKind {
119    Simplex,
120    Duplex,
121}
122
123const SIMPLEX_MAGIC: [u8; 4] = *b"SMP\0";
124const DUPLEX_MAGIC: [u8; 4] = *b"DPX\0";
125
126/// Fixed-size header sent at the start of every physical connection.
127/// Written/read directly on the wire (not framed), before any session
128/// framing begins.
129///
130/// Wire format (13 bytes, big-endian):
131/// ```text
132/// [magic: 4B ("SMP\0" | "DPX\0")] [session_id: 8B u64 BE] [stream_id: 1B u8]
133/// ```
134const LINK_INIT_SIZE: usize = 4 + 8 + 1;
135
136/// Parsed LinkInit header.
137#[derive(Debug, Clone, Copy)]
138pub(crate) struct LinkInit {
139    pub session_id: SessionId,
140    pub stream_id: u8,
141    pub kind: ProtocolKind,
142}
143
144/// Write a LinkInit header to the stream, tagged by `kind`.
145pub(crate) async fn write_link_init<S: AsyncWrite + Unpin>(
146    stream: &mut S,
147    session_id: SessionId,
148    stream_id: u8,
149    kind: ProtocolKind,
150) -> Result<(), std::io::Error> {
151    let mut buf = [0u8; LINK_INIT_SIZE];
152    let magic = match kind {
153        ProtocolKind::Simplex => &SIMPLEX_MAGIC,
154        ProtocolKind::Duplex => &DUPLEX_MAGIC,
155    };
156    buf[0..4].copy_from_slice(magic);
157    buf[4..12].copy_from_slice(&session_id.0.to_be_bytes());
158    buf[12] = stream_id;
159    stream.write_all(&buf).await
160}
161
162/// Read a LinkInit header from the stream, recovering the session
163/// id and protocol kind the client wrote.
164async fn read_link_init<S: AsyncRead + Unpin>(stream: &mut S) -> Result<LinkInit, std::io::Error> {
165    let mut buf = [0u8; LINK_INIT_SIZE];
166    stream.read_exact(&mut buf).await?;
167    let kind = match &buf[0..4] {
168        m if m == SIMPLEX_MAGIC => ProtocolKind::Simplex,
169        m if m == DUPLEX_MAGIC => ProtocolKind::Duplex,
170        other => {
171            return Err(std::io::Error::new(
172                std::io::ErrorKind::InvalidData,
173                format!(
174                    "invalid LinkInit magic: expected {:?} or {:?}, got {:?}",
175                    SIMPLEX_MAGIC, DUPLEX_MAGIC, other
176                ),
177            ));
178        }
179    };
180    let session_id = SessionId(u64::from_be_bytes(buf[4..12].try_into().unwrap()));
181    let stream_id = buf[12];
182    Ok(LinkInit {
183        session_id,
184        stream_id,
185        kind,
186    })
187}
188
189/// Prepare an accepted byte stream for dispatch: optionally negotiate
190/// TLS for TLS transports, read the [`LinkInit`] header, and (when
191/// `expected_kind` is set) validate the client's [`ProtocolKind`].
192///
193/// Shared by the simplex, duplex, and muxed accept loops so each
194/// `prepare` closure stays a thin wrapper over this function. Pass
195/// `expected_kind = None` to accept either kind (used by the mux
196/// listener, which dispatches by kind after the header is read).
197pub(super) async fn prepare_accepted_stream(
198    stream: Box<dyn Stream>,
199    source: ChannelAddr,
200    dest: ChannelAddr,
201    expected_kind: Option<ProtocolKind>,
202) -> Result<(LinkInit, Box<dyn Stream>), anyhow::Error> {
203    let is_tls = dest.transport().is_tls();
204    let (link_init, stream): (LinkInit, Box<dyn Stream>) = if is_tls {
205        let tls_acceptor = match dest.transport() {
206            ChannelTransport::Tls => tls::tls_acceptor()?,
207            _ => meta::tls_acceptor(true)?,
208        };
209        let mut tls_stream = tls_acceptor.accept(stream).await?;
210        let link_init = read_link_init(&mut tls_stream)
211            .await
212            .map_err(|e| anyhow::anyhow!("LinkInit read failed from {}: {}", source, e))?;
213        (link_init, Box::new(tls_stream))
214    } else {
215        let mut stream = stream;
216        let link_init = read_link_init(&mut stream)
217            .await
218            .map_err(|e| anyhow::anyhow!("LinkInit read failed from {}: {}", source, e))?;
219        (link_init, stream)
220    };
221    if let Some(expected) = expected_kind
222        && link_init.kind != expected
223    {
224        return Err(anyhow::anyhow!(
225            "{:?} server received {:?} client from {}",
226            expected,
227            link_init.kind,
228            source
229        ));
230    }
231    Ok((link_init, stream))
232}
233
234/// Future type produced by [`preparer_for`]'s closure.
235type PreparedStreamFut = std::pin::Pin<
236    Box<
237        dyn std::future::Future<Output = Result<(LinkInit, Box<dyn Stream>), anyhow::Error>> + Send,
238    >,
239>;
240
241/// Build a `prepare` closure suitable for [`server::accept_loop`] that
242/// wraps [`prepare_accepted_stream`] with the given destination address
243/// and expected [`ProtocolKind`]. Pass `expected_kind = None` for the
244/// muxed listener path, which dispatches on `link_init.kind` after the
245/// header is read instead of validating up front.
246pub(super) fn preparer_for(
247    dest: ChannelAddr,
248    expected_kind: Option<ProtocolKind>,
249) -> impl Fn(Box<dyn Stream>, ChannelAddr) -> PreparedStreamFut + Clone + Send + 'static {
250    move |stream, source| {
251        let dest = dest.clone();
252        Box::pin(prepare_accepted_stream(stream, source, dest, expected_kind))
253    }
254}
255
256/// Link represents a network link through which connections may be
257/// acquired. The session ID is baked in. Initiator links dial;
258/// acceptor links wait for dispatched streams.
259#[async_trait]
260pub(crate) trait Link: Send + Sync + Debug + 'static {
261    /// The underlying stream type.
262    type Stream: Stream;
263
264    /// The address of the link's destination.
265    fn dest(&self) -> ChannelAddr;
266
267    /// The session ID for this link.
268    fn link_id(&self) -> SessionId;
269
270    /// Acquire the next usable connection. For initiator links this
271    /// dials; for acceptor links this waits on a dispatch channel.
272    async fn next(&mut self) -> Result<Self::Stream, ClientError>;
273}
274
275use session::Session;
276
277use crate::config;
278use crate::metrics;
279
280/// TCP keepalive probe interval after the idle period elapses.
281const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
282
283/// Number of failed keepalive probes before the kernel marks the
284/// connection dead.
285const TCP_KEEPALIVE_RETRIES: u32 = 3;
286
287/// Enable TCP keepalive on a freshly-created socket so the kernel can
288/// surface peer death on otherwise-idle connections.
289/// [`config::CHANNEL_TCP_KEEPALIVE_IDLE`] is the kernel idle period —
290/// the gap from last activity to the first probe — so on a healthy
291/// idle connection it's also the probe cadence. Total detection time
292/// is `idle + TCP_KEEPALIVE_RETRIES * TCP_KEEPALIVE_INTERVAL`. Logs
293/// and ignores errors: keepalive is best-effort and some test
294/// harnesses use sockets that don't support it.
295fn set_tcp_keepalive(stream: &tokio::net::TcpStream) {
296    let idle = hyperactor_config::global::get(config::CHANNEL_TCP_KEEPALIVE_IDLE);
297
298    let ka = socket2::TcpKeepalive::new()
299        .with_time(idle)
300        .with_interval(TCP_KEEPALIVE_INTERVAL)
301        .with_retries(TCP_KEEPALIVE_RETRIES);
302
303    let sock = socket2::SockRef::from(stream);
304    if let Err(err) = sock.set_tcp_keepalive(&ka) {
305        tracing::warn!(?err, "failed to set TCP keepalive on stream");
306    }
307}
308
309/// Swaps the congestion-control algorithm on an established socket. The algorithm
310/// comes from the [`config::CHANNEL_TCP_CONGESTION`] attribute and defaults to
311/// empty (off), so this is a no-op for the overwhelming majority of channels; only
312/// bandwidth-bound long-haul paths (e.g. remotemount's client->leader block ship)
313/// opt in (env `HYPERACTOR_CHANNEL_TCP_CONGESTION` or
314/// `configure(channel_tcp_congestion="bbr")`).
315///
316/// The usual Linux default is `cubic`, which underfills a link with a high
317/// bandwidth-delay product; `bbr` recovers that bandwidth. The algorithm is
318/// (re)selected on an established socket, so this must run after connect/accept
319/// rather than on the socket options up front.
320///
321/// BEST-EFFORT, like [`set_tcp_keepalive`] above: a rejected algorithm is logged and the
322/// host default is kept, rather than failing the connect. Whether a host permits a given
323/// controller cannot be known ahead of the call -- `tcp_allowed_congestion_control` is
324/// what an UNPRIVILEGED socket may select, so it is wrong under `CAP_NET_ADMIN`, absent
325/// where `/proc/sys` is masked, and nonexistent on FreeBSD -- so a caller cannot check
326/// first. Failing the connect would therefore turn a tuning preference into an outage on
327/// any host that happens not to allow it, which is a far worse trade than running untuned.
328///
329/// It is loud rather than silent, because untuned-but-believed-tuned is its own bug: the
330/// warning names the algorithm and the errno, which is the part that says what to fix
331/// (`ENOENT` is a name the host does not know, `EPERM` one it knows but will not let this
332/// process select). Note this reports the EFFECTIVE outcome; a read back of the config
333/// still reports what was requested, so the log is the authority on what actually took.
334fn set_tcp_congestion(stream: &tokio::net::TcpStream) {
335    let congestion = hyperactor_config::global::get_cloned(config::CHANNEL_TCP_CONGESTION);
336    let algo = Some(congestion.as_str()).filter(|algo| !algo.is_empty());
337    if let Err(err) = apply_tcp_congestion(stream, algo) {
338        tracing::warn!(?err, "leaving the host TCP congestion default in place");
339    }
340}
341
342/// Applies the resolved tuning to `stream`. Split from [`set_tcp_congestion`] so the
343/// socket call can be unit-tested without touching process-global config, and so the
344/// error is available to a caller that wants it -- [`set_tcp_congestion`] only logs.
345///
346/// The error names the algorithm because the bare errno does not: an unknown one reports
347/// `ENOENT`, which renders as "No such file or directory" without saying which file.
348#[cfg(any(target_os = "linux", target_os = "freebsd"))]
349fn apply_tcp_congestion(
350    stream: &tokio::net::TcpStream,
351    congestion: Option<&str>,
352) -> std::io::Result<()> {
353    let Some(algo) = congestion else {
354        return Ok(());
355    };
356    socket2::SockRef::from(stream)
357        .set_tcp_congestion(algo.as_bytes())
358        .map_err(|err| {
359            std::io::Error::new(
360                err.kind(),
361                format!("cannot select TCP congestion control {algo:?} ({err})"),
362            )
363        })
364}
365
366/// Fallback where `socket2` does not expose `set_tcp_congestion` (e.g. macOS): no
367/// algorithm can be selected, so an explicit request is an error. [`set_tcp_congestion`]
368/// turns that into a warning; an empty attribute never reaches here at all.
369#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
370fn apply_tcp_congestion(
371    _stream: &tokio::net::TcpStream,
372    congestion: Option<&str>,
373) -> std::io::Result<()> {
374    let Some(algo) = congestion else {
375        return Ok(());
376    };
377    Err(std::io::Error::new(
378        std::io::ErrorKind::Unsupported,
379        format!("cannot select TCP congestion control {algo:?}: not supported on this platform"),
380    ))
381}
382
383pub(crate) enum LinkStatus {
384    NeverConnected,
385    Connected(tokio::time::Instant),
386    Disconnected {
387        last_connected: tokio::time::Instant,
388        since: tokio::time::Instant,
389    },
390}
391
392impl LinkStatus {
393    fn connected(&mut self) {
394        *self = LinkStatus::Connected(tokio::time::Instant::now());
395    }
396
397    fn disconnected(&mut self) {
398        match *self {
399            LinkStatus::Connected(at) => {
400                *self = LinkStatus::Disconnected {
401                    last_connected: at,
402                    since: tokio::time::Instant::now(),
403                };
404            }
405            // Already disconnected or never connected — leave as is.
406            _ => {}
407        }
408    }
409}
410
411impl std::fmt::Display for LinkStatus {
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        match self {
414            LinkStatus::NeverConnected => write!(f, "never connected"),
415            LinkStatus::Connected(at) => {
416                write!(f, "connected for {:.1}s", at.elapsed().as_secs_f64())
417            }
418            LinkStatus::Disconnected {
419                last_connected,
420                since,
421            } => {
422                write!(
423                    f,
424                    "last connected {:.1}s ago, disconnected for {:.1}s",
425                    last_connected.elapsed().as_secs_f64(),
426                    since.elapsed().as_secs_f64(),
427                )
428            }
429        }
430    }
431}
432
433/// Log a send-loop error and return `true` if the error is terminal
434/// (caller should exit), `false` if recoverable (caller should reconnect).
435fn log_send_error(
436    error: &session::SendLoopError,
437    dest: &ChannelAddr,
438    session_id: u64,
439    mode: &str,
440    link_status: &LinkStatus,
441) -> bool {
442    match error {
443        session::SendLoopError::Io(err) => {
444            tracing::info!(dest = %dest, session_id, error = %err, mode, "send error; {link_status}");
445            metrics::CHANNEL_ERRORS.add(
446                1,
447                hyperactor_telemetry::kv_pairs!(
448                    "dest" => dest.to_string(),
449                    "session_id" => session_id.to_string(),
450                    "error_type" => metrics::ChannelErrorType::SendError.as_str(),
451                    "mode" => mode.to_string(),
452                ),
453            );
454            false
455        }
456        session::SendLoopError::AppClosed => true,
457        session::SendLoopError::Rejected(reason) => {
458            tracing::error!(dest = %dest, session_id, mode, "server rejected connection: {reason}; {link_status}");
459            true
460        }
461        session::SendLoopError::ServerClosed => {
462            tracing::info!(dest = %dest, session_id, mode, "server closed the channel; {link_status}");
463            true
464        }
465        session::SendLoopError::DeliveryTimeout => {
466            let timeout = hyperactor_config::global::get(config::MESSAGE_DELIVERY_TIMEOUT);
467            tracing::error!(
468                dest = %dest, session_id, mode,
469                "failed to receive ack within timeout {timeout:?}; link is currently connected; {link_status}"
470            );
471            true
472        }
473        session::SendLoopError::OversizedFrame { size, max } => {
474            tracing::error!(
475                dest = %dest,
476                session_id,
477                mode,
478                "oversized frame: len={size} > max={max}; {link_status}"
479            );
480            true
481        }
482    }
483}
484
485/// Map a terminal [`session::SendLoopError`] to a typed [`CloseReason`] for
486/// the `TxStatus` watcher. Variants that callers want to branch on (e.g.
487/// `DialMailboxRouter` keys cache eviction on `SequenceMismatch`) get their
488/// own typed reason; the rest flatten to `Other` carrying the same
489/// `{log_id}: {e}` text used previously for logging.
490fn classify_send_loop_error(error: &session::SendLoopError, log_id: &str) -> CloseReason {
491    match error {
492        session::SendLoopError::Rejected(reason) if reason.contains("out-of-sequence message") => {
493            CloseReason::SequenceMismatch(reason.clone())
494        }
495        session::SendLoopError::OversizedFrame { size, max } => CloseReason::OversizedFrame {
496            size: *size,
497            max: *max,
498        },
499        _ => CloseReason::Other(format!("{log_id}: {error}")),
500    }
501}
502
503/// Establish a simplex (send-only) session over the given link. Returns a send handle.
504pub(crate) fn spawn<M: RemoteMessage>(link: impl Link) -> super::ChannelTx<M> {
505    spawn_inner::<M>(link)
506}
507
508/// Establish a multi-stream (unordered) simplex session over N
509/// links sharing the same `SessionId`. Returns a single send handle
510/// that distributes frames across streams.
511pub(crate) fn spawn_unordered<M: RemoteMessage>(
512    links: Vec<impl Link + 'static>,
513) -> super::ChannelTx<M> {
514    assert!(!links.is_empty());
515    if links.len() == 1 {
516        return spawn(links.into_iter().next().unwrap());
517    }
518
519    let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
520    let dest = links[0].dest();
521    let session_id = links[0].link_id();
522    let (notify, status) = watch::channel(TxStatus::Active);
523    let tx = super::ChannelTx {
524        sender,
525        dest: dest.clone(),
526        status,
527    };
528
529    let num_streams = links.len();
530
531    crate::init::get_runtime().spawn(async move {
532        // Shared MPMC work queue. The dispatcher enqueues *unserialized*
533        // messages; each writer serializes its own pulls before writing.
534        // This spreads serialization cost across all N writer tasks
535        // instead of bottlenecking on the dispatcher.
536        let (queue_tx, queue_rx) =
537            async_channel::bounded::<session::PendingMessage<M>>(num_streams * 8);
538
539        // Shared unacked buffer: any writer's ack reader can prune it.
540        // BTreeMap keyed by seq — writers insert out of order
541        // (multiple streams, interleaved), so not a VecDeque.
542        let unacked: Arc<
543            tokio::sync::Mutex<std::collections::BTreeMap<u64, session::QueuedMessage<M>>>,
544        > = Arc::new(tokio::sync::Mutex::new(std::collections::BTreeMap::new()));
545
546        let mut writer_handles: Vec<tokio::task::JoinHandle<()>> = Vec::with_capacity(num_streams);
547        let log_id = format!("session {}.{:016x}", dest, session_id.0);
548
549        for (i, link) in links.into_iter().enumerate() {
550            let dest = dest.clone();
551            let unacked = unacked.clone();
552            let queue_rx = queue_rx.clone();
553            let log_id = log_id.clone();
554
555            writer_handles.push(tokio::spawn(async move {
556                let mut session = Session::new(link);
557                let mut reconnect_backoff = ExponentialBackoffBuilder::new()
558                    .with_initial_interval(Duration::from_millis(10))
559                    .with_multiplier(2.0)
560                    .with_randomization_factor(0.1)
561                    .with_max_interval(Duration::from_secs(5))
562                    .with_max_elapsed_time(None)
563                    .build();
564
565                loop {
566                    let connected = match session.connect().await {
567                        Ok(s) => s,
568                        Err(_) => {
569                            tracing::info!(
570                                dest = %dest, stream = i,
571                                "multi-stream writer {} connect failed", i
572                            );
573                            break;
574                        }
575                    };
576                    tracing::info!(
577                        dest = %dest, stream = i, "multi-stream writer {} connected", i
578                    );
579
580                    let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
581                    let connected_at = tokio::time::Instant::now();
582
583                    // Pull from the shared queue; serialize locally; write; interleave ack reads.
584                    let result: Result<(), session::SendLoopError> = async {
585                        loop {
586                            tokio::select! {
587                                biased;
588
589                                ack_result = stream.next() => {
590                                    match ack_result {
591                                        Ok(Some(buffer)) => {
592                                            let response = deserialize_response(buffer)
593                                                .map_err(|e| session::SendLoopError::Io(e.into()))?;
594                                            match response {
595                                                NetRxResponse::Ack(ack) => {
596                                                    let mut guard = unacked.lock().await;
597                                                    // Remove all entries with seq <= ack.
598                                                    let retain: std::collections::BTreeMap<u64, session::QueuedMessage<M>> = guard.split_off(&(ack + 1));
599                                                    let accepted = std::mem::replace(&mut *guard, retain);
600                                                    drop(guard);
601                                                    accepted.into_values().for_each(|queued| {
602                                                        queued.completion.accept();
603                                                    });
604                                                }
605                                                NetRxResponse::Reject(reason) => {
606                                                    return Err(session::SendLoopError::Rejected(reason));
607                                                }
608                                                NetRxResponse::Closed => {
609                                                    return Err(session::SendLoopError::ServerClosed);
610                                                }
611                                            }
612                                        }
613                                        Ok(None) => return Ok(()),
614                                        Err(e) => return Err(session::SendLoopError::Io(e.into())),
615                                    }
616                                }
617
618                                msg = queue_rx.recv() => {
619                                    let pending = match msg {
620                                        Ok(m) => m,
621                                        // Dispatcher closed the queue: clean shutdown.
622                                        Err(_) => return Ok(()),
623                                    };
624                                    let session::PendingMessage {
625                                        seq,
626                                        message,
627                                        received_at,
628                                        completion,
629                                    } = pending;
630                                    let frame = Frame::Message(seq, message);
631                                    let serialized = match serde_multipart::serialize_bincode(&frame) {
632                                        Ok(m) => m,
633                                        Err(e) => {
634                                            tracing::error!(
635                                                "{log_id}: serialization error: {e}"
636                                            );
637                                            completion.accept();
638                                            continue;
639                                        }
640                                    };
641                                    let mut queued = session::QueuedMessage {
642                                        seq,
643                                        message: serialized,
644                                        received_at,
645                                        sent_at: None,
646                                        completion,
647                                    };
648                                    let framed = queued.message.clone().framed();
649                                    stream.write(framed).drive().await.map_err(|e| {
650                                        session::SendLoopError::Io(e.into())
651                                    })?;
652                                    queued.sent_at = Some(tokio::time::Instant::now());
653                                    unacked.lock().await.insert(queued.seq, queued);
654                                }
655                            }
656                        }
657                    }
658                    .await;
659
660                    session = connected.release();
661
662                    if connected_at.elapsed() > Duration::from_secs(1) {
663                        reconnect_backoff.reset();
664                    }
665
666                    match result {
667                        Ok(()) => {
668                            if queue_rx.is_closed() {
669                                // Dispatcher is gone and queue is drained.
670                                break;
671                            }
672                            if let Some(delay) = reconnect_backoff.next_backoff() {
673                                tokio::time::sleep(delay).await;
674                            }
675                        }
676                        Err(ref e) => {
677                            if log_send_error(e, &dest, session_id.0, "multi-stream", &LinkStatus::NeverConnected) {
678                                break;
679                            }
680                            if let Some(delay) = reconnect_backoff.next_backoff() {
681                                tokio::time::sleep(delay).await;
682                            }
683                        }
684                    }
685                }
686
687                tracing::info!(
688                    dest = %dest,
689                    stream = i,
690                    "multi-stream writer {} shutting down",
691                    i,
692                );
693            }));
694        }
695
696        let cleanup_rx = queue_rx.clone();
697        // Drop our local receiver clone so the queue closes once the
698        // dispatcher's sender (queue_tx) is dropped at shutdown.
699        drop(queue_rx);
700
701        // Dispatcher: receive from app and enqueue for writers — no
702        // serialization here; the writer that pulls the item serializes
703        // it before writing.
704        let mut next_seq = 0u64;
705
706        tracing::info!(
707            %dest, session = %log_id, num_streams,
708            "multi-stream dispatcher started"
709        );
710
711        while let Some((message, completion, received_at)) = receiver.recv().await {
712            let pending = session::PendingMessage {
713                seq: next_seq,
714                message,
715                received_at,
716                completion,
717            };
718            next_seq += 1;
719
720            if let Err(async_channel::SendError(pending)) = queue_tx.send(pending).await {
721                pending.completion.accept();
722                // All writers are gone.
723                break;
724            }
725        }
726
727        // Shutdown: close the shared queue and wait for writers to drain.
728        drop(queue_tx);
729        for handle in writer_handles {
730            let _ = handle.await;
731        }
732        while let Ok(pending) = cleanup_rx.try_recv() {
733            pending.completion.accept();
734        }
735        for queued in Arc::into_inner(unacked)
736            .expect("writer handles should drop their unacked clones")
737            .into_inner()
738            .into_values()
739        {
740            queued.completion.accept();
741        }
742
743        let reason = format!("{log_id}: dispatcher closed");
744        let _ = notify.send(TxStatus::Closed(CloseReason::Other(reason)));
745    });
746
747    tx
748}
749
750fn spawn_inner<M: RemoteMessage>(link: impl Link) -> super::ChannelTx<M> {
751    let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
752    let dest = link.dest();
753    let session_id = link.link_id();
754    let (notify, status) = watch::channel(TxStatus::Active);
755    let tx = super::ChannelTx {
756        sender,
757        dest: dest.clone(),
758        status,
759    };
760    crate::init::get_runtime().spawn(async move {
761        let mut session = Session::new(link);
762        let log_id = format!("session {}.{:016x}", dest, session_id.0);
763        let mut deliveries = session::Deliveries {
764            outbox: session::Outbox::new(log_id.clone(), dest.clone(), session_id.0),
765            unacked: session::Unacked::new(None, log_id.clone()),
766        };
767        let mut receiver = receiver;
768
769        // Lazy connect: wait for first message.
770        match receiver.recv().await {
771            Some(msg) => {
772                if let Err(err) = deliveries.outbox.push_back(msg) {
773                    tracing::error!(
774                        dest = %dest,
775                        session_id = session_id.0,
776                        error = %err,
777                        "failed to push message to outbox"
778                    );
779                    let _ = notify.send(TxStatus::Closed(CloseReason::Other(
780                        "failed to push to outbox".into(),
781                    )));
782                    return;
783                }
784            }
785            None => {
786                let _ = notify.send(TxStatus::Closed(CloseReason::Other(
787                    "sender dropped".into(),
788                )));
789                return;
790            }
791        }
792
793        let mut reconnect_backoff = ExponentialBackoffBuilder::new()
794            .with_initial_interval(Duration::from_millis(10))
795            .with_multiplier(2.0)
796            .with_randomization_factor(0.1)
797            .with_max_interval(Duration::from_secs(5))
798            .with_max_elapsed_time(None)
799            .build();
800
801        let mut link_status = LinkStatus::NeverConnected;
802
803        let reason: CloseReason = 'outer: loop {
804            let connected = match deliveries.expiry_time() {
805                Some(deadline) => match session.connect_by(deadline).await {
806                    Ok(s) => s,
807                    Err(_) => {
808                        let timeout =
809                            hyperactor_config::global::get(config::MESSAGE_DELIVERY_TIMEOUT);
810                        let error_msg = if deliveries.outbox.is_expired(timeout) {
811                            format!("failed to deliver message within timeout {timeout:?}; {link_status}")
812                        } else {
813                            format!(
814                                "failed to receive ack within timeout {timeout:?}; \
815                                 link is currently broken; {link_status}",
816                            )
817                        };
818                        tracing::error!(
819                            dest = %dest, session_id = session_id.0, "{}", error_msg
820                        );
821                        break 'outer CloseReason::Other(format!("{log_id}: {error_msg}"));
822                    }
823                },
824                None => match session.connect().await {
825                    Ok(s) => s,
826                    Err(_) => break 'outer CloseReason::Other("session shut down".into()),
827                },
828            };
829
830            metrics::CHANNEL_CONNECTIONS.add(
831                1,
832                hyperactor_telemetry::kv_pairs!(
833                    "transport" => dest.transport().to_string(),
834                    "mode" => "simplex",
835                    "reason" => "link connected",
836                ),
837            );
838
839            if !deliveries.unacked.is_empty() {
840                metrics::CHANNEL_RECONNECTIONS.add(
841                    1,
842                    hyperactor_telemetry::kv_pairs!(
843                        "dest" => dest.to_string(),
844                        "transport" => dest.transport().to_string(),
845                        "mode" => "simplex",
846                        "reason" => "reconnect_with_unacked",
847                    ),
848                );
849            }
850            deliveries.requeue_unacked();
851
852            link_status.connected();
853            let connected_at = tokio::time::Instant::now();
854
855            let result = {
856                let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
857                session::send_connected(&stream, &mut deliveries, &mut receiver).await
858            };
859            session = connected.release();
860
861            link_status.disconnected();
862
863            // Reset backoff if the connection was alive long enough to have
864            // been useful (i.e. not an immediate EOF/error).
865            if connected_at.elapsed() > Duration::from_secs(1) {
866                reconnect_backoff.reset();
867            }
868
869            match result {
870                Ok(()) => {
871                    // EOF — connection closed normally, reconnect after backoff.
872                    if let Some(delay) = reconnect_backoff.next_backoff() {
873                        tracing::info!(
874                            dest = %dest,
875                            session_id = session_id.0,
876                            delay_ms = delay.as_millis() as u64,
877                            "send_connected returned EOF, reconnecting after backoff; {link_status}"
878                        );
879                        tokio::time::sleep(delay).await;
880                    }
881                }
882                Err(ref e) => {
883                    if log_send_error(e, &dest, session_id.0, "simplex", &link_status) {
884                        break 'outer classify_send_loop_error(e, &log_id);
885                    }
886                    // Recoverable error — reconnect after backoff.
887                    if let Some(delay) = reconnect_backoff.next_backoff() {
888                        tracing::info!(
889                            dest = %dest,
890                            session_id = session_id.0,
891                            delay_ms = delay.as_millis() as u64,
892                            error = %e,
893                            "send_connected returned recoverable error, reconnecting after backoff; {link_status}"
894                        );
895                        tokio::time::sleep(delay).await;
896                    }
897                }
898            }
899        };
900
901        tracing::info!(
902            dest = %dest, session_id = session_id.0, "ChannelTx closing: {reason}"
903        );
904
905        let send_error_reason = match &reason {
906            CloseReason::OversizedFrame { size, max } => Some(SendErrorReason::OversizedFrame {
907                len: *size,
908                max: *max,
909            }),
910            _ => Some(SendErrorReason::Other(reason.to_string())),
911        };
912        receiver.close();
913        deliveries
914            .unacked
915            .deque
916            .drain(..)
917            .chain(deliveries.outbox.deque.drain(..))
918            .for_each(|queued| queued.try_return(send_error_reason.clone()));
919        while let Ok((msg, completion, _)) = receiver.try_recv() {
920            completion.reject(SendError {
921                error: ChannelError::Closed,
922                message: msg,
923                reason: send_error_reason.clone(),
924            });
925        }
926
927        let _ = notify.send(TxStatus::Closed(reason));
928    }.instrument(tracing::debug_span!("net tx loop")));
929    tx
930}
931
932/// Transport-agnostic link that dispatches to the appropriate
933/// transport based on the channel address.
934#[derive(Debug)]
935pub(crate) enum NetLink {
936    Tcp(tcp::TcpLink),
937    Unix(unix::UnixLink),
938    Tls(tls::TlsLink),
939    Quic(quic::QuicLink),
940    Local(local::stream::LocalLink),
941}
942
943/// Create a link for the given channel address with the given
944/// `session_id` and `stream_id`. Single-stream callers pass a fresh
945/// `SessionId::random()` and `stream_id = 0`.
946/// Tagged with the protocol kind the client intends to speak.
947pub(crate) fn link(
948    addr: ChannelAddr,
949    session_id: SessionId,
950    stream_id: u8,
951    kind: ProtocolKind,
952) -> Result<NetLink, ClientError> {
953    match addr {
954        ChannelAddr::Tcp(socket_addr) => Ok(NetLink::Tcp(tcp::link(
955            socket_addr,
956            session_id,
957            stream_id,
958            kind,
959        ))),
960        ChannelAddr::Unix(unix_addr) => Ok(NetLink::Unix(unix::link(
961            unix_addr, session_id, stream_id, kind,
962        ))),
963        ChannelAddr::Local(port) => {
964            local::stream::check(port)?;
965            Ok(NetLink::Local(local::stream::link(
966                port, session_id, stream_id, kind,
967            )))
968        }
969        ChannelAddr::Tls(tls_addr) => Ok(NetLink::Tls(tls::link(
970            tls_addr, session_id, stream_id, kind,
971        )?)),
972        ChannelAddr::MetaTls(meta_addr) => Ok(NetLink::Tls(meta::link(
973            meta_addr, session_id, stream_id, kind,
974        )?)),
975        ChannelAddr::Quic(quic_addr) => Ok(NetLink::Quic(quic::link(
976            quic_addr,
977            quic::QuicAddrType::Quic,
978            session_id,
979            stream_id,
980            kind,
981        )?)),
982        ChannelAddr::MetaQuic(meta_addr) => Ok(NetLink::Quic(quic::link(
983            meta_addr,
984            quic::QuicAddrType::MetaQuic,
985            session_id,
986            stream_id,
987            kind,
988        )?)),
989        other => Err(ClientError::Connect(
990            other,
991            std::io::Error::other("unsupported transport"),
992            "unsupported transport".into(),
993        )),
994    }
995}
996
997#[async_trait]
998impl Link for NetLink {
999    type Stream = Box<dyn Stream>;
1000
1001    fn dest(&self) -> ChannelAddr {
1002        match self {
1003            Self::Tcp(l) => l.dest(),
1004            Self::Unix(l) => l.dest(),
1005            Self::Tls(l) => l.dest(),
1006            Self::Quic(l) => l.dest(),
1007            Self::Local(l) => l.dest(),
1008        }
1009    }
1010
1011    fn link_id(&self) -> SessionId {
1012        match self {
1013            Self::Tcp(l) => l.link_id(),
1014            Self::Unix(l) => l.link_id(),
1015            Self::Tls(l) => l.link_id(),
1016            Self::Quic(l) => l.link_id(),
1017            Self::Local(l) => l.link_id(),
1018        }
1019    }
1020
1021    async fn next(&mut self) -> Result<Box<dyn Stream>, ClientError> {
1022        match self {
1023            Self::Tcp(l) => Ok(Box::new(l.next().await?)),
1024            Self::Unix(l) => Ok(Box::new(l.next().await?)),
1025            Self::Tls(l) => Ok(Box::new(l.next().await?)),
1026            Self::Quic(l) => Ok(Box::new(l.next().await?)),
1027            Self::Local(l) => Ok(Box::new(l.next().await?)),
1028        }
1029    }
1030}
1031
1032/// Listener represents the server side of a network link: it accepts inbound connections.
1033///
1034/// This is the counterpart to [`Link`]. Each transport module (tcp, unix, tls)
1035/// provides both a `Link` impl (for dialing) and a `Listener` impl (for accepting).
1036#[async_trait]
1037pub(crate) trait Listener: Send + Unpin + 'static {
1038    /// The underlying stream type produced by accepting a connection.
1039    type Stream: Stream;
1040
1041    /// Accept the next inbound connection, returning the stream and the peer's address.
1042    async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError>;
1043}
1044
1045/// Transport-agnostic listener that dispatches to the appropriate
1046/// transport based on the channel address. TLS has no variant — it
1047/// uses `Tcp` under the hood (the TLS handshake happens in `prepare`,
1048/// not the listener).
1049#[derive(Debug)]
1050pub(crate) enum NetListener {
1051    Tcp(tcp::TcpSocketListener),
1052    Unix(unix::UnixSocketListener),
1053    Quic(quic::QuicSocketListener),
1054    Local(local::stream::LocalListener),
1055}
1056
1057#[async_trait]
1058impl Listener for NetListener {
1059    type Stream = Box<dyn Stream>;
1060
1061    async fn accept(&mut self) -> Result<(Box<dyn Stream>, ChannelAddr), ServerError> {
1062        match self {
1063            Self::Tcp(l) => {
1064                let (stream, addr) = l.accept().await?;
1065                Ok((Box::new(stream), addr))
1066            }
1067            Self::Unix(l) => {
1068                let (stream, addr) = l.accept().await?;
1069                Ok((Box::new(stream), addr))
1070            }
1071            Self::Quic(l) => {
1072                let (stream, addr) = l.accept().await?;
1073                Ok((Box::new(stream), addr))
1074            }
1075            Self::Local(l) => {
1076                let (stream, addr) = l.accept().await?;
1077                Ok((Box::new(stream), addr))
1078            }
1079        }
1080    }
1081}
1082
1083/// Bind a listener for the given channel address, optionally using a pre-opened TCP listener.
1084/// Returns the listener and the canonical address callers should advertise.
1085/// When `prebound` is `Some`, it is used for TCP/TLS transports instead of binding a new socket.
1086pub(crate) fn listen_with_prebound(
1087    addr: ChannelAddr,
1088    prebound: Option<std::net::TcpListener>,
1089) -> Result<(NetListener, ChannelAddr), ServerError> {
1090    match addr {
1091        ChannelAddr::Tcp(socket_addr) => {
1092            let std_listener = match prebound {
1093                Some(l) => l,
1094                None => std::net::TcpListener::bind(socket_addr)
1095                    .map_err(|err| ServerError::Listen(ChannelAddr::Tcp(socket_addr), err))?,
1096            };
1097            std_listener
1098                .set_nonblocking(true)
1099                .map_err(|e| ServerError::Listen(ChannelAddr::Tcp(socket_addr), e))?;
1100            let tokio_listener = tokio::net::TcpListener::from_std(std_listener)
1101                .map_err(|e| ServerError::Listen(ChannelAddr::Tcp(socket_addr), e))?;
1102            let local_addr = tokio_listener
1103                .local_addr()
1104                .map_err(|err| ServerError::Resolve(ChannelAddr::Tcp(socket_addr), err))?;
1105            let listener = tcp::TcpSocketListener {
1106                inner: tokio_listener,
1107                addr: local_addr,
1108            };
1109            Ok((NetListener::Tcp(listener), ChannelAddr::Tcp(local_addr)))
1110        }
1111        ChannelAddr::Unix(ref unix_addr) => {
1112            use std::os::unix::net::UnixDatagram as StdUnixDatagram;
1113            use std::os::unix::net::UnixListener as StdUnixListener;
1114
1115            let caddr = addr.clone();
1116            let maybe_listener = match unix_addr {
1117                unix::SocketAddr::Bound(sock_addr) => StdUnixListener::bind_addr(sock_addr),
1118                unix::SocketAddr::Unbound => StdUnixDatagram::unbound()
1119                    .and_then(|u| u.local_addr())
1120                    .and_then(|uaddr| StdUnixListener::bind_addr(&uaddr)),
1121            };
1122            let std_listener =
1123                maybe_listener.map_err(|err| ServerError::Listen(caddr.clone(), err))?;
1124            std_listener
1125                .set_nonblocking(true)
1126                .map_err(|err| ServerError::Listen(caddr.clone(), err))?;
1127            let local_addr = std_listener
1128                .local_addr()
1129                .map_err(|err| ServerError::Resolve(caddr.clone(), err))?;
1130            let tokio_listener = tokio::net::UnixListener::from_std(std_listener)
1131                .map_err(|err| ServerError::Io(caddr, err))?;
1132            let bound_addr = unix::SocketAddr::new(local_addr);
1133            let listener = unix::UnixSocketListener {
1134                inner: tokio_listener,
1135                addr: bound_addr.clone(),
1136            };
1137            Ok((NetListener::Unix(listener), ChannelAddr::Unix(bound_addr)))
1138        }
1139        ChannelAddr::Local(_) => {
1140            let (listener, addr) = local::stream::listen(addr, prebound)?;
1141            Ok((NetListener::Local(listener), addr))
1142        }
1143        addr @ (ChannelAddr::Tls(_) | ChannelAddr::MetaTls(_)) => {
1144            let is_meta = matches!(addr, ChannelAddr::MetaTls(_));
1145            let tls_addr = match addr {
1146                ChannelAddr::Tls(a) | ChannelAddr::MetaTls(a) => a,
1147                _ => unreachable!(),
1148            };
1149            let TlsAddr { hostname, port } = tls_addr;
1150            let make_channel_addr = |h: &str, p: Port| {
1151                if is_meta {
1152                    ChannelAddr::MetaTls(TlsAddr::new(h, p))
1153                } else {
1154                    ChannelAddr::Tls(TlsAddr::new(h, p))
1155                }
1156            };
1157
1158            let addrs: Vec<core::net::SocketAddr> = (hostname.as_ref(), port)
1159                .to_socket_addrs()
1160                .map_err(|err| ServerError::Resolve(make_channel_addr(&hostname, port), err))?
1161                .collect();
1162
1163            if addrs.is_empty() {
1164                return Err(ServerError::Resolve(
1165                    make_channel_addr(&hostname, port),
1166                    std::io::Error::other("no available socket addr"),
1167                ));
1168            }
1169
1170            let channel_addr = make_channel_addr(&hostname, port);
1171            let std_listener = match prebound {
1172                Some(l) => l,
1173                None => std::net::TcpListener::bind(&addrs[..])
1174                    .map_err(|err| ServerError::Listen(channel_addr.clone(), err))?,
1175            };
1176            std_listener
1177                .set_nonblocking(true)
1178                .map_err(|e| ServerError::Listen(channel_addr.clone(), e))?;
1179            let tokio_listener = tokio::net::TcpListener::from_std(std_listener)
1180                .map_err(|e| ServerError::Listen(channel_addr.clone(), e))?;
1181            let local_addr = tokio_listener
1182                .local_addr()
1183                .map_err(|err| ServerError::Resolve(channel_addr, err))?;
1184            let listener = tcp::TcpSocketListener {
1185                inner: tokio_listener,
1186                addr: local_addr,
1187            };
1188            Ok((
1189                NetListener::Tcp(listener),
1190                make_channel_addr(&hostname, local_addr.port()),
1191            ))
1192        }
1193        addr @ (ChannelAddr::Quic(_) | ChannelAddr::MetaQuic(_)) => {
1194            if prebound.is_some() {
1195                return Err(ServerError::Listen(
1196                    addr,
1197                    std::io::Error::other("pre-opened listener not supported for QUIC transport"),
1198                ));
1199            }
1200            let addr_type = match addr {
1201                ChannelAddr::Quic(_) => quic::QuicAddrType::Quic,
1202                ChannelAddr::MetaQuic(_) => quic::QuicAddrType::MetaQuic,
1203                _ => unreachable!(),
1204            };
1205            let tls_addr = match addr {
1206                ChannelAddr::Quic(a) | ChannelAddr::MetaQuic(a) => a,
1207                _ => unreachable!(),
1208            };
1209            let (listener, bound_addr) = quic::listen(tls_addr, addr_type)?;
1210            Ok((NetListener::Quic(listener), bound_addr))
1211        }
1212        ChannelAddr::Alias { dial_to, bind_to } => {
1213            // Bind the socket on `bind_to` (e.g. a wildcard interface), but
1214            // advertise `dial_to` as the canonical address. Callers refer to
1215            // this server by its dial address; the listener merely needs to
1216            // accept the connections that `dial_to` is routed to (e.g. via
1217            // NAT). The alias is fully consumed here -- everything downstream,
1218            // including the proc namespace derived from this address, uses
1219            // `dial_to`, which is what remote peers independently construct.
1220            let (listener, _bound_addr) = listen_with_prebound(*bind_to, prebound)?;
1221            Ok((listener, *dial_to))
1222        }
1223    }
1224}
1225
1226/// Frames are the messages sent between clients and servers over sessions.
1227#[derive(Debug, Serialize, Deserialize, EnumAsInner, PartialEq)]
1228pub(super) enum Frame<M> {
1229    /// Send a message with the provided sequence number.
1230    Message(u64, M),
1231}
1232
1233#[derive(Debug, Serialize, Deserialize, EnumAsInner)]
1234pub(super) enum NetRxResponse {
1235    Ack(u64),
1236    /// This session is rejected with the given reason. ChannelTx should stop reconnecting.
1237    Reject(String),
1238    /// This channel is closed.
1239    Closed,
1240}
1241
1242pub(super) fn serialize_response(
1243    response: NetRxResponse,
1244) -> Result<Bytes, bincode::error::EncodeError> {
1245    bincode::serde::encode_to_vec(&response, bincode::config::legacy()).map(|bytes| bytes.into())
1246}
1247
1248pub(super) fn deserialize_response(
1249    data: Bytes,
1250) -> Result<NetRxResponse, bincode::error::DecodeError> {
1251    bincode::serde::decode_from_slice(&data, bincode::config::legacy()).map(|(v, _)| v)
1252}
1253
1254/// Error returned during server operations.
1255#[derive(Debug, thiserror::Error)]
1256pub enum ServerError {
1257    /// An I/O error occurred while operating on the server at the given address.
1258    #[error("io: {1}")]
1259    Io(ChannelAddr, #[source] std::io::Error),
1260    /// Listening on the given address failed.
1261    #[error("listen: {0} {1}")]
1262    Listen(ChannelAddr, #[source] std::io::Error),
1263    /// Resolving the given address failed.
1264    #[error("resolve: {0} {1}")]
1265    Resolve(ChannelAddr, #[source] std::io::Error),
1266    /// An internal server error occurred for the given address.
1267    #[error("internal: {0} {1}")]
1268    Internal(ChannelAddr, #[source] anyhow::Error),
1269}
1270
1271#[derive(thiserror::Error, Debug)]
1272pub enum ClientError {
1273    #[error("connection to {0} failed: {1}: {2}")]
1274    Connect(ChannelAddr, std::io::Error, String),
1275    #[error("connection to {0} failed after {1:?} of retries: {2}")]
1276    ConnectTimeout(ChannelAddr, Duration, #[source] std::io::Error),
1277    #[error("unable to resolve address: {0}")]
1278    Resolve(ChannelAddr),
1279    #[error("io: {0} {1}")]
1280    Io(ChannelAddr, std::io::Error),
1281    #[error("send {0}: serialize: {1}")]
1282    Serialize(ChannelAddr, bincode::error::EncodeError),
1283    #[error("invalid address: {0}")]
1284    InvalidAddress(String),
1285}
1286
1287/// Tells whether the address is a 'net' address. These currently have different semantics
1288/// from local transports.
1289#[cfg(test)]
1290pub(super) fn is_net_addr(addr: &ChannelAddr) -> bool {
1291    matches!(
1292        addr.transport(),
1293        ChannelTransport::Tcp(_)
1294            | ChannelTransport::MetaTls(_)
1295            | ChannelTransport::Tls
1296            | ChannelTransport::Quic
1297            | ChannelTransport::MetaQuic(_)
1298            | ChannelTransport::Unix
1299            | ChannelTransport::Local
1300    )
1301}
1302
1303pub(crate) mod unix {
1304
1305    use core::str;
1306    use std::os::unix::net::SocketAddr as StdSocketAddr;
1307    use std::os::unix::net::UnixStream as StdUnixStream;
1308
1309    use rand::RngExt as _;
1310    use rand::distr::Alphanumeric;
1311    use tokio::net::UnixListener;
1312    use tokio::net::UnixStream;
1313
1314    use super::*;
1315
1316    #[derive(Debug)]
1317    pub(crate) struct UnixLink {
1318        pub(super) addr: SocketAddr,
1319        pub(super) session_id: SessionId,
1320        pub(super) stream_id: u8,
1321        pub(super) kind: ProtocolKind,
1322    }
1323
1324    #[async_trait]
1325    impl Link for UnixLink {
1326        type Stream = UnixStream;
1327
1328        fn dest(&self) -> ChannelAddr {
1329            ChannelAddr::Unix(self.addr.clone())
1330        }
1331
1332        fn link_id(&self) -> SessionId {
1333            self.session_id
1334        }
1335
1336        async fn next(&mut self) -> Result<Self::Stream, ClientError> {
1337            let session_id = self.session_id;
1338            let sock_addr = match &self.addr {
1339                SocketAddr::Bound(a) => a,
1340                SocketAddr::Unbound => return Err(ClientError::Resolve(self.dest())),
1341            };
1342            let mut backoff = ExponentialBackoffBuilder::new()
1343                .with_initial_interval(Duration::from_millis(1))
1344                .with_multiplier(2.0)
1345                .with_randomization_factor(0.1)
1346                .with_max_interval(Duration::from_millis(1000))
1347                .with_max_elapsed_time(None)
1348                .build();
1349            loop {
1350                match StdUnixStream::connect_addr(sock_addr) {
1351                    Ok(std_stream) => {
1352                        std_stream
1353                            .set_nonblocking(true)
1354                            .map_err(|err| ClientError::Io(self.dest(), err))?;
1355                        let mut stream = UnixStream::from_std(std_stream)
1356                            .map_err(|err| ClientError::Io(self.dest(), err))?;
1357                        write_link_init(&mut stream, session_id, self.stream_id, self.kind)
1358                            .await
1359                            .map_err(|err| ClientError::Io(self.dest(), err))?;
1360                        return Ok(stream);
1361                    }
1362                    Err(err) => {
1363                        tracing::debug!(error = %err, "unix connect failed, backing off");
1364                        if let Some(delay) = backoff.next_backoff() {
1365                            tokio::time::sleep(delay).await;
1366                        }
1367                    }
1368                }
1369            }
1370        }
1371    }
1372
1373    /// Server-side listener for Unix domain sockets.
1374    #[derive(Debug)]
1375    pub(crate) struct UnixSocketListener {
1376        pub(super) inner: UnixListener,
1377        pub(super) addr: SocketAddr,
1378    }
1379
1380    #[async_trait]
1381    impl super::Listener for UnixSocketListener {
1382        type Stream = UnixStream;
1383
1384        async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError> {
1385            let (stream, peer_addr) = self
1386                .inner
1387                .accept()
1388                .await
1389                .map_err(|err| ServerError::Io(ChannelAddr::Unix(self.addr.clone()), err))?;
1390            // tokio::net::unix::SocketAddr -> std::os::unix::net::SocketAddr
1391            let std_addr: StdSocketAddr = peer_addr.into();
1392            Ok((stream, ChannelAddr::Unix(SocketAddr::new(std_addr))))
1393        }
1394    }
1395
1396    /// Create a unix link to the given socket address.
1397    pub(crate) fn link(
1398        addr: SocketAddr,
1399        session_id: SessionId,
1400        stream_id: u8,
1401        kind: ProtocolKind,
1402    ) -> UnixLink {
1403        UnixLink {
1404            addr,
1405            session_id,
1406            stream_id,
1407            kind,
1408        }
1409    }
1410
1411    /// Wrapper around std-lib's unix::SocketAddr that lets us implement equality functions
1412    #[derive(Clone, Debug)]
1413    pub enum SocketAddr {
1414        Bound(Box<StdSocketAddr>),
1415        Unbound,
1416    }
1417
1418    impl PartialOrd for SocketAddr {
1419        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1420            Some(self.cmp(other))
1421        }
1422    }
1423
1424    impl Ord for SocketAddr {
1425        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1426            self.to_string().cmp(&other.to_string())
1427        }
1428    }
1429
1430    impl<'de> Deserialize<'de> for SocketAddr {
1431        fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1432        where
1433            D: serde::Deserializer<'de>,
1434        {
1435            let s = String::deserialize(deserializer)?;
1436            Self::from_str(&s).map_err(D::Error::custom)
1437        }
1438    }
1439
1440    impl Serialize for SocketAddr {
1441        fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1442        where
1443            S: serde::Serializer,
1444        {
1445            serializer.serialize_str(String::from(self).as_str())
1446        }
1447    }
1448
1449    impl From<&SocketAddr> for String {
1450        fn from(value: &SocketAddr) -> Self {
1451            match value {
1452                SocketAddr::Bound(addr) => match addr.as_pathname() {
1453                    Some(path) => path
1454                        .to_str()
1455                        .expect("unable to get str for path")
1456                        .to_string(),
1457                    #[cfg(target_os = "linux")]
1458                    _ => match addr.as_abstract_name() {
1459                        Some(name) => format!("@{}", String::from_utf8_lossy(name)),
1460                        _ => String::from("(unnamed)"),
1461                    },
1462                    #[cfg(not(target_os = "linux"))]
1463                    _ => String::from("(unnamed)"),
1464                },
1465                SocketAddr::Unbound => String::from("(unbound)"),
1466            }
1467        }
1468    }
1469
1470    impl FromStr for SocketAddr {
1471        type Err = anyhow::Error;
1472
1473        fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1474            match s {
1475                "" => {
1476                    // TODO: ensure this socket doesn't already exist. 24 bytes of randomness should be good for now but is not perfect.
1477                    // We can't use annon sockets because those are not valid across processes that aren't in the same process hierarchy aka forked.
1478                    let random_string = rand::rng()
1479                        .sample_iter(&Alphanumeric)
1480                        .take(24)
1481                        .map(char::from)
1482                        .collect::<String>();
1483                    SocketAddr::from_abstract_name(&random_string)
1484                }
1485                // by convention, named sockets are displayed with an '@' prefix
1486                name if name.starts_with("@") => {
1487                    SocketAddr::from_abstract_name(name.strip_prefix("@").unwrap())
1488                }
1489                path => SocketAddr::from_pathname(path),
1490            }
1491        }
1492    }
1493
1494    impl Eq for SocketAddr {}
1495    impl std::hash::Hash for SocketAddr {
1496        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1497            String::from(self).hash(state);
1498        }
1499    }
1500    impl PartialEq for SocketAddr {
1501        fn eq(&self, other: &Self) -> bool {
1502            match (self, other) {
1503                (Self::Bound(saddr), Self::Bound(oaddr)) => {
1504                    if saddr.is_unnamed() || oaddr.is_unnamed() {
1505                        return false;
1506                    }
1507
1508                    #[cfg(target_os = "linux")]
1509                    {
1510                        saddr.as_pathname() == oaddr.as_pathname()
1511                            && saddr.as_abstract_name() == oaddr.as_abstract_name()
1512                    }
1513                    #[cfg(not(target_os = "linux"))]
1514                    {
1515                        // On non-Linux platforms, only compare pathname since no abstract names
1516                        saddr.as_pathname() == oaddr.as_pathname()
1517                    }
1518                }
1519                (Self::Unbound, _) | (_, Self::Unbound) => false,
1520            }
1521        }
1522    }
1523
1524    impl fmt::Display for SocketAddr {
1525        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1526            match self {
1527                Self::Bound(addr) => match addr.as_pathname() {
1528                    Some(path) => {
1529                        write!(f, "{}", path.to_string_lossy())
1530                    }
1531                    #[cfg(target_os = "linux")]
1532                    _ => match addr.as_abstract_name() {
1533                        Some(name) => {
1534                            if name.starts_with(b"@") {
1535                                return write!(f, "{}", String::from_utf8_lossy(name));
1536                            }
1537                            write!(f, "@{}", String::from_utf8_lossy(name))
1538                        }
1539                        _ => write!(f, "(unnamed)"),
1540                    },
1541                    #[cfg(not(target_os = "linux"))]
1542                    _ => write!(f, "(unnamed)"),
1543                },
1544                Self::Unbound => write!(f, "(unbound)"),
1545            }
1546        }
1547    }
1548
1549    impl SocketAddr {
1550        /// Wraps the stdlib socket address for use with this module
1551        pub fn new(addr: StdSocketAddr) -> Self {
1552            Self::Bound(Box::new(addr))
1553        }
1554
1555        /// Abstract socket names start with a "@" by convention when displayed. If there is an
1556        /// "@" prefix, it will be stripped from the name before used.
1557        #[cfg(target_os = "linux")]
1558        pub fn from_abstract_name(name: &str) -> anyhow::Result<Self> {
1559            Ok(Self::new(StdSocketAddr::from_abstract_name(
1560                name.strip_prefix("@").unwrap_or(name),
1561            )?))
1562        }
1563
1564        #[cfg(not(target_os = "linux"))]
1565        pub fn from_abstract_name(name: &str) -> anyhow::Result<Self> {
1566            // On non-Linux platforms, convert abstract names to filesystem paths
1567            let name = name.strip_prefix("@").unwrap_or(name);
1568            let path = Self::abstract_to_filesystem_path(name);
1569            Self::from_pathname(&path.to_string_lossy())
1570        }
1571
1572        #[cfg(not(target_os = "linux"))]
1573        fn abstract_to_filesystem_path(abstract_name: &str) -> std::path::PathBuf {
1574            use std::collections::hash_map::DefaultHasher;
1575            use std::hash::Hash;
1576            use std::hash::Hasher;
1577
1578            // Generate a stable hash of the abstract name for deterministic paths
1579            let mut hasher = DefaultHasher::new();
1580            abstract_name.hash(&mut hasher);
1581            let hash = hasher.finish();
1582
1583            // Include process ID to prevent inter-process conflicts
1584            let process_id = std::process::id();
1585
1586            // TODO: we just leak these. Should we do something smarter?
1587            std::path::PathBuf::from(format!("/tmp/hyperactor_{}_{:x}", process_id, hash))
1588        }
1589
1590        /// Pathnames may be absolute or relative.
1591        pub fn from_pathname(name: &str) -> anyhow::Result<Self> {
1592            Ok(Self::new(StdSocketAddr::from_pathname(name)?))
1593        }
1594    }
1595
1596    impl TryFrom<SocketAddr> for StdSocketAddr {
1597        type Error = anyhow::Error;
1598
1599        fn try_from(value: SocketAddr) -> Result<Self, Self::Error> {
1600            match value {
1601                SocketAddr::Bound(addr) => Ok(*addr),
1602                SocketAddr::Unbound => Err(anyhow::anyhow!(
1603                    "std::os::unix::SocketAddr must be a bound address"
1604                )),
1605            }
1606        }
1607    }
1608}
1609
1610pub(crate) mod tcp {
1611    use tokio::net::TcpListener;
1612    use tokio::net::TcpStream;
1613
1614    use super::*;
1615
1616    #[derive(Debug)]
1617    pub(crate) struct TcpLink {
1618        pub(super) addr: SocketAddr,
1619        pub(super) session_id: SessionId,
1620        pub(super) stream_id: u8,
1621        pub(super) kind: ProtocolKind,
1622    }
1623
1624    #[async_trait]
1625    impl Link for TcpLink {
1626        type Stream = TcpStream;
1627
1628        fn dest(&self) -> ChannelAddr {
1629            ChannelAddr::Tcp(self.addr)
1630        }
1631
1632        fn link_id(&self) -> SessionId {
1633            self.session_id
1634        }
1635
1636        async fn next(&mut self) -> Result<Self::Stream, ClientError> {
1637            let session_id = self.session_id;
1638            let reconnect_timeout =
1639                hyperactor_config::global::get(config::CHANNEL_RECONNECT_TIMEOUT);
1640            let mut backoff = ExponentialBackoffBuilder::new()
1641                .with_initial_interval(Duration::from_millis(1))
1642                .with_multiplier(2.0)
1643                .with_randomization_factor(0.1)
1644                .with_max_interval(Duration::from_millis(1000))
1645                .with_max_elapsed_time(Some(reconnect_timeout))
1646                .build();
1647            loop {
1648                match TcpStream::connect(&self.addr).await {
1649                    Ok(mut stream) => {
1650                        stream.set_nodelay(true).map_err(|err| {
1651                            ClientError::Connect(
1652                                self.dest(),
1653                                err,
1654                                "cannot disable Nagle algorithm".to_string(),
1655                            )
1656                        })?;
1657                        set_tcp_keepalive(&stream);
1658                        set_tcp_congestion(&stream);
1659                        write_link_init(&mut stream, session_id, self.stream_id, self.kind)
1660                            .await
1661                            .map_err(|err| ClientError::Io(self.dest(), err))?;
1662                        return Ok(stream);
1663                    }
1664                    Err(err) => {
1665                        tracing::debug!(error = %err, "tcp connect failed, backing off");
1666                        match backoff.next_backoff() {
1667                            Some(delay) => tokio::time::sleep(delay).await,
1668                            None => {
1669                                return Err(ClientError::ConnectTimeout(
1670                                    self.dest(),
1671                                    reconnect_timeout,
1672                                    err,
1673                                ));
1674                            }
1675                        }
1676                    }
1677                }
1678            }
1679        }
1680    }
1681
1682    /// Server-side listener for TCP sockets.
1683    #[derive(Debug)]
1684    pub(crate) struct TcpSocketListener {
1685        pub(super) inner: TcpListener,
1686        pub(super) addr: SocketAddr,
1687    }
1688
1689    #[async_trait]
1690    impl super::Listener for TcpSocketListener {
1691        type Stream = TcpStream;
1692
1693        async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError> {
1694            let (stream, peer_addr) = self
1695                .inner
1696                .accept()
1697                .await
1698                .map_err(|err| ServerError::Io(ChannelAddr::Tcp(self.addr), err))?;
1699            stream
1700                .set_nodelay(true)
1701                .map_err(|err| ServerError::Io(ChannelAddr::Tcp(self.addr), err))?;
1702            set_tcp_keepalive(&stream);
1703            set_tcp_congestion(&stream);
1704            Ok((stream, ChannelAddr::Tcp(peer_addr)))
1705        }
1706    }
1707
1708    /// Create a TCP link to the given socket address.
1709    pub(crate) fn link(
1710        addr: SocketAddr,
1711        session_id: SessionId,
1712        stream_id: u8,
1713        kind: ProtocolKind,
1714    ) -> TcpLink {
1715        TcpLink {
1716            addr,
1717            session_id,
1718            stream_id,
1719            kind,
1720        }
1721    }
1722}
1723
1724// TODO: Try to simplify the TLS creation T208304433
1725pub(crate) mod meta {
1726    use std::io;
1727    use std::path::PathBuf;
1728    use std::sync::Arc;
1729
1730    use anyhow::Result;
1731    use tokio_rustls::TlsAcceptor;
1732    use tokio_rustls::TlsConnector;
1733
1734    use super::*;
1735    use crate::config::Pem;
1736    use crate::config::PemBundle;
1737
1738    const THRIFT_TLS_SRV_CA_PATH_ENV: &str = "THRIFT_TLS_SRV_CA_PATH";
1739    const DEFAULT_SRV_CA_PATH: &str = "/var/facebook/rootcanal/ca.pem";
1740    const THRIFT_TLS_CL_CERT_PATH_ENV: &str = "THRIFT_TLS_CL_CERT_PATH";
1741    const THRIFT_TLS_CL_KEY_PATH_ENV: &str = "THRIFT_TLS_CL_KEY_PATH";
1742    const DEFAULT_SERVER_PEM_PATH: &str = "/var/facebook/x509_identities/server.pem";
1743
1744    #[allow(clippy::result_large_err)] // TODO: Consider reducing the size of `ChannelError`.
1745    pub(crate) fn parse(addr_string: &str) -> Result<ChannelAddr, ChannelError> {
1746        // Use right split to allow for ipv6 addresses where ":" is expected.
1747        let parts = addr_string.rsplit_once(":");
1748        match parts {
1749            Some((hostname, port_str)) => {
1750                let Ok(port) = port_str.parse() else {
1751                    return Err(ChannelError::InvalidAddress(addr_string.to_string()));
1752                };
1753                Ok(ChannelAddr::MetaTls(TlsAddr::new(hostname, port)))
1754            }
1755            _ => Err(ChannelError::InvalidAddress(addr_string.to_string())),
1756        }
1757    }
1758
1759    /// Construct a PemBundle for server operations from Meta-specific paths.
1760    /// Server cert and key come from the same file (server.pem).
1761    pub(super) fn get_server_pem_bundle() -> PemBundle {
1762        let ca_path = std::env::var_os(THRIFT_TLS_SRV_CA_PATH_ENV)
1763            .map(PathBuf::from)
1764            .unwrap_or_else(|| PathBuf::from(DEFAULT_SRV_CA_PATH));
1765        let server_pem_path = PathBuf::from(DEFAULT_SERVER_PEM_PATH);
1766        PemBundle {
1767            ca: Pem::File(ca_path),
1768            cert: Pem::File(server_pem_path.clone()),
1769            key: Pem::File(server_pem_path),
1770        }
1771    }
1772
1773    /// Construct a PemBundle for client operations from Meta-specific env vars.
1774    /// Returns None if client cert/key env vars are not set.
1775    fn get_client_pem_bundle() -> Option<PemBundle> {
1776        let cert_path = std::env::var_os(THRIFT_TLS_CL_CERT_PATH_ENV)?;
1777        let key_path = std::env::var_os(THRIFT_TLS_CL_KEY_PATH_ENV)?;
1778        let ca_path = std::env::var_os(THRIFT_TLS_SRV_CA_PATH_ENV)
1779            .map(PathBuf::from)
1780            .unwrap_or_else(|| PathBuf::from(DEFAULT_SRV_CA_PATH));
1781        Some(PemBundle {
1782            ca: Pem::File(ca_path),
1783            cert: Pem::File(PathBuf::from(cert_path)),
1784            key: Pem::File(PathBuf::from(key_path)),
1785        })
1786    }
1787
1788    /// Creates a TLS acceptor by looking for necessary certs and keys in a Meta server environment.
1789    pub(crate) fn tls_acceptor(enforce_client_tls: bool) -> Result<TlsAcceptor> {
1790        Ok(TlsAcceptor::from(Arc::new(server_config(
1791            enforce_client_tls,
1792        )?)))
1793    }
1794
1795    /// Try to create a TLS connector for Meta environments.
1796    ///
1797    /// Returns `Ok` when the root CA is present (optional client certs
1798    /// are added when `THRIFT_TLS_CL_CERT_PATH` / `THRIFT_TLS_CL_KEY_PATH`
1799    /// are set).
1800    pub(super) fn try_tls_connector() -> Result<TlsConnector> {
1801        tls_connector()
1802    }
1803
1804    /// Creates a TLS connector by looking for necessary certs and keys in a Meta server environment.
1805    /// Supports optional client authentication (unlike the tls module which always requires it).
1806    fn tls_connector() -> Result<TlsConnector> {
1807        Ok(TlsConnector::from(Arc::new(client_config()?)))
1808    }
1809
1810    pub(super) fn server_config(enforce_client_tls: bool) -> Result<rustls::ServerConfig> {
1811        let bundle = get_server_pem_bundle();
1812        tls::server_config_from_bundle(&bundle, enforce_client_tls)
1813    }
1814
1815    pub(super) fn client_config() -> Result<rustls::ClientConfig> {
1816        Ok(if let Some(bundle) = get_client_pem_bundle() {
1817            tls::client_config_from_bundle(&bundle)?
1818        } else {
1819            let ca_path = std::env::var_os(THRIFT_TLS_SRV_CA_PATH_ENV)
1820                .map(PathBuf::from)
1821                .unwrap_or_else(|| PathBuf::from(DEFAULT_SRV_CA_PATH));
1822            let ca_pem = Pem::File(ca_path);
1823            tls::client_config_from_ca(&ca_pem)?
1824        })
1825    }
1826
1827    /// Create a MetaTLS link to the given address.
1828    pub fn link(
1829        addr: TlsAddr,
1830        session_id: SessionId,
1831        stream_id: u8,
1832        kind: ProtocolKind,
1833    ) -> Result<tls::TlsLink, ClientError> {
1834        let connector = tls_connector().map_err(|e| {
1835            ClientError::Connect(
1836                ChannelAddr::MetaTls(addr.clone()),
1837                io::Error::other(e.to_string()),
1838                "failed to create TLS connector".to_string(),
1839            )
1840        })?;
1841        let TlsAddr { hostname, port } = addr;
1842        Ok(tls::TlsLink {
1843            hostname,
1844            port,
1845            connector,
1846            addr_type: tls::TlsAddrType::MetaTls,
1847            session_id,
1848            stream_id,
1849            kind,
1850        })
1851    }
1852}
1853
1854/// TLS transport module using configurable certificates via hyperactor config attributes.
1855pub(crate) mod tls {
1856    use std::io;
1857    use std::io::BufReader;
1858    use std::sync::Arc;
1859
1860    use anyhow::Context;
1861    use anyhow::Result;
1862    use rustls::ClientConfig;
1863    use rustls::RootCertStore;
1864    use rustls::ServerConfig;
1865    use rustls::pki_types::CertificateDer;
1866    use rustls::pki_types::PrivateKeyDer;
1867    use rustls::pki_types::ServerName;
1868    use tokio::net::TcpStream;
1869    use tokio_rustls::TlsAcceptor;
1870    use tokio_rustls::TlsConnector;
1871    use tokio_rustls::client::TlsStream;
1872
1873    use super::*;
1874    use crate::channel::TlsAddr;
1875    use crate::config::Pem;
1876    use crate::config::PemBundle;
1877    use crate::config::TLS_CA;
1878    use crate::config::TLS_CERT;
1879    use crate::config::TLS_KEY;
1880
1881    /// Distinguishes between Tls and MetaTls for address construction.
1882    #[derive(Debug, Clone, Copy)]
1883    pub(crate) enum TlsAddrType {
1884        Tls,
1885        MetaTls,
1886    }
1887
1888    /// Parse an address string into a TlsAddr.
1889    #[allow(clippy::result_large_err)]
1890    pub(crate) fn parse(addr_string: &str) -> Result<ChannelAddr, ChannelError> {
1891        // Use right split to allow for ipv6 addresses where ":" is expected.
1892        let parts = addr_string.rsplit_once(":");
1893        match parts {
1894            Some((hostname, port_str)) => {
1895                let Ok(port) = port_str.parse() else {
1896                    return Err(ChannelError::InvalidAddress(addr_string.to_string()));
1897                };
1898                Ok(ChannelAddr::Tls(TlsAddr::new(hostname, port)))
1899            }
1900            _ => Err(ChannelError::InvalidAddress(addr_string.to_string())),
1901        }
1902    }
1903
1904    /// Load certificates from a Pem value.
1905    pub(super) fn load_certs(pem: &Pem) -> Result<Vec<CertificateDer<'static>>> {
1906        let mut reader = BufReader::new(pem.reader()?);
1907        let certs = rustls_pemfile::certs(&mut reader)
1908            .filter_map(Result::ok)
1909            .collect();
1910        Ok(certs)
1911    }
1912
1913    /// Load a private key from a Pem value.
1914    pub(super) fn load_key(pem: &Pem) -> Result<PrivateKeyDer<'static>> {
1915        let mut reader = BufReader::new(pem.reader()?);
1916        loop {
1917            break match rustls_pemfile::read_one(&mut reader)? {
1918                Some(rustls_pemfile::Item::Pkcs1Key(key)) => Ok(PrivateKeyDer::Pkcs1(key)),
1919                Some(rustls_pemfile::Item::Pkcs8Key(key)) => Ok(PrivateKeyDer::Pkcs8(key)),
1920                Some(rustls_pemfile::Item::Sec1Key(key)) => Ok(PrivateKeyDer::Sec1(key)),
1921                Some(_) => continue,
1922                None => anyhow::bail!("no private key found in TLS key file"),
1923            };
1924        }
1925    }
1926
1927    /// Build root certificate store from the CA pem.
1928    pub(super) fn build_root_store(ca_pem: &Pem) -> Result<RootCertStore> {
1929        let mut root_store = RootCertStore::empty();
1930        let certs = load_certs(ca_pem)?;
1931        root_store.add_parsable_certificates(certs);
1932        Ok(root_store)
1933    }
1934
1935    /// Get the PEM bundle from configuration.
1936    pub(super) fn get_pem_bundle() -> PemBundle {
1937        PemBundle {
1938            ca: hyperactor_config::global::get_cloned(TLS_CA),
1939            cert: hyperactor_config::global::get_cloned(TLS_CERT),
1940            key: hyperactor_config::global::get_cloned(TLS_KEY),
1941        }
1942    }
1943
1944    fn install_default_crypto_provider() {
1945        // Ensure ring is installed as the process-level crypto provider.
1946        // No-op when already installed (e.g. under Buck with native-tls).
1947        let _ = rustls::crypto::ring::default_provider().install_default();
1948    }
1949
1950    /// Creates a Rustls server config using certificates from the provided PEM bundle.
1951    /// If `enforce_client_tls` is true, requires client certificates for mutual TLS.
1952    pub(super) fn server_config_from_bundle(
1953        bundle: &PemBundle,
1954        enforce_client_tls: bool,
1955    ) -> Result<ServerConfig> {
1956        install_default_crypto_provider();
1957
1958        let certs = load_certs(&bundle.cert).context("load TLS certificate")?;
1959        let key = load_key(&bundle.key).context("load TLS key")?;
1960        let root_store = build_root_store(&bundle.ca).context("build root cert store")?;
1961
1962        let config = ServerConfig::builder();
1963        let config = if enforce_client_tls {
1964            // Build server config with mutual TLS (require client certs)
1965            let client_verifier =
1966                rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
1967                    .build()
1968                    .map_err(|e| anyhow::anyhow!("failed to build client verifier: {}", e))?;
1969            config.with_client_cert_verifier(client_verifier)
1970        } else {
1971            config.with_no_client_auth()
1972        }
1973        .with_single_cert(certs, key)?;
1974
1975        Ok(config)
1976    }
1977
1978    /// Creates a TLS acceptor using certificates from the provided PEM bundle.
1979    /// If `enforce_client_tls` is true, requires client certificates for mutual TLS.
1980    pub(super) fn tls_acceptor_from_bundle(
1981        bundle: &PemBundle,
1982        enforce_client_tls: bool,
1983    ) -> Result<TlsAcceptor> {
1984        let config = server_config_from_bundle(bundle, enforce_client_tls)?;
1985        Ok(TlsAcceptor::from(Arc::new(config)))
1986    }
1987
1988    /// Creates a TLS acceptor using certificates from config (always enforces mutual TLS).
1989    pub(crate) fn tls_acceptor() -> Result<TlsAcceptor> {
1990        tls_acceptor_from_bundle(&get_pem_bundle(), true)
1991    }
1992
1993    /// Creates a Rustls client config using only CA roots.
1994    pub(super) fn client_config_from_ca(ca_pem: &Pem) -> Result<ClientConfig> {
1995        install_default_crypto_provider();
1996
1997        let root_store = build_root_store(ca_pem).context("build root cert store")?;
1998        Ok(ClientConfig::builder()
1999            .with_root_certificates(Arc::new(root_store))
2000            .with_no_client_auth())
2001    }
2002
2003    /// Creates a Rustls client config using certificates from the provided PEM bundle.
2004    pub(super) fn client_config_from_bundle(bundle: &PemBundle) -> Result<ClientConfig> {
2005        install_default_crypto_provider();
2006
2007        let certs = load_certs(&bundle.cert).context("load TLS certificate")?;
2008        let key = load_key(&bundle.key).context("load TLS key")?;
2009        let root_store = build_root_store(&bundle.ca).context("build root cert store")?;
2010
2011        let config = ClientConfig::builder()
2012            .with_root_certificates(Arc::new(root_store))
2013            .with_client_auth_cert(certs, key)
2014            .context("configure client auth")?;
2015
2016        Ok(config)
2017    }
2018
2019    /// Creates a TLS connector using certificates from the provided PEM bundle.
2020    pub(super) fn tls_connector_from_bundle(bundle: &PemBundle) -> Result<TlsConnector> {
2021        let config = client_config_from_bundle(bundle)?;
2022        Ok(TlsConnector::from(Arc::new(config)))
2023    }
2024
2025    /// Creates a TLS connector using certificates from config.
2026    fn tls_connector() -> Result<TlsConnector> {
2027        tls_connector_from_bundle(&get_pem_bundle())
2028    }
2029
2030    /// Shared TLS link implementation used by both tls and metatls transports.
2031    pub(crate) struct TlsLink {
2032        pub(crate) hostname: Hostname,
2033        pub(crate) port: Port,
2034        pub(crate) connector: TlsConnector,
2035        pub(crate) addr_type: TlsAddrType,
2036        pub(crate) session_id: SessionId,
2037        pub(crate) stream_id: u8,
2038        pub(crate) kind: ProtocolKind,
2039    }
2040
2041    impl std::fmt::Debug for TlsLink {
2042        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2043            f.debug_struct("TlsLink")
2044                .field("hostname", &self.hostname)
2045                .field("port", &self.port)
2046                .field("addr_type", &self.addr_type)
2047                .finish()
2048        }
2049    }
2050
2051    #[async_trait]
2052    impl Link for TlsLink {
2053        type Stream = TlsStream<TcpStream>;
2054
2055        fn dest(&self) -> ChannelAddr {
2056            let addr = TlsAddr::new(self.hostname.clone(), self.port);
2057            match self.addr_type {
2058                TlsAddrType::Tls => ChannelAddr::Tls(addr),
2059                TlsAddrType::MetaTls => ChannelAddr::MetaTls(addr),
2060            }
2061        }
2062
2063        fn link_id(&self) -> SessionId {
2064            self.session_id
2065        }
2066
2067        async fn next(&mut self) -> Result<Self::Stream, ClientError> {
2068            let session_id = self.session_id;
2069            let server_name = ServerName::try_from(self.hostname.clone()).map_err(|e| {
2070                ClientError::Connect(
2071                    self.dest(),
2072                    io::Error::other(e.to_string()),
2073                    "invalid server name".to_string(),
2074                )
2075            })?;
2076            let reconnect_timeout =
2077                hyperactor_config::global::get(config::CHANNEL_RECONNECT_TIMEOUT);
2078            let mut backoff = ExponentialBackoffBuilder::new()
2079                .with_initial_interval(Duration::from_millis(1))
2080                .with_multiplier(2.0)
2081                .with_randomization_factor(0.1)
2082                .with_max_interval(Duration::from_millis(1000))
2083                .with_max_elapsed_time(Some(reconnect_timeout))
2084                .build();
2085            loop {
2086                let mut addrs = (self.hostname.as_ref(), self.port)
2087                    .to_socket_addrs()
2088                    .map_err(|_| ClientError::Resolve(self.dest()))?;
2089                let addr = addrs.next().ok_or(ClientError::Resolve(self.dest()))?;
2090                match TcpStream::connect(&addr).await {
2091                    Ok(stream) => {
2092                        stream.set_nodelay(true).map_err(|err| {
2093                            ClientError::Connect(
2094                                self.dest(),
2095                                err,
2096                                "cannot disable Nagle algorithm".to_string(),
2097                            )
2098                        })?;
2099                        set_tcp_keepalive(&stream);
2100                        set_tcp_congestion(&stream);
2101                        let mut tls_stream = self
2102                            .connector
2103                            .connect(server_name.clone(), stream)
2104                            .await
2105                            .map_err(|err| {
2106                                tracing::info!(
2107                                    dest = %self.dest(),
2108                                    error = %err,
2109                                    "TLS handshake failed"
2110                                );
2111                                ClientError::Connect(
2112                                    self.dest(),
2113                                    err,
2114                                    format!("cannot establish TLS connection to {:?}", server_name),
2115                                )
2116                            })?;
2117                        write_link_init(&mut tls_stream, session_id, self.stream_id, self.kind)
2118                            .await
2119                            .map_err(|err| ClientError::Io(self.dest(), err))?;
2120                        return Ok(tls_stream);
2121                    }
2122                    Err(err) => {
2123                        tracing::debug!(error = %err, "tls connect failed, backing off");
2124                        match backoff.next_backoff() {
2125                            Some(delay) => tokio::time::sleep(delay).await,
2126                            None => {
2127                                return Err(ClientError::ConnectTimeout(
2128                                    self.dest(),
2129                                    reconnect_timeout,
2130                                    err,
2131                                ));
2132                            }
2133                        }
2134                    }
2135                }
2136            }
2137        }
2138    }
2139
2140    /// Create a TLS link to the given address.
2141    pub fn link(
2142        addr: TlsAddr,
2143        session_id: SessionId,
2144        stream_id: u8,
2145        kind: ProtocolKind,
2146    ) -> Result<TlsLink, ClientError> {
2147        let connector = tls_connector().map_err(|e| {
2148            ClientError::Connect(
2149                ChannelAddr::Tls(addr.clone()),
2150                io::Error::other(e.to_string()),
2151                "failed to create TLS connector".to_string(),
2152            )
2153        })?;
2154        let TlsAddr { hostname, port } = addr;
2155        Ok(TlsLink {
2156            hostname,
2157            port,
2158            connector,
2159            addr_type: TlsAddrType::Tls,
2160            session_id,
2161            stream_id,
2162            kind,
2163        })
2164    }
2165
2166    #[cfg(test)]
2167    mod tests {
2168        use timed_test::async_timed_test;
2169
2170        use super::*;
2171        use crate::channel::ChannelTx;
2172        use crate::channel::Rx;
2173        use crate::channel::Tx;
2174        use crate::channel::dial;
2175        use crate::channel::net::server;
2176        use crate::channel::serve;
2177        use crate::channel::unordered;
2178        use crate::config::Pem;
2179        use crate::config::TLS_CA;
2180        use crate::config::TLS_CERT;
2181        use crate::config::TLS_KEY;
2182
2183        // Dummy test certificates generated with openssl for testing only.
2184        // These certificates include Subject Alternative Names (SAN) for localhost, 127.0.0.1, and ::1
2185        // CA certificate
2186        const TEST_CA_CERT: &str = r#"-----BEGIN CERTIFICATE-----
2187MIIDBTCCAe2gAwIBAgIUaGNmboiIosG+8Up0vgDr/+cg+2IwDQYJKoZIhvcNAQEL
2188BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAxMjgxNzA4MzlaFw0yNzAxMjgx
2189NzA4MzlaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
2190DwAwggEKAoIBAQC9RBoMYXCajklswt8Vi1JI1lEYzic0WNOmz45vG/7H6jTWkgL3
2191K5Ri+Seg3MobDNc48YHWXYm4hP9wCzkx8ih3ntT5XiY1My/G3jLUuoIEE9pF/BoJ
2192YQwZVoPNFhA9WhXNRsINf1cXFf8NzRfXpxBfKWtQJxYXU4JiDBQ6rLnQQABo8JmQ
2193vYFhJbBaYip5jTSiVNn7mB1zNr5jsVxuoSF53Pb7xQ76bwBdOq4zd6PSxL5/lr4G
2194cHSoxwZQdZMG7PL6hbxDQ2S2YI2lYVET1zwc2WPKCfjbEXBC/jzx828CInQtuksk
219518gJt6xHkTFEA8CSA29GM3lejnwYWf51xyyBAgMBAAGjUzBRMB0GA1UdDgQWBBRX
2196cbxSZ70NsUkAS3Hhy6irugywJDAfBgNVHSMEGDAWgBRXcbxSZ70NsUkAS3Hhy6ir
2197ugywJDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA7aAFfyW67
2198Z+uGSVYhpsT/uH/3Z3nr7X1smTz5CGEfq2czEcTC7gbYI2l8GZ47GPfnAvHTBZVm
2199V/XncBCsj7/thOh2jYEHFyCbPckoaSCRyCOnK7LPUlr4HN5uP9EFe45qBLCJDEoY
2200GTTw7MtzwdovfjchNfKQCTtkBJCXQ95WLCf6UOh02Sn28UTlgfXzF0X0FrcWqWa3
2201uJZd4XOo4O6hKKlHaBaQPiEr++1xc3SWPV7jZHbckI/vKBnDdEZ9JQX5fFZuypUI
2202sgomYHxvxrU2hWx+7k53CRdjfaIvT9Ie44z9sSdsU/+blw2S8f/ZTmuECoIAAXYO
22030qpzlxZMdr7T
2204-----END CERTIFICATE-----"#;
2205
2206        // Server certificate (signed by CA) with SAN for localhost, 127.0.0.1, ::1
2207        const TEST_SERVER_CERT: &str = r#"-----BEGIN CERTIFICATE-----
2208MIIDJDCCAgygAwIBAgIUaz66DsWaH5ZXM4hCFnbVbMsyN1cwDQYJKoZIhvcNAQEL
2209BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAxMjgxNzA4MzlaFw0yNzAxMjgx
2210NzA4MzlaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD
2211ggEPADCCAQoCggEBAKCbp++qNyTn5LOsV0h9gLKJALBcjg2A14I3804N9UyDhPW2
2212QKQ2W424u2P1MfKrw/2C+CErGlrADlnco2RQVDAarAIuGdFvBOt5UezqOS7Mk4OS
22139MlS7NZnMbc37KuM9UIG5ScJjXR/Z5z9dxeR0I9y3n0Ix6khbV7tOSHobiweI0FI
22148LftBS+CQnXr6vbWPcHcW6Z0FHUv7IWhqMWmv9PlZRGe9Y6VzXrRp0PBnZMOnAYf
2215aMQUwYRswWdm9j9Z1sMdTJ14G+KVmO3Vj6XI6Sm9uIcYhlwG/kORwogJFWlVuP9o
2216rloFRCjyHJ1d7GZqqnRyHHDDCBms8ed+3YfEYQECAwEAAaNwMG4wLAYDVR0RBCUw
2217I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB0GA1UdDgQWBBQl
2218J4vxUoCzqqeTwQAiLqE8wYezKzAfBgNVHSMEGDAWgBRXcbxSZ70NsUkAS3Hhy6ir
2219ugywJDANBgkqhkiG9w0BAQsFAAOCAQEAnXHIBDQ4AHAMV71piTOuI41ShASQed6L
2220bi7XUMZgZDslLkfU1vnP3BlwpliraBsAytSYQC6kbytOuz1uQ4K7yLb2tAAmUgEO
2221EdIVt9SXr5tCcIPeLmInF0pysPqjZO8n7vtJyd9gryKqdhm1uzA7WQWq/Az8a9Sk
2222uW2J6Oc5p6P7Mf3/ixqXzvGRo8rzu0CUJOJ67UTE/HhbJuplQ5dep5CEEOAIsAtH
2223zn9O4rW92ueBkoBJM++YILS1vQ7jKc2N3RNrnHm7FeootBrtR9mBi0TH97K73ZPZ
22242Cdhnym0CsCJggrllFGH32cYo7+K2PO7/4oj5XbBCSWcssicvd8ovg==
2225-----END CERTIFICATE-----"#;
2226
2227        // Server private key
2228        // test-only embedded key -- though it might expire at some point:
2229        // @lint-ignore PRIVATEKEY insecure-private-key-storage
2230        const TEST_SERVER_KEY: &str = r#"-----BEGIN PRIVATE KEY-----
2231MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCgm6fvqjck5+Sz
2232rFdIfYCyiQCwXI4NgNeCN/NODfVMg4T1tkCkNluNuLtj9THyq8P9gvghKxpawA5Z
22333KNkUFQwGqwCLhnRbwTreVHs6jkuzJODkvTJUuzWZzG3N+yrjPVCBuUnCY10f2ec
2234/XcXkdCPct59CMepIW1e7Tkh6G4sHiNBSPC37QUvgkJ16+r21j3B3FumdBR1L+yF
2235oajFpr/T5WURnvWOlc160adDwZ2TDpwGH2jEFMGEbMFnZvY/WdbDHUydeBvilZjt
22361Y+lyOkpvbiHGIZcBv5DkcKICRVpVbj/aK5aBUQo8hydXexmaqp0chxwwwgZrPHn
2237ft2HxGEBAgMBAAECgf8G5qlQov+7ljs9fSpC8yGUik59RXzVF7Qq5DyQHglsQDp2
2238VF5yr+M/M7DZmq+KvdauDfKbej6np5j2Q4TByrHTX1IExfZWCW8srwnWJDpQyHmO
2239LcJW5DlI/SYluUFyHZxsOd+ezcpGNzM8i6eSW7GaeFUXCkmJ+uW4LnlF+7bALnnd
2240D6sak/58EsII+IJyd4lFn+voszlPn3CZGR0jkp21rvpaKgrMIsKVWWQO/sLDU5pr
2241VbpBThcLU5gRcnQouQX12e2VTCIlFu75WTsJ8V/KnEaOZUVlU/B/Bs+WQF3U+/Jo
2242eX4N+D6OsEcNQjERAFyWujxsl1WpD4uSsbFMN0ECgYEA2b7AdL+oKPQHku2KcBhr
2243Zw8K4tMDlr2VPPNwZcBTLo+O71vv/xXjMcXrXmowzkgEQckUmt1VB46riyydhwdP
2244/n9ciWcz0Va/nwHR6Y9F9unBiyUBP7PRhRyjQyRZZRGDSJvP+Xmc5UJFpRr07VLU
2245nfgMXDj37vXzKDpfhdEB2nkCgYEAvNMfA8P8w3+6246x5YHflvTkPdw+2oyge+LD
2246mphB/w7SF8mlyNGloj3+KBZmd9SkvT57wCvO96Y9/n+mBAVisRggc0hK4ymOVYhb
2247+im/JvqGQMbVeg6iCOHnWdaZf9tL8uVsegQy3kVTN7vAa+CMFgX1dt65cGBX6XkB
224844pYmMkCgYALhbiRdQLlB+TOtZs5y1EDpxwgXKI3+9hF3Wv5NnAwapBZwje0++eF
22493r9Rw7TJda4j/QwGFehF+hrBxp6fYpetE/hFnRx0225Qb7w368j8A+ql/lNOl6li
2250rd1F1EqWupKD6RrcTL8sspEU55RGaretlE6zIqCcGI/BdTVQ03qRoQKBgHDC3zWf
2251d7XD9HGjQGdfbIe4jQjIGxzmd/wjik4q+NZ5IkukVwWa9P/zZ3DHF8Ad05dT1hEH
22522FwaAdGWpyyljq9VSiOuG1KXAXHgsZSuE4ISf9P1KYzvaiJFzaPfvOEWs79E9MfU
22539A+6dJzG2X1SpjWMr26iSTlrv3QkmFUqzAfJAoGASBkn4wls+oC5rv/Mch43pBv5
2254UmKru4ltnEHJZdbSi2DJ+AnDLD222JCasb1VT1tm2XgW6DBqrdVRPPP6GOlB0MHU
2255+3ULtZxAczt7I+ST2bo0/DV2Hse89Cm63w4wLOiVZs7+1wrAzJZLokWF7Q5gesra
2256u19txmtkiMEH+aNmekk=
2257-----END PRIVATE KEY-----"#;
2258
2259        #[async_timed_test(timeout_secs = 30)]
2260        async fn test_tls_basic() {
2261            // Ensure ring is installed as the default crypto provider
2262            // (no-op if already installed, e.g. under Buck with native-tls).
2263            let _ = rustls::crypto::ring::default_provider().install_default();
2264
2265            // Set up TLS config using the standard override pattern
2266            let config = hyperactor_config::global::lock();
2267            let _guard_cert =
2268                config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2269            let _guard_key =
2270                config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2271            let _guard_ca =
2272                config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2273
2274            // Create a TLS server bound to localhost with dynamic port
2275            let addr = TlsAddr::new("localhost", 0);
2276
2277            let (local_addr, mut rx) =
2278                server::serve::<u64>(ChannelAddr::Tls(addr), None).expect("failed to serve");
2279
2280            // Dial the server
2281            let tx: ChannelTx<u64> = super::spawn(
2282                link(
2283                    match &local_addr {
2284                        ChannelAddr::Tls(addr) => addr.clone(),
2285                        _ => panic!("unexpected address type"),
2286                    },
2287                    SessionId::random(),
2288                    0,
2289                    super::ProtocolKind::Simplex,
2290                )
2291                .expect("failed to create link"),
2292            );
2293
2294            // Send a message
2295            tx.post(42u64);
2296
2297            // Receive the message
2298            let received = rx.recv().await.expect("failed to receive");
2299            assert_eq!(received, 42u64);
2300        }
2301
2302        #[async_timed_test(timeout_secs = 30)]
2303        async fn test_quic_basic() {
2304            let _ = rustls::crypto::ring::default_provider().install_default();
2305
2306            let config = hyperactor_config::global::lock();
2307            let _guard_cert =
2308                config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2309            let _guard_key =
2310                config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2311            let _guard_ca =
2312                config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2313
2314            let (addr, mut rx) =
2315                serve::<u64>(ChannelAddr::Quic(TlsAddr::new("localhost", 0))).unwrap();
2316            let tx = dial::<u64>(addr).unwrap();
2317
2318            tx.post(42u64);
2319
2320            let received = rx.recv().await.expect("failed to receive");
2321            assert_eq!(received, 42u64);
2322        }
2323
2324        #[async_timed_test(timeout_secs = 30)]
2325        async fn test_quic_unordered_basic() {
2326            let _ = rustls::crypto::ring::default_provider().install_default();
2327
2328            let config = hyperactor_config::global::lock();
2329            let _guard_cert =
2330                config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2331            let _guard_key =
2332                config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2333            let _guard_ca =
2334                config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2335
2336            let (addr, mut rx) =
2337                unordered::serve::<u64>(ChannelAddr::Quic(TlsAddr::new("localhost", 0))).unwrap();
2338            let tx = unordered::dial::<u64>(addr, 2).unwrap();
2339
2340            for i in 0..8 {
2341                tx.post(i);
2342            }
2343
2344            let mut received = Vec::new();
2345            for _ in 0..8 {
2346                received.push(rx.recv().await.expect("failed to receive"));
2347            }
2348            received.sort();
2349            assert_eq!(received, (0..8).collect::<Vec<_>>());
2350        }
2351
2352        #[async_timed_test(timeout_secs = 30)]
2353        async fn test_tls_multiple_messages() {
2354            let _ = rustls::crypto::ring::default_provider().install_default();
2355
2356            // Set up TLS config using the standard override pattern
2357            let config = hyperactor_config::global::lock();
2358            let _guard_cert =
2359                config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2360            let _guard_key =
2361                config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2362            let _guard_ca =
2363                config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2364
2365            let addr = TlsAddr::new("localhost", 0);
2366
2367            let (local_addr, mut rx) =
2368                server::serve::<String>(ChannelAddr::Tls(addr), None).expect("failed to serve");
2369            let tx: ChannelTx<String> = super::spawn(
2370                link(
2371                    match &local_addr {
2372                        ChannelAddr::Tls(addr) => addr.clone(),
2373                        _ => panic!("unexpected address type"),
2374                    },
2375                    SessionId::random(),
2376                    0,
2377                    super::ProtocolKind::Simplex,
2378                )
2379                .expect("failed to create link"),
2380            );
2381
2382            // Send multiple messages
2383            for i in 0..10 {
2384                tx.post(format!("message {}", i));
2385            }
2386
2387            // Receive all messages
2388            for i in 0..10 {
2389                let received = rx.recv().await.expect("failed to receive");
2390                assert_eq!(received, format!("message {}", i));
2391            }
2392        }
2393
2394        #[test]
2395        fn test_tls_parse_hostname_port() {
2396            let addr = parse("localhost:8080").expect("failed to parse");
2397            assert!(matches!(
2398                addr,
2399                ChannelAddr::Tls(TlsAddr { hostname, port })
2400                    if hostname == "localhost" && port == 8080
2401            ));
2402        }
2403
2404        #[test]
2405        fn test_tls_parse_socket_addr() {
2406            let addr = parse("127.0.0.1:8080").expect("failed to parse");
2407            assert!(matches!(
2408                addr,
2409                ChannelAddr::Tls(TlsAddr { hostname, port })
2410                    if hostname == "127.0.0.1" && port == 8080
2411            ));
2412        }
2413
2414        #[test]
2415        fn test_tls_certs_parsing() {
2416            // Verify that the test certificates can be parsed correctly
2417            let cert_pem = Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec());
2418            let key_pem = Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec());
2419            let ca_pem = Pem::Value(TEST_CA_CERT.as_bytes().to_vec());
2420
2421            let certs = super::load_certs(&cert_pem).expect("failed to load certs");
2422            assert!(!certs.is_empty(), "expected at least one certificate");
2423
2424            let _key = super::load_key(&key_pem).expect("failed to load key");
2425
2426            let root_store = super::build_root_store(&ca_pem).expect("failed to build root store");
2427            assert!(!root_store.is_empty(), "expected at least one CA cert");
2428        }
2429
2430        #[test]
2431        fn test_tls_acceptor_creation() {
2432            // Ensure ring is installed as the default crypto provider
2433            // (no-op if already installed, e.g. under Buck with native-tls).
2434            let _ = rustls::crypto::ring::default_provider().install_default();
2435
2436            // Set up TLS config using the standard override pattern
2437            let config = hyperactor_config::global::lock();
2438            let _guard_cert =
2439                config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2440            let _guard_key =
2441                config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2442            let _guard_ca =
2443                config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2444
2445            // Verify that we can create a TLS acceptor
2446            let _acceptor = super::tls_acceptor().expect("failed to create TLS acceptor");
2447        }
2448
2449        #[test]
2450        fn test_tls_connector_creation() {
2451            // Ensure ring is installed as the default crypto provider
2452            // (no-op if already installed, e.g. under Buck with native-tls).
2453            let _ = rustls::crypto::ring::default_provider().install_default();
2454
2455            // Set up TLS config using the standard override pattern
2456            let config = hyperactor_config::global::lock();
2457            let _guard_cert =
2458                config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2459            let _guard_key =
2460                config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2461            let _guard_ca =
2462                config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2463
2464            // Verify that we can create a TLS connector
2465            let _connector = super::tls_connector().expect("failed to create TLS connector");
2466        }
2467    }
2468}
2469
2470/// Build the OSS PemBundle from hyperactor_config attributes.
2471fn oss_pem_bundle() -> crate::config::PemBundle {
2472    crate::config::PemBundle {
2473        ca: hyperactor_config::global::get_cloned(crate::config::TLS_CA),
2474        cert: hyperactor_config::global::get_cloned(crate::config::TLS_CERT),
2475        key: hyperactor_config::global::get_cloned(crate::config::TLS_KEY),
2476    }
2477}
2478
2479/// Try to find a usable TLS [`PemBundle`](crate::config::PemBundle)
2480/// by probing the same sources as [`try_tls_acceptor_with_pem_bundle`] /
2481/// [`try_tls_connector`].
2482///
2483/// Returns the first bundle whose CA certificate is readable.
2484/// Only CA readability is checked — cert and key are returned as-is
2485/// and may not be valid. Callers that cannot use `tokio_rustls` types
2486/// directly (e.g. reqwest) can read the raw PEM bytes via
2487/// [`Pem::reader`](crate::config::Pem::reader).
2488pub fn try_tls_pem_bundle() -> Option<crate::config::PemBundle> {
2489    let oss_bundle = oss_pem_bundle();
2490    if oss_bundle.ca.reader().is_ok() {
2491        return Some(oss_bundle);
2492    }
2493    tracing::debug!("OSS TLS bundle: CA not readable, trying Meta paths");
2494
2495    let meta_bundle = meta::get_server_pem_bundle();
2496    if meta_bundle.ca.reader().is_ok() {
2497        return Some(meta_bundle);
2498    }
2499    tracing::debug!("Meta TLS bundle: CA not readable, no TLS available");
2500
2501    None
2502}
2503
2504/// Try to build a [`TlsAcceptor`](tokio_rustls::TlsAcceptor) and retain the
2505/// exact [`PemBundle`](crate::config::PemBundle) selected for it.
2506///
2507/// Detection order:
2508/// 1. **OSS / explicit config** — `HYPERACTOR_TLS_CERT`,
2509///    `HYPERACTOR_TLS_KEY`, and `HYPERACTOR_TLS_CA` (read via
2510///    [`hyperactor_config`]).
2511/// 2. **Meta default paths** —
2512///    `/var/facebook/x509_identities/server.pem` and
2513///    `/var/facebook/rootcanal/ca.pem`. These are present on
2514///    devservers and in MAST / Tupperware containers.
2515/// 3. **None** — no usable certificates found; caller should fall
2516///    back to plain HTTP.
2517///
2518/// When `enforce_client_tls` is `true`, the returned acceptor
2519/// requires clients to present a valid certificate signed by the
2520/// configured CA (mutual TLS via `WebPkiClientVerifier`). When
2521/// `false`, the acceptor authenticates itself but does not demand
2522/// client certificates.
2523pub fn try_tls_acceptor_with_pem_bundle(
2524    enforce_client_tls: bool,
2525) -> Option<(tokio_rustls::TlsAcceptor, crate::config::PemBundle)> {
2526    let oss_bundle = oss_pem_bundle();
2527    if let Ok(acceptor) = tls::tls_acceptor_from_bundle(&oss_bundle, enforce_client_tls) {
2528        return Some((acceptor, oss_bundle));
2529    }
2530    tracing::debug!("OSS TLS acceptor failed, trying Meta paths");
2531
2532    let meta_bundle = meta::get_server_pem_bundle();
2533    if let Ok(acceptor) = tls::tls_acceptor_from_bundle(&meta_bundle, enforce_client_tls) {
2534        return Some((acceptor, meta_bundle));
2535    }
2536    tracing::debug!("Meta TLS acceptor failed, no TLS available");
2537
2538    None
2539}
2540
2541/// Try to build a [`TlsConnector`](tokio_rustls::TlsConnector) for an
2542/// HTTP client that needs to connect to a TLS-enabled server.
2543///
2544/// Detection mirrors [`try_tls_acceptor_with_pem_bundle`]:
2545/// 1. **OSS** — `HYPERACTOR_TLS_CA` (and optionally
2546///    `HYPERACTOR_TLS_CERT` + `HYPERACTOR_TLS_KEY` for mutual TLS).
2547/// 2. **Meta** — root CA at `/var/facebook/rootcanal/ca.pem`,
2548///    optional client certs from `THRIFT_TLS_CL_CERT_PATH` /
2549///    `THRIFT_TLS_CL_KEY_PATH`.
2550/// 3. **None** — no usable CA found; caller should fall back to plain
2551///    HTTP.
2552pub fn try_tls_connector() -> Option<tokio_rustls::TlsConnector> {
2553    let oss_bundle = oss_pem_bundle();
2554    if let Ok(connector) = tls::tls_connector_from_bundle(&oss_bundle) {
2555        return Some(connector);
2556    }
2557    if let Ok(config) = tls::client_config_from_ca(&oss_bundle.ca) {
2558        return Some(tokio_rustls::TlsConnector::from(Arc::new(config)));
2559    }
2560    tracing::debug!("OSS TLS connector failed, trying Meta paths");
2561
2562    if let Ok(connector) = meta::try_tls_connector() {
2563        return Some(connector);
2564    }
2565    tracing::debug!("Meta TLS connector failed, no TLS available");
2566
2567    None
2568}
2569
2570#[cfg(test)]
2571mod tests {
2572
2573    #![expect(
2574        clippy::await_holding_invalid_type,
2575        reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
2576    )]
2577
2578    use std::assert_matches;
2579    use std::collections::VecDeque;
2580    use std::marker::PhantomData;
2581    use std::sync::Arc;
2582    use std::sync::RwLock;
2583    use std::sync::atomic::AtomicBool;
2584    use std::sync::atomic::AtomicU64;
2585    use std::sync::atomic::Ordering;
2586    use std::time::Duration;
2587    #[cfg(target_os = "linux")] // uses abstract names
2588    use std::time::UNIX_EPOCH;
2589
2590    #[cfg(target_os = "linux")] // uses abstract names
2591    use anyhow::Result;
2592    use bytes::Bytes;
2593    use rand::RngExt as _;
2594    use rand::SeedableRng as _;
2595    use rand::distr::Alphanumeric;
2596    use rand::rngs::SysRng;
2597    use timed_test::async_timed_test;
2598    use tokio::io::AsyncRead;
2599    use tokio::io::AsyncWrite;
2600    use tokio::io::DuplexStream;
2601    use tokio::io::ReadHalf;
2602    use tokio::io::WriteHalf;
2603    use tokio::task::JoinHandle;
2604    use tokio::time::Instant;
2605    use tokio_util::sync::CancellationToken;
2606
2607    use super::server;
2608    use super::*;
2609    use crate::channel;
2610    use crate::channel::ChannelRx;
2611    use crate::channel::ChannelTx;
2612    use crate::channel::net::framed::FrameReader;
2613    use crate::channel::net::framed::FrameWrite;
2614    use crate::channel::net::server::AcceptorLink;
2615    use crate::config;
2616    use crate::metrics;
2617    use crate::sync::mvar::MVar;
2618
2619    /// Like the `logs_assert` injected by `#[traced_test]`, but without scope
2620    /// filtering. Use when asserting on events emitted outside the test's span
2621    /// (e.g. from spawned tasks or panic hooks).
2622    fn logs_assert_unscoped(f: impl Fn(&[&str]) -> Result<(), String>) {
2623        let buf = tracing_test::internal::global_buf().lock().unwrap();
2624        let logs_str = std::str::from_utf8(&buf).expect("Logs contain invalid UTF8");
2625        let lines: Vec<&str> = logs_str.lines().collect();
2626        match f(&lines) {
2627            Ok(()) => {}
2628            Err(msg) => panic!("{}", msg),
2629        }
2630    }
2631
2632    #[cfg(target_os = "linux")] // uses abstract names
2633    #[tracing_test::traced_test]
2634    #[tokio::test]
2635    async fn test_unix_basic() -> Result<()> {
2636        let timestamp = std::time::SystemTime::now()
2637            .duration_since(UNIX_EPOCH)
2638            .unwrap()
2639            .as_nanos();
2640        let unique_address = format!("test_unix_basic_{}", timestamp);
2641
2642        let (addr, mut rx) = server::serve::<u64>(
2643            ChannelAddr::Unix(unix::SocketAddr::from_abstract_name(&unique_address)?),
2644            None,
2645        )
2646        .unwrap();
2647
2648        // It is important to keep Tx alive until all expected messages are
2649        // received. Otherwise, the channel would be closed when Tx is dropped.
2650        // Although the messages are sent to the server's buffer before the
2651        // channel was closed, ChannelRx could still error out before taking them
2652        // out of the buffer because ChannelRx could not ack through the closed
2653        // channel.
2654        {
2655            let tx: ChannelTx<u64> = channel::dial::<u64>(addr.clone()).unwrap();
2656            tx.post(123);
2657            assert_eq!(rx.recv().await.unwrap(), 123);
2658        }
2659
2660        {
2661            let tx = channel::dial::<u64>(addr.clone()).unwrap();
2662            tx.post(321);
2663            tx.post(111);
2664            tx.post(444);
2665
2666            assert_eq!(rx.recv().await.unwrap(), 321);
2667            assert_eq!(rx.recv().await.unwrap(), 111);
2668            assert_eq!(rx.recv().await.unwrap(), 444);
2669        }
2670
2671        {
2672            let tx = channel::dial::<u64>(addr).unwrap();
2673            drop(rx);
2674
2675            assert_matches!(
2676                tx.try_post(123).await,
2677                Err(SendError {
2678                    error: ChannelError::Closed,
2679                    message: 123,
2680                    ..
2681                })
2682            );
2683        }
2684
2685        Ok(())
2686    }
2687
2688    #[cfg(target_os = "linux")] // uses abstract names
2689    #[tracing_test::traced_test]
2690    #[tokio::test]
2691    async fn test_unix_basic_client_before_server() -> Result<()> {
2692        // We run this test on Unix because we can pick our own port names more easily.
2693        let timestamp = std::time::SystemTime::now()
2694            .duration_since(UNIX_EPOCH)
2695            .unwrap()
2696            .as_nanos();
2697        let socket_addr =
2698            unix::SocketAddr::from_abstract_name(&format!("test_unix_basic_{}", timestamp))
2699                .unwrap();
2700
2701        // Dial the channel before we actually serve it.
2702        let addr = ChannelAddr::Unix(socket_addr.clone());
2703        let tx = crate::channel::dial::<u64>(addr.clone()).unwrap();
2704        tx.post(123);
2705
2706        let (_, mut rx) = server::serve::<u64>(ChannelAddr::Unix(socket_addr), None).unwrap();
2707        assert_eq!(rx.recv().await.unwrap(), 123);
2708
2709        tx.post(321);
2710        tx.post(111);
2711        tx.post(444);
2712
2713        assert_eq!(rx.recv().await.unwrap(), 321);
2714        assert_eq!(rx.recv().await.unwrap(), 111);
2715        assert_eq!(rx.recv().await.unwrap(), 444);
2716
2717        Ok(())
2718    }
2719
2720    // The channel hides the socket fd behind trait objects, so exercise the
2721    // tuning helper directly against a real connected stream, mirroring what the
2722    // connect/accept paths do. `reno` is universally available and differs from
2723    // the `cubic` default, so reading it back proves the setsockopt took effect.
2724    #[cfg(target_os = "linux")]
2725    #[tokio::test]
2726    async fn apply_tcp_congestion_sets_congestion_control() {
2727        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2728        let addr = listener.local_addr().unwrap();
2729        let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2730        let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2731        let _server = accept.await.unwrap();
2732
2733        apply_tcp_congestion(&stream, Some("reno")).unwrap();
2734
2735        let cc = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2736        assert!(
2737            cc.starts_with(b"reno"),
2738            "expected reno congestion control, got {:?}",
2739            String::from_utf8_lossy(&cc)
2740        );
2741    }
2742
2743    // The point of the best-effort change: an algorithm this host will not accept must
2744    // leave the socket on its default and let the connection live. Before, it failed the
2745    // connect -- turning a tuning preference into an outage on any host that happens not
2746    // to permit the controller, which a caller cannot check for in advance.
2747    #[cfg(target_os = "linux")]
2748    #[tracing_test::traced_test]
2749    #[tokio::test]
2750    async fn set_tcp_congestion_keeps_the_connection_when_the_host_refuses() {
2751        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2752        let addr = listener.local_addr().unwrap();
2753        let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2754        let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2755        let _server = accept.await.unwrap();
2756        let before = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2757
2758        let config = hyperactor_config::global::lock();
2759        let _guard = config.override_key(
2760            config::CHANNEL_TCP_CONGESTION,
2761            "not_a_real_cc_algo".to_string(),
2762        );
2763
2764        set_tcp_congestion(&stream);
2765
2766        let after = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2767        assert_eq!(before, after, "the socket should keep the host default");
2768        // Untuned-but-believed-tuned is its own bug, so the fallback has to be visible --
2769        // and has to name the algorithm, since the bare errno says "No such file".
2770        assert!(logs_contain("not_a_real_cc_algo"));
2771    }
2772
2773    // The case almost every channel takes: nothing configured, nothing touched.
2774    #[cfg(target_os = "linux")]
2775    #[tokio::test]
2776    async fn set_tcp_congestion_is_a_noop_when_unconfigured() {
2777        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2778        let addr = listener.local_addr().unwrap();
2779        let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2780        let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2781        let _server = accept.await.unwrap();
2782        let before = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2783
2784        let config = hyperactor_config::global::lock();
2785        let _guard = config.override_key(config::CHANNEL_TCP_CONGESTION, String::new());
2786
2787        set_tcp_congestion(&stream);
2788
2789        let after = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2790        assert_eq!(before, after);
2791    }
2792
2793    // An algorithm the host does not provide must fail the caller rather than
2794    // being ignored: a path the operator believes is tuned would otherwise keep
2795    // running on the host default. `None` (the empty-config case) stays a no-op.
2796    #[cfg(target_os = "linux")]
2797    #[tokio::test]
2798    async fn apply_tcp_congestion_rejects_unknown_algorithm() {
2799        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2800        let addr = listener.local_addr().unwrap();
2801        let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2802        let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2803        let _server = accept.await.unwrap();
2804
2805        let err = apply_tcp_congestion(&stream, Some("not_a_real_cc_algo"))
2806            .expect_err("an unsupported congestion-control algorithm should be an error");
2807        // The bare errno is ENOENT ("No such file or directory"), so the message has
2808        // to name the algorithm to be actionable.
2809        let msg = err.to_string();
2810        assert!(
2811            msg.contains("not_a_real_cc_algo"),
2812            "error should name the rejected algorithm, got {msg:?}"
2813        );
2814
2815        apply_tcp_congestion(&stream, None).expect("None should be a no-op, not an error");
2816    }
2817
2818    #[tracing_test::traced_test]
2819    #[async_timed_test(timeout_secs = 60)]
2820    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" })
2821    #[cfg_attr(not(fbcode_build), ignore)]
2822    async fn test_tcp_basic() {
2823        let (addr, mut rx) =
2824            server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2825        {
2826            let tx = channel::dial::<u64>(addr.clone()).unwrap();
2827            tx.post(123);
2828            assert_eq!(rx.recv().await.unwrap(), 123);
2829        }
2830
2831        {
2832            let tx = channel::dial::<u64>(addr.clone()).unwrap();
2833            tx.post(321);
2834            tx.post(111);
2835            tx.post(444);
2836
2837            assert_eq!(rx.recv().await.unwrap(), 321);
2838            assert_eq!(rx.recv().await.unwrap(), 111);
2839            assert_eq!(rx.recv().await.unwrap(), 444);
2840        }
2841
2842        {
2843            let tx = channel::dial::<u64>(addr).unwrap();
2844            drop(rx);
2845
2846            assert_matches!(
2847                tx.try_post(123).await,
2848                Err(SendError {
2849                    error: ChannelError::Closed,
2850                    message: 123,
2851                    ..
2852                })
2853            );
2854        }
2855    }
2856
2857    #[async_timed_test(timeout_secs = 20)]
2858    #[cfg_attr(not(fbcode_build), ignore)]
2859    async fn test_tcp_unreachable_peer_surfaces_closed() {
2860        // With a bounded reconnect timeout, dialing an unreachable peer must
2861        // surface as TxStatus::Closed — not loop forever. The
2862        // `async_timed_test` timeout above is the only failure backstop; the
2863        // body itself is purely event-driven on the status watch.
2864        let config = hyperactor_config::global::lock();
2865        // `connect_by` retries `link.next()` until MESSAGE_DELIVERY_TIMEOUT
2866        // when an outbound message is pending. Bound both so the test surfaces
2867        // Closed within a few seconds rather than the 30s default.
2868        let _g1 = config.override_key(config::CHANNEL_RECONNECT_TIMEOUT, Duration::from_secs(1));
2869        let _g2 = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(3));
2870
2871        // Bind a listener to grab a free local port, then drop it so connects
2872        // get ECONNREFUSED.
2873        let (addr, rx) =
2874            server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2875        drop(rx);
2876
2877        let tx = channel::dial::<u64>(addr.clone()).unwrap();
2878        tx.post(123); // primes the send loop so the connect path runs
2879
2880        let mut status = tx.status().clone();
2881        while !status.borrow_and_update().is_closed() {
2882            status.changed().await.unwrap();
2883        }
2884    }
2885
2886    // The message size is limited by CODEC_MAX_FRAME_LENGTH.
2887    //
2888    // Sends a payload of `default_size_in_bytes` (100 MiB) over a TCP
2889    // loopback. Real-time wall clock on a loaded build host can take
2890    // several seconds; the 30s timeout is comfortable headroom while
2891    // still surfacing genuine hangs.
2892    #[async_timed_test(timeout_secs = 30)]
2893    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" })
2894    #[cfg_attr(not(fbcode_build), ignore)]
2895    async fn test_tcp_message_size() {
2896        let default_size_in_bytes = 100 * 1024 * 1024;
2897        // Use temporary config for this test
2898        let config = hyperactor_config::global::lock();
2899        let _guard1 = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(1));
2900        let _guard2 = config.override_key(config::CODEC_MAX_FRAME_LENGTH, default_size_in_bytes);
2901
2902        let (addr, mut rx) =
2903            server::serve::<String>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2904
2905        let tx = channel::dial::<String>(addr.clone()).unwrap();
2906        // Default size is okay
2907        {
2908            // Leave some headroom because Tx will wrap the payload in Frame::Message.
2909            let message = "a".repeat(default_size_in_bytes - 1024);
2910            tx.post(message.clone());
2911            assert_eq!(rx.recv().await.unwrap(), message);
2912        }
2913        // Bigger than the default size will fail.
2914        {
2915            let message = "a".repeat(default_size_in_bytes + 1024);
2916            let returned = tx.try_post(message.clone()).await.unwrap_err();
2917            assert_eq!(message, returned.message);
2918        }
2919    }
2920
2921    #[async_timed_test(timeout_secs = 30)]
2922    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" })
2923    #[cfg_attr(not(fbcode_build), ignore)]
2924    async fn test_ack_flush() {
2925        let config = hyperactor_config::global::lock();
2926        // Set a large value to effectively prevent acks from being sent except
2927        // during shutdown flush.
2928        let _guard_message_ack =
2929            config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 100000000);
2930        let _guard_delivery_timeout =
2931            config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(5));
2932
2933        let (addr, mut net_rx) =
2934            server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2935        let net_tx = channel::dial::<u64>(addr.clone()).unwrap();
2936        let receipt = net_tx.try_post(1);
2937        assert_eq!(net_rx.recv().await.unwrap(), 1);
2938        drop(net_rx);
2939        assert!(receipt.await.is_ok());
2940    }
2941
2942    #[async_timed_test(timeout_secs = 60)]
2943    // TODO: OSS: failed to retrieve ipv6 address
2944    #[cfg_attr(not(fbcode_build), ignore)]
2945    async fn test_meta_tls_basic() {
2946        hyperactor_telemetry::initialize_logging_for_test();
2947
2948        let addr = ChannelAddr::any(ChannelTransport::MetaTls(TlsMode::IpV6));
2949        let meta_addr = match addr {
2950            ChannelAddr::MetaTls(meta_addr) => meta_addr,
2951            _ => panic!("expected MetaTls address"),
2952        };
2953        let (local_addr, mut rx) =
2954            server::serve::<u64>(ChannelAddr::MetaTls(meta_addr), None).unwrap();
2955        {
2956            let tx = channel::dial::<u64>(local_addr.clone()).unwrap();
2957            tx.post(123);
2958        }
2959        assert_eq!(rx.recv().await.unwrap(), 123);
2960
2961        {
2962            let tx = channel::dial::<u64>(local_addr.clone()).unwrap();
2963            tx.post(321);
2964            tx.post(111);
2965            tx.post(444);
2966            assert_eq!(rx.recv().await.unwrap(), 321);
2967            assert_eq!(rx.recv().await.unwrap(), 111);
2968            assert_eq!(rx.recv().await.unwrap(), 444);
2969        }
2970
2971        {
2972            let tx = channel::dial::<u64>(local_addr).unwrap();
2973            drop(rx);
2974
2975            assert_matches!(
2976                tx.try_post(123).await,
2977                Err(SendError {
2978                    error: ChannelError::Closed,
2979                    message: 123,
2980                    ..
2981                })
2982            );
2983        }
2984    }
2985
2986    #[derive(Clone, Debug, Default)]
2987    struct NetworkFlakiness {
2988        // A tuple of:
2989        //   1. the probability of a network failure when sending a message.
2990        //   2. the max number of disconnections allowed.
2991        //   3. the minimum duration between disconnections.
2992        //
2993        //   2 and 3 are useful to prevent frequent disconnections leading to
2994        //   unacked messages being sent repeatedly.
2995        disconnect_params: Option<(f64, u64, Duration)>,
2996        // The max possible latency when sending a message. The actual latency
2997        // is randomly generated between 0 and max_latency.
2998        latency_range: Option<(Duration, Duration)>,
2999    }
3000
3001    impl NetworkFlakiness {
3002        // Calculate whether to disconnect
3003        async fn should_disconnect(
3004            &self,
3005            rng: &mut impl rand::Rng,
3006            disconnected_count: u64,
3007            prev_disconnected_at: &RwLock<Instant>,
3008        ) -> bool {
3009            let Some((prob, max_disconnects, duration)) = &self.disconnect_params else {
3010                return false;
3011            };
3012
3013            let disconnected_at = prev_disconnected_at.read().unwrap();
3014            if disconnected_at.elapsed() > *duration && disconnected_count < *max_disconnects {
3015                rng.random_bool(*prob)
3016            } else {
3017                false
3018            }
3019        }
3020    }
3021
3022    struct MockLink<M> {
3023        buffer_size: usize,
3024        session_id: SessionId,
3025        receiver_storage: Arc<MVar<DuplexStream>>,
3026        // If true, `next()` on this link will always return an error.
3027        fail_connects: Arc<AtomicBool>,
3028        // Used to break the existing connection, if there is one. It still
3029        // allows reconnect.
3030        disconnect_signal: watch::Sender<()>,
3031        network_flakiness: NetworkFlakiness,
3032        disconnected_count: Arc<AtomicU64>,
3033        prev_disconnected_at: Arc<RwLock<Instant>>,
3034        // If set, print logs every `debug_log_sampling_rate` messages. This
3035        // is normally set only when debugging a test failure.
3036        debug_log_sampling_rate: Option<u64>,
3037        _message_type: PhantomData<M>,
3038    }
3039
3040    impl<M> fmt::Debug for MockLink<M> {
3041        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3042            f.debug_struct("MockLink")
3043                .field("buffer_size", &self.buffer_size)
3044                .field("receiver_storage", &"<MVar<DuplexStream>>")
3045                .field("fail_connects", &self.fail_connects)
3046                .field("disconnect_signal", &"<watch::Sender>")
3047                .field("network_flakiness", &self.network_flakiness)
3048                .field("disconnected_count", &self.disconnected_count)
3049                .field("prev_disconnected_at", &"<RwLock<Instant>>")
3050                .field("debug_log_sampling_rate", &self.debug_log_sampling_rate)
3051                .finish()
3052        }
3053    }
3054
3055    impl<M: RemoteMessage> MockLink<M> {
3056        fn new() -> Self {
3057            let (sender, _) = watch::channel(());
3058            Self {
3059                buffer_size: 64,
3060                session_id: SessionId::random(),
3061                receiver_storage: Arc::new(MVar::empty()),
3062                fail_connects: Arc::new(AtomicBool::new(false)),
3063                disconnect_signal: sender,
3064                network_flakiness: NetworkFlakiness::default(),
3065                disconnected_count: Arc::new(AtomicU64::new(0)),
3066                prev_disconnected_at: Arc::new(RwLock::new(tokio::time::Instant::now())),
3067                debug_log_sampling_rate: None,
3068                _message_type: PhantomData,
3069            }
3070        }
3071
3072        // If `fail_connects` is true, `next()` on this link will
3073        // always return an error.
3074        fn fail_connects() -> Self {
3075            Self {
3076                fail_connects: Arc::new(AtomicBool::new(true)),
3077                ..Self::new()
3078            }
3079        }
3080
3081        fn with_network_flakiness(network_flakiness: NetworkFlakiness) -> Self {
3082            if let Some((min, max)) = network_flakiness.latency_range {
3083                assert!(min < max);
3084            }
3085
3086            Self {
3087                network_flakiness,
3088                ..Self::new()
3089            }
3090        }
3091
3092        fn receiver_storage(&self) -> Arc<MVar<DuplexStream>> {
3093            self.receiver_storage.clone()
3094        }
3095
3096        fn disconnected_count(&self) -> Arc<AtomicU64> {
3097            self.disconnected_count.clone()
3098        }
3099
3100        fn disconnect_signal(&self) -> &watch::Sender<()> {
3101            &self.disconnect_signal
3102        }
3103
3104        fn fail_connects_switch(&self) -> Arc<AtomicBool> {
3105            self.fail_connects.clone()
3106        }
3107
3108        fn set_buffer_size(&mut self, size: usize) {
3109            self.buffer_size = size;
3110        }
3111
3112        fn set_sampling_rate(&mut self, sampling_rate: u64) {
3113            self.debug_log_sampling_rate = Some(sampling_rate);
3114        }
3115    }
3116
3117    #[async_trait]
3118    impl<M: RemoteMessage> Link for MockLink<M> {
3119        type Stream = DuplexStream;
3120
3121        fn dest(&self) -> ChannelAddr {
3122            ChannelAddr::Local(u64::MAX)
3123        }
3124
3125        fn link_id(&self) -> SessionId {
3126            self.session_id
3127        }
3128
3129        async fn next(&mut self) -> Result<Self::Stream, ClientError> {
3130            let session_id = self.session_id;
3131            tracing::debug!("MockLink starts to connect.");
3132            if self.fail_connects.load(Ordering::Acquire) {
3133                return Err(ClientError::Connect(
3134                    self.dest(),
3135                    std::io::Error::other("intentional error"),
3136                    "expected failure injected by the mock".to_string(),
3137                ));
3138            }
3139
3140            // Add relays between server and client streams. The
3141            // relays provides the place to inject network flakiness.
3142            // The message flow looks like:
3143            //
3144            // server <-> server relay <-> injection logic <-> client relay <-> client
3145            async fn relay_message<M: RemoteMessage>(
3146                mut disconnect_signal: watch::Receiver<()>,
3147                network_flakiness: NetworkFlakiness,
3148                disconnected_count: Arc<AtomicU64>,
3149                prev_disconnected_at: Arc<RwLock<Instant>>,
3150                mut reader: FrameReader<ReadHalf<DuplexStream>>,
3151                mut writer: WriteHalf<DuplexStream>,
3152                // Used by client and server tokio tasks to coordinate
3153                // stopping together.
3154                task_coordination_token: CancellationToken,
3155                debug_log_sampling_rate: Option<u64>,
3156                // Whether the relayed message is from client to
3157                // server.
3158                is_from_client: bool,
3159            ) {
3160                // Used to simulate latency. Briefly, messages are
3161                // buffered in the queue and wait for the expected
3162                // latency elapse.
3163                async fn wait_for_latency_elapse(
3164                    queue: &VecDeque<(Bytes, Instant)>,
3165                    network_flakiness: &NetworkFlakiness,
3166                    rng: &mut impl rand::Rng,
3167                ) {
3168                    if let Some((min, max)) = network_flakiness.latency_range {
3169                        let diff = max.abs_diff(min);
3170                        let factor = rng.random_range(0.0..=1.0);
3171                        let latency = min + diff.mul_f64(factor);
3172                        tokio::time::sleep_until(queue.front().unwrap().1 + latency).await;
3173                    }
3174                }
3175
3176                let mut rng = rand::rngs::SmallRng::try_from_rng(&mut SysRng).unwrap();
3177                let mut queue: VecDeque<(Bytes, Instant)> = VecDeque::new();
3178                let mut send_count = 0u64;
3179
3180                loop {
3181                    tokio::select! {
3182                        read_res = reader.next() => {
3183                            match read_res {
3184                                Ok(Some((_, data))) => {
3185                                    queue.push_back((data, tokio::time::Instant::now()));
3186                                }
3187                                Ok(None) | Err(_) => {
3188                                        tracing::debug!("The upstream is closed or dropped. MockLink disconnects");
3189                                        break;
3190                                }
3191                            }
3192                        }
3193                        _ = wait_for_latency_elapse(&queue, &network_flakiness, &mut rng), if !queue.is_empty() => {
3194                            let count = disconnected_count.load(Ordering::Relaxed);
3195                            if network_flakiness.should_disconnect(&mut rng, count, &prev_disconnected_at).await {
3196                                tracing::debug!("MockLink disconnects");
3197                                disconnected_count.fetch_add(1, Ordering::Relaxed);
3198
3199                                metrics::CHANNEL_RECONNECTIONS.add(
3200                                    1,
3201                                    hyperactor_telemetry::kv_pairs!(
3202                                        "transport" => "mock",
3203                                        "reason" => "network_flakiness",
3204                                    ),
3205                                );
3206
3207                                let mut w = prev_disconnected_at.write().unwrap();
3208                                *w = tokio::time::Instant::now();
3209                                break;
3210                            }
3211                            let data = queue.pop_front().unwrap().0;
3212                            let is_sampled = debug_log_sampling_rate.is_some_and(|sample_rate| send_count % sample_rate == 1);
3213                            if is_sampled {
3214                                if is_from_client {
3215                                    if let Ok((Frame::Message(_seq, _msg), _)) = bincode::serde::decode_from_slice::<Frame<M>, _>(&data, bincode::config::legacy()) {
3216                                        tracing::debug!("MockLink relays a msg from client. msg type: {}", std::any::type_name::<M>());
3217                                    }
3218                                } else {
3219                                    let result = deserialize_response(data.clone());
3220                                    if let Ok(NetRxResponse::Ack(seq)) = result {
3221                                        tracing::debug!("MockLink relays an ack from server. seq: {}", seq);
3222                                    }
3223                                }
3224                            }
3225                            let mut fw  = FrameWrite::new(writer, data, hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH), 0).unwrap();
3226                            if fw.send().await.is_err() {
3227                                break;
3228                            }
3229                            writer = fw.complete();
3230                            send_count += 1;
3231                        }
3232                        _ = task_coordination_token.cancelled() => break,
3233
3234                        changed = disconnect_signal.changed() => {
3235                            tracing::debug!("MockLink disconnects per disconnect_signal {:?}", changed);
3236                            break;
3237                        }
3238                    }
3239                }
3240
3241                task_coordination_token.cancel();
3242            }
3243
3244            let (server, mut server_relay) = tokio::io::duplex(self.buffer_size);
3245            let (client, client_relay) = tokio::io::duplex(self.buffer_size);
3246
3247            // Write LinkInit on server_relay so it's readable from `server`.
3248            // This simulates the client sending LinkInit over the wire before
3249            // the frame-level relay begins.
3250            write_link_init(&mut server_relay, session_id, 0, ProtocolKind::Simplex)
3251                .await
3252                .map_err(|err| ClientError::Io(self.dest(), err))?;
3253
3254            let (server_r, server_writer) = tokio::io::split(server_relay);
3255            let (client_r, client_writer) = tokio::io::split(client_relay);
3256
3257            let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
3258            let server_reader = FrameReader::new(server_r, max_len);
3259            let client_reader = FrameReader::new(client_r, max_len);
3260
3261            let task_coordination_token = CancellationToken::new();
3262            let _server_relay_task_handle = tokio::spawn(relay_message::<M>(
3263                self.disconnect_signal.subscribe(),
3264                self.network_flakiness.clone(),
3265                self.disconnected_count.clone(),
3266                self.prev_disconnected_at.clone(),
3267                server_reader,
3268                client_writer,
3269                task_coordination_token.clone(),
3270                self.debug_log_sampling_rate,
3271                /*is_from_client*/ false,
3272            ));
3273            let _client_relay_task_handle = tokio::spawn(relay_message::<M>(
3274                self.disconnect_signal.subscribe(),
3275                self.network_flakiness.clone(),
3276                self.disconnected_count.clone(),
3277                self.prev_disconnected_at.clone(),
3278                client_reader,
3279                server_writer,
3280                task_coordination_token,
3281                self.debug_log_sampling_rate,
3282                /*is_from_client*/ true,
3283            ));
3284
3285            self.receiver_storage.put(server).await;
3286            Ok(client)
3287        }
3288    }
3289
3290    struct MockLinkListener {
3291        receiver_storage: Arc<MVar<DuplexStream>>,
3292        channel_addr: ChannelAddr,
3293    }
3294
3295    impl MockLinkListener {
3296        fn new(receiver_storage: Arc<MVar<DuplexStream>>, channel_addr: ChannelAddr) -> Self {
3297            Self {
3298                receiver_storage,
3299                channel_addr,
3300            }
3301        }
3302    }
3303
3304    impl fmt::Debug for MockLinkListener {
3305        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3306            f.debug_struct("MockLinkListener")
3307                .field("channel_addr", &self.channel_addr)
3308                .finish()
3309        }
3310    }
3311
3312    #[async_trait]
3313    impl super::Listener for MockLinkListener {
3314        type Stream = DuplexStream;
3315
3316        async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError> {
3317            let stream = self.receiver_storage.take().await;
3318            Ok((stream, self.channel_addr.clone()))
3319        }
3320    }
3321
3322    /// Create an AcceptorLink-based server test rig. Returns the
3323    /// session task handle, the channel sender for dispatching
3324    /// streams, the message receiver, and a cancellation token.
3325    fn serve_acceptor_test<M: RemoteMessage>(
3326        session_id: SessionId,
3327    ) -> (
3328        JoinHandle<()>,
3329        mpsc::UnboundedSender<DuplexStream>,
3330        mpsc::Receiver<M>,
3331        CancellationToken,
3332    ) {
3333        let (acceptor_tx, acceptor_rx) = mpsc::unbounded_channel::<DuplexStream>();
3334        let cancel_token = CancellationToken::new();
3335        let link = AcceptorLink {
3336            dest: ChannelAddr::Local(u64::MAX),
3337            session_id,
3338            stream: acceptor_rx,
3339            cancel: cancel_token.clone(),
3340        };
3341        let (tx, rx) = mpsc::channel::<M>(1024);
3342        let ct = cancel_token.clone();
3343        let handle = tokio::spawn(async move {
3344            let mut session = Session::new(link);
3345            let mut next = session::Next { seq: 0, ack: 0 };
3346
3347            loop {
3348                let connected = match session.connect().await {
3349                    Ok(s) => s,
3350                    Err(_) => break,
3351                };
3352
3353                let result = {
3354                    let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
3355                    tokio::select! {
3356                        r = session::recv_connected::<M, _, _>(&stream, &tx, &mut next) => r,
3357                        _ = ct.cancelled() => Err(session::RecvLoopError::Cancelled),
3358                    }
3359                };
3360
3361                // Flush remaining ack if behind.
3362                if next.ack < next.seq {
3363                    let ack = serialize_response(NetRxResponse::Ack(next.seq - 1)).unwrap();
3364                    let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
3365                    let mut completion = stream.write(ack);
3366                    match completion.drive().await {
3367                        Ok(()) => {
3368                            next.ack = next.seq;
3369                        }
3370                        Err(e) => {
3371                            tracing::debug!(
3372                                error = %e,
3373                                "failed to flush acks during cleanup"
3374                            );
3375                        }
3376                    }
3377                }
3378
3379                // Send reject or closed response if appropriate.
3380                let terminal_response = match &result {
3381                    Err(session::RecvLoopError::SequenceError(reason)) => {
3382                        Some(NetRxResponse::Reject(reason.clone()))
3383                    }
3384                    Err(session::RecvLoopError::Cancelled) => Some(NetRxResponse::Closed),
3385                    _ => None,
3386                };
3387                if let Some(rsp) = terminal_response {
3388                    let data = serialize_response(rsp).unwrap();
3389                    let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
3390                    let mut completion = stream.write(data);
3391                    let _ = completion.drive().await;
3392                }
3393
3394                let recoverable = matches!(&result, Ok(()) | Err(session::RecvLoopError::Io(_)));
3395                session = connected.release();
3396                if recoverable {
3397                    continue;
3398                }
3399                break;
3400            }
3401        });
3402        (handle, acceptor_tx, rx, cancel_token)
3403    }
3404
3405    async fn write_stream<M, W>(
3406        mut writer: W,
3407        _session_id: u64,
3408        messages: &[(u64, M)],
3409        _init: bool,
3410    ) -> W
3411    where
3412        M: RemoteMessage + PartialEq + Clone,
3413        W: AsyncWrite + Unpin,
3414    {
3415        for (seq, message) in messages {
3416            let message =
3417                serde_multipart::serialize_bincode(&Frame::<M>::Message(*seq, message.clone()))
3418                    .unwrap();
3419            let mut fw = FrameWrite::new(
3420                writer,
3421                message.framed(),
3422                hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3423                0,
3424            )
3425            .map_err(|(_w, e)| e)
3426            .unwrap();
3427            fw.send().await.unwrap();
3428            writer = fw.complete();
3429        }
3430
3431        writer
3432    }
3433
3434    #[async_timed_test(timeout_secs = 60)]
3435    async fn test_persistent_server_session() {
3436        let config = hyperactor_config::global::lock();
3437        let _guard = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1);
3438
3439        async fn verify_ack(reader: &mut FrameReader<ReadHalf<DuplexStream>>, expected_last: u64) {
3440            let mut last_acked: i128 = -1;
3441            loop {
3442                let (_, bytes) = reader.next().await.unwrap().unwrap();
3443                let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
3444                assert!(
3445                    acked as i128 > last_acked,
3446                    "acks should be delivered in ascending order"
3447                );
3448                last_acked = acked as i128;
3449                assert!(acked <= expected_last);
3450                if acked == expected_last {
3451                    break;
3452                }
3453            }
3454        }
3455
3456        let session_id = SessionId(123);
3457        let (_handle, acceptor_tx, mut rx, cancel_token) = serve_acceptor_test::<u64>(session_id);
3458
3459        // First connection: send messages, verify delivery and ack.
3460        {
3461            let (sender, receiver) = tokio::io::duplex(5000);
3462            acceptor_tx.send(receiver).unwrap();
3463
3464            let (r, writer) = tokio::io::split(sender);
3465            let mut reader = FrameReader::new(
3466                r,
3467                hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3468            );
3469
3470            let _writer = write_stream(
3471                writer,
3472                123,
3473                &[
3474                    (0u64, 100u64),
3475                    (1u64, 101u64),
3476                    (2u64, 102u64),
3477                    (3u64, 103u64),
3478                ],
3479                true,
3480            )
3481            .await;
3482
3483            assert_eq!(rx.recv().await, Some(100));
3484            assert_eq!(rx.recv().await, Some(101));
3485            assert_eq!(rx.recv().await, Some(102));
3486            assert_eq!(rx.recv().await, Some(103));
3487
3488            verify_ack(&mut reader, 3).await;
3489            // Drop reader and writer to close the connection.
3490        }
3491
3492        // Second connection (reconnection): retransmitted messages are deduped.
3493        {
3494            let (sender2, receiver2) = tokio::io::duplex(5000);
3495            acceptor_tx.send(receiver2).unwrap();
3496
3497            let (r2, writer2) = tokio::io::split(sender2);
3498            let mut reader2 = FrameReader::new(
3499                r2,
3500                hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3501            );
3502
3503            let _ = write_stream(
3504                writer2,
3505                123,
3506                &[
3507                    (2u64, 102u64),
3508                    (3u64, 103u64),
3509                    (4u64, 104u64),
3510                    (5u64, 105u64),
3511                ],
3512                true,
3513            )
3514            .await;
3515
3516            // 102 and 103 are retransmits; only 104 and 105 are new.
3517            assert_eq!(rx.recv().await, Some(104));
3518            assert_eq!(rx.recv().await, Some(105));
3519
3520            verify_ack(&mut reader2, 5).await;
3521
3522            cancel_token.cancel();
3523        }
3524    }
3525
3526    #[async_timed_test(timeout_secs = 60)]
3527    async fn test_ack_from_server_session() {
3528        let config = hyperactor_config::global::lock();
3529        let _guard = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1);
3530        let session_id = SessionId(123);
3531        let (_handle, acceptor_tx, mut rx, cancel_token) = serve_acceptor_test::<u64>(session_id);
3532
3533        let (sender, receiver) = tokio::io::duplex(5000);
3534        acceptor_tx.send(receiver).unwrap();
3535        let (r, mut writer) = tokio::io::split(sender);
3536        let mut reader = FrameReader::new(
3537            r,
3538            hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3539        );
3540
3541        for i in 0u64..100u64 {
3542            writer = write_stream(writer, 123, &[(i, 100u64 + i)], /*init*/ i == 0u64).await;
3543            assert_eq!(rx.recv().await, Some(100u64 + i));
3544            let (_, bytes) = reader.next().await.unwrap().unwrap();
3545            let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
3546            assert_eq!(acked, i);
3547        }
3548
3549        // Wait long enough to ensure server processed everything.
3550        tokio::time::sleep(Duration::from_secs(5)).await;
3551
3552        cancel_token.cancel();
3553
3554        // Should send NetRxResponse::Closed before stopping.
3555        let (_, bytes) = reader.next().await.unwrap().unwrap();
3556        assert!(deserialize_response(bytes).unwrap().is_closed());
3557    }
3558
3559    #[tracing_test::traced_test]
3560    async fn verify_tx_closed(tx_status: &mut watch::Receiver<TxStatus>, expected_log: &str) {
3561        match tokio::time::timeout(Duration::from_secs(5), tx_status.changed()).await {
3562            Ok(Ok(())) => {
3563                let current_status = tx_status.borrow().clone();
3564                assert!(current_status.is_closed());
3565                logs_assert_unscoped(|logs| {
3566                    if logs.iter().any(|log| log.contains(expected_log)) {
3567                        Ok(())
3568                    } else {
3569                        Err("expected log not found".to_string())
3570                    }
3571                });
3572            }
3573            Ok(Err(_)) => panic!("watch::Receiver::changed() failed because sender is dropped."),
3574            Err(_) => panic!("timeout before tx_status changed"),
3575        }
3576    }
3577
3578    #[tracing_test::traced_test]
3579    #[tokio::test]
3580    // TODO: OSS: The logs_assert function returned an error: expected log not found
3581    #[cfg_attr(not(fbcode_build), ignore)]
3582    async fn test_tcp_tx_delivery_timeout() {
3583        // This link always fails to connect.
3584        let link = MockLink::<u64>::fail_connects();
3585        let tx = spawn::<u64>(link);
3586        // Override the default (1m) for the purposes of this test.
3587        let config = hyperactor_config::global::lock();
3588        let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(1));
3589        let mut tx_receiver = tx.status().clone();
3590        let _receipt = tx.try_post(123);
3591        verify_tx_closed(&mut tx_receiver, "failed to deliver message within timeout").await;
3592    }
3593
3594    async fn take_receiver(
3595        receiver_storage: &MVar<DuplexStream>,
3596    ) -> (FrameReader<ReadHalf<DuplexStream>>, WriteHalf<DuplexStream>) {
3597        let mut receiver = receiver_storage.take().await;
3598        // Read and discard the LinkInit header that MockLink::connect() writes.
3599        let _link_init = read_link_init(&mut receiver).await.expect("read LinkInit");
3600        let (r, writer) = tokio::io::split(receiver);
3601        let reader = FrameReader::new(
3602            r,
3603            hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3604        );
3605        (reader, writer)
3606    }
3607
3608    async fn verify_message<M: RemoteMessage + PartialEq + std::fmt::Debug>(
3609        reader: &mut FrameReader<ReadHalf<DuplexStream>>,
3610        expect: (u64, M),
3611        loc: u32,
3612    ) {
3613        let expected = Frame::Message(expect.0, expect.1);
3614        let (_, bytes) = reader.next().await.unwrap().expect("unexpected EOF");
3615        let message = serde_multipart::Message::from_framed(bytes).unwrap();
3616        let frame: Frame<M> = serde_multipart::deserialize_bincode(message).unwrap();
3617
3618        assert_eq!(frame, expected, "from ln={loc}");
3619    }
3620
3621    async fn verify_stream<M: RemoteMessage + PartialEq + std::fmt::Debug + Clone>(
3622        reader: &mut FrameReader<ReadHalf<DuplexStream>>,
3623        expects: &[(u64, M)],
3624        _expect_session_id: Option<u64>,
3625        loc: u32,
3626    ) {
3627        for expect in expects {
3628            verify_message(reader, expect.clone(), loc).await;
3629        }
3630    }
3631
3632    async fn net_tx_send(tx: &ChannelTx<u64>, msgs: &[u64]) {
3633        for msg in msgs {
3634            tx.post(*msg);
3635        }
3636    }
3637
3638    // Happy path: all messages are acked.
3639    #[async_timed_test(timeout_secs = 30)]
3640    async fn test_ack_in_net_tx_basic() {
3641        let link = MockLink::<u64>::new();
3642        let receiver_storage = link.receiver_storage();
3643        let tx = spawn::<u64>(link);
3644
3645        // Send some messages, but not acking any of them.
3646        net_tx_send(&tx, &[100, 101, 102, 103, 104]).await;
3647        {
3648            let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3649            verify_stream(
3650                &mut reader,
3651                &[
3652                    (0u64, 100u64),
3653                    (1u64, 101u64),
3654                    (2u64, 102u64),
3655                    (3u64, 103u64),
3656                    (4u64, 104u64),
3657                ],
3658                None,
3659                line!(),
3660            )
3661            .await;
3662
3663            for i in 0u64..5u64 {
3664                writer = FrameWrite::write_frame(
3665                    writer,
3666                    serialize_response(NetRxResponse::Ack(i)).unwrap(),
3667                    1024,
3668                    0,
3669                )
3670                .await
3671                .map_err(|(_, e)| e)
3672                .unwrap();
3673            }
3674            // Wait for the acks to be processed by ChannelTx.
3675            tokio::time::sleep(Duration::from_secs(3)).await;
3676            // Drop both halves to break the in-memory connection (parity with old drop of DuplexStream).
3677            drop(reader);
3678            drop(writer);
3679        };
3680
3681        // Sent a new message to verify all sent messages will not be resent.
3682        net_tx_send(&tx, &[105u64]).await;
3683        {
3684            let (mut reader, _writer) = take_receiver(&receiver_storage).await;
3685            verify_stream(&mut reader, &[(5u64, 105u64)], None, line!()).await;
3686            // Reader/writer dropped here. This breaks the connection.
3687        };
3688    }
3689
3690    // Verify unacked message will be resent after reconnection.
3691    #[async_timed_test(timeout_secs = 60)]
3692    async fn test_persistent_net_tx() {
3693        let link = MockLink::<u64>::new();
3694        let receiver_storage = link.receiver_storage();
3695
3696        let tx = spawn::<u64>(link);
3697
3698        // Send some messages, but not acking any of them.
3699        net_tx_send(&tx, &[100, 101, 102, 103, 104]).await;
3700
3701        // How many times to reconnect. Keep this small because the send loop
3702        // applies exponential backoff between reconnections, and mock connections
3703        // are too short-lived to trigger the backoff reset.
3704        let n = 3;
3705
3706        // Reconnect multiple times. The messages should be resent every time
3707        // because none of them is acked.
3708        for i in 0..n {
3709            {
3710                let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3711                verify_stream(
3712                    &mut reader,
3713                    &[
3714                        (0u64, 100u64),
3715                        (1u64, 101u64),
3716                        (2u64, 102u64),
3717                        (3u64, 103u64),
3718                        (4u64, 104u64),
3719                    ],
3720                    None,
3721                    line!(),
3722                )
3723                .await;
3724
3725                // In the last iteration, ack part of the messages. This should
3726                // prune them from future resent.
3727                if i == n - 1 {
3728                    writer = FrameWrite::write_frame(
3729                        writer,
3730                        serialize_response(NetRxResponse::Ack(1)).unwrap(),
3731                        1024,
3732                        0,
3733                    )
3734                    .await
3735                    .map_err(|(_, e)| e)
3736                    .unwrap();
3737                    // Wait for the acks to be processed by ChannelTx.
3738                    tokio::time::sleep(Duration::from_secs(3)).await;
3739                }
3740                // client DuplexStream is dropped here. This breaks the connection.
3741                drop(reader);
3742                drop(writer);
3743            };
3744        }
3745
3746        // Verify only unacked are resent.
3747        for _ in 0..n {
3748            {
3749                let (mut reader, mut _writer) = take_receiver(&receiver_storage).await;
3750                verify_stream(
3751                    &mut reader,
3752                    &[(2u64, 102u64), (3u64, 103u64), (4u64, 104u64)],
3753                    None,
3754                    line!(),
3755                )
3756                .await;
3757                // drop(reader/_writer) at scope end
3758            };
3759        }
3760
3761        // Now send more messages.
3762        net_tx_send(&tx, &[105u64, 106u64, 107u64, 108u64, 109u64]).await;
3763        // Verify the unacked messages from the 1st send will be grouped with
3764        // the 2nd send.
3765        for i in 0..n {
3766            {
3767                let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3768                verify_stream(
3769                    &mut reader,
3770                    &[
3771                        // From the 1st send.
3772                        (2u64, 102u64),
3773                        (3u64, 103u64),
3774                        (4u64, 104u64),
3775                        // From the 2nd send.
3776                        (5u64, 105u64),
3777                        (6u64, 106u64),
3778                        (7u64, 107u64),
3779                        (8u64, 108u64),
3780                        (9u64, 109u64),
3781                    ],
3782                    None,
3783                    line!(),
3784                )
3785                .await;
3786
3787                // In the last iteration, ack part of the messages from the 1st
3788                // sent.
3789                if i == n - 1 {
3790                    // Intentionally ack 1 again to verify it is okay to ack
3791                    // messages that was already acked.
3792                    writer = FrameWrite::write_frame(
3793                        writer,
3794                        serialize_response(NetRxResponse::Ack(1)).unwrap(),
3795                        1024,
3796                        0,
3797                    )
3798                    .await
3799                    .map_err(|(_, e)| e)
3800                    .unwrap();
3801                    writer = FrameWrite::write_frame(
3802                        writer,
3803                        serialize_response(NetRxResponse::Ack(2)).unwrap(),
3804                        1024,
3805                        0,
3806                    )
3807                    .await
3808                    .map_err(|(_, e)| e)
3809                    .unwrap();
3810                    writer = FrameWrite::write_frame(
3811                        writer,
3812                        serialize_response(NetRxResponse::Ack(3)).unwrap(),
3813                        1024,
3814                        0,
3815                    )
3816                    .await
3817                    .map_err(|(_, e)| e)
3818                    .unwrap();
3819                    // Wait for the acks to be processed by ChannelTx.
3820                    tokio::time::sleep(Duration::from_secs(3)).await;
3821                }
3822                // client DuplexStream is dropped here. This breaks the connection.
3823                drop(reader);
3824                drop(writer);
3825            };
3826        }
3827
3828        for i in 0..n {
3829            {
3830                let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3831                verify_stream(
3832                    &mut reader,
3833                    &[
3834                        // From the 1st send.
3835                        (4u64, 104),
3836                        // From the 2nd send.
3837                        (5u64, 105u64),
3838                        (6u64, 106u64),
3839                        (7u64, 107u64),
3840                        (8u64, 108u64),
3841                        (9u64, 109u64),
3842                    ],
3843                    None,
3844                    line!(),
3845                )
3846                .await;
3847
3848                // In the last iteration, ack part of the messages from the 2nd send.
3849                if i == n - 1 {
3850                    writer = FrameWrite::write_frame(
3851                        writer,
3852                        serialize_response(NetRxResponse::Ack(7)).unwrap(),
3853                        1024,
3854                        0,
3855                    )
3856                    .await
3857                    .map_err(|(_, e)| e)
3858                    .unwrap();
3859                    // Wait for the acks to be processed by ChannelTx.
3860                    tokio::time::sleep(Duration::from_secs(3)).await;
3861                }
3862                // client DuplexStream is dropped here. This breaks the connection.
3863                drop(reader);
3864                drop(writer);
3865            };
3866        }
3867
3868        for _ in 0..n {
3869            {
3870                let (mut reader, writer) = take_receiver(&receiver_storage).await;
3871                verify_stream(
3872                    &mut reader,
3873                    &[
3874                        // From the 2nd send.
3875                        (8u64, 108u64),
3876                        (9u64, 109u64),
3877                    ],
3878                    None,
3879                    line!(),
3880                )
3881                .await;
3882                // client DuplexStream is dropped here. This breaks the connection.
3883                drop(reader);
3884                drop(writer);
3885            };
3886        }
3887    }
3888
3889    #[async_timed_test(timeout_secs = 15)]
3890    async fn test_ack_before_redelivery_in_net_tx() {
3891        let link = MockLink::<u64>::new();
3892        let receiver_storage = link.receiver_storage();
3893        let net_tx = spawn::<u64>(link);
3894
3895        // Verify sent-and-ack a message. This is necessary for the test to
3896        // trigger a connection.
3897        let receipt = net_tx.try_post(100);
3898        let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3899        verify_stream(&mut reader, &[(0u64, 100u64)], None, line!()).await;
3900        // ack it
3901        writer = FrameWrite::write_frame(
3902            writer,
3903            serialize_response(NetRxResponse::Ack(0)).unwrap(),
3904            1024,
3905            0,
3906        )
3907        .await
3908        .map_err(|(_, e)| e)
3909        .unwrap();
3910        // confirm Tx received ack
3911        assert!(receipt.await.is_ok());
3912
3913        // Now fake an unknown delivery for Tx:
3914        // Although Tx did not actually send seq=1, we still ack it from Rx to
3915        // pretend Tx already sent it, just it did not know it was sent
3916        // successfully.
3917        let _ = FrameWrite::write_frame(
3918            writer,
3919            serialize_response(NetRxResponse::Ack(1)).unwrap(),
3920            1024,
3921            0,
3922        )
3923        .await
3924        .map_err(|(_, e)| e)
3925        .unwrap();
3926
3927        let receipt = net_tx.try_post(101);
3928        // Verify the message is sent to Rx.
3929        verify_message(&mut reader, (1u64, 101u64), line!()).await;
3930        // although we did not ack the message after it is sent, since we already
3931        // acked it previously, Tx will treat it as acked, and considered the
3932        // message delivered successfully.
3933        //
3934        assert!(receipt.await.is_ok());
3935    }
3936
3937    async fn verify_ack_exceeded_limit(disconnect_before_ack: bool) {
3938        // Use temporary config for this test
3939        let config = hyperactor_config::global::lock();
3940        let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(2));
3941
3942        let link: MockLink<u64> = MockLink::<u64>::new();
3943        let disconnect_signal = link.disconnect_signal().clone();
3944        let fail_connect_switch = link.fail_connects_switch();
3945        let receiver_storage = link.receiver_storage();
3946        let tx = spawn::<u64>(link);
3947        let mut tx_status = tx.status().clone();
3948        // send a message
3949        tx.post(100);
3950        let (mut reader, writer) = take_receiver(&receiver_storage).await;
3951        // Confirm message is sent to rx.
3952        verify_stream(&mut reader, &[(0u64, 100u64)], None, line!()).await;
3953        // ack it
3954        let _ = FrameWrite::write_frame(
3955            writer,
3956            serialize_response(NetRxResponse::Ack(0)).unwrap(),
3957            hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3958            0,
3959        )
3960        .await
3961        .map_err(|(_, e)| e)
3962        .unwrap();
3963        tokio::time::sleep(Duration::from_secs(3)).await;
3964        // Channel should be still alive because ack was sent.
3965        assert!(!tx_status.has_changed().unwrap());
3966        assert_eq!(*tx_status.borrow(), TxStatus::Active);
3967
3968        tx.post(101);
3969        // Confirm message is sent to rx.
3970        verify_message(&mut reader, (1u64, 101u64), line!()).await;
3971
3972        if disconnect_before_ack {
3973            // Prevent link from reconnect
3974            fail_connect_switch.store(true, Ordering::Release);
3975            // Break the existing connection
3976            disconnect_signal.send(()).unwrap();
3977        }
3978
3979        // Verify the channel is closed due to ack timeout based on the log.
3980        let expected_log: &str = if disconnect_before_ack {
3981            "failed to receive ack within timeout 2s; link is currently broken"
3982        } else {
3983            "failed to receive ack within timeout 2s; link is currently connected"
3984        };
3985
3986        verify_tx_closed(&mut tx_status, expected_log).await;
3987    }
3988
3989    #[tracing_test::traced_test]
3990    #[async_timed_test(timeout_secs = 30)]
3991    // TODO: OSS: The logs_assert function returned an error: expected log not found
3992    #[cfg_attr(not(fbcode_build), ignore)]
3993    async fn test_ack_exceeded_limit_with_connected_link() {
3994        verify_ack_exceeded_limit(false).await;
3995    }
3996
3997    #[tracing_test::traced_test]
3998    #[async_timed_test(timeout_secs = 30)]
3999    // TODO: OSS: The logs_assert function returned an error: expected log not found
4000    #[cfg_attr(not(fbcode_build), ignore)]
4001    async fn test_ack_exceeded_limit_with_broken_link() {
4002        verify_ack_exceeded_limit(true).await;
4003    }
4004
4005    // Verify a large number of messages can be delivered and acked with the
4006    // presence of flakiness in the network, i.e. random delay and disconnection.
4007    #[async_timed_test(timeout_secs = 60)]
4008    async fn test_network_flakiness_in_channel() {
4009        hyperactor_telemetry::initialize_logging_for_test();
4010
4011        let sampling_rate = 100;
4012        let mut link = MockLink::<u64>::with_network_flakiness(NetworkFlakiness {
4013            disconnect_params: Some((0.001, 15, Duration::from_millis(400))),
4014            latency_range: Some((Duration::from_millis(100), Duration::from_millis(200))),
4015        });
4016        link.set_sampling_rate(sampling_rate);
4017        // Set a large buffer size to improve throughput.
4018        link.set_buffer_size(1024000);
4019        let disconnected_count = link.disconnected_count();
4020        let receiver_storage = link.receiver_storage();
4021        let listener = MockLinkListener::new(receiver_storage.clone(), link.dest());
4022        let local_addr = listener.channel_addr.clone();
4023        let (_, mut nx): (ChannelAddr, ChannelRx<u64>) =
4024            super::server::serve_with_listener(listener, local_addr).unwrap();
4025        let tx = spawn::<u64>(link);
4026        let messages: Vec<_> = (0..10001).collect();
4027        let messages_clone = messages.clone();
4028        // Put the sender side in a separate task so we can start the receiver
4029        // side concurrently.
4030        let send_task_handle = tokio::spawn(async move {
4031            for message in messages_clone {
4032                // Add a small delay between messages to give ChannelRx time to ack.
4033                // Technically, this test still can pass without this delay. But
4034                // the test will need a might larger timeout. The reason is
4035                // fairly convoluted:
4036                //
4037                // MockLink uses the number of delivery to calculate the disconnection
4038                // probability. If ChannelRx sends messages much faster than ChannelTx
4039                // can ack them, there is a higher chance that the messages are
4040                // not acked before reconnect. Then those message would be redelivered.
4041                // The repeated redelivery increases the total time of sending
4042                // these messages.
4043                tokio::time::sleep(Duration::from_micros(rand::random::<u64>() % 100)).await;
4044                tx.post(message);
4045            }
4046            tracing::debug!("ChannelTx sent all messages");
4047            // It is important to return tx instead of dropping it here, because
4048            // Rx might not receive all messages yet.
4049            tx
4050        });
4051
4052        for message in &messages {
4053            if message % sampling_rate == 0 {
4054                tracing::debug!("ChannelRx received a message: {message}");
4055            }
4056            assert_eq!(nx.recv().await.unwrap(), *message);
4057        }
4058        tracing::debug!("ChannelRx received all messages");
4059
4060        let send_result = send_task_handle.await;
4061        assert!(send_result.is_ok());
4062
4063        tracing::debug!(
4064            "MockLink disconnected {} times.",
4065            disconnected_count.load(Ordering::SeqCst)
4066        );
4067        // TODO(pzhang) after the return_handle work in ChannelTx is done, add a
4068        // check here to verify the messages are acked correctly.
4069    }
4070
4071    #[async_timed_test(timeout_secs = 60)]
4072    async fn test_ack_every_n_messages() {
4073        let config = hyperactor_config::global::lock();
4074        let _guard_message_ack = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 600);
4075        let _guard_time_interval =
4076            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(1000));
4077        sparse_ack().await;
4078    }
4079
4080    #[async_timed_test(timeout_secs = 60)]
4081    async fn test_ack_every_time_interval() {
4082        let config = hyperactor_config::global::lock();
4083        let _guard_message_ack =
4084            config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 100000000);
4085        let _guard_time_interval = config.override_key(
4086            config::MESSAGE_ACK_TIME_INTERVAL,
4087            Duration::from_millis(500),
4088        );
4089        sparse_ack().await;
4090    }
4091
4092    async fn sparse_ack() {
4093        let mut link = MockLink::<u64>::new();
4094        // Set a large buffer size to improve throughput.
4095        link.set_buffer_size(1024000);
4096        let disconnected_count = link.disconnected_count();
4097        let receiver_storage = link.receiver_storage();
4098        let listener = MockLinkListener::new(receiver_storage.clone(), link.dest());
4099        let local_addr = listener.channel_addr.clone();
4100        let (_, mut nx): (ChannelAddr, ChannelRx<u64>) =
4101            super::server::serve_with_listener(listener, local_addr).unwrap();
4102        let tx = spawn::<u64>(link);
4103        let messages: Vec<_> = (0..20001).collect();
4104        let messages_clone = messages.clone();
4105        // Put the sender side in a separate task so we can start the receiver
4106        // side concurrently.
4107        let send_task_handle = tokio::spawn(async move {
4108            for message in messages_clone {
4109                tokio::time::sleep(Duration::from_micros(rand::random::<u64>() % 100)).await;
4110                tx.post(message);
4111            }
4112            tokio::time::sleep(Duration::from_secs(5)).await;
4113            tracing::debug!("ChannelTx sent all messages");
4114            tx
4115        });
4116
4117        for message in &messages {
4118            assert_eq!(nx.recv().await.unwrap(), *message);
4119        }
4120        tracing::debug!("ChannelRx received all messages");
4121
4122        let send_result = send_task_handle.await;
4123        assert!(send_result.is_ok());
4124
4125        tracing::debug!(
4126            "MockLink disconnected {} times.",
4127            disconnected_count.load(Ordering::SeqCst)
4128        );
4129    }
4130
4131    #[test]
4132    fn test_metatls_parsing() {
4133        // host:port
4134        let channel: ChannelAddr = "metatls!localhost:1234".parse().unwrap();
4135        assert_eq!(
4136            channel,
4137            ChannelAddr::MetaTls(TlsAddr::new("localhost", 1234))
4138        );
4139        // ipv4:port - parsed as hostname with ip normalization
4140        let channel: ChannelAddr = "metatls!1.2.3.4:1234".parse().unwrap();
4141        assert_eq!(channel, ChannelAddr::MetaTls(TlsAddr::new("1.2.3.4", 1234)));
4142        // ipv6:port
4143        let channel: ChannelAddr = "metatls!2401:db00:33c:6902:face:0:2a2:0:1234"
4144            .parse()
4145            .unwrap();
4146        assert_eq!(
4147            channel,
4148            ChannelAddr::MetaTls(TlsAddr::new("2401:db00:33c:6902:face:0:2a2:0", 1234))
4149        );
4150
4151        let channel: ChannelAddr = "metatls![::]:1234".parse().unwrap();
4152        assert_eq!(channel, ChannelAddr::MetaTls(TlsAddr::new("::", 1234)));
4153    }
4154
4155    #[async_timed_test(timeout_secs = 300)]
4156    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" })
4157    #[cfg_attr(not(fbcode_build), ignore)]
4158    async fn test_tcp_throughput() {
4159        let config = hyperactor_config::global::lock();
4160        let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_mins(5));
4161
4162        let socket_addr: SocketAddr = "[::1]:0".parse().unwrap();
4163        let (local_addr, mut rx) =
4164            server::serve::<String>(ChannelAddr::Tcp(socket_addr), None).unwrap();
4165
4166        // Test with 10 connections (senders), each sends 500K messages, 5M messages in total.
4167        let total_num_msgs = 500000;
4168
4169        let receive_handle = tokio::spawn(async move {
4170            let mut num = 0;
4171            for _ in 0..10 * total_num_msgs {
4172                rx.recv().await.unwrap();
4173                num += 1;
4174
4175                if num % 100000 == 0 {
4176                    tracing::info!("total number of received messages: {}", num);
4177                }
4178            }
4179        });
4180
4181        let mut tx_handles = vec![];
4182        let mut txs = vec![];
4183        for _ in 0..10 {
4184            let server_addr = local_addr.clone();
4185            let tx = Arc::new(channel::dial::<String>(server_addr).unwrap());
4186            let tx2 = Arc::clone(&tx);
4187            txs.push(tx);
4188            tx_handles.push(tokio::spawn(async move {
4189                let random_string = rand::rng()
4190                    .sample_iter(&Alphanumeric)
4191                    .take(2048)
4192                    .map(char::from)
4193                    .collect::<String>();
4194                for _ in 0..total_num_msgs {
4195                    tx2.post(random_string.clone());
4196                }
4197            }));
4198        }
4199
4200        receive_handle.await.unwrap();
4201        for handle in tx_handles {
4202            handle.await.unwrap();
4203        }
4204    }
4205
4206    #[tracing_test::traced_test]
4207    #[async_timed_test(timeout_secs = 60)]
4208    // TODO: OSS: The logs_assert function returned an error: expected log not found
4209    #[cfg_attr(not(fbcode_build), ignore)]
4210    async fn test_net_tx_closed_on_server_reject() {
4211        let link = MockLink::<u64>::new();
4212        let receiver_storage = link.receiver_storage();
4213        let mut tx = spawn::<u64>(link);
4214        net_tx_send(&tx, &[100]).await;
4215
4216        {
4217            let (_reader, writer) = take_receiver(&receiver_storage).await;
4218            let _ = FrameWrite::write_frame(
4219                writer,
4220                serialize_response(NetRxResponse::Reject("testing".to_string())).unwrap(),
4221                1024,
4222                0,
4223            )
4224            .await
4225            .map_err(|(_, e)| e);
4226
4227            // Wait for response to be processed by ChannelTx before dropping reader/writer. Otherwise
4228            // the channel will be closed and we will get the wrong error.
4229            tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
4230        }
4231
4232        verify_tx_closed(&mut tx.status, "server rejected connection").await;
4233    }
4234
4235    #[async_timed_test(timeout_secs = 60)]
4236    async fn test_server_rejects_conn_on_out_of_sequence_message() {
4237        let config = hyperactor_config::global::lock();
4238        let _guard = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1);
4239        let session_id = SessionId(123);
4240        let (_handle, acceptor_tx, mut rx, _cancel_token) = serve_acceptor_test::<u64>(session_id);
4241
4242        let (sender, receiver) = tokio::io::duplex(5000);
4243        acceptor_tx.send(receiver).unwrap();
4244        let (r, writer) = tokio::io::split(sender);
4245        let mut reader = FrameReader::new(
4246            r,
4247            hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
4248        );
4249
4250        let _ = write_stream(writer, 123, &[(0, 100u64), (1, 101u64), (3, 103u64)], true).await;
4251        assert_eq!(rx.recv().await, Some(100u64));
4252        assert_eq!(rx.recv().await, Some(101u64));
4253        let (_, bytes) = reader.next().await.unwrap().unwrap();
4254        let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
4255        assert_eq!(acked, 0);
4256        let (_, bytes) = reader.next().await.unwrap().unwrap();
4257        let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
4258        assert_eq!(acked, 1);
4259        let (_, bytes) = reader.next().await.unwrap().unwrap();
4260        assert!(deserialize_response(bytes).unwrap().is_reject());
4261    }
4262
4263    #[async_timed_test(timeout_secs = 60)]
4264    // TODO: OSS: called `Result::unwrap()` on an `Err` value: Listen(Tcp([::1]:0), Os { code: 99, kind: AddrNotAvailable, message: "Cannot assign requested address" })
4265    #[cfg_attr(not(fbcode_build), ignore)]
4266    async fn test_stop_net_tx_after_stopping_net_rx() {
4267        hyperactor_telemetry::initialize_logging_for_test();
4268
4269        let config = hyperactor_config::global::lock();
4270        let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_mins(5));
4271        let (addr, mut rx) =
4272            server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
4273        let socket_addr = match addr {
4274            ChannelAddr::Tcp(a) => a,
4275            _ => panic!("unexpected channel type"),
4276        };
4277        let tx: ChannelTx<u64> = spawn(tcp::link(
4278            socket_addr,
4279            SessionId::random(),
4280            0,
4281            ProtocolKind::Simplex,
4282        ));
4283        // ChannelTx will not establish a connection until it sends the 1st message.
4284        // Without a live connection, ChannelTx cannot received the Closed message
4285        // from ChannelRx. Therefore, we need to send a message to establish the
4286        //connection.
4287        tx.send(100).await.unwrap();
4288        assert_eq!(rx.recv().await.unwrap(), 100);
4289        // Drop rx will close the ChannelRx server.
4290        rx.server.stop("testing");
4291        assert!(rx.recv().await.is_err());
4292
4293        // ChannelTx will only read from the stream when it needs to send a message
4294        // or wait for an ack. Therefore we need to send a message to trigger that.
4295        tx.post(101);
4296        let mut watcher = tx.status().clone();
4297        // When ChannelRx exits, it should notify ChannelTx to exit as well.
4298        let _ = watcher.wait_for(|val| val.is_closed()).await;
4299        // wait_for could return Err due to race between when watch's sender was
4300        // dropped and when wait_for was called. So we still need to do an
4301        // equality check.
4302        assert!(watcher.borrow().is_closed());
4303    }
4304
4305    /// Yields pre-built `DuplexStream`s to the accept loop and
4306    /// blocks once drained. Lets the `rx_join_flushes_pending_ack_*`
4307    /// tests inspect the wire from the other end.
4308    struct QueueListener {
4309        streams: std::collections::VecDeque<DuplexStream>,
4310        addr: ChannelAddr,
4311    }
4312
4313    #[async_trait]
4314    impl super::Listener for QueueListener {
4315        type Stream = DuplexStream;
4316
4317        async fn accept(&mut self) -> Result<(DuplexStream, ChannelAddr), ServerError> {
4318            match self.streams.pop_front() {
4319                Some(s) => Ok((s, self.addr.clone())),
4320                None => std::future::pending().await,
4321            }
4322        }
4323    }
4324
4325    /// Stream wrapper that gates writes (server-side terminal cleanup)
4326    /// until the test releases the gate. Reads pass through. Used by
4327    /// the mux drain-aware regression tests to make the dispatch's
4328    /// final ack/Closed write blockable, so a cancel-only handle's
4329    /// "join returns before cleanup" race is observable.
4330    #[derive(Debug)]
4331    pub(super) struct GatedWriteStream {
4332        inner: DuplexStream,
4333        gate: Arc<GateState>,
4334    }
4335
4336    #[derive(Debug)]
4337    pub(super) struct GateState {
4338        open: AtomicBool,
4339        waker: std::sync::Mutex<Option<std::task::Waker>>,
4340    }
4341
4342    impl GateState {
4343        pub(super) fn new() -> Arc<Self> {
4344            Arc::new(Self {
4345                open: AtomicBool::new(false),
4346                waker: std::sync::Mutex::new(None),
4347            })
4348        }
4349
4350        /// Open the gate so any pending writes can proceed.
4351        pub(super) fn open(&self) {
4352            self.open.store(true, Ordering::Release);
4353            if let Some(w) = self.waker.lock().unwrap().take() {
4354                w.wake();
4355            }
4356        }
4357    }
4358
4359    impl GatedWriteStream {
4360        pub(super) fn new(inner: DuplexStream, gate: Arc<GateState>) -> Self {
4361            Self { inner, gate }
4362        }
4363    }
4364
4365    impl AsyncRead for GatedWriteStream {
4366        fn poll_read(
4367            mut self: std::pin::Pin<&mut Self>,
4368            cx: &mut std::task::Context<'_>,
4369            buf: &mut tokio::io::ReadBuf<'_>,
4370        ) -> std::task::Poll<std::io::Result<()>> {
4371            std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
4372        }
4373    }
4374
4375    impl AsyncWrite for GatedWriteStream {
4376        fn poll_write(
4377            mut self: std::pin::Pin<&mut Self>,
4378            cx: &mut std::task::Context<'_>,
4379            buf: &[u8],
4380        ) -> std::task::Poll<std::io::Result<usize>> {
4381            if !self.gate.open.load(Ordering::Acquire) {
4382                *self.gate.waker.lock().unwrap() = Some(cx.waker().clone());
4383                if !self.gate.open.load(Ordering::Acquire) {
4384                    return std::task::Poll::Pending;
4385                }
4386            }
4387            std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
4388        }
4389
4390        fn poll_flush(
4391            mut self: std::pin::Pin<&mut Self>,
4392            cx: &mut std::task::Context<'_>,
4393        ) -> std::task::Poll<std::io::Result<()>> {
4394            std::pin::Pin::new(&mut self.inner).poll_flush(cx)
4395        }
4396
4397        fn poll_shutdown(
4398            mut self: std::pin::Pin<&mut Self>,
4399            cx: &mut std::task::Context<'_>,
4400        ) -> std::task::Poll<std::io::Result<()>> {
4401            std::pin::Pin::new(&mut self.inner).poll_shutdown(cx)
4402        }
4403    }
4404
4405    /// `Listener` over a single pre-prepared `GatedWriteStream`. Used
4406    /// to drive the mux drain-aware regression tests with a stream
4407    /// whose terminal write is blockable.
4408    struct GatedQueueListener {
4409        streams: std::collections::VecDeque<GatedWriteStream>,
4410        addr: ChannelAddr,
4411    }
4412
4413    #[async_trait]
4414    impl super::Listener for GatedQueueListener {
4415        type Stream = GatedWriteStream;
4416
4417        async fn accept(&mut self) -> Result<(GatedWriteStream, ChannelAddr), ServerError> {
4418            match self.streams.pop_front() {
4419                Some(s) => Ok((s, self.addr.clone())),
4420                None => std::future::pending().await,
4421            }
4422        }
4423    }
4424
4425    /// In-memory connection: server end goes into the listener; the
4426    /// test reads from `client_r`.
4427    struct PreparedConnection {
4428        server_side: DuplexStream,
4429        // Kept alive so the server's recv-loop stays in its `select!`
4430        // on cancellation rather than exiting on EOF. Tests must
4431        // exercise the cancel-flush path.
4432        _client_w: tokio::io::WriteHalf<DuplexStream>,
4433        client_r: ReadHalf<DuplexStream>,
4434    }
4435
4436    /// Write `LinkInit` and the framed `Frame::Message(seq, value)`
4437    /// payloads on the client side; return both halves. The caller
4438    /// chooses the [`ProtocolKind`] so simplex tests can stamp
4439    /// `Simplex` and duplex tests can stamp `Duplex`, matching
4440    /// production handshake validation.
4441    async fn prepare_connection(
4442        session_id: SessionId,
4443        stream_id: u8,
4444        kind: super::ProtocolKind,
4445        messages: &[(u64, u64)],
4446    ) -> PreparedConnection {
4447        let (client_side, server_side) = tokio::io::duplex(8192);
4448        let (client_r, mut client_w) = tokio::io::split(client_side);
4449
4450        super::write_link_init(&mut client_w, session_id, stream_id, kind)
4451            .await
4452            .unwrap();
4453        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4454        for (seq, value) in messages {
4455            let payload =
4456                serde_multipart::serialize_bincode(&Frame::<u64>::Message(*seq, *value)).unwrap();
4457            let mut fw = FrameWrite::new(client_w, payload.framed(), max_len, 0)
4458                .map_err(|(_w, e)| e)
4459                .unwrap();
4460            fw.send().await.unwrap();
4461            client_w = fw.complete();
4462        }
4463
4464        PreparedConnection {
4465            server_side,
4466            _client_w: client_w,
4467            client_r,
4468        }
4469    }
4470
4471    /// Test plan for `run_separate_sessions_flush_test`: each entry
4472    /// describes one connection that lives on its own session.
4473    struct SeparateSessionPlan {
4474        session_id: SessionId,
4475        stream_id: u8,
4476        messages: Vec<(u64, u64)>,
4477    }
4478
4479    /// Drive the rx.join flush test across multiple connections,
4480    /// each on its own session. Verifies:
4481    ///
4482    /// 1. Every message sent reaches the application via `rx.recv()`.
4483    /// 2. After application delivery completes, *no* ack frames have
4484    ///    been emitted on any connection's read side — the policy
4485    ///    thresholds are out of reach.
4486    /// 3. After `rx.join()` returns, every connection has exactly
4487    ///    one `NetRxResponse::Ack(highest_seq)` frame on its read
4488    ///    side, followed by a `NetRxResponse::Closed` terminal frame.
4489    async fn run_separate_sessions_flush_test(
4490        plans: Vec<SeparateSessionPlan>,
4491        stream_id_label: &str,
4492    ) {
4493        let config = hyperactor_config::global::lock();
4494        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
4495        let _g_time =
4496            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
4497
4498        // Build all connections and stage them into a `QueueListener`.
4499        let mut conns: Vec<PreparedConnection> = Vec::with_capacity(plans.len());
4500        let mut expected_messages: std::collections::HashSet<u64> =
4501            std::collections::HashSet::new();
4502        let mut expected_acks: Vec<u64> = Vec::with_capacity(plans.len());
4503        for plan in &plans {
4504            for (_seq, value) in &plan.messages {
4505                expected_messages.insert(*value);
4506            }
4507            expected_acks.push(plan.messages.iter().map(|(s, _)| *s).max().unwrap());
4508            conns.push(
4509                prepare_connection(
4510                    plan.session_id,
4511                    plan.stream_id,
4512                    super::ProtocolKind::Simplex,
4513                    &plan.messages,
4514                )
4515                .await,
4516            );
4517        }
4518
4519        let addr = ChannelAddr::Local(u64::MAX);
4520        let listener = QueueListener {
4521            streams: conns
4522                .iter_mut()
4523                .map(|c| {
4524                    // Move server_side out of each PreparedConnection by replacing it with a placeholder.
4525                    std::mem::replace(&mut c.server_side, tokio::io::duplex(1).0)
4526                })
4527                .collect(),
4528            addr: addr.clone(),
4529        };
4530        let (_addr, mut rx) = super::server::serve_with_listener::<u64, _>(listener, addr).unwrap();
4531
4532        // Drain every expected message off the application channel.
4533        let mut received: std::collections::HashSet<u64> = std::collections::HashSet::new();
4534        for _ in 0..expected_messages.len() {
4535            received.insert(rx.recv().await.unwrap());
4536        }
4537        assert_eq!(
4538            received, expected_messages,
4539            "{stream_id_label}: every produced message should reach the application"
4540        );
4541
4542        // Give any policy-driven ack timer a generous grace period to
4543        // fire. Because `MESSAGE_ACK_EVERY_N_MESSAGES` and
4544        // `MESSAGE_ACK_TIME_INTERVAL` are out of reach, no ack should
4545        // be emitted yet — but if a regression makes them reachable
4546        // (or adds a new spontaneous emission path), this sleep gives
4547        // it room to do so before the assertion below.
4548        tokio::time::sleep(Duration::from_millis(100)).await;
4549
4550        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4551        let mut readers: Vec<FrameReader<ReadHalf<DuplexStream>>> = conns
4552            .into_iter()
4553            .map(|c| FrameReader::new(c.client_r, max_len))
4554            .collect();
4555        for (idx, reader) in readers.iter_mut().enumerate() {
4556            match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
4557                Err(_) => {} // timeout — no frame, expected.
4558                Ok(Err(e)) => panic!(
4559                    "{stream_id_label}: connection {idx} frame reader error before rx.join: {e}"
4560                ),
4561                Ok(Ok(None)) => {
4562                    panic!("{stream_id_label}: connection {idx} closed before rx.join()")
4563                }
4564                Ok(Ok(Some((_, bytes)))) => {
4565                    let resp = super::deserialize_response(bytes).unwrap();
4566                    panic!(
4567                        "{stream_id_label}: connection {idx} unexpectedly received {resp:?} \
4568                         before rx.join()"
4569                    );
4570                }
4571            }
4572        }
4573
4574        rx.join().await;
4575
4576        // After rx.join() returns, every connection must have its
4577        // terminal cleanup frames already written: an `Ack` covering
4578        // the highest seq it sent, then a `Closed`.
4579        for (idx, (reader, expected_ack)) in readers.iter_mut().zip(&expected_acks).enumerate() {
4580            let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4581                .await
4582                .unwrap_or_else(|_| {
4583                    panic!(
4584                        "{stream_id_label}: connection {idx} produced no Ack frame within 50ms \
4585                         after rx.join()"
4586                    )
4587                })
4588                .expect("frame reader error")
4589                .expect("frame reader returned None");
4590            let acked = super::deserialize_response(bytes.1)
4591                .unwrap()
4592                .into_ack()
4593                .unwrap_or_else(|other| {
4594                    panic!("{stream_id_label}: connection {idx} expected Ack, got {other:?}")
4595                });
4596            assert_eq!(
4597                acked, *expected_ack,
4598                "{stream_id_label}: connection {idx} ack mismatch"
4599            );
4600
4601            let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4602                .await
4603                .unwrap_or_else(|_| {
4604                    panic!(
4605                        "{stream_id_label}: connection {idx} produced no Closed frame within 50ms"
4606                    )
4607                })
4608                .expect("frame reader error")
4609                .expect("frame reader returned None");
4610            assert!(
4611                super::deserialize_response(bytes.1).unwrap().is_closed(),
4612                "{stream_id_label}: connection {idx} expected Closed terminal frame"
4613            );
4614        }
4615    }
4616
4617    #[async_timed_test(timeout_secs = 30)]
4618    async fn rx_join_flushes_pending_ack_single_stream() {
4619        // Three independent single-stream sessions, each with three
4620        // framed messages. Verifies every recv-loop's terminal flush
4621        // ran by the time `rx.join()` returns.
4622        let plans = (1u64..=3)
4623            .map(|sid| SeparateSessionPlan {
4624                session_id: SessionId(sid),
4625                stream_id: 0,
4626                messages: (0u64..3).map(|seq| (seq, sid * 100 + seq)).collect(),
4627            })
4628            .collect();
4629        run_separate_sessions_flush_test(plans, "single-stream").await;
4630    }
4631
4632    #[async_timed_test(timeout_secs = 30)]
4633    async fn rx_join_flushes_pending_ack_multi_stream() {
4634        // Three independent multi-stream sessions, each with three
4635        // framed messages, each on its own session_id with a single
4636        // stream_id of 1. Exercises `dispatch_multi_stream`'s terminal
4637        // cleanup once per session.
4638        let plans = (1u64..=3)
4639            .map(|sid| SeparateSessionPlan {
4640                session_id: SessionId(sid),
4641                stream_id: 1,
4642                messages: (0u64..3).map(|seq| (seq, sid * 100 + seq)).collect(),
4643            })
4644            .collect();
4645        run_separate_sessions_flush_test(plans, "multi-stream").await;
4646    }
4647
4648    /// One session, multiple stream_ids — exercises `AckWatermark`'s
4649    /// shared-watermark path. Three streams in the same session each
4650    /// send three messages with disjoint seqs filling the contiguous
4651    /// range 0..=8. Each stream's cleanup reads `highest_uncommitted`
4652    /// and emits `Ack(8)` on its own wire so the peer's per-wire
4653    /// ChannelTx sees an ack for messages it sent there; the receiver
4654    /// discards duplicates. Every stream also emits its own `Closed`.
4655    #[async_timed_test(timeout_secs = 30)]
4656    async fn rx_join_flushes_pending_ack_shared_multi_stream_session() {
4657        let config = hyperactor_config::global::lock();
4658        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
4659        let _g_time =
4660            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
4661
4662        let session_id = SessionId(99);
4663        let num_streams = 3u8;
4664        let msgs_per_stream = 3u64;
4665        let mut conns: Vec<PreparedConnection> = Vec::with_capacity(num_streams as usize);
4666        let mut expected_messages: std::collections::HashSet<u64> =
4667            std::collections::HashSet::new();
4668        for stream_id in 1..=num_streams {
4669            let messages: Vec<(u64, u64)> = (0u64..msgs_per_stream)
4670                .map(|i| {
4671                    let seq = (stream_id as u64 - 1) * msgs_per_stream + i;
4672                    (seq, 1000 + seq)
4673                })
4674                .collect();
4675            for (_, v) in &messages {
4676                expected_messages.insert(*v);
4677            }
4678            conns.push(
4679                prepare_connection(
4680                    session_id,
4681                    stream_id,
4682                    super::ProtocolKind::Simplex,
4683                    &messages,
4684                )
4685                .await,
4686            );
4687        }
4688        let highest_seq = num_streams as u64 * msgs_per_stream - 1;
4689
4690        let addr = ChannelAddr::Local(u64::MAX);
4691        let listener = QueueListener {
4692            streams: conns
4693                .iter_mut()
4694                .map(|c| std::mem::replace(&mut c.server_side, tokio::io::duplex(1).0))
4695                .collect(),
4696            addr: addr.clone(),
4697        };
4698        let (_addr, mut rx) = super::server::serve_with_listener::<u64, _>(listener, addr).unwrap();
4699
4700        let mut received: std::collections::HashSet<u64> = std::collections::HashSet::new();
4701        for _ in 0..expected_messages.len() {
4702            received.insert(rx.recv().await.unwrap());
4703        }
4704        assert_eq!(
4705            received, expected_messages,
4706            "shared-session multi-stream: every message reaches the application"
4707        );
4708
4709        tokio::time::sleep(Duration::from_millis(100)).await;
4710
4711        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4712        let mut readers: Vec<FrameReader<ReadHalf<DuplexStream>>> = conns
4713            .into_iter()
4714            .map(|c| FrameReader::new(c.client_r, max_len))
4715            .collect();
4716        for (idx, reader) in readers.iter_mut().enumerate() {
4717            match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
4718                Err(_) | Ok(Ok(None)) => {} // timeout / EOF — no early ack, good.
4719                Ok(Err(e)) => {
4720                    panic!("shared-session multi-stream: stream {idx} frame reader error: {e}")
4721                }
4722                Ok(Ok(Some((_, bytes)))) => {
4723                    let resp = super::deserialize_response(bytes).unwrap();
4724                    panic!(
4725                        "shared-session multi-stream: stream {idx} unexpectedly received \
4726                         {resp:?} before rx.join()"
4727                    );
4728                }
4729            }
4730        }
4731
4732        rx.join().await;
4733
4734        // Drain every frame on every reader. Terminal cleanup may emit
4735        // `Ack(highest_seq)` on one or more wires before `Closed`, but
4736        // once one cleanup commits the shared watermark, later streams
4737        // may emit only `Closed`.
4738        let mut ack_count = 0;
4739        let mut closed_count = 0;
4740        for (idx, reader) in readers.iter_mut().enumerate() {
4741            loop {
4742                match tokio::time::timeout(Duration::from_millis(50), reader.next()).await {
4743                    Err(_) => panic!(
4744                        "shared-session multi-stream: stream {idx} did not yield expected \
4745                         frames within 50ms after rx.join()"
4746                    ),
4747                    Ok(Err(e)) => panic!("frame reader error: {e}"),
4748                    Ok(Ok(None)) => break,
4749                    Ok(Ok(Some((_, bytes)))) => {
4750                        let resp = super::deserialize_response(bytes).unwrap();
4751                        match resp {
4752                            NetRxResponse::Ack(seq) => {
4753                                assert_eq!(
4754                                    seq, highest_seq,
4755                                    "shared-session multi-stream: ack should cover the full \
4756                                     contiguous range 0..={highest_seq}"
4757                                );
4758                                ack_count += 1;
4759                            }
4760                            NetRxResponse::Closed => {
4761                                closed_count += 1;
4762                                break;
4763                            }
4764                            other => panic!(
4765                                "shared-session multi-stream: stream {idx} unexpected {other:?}"
4766                            ),
4767                        }
4768                    }
4769                }
4770            }
4771        }
4772        assert!(
4773            ack_count >= 1,
4774            "shared-session multi-stream: expected at least one Ack({highest_seq}); \
4775             got {ack_count}"
4776        );
4777        assert!(
4778            ack_count <= num_streams as usize,
4779            "shared-session multi-stream: expected at most {num_streams} Ack({highest_seq}) \
4780             frames; got {ack_count}"
4781        );
4782        assert_eq!(
4783            closed_count, num_streams as usize,
4784            "shared-session multi-stream: every stream should emit its own Closed frame"
4785        );
4786    }
4787
4788    /// `duplex::serve_with_listener` (the test-only duplex server)
4789    /// must enforce the same `ProtocolKind::Duplex` handshake check as
4790    /// production `duplex::serve`. Otherwise the duplex ack-flush
4791    /// tests below could pass with a wire header that the real server
4792    /// would reject — weakening coverage exactly where shutdown
4793    /// behavior is most sensitive.
4794    #[async_timed_test(timeout_secs = 30)]
4795    async fn duplex_test_listener_rejects_simplex_link_init() {
4796        let conn =
4797            prepare_connection(SessionId(1), 0, super::ProtocolKind::Simplex, &[(0, 123)]).await;
4798        let addr = ChannelAddr::Local(u64::MAX);
4799        let listener = QueueListener {
4800            streams: std::collections::VecDeque::from([conn.server_side]),
4801            addr: addr.clone(),
4802        };
4803
4804        let mut server = super::duplex::serve_with_listener::<u64, u64, _>(listener, addr).unwrap();
4805
4806        assert!(
4807            tokio::time::timeout(Duration::from_millis(500), server.accept())
4808                .await
4809                .is_err(),
4810            "duplex test server accepted a simplex LinkInit",
4811        );
4812    }
4813
4814    /// Duplex analog of `rx_join_flushes_pending_ack_single_stream`.
4815    /// Three independent duplex sessions, each with three framed
4816    /// messages. Verifies every `dispatch_duplex_stream`'s terminal
4817    /// flush ran by the time `DuplexServer::join()` returns —
4818    /// structured concurrency makes the listener task await every
4819    /// inline recv/send loop before resolving.
4820    #[async_timed_test(timeout_secs = 30)]
4821    async fn server_join_flushes_pending_ack_duplex_session() {
4822        let config = hyperactor_config::global::lock();
4823        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
4824        let _g_time =
4825            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
4826
4827        let session_count = 3u64;
4828        let msgs_per_session = 3u64;
4829        let mut conns: Vec<PreparedConnection> = Vec::with_capacity(session_count as usize);
4830        let mut expected_messages: std::collections::HashSet<u64> =
4831            std::collections::HashSet::new();
4832        let mut expected_acks: Vec<u64> = Vec::with_capacity(session_count as usize);
4833        for sid in 1..=session_count {
4834            let messages: Vec<(u64, u64)> = (0u64..msgs_per_session)
4835                .map(|seq| (seq, sid * 100 + seq))
4836                .collect();
4837            for (_, v) in &messages {
4838                expected_messages.insert(*v);
4839            }
4840            expected_acks.push(messages.iter().map(|(s, _)| *s).max().unwrap());
4841            conns.push(
4842                prepare_connection(SessionId(sid), 0, super::ProtocolKind::Duplex, &messages).await,
4843            );
4844        }
4845
4846        let addr = ChannelAddr::Local(u64::MAX);
4847        let listener = QueueListener {
4848            streams: conns
4849                .iter_mut()
4850                .map(|c| std::mem::replace(&mut c.server_side, tokio::io::duplex(1).0))
4851                .collect(),
4852            addr: addr.clone(),
4853        };
4854        let mut server = super::duplex::serve_with_listener::<u64, u64, _>(listener, addr).unwrap();
4855
4856        // Accept one (rx, tx) pair per session. Hold the tx halves
4857        // alive so the server's send-loop stays parked in `select!`
4858        // (it never sees an `AppClosed` terminal) — the test must
4859        // exercise the cancel-driven flush path, not an
4860        // app-disconnect one.
4861        let mut all_rx: Vec<super::duplex::DuplexRx<u64>> =
4862            Vec::with_capacity(session_count as usize);
4863        let mut all_tx: Vec<super::duplex::DuplexTx<u64>> =
4864            Vec::with_capacity(session_count as usize);
4865        for _ in 0..session_count {
4866            let (rx, tx) = server.accept().await.unwrap();
4867            all_rx.push(rx);
4868            all_tx.push(tx);
4869        }
4870
4871        // Drain the messages each session sent. Each session's
4872        // dispatch task delivers exactly its own `msgs_per_session`
4873        // values to its own `rx`, but the order in which sessions
4874        // are accepted is non-deterministic — collect into a set.
4875        let mut received: std::collections::HashSet<u64> = std::collections::HashSet::new();
4876        for rx in all_rx.iter_mut() {
4877            for _ in 0..msgs_per_session {
4878                received.insert(rx.recv().await.unwrap());
4879            }
4880        }
4881        assert_eq!(
4882            received, expected_messages,
4883            "duplex: every produced message should reach the application"
4884        );
4885
4886        // Policy thresholds are out of reach, so no ack should fire
4887        // spontaneously. The sleep gives any regression that adds a
4888        // new emission path room to surface.
4889        tokio::time::sleep(Duration::from_millis(100)).await;
4890
4891        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4892        let mut readers: Vec<FrameReader<ReadHalf<DuplexStream>>> = conns
4893            .into_iter()
4894            .map(|c| FrameReader::new(c.client_r, max_len))
4895            .collect();
4896        for (idx, reader) in readers.iter_mut().enumerate() {
4897            match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
4898                Err(_) => {} // timeout — no frame, expected.
4899                Ok(Err(e)) => {
4900                    panic!("duplex: connection {idx} frame reader error before join: {e}")
4901                }
4902                Ok(Ok(None)) => {
4903                    panic!("duplex: connection {idx} closed before server.join()")
4904                }
4905                Ok(Ok(Some((_, bytes)))) => {
4906                    let resp = super::deserialize_response(bytes).unwrap();
4907                    panic!(
4908                        "duplex: connection {idx} unexpectedly received {resp:?} \
4909                         before server.join()"
4910                    );
4911                }
4912            }
4913        }
4914
4915        // Trigger graceful shutdown. Holding `all_tx` / `all_rx`
4916        // alive keeps the send-loop parked and `inbound_tx` valid,
4917        // so the dispatch task only exits via the cancel branch of
4918        // its `select!` — exercising the structured-concurrency
4919        // flush-on-cancel path.
4920        server.join().await;
4921
4922        // After server.join() returns, every connection must have
4923        // its terminal cleanup frames already on the wire: an
4924        // `Ack(highest_seq)` covering the messages it sent, then a
4925        // `Closed`.
4926        for (idx, (reader, expected_ack)) in readers.iter_mut().zip(&expected_acks).enumerate() {
4927            let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4928                .await
4929                .unwrap_or_else(|_| {
4930                    panic!(
4931                        "duplex: connection {idx} produced no Ack frame within 50ms after \
4932                         server.join()"
4933                    )
4934                })
4935                .expect("frame reader error")
4936                .expect("frame reader returned None");
4937            let acked = super::deserialize_response(bytes.1)
4938                .unwrap()
4939                .into_ack()
4940                .unwrap_or_else(|other| {
4941                    panic!("duplex: connection {idx} expected Ack, got {other:?}")
4942                });
4943            assert_eq!(
4944                acked, *expected_ack,
4945                "duplex: connection {idx} ack mismatch"
4946            );
4947
4948            let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4949                .await
4950                .unwrap_or_else(|_| {
4951                    panic!("duplex: connection {idx} produced no Closed frame within 50ms")
4952                })
4953                .expect("frame reader error")
4954                .expect("frame reader returned None");
4955            assert!(
4956                super::deserialize_response(bytes.1).unwrap().is_closed(),
4957                "duplex: connection {idx} expected Closed terminal frame"
4958            );
4959        }
4960    }
4961
4962    /// Test-only [`Link`] that yields each pre-built `DuplexStream`
4963    /// once. Writes `LinkInit` on the stream before returning it so
4964    /// the test (which holds the other end) sees the same wire
4965    /// format a real `TcpLink` produces.
4966    struct DuplexDialMockLink {
4967        session_id: SessionId,
4968        streams: std::collections::VecDeque<DuplexStream>,
4969    }
4970
4971    impl fmt::Debug for DuplexDialMockLink {
4972        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4973            f.debug_struct("DuplexDialMockLink")
4974                .field("session_id", &self.session_id)
4975                .field("remaining_streams", &self.streams.len())
4976                .finish()
4977        }
4978    }
4979
4980    #[async_trait]
4981    impl super::Link for DuplexDialMockLink {
4982        type Stream = DuplexStream;
4983
4984        fn dest(&self) -> ChannelAddr {
4985            ChannelAddr::Local(u64::MAX)
4986        }
4987
4988        fn link_id(&self) -> SessionId {
4989            self.session_id
4990        }
4991
4992        async fn next(&mut self) -> Result<DuplexStream, ClientError> {
4993            match self.streams.pop_front() {
4994                Some(mut stream) => {
4995                    super::write_link_init(
4996                        &mut stream,
4997                        self.session_id,
4998                        0,
4999                        super::ProtocolKind::Duplex,
5000                    )
5001                    .await
5002                    .map_err(|err| ClientError::Io(self.dest(), err))?;
5003                    Ok(stream)
5004                }
5005                None => Err(ClientError::Connect(
5006                    self.dest(),
5007                    std::io::Error::other("mock link exhausted"),
5008                    "no more streams".into(),
5009                )),
5010            }
5011        }
5012    }
5013
5014    /// Acceptor-side counterpart of
5015    /// [`duplex_dial_flushes_pending_ack_on_app_closed`]. The
5016    /// application drops its `DuplexTx`, the dispatch task's
5017    /// `send_connected` returns `SendLoopError::AppClosed` —
5018    /// terminal — so `dispatch_duplex_stream`'s loop exits via the
5019    /// flush + `Closed` + break path. Verifies the cumulative ack
5020    /// and the terminal `Closed` are written on
5021    /// `INITIATOR_TO_ACCEPTOR` before the dispatch task ends.
5022    #[async_timed_test(timeout_secs = 30)]
5023    async fn duplex_serve_flushes_pending_ack_on_app_closed() {
5024        let config = hyperactor_config::global::lock();
5025        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5026        let _g_time =
5027            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5028
5029        let session_id = SessionId(1);
5030        let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5031        let expected_ack: u64 = 2;
5032        let conn = prepare_connection(session_id, 0, super::ProtocolKind::Duplex, &messages).await;
5033
5034        let addr = ChannelAddr::Local(u64::MAX);
5035        let listener = QueueListener {
5036            streams: std::collections::VecDeque::from([conn.server_side]),
5037            addr: addr.clone(),
5038        };
5039
5040        let mut server = super::duplex::serve_with_listener::<u64, u64, _>(listener, addr).unwrap();
5041
5042        let (mut server_rx, server_tx) = server.accept().await.unwrap();
5043
5044        // Drain the messages the test wrote on `INITIATOR_TO_ACCEPTOR`.
5045        let mut received: Vec<u64> = Vec::with_capacity(messages.len());
5046        for _ in &messages {
5047            received.push(server_rx.recv().await.unwrap());
5048        }
5049        let expected_values: Vec<u64> = messages.iter().map(|(_, v)| *v).collect();
5050        assert_eq!(
5051            received, expected_values,
5052            "duplex serve: every message should reach the application"
5053        );
5054
5055        // Policy thresholds are unreachable, so no ack should fire
5056        // spontaneously.
5057        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5058        tokio::time::sleep(Duration::from_millis(100)).await;
5059        let mut reader = FrameReader::new(conn.client_r, max_len);
5060        match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5061            Err(_) => {} // timeout — expected.
5062            Ok(Err(e)) => panic!("duplex serve: frame reader error before app close: {e}"),
5063            Ok(Ok(None)) => panic!("duplex serve: wire closed before app close"),
5064            Ok(Ok(Some((_, bytes)))) => {
5065                let resp = super::deserialize_response(bytes).unwrap();
5066                panic!("duplex serve: unexpectedly received {resp:?} before app close");
5067            }
5068        }
5069
5070        // Drop the application's `DuplexTx` to trigger `AppClosed`
5071        // in `send_connected` — terminal — so the dispatch task
5072        // exits. The flush logic must write the cumulative ack on
5073        // `INITIATOR_TO_ACCEPTOR` and the terminal `Closed` before
5074        // the loop breaks.
5075        drop(server_tx);
5076
5077        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5078            .await
5079            .unwrap_or_else(|_| panic!("duplex serve: produced no Ack frame within 100ms"))
5080            .expect("frame reader error")
5081            .expect("frame reader returned None");
5082        let acked = super::deserialize_response(bytes.1)
5083            .unwrap()
5084            .into_ack()
5085            .unwrap_or_else(|other| panic!("duplex serve: expected Ack, got {other:?}"));
5086        assert_eq!(
5087            acked, expected_ack,
5088            "duplex serve: ack should cover the highest seq received"
5089        );
5090
5091        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5092            .await
5093            .unwrap_or_else(|_| panic!("duplex serve: produced no Closed frame within 100ms"))
5094            .expect("frame reader error")
5095            .expect("frame reader returned None");
5096        assert!(
5097            super::deserialize_response(bytes.1).unwrap().is_closed(),
5098            "duplex serve: expected Closed terminal frame after Ack"
5099        );
5100
5101        drop(server_rx);
5102        // `server.join()` triggers the listener-task cancel so the
5103        // outer `accept_loop` returns. The dispatch task already
5104        // exited via `AppClosed` and was drained from the JoinSet,
5105        // so this just stops the listener.
5106        server.join().await;
5107    }
5108
5109    /// Wire-level regression for the mux per-half drain-aware
5110    /// `ServerHandle`. After `DuplexServer::stop()` is called on the
5111    /// mux's duplex half, `DuplexServer::join()` must wait for every
5112    /// dispatched session's terminal cleanup (final ack flush +
5113    /// `Closed` emit) to reach the wire — not merely for the
5114    /// cancellation token to fire. Adversarial test #4 from the AI
5115    /// review's coverage gap. Prior to the drain-aware handle
5116    /// (`cancel_only_handle`), `join` could report shutdown complete
5117    /// before the dispatch's flush reached the wire.
5118    #[async_timed_test(timeout_secs = 30)]
5119    async fn mux_duplex_join_flushes_pending_ack_for_session() {
5120        let config = hyperactor_config::global::lock();
5121        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5122        let _g_time =
5123            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5124
5125        let session_id = SessionId(1);
5126        let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5127        let expected_ack: u64 = 2;
5128        let conn = prepare_connection(session_id, 0, super::ProtocolKind::Duplex, &messages).await;
5129
5130        let addr = ChannelAddr::Local(u64::MAX);
5131        let listener = QueueListener {
5132            streams: std::collections::VecDeque::from([conn.server_side]),
5133            addr: addr.clone(),
5134        };
5135
5136        let mut parts =
5137            super::mux::serve_with_listener::<u64, u64, u64, _>(listener, addr).unwrap();
5138
5139        // Accept the duplex session and drain the inbound messages.
5140        // Hold `_server_tx` alive across the shutdown so the dispatch
5141        // is parked on the cancel branch (not `AppClosed`); the
5142        // adversarial path is precisely the cancel-driven cleanup.
5143        let (mut server_rx, _server_tx) = parts.duplex.accept().await.unwrap();
5144        for _ in &messages {
5145            server_rx.recv().await.unwrap();
5146        }
5147
5148        // No spontaneous ack expected before shutdown.
5149        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5150        let mut reader = FrameReader::new(conn.client_r, max_len);
5151        tokio::time::sleep(Duration::from_millis(50)).await;
5152        match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5153            Err(_) => {} // timeout expected.
5154            Ok(Err(e)) => panic!("mux: frame reader error before shutdown: {e}"),
5155            Ok(Ok(None)) => panic!("mux: wire closed before shutdown"),
5156            Ok(Ok(Some((_, bytes)))) => {
5157                let resp = super::deserialize_response(bytes).unwrap();
5158                panic!("mux: unexpectedly received {resp:?} before shutdown");
5159            }
5160        }
5161
5162        // `DuplexServer::stop` fires the shared cancel; `join`
5163        // awaits the per-half handle's join_handle. With the
5164        // drain-aware handle, that resolves only after the
5165        // coordinator signals `drained` — i.e., after the
5166        // dispatch's terminal cleanup has reached the wire.
5167        parts.duplex.stop("test mux duplex join flush");
5168        parts.duplex.join().await;
5169
5170        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5171            .await
5172            .unwrap_or_else(|_| panic!("mux: produced no Ack frame within 100ms after join"))
5173            .expect("frame reader error")
5174            .expect("frame reader returned None");
5175        let acked = super::deserialize_response(bytes.1)
5176            .unwrap()
5177            .into_ack()
5178            .unwrap_or_else(|other| panic!("mux: expected Ack, got {other:?}"));
5179        assert_eq!(
5180            acked, expected_ack,
5181            "mux: ack should cover the highest seq received"
5182        );
5183
5184        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5185            .await
5186            .unwrap_or_else(|_| panic!("mux: produced no Closed frame within 100ms after join"))
5187            .expect("frame reader error")
5188            .expect("frame reader returned None");
5189        assert!(
5190            super::deserialize_response(bytes.1).unwrap().is_closed(),
5191            "mux: expected Closed terminal frame after Ack"
5192        );
5193
5194        // Tear down the listener after asserting on the wire.
5195        let _ = parts.simplex;
5196        let _ = parts.join_handle.await;
5197    }
5198
5199    /// Strict adversarial regression for the mux per-half drain-aware
5200    /// `ServerHandle`. Uses a [`GatedWriteStream`] to block the
5201    /// dispatch's terminal write so that — under the old
5202    /// `cancel_only_handle` behavior — `DuplexServer::join()` after
5203    /// `DuplexServer::stop()` would return *before* the Ack reached
5204    /// the wire. With the drain-aware handle, `join` waits for the
5205    /// listener's accept-loop to drain, which awaits the dispatch's
5206    /// blocked write; thus `join` is pinned `Pending` until the test
5207    /// opens the gate.
5208    #[async_timed_test(timeout_secs = 30)]
5209    async fn mux_duplex_join_blocks_until_terminal_write_completes() {
5210        let config = hyperactor_config::global::lock();
5211        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5212        let _g_time =
5213            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5214
5215        let session_id = SessionId(1);
5216        let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5217        let expected_ack: u64 = 2;
5218        let conn = prepare_connection(session_id, 0, super::ProtocolKind::Duplex, &messages).await;
5219
5220        // Wrap the server side so its writes (the dispatch's terminal
5221        // ack/Closed) are gated.
5222        let gate = GateState::new();
5223        let server_side_gated = GatedWriteStream::new(conn.server_side, gate.clone());
5224
5225        let addr = ChannelAddr::Local(u64::MAX);
5226        let listener = GatedQueueListener {
5227            streams: std::collections::VecDeque::from([server_side_gated]),
5228            addr: addr.clone(),
5229        };
5230
5231        let mut parts =
5232            super::mux::serve_with_listener::<u64, u64, u64, _>(listener, addr).unwrap();
5233
5234        let (mut server_rx, _server_tx) = parts.duplex.accept().await.unwrap();
5235        for _ in &messages {
5236            server_rx.recv().await.unwrap();
5237        }
5238
5239        // Stop + spawn join. With the drain-aware handle, join
5240        // should be pinned Pending until the gate opens. With a
5241        // `cancel_only_handle`, join would return immediately on
5242        // cancel — observable here as a non-Pending poll despite
5243        // the blocked write.
5244        parts
5245            .duplex
5246            .stop("test mux duplex join blocks until terminal write");
5247        let mut join_task = tokio::spawn(async move { parts.duplex.join().await });
5248
5249        let blocked = tokio::time::timeout(Duration::from_millis(200), &mut join_task).await;
5250        assert!(
5251            blocked.is_err(),
5252            "duplex.join() returned before the gated terminal write could complete"
5253        );
5254
5255        // Open the gate so dispatch's Ack/Closed writes proceed; join
5256        // should resolve once the accept loop drains.
5257        gate.open();
5258
5259        tokio::time::timeout(Duration::from_secs(5), join_task)
5260            .await
5261            .expect("duplex.join() did not resolve after gate opened")
5262            .expect("join task panicked");
5263
5264        // Verify the wire actually received Ack and Closed.
5265        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5266        let mut reader = FrameReader::new(conn.client_r, max_len);
5267
5268        let bytes = tokio::time::timeout(Duration::from_millis(200), reader.next())
5269            .await
5270            .expect("no Ack frame")
5271            .expect("frame reader error")
5272            .expect("frame reader returned None");
5273        let acked = super::deserialize_response(bytes.1)
5274            .unwrap()
5275            .into_ack()
5276            .unwrap_or_else(|other| panic!("expected Ack, got {other:?}"));
5277        assert_eq!(acked, expected_ack);
5278
5279        let bytes = tokio::time::timeout(Duration::from_millis(200), reader.next())
5280            .await
5281            .expect("no Closed frame")
5282            .expect("frame reader error")
5283            .expect("frame reader returned None");
5284        assert!(super::deserialize_response(bytes.1).unwrap().is_closed());
5285    }
5286
5287    /// Wire-level regression for the per-half `ServerHandle`'s
5288    /// drain-aware contract. Calling `ChannelRx::join()` on the mux's
5289    /// simplex half must wait for the dispatch's terminal cleanup
5290    /// (final ack flush + `Closed` emit) to reach the wire — not
5291    /// merely for the cancellation token to fire. Adversarial test
5292    /// #2 from the AI review.
5293    #[async_timed_test(timeout_secs = 30)]
5294    async fn mux_split_simplex_join_flushes_final_ack() {
5295        let config = hyperactor_config::global::lock();
5296        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5297        let _g_time =
5298            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5299
5300        let session_id = SessionId(1);
5301        let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5302        let expected_ack: u64 = 2;
5303        let conn = prepare_connection(session_id, 0, super::ProtocolKind::Simplex, &messages).await;
5304
5305        let addr = ChannelAddr::Local(u64::MAX);
5306        let listener = QueueListener {
5307            streams: std::collections::VecDeque::from([conn.server_side]),
5308            addr: addr.clone(),
5309        };
5310
5311        let mut parts =
5312            super::mux::serve_with_listener::<u64, u64, u64, _>(listener, addr).unwrap();
5313
5314        // Drain inbound through the simplex half.
5315        for _ in &messages {
5316            parts.simplex.recv().await.unwrap();
5317        }
5318
5319        // No spontaneous ack expected.
5320        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5321        let mut reader = FrameReader::new(conn.client_r, max_len);
5322        tokio::time::sleep(Duration::from_millis(50)).await;
5323        match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5324            Err(_) => {}
5325            Ok(Err(e)) => panic!("mux split: frame reader error before join: {e}"),
5326            Ok(Ok(None)) => panic!("mux split: wire closed before join"),
5327            Ok(Ok(Some((_, bytes)))) => {
5328                let resp = super::deserialize_response(bytes).unwrap();
5329                panic!("mux split: unexpectedly received {resp:?} before join");
5330            }
5331        }
5332
5333        // `ChannelRx::join` consumes the `ChannelRx`; with the drain-aware
5334        // handle it must wait for the dispatch's terminal cleanup.
5335        // Once join returns, the Ack and Closed frames must already
5336        // be on the wire.
5337        parts.simplex.join().await;
5338
5339        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5340            .await
5341            .unwrap_or_else(|_| panic!("mux split: produced no Ack frame within 100ms after join"))
5342            .expect("frame reader error")
5343            .expect("frame reader returned None");
5344        let acked = super::deserialize_response(bytes.1)
5345            .unwrap()
5346            .into_ack()
5347            .unwrap_or_else(|other| panic!("mux split: expected Ack, got {other:?}"));
5348        assert_eq!(
5349            acked, expected_ack,
5350            "mux split: simplex.join must flush the final ack before returning"
5351        );
5352
5353        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5354            .await
5355            .unwrap_or_else(|_| {
5356                panic!("mux split: produced no Closed frame within 100ms after join")
5357            })
5358            .expect("frame reader error")
5359            .expect("frame reader returned None");
5360        assert!(
5361            super::deserialize_response(bytes.1).unwrap().is_closed(),
5362            "mux split: expected Closed terminal frame after Ack"
5363        );
5364
5365        // Tear down the listener after asserting on the wire.
5366        let _ = parts.duplex;
5367        parts.cancel.cancel();
5368        let _ = parts.join_handle.await;
5369    }
5370
5371    /// [`DuplexClient::stop`] cancels the recv/send loop's
5372    /// cancellation token, and [`DuplexClient::join`] waits for the
5373    /// spawned task to finish. The `select!`s observe cancel and the
5374    /// loop exits via the flush + `Closed` + break path. Verifies the
5375    /// cumulative ack and the terminal `Closed` are written on
5376    /// `ACCEPTOR_TO_INITIATOR` before the spawned task ends.
5377    #[async_timed_test(timeout_secs = 30)]
5378    async fn duplex_client_stop_then_join_flushes_pending_ack() {
5379        let config = hyperactor_config::global::lock();
5380        let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5381        let _g_time =
5382            config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5383
5384        let session_id = SessionId(123);
5385        let (client_side, server_side) = tokio::io::duplex(8192);
5386        let (mut test_r, mut test_w) = tokio::io::split(server_side);
5387
5388        let link = DuplexDialMockLink {
5389            session_id,
5390            streams: std::collections::VecDeque::from([client_side]),
5391        };
5392
5393        let mut dial_client = super::duplex::spawn::<u64, u64>(link);
5394        let _dial_tx = dial_client.tx();
5395        let mut dial_rx = dial_client.take_rx().unwrap();
5396
5397        // Drain the LinkInit the dial-side wrote on connect.
5398        super::read_link_init(&mut test_r).await.unwrap();
5399
5400        // Test acts as the acceptor: write framed `Frame::Message`s
5401        // on `ACCEPTOR_TO_INITIATOR` so the dial-side recv-loop
5402        // reads them and forwards to the application via
5403        // `inbound_tx`.
5404        let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5405        let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5406        let expected_ack: u64 = 2;
5407        for (seq, value) in &messages {
5408            let payload =
5409                serde_multipart::serialize_bincode(&Frame::<u64>::Message(*seq, *value)).unwrap();
5410            let mut fw = FrameWrite::new(
5411                test_w,
5412                payload.framed(),
5413                max_len,
5414                super::ACCEPTOR_TO_INITIATOR,
5415            )
5416            .map_err(|(_w, e)| e)
5417            .unwrap();
5418            fw.send().await.unwrap();
5419            test_w = fw.complete();
5420        }
5421
5422        // Drain the messages on the dial-side rx so the dial-side's
5423        // `recv_next.seq` advances past every seq.
5424        let mut received: Vec<u64> = Vec::with_capacity(messages.len());
5425        for _ in &messages {
5426            received.push(dial_rx.recv().await.unwrap());
5427        }
5428        let expected_values: Vec<u64> = messages.iter().map(|(_, v)| *v).collect();
5429        assert_eq!(
5430            received, expected_values,
5431            "dial: every message should reach the application"
5432        );
5433
5434        // Policy thresholds are unreachable, so no ack should fire
5435        // spontaneously.
5436        tokio::time::sleep(Duration::from_millis(100)).await;
5437        let mut reader = FrameReader::new(test_r, max_len);
5438        match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5439            Err(_) => {} // timeout — expected.
5440            Ok(Err(e)) => panic!("dial: frame reader error before join: {e}"),
5441            Ok(Ok(None)) => panic!("dial: wire closed before join"),
5442            Ok(Ok(Some((_, bytes)))) => {
5443                let resp = super::deserialize_response(bytes).unwrap();
5444                panic!("dial: unexpectedly received {resp:?} before join");
5445            }
5446        }
5447
5448        // Trigger graceful shutdown. `stop` propagates cancellation
5449        // into the spawn loop's `select!`s; `join` waits until the
5450        // loop exits via Cancelled (terminal) and the flush logic
5451        // writes the cumulative ack and the `Closed` terminal frame.
5452        dial_client.join().await;
5453
5454        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5455            .await
5456            .unwrap_or_else(|_| panic!("dial: produced no Ack frame within 100ms after join"))
5457            .expect("frame reader error")
5458            .expect("frame reader returned None");
5459        let acked = super::deserialize_response(bytes.1)
5460            .unwrap()
5461            .into_ack()
5462            .unwrap_or_else(|other| panic!("dial: expected Ack, got {other:?}"));
5463        assert_eq!(
5464            acked, expected_ack,
5465            "dial: ack should cover the highest seq received"
5466        );
5467
5468        // After the ack, the dial-side writes the terminal
5469        // `Closed` response on `ACCEPTOR_TO_INITIATOR` (mirrors
5470        // `dispatch_duplex_stream`), then releases the connection.
5471        let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5472            .await
5473            .unwrap_or_else(|_| panic!("dial: produced no Closed frame within 100ms after join"))
5474            .expect("frame reader error")
5475            .expect("frame reader returned None");
5476        assert!(
5477            super::deserialize_response(bytes.1).unwrap().is_closed(),
5478            "dial: expected Closed terminal frame after Ack"
5479        );
5480
5481        drop(dial_rx);
5482        drop(test_w);
5483    }
5484
5485    /// Verifies that an in-progress [`DuplexRx::recv`] on the
5486    /// receiver returned by [`DuplexClient::take_rx`] resolves with
5487    /// [`ChannelError::Closed`] when [`DuplexClient::stop`] is called
5488    /// and [`DuplexClient::join`] waits concurrently. Structured
5489    /// concurrency guarantees the spawned task drops `inbound_tx`
5490    /// before `join` returns, so the receiver observes the close
5491    /// deterministically.
5492    #[async_timed_test(timeout_secs = 30)]
5493    async fn duplex_client_stop_then_join_terminates_in_progress_recv() {
5494        let session_id = SessionId(123);
5495        let (client_side, server_side) = tokio::io::duplex(8192);
5496        let (mut test_r, _test_w) = tokio::io::split(server_side);
5497
5498        let link = DuplexDialMockLink {
5499            session_id,
5500            streams: std::collections::VecDeque::from([client_side]),
5501        };
5502
5503        let mut dial_client = super::duplex::spawn::<u64, u64>(link);
5504        let mut dial_rx = dial_client.take_rx().unwrap();
5505
5506        // Drain the LinkInit so the dial-side has finished its
5507        // setup before we kick off the recv() under test.
5508        super::read_link_init(&mut test_r).await.unwrap();
5509
5510        // Park a recv() in a separate task; the dial-side hasn't
5511        // forwarded any inbound frames so this future will sit in
5512        // `inbound_rx.recv().await` indefinitely until stop/join
5513        // makes the spawned task drop its `inbound_tx`.
5514        let recv_handle: tokio::task::JoinHandle<Result<u64, ChannelError>> =
5515            tokio::spawn(async move { dial_rx.recv().await });
5516
5517        // `join` cancels the spawned task; on exit the task drops
5518        // its `inbound_tx`, which closes `inbound_rx` and resolves
5519        // the recv() with `ChannelError::Closed`.
5520        dial_client.join().await;
5521
5522        let result = tokio::time::timeout(Duration::from_millis(100), recv_handle)
5523            .await
5524            .expect("parked recv should resolve within 100ms after join")
5525            .expect("recv task should not panic");
5526        assert!(
5527            matches!(result, Err(ChannelError::Closed)),
5528            "in-progress recv should resolve with ChannelError::Closed after join, got {result:?}"
5529        );
5530    }
5531}