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