1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90pub(crate) struct SessionId(u64);
91
92impl SessionId {
93 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
105pub(crate) const INITIATOR_TO_ACCEPTOR: u8 = 0;
107
108pub(crate) const ACCEPTOR_TO_INITIATOR: u8 = 1;
110
111#[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
126const LINK_INIT_SIZE: usize = 4 + 8 + 1;
135
136#[derive(Debug, Clone, Copy)]
138pub(crate) struct LinkInit {
139 pub session_id: SessionId,
140 pub stream_id: u8,
141 pub kind: ProtocolKind,
142}
143
144pub(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
162async 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
189pub(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
234type PreparedStreamFut = std::pin::Pin<
236 Box<
237 dyn std::future::Future<Output = Result<(LinkInit, Box<dyn Stream>), anyhow::Error>> + Send,
238 >,
239>;
240
241pub(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#[async_trait]
260pub(crate) trait Link: Send + Sync + Debug + 'static {
261 type Stream: Stream;
263
264 fn dest(&self) -> ChannelAddr;
266
267 fn link_id(&self) -> SessionId;
269
270 async fn next(&mut self) -> Result<Self::Stream, ClientError>;
273}
274
275use session::Session;
276
277use crate::config;
278use crate::metrics;
279
280const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
282
283const TCP_KEEPALIVE_RETRIES: u32 = 3;
286
287fn 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
309fn set_tcp_congestion(stream: &tokio::net::TcpStream) {
335 let congestion = hyperactor_config::global::get_cloned(config::CHANNEL_TCP_CONGESTION);
336 let algo = Some(congestion.as_str()).filter(|algo| !algo.is_empty());
337 if let Err(err) = apply_tcp_congestion(stream, algo) {
338 tracing::warn!(?err, "leaving the host TCP congestion default in place");
339 }
340}
341
342#[cfg(any(target_os = "linux", target_os = "freebsd"))]
349fn apply_tcp_congestion(
350 stream: &tokio::net::TcpStream,
351 congestion: Option<&str>,
352) -> std::io::Result<()> {
353 let Some(algo) = congestion else {
354 return Ok(());
355 };
356 socket2::SockRef::from(stream)
357 .set_tcp_congestion(algo.as_bytes())
358 .map_err(|err| {
359 std::io::Error::new(
360 err.kind(),
361 format!("cannot select TCP congestion control {algo:?} ({err})"),
362 )
363 })
364}
365
366#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
370fn apply_tcp_congestion(
371 _stream: &tokio::net::TcpStream,
372 congestion: Option<&str>,
373) -> std::io::Result<()> {
374 let Some(algo) = congestion else {
375 return Ok(());
376 };
377 Err(std::io::Error::new(
378 std::io::ErrorKind::Unsupported,
379 format!("cannot select TCP congestion control {algo:?}: not supported on this platform"),
380 ))
381}
382
383pub(crate) enum LinkStatus {
384 NeverConnected,
385 Connected(tokio::time::Instant),
386 Disconnected {
387 last_connected: tokio::time::Instant,
388 since: tokio::time::Instant,
389 },
390}
391
392impl LinkStatus {
393 fn connected(&mut self) {
394 *self = LinkStatus::Connected(tokio::time::Instant::now());
395 }
396
397 fn disconnected(&mut self) {
398 match *self {
399 LinkStatus::Connected(at) => {
400 *self = LinkStatus::Disconnected {
401 last_connected: at,
402 since: tokio::time::Instant::now(),
403 };
404 }
405 _ => {}
407 }
408 }
409}
410
411impl std::fmt::Display for LinkStatus {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 match self {
414 LinkStatus::NeverConnected => write!(f, "never connected"),
415 LinkStatus::Connected(at) => {
416 write!(f, "connected for {:.1}s", at.elapsed().as_secs_f64())
417 }
418 LinkStatus::Disconnected {
419 last_connected,
420 since,
421 } => {
422 write!(
423 f,
424 "last connected {:.1}s ago, disconnected for {:.1}s",
425 last_connected.elapsed().as_secs_f64(),
426 since.elapsed().as_secs_f64(),
427 )
428 }
429 }
430 }
431}
432
433fn log_send_error(
436 error: &session::SendLoopError,
437 dest: &ChannelAddr,
438 session_id: u64,
439 mode: &str,
440 link_status: &LinkStatus,
441) -> bool {
442 match error {
443 session::SendLoopError::Io(err) => {
444 tracing::info!(dest = %dest, session_id, error = %err, mode, "send error; {link_status}");
445 metrics::CHANNEL_ERRORS.add(
446 1,
447 hyperactor_telemetry::kv_pairs!(
448 "dest" => dest.to_string(),
449 "session_id" => session_id.to_string(),
450 "error_type" => metrics::ChannelErrorType::SendError.as_str(),
451 "mode" => mode.to_string(),
452 ),
453 );
454 false
455 }
456 session::SendLoopError::AppClosed => true,
457 session::SendLoopError::Rejected(reason) => {
458 tracing::error!(dest = %dest, session_id, mode, "server rejected connection: {reason}; {link_status}");
459 true
460 }
461 session::SendLoopError::ServerClosed => {
462 tracing::info!(dest = %dest, session_id, mode, "server closed the channel; {link_status}");
463 true
464 }
465 session::SendLoopError::DeliveryTimeout => {
466 let timeout = hyperactor_config::global::get(config::MESSAGE_DELIVERY_TIMEOUT);
467 tracing::error!(
468 dest = %dest, session_id, mode,
469 "failed to receive ack within timeout {timeout:?}; link is currently connected; {link_status}"
470 );
471 true
472 }
473 session::SendLoopError::OversizedFrame { size, max } => {
474 tracing::error!(
475 dest = %dest,
476 session_id,
477 mode,
478 "oversized frame: len={size} > max={max}; {link_status}"
479 );
480 true
481 }
482 }
483}
484
485fn classify_send_loop_error(error: &session::SendLoopError, log_id: &str) -> CloseReason {
491 match error {
492 session::SendLoopError::Rejected(reason) if reason.contains("out-of-sequence message") => {
493 CloseReason::SequenceMismatch(reason.clone())
494 }
495 session::SendLoopError::OversizedFrame { size, max } => CloseReason::OversizedFrame {
496 size: *size,
497 max: *max,
498 },
499 _ => CloseReason::Other(format!("{log_id}: {error}")),
500 }
501}
502
503pub(crate) fn spawn<M: RemoteMessage>(link: impl Link) -> super::ChannelTx<M> {
505 spawn_inner::<M>(link)
506}
507
508pub(crate) fn spawn_unordered<M: RemoteMessage>(
512 links: Vec<impl Link + 'static>,
513) -> super::ChannelTx<M> {
514 assert!(!links.is_empty());
515 if links.len() == 1 {
516 return spawn(links.into_iter().next().unwrap());
517 }
518
519 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
520 let dest = links[0].dest();
521 let session_id = links[0].link_id();
522 let (notify, status) = watch::channel(TxStatus::Active);
523 let tx = super::ChannelTx {
524 sender,
525 dest: dest.clone(),
526 status,
527 };
528
529 let num_streams = links.len();
530
531 crate::init::get_runtime().spawn(async move {
532 let (queue_tx, queue_rx) =
537 async_channel::bounded::<session::PendingMessage<M>>(num_streams * 8);
538
539 let unacked: Arc<
543 tokio::sync::Mutex<std::collections::BTreeMap<u64, session::QueuedMessage<M>>>,
544 > = Arc::new(tokio::sync::Mutex::new(std::collections::BTreeMap::new()));
545
546 let mut writer_handles: Vec<tokio::task::JoinHandle<()>> = Vec::with_capacity(num_streams);
547 let log_id = format!("session {}.{:016x}", dest, session_id.0);
548
549 for (i, link) in links.into_iter().enumerate() {
550 let dest = dest.clone();
551 let unacked = unacked.clone();
552 let queue_rx = queue_rx.clone();
553 let log_id = log_id.clone();
554
555 writer_handles.push(tokio::spawn(async move {
556 let mut session = Session::new(link);
557 let mut reconnect_backoff = ExponentialBackoffBuilder::new()
558 .with_initial_interval(Duration::from_millis(10))
559 .with_multiplier(2.0)
560 .with_randomization_factor(0.1)
561 .with_max_interval(Duration::from_secs(5))
562 .with_max_elapsed_time(None)
563 .build();
564
565 loop {
566 let connected = match session.connect().await {
567 Ok(s) => s,
568 Err(_) => {
569 tracing::info!(
570 dest = %dest, stream = i,
571 "multi-stream writer {} connect failed", i
572 );
573 break;
574 }
575 };
576 tracing::info!(
577 dest = %dest, stream = i, "multi-stream writer {} connected", i
578 );
579
580 let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
581 let connected_at = tokio::time::Instant::now();
582
583 let result: Result<(), session::SendLoopError> = async {
585 loop {
586 tokio::select! {
587 biased;
588
589 ack_result = stream.next() => {
590 match ack_result {
591 Ok(Some(buffer)) => {
592 let response = deserialize_response(buffer)
593 .map_err(|e| session::SendLoopError::Io(e.into()))?;
594 match response {
595 NetRxResponse::Ack(ack) => {
596 let mut guard = unacked.lock().await;
597 let retain: std::collections::BTreeMap<u64, session::QueuedMessage<M>> = guard.split_off(&(ack + 1));
599 let accepted = std::mem::replace(&mut *guard, retain);
600 drop(guard);
601 accepted.into_values().for_each(|queued| {
602 queued.completion.accept();
603 });
604 }
605 NetRxResponse::Reject(reason) => {
606 return Err(session::SendLoopError::Rejected(reason));
607 }
608 NetRxResponse::Closed => {
609 return Err(session::SendLoopError::ServerClosed);
610 }
611 }
612 }
613 Ok(None) => return Ok(()),
614 Err(e) => return Err(session::SendLoopError::Io(e.into())),
615 }
616 }
617
618 msg = queue_rx.recv() => {
619 let pending = match msg {
620 Ok(m) => m,
621 Err(_) => return Ok(()),
623 };
624 let session::PendingMessage {
625 seq,
626 message,
627 received_at,
628 completion,
629 } = pending;
630 let frame = Frame::Message(seq, message);
631 let serialized = match serde_multipart::serialize_bincode(&frame) {
632 Ok(m) => m,
633 Err(e) => {
634 tracing::error!(
635 "{log_id}: serialization error: {e}"
636 );
637 completion.accept();
638 continue;
639 }
640 };
641 let mut queued = session::QueuedMessage {
642 seq,
643 message: serialized,
644 received_at,
645 sent_at: None,
646 completion,
647 };
648 let framed = queued.message.clone().framed();
649 stream.write(framed).drive().await.map_err(|e| {
650 session::SendLoopError::Io(e.into())
651 })?;
652 queued.sent_at = Some(tokio::time::Instant::now());
653 unacked.lock().await.insert(queued.seq, queued);
654 }
655 }
656 }
657 }
658 .await;
659
660 session = connected.release();
661
662 if connected_at.elapsed() > Duration::from_secs(1) {
663 reconnect_backoff.reset();
664 }
665
666 match result {
667 Ok(()) => {
668 if queue_rx.is_closed() {
669 break;
671 }
672 if let Some(delay) = reconnect_backoff.next_backoff() {
673 tokio::time::sleep(delay).await;
674 }
675 }
676 Err(ref e) => {
677 if log_send_error(e, &dest, session_id.0, "multi-stream", &LinkStatus::NeverConnected) {
678 break;
679 }
680 if let Some(delay) = reconnect_backoff.next_backoff() {
681 tokio::time::sleep(delay).await;
682 }
683 }
684 }
685 }
686
687 tracing::info!(
688 dest = %dest,
689 stream = i,
690 "multi-stream writer {} shutting down",
691 i,
692 );
693 }));
694 }
695
696 let cleanup_rx = queue_rx.clone();
697 drop(queue_rx);
700
701 let mut next_seq = 0u64;
705
706 tracing::info!(
707 %dest, session = %log_id, num_streams,
708 "multi-stream dispatcher started"
709 );
710
711 while let Some((message, completion, received_at)) = receiver.recv().await {
712 let pending = session::PendingMessage {
713 seq: next_seq,
714 message,
715 received_at,
716 completion,
717 };
718 next_seq += 1;
719
720 if let Err(async_channel::SendError(pending)) = queue_tx.send(pending).await {
721 pending.completion.accept();
722 break;
724 }
725 }
726
727 drop(queue_tx);
729 for handle in writer_handles {
730 let _ = handle.await;
731 }
732 while let Ok(pending) = cleanup_rx.try_recv() {
733 pending.completion.accept();
734 }
735 for queued in Arc::into_inner(unacked)
736 .expect("writer handles should drop their unacked clones")
737 .into_inner()
738 .into_values()
739 {
740 queued.completion.accept();
741 }
742
743 let reason = format!("{log_id}: dispatcher closed");
744 let _ = notify.send(TxStatus::Closed(CloseReason::Other(reason)));
745 });
746
747 tx
748}
749
750fn spawn_inner<M: RemoteMessage>(link: impl Link) -> super::ChannelTx<M> {
751 let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
752 let dest = link.dest();
753 let session_id = link.link_id();
754 let (notify, status) = watch::channel(TxStatus::Active);
755 let tx = super::ChannelTx {
756 sender,
757 dest: dest.clone(),
758 status,
759 };
760 crate::init::get_runtime().spawn(async move {
761 let mut session = Session::new(link);
762 let log_id = format!("session {}.{:016x}", dest, session_id.0);
763 let mut deliveries = session::Deliveries {
764 outbox: session::Outbox::new(log_id.clone(), dest.clone(), session_id.0),
765 unacked: session::Unacked::new(None, log_id.clone()),
766 };
767 let mut receiver = receiver;
768
769 match receiver.recv().await {
771 Some(msg) => {
772 if let Err(err) = deliveries.outbox.push_back(msg) {
773 tracing::error!(
774 dest = %dest,
775 session_id = session_id.0,
776 error = %err,
777 "failed to push message to outbox"
778 );
779 let _ = notify.send(TxStatus::Closed(CloseReason::Other(
780 "failed to push to outbox".into(),
781 )));
782 return;
783 }
784 }
785 None => {
786 let _ = notify.send(TxStatus::Closed(CloseReason::Other(
787 "sender dropped".into(),
788 )));
789 return;
790 }
791 }
792
793 let mut reconnect_backoff = ExponentialBackoffBuilder::new()
794 .with_initial_interval(Duration::from_millis(10))
795 .with_multiplier(2.0)
796 .with_randomization_factor(0.1)
797 .with_max_interval(Duration::from_secs(5))
798 .with_max_elapsed_time(None)
799 .build();
800
801 let mut link_status = LinkStatus::NeverConnected;
802
803 let reason: CloseReason = 'outer: loop {
804 let connected = match deliveries.expiry_time() {
805 Some(deadline) => match session.connect_by(deadline).await {
806 Ok(s) => s,
807 Err(_) => {
808 let timeout =
809 hyperactor_config::global::get(config::MESSAGE_DELIVERY_TIMEOUT);
810 let error_msg = if deliveries.outbox.is_expired(timeout) {
811 format!("failed to deliver message within timeout {timeout:?}; {link_status}")
812 } else {
813 format!(
814 "failed to receive ack within timeout {timeout:?}; \
815 link is currently broken; {link_status}",
816 )
817 };
818 tracing::error!(
819 dest = %dest, session_id = session_id.0, "{}", error_msg
820 );
821 break 'outer CloseReason::Other(format!("{log_id}: {error_msg}"));
822 }
823 },
824 None => match session.connect().await {
825 Ok(s) => s,
826 Err(_) => break 'outer CloseReason::Other("session shut down".into()),
827 },
828 };
829
830 metrics::CHANNEL_CONNECTIONS.add(
831 1,
832 hyperactor_telemetry::kv_pairs!(
833 "transport" => dest.transport().to_string(),
834 "mode" => "simplex",
835 "reason" => "link connected",
836 ),
837 );
838
839 if !deliveries.unacked.is_empty() {
840 metrics::CHANNEL_RECONNECTIONS.add(
841 1,
842 hyperactor_telemetry::kv_pairs!(
843 "dest" => dest.to_string(),
844 "transport" => dest.transport().to_string(),
845 "mode" => "simplex",
846 "reason" => "reconnect_with_unacked",
847 ),
848 );
849 }
850 deliveries.requeue_unacked();
851
852 link_status.connected();
853 let connected_at = tokio::time::Instant::now();
854
855 let result = {
856 let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
857 session::send_connected(&stream, &mut deliveries, &mut receiver).await
858 };
859 session = connected.release();
860
861 link_status.disconnected();
862
863 if connected_at.elapsed() > Duration::from_secs(1) {
866 reconnect_backoff.reset();
867 }
868
869 match result {
870 Ok(()) => {
871 if let Some(delay) = reconnect_backoff.next_backoff() {
873 tracing::info!(
874 dest = %dest,
875 session_id = session_id.0,
876 delay_ms = delay.as_millis() as u64,
877 "send_connected returned EOF, reconnecting after backoff; {link_status}"
878 );
879 tokio::time::sleep(delay).await;
880 }
881 }
882 Err(ref e) => {
883 if log_send_error(e, &dest, session_id.0, "simplex", &link_status) {
884 break 'outer classify_send_loop_error(e, &log_id);
885 }
886 if let Some(delay) = reconnect_backoff.next_backoff() {
888 tracing::info!(
889 dest = %dest,
890 session_id = session_id.0,
891 delay_ms = delay.as_millis() as u64,
892 error = %e,
893 "send_connected returned recoverable error, reconnecting after backoff; {link_status}"
894 );
895 tokio::time::sleep(delay).await;
896 }
897 }
898 }
899 };
900
901 tracing::info!(
902 dest = %dest, session_id = session_id.0, "ChannelTx closing: {reason}"
903 );
904
905 let send_error_reason = match &reason {
906 CloseReason::OversizedFrame { size, max } => Some(SendErrorReason::OversizedFrame {
907 len: *size,
908 max: *max,
909 }),
910 _ => Some(SendErrorReason::Other(reason.to_string())),
911 };
912 receiver.close();
913 deliveries
914 .unacked
915 .deque
916 .drain(..)
917 .chain(deliveries.outbox.deque.drain(..))
918 .for_each(|queued| queued.try_return(send_error_reason.clone()));
919 while let Ok((msg, completion, _)) = receiver.try_recv() {
920 completion.reject(SendError {
921 error: ChannelError::Closed,
922 message: msg,
923 reason: send_error_reason.clone(),
924 });
925 }
926
927 let _ = notify.send(TxStatus::Closed(reason));
928 }.instrument(tracing::debug_span!("net tx loop")));
929 tx
930}
931
932#[derive(Debug)]
935pub(crate) enum NetLink {
936 Tcp(tcp::TcpLink),
937 Unix(unix::UnixLink),
938 Tls(tls::TlsLink),
939 Quic(quic::QuicLink),
940 Local(local::stream::LocalLink),
941}
942
943pub(crate) fn link(
948 addr: ChannelAddr,
949 session_id: SessionId,
950 stream_id: u8,
951 kind: ProtocolKind,
952) -> Result<NetLink, ClientError> {
953 match addr {
954 ChannelAddr::Tcp(socket_addr) => Ok(NetLink::Tcp(tcp::link(
955 socket_addr,
956 session_id,
957 stream_id,
958 kind,
959 ))),
960 ChannelAddr::Unix(unix_addr) => Ok(NetLink::Unix(unix::link(
961 unix_addr, session_id, stream_id, kind,
962 ))),
963 ChannelAddr::Local(port) => {
964 local::stream::check(port)?;
965 Ok(NetLink::Local(local::stream::link(
966 port, session_id, stream_id, kind,
967 )))
968 }
969 ChannelAddr::Tls(tls_addr) => Ok(NetLink::Tls(tls::link(
970 tls_addr, session_id, stream_id, kind,
971 )?)),
972 ChannelAddr::MetaTls(meta_addr) => Ok(NetLink::Tls(meta::link(
973 meta_addr, session_id, stream_id, kind,
974 )?)),
975 ChannelAddr::Quic(quic_addr) => Ok(NetLink::Quic(quic::link(
976 quic_addr,
977 quic::QuicAddrType::Quic,
978 session_id,
979 stream_id,
980 kind,
981 )?)),
982 ChannelAddr::MetaQuic(meta_addr) => Ok(NetLink::Quic(quic::link(
983 meta_addr,
984 quic::QuicAddrType::MetaQuic,
985 session_id,
986 stream_id,
987 kind,
988 )?)),
989 other => Err(ClientError::Connect(
990 other,
991 std::io::Error::other("unsupported transport"),
992 "unsupported transport".into(),
993 )),
994 }
995}
996
997#[async_trait]
998impl Link for NetLink {
999 type Stream = Box<dyn Stream>;
1000
1001 fn dest(&self) -> ChannelAddr {
1002 match self {
1003 Self::Tcp(l) => l.dest(),
1004 Self::Unix(l) => l.dest(),
1005 Self::Tls(l) => l.dest(),
1006 Self::Quic(l) => l.dest(),
1007 Self::Local(l) => l.dest(),
1008 }
1009 }
1010
1011 fn link_id(&self) -> SessionId {
1012 match self {
1013 Self::Tcp(l) => l.link_id(),
1014 Self::Unix(l) => l.link_id(),
1015 Self::Tls(l) => l.link_id(),
1016 Self::Quic(l) => l.link_id(),
1017 Self::Local(l) => l.link_id(),
1018 }
1019 }
1020
1021 async fn next(&mut self) -> Result<Box<dyn Stream>, ClientError> {
1022 match self {
1023 Self::Tcp(l) => Ok(Box::new(l.next().await?)),
1024 Self::Unix(l) => Ok(Box::new(l.next().await?)),
1025 Self::Tls(l) => Ok(Box::new(l.next().await?)),
1026 Self::Quic(l) => Ok(Box::new(l.next().await?)),
1027 Self::Local(l) => Ok(Box::new(l.next().await?)),
1028 }
1029 }
1030}
1031
1032#[async_trait]
1037pub(crate) trait Listener: Send + Unpin + 'static {
1038 type Stream: Stream;
1040
1041 async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError>;
1043}
1044
1045#[derive(Debug)]
1050pub(crate) enum NetListener {
1051 Tcp(tcp::TcpSocketListener),
1052 Unix(unix::UnixSocketListener),
1053 Quic(quic::QuicSocketListener),
1054 Local(local::stream::LocalListener),
1055}
1056
1057#[async_trait]
1058impl Listener for NetListener {
1059 type Stream = Box<dyn Stream>;
1060
1061 async fn accept(&mut self) -> Result<(Box<dyn Stream>, ChannelAddr), ServerError> {
1062 match self {
1063 Self::Tcp(l) => {
1064 let (stream, addr) = l.accept().await?;
1065 Ok((Box::new(stream), addr))
1066 }
1067 Self::Unix(l) => {
1068 let (stream, addr) = l.accept().await?;
1069 Ok((Box::new(stream), addr))
1070 }
1071 Self::Quic(l) => {
1072 let (stream, addr) = l.accept().await?;
1073 Ok((Box::new(stream), addr))
1074 }
1075 Self::Local(l) => {
1076 let (stream, addr) = l.accept().await?;
1077 Ok((Box::new(stream), addr))
1078 }
1079 }
1080 }
1081}
1082
1083pub(crate) fn listen_with_prebound(
1087 addr: ChannelAddr,
1088 prebound: Option<std::net::TcpListener>,
1089) -> Result<(NetListener, ChannelAddr), ServerError> {
1090 match addr {
1091 ChannelAddr::Tcp(socket_addr) => {
1092 let std_listener = match prebound {
1093 Some(l) => l,
1094 None => std::net::TcpListener::bind(socket_addr)
1095 .map_err(|err| ServerError::Listen(ChannelAddr::Tcp(socket_addr), err))?,
1096 };
1097 std_listener
1098 .set_nonblocking(true)
1099 .map_err(|e| ServerError::Listen(ChannelAddr::Tcp(socket_addr), e))?;
1100 let tokio_listener = tokio::net::TcpListener::from_std(std_listener)
1101 .map_err(|e| ServerError::Listen(ChannelAddr::Tcp(socket_addr), e))?;
1102 let local_addr = tokio_listener
1103 .local_addr()
1104 .map_err(|err| ServerError::Resolve(ChannelAddr::Tcp(socket_addr), err))?;
1105 let listener = tcp::TcpSocketListener {
1106 inner: tokio_listener,
1107 addr: local_addr,
1108 };
1109 Ok((NetListener::Tcp(listener), ChannelAddr::Tcp(local_addr)))
1110 }
1111 ChannelAddr::Unix(ref unix_addr) => {
1112 use std::os::unix::net::UnixDatagram as StdUnixDatagram;
1113 use std::os::unix::net::UnixListener as StdUnixListener;
1114
1115 let caddr = addr.clone();
1116 let maybe_listener = match unix_addr {
1117 unix::SocketAddr::Bound(sock_addr) => StdUnixListener::bind_addr(sock_addr),
1118 unix::SocketAddr::Unbound => StdUnixDatagram::unbound()
1119 .and_then(|u| u.local_addr())
1120 .and_then(|uaddr| StdUnixListener::bind_addr(&uaddr)),
1121 };
1122 let std_listener =
1123 maybe_listener.map_err(|err| ServerError::Listen(caddr.clone(), err))?;
1124 std_listener
1125 .set_nonblocking(true)
1126 .map_err(|err| ServerError::Listen(caddr.clone(), err))?;
1127 let local_addr = std_listener
1128 .local_addr()
1129 .map_err(|err| ServerError::Resolve(caddr.clone(), err))?;
1130 let tokio_listener = tokio::net::UnixListener::from_std(std_listener)
1131 .map_err(|err| ServerError::Io(caddr, err))?;
1132 let bound_addr = unix::SocketAddr::new(local_addr);
1133 let listener = unix::UnixSocketListener {
1134 inner: tokio_listener,
1135 addr: bound_addr.clone(),
1136 };
1137 Ok((NetListener::Unix(listener), ChannelAddr::Unix(bound_addr)))
1138 }
1139 ChannelAddr::Local(_) => {
1140 let (listener, addr) = local::stream::listen(addr, prebound)?;
1141 Ok((NetListener::Local(listener), addr))
1142 }
1143 addr @ (ChannelAddr::Tls(_) | ChannelAddr::MetaTls(_)) => {
1144 let is_meta = matches!(addr, ChannelAddr::MetaTls(_));
1145 let tls_addr = match addr {
1146 ChannelAddr::Tls(a) | ChannelAddr::MetaTls(a) => a,
1147 _ => unreachable!(),
1148 };
1149 let TlsAddr { hostname, port } = tls_addr;
1150 let make_channel_addr = |h: &str, p: Port| {
1151 if is_meta {
1152 ChannelAddr::MetaTls(TlsAddr::new(h, p))
1153 } else {
1154 ChannelAddr::Tls(TlsAddr::new(h, p))
1155 }
1156 };
1157
1158 let addrs: Vec<core::net::SocketAddr> = (hostname.as_ref(), port)
1159 .to_socket_addrs()
1160 .map_err(|err| ServerError::Resolve(make_channel_addr(&hostname, port), err))?
1161 .collect();
1162
1163 if addrs.is_empty() {
1164 return Err(ServerError::Resolve(
1165 make_channel_addr(&hostname, port),
1166 std::io::Error::other("no available socket addr"),
1167 ));
1168 }
1169
1170 let channel_addr = make_channel_addr(&hostname, port);
1171 let std_listener = match prebound {
1172 Some(l) => l,
1173 None => std::net::TcpListener::bind(&addrs[..])
1174 .map_err(|err| ServerError::Listen(channel_addr.clone(), err))?,
1175 };
1176 std_listener
1177 .set_nonblocking(true)
1178 .map_err(|e| ServerError::Listen(channel_addr.clone(), e))?;
1179 let tokio_listener = tokio::net::TcpListener::from_std(std_listener)
1180 .map_err(|e| ServerError::Listen(channel_addr.clone(), e))?;
1181 let local_addr = tokio_listener
1182 .local_addr()
1183 .map_err(|err| ServerError::Resolve(channel_addr, err))?;
1184 let listener = tcp::TcpSocketListener {
1185 inner: tokio_listener,
1186 addr: local_addr,
1187 };
1188 Ok((
1189 NetListener::Tcp(listener),
1190 make_channel_addr(&hostname, local_addr.port()),
1191 ))
1192 }
1193 addr @ (ChannelAddr::Quic(_) | ChannelAddr::MetaQuic(_)) => {
1194 if prebound.is_some() {
1195 return Err(ServerError::Listen(
1196 addr,
1197 std::io::Error::other("pre-opened listener not supported for QUIC transport"),
1198 ));
1199 }
1200 let addr_type = match addr {
1201 ChannelAddr::Quic(_) => quic::QuicAddrType::Quic,
1202 ChannelAddr::MetaQuic(_) => quic::QuicAddrType::MetaQuic,
1203 _ => unreachable!(),
1204 };
1205 let tls_addr = match addr {
1206 ChannelAddr::Quic(a) | ChannelAddr::MetaQuic(a) => a,
1207 _ => unreachable!(),
1208 };
1209 let (listener, bound_addr) = quic::listen(tls_addr, addr_type)?;
1210 Ok((NetListener::Quic(listener), bound_addr))
1211 }
1212 ChannelAddr::Alias { dial_to, bind_to } => {
1213 let (listener, _bound_addr) = listen_with_prebound(*bind_to, prebound)?;
1221 Ok((listener, *dial_to))
1222 }
1223 }
1224}
1225
1226#[derive(Debug, Serialize, Deserialize, EnumAsInner, PartialEq)]
1228pub(super) enum Frame<M> {
1229 Message(u64, M),
1231}
1232
1233#[derive(Debug, Serialize, Deserialize, EnumAsInner)]
1234pub(super) enum NetRxResponse {
1235 Ack(u64),
1236 Reject(String),
1238 Closed,
1240}
1241
1242pub(super) fn serialize_response(
1243 response: NetRxResponse,
1244) -> Result<Bytes, bincode::error::EncodeError> {
1245 bincode::serde::encode_to_vec(&response, bincode::config::legacy()).map(|bytes| bytes.into())
1246}
1247
1248pub(super) fn deserialize_response(
1249 data: Bytes,
1250) -> Result<NetRxResponse, bincode::error::DecodeError> {
1251 bincode::serde::decode_from_slice(&data, bincode::config::legacy()).map(|(v, _)| v)
1252}
1253
1254#[derive(Debug, thiserror::Error)]
1256pub enum ServerError {
1257 #[error("io: {1}")]
1259 Io(ChannelAddr, #[source] std::io::Error),
1260 #[error("listen: {0} {1}")]
1262 Listen(ChannelAddr, #[source] std::io::Error),
1263 #[error("resolve: {0} {1}")]
1265 Resolve(ChannelAddr, #[source] std::io::Error),
1266 #[error("internal: {0} {1}")]
1268 Internal(ChannelAddr, #[source] anyhow::Error),
1269}
1270
1271#[derive(thiserror::Error, Debug)]
1272pub enum ClientError {
1273 #[error("connection to {0} failed: {1}: {2}")]
1274 Connect(ChannelAddr, std::io::Error, String),
1275 #[error("connection to {0} failed after {1:?} of retries: {2}")]
1276 ConnectTimeout(ChannelAddr, Duration, #[source] std::io::Error),
1277 #[error("unable to resolve address: {0}")]
1278 Resolve(ChannelAddr),
1279 #[error("io: {0} {1}")]
1280 Io(ChannelAddr, std::io::Error),
1281 #[error("send {0}: serialize: {1}")]
1282 Serialize(ChannelAddr, bincode::error::EncodeError),
1283 #[error("invalid address: {0}")]
1284 InvalidAddress(String),
1285}
1286
1287#[cfg(test)]
1290pub(super) fn is_net_addr(addr: &ChannelAddr) -> bool {
1291 matches!(
1292 addr.transport(),
1293 ChannelTransport::Tcp(_)
1294 | ChannelTransport::MetaTls(_)
1295 | ChannelTransport::Tls
1296 | ChannelTransport::Quic
1297 | ChannelTransport::MetaQuic(_)
1298 | ChannelTransport::Unix
1299 | ChannelTransport::Local
1300 )
1301}
1302
1303pub(crate) mod unix {
1304
1305 use core::str;
1306 use std::os::unix::net::SocketAddr as StdSocketAddr;
1307 use std::os::unix::net::UnixStream as StdUnixStream;
1308
1309 use rand::RngExt as _;
1310 use rand::distr::Alphanumeric;
1311 use tokio::net::UnixListener;
1312 use tokio::net::UnixStream;
1313
1314 use super::*;
1315
1316 #[derive(Debug)]
1317 pub(crate) struct UnixLink {
1318 pub(super) addr: SocketAddr,
1319 pub(super) session_id: SessionId,
1320 pub(super) stream_id: u8,
1321 pub(super) kind: ProtocolKind,
1322 }
1323
1324 #[async_trait]
1325 impl Link for UnixLink {
1326 type Stream = UnixStream;
1327
1328 fn dest(&self) -> ChannelAddr {
1329 ChannelAddr::Unix(self.addr.clone())
1330 }
1331
1332 fn link_id(&self) -> SessionId {
1333 self.session_id
1334 }
1335
1336 async fn next(&mut self) -> Result<Self::Stream, ClientError> {
1337 let session_id = self.session_id;
1338 let sock_addr = match &self.addr {
1339 SocketAddr::Bound(a) => a,
1340 SocketAddr::Unbound => return Err(ClientError::Resolve(self.dest())),
1341 };
1342 let mut backoff = ExponentialBackoffBuilder::new()
1343 .with_initial_interval(Duration::from_millis(1))
1344 .with_multiplier(2.0)
1345 .with_randomization_factor(0.1)
1346 .with_max_interval(Duration::from_millis(1000))
1347 .with_max_elapsed_time(None)
1348 .build();
1349 loop {
1350 match StdUnixStream::connect_addr(sock_addr) {
1351 Ok(std_stream) => {
1352 std_stream
1353 .set_nonblocking(true)
1354 .map_err(|err| ClientError::Io(self.dest(), err))?;
1355 let mut stream = UnixStream::from_std(std_stream)
1356 .map_err(|err| ClientError::Io(self.dest(), err))?;
1357 write_link_init(&mut stream, session_id, self.stream_id, self.kind)
1358 .await
1359 .map_err(|err| ClientError::Io(self.dest(), err))?;
1360 return Ok(stream);
1361 }
1362 Err(err) => {
1363 tracing::debug!(error = %err, "unix connect failed, backing off");
1364 if let Some(delay) = backoff.next_backoff() {
1365 tokio::time::sleep(delay).await;
1366 }
1367 }
1368 }
1369 }
1370 }
1371 }
1372
1373 #[derive(Debug)]
1375 pub(crate) struct UnixSocketListener {
1376 pub(super) inner: UnixListener,
1377 pub(super) addr: SocketAddr,
1378 }
1379
1380 #[async_trait]
1381 impl super::Listener for UnixSocketListener {
1382 type Stream = UnixStream;
1383
1384 async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError> {
1385 let (stream, peer_addr) = self
1386 .inner
1387 .accept()
1388 .await
1389 .map_err(|err| ServerError::Io(ChannelAddr::Unix(self.addr.clone()), err))?;
1390 let std_addr: StdSocketAddr = peer_addr.into();
1392 Ok((stream, ChannelAddr::Unix(SocketAddr::new(std_addr))))
1393 }
1394 }
1395
1396 pub(crate) fn link(
1398 addr: SocketAddr,
1399 session_id: SessionId,
1400 stream_id: u8,
1401 kind: ProtocolKind,
1402 ) -> UnixLink {
1403 UnixLink {
1404 addr,
1405 session_id,
1406 stream_id,
1407 kind,
1408 }
1409 }
1410
1411 #[derive(Clone, Debug)]
1413 pub enum SocketAddr {
1414 Bound(Box<StdSocketAddr>),
1415 Unbound,
1416 }
1417
1418 impl PartialOrd for SocketAddr {
1419 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1420 Some(self.cmp(other))
1421 }
1422 }
1423
1424 impl Ord for SocketAddr {
1425 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1426 self.to_string().cmp(&other.to_string())
1427 }
1428 }
1429
1430 impl<'de> Deserialize<'de> for SocketAddr {
1431 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1432 where
1433 D: serde::Deserializer<'de>,
1434 {
1435 let s = String::deserialize(deserializer)?;
1436 Self::from_str(&s).map_err(D::Error::custom)
1437 }
1438 }
1439
1440 impl Serialize for SocketAddr {
1441 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1442 where
1443 S: serde::Serializer,
1444 {
1445 serializer.serialize_str(String::from(self).as_str())
1446 }
1447 }
1448
1449 impl From<&SocketAddr> for String {
1450 fn from(value: &SocketAddr) -> Self {
1451 match value {
1452 SocketAddr::Bound(addr) => match addr.as_pathname() {
1453 Some(path) => path
1454 .to_str()
1455 .expect("unable to get str for path")
1456 .to_string(),
1457 #[cfg(target_os = "linux")]
1458 _ => match addr.as_abstract_name() {
1459 Some(name) => format!("@{}", String::from_utf8_lossy(name)),
1460 _ => String::from("(unnamed)"),
1461 },
1462 #[cfg(not(target_os = "linux"))]
1463 _ => String::from("(unnamed)"),
1464 },
1465 SocketAddr::Unbound => String::from("(unbound)"),
1466 }
1467 }
1468 }
1469
1470 impl FromStr for SocketAddr {
1471 type Err = anyhow::Error;
1472
1473 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1474 match s {
1475 "" => {
1476 let random_string = rand::rng()
1479 .sample_iter(&Alphanumeric)
1480 .take(24)
1481 .map(char::from)
1482 .collect::<String>();
1483 SocketAddr::from_abstract_name(&random_string)
1484 }
1485 name if name.starts_with("@") => {
1487 SocketAddr::from_abstract_name(name.strip_prefix("@").unwrap())
1488 }
1489 path => SocketAddr::from_pathname(path),
1490 }
1491 }
1492 }
1493
1494 impl Eq for SocketAddr {}
1495 impl std::hash::Hash for SocketAddr {
1496 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1497 String::from(self).hash(state);
1498 }
1499 }
1500 impl PartialEq for SocketAddr {
1501 fn eq(&self, other: &Self) -> bool {
1502 match (self, other) {
1503 (Self::Bound(saddr), Self::Bound(oaddr)) => {
1504 if saddr.is_unnamed() || oaddr.is_unnamed() {
1505 return false;
1506 }
1507
1508 #[cfg(target_os = "linux")]
1509 {
1510 saddr.as_pathname() == oaddr.as_pathname()
1511 && saddr.as_abstract_name() == oaddr.as_abstract_name()
1512 }
1513 #[cfg(not(target_os = "linux"))]
1514 {
1515 saddr.as_pathname() == oaddr.as_pathname()
1517 }
1518 }
1519 (Self::Unbound, _) | (_, Self::Unbound) => false,
1520 }
1521 }
1522 }
1523
1524 impl fmt::Display for SocketAddr {
1525 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1526 match self {
1527 Self::Bound(addr) => match addr.as_pathname() {
1528 Some(path) => {
1529 write!(f, "{}", path.to_string_lossy())
1530 }
1531 #[cfg(target_os = "linux")]
1532 _ => match addr.as_abstract_name() {
1533 Some(name) => {
1534 if name.starts_with(b"@") {
1535 return write!(f, "{}", String::from_utf8_lossy(name));
1536 }
1537 write!(f, "@{}", String::from_utf8_lossy(name))
1538 }
1539 _ => write!(f, "(unnamed)"),
1540 },
1541 #[cfg(not(target_os = "linux"))]
1542 _ => write!(f, "(unnamed)"),
1543 },
1544 Self::Unbound => write!(f, "(unbound)"),
1545 }
1546 }
1547 }
1548
1549 impl SocketAddr {
1550 pub fn new(addr: StdSocketAddr) -> Self {
1552 Self::Bound(Box::new(addr))
1553 }
1554
1555 #[cfg(target_os = "linux")]
1558 pub fn from_abstract_name(name: &str) -> anyhow::Result<Self> {
1559 Ok(Self::new(StdSocketAddr::from_abstract_name(
1560 name.strip_prefix("@").unwrap_or(name),
1561 )?))
1562 }
1563
1564 #[cfg(not(target_os = "linux"))]
1565 pub fn from_abstract_name(name: &str) -> anyhow::Result<Self> {
1566 let name = name.strip_prefix("@").unwrap_or(name);
1568 let path = Self::abstract_to_filesystem_path(name);
1569 Self::from_pathname(&path.to_string_lossy())
1570 }
1571
1572 #[cfg(not(target_os = "linux"))]
1573 fn abstract_to_filesystem_path(abstract_name: &str) -> std::path::PathBuf {
1574 use std::collections::hash_map::DefaultHasher;
1575 use std::hash::Hash;
1576 use std::hash::Hasher;
1577
1578 let mut hasher = DefaultHasher::new();
1580 abstract_name.hash(&mut hasher);
1581 let hash = hasher.finish();
1582
1583 let process_id = std::process::id();
1585
1586 std::path::PathBuf::from(format!("/tmp/hyperactor_{}_{:x}", process_id, hash))
1588 }
1589
1590 pub fn from_pathname(name: &str) -> anyhow::Result<Self> {
1592 Ok(Self::new(StdSocketAddr::from_pathname(name)?))
1593 }
1594 }
1595
1596 impl TryFrom<SocketAddr> for StdSocketAddr {
1597 type Error = anyhow::Error;
1598
1599 fn try_from(value: SocketAddr) -> Result<Self, Self::Error> {
1600 match value {
1601 SocketAddr::Bound(addr) => Ok(*addr),
1602 SocketAddr::Unbound => Err(anyhow::anyhow!(
1603 "std::os::unix::SocketAddr must be a bound address"
1604 )),
1605 }
1606 }
1607 }
1608}
1609
1610pub(crate) mod tcp {
1611 use tokio::net::TcpListener;
1612 use tokio::net::TcpStream;
1613
1614 use super::*;
1615
1616 #[derive(Debug)]
1617 pub(crate) struct TcpLink {
1618 pub(super) addr: SocketAddr,
1619 pub(super) session_id: SessionId,
1620 pub(super) stream_id: u8,
1621 pub(super) kind: ProtocolKind,
1622 }
1623
1624 #[async_trait]
1625 impl Link for TcpLink {
1626 type Stream = TcpStream;
1627
1628 fn dest(&self) -> ChannelAddr {
1629 ChannelAddr::Tcp(self.addr)
1630 }
1631
1632 fn link_id(&self) -> SessionId {
1633 self.session_id
1634 }
1635
1636 async fn next(&mut self) -> Result<Self::Stream, ClientError> {
1637 let session_id = self.session_id;
1638 let reconnect_timeout =
1639 hyperactor_config::global::get(config::CHANNEL_RECONNECT_TIMEOUT);
1640 let mut backoff = ExponentialBackoffBuilder::new()
1641 .with_initial_interval(Duration::from_millis(1))
1642 .with_multiplier(2.0)
1643 .with_randomization_factor(0.1)
1644 .with_max_interval(Duration::from_millis(1000))
1645 .with_max_elapsed_time(Some(reconnect_timeout))
1646 .build();
1647 loop {
1648 match TcpStream::connect(&self.addr).await {
1649 Ok(mut stream) => {
1650 stream.set_nodelay(true).map_err(|err| {
1651 ClientError::Connect(
1652 self.dest(),
1653 err,
1654 "cannot disable Nagle algorithm".to_string(),
1655 )
1656 })?;
1657 set_tcp_keepalive(&stream);
1658 set_tcp_congestion(&stream);
1659 write_link_init(&mut stream, session_id, self.stream_id, self.kind)
1660 .await
1661 .map_err(|err| ClientError::Io(self.dest(), err))?;
1662 return Ok(stream);
1663 }
1664 Err(err) => {
1665 tracing::debug!(error = %err, "tcp connect failed, backing off");
1666 match backoff.next_backoff() {
1667 Some(delay) => tokio::time::sleep(delay).await,
1668 None => {
1669 return Err(ClientError::ConnectTimeout(
1670 self.dest(),
1671 reconnect_timeout,
1672 err,
1673 ));
1674 }
1675 }
1676 }
1677 }
1678 }
1679 }
1680 }
1681
1682 #[derive(Debug)]
1684 pub(crate) struct TcpSocketListener {
1685 pub(super) inner: TcpListener,
1686 pub(super) addr: SocketAddr,
1687 }
1688
1689 #[async_trait]
1690 impl super::Listener for TcpSocketListener {
1691 type Stream = TcpStream;
1692
1693 async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError> {
1694 let (stream, peer_addr) = self
1695 .inner
1696 .accept()
1697 .await
1698 .map_err(|err| ServerError::Io(ChannelAddr::Tcp(self.addr), err))?;
1699 stream
1700 .set_nodelay(true)
1701 .map_err(|err| ServerError::Io(ChannelAddr::Tcp(self.addr), err))?;
1702 set_tcp_keepalive(&stream);
1703 set_tcp_congestion(&stream);
1704 Ok((stream, ChannelAddr::Tcp(peer_addr)))
1705 }
1706 }
1707
1708 pub(crate) fn link(
1710 addr: SocketAddr,
1711 session_id: SessionId,
1712 stream_id: u8,
1713 kind: ProtocolKind,
1714 ) -> TcpLink {
1715 TcpLink {
1716 addr,
1717 session_id,
1718 stream_id,
1719 kind,
1720 }
1721 }
1722}
1723
1724pub(crate) mod meta {
1726 use std::io;
1727 use std::path::PathBuf;
1728 use std::sync::Arc;
1729
1730 use anyhow::Result;
1731 use tokio_rustls::TlsAcceptor;
1732 use tokio_rustls::TlsConnector;
1733
1734 use super::*;
1735 use crate::config::Pem;
1736 use crate::config::PemBundle;
1737
1738 const THRIFT_TLS_SRV_CA_PATH_ENV: &str = "THRIFT_TLS_SRV_CA_PATH";
1739 const DEFAULT_SRV_CA_PATH: &str = "/var/facebook/rootcanal/ca.pem";
1740 const THRIFT_TLS_CL_CERT_PATH_ENV: &str = "THRIFT_TLS_CL_CERT_PATH";
1741 const THRIFT_TLS_CL_KEY_PATH_ENV: &str = "THRIFT_TLS_CL_KEY_PATH";
1742 const DEFAULT_SERVER_PEM_PATH: &str = "/var/facebook/x509_identities/server.pem";
1743
1744 #[allow(clippy::result_large_err)] pub(crate) fn parse(addr_string: &str) -> Result<ChannelAddr, ChannelError> {
1746 let parts = addr_string.rsplit_once(":");
1748 match parts {
1749 Some((hostname, port_str)) => {
1750 let Ok(port) = port_str.parse() else {
1751 return Err(ChannelError::InvalidAddress(addr_string.to_string()));
1752 };
1753 Ok(ChannelAddr::MetaTls(TlsAddr::new(hostname, port)))
1754 }
1755 _ => Err(ChannelError::InvalidAddress(addr_string.to_string())),
1756 }
1757 }
1758
1759 pub(super) fn get_server_pem_bundle() -> PemBundle {
1762 let ca_path = std::env::var_os(THRIFT_TLS_SRV_CA_PATH_ENV)
1763 .map(PathBuf::from)
1764 .unwrap_or_else(|| PathBuf::from(DEFAULT_SRV_CA_PATH));
1765 let server_pem_path = PathBuf::from(DEFAULT_SERVER_PEM_PATH);
1766 PemBundle {
1767 ca: Pem::File(ca_path),
1768 cert: Pem::File(server_pem_path.clone()),
1769 key: Pem::File(server_pem_path),
1770 }
1771 }
1772
1773 fn get_client_pem_bundle() -> Option<PemBundle> {
1776 let cert_path = std::env::var_os(THRIFT_TLS_CL_CERT_PATH_ENV)?;
1777 let key_path = std::env::var_os(THRIFT_TLS_CL_KEY_PATH_ENV)?;
1778 let ca_path = std::env::var_os(THRIFT_TLS_SRV_CA_PATH_ENV)
1779 .map(PathBuf::from)
1780 .unwrap_or_else(|| PathBuf::from(DEFAULT_SRV_CA_PATH));
1781 Some(PemBundle {
1782 ca: Pem::File(ca_path),
1783 cert: Pem::File(PathBuf::from(cert_path)),
1784 key: Pem::File(PathBuf::from(key_path)),
1785 })
1786 }
1787
1788 pub(crate) fn tls_acceptor(enforce_client_tls: bool) -> Result<TlsAcceptor> {
1790 Ok(TlsAcceptor::from(Arc::new(server_config(
1791 enforce_client_tls,
1792 )?)))
1793 }
1794
1795 pub(super) fn try_tls_connector() -> Result<TlsConnector> {
1801 tls_connector()
1802 }
1803
1804 fn tls_connector() -> Result<TlsConnector> {
1807 Ok(TlsConnector::from(Arc::new(client_config()?)))
1808 }
1809
1810 pub(super) fn server_config(enforce_client_tls: bool) -> Result<rustls::ServerConfig> {
1811 let bundle = get_server_pem_bundle();
1812 tls::server_config_from_bundle(&bundle, enforce_client_tls)
1813 }
1814
1815 pub(super) fn client_config() -> Result<rustls::ClientConfig> {
1816 Ok(if let Some(bundle) = get_client_pem_bundle() {
1817 tls::client_config_from_bundle(&bundle)?
1818 } else {
1819 let ca_path = std::env::var_os(THRIFT_TLS_SRV_CA_PATH_ENV)
1820 .map(PathBuf::from)
1821 .unwrap_or_else(|| PathBuf::from(DEFAULT_SRV_CA_PATH));
1822 let ca_pem = Pem::File(ca_path);
1823 tls::client_config_from_ca(&ca_pem)?
1824 })
1825 }
1826
1827 pub fn link(
1829 addr: TlsAddr,
1830 session_id: SessionId,
1831 stream_id: u8,
1832 kind: ProtocolKind,
1833 ) -> Result<tls::TlsLink, ClientError> {
1834 let connector = tls_connector().map_err(|e| {
1835 ClientError::Connect(
1836 ChannelAddr::MetaTls(addr.clone()),
1837 io::Error::other(e.to_string()),
1838 "failed to create TLS connector".to_string(),
1839 )
1840 })?;
1841 let TlsAddr { hostname, port } = addr;
1842 Ok(tls::TlsLink {
1843 hostname,
1844 port,
1845 connector,
1846 addr_type: tls::TlsAddrType::MetaTls,
1847 session_id,
1848 stream_id,
1849 kind,
1850 })
1851 }
1852}
1853
1854pub(crate) mod tls {
1856 use std::io;
1857 use std::io::BufReader;
1858 use std::sync::Arc;
1859
1860 use anyhow::Context;
1861 use anyhow::Result;
1862 use rustls::ClientConfig;
1863 use rustls::RootCertStore;
1864 use rustls::ServerConfig;
1865 use rustls::pki_types::CertificateDer;
1866 use rustls::pki_types::PrivateKeyDer;
1867 use rustls::pki_types::ServerName;
1868 use tokio::net::TcpStream;
1869 use tokio_rustls::TlsAcceptor;
1870 use tokio_rustls::TlsConnector;
1871 use tokio_rustls::client::TlsStream;
1872
1873 use super::*;
1874 use crate::channel::TlsAddr;
1875 use crate::config::Pem;
1876 use crate::config::PemBundle;
1877 use crate::config::TLS_CA;
1878 use crate::config::TLS_CERT;
1879 use crate::config::TLS_KEY;
1880
1881 #[derive(Debug, Clone, Copy)]
1883 pub(crate) enum TlsAddrType {
1884 Tls,
1885 MetaTls,
1886 }
1887
1888 #[allow(clippy::result_large_err)]
1890 pub(crate) fn parse(addr_string: &str) -> Result<ChannelAddr, ChannelError> {
1891 let parts = addr_string.rsplit_once(":");
1893 match parts {
1894 Some((hostname, port_str)) => {
1895 let Ok(port) = port_str.parse() else {
1896 return Err(ChannelError::InvalidAddress(addr_string.to_string()));
1897 };
1898 Ok(ChannelAddr::Tls(TlsAddr::new(hostname, port)))
1899 }
1900 _ => Err(ChannelError::InvalidAddress(addr_string.to_string())),
1901 }
1902 }
1903
1904 pub(super) fn load_certs(pem: &Pem) -> Result<Vec<CertificateDer<'static>>> {
1906 let mut reader = BufReader::new(pem.reader()?);
1907 let certs = rustls_pemfile::certs(&mut reader)
1908 .filter_map(Result::ok)
1909 .collect();
1910 Ok(certs)
1911 }
1912
1913 pub(super) fn load_key(pem: &Pem) -> Result<PrivateKeyDer<'static>> {
1915 let mut reader = BufReader::new(pem.reader()?);
1916 loop {
1917 break match rustls_pemfile::read_one(&mut reader)? {
1918 Some(rustls_pemfile::Item::Pkcs1Key(key)) => Ok(PrivateKeyDer::Pkcs1(key)),
1919 Some(rustls_pemfile::Item::Pkcs8Key(key)) => Ok(PrivateKeyDer::Pkcs8(key)),
1920 Some(rustls_pemfile::Item::Sec1Key(key)) => Ok(PrivateKeyDer::Sec1(key)),
1921 Some(_) => continue,
1922 None => anyhow::bail!("no private key found in TLS key file"),
1923 };
1924 }
1925 }
1926
1927 pub(super) fn build_root_store(ca_pem: &Pem) -> Result<RootCertStore> {
1929 let mut root_store = RootCertStore::empty();
1930 let certs = load_certs(ca_pem)?;
1931 root_store.add_parsable_certificates(certs);
1932 Ok(root_store)
1933 }
1934
1935 pub(super) fn get_pem_bundle() -> PemBundle {
1937 PemBundle {
1938 ca: hyperactor_config::global::get_cloned(TLS_CA),
1939 cert: hyperactor_config::global::get_cloned(TLS_CERT),
1940 key: hyperactor_config::global::get_cloned(TLS_KEY),
1941 }
1942 }
1943
1944 fn install_default_crypto_provider() {
1945 let _ = rustls::crypto::ring::default_provider().install_default();
1948 }
1949
1950 pub(super) fn server_config_from_bundle(
1953 bundle: &PemBundle,
1954 enforce_client_tls: bool,
1955 ) -> Result<ServerConfig> {
1956 install_default_crypto_provider();
1957
1958 let certs = load_certs(&bundle.cert).context("load TLS certificate")?;
1959 let key = load_key(&bundle.key).context("load TLS key")?;
1960 let root_store = build_root_store(&bundle.ca).context("build root cert store")?;
1961
1962 let config = ServerConfig::builder();
1963 let config = if enforce_client_tls {
1964 let client_verifier =
1966 rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
1967 .build()
1968 .map_err(|e| anyhow::anyhow!("failed to build client verifier: {}", e))?;
1969 config.with_client_cert_verifier(client_verifier)
1970 } else {
1971 config.with_no_client_auth()
1972 }
1973 .with_single_cert(certs, key)?;
1974
1975 Ok(config)
1976 }
1977
1978 pub(super) fn tls_acceptor_from_bundle(
1981 bundle: &PemBundle,
1982 enforce_client_tls: bool,
1983 ) -> Result<TlsAcceptor> {
1984 let config = server_config_from_bundle(bundle, enforce_client_tls)?;
1985 Ok(TlsAcceptor::from(Arc::new(config)))
1986 }
1987
1988 pub(crate) fn tls_acceptor() -> Result<TlsAcceptor> {
1990 tls_acceptor_from_bundle(&get_pem_bundle(), true)
1991 }
1992
1993 pub(super) fn client_config_from_ca(ca_pem: &Pem) -> Result<ClientConfig> {
1995 install_default_crypto_provider();
1996
1997 let root_store = build_root_store(ca_pem).context("build root cert store")?;
1998 Ok(ClientConfig::builder()
1999 .with_root_certificates(Arc::new(root_store))
2000 .with_no_client_auth())
2001 }
2002
2003 pub(super) fn client_config_from_bundle(bundle: &PemBundle) -> Result<ClientConfig> {
2005 install_default_crypto_provider();
2006
2007 let certs = load_certs(&bundle.cert).context("load TLS certificate")?;
2008 let key = load_key(&bundle.key).context("load TLS key")?;
2009 let root_store = build_root_store(&bundle.ca).context("build root cert store")?;
2010
2011 let config = ClientConfig::builder()
2012 .with_root_certificates(Arc::new(root_store))
2013 .with_client_auth_cert(certs, key)
2014 .context("configure client auth")?;
2015
2016 Ok(config)
2017 }
2018
2019 pub(super) fn tls_connector_from_bundle(bundle: &PemBundle) -> Result<TlsConnector> {
2021 let config = client_config_from_bundle(bundle)?;
2022 Ok(TlsConnector::from(Arc::new(config)))
2023 }
2024
2025 fn tls_connector() -> Result<TlsConnector> {
2027 tls_connector_from_bundle(&get_pem_bundle())
2028 }
2029
2030 pub(crate) struct TlsLink {
2032 pub(crate) hostname: Hostname,
2033 pub(crate) port: Port,
2034 pub(crate) connector: TlsConnector,
2035 pub(crate) addr_type: TlsAddrType,
2036 pub(crate) session_id: SessionId,
2037 pub(crate) stream_id: u8,
2038 pub(crate) kind: ProtocolKind,
2039 }
2040
2041 impl std::fmt::Debug for TlsLink {
2042 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2043 f.debug_struct("TlsLink")
2044 .field("hostname", &self.hostname)
2045 .field("port", &self.port)
2046 .field("addr_type", &self.addr_type)
2047 .finish()
2048 }
2049 }
2050
2051 #[async_trait]
2052 impl Link for TlsLink {
2053 type Stream = TlsStream<TcpStream>;
2054
2055 fn dest(&self) -> ChannelAddr {
2056 let addr = TlsAddr::new(self.hostname.clone(), self.port);
2057 match self.addr_type {
2058 TlsAddrType::Tls => ChannelAddr::Tls(addr),
2059 TlsAddrType::MetaTls => ChannelAddr::MetaTls(addr),
2060 }
2061 }
2062
2063 fn link_id(&self) -> SessionId {
2064 self.session_id
2065 }
2066
2067 async fn next(&mut self) -> Result<Self::Stream, ClientError> {
2068 let session_id = self.session_id;
2069 let server_name = ServerName::try_from(self.hostname.clone()).map_err(|e| {
2070 ClientError::Connect(
2071 self.dest(),
2072 io::Error::other(e.to_string()),
2073 "invalid server name".to_string(),
2074 )
2075 })?;
2076 let reconnect_timeout =
2077 hyperactor_config::global::get(config::CHANNEL_RECONNECT_TIMEOUT);
2078 let mut backoff = ExponentialBackoffBuilder::new()
2079 .with_initial_interval(Duration::from_millis(1))
2080 .with_multiplier(2.0)
2081 .with_randomization_factor(0.1)
2082 .with_max_interval(Duration::from_millis(1000))
2083 .with_max_elapsed_time(Some(reconnect_timeout))
2084 .build();
2085 loop {
2086 let mut addrs = (self.hostname.as_ref(), self.port)
2087 .to_socket_addrs()
2088 .map_err(|_| ClientError::Resolve(self.dest()))?;
2089 let addr = addrs.next().ok_or(ClientError::Resolve(self.dest()))?;
2090 match TcpStream::connect(&addr).await {
2091 Ok(stream) => {
2092 stream.set_nodelay(true).map_err(|err| {
2093 ClientError::Connect(
2094 self.dest(),
2095 err,
2096 "cannot disable Nagle algorithm".to_string(),
2097 )
2098 })?;
2099 set_tcp_keepalive(&stream);
2100 set_tcp_congestion(&stream);
2101 let mut tls_stream = self
2102 .connector
2103 .connect(server_name.clone(), stream)
2104 .await
2105 .map_err(|err| {
2106 tracing::info!(
2107 dest = %self.dest(),
2108 error = %err,
2109 "TLS handshake failed"
2110 );
2111 ClientError::Connect(
2112 self.dest(),
2113 err,
2114 format!("cannot establish TLS connection to {:?}", server_name),
2115 )
2116 })?;
2117 write_link_init(&mut tls_stream, session_id, self.stream_id, self.kind)
2118 .await
2119 .map_err(|err| ClientError::Io(self.dest(), err))?;
2120 return Ok(tls_stream);
2121 }
2122 Err(err) => {
2123 tracing::debug!(error = %err, "tls connect failed, backing off");
2124 match backoff.next_backoff() {
2125 Some(delay) => tokio::time::sleep(delay).await,
2126 None => {
2127 return Err(ClientError::ConnectTimeout(
2128 self.dest(),
2129 reconnect_timeout,
2130 err,
2131 ));
2132 }
2133 }
2134 }
2135 }
2136 }
2137 }
2138 }
2139
2140 pub fn link(
2142 addr: TlsAddr,
2143 session_id: SessionId,
2144 stream_id: u8,
2145 kind: ProtocolKind,
2146 ) -> Result<TlsLink, ClientError> {
2147 let connector = tls_connector().map_err(|e| {
2148 ClientError::Connect(
2149 ChannelAddr::Tls(addr.clone()),
2150 io::Error::other(e.to_string()),
2151 "failed to create TLS connector".to_string(),
2152 )
2153 })?;
2154 let TlsAddr { hostname, port } = addr;
2155 Ok(TlsLink {
2156 hostname,
2157 port,
2158 connector,
2159 addr_type: TlsAddrType::Tls,
2160 session_id,
2161 stream_id,
2162 kind,
2163 })
2164 }
2165
2166 #[cfg(test)]
2167 mod tests {
2168 use timed_test::async_timed_test;
2169
2170 use super::*;
2171 use crate::channel::ChannelTx;
2172 use crate::channel::Rx;
2173 use crate::channel::Tx;
2174 use crate::channel::dial;
2175 use crate::channel::net::server;
2176 use crate::channel::serve;
2177 use crate::channel::unordered;
2178 use crate::config::Pem;
2179 use crate::config::TLS_CA;
2180 use crate::config::TLS_CERT;
2181 use crate::config::TLS_KEY;
2182
2183 const TEST_CA_CERT: &str = r#"-----BEGIN CERTIFICATE-----
2187MIIDBTCCAe2gAwIBAgIUaGNmboiIosG+8Up0vgDr/+cg+2IwDQYJKoZIhvcNAQEL
2188BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAxMjgxNzA4MzlaFw0yNzAxMjgx
2189NzA4MzlaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
2190DwAwggEKAoIBAQC9RBoMYXCajklswt8Vi1JI1lEYzic0WNOmz45vG/7H6jTWkgL3
2191K5Ri+Seg3MobDNc48YHWXYm4hP9wCzkx8ih3ntT5XiY1My/G3jLUuoIEE9pF/BoJ
2192YQwZVoPNFhA9WhXNRsINf1cXFf8NzRfXpxBfKWtQJxYXU4JiDBQ6rLnQQABo8JmQ
2193vYFhJbBaYip5jTSiVNn7mB1zNr5jsVxuoSF53Pb7xQ76bwBdOq4zd6PSxL5/lr4G
2194cHSoxwZQdZMG7PL6hbxDQ2S2YI2lYVET1zwc2WPKCfjbEXBC/jzx828CInQtuksk
219518gJt6xHkTFEA8CSA29GM3lejnwYWf51xyyBAgMBAAGjUzBRMB0GA1UdDgQWBBRX
2196cbxSZ70NsUkAS3Hhy6irugywJDAfBgNVHSMEGDAWgBRXcbxSZ70NsUkAS3Hhy6ir
2197ugywJDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA7aAFfyW67
2198Z+uGSVYhpsT/uH/3Z3nr7X1smTz5CGEfq2czEcTC7gbYI2l8GZ47GPfnAvHTBZVm
2199V/XncBCsj7/thOh2jYEHFyCbPckoaSCRyCOnK7LPUlr4HN5uP9EFe45qBLCJDEoY
2200GTTw7MtzwdovfjchNfKQCTtkBJCXQ95WLCf6UOh02Sn28UTlgfXzF0X0FrcWqWa3
2201uJZd4XOo4O6hKKlHaBaQPiEr++1xc3SWPV7jZHbckI/vKBnDdEZ9JQX5fFZuypUI
2202sgomYHxvxrU2hWx+7k53CRdjfaIvT9Ie44z9sSdsU/+blw2S8f/ZTmuECoIAAXYO
22030qpzlxZMdr7T
2204-----END CERTIFICATE-----"#;
2205
2206 const TEST_SERVER_CERT: &str = r#"-----BEGIN CERTIFICATE-----
2208MIIDJDCCAgygAwIBAgIUaz66DsWaH5ZXM4hCFnbVbMsyN1cwDQYJKoZIhvcNAQEL
2209BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAxMjgxNzA4MzlaFw0yNzAxMjgx
2210NzA4MzlaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD
2211ggEPADCCAQoCggEBAKCbp++qNyTn5LOsV0h9gLKJALBcjg2A14I3804N9UyDhPW2
2212QKQ2W424u2P1MfKrw/2C+CErGlrADlnco2RQVDAarAIuGdFvBOt5UezqOS7Mk4OS
22139MlS7NZnMbc37KuM9UIG5ScJjXR/Z5z9dxeR0I9y3n0Ix6khbV7tOSHobiweI0FI
22148LftBS+CQnXr6vbWPcHcW6Z0FHUv7IWhqMWmv9PlZRGe9Y6VzXrRp0PBnZMOnAYf
2215aMQUwYRswWdm9j9Z1sMdTJ14G+KVmO3Vj6XI6Sm9uIcYhlwG/kORwogJFWlVuP9o
2216rloFRCjyHJ1d7GZqqnRyHHDDCBms8ed+3YfEYQECAwEAAaNwMG4wLAYDVR0RBCUw
2217I4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB0GA1UdDgQWBBQl
2218J4vxUoCzqqeTwQAiLqE8wYezKzAfBgNVHSMEGDAWgBRXcbxSZ70NsUkAS3Hhy6ir
2219ugywJDANBgkqhkiG9w0BAQsFAAOCAQEAnXHIBDQ4AHAMV71piTOuI41ShASQed6L
2220bi7XUMZgZDslLkfU1vnP3BlwpliraBsAytSYQC6kbytOuz1uQ4K7yLb2tAAmUgEO
2221EdIVt9SXr5tCcIPeLmInF0pysPqjZO8n7vtJyd9gryKqdhm1uzA7WQWq/Az8a9Sk
2222uW2J6Oc5p6P7Mf3/ixqXzvGRo8rzu0CUJOJ67UTE/HhbJuplQ5dep5CEEOAIsAtH
2223zn9O4rW92ueBkoBJM++YILS1vQ7jKc2N3RNrnHm7FeootBrtR9mBi0TH97K73ZPZ
22242Cdhnym0CsCJggrllFGH32cYo7+K2PO7/4oj5XbBCSWcssicvd8ovg==
2225-----END CERTIFICATE-----"#;
2226
2227 const TEST_SERVER_KEY: &str = r#"-----BEGIN PRIVATE KEY-----
2231MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCgm6fvqjck5+Sz
2232rFdIfYCyiQCwXI4NgNeCN/NODfVMg4T1tkCkNluNuLtj9THyq8P9gvghKxpawA5Z
22333KNkUFQwGqwCLhnRbwTreVHs6jkuzJODkvTJUuzWZzG3N+yrjPVCBuUnCY10f2ec
2234/XcXkdCPct59CMepIW1e7Tkh6G4sHiNBSPC37QUvgkJ16+r21j3B3FumdBR1L+yF
2235oajFpr/T5WURnvWOlc160adDwZ2TDpwGH2jEFMGEbMFnZvY/WdbDHUydeBvilZjt
22361Y+lyOkpvbiHGIZcBv5DkcKICRVpVbj/aK5aBUQo8hydXexmaqp0chxwwwgZrPHn
2237ft2HxGEBAgMBAAECgf8G5qlQov+7ljs9fSpC8yGUik59RXzVF7Qq5DyQHglsQDp2
2238VF5yr+M/M7DZmq+KvdauDfKbej6np5j2Q4TByrHTX1IExfZWCW8srwnWJDpQyHmO
2239LcJW5DlI/SYluUFyHZxsOd+ezcpGNzM8i6eSW7GaeFUXCkmJ+uW4LnlF+7bALnnd
2240D6sak/58EsII+IJyd4lFn+voszlPn3CZGR0jkp21rvpaKgrMIsKVWWQO/sLDU5pr
2241VbpBThcLU5gRcnQouQX12e2VTCIlFu75WTsJ8V/KnEaOZUVlU/B/Bs+WQF3U+/Jo
2242eX4N+D6OsEcNQjERAFyWujxsl1WpD4uSsbFMN0ECgYEA2b7AdL+oKPQHku2KcBhr
2243Zw8K4tMDlr2VPPNwZcBTLo+O71vv/xXjMcXrXmowzkgEQckUmt1VB46riyydhwdP
2244/n9ciWcz0Va/nwHR6Y9F9unBiyUBP7PRhRyjQyRZZRGDSJvP+Xmc5UJFpRr07VLU
2245nfgMXDj37vXzKDpfhdEB2nkCgYEAvNMfA8P8w3+6246x5YHflvTkPdw+2oyge+LD
2246mphB/w7SF8mlyNGloj3+KBZmd9SkvT57wCvO96Y9/n+mBAVisRggc0hK4ymOVYhb
2247+im/JvqGQMbVeg6iCOHnWdaZf9tL8uVsegQy3kVTN7vAa+CMFgX1dt65cGBX6XkB
224844pYmMkCgYALhbiRdQLlB+TOtZs5y1EDpxwgXKI3+9hF3Wv5NnAwapBZwje0++eF
22493r9Rw7TJda4j/QwGFehF+hrBxp6fYpetE/hFnRx0225Qb7w368j8A+ql/lNOl6li
2250rd1F1EqWupKD6RrcTL8sspEU55RGaretlE6zIqCcGI/BdTVQ03qRoQKBgHDC3zWf
2251d7XD9HGjQGdfbIe4jQjIGxzmd/wjik4q+NZ5IkukVwWa9P/zZ3DHF8Ad05dT1hEH
22522FwaAdGWpyyljq9VSiOuG1KXAXHgsZSuE4ISf9P1KYzvaiJFzaPfvOEWs79E9MfU
22539A+6dJzG2X1SpjWMr26iSTlrv3QkmFUqzAfJAoGASBkn4wls+oC5rv/Mch43pBv5
2254UmKru4ltnEHJZdbSi2DJ+AnDLD222JCasb1VT1tm2XgW6DBqrdVRPPP6GOlB0MHU
2255+3ULtZxAczt7I+ST2bo0/DV2Hse89Cm63w4wLOiVZs7+1wrAzJZLokWF7Q5gesra
2256u19txmtkiMEH+aNmekk=
2257-----END PRIVATE KEY-----"#;
2258
2259 #[async_timed_test(timeout_secs = 30)]
2260 async fn test_tls_basic() {
2261 let _ = rustls::crypto::ring::default_provider().install_default();
2264
2265 let config = hyperactor_config::global::lock();
2267 let _guard_cert =
2268 config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2269 let _guard_key =
2270 config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2271 let _guard_ca =
2272 config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2273
2274 let addr = TlsAddr::new("localhost", 0);
2276
2277 let (local_addr, mut rx) =
2278 server::serve::<u64>(ChannelAddr::Tls(addr), None).expect("failed to serve");
2279
2280 let tx: ChannelTx<u64> = super::spawn(
2282 link(
2283 match &local_addr {
2284 ChannelAddr::Tls(addr) => addr.clone(),
2285 _ => panic!("unexpected address type"),
2286 },
2287 SessionId::random(),
2288 0,
2289 super::ProtocolKind::Simplex,
2290 )
2291 .expect("failed to create link"),
2292 );
2293
2294 tx.post(42u64);
2296
2297 let received = rx.recv().await.expect("failed to receive");
2299 assert_eq!(received, 42u64);
2300 }
2301
2302 #[async_timed_test(timeout_secs = 30)]
2303 async fn test_quic_basic() {
2304 let _ = rustls::crypto::ring::default_provider().install_default();
2305
2306 let config = hyperactor_config::global::lock();
2307 let _guard_cert =
2308 config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2309 let _guard_key =
2310 config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2311 let _guard_ca =
2312 config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2313
2314 let (addr, mut rx) =
2315 serve::<u64>(ChannelAddr::Quic(TlsAddr::new("localhost", 0))).unwrap();
2316 let tx = dial::<u64>(addr).unwrap();
2317
2318 tx.post(42u64);
2319
2320 let received = rx.recv().await.expect("failed to receive");
2321 assert_eq!(received, 42u64);
2322 }
2323
2324 #[async_timed_test(timeout_secs = 30)]
2325 async fn test_quic_unordered_basic() {
2326 let _ = rustls::crypto::ring::default_provider().install_default();
2327
2328 let config = hyperactor_config::global::lock();
2329 let _guard_cert =
2330 config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2331 let _guard_key =
2332 config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2333 let _guard_ca =
2334 config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2335
2336 let (addr, mut rx) =
2337 unordered::serve::<u64>(ChannelAddr::Quic(TlsAddr::new("localhost", 0))).unwrap();
2338 let tx = unordered::dial::<u64>(addr, 2).unwrap();
2339
2340 for i in 0..8 {
2341 tx.post(i);
2342 }
2343
2344 let mut received = Vec::new();
2345 for _ in 0..8 {
2346 received.push(rx.recv().await.expect("failed to receive"));
2347 }
2348 received.sort();
2349 assert_eq!(received, (0..8).collect::<Vec<_>>());
2350 }
2351
2352 #[async_timed_test(timeout_secs = 30)]
2353 async fn test_tls_multiple_messages() {
2354 let _ = rustls::crypto::ring::default_provider().install_default();
2355
2356 let config = hyperactor_config::global::lock();
2358 let _guard_cert =
2359 config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2360 let _guard_key =
2361 config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2362 let _guard_ca =
2363 config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2364
2365 let addr = TlsAddr::new("localhost", 0);
2366
2367 let (local_addr, mut rx) =
2368 server::serve::<String>(ChannelAddr::Tls(addr), None).expect("failed to serve");
2369 let tx: ChannelTx<String> = super::spawn(
2370 link(
2371 match &local_addr {
2372 ChannelAddr::Tls(addr) => addr.clone(),
2373 _ => panic!("unexpected address type"),
2374 },
2375 SessionId::random(),
2376 0,
2377 super::ProtocolKind::Simplex,
2378 )
2379 .expect("failed to create link"),
2380 );
2381
2382 for i in 0..10 {
2384 tx.post(format!("message {}", i));
2385 }
2386
2387 for i in 0..10 {
2389 let received = rx.recv().await.expect("failed to receive");
2390 assert_eq!(received, format!("message {}", i));
2391 }
2392 }
2393
2394 #[test]
2395 fn test_tls_parse_hostname_port() {
2396 let addr = parse("localhost:8080").expect("failed to parse");
2397 assert!(matches!(
2398 addr,
2399 ChannelAddr::Tls(TlsAddr { hostname, port })
2400 if hostname == "localhost" && port == 8080
2401 ));
2402 }
2403
2404 #[test]
2405 fn test_tls_parse_socket_addr() {
2406 let addr = parse("127.0.0.1:8080").expect("failed to parse");
2407 assert!(matches!(
2408 addr,
2409 ChannelAddr::Tls(TlsAddr { hostname, port })
2410 if hostname == "127.0.0.1" && port == 8080
2411 ));
2412 }
2413
2414 #[test]
2415 fn test_tls_certs_parsing() {
2416 let cert_pem = Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec());
2418 let key_pem = Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec());
2419 let ca_pem = Pem::Value(TEST_CA_CERT.as_bytes().to_vec());
2420
2421 let certs = super::load_certs(&cert_pem).expect("failed to load certs");
2422 assert!(!certs.is_empty(), "expected at least one certificate");
2423
2424 let _key = super::load_key(&key_pem).expect("failed to load key");
2425
2426 let root_store = super::build_root_store(&ca_pem).expect("failed to build root store");
2427 assert!(!root_store.is_empty(), "expected at least one CA cert");
2428 }
2429
2430 #[test]
2431 fn test_tls_acceptor_creation() {
2432 let _ = rustls::crypto::ring::default_provider().install_default();
2435
2436 let config = hyperactor_config::global::lock();
2438 let _guard_cert =
2439 config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2440 let _guard_key =
2441 config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2442 let _guard_ca =
2443 config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2444
2445 let _acceptor = super::tls_acceptor().expect("failed to create TLS acceptor");
2447 }
2448
2449 #[test]
2450 fn test_tls_connector_creation() {
2451 let _ = rustls::crypto::ring::default_provider().install_default();
2454
2455 let config = hyperactor_config::global::lock();
2457 let _guard_cert =
2458 config.override_key(TLS_CERT, Pem::Value(TEST_SERVER_CERT.as_bytes().to_vec()));
2459 let _guard_key =
2460 config.override_key(TLS_KEY, Pem::Value(TEST_SERVER_KEY.as_bytes().to_vec()));
2461 let _guard_ca =
2462 config.override_key(TLS_CA, Pem::Value(TEST_CA_CERT.as_bytes().to_vec()));
2463
2464 let _connector = super::tls_connector().expect("failed to create TLS connector");
2466 }
2467 }
2468}
2469
2470fn oss_pem_bundle() -> crate::config::PemBundle {
2472 crate::config::PemBundle {
2473 ca: hyperactor_config::global::get_cloned(crate::config::TLS_CA),
2474 cert: hyperactor_config::global::get_cloned(crate::config::TLS_CERT),
2475 key: hyperactor_config::global::get_cloned(crate::config::TLS_KEY),
2476 }
2477}
2478
2479pub fn try_tls_pem_bundle() -> Option<crate::config::PemBundle> {
2489 let oss_bundle = oss_pem_bundle();
2490 if oss_bundle.ca.reader().is_ok() {
2491 return Some(oss_bundle);
2492 }
2493 tracing::debug!("OSS TLS bundle: CA not readable, trying Meta paths");
2494
2495 let meta_bundle = meta::get_server_pem_bundle();
2496 if meta_bundle.ca.reader().is_ok() {
2497 return Some(meta_bundle);
2498 }
2499 tracing::debug!("Meta TLS bundle: CA not readable, no TLS available");
2500
2501 None
2502}
2503
2504pub fn try_tls_acceptor_with_pem_bundle(
2524 enforce_client_tls: bool,
2525) -> Option<(tokio_rustls::TlsAcceptor, crate::config::PemBundle)> {
2526 let oss_bundle = oss_pem_bundle();
2527 if let Ok(acceptor) = tls::tls_acceptor_from_bundle(&oss_bundle, enforce_client_tls) {
2528 return Some((acceptor, oss_bundle));
2529 }
2530 tracing::debug!("OSS TLS acceptor failed, trying Meta paths");
2531
2532 let meta_bundle = meta::get_server_pem_bundle();
2533 if let Ok(acceptor) = tls::tls_acceptor_from_bundle(&meta_bundle, enforce_client_tls) {
2534 return Some((acceptor, meta_bundle));
2535 }
2536 tracing::debug!("Meta TLS acceptor failed, no TLS available");
2537
2538 None
2539}
2540
2541pub fn try_tls_connector() -> Option<tokio_rustls::TlsConnector> {
2553 let oss_bundle = oss_pem_bundle();
2554 if let Ok(connector) = tls::tls_connector_from_bundle(&oss_bundle) {
2555 return Some(connector);
2556 }
2557 if let Ok(config) = tls::client_config_from_ca(&oss_bundle.ca) {
2558 return Some(tokio_rustls::TlsConnector::from(Arc::new(config)));
2559 }
2560 tracing::debug!("OSS TLS connector failed, trying Meta paths");
2561
2562 if let Ok(connector) = meta::try_tls_connector() {
2563 return Some(connector);
2564 }
2565 tracing::debug!("Meta TLS connector failed, no TLS available");
2566
2567 None
2568}
2569
2570#[cfg(test)]
2571mod tests {
2572
2573 #![expect(
2574 clippy::await_holding_invalid_type,
2575 reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
2576 )]
2577
2578 use std::assert_matches;
2579 use std::collections::VecDeque;
2580 use std::marker::PhantomData;
2581 use std::sync::Arc;
2582 use std::sync::RwLock;
2583 use std::sync::atomic::AtomicBool;
2584 use std::sync::atomic::AtomicU64;
2585 use std::sync::atomic::Ordering;
2586 use std::time::Duration;
2587 #[cfg(target_os = "linux")] use std::time::UNIX_EPOCH;
2589
2590 #[cfg(target_os = "linux")] use anyhow::Result;
2592 use bytes::Bytes;
2593 use rand::RngExt as _;
2594 use rand::SeedableRng as _;
2595 use rand::distr::Alphanumeric;
2596 use rand::rngs::SysRng;
2597 use timed_test::async_timed_test;
2598 use tokio::io::AsyncRead;
2599 use tokio::io::AsyncWrite;
2600 use tokio::io::DuplexStream;
2601 use tokio::io::ReadHalf;
2602 use tokio::io::WriteHalf;
2603 use tokio::task::JoinHandle;
2604 use tokio::time::Instant;
2605 use tokio_util::sync::CancellationToken;
2606
2607 use super::server;
2608 use super::*;
2609 use crate::channel;
2610 use crate::channel::ChannelRx;
2611 use crate::channel::ChannelTx;
2612 use crate::channel::net::framed::FrameReader;
2613 use crate::channel::net::framed::FrameWrite;
2614 use crate::channel::net::server::AcceptorLink;
2615 use crate::config;
2616 use crate::metrics;
2617 use crate::sync::mvar::MVar;
2618
2619 fn logs_assert_unscoped(f: impl Fn(&[&str]) -> Result<(), String>) {
2623 let buf = tracing_test::internal::global_buf().lock().unwrap();
2624 let logs_str = std::str::from_utf8(&buf).expect("Logs contain invalid UTF8");
2625 let lines: Vec<&str> = logs_str.lines().collect();
2626 match f(&lines) {
2627 Ok(()) => {}
2628 Err(msg) => panic!("{}", msg),
2629 }
2630 }
2631
2632 #[cfg(target_os = "linux")] #[tracing_test::traced_test]
2634 #[tokio::test]
2635 async fn test_unix_basic() -> Result<()> {
2636 let timestamp = std::time::SystemTime::now()
2637 .duration_since(UNIX_EPOCH)
2638 .unwrap()
2639 .as_nanos();
2640 let unique_address = format!("test_unix_basic_{}", timestamp);
2641
2642 let (addr, mut rx) = server::serve::<u64>(
2643 ChannelAddr::Unix(unix::SocketAddr::from_abstract_name(&unique_address)?),
2644 None,
2645 )
2646 .unwrap();
2647
2648 {
2655 let tx: ChannelTx<u64> = channel::dial::<u64>(addr.clone()).unwrap();
2656 tx.post(123);
2657 assert_eq!(rx.recv().await.unwrap(), 123);
2658 }
2659
2660 {
2661 let tx = channel::dial::<u64>(addr.clone()).unwrap();
2662 tx.post(321);
2663 tx.post(111);
2664 tx.post(444);
2665
2666 assert_eq!(rx.recv().await.unwrap(), 321);
2667 assert_eq!(rx.recv().await.unwrap(), 111);
2668 assert_eq!(rx.recv().await.unwrap(), 444);
2669 }
2670
2671 {
2672 let tx = channel::dial::<u64>(addr).unwrap();
2673 drop(rx);
2674
2675 assert_matches!(
2676 tx.try_post(123).await,
2677 Err(SendError {
2678 error: ChannelError::Closed,
2679 message: 123,
2680 ..
2681 })
2682 );
2683 }
2684
2685 Ok(())
2686 }
2687
2688 #[cfg(target_os = "linux")] #[tracing_test::traced_test]
2690 #[tokio::test]
2691 async fn test_unix_basic_client_before_server() -> Result<()> {
2692 let timestamp = std::time::SystemTime::now()
2694 .duration_since(UNIX_EPOCH)
2695 .unwrap()
2696 .as_nanos();
2697 let socket_addr =
2698 unix::SocketAddr::from_abstract_name(&format!("test_unix_basic_{}", timestamp))
2699 .unwrap();
2700
2701 let addr = ChannelAddr::Unix(socket_addr.clone());
2703 let tx = crate::channel::dial::<u64>(addr.clone()).unwrap();
2704 tx.post(123);
2705
2706 let (_, mut rx) = server::serve::<u64>(ChannelAddr::Unix(socket_addr), None).unwrap();
2707 assert_eq!(rx.recv().await.unwrap(), 123);
2708
2709 tx.post(321);
2710 tx.post(111);
2711 tx.post(444);
2712
2713 assert_eq!(rx.recv().await.unwrap(), 321);
2714 assert_eq!(rx.recv().await.unwrap(), 111);
2715 assert_eq!(rx.recv().await.unwrap(), 444);
2716
2717 Ok(())
2718 }
2719
2720 #[cfg(target_os = "linux")]
2725 #[tokio::test]
2726 async fn apply_tcp_congestion_sets_congestion_control() {
2727 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2728 let addr = listener.local_addr().unwrap();
2729 let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2730 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2731 let _server = accept.await.unwrap();
2732
2733 apply_tcp_congestion(&stream, Some("reno")).unwrap();
2734
2735 let cc = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2736 assert!(
2737 cc.starts_with(b"reno"),
2738 "expected reno congestion control, got {:?}",
2739 String::from_utf8_lossy(&cc)
2740 );
2741 }
2742
2743 #[cfg(target_os = "linux")]
2748 #[tracing_test::traced_test]
2749 #[tokio::test]
2750 async fn set_tcp_congestion_keeps_the_connection_when_the_host_refuses() {
2751 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2752 let addr = listener.local_addr().unwrap();
2753 let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2754 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2755 let _server = accept.await.unwrap();
2756 let before = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2757
2758 let config = hyperactor_config::global::lock();
2759 let _guard = config.override_key(
2760 config::CHANNEL_TCP_CONGESTION,
2761 "not_a_real_cc_algo".to_string(),
2762 );
2763
2764 set_tcp_congestion(&stream);
2765
2766 let after = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2767 assert_eq!(before, after, "the socket should keep the host default");
2768 assert!(logs_contain("not_a_real_cc_algo"));
2771 }
2772
2773 #[cfg(target_os = "linux")]
2775 #[tokio::test]
2776 async fn set_tcp_congestion_is_a_noop_when_unconfigured() {
2777 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2778 let addr = listener.local_addr().unwrap();
2779 let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2780 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2781 let _server = accept.await.unwrap();
2782 let before = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2783
2784 let config = hyperactor_config::global::lock();
2785 let _guard = config.override_key(config::CHANNEL_TCP_CONGESTION, String::new());
2786
2787 set_tcp_congestion(&stream);
2788
2789 let after = socket2::SockRef::from(&stream).tcp_congestion().unwrap();
2790 assert_eq!(before, after);
2791 }
2792
2793 #[cfg(target_os = "linux")]
2797 #[tokio::test]
2798 async fn apply_tcp_congestion_rejects_unknown_algorithm() {
2799 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2800 let addr = listener.local_addr().unwrap();
2801 let accept = tokio::spawn(async move { listener.accept().await.unwrap() });
2802 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
2803 let _server = accept.await.unwrap();
2804
2805 let err = apply_tcp_congestion(&stream, Some("not_a_real_cc_algo"))
2806 .expect_err("an unsupported congestion-control algorithm should be an error");
2807 let msg = err.to_string();
2810 assert!(
2811 msg.contains("not_a_real_cc_algo"),
2812 "error should name the rejected algorithm, got {msg:?}"
2813 );
2814
2815 apply_tcp_congestion(&stream, None).expect("None should be a no-op, not an error");
2816 }
2817
2818 #[tracing_test::traced_test]
2819 #[async_timed_test(timeout_secs = 60)]
2820 #[cfg_attr(not(fbcode_build), ignore)]
2822 async fn test_tcp_basic() {
2823 let (addr, mut rx) =
2824 server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2825 {
2826 let tx = channel::dial::<u64>(addr.clone()).unwrap();
2827 tx.post(123);
2828 assert_eq!(rx.recv().await.unwrap(), 123);
2829 }
2830
2831 {
2832 let tx = channel::dial::<u64>(addr.clone()).unwrap();
2833 tx.post(321);
2834 tx.post(111);
2835 tx.post(444);
2836
2837 assert_eq!(rx.recv().await.unwrap(), 321);
2838 assert_eq!(rx.recv().await.unwrap(), 111);
2839 assert_eq!(rx.recv().await.unwrap(), 444);
2840 }
2841
2842 {
2843 let tx = channel::dial::<u64>(addr).unwrap();
2844 drop(rx);
2845
2846 assert_matches!(
2847 tx.try_post(123).await,
2848 Err(SendError {
2849 error: ChannelError::Closed,
2850 message: 123,
2851 ..
2852 })
2853 );
2854 }
2855 }
2856
2857 #[async_timed_test(timeout_secs = 20)]
2858 #[cfg_attr(not(fbcode_build), ignore)]
2859 async fn test_tcp_unreachable_peer_surfaces_closed() {
2860 let config = hyperactor_config::global::lock();
2865 let _g1 = config.override_key(config::CHANNEL_RECONNECT_TIMEOUT, Duration::from_secs(1));
2869 let _g2 = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(3));
2870
2871 let (addr, rx) =
2874 server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2875 drop(rx);
2876
2877 let tx = channel::dial::<u64>(addr.clone()).unwrap();
2878 tx.post(123); let mut status = tx.status().clone();
2881 while !status.borrow_and_update().is_closed() {
2882 status.changed().await.unwrap();
2883 }
2884 }
2885
2886 #[async_timed_test(timeout_secs = 30)]
2893 #[cfg_attr(not(fbcode_build), ignore)]
2895 async fn test_tcp_message_size() {
2896 let default_size_in_bytes = 100 * 1024 * 1024;
2897 let config = hyperactor_config::global::lock();
2899 let _guard1 = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(1));
2900 let _guard2 = config.override_key(config::CODEC_MAX_FRAME_LENGTH, default_size_in_bytes);
2901
2902 let (addr, mut rx) =
2903 server::serve::<String>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2904
2905 let tx = channel::dial::<String>(addr.clone()).unwrap();
2906 {
2908 let message = "a".repeat(default_size_in_bytes - 1024);
2910 tx.post(message.clone());
2911 assert_eq!(rx.recv().await.unwrap(), message);
2912 }
2913 {
2915 let message = "a".repeat(default_size_in_bytes + 1024);
2916 let returned = tx.try_post(message.clone()).await.unwrap_err();
2917 assert_eq!(message, returned.message);
2918 }
2919 }
2920
2921 #[async_timed_test(timeout_secs = 30)]
2922 #[cfg_attr(not(fbcode_build), ignore)]
2924 async fn test_ack_flush() {
2925 let config = hyperactor_config::global::lock();
2926 let _guard_message_ack =
2929 config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 100000000);
2930 let _guard_delivery_timeout =
2931 config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(5));
2932
2933 let (addr, mut net_rx) =
2934 server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
2935 let net_tx = channel::dial::<u64>(addr.clone()).unwrap();
2936 let receipt = net_tx.try_post(1);
2937 assert_eq!(net_rx.recv().await.unwrap(), 1);
2938 drop(net_rx);
2939 assert!(receipt.await.is_ok());
2940 }
2941
2942 #[async_timed_test(timeout_secs = 60)]
2943 #[cfg_attr(not(fbcode_build), ignore)]
2945 async fn test_meta_tls_basic() {
2946 hyperactor_telemetry::initialize_logging_for_test();
2947
2948 let addr = ChannelAddr::any(ChannelTransport::MetaTls(TlsMode::IpV6));
2949 let meta_addr = match addr {
2950 ChannelAddr::MetaTls(meta_addr) => meta_addr,
2951 _ => panic!("expected MetaTls address"),
2952 };
2953 let (local_addr, mut rx) =
2954 server::serve::<u64>(ChannelAddr::MetaTls(meta_addr), None).unwrap();
2955 {
2956 let tx = channel::dial::<u64>(local_addr.clone()).unwrap();
2957 tx.post(123);
2958 }
2959 assert_eq!(rx.recv().await.unwrap(), 123);
2960
2961 {
2962 let tx = channel::dial::<u64>(local_addr.clone()).unwrap();
2963 tx.post(321);
2964 tx.post(111);
2965 tx.post(444);
2966 assert_eq!(rx.recv().await.unwrap(), 321);
2967 assert_eq!(rx.recv().await.unwrap(), 111);
2968 assert_eq!(rx.recv().await.unwrap(), 444);
2969 }
2970
2971 {
2972 let tx = channel::dial::<u64>(local_addr).unwrap();
2973 drop(rx);
2974
2975 assert_matches!(
2976 tx.try_post(123).await,
2977 Err(SendError {
2978 error: ChannelError::Closed,
2979 message: 123,
2980 ..
2981 })
2982 );
2983 }
2984 }
2985
2986 #[derive(Clone, Debug, Default)]
2987 struct NetworkFlakiness {
2988 disconnect_params: Option<(f64, u64, Duration)>,
2996 latency_range: Option<(Duration, Duration)>,
2999 }
3000
3001 impl NetworkFlakiness {
3002 async fn should_disconnect(
3004 &self,
3005 rng: &mut impl rand::Rng,
3006 disconnected_count: u64,
3007 prev_disconnected_at: &RwLock<Instant>,
3008 ) -> bool {
3009 let Some((prob, max_disconnects, duration)) = &self.disconnect_params else {
3010 return false;
3011 };
3012
3013 let disconnected_at = prev_disconnected_at.read().unwrap();
3014 if disconnected_at.elapsed() > *duration && disconnected_count < *max_disconnects {
3015 rng.random_bool(*prob)
3016 } else {
3017 false
3018 }
3019 }
3020 }
3021
3022 struct MockLink<M> {
3023 buffer_size: usize,
3024 session_id: SessionId,
3025 receiver_storage: Arc<MVar<DuplexStream>>,
3026 fail_connects: Arc<AtomicBool>,
3028 disconnect_signal: watch::Sender<()>,
3031 network_flakiness: NetworkFlakiness,
3032 disconnected_count: Arc<AtomicU64>,
3033 prev_disconnected_at: Arc<RwLock<Instant>>,
3034 debug_log_sampling_rate: Option<u64>,
3037 _message_type: PhantomData<M>,
3038 }
3039
3040 impl<M> fmt::Debug for MockLink<M> {
3041 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3042 f.debug_struct("MockLink")
3043 .field("buffer_size", &self.buffer_size)
3044 .field("receiver_storage", &"<MVar<DuplexStream>>")
3045 .field("fail_connects", &self.fail_connects)
3046 .field("disconnect_signal", &"<watch::Sender>")
3047 .field("network_flakiness", &self.network_flakiness)
3048 .field("disconnected_count", &self.disconnected_count)
3049 .field("prev_disconnected_at", &"<RwLock<Instant>>")
3050 .field("debug_log_sampling_rate", &self.debug_log_sampling_rate)
3051 .finish()
3052 }
3053 }
3054
3055 impl<M: RemoteMessage> MockLink<M> {
3056 fn new() -> Self {
3057 let (sender, _) = watch::channel(());
3058 Self {
3059 buffer_size: 64,
3060 session_id: SessionId::random(),
3061 receiver_storage: Arc::new(MVar::empty()),
3062 fail_connects: Arc::new(AtomicBool::new(false)),
3063 disconnect_signal: sender,
3064 network_flakiness: NetworkFlakiness::default(),
3065 disconnected_count: Arc::new(AtomicU64::new(0)),
3066 prev_disconnected_at: Arc::new(RwLock::new(tokio::time::Instant::now())),
3067 debug_log_sampling_rate: None,
3068 _message_type: PhantomData,
3069 }
3070 }
3071
3072 fn fail_connects() -> Self {
3075 Self {
3076 fail_connects: Arc::new(AtomicBool::new(true)),
3077 ..Self::new()
3078 }
3079 }
3080
3081 fn with_network_flakiness(network_flakiness: NetworkFlakiness) -> Self {
3082 if let Some((min, max)) = network_flakiness.latency_range {
3083 assert!(min < max);
3084 }
3085
3086 Self {
3087 network_flakiness,
3088 ..Self::new()
3089 }
3090 }
3091
3092 fn receiver_storage(&self) -> Arc<MVar<DuplexStream>> {
3093 self.receiver_storage.clone()
3094 }
3095
3096 fn disconnected_count(&self) -> Arc<AtomicU64> {
3097 self.disconnected_count.clone()
3098 }
3099
3100 fn disconnect_signal(&self) -> &watch::Sender<()> {
3101 &self.disconnect_signal
3102 }
3103
3104 fn fail_connects_switch(&self) -> Arc<AtomicBool> {
3105 self.fail_connects.clone()
3106 }
3107
3108 fn set_buffer_size(&mut self, size: usize) {
3109 self.buffer_size = size;
3110 }
3111
3112 fn set_sampling_rate(&mut self, sampling_rate: u64) {
3113 self.debug_log_sampling_rate = Some(sampling_rate);
3114 }
3115 }
3116
3117 #[async_trait]
3118 impl<M: RemoteMessage> Link for MockLink<M> {
3119 type Stream = DuplexStream;
3120
3121 fn dest(&self) -> ChannelAddr {
3122 ChannelAddr::Local(u64::MAX)
3123 }
3124
3125 fn link_id(&self) -> SessionId {
3126 self.session_id
3127 }
3128
3129 async fn next(&mut self) -> Result<Self::Stream, ClientError> {
3130 let session_id = self.session_id;
3131 tracing::debug!("MockLink starts to connect.");
3132 if self.fail_connects.load(Ordering::Acquire) {
3133 return Err(ClientError::Connect(
3134 self.dest(),
3135 std::io::Error::other("intentional error"),
3136 "expected failure injected by the mock".to_string(),
3137 ));
3138 }
3139
3140 async fn relay_message<M: RemoteMessage>(
3146 mut disconnect_signal: watch::Receiver<()>,
3147 network_flakiness: NetworkFlakiness,
3148 disconnected_count: Arc<AtomicU64>,
3149 prev_disconnected_at: Arc<RwLock<Instant>>,
3150 mut reader: FrameReader<ReadHalf<DuplexStream>>,
3151 mut writer: WriteHalf<DuplexStream>,
3152 task_coordination_token: CancellationToken,
3155 debug_log_sampling_rate: Option<u64>,
3156 is_from_client: bool,
3159 ) {
3160 async fn wait_for_latency_elapse(
3164 queue: &VecDeque<(Bytes, Instant)>,
3165 network_flakiness: &NetworkFlakiness,
3166 rng: &mut impl rand::Rng,
3167 ) {
3168 if let Some((min, max)) = network_flakiness.latency_range {
3169 let diff = max.abs_diff(min);
3170 let factor = rng.random_range(0.0..=1.0);
3171 let latency = min + diff.mul_f64(factor);
3172 tokio::time::sleep_until(queue.front().unwrap().1 + latency).await;
3173 }
3174 }
3175
3176 let mut rng = rand::rngs::SmallRng::try_from_rng(&mut SysRng).unwrap();
3177 let mut queue: VecDeque<(Bytes, Instant)> = VecDeque::new();
3178 let mut send_count = 0u64;
3179
3180 loop {
3181 tokio::select! {
3182 read_res = reader.next() => {
3183 match read_res {
3184 Ok(Some((_, data))) => {
3185 queue.push_back((data, tokio::time::Instant::now()));
3186 }
3187 Ok(None) | Err(_) => {
3188 tracing::debug!("The upstream is closed or dropped. MockLink disconnects");
3189 break;
3190 }
3191 }
3192 }
3193 _ = wait_for_latency_elapse(&queue, &network_flakiness, &mut rng), if !queue.is_empty() => {
3194 let count = disconnected_count.load(Ordering::Relaxed);
3195 if network_flakiness.should_disconnect(&mut rng, count, &prev_disconnected_at).await {
3196 tracing::debug!("MockLink disconnects");
3197 disconnected_count.fetch_add(1, Ordering::Relaxed);
3198
3199 metrics::CHANNEL_RECONNECTIONS.add(
3200 1,
3201 hyperactor_telemetry::kv_pairs!(
3202 "transport" => "mock",
3203 "reason" => "network_flakiness",
3204 ),
3205 );
3206
3207 let mut w = prev_disconnected_at.write().unwrap();
3208 *w = tokio::time::Instant::now();
3209 break;
3210 }
3211 let data = queue.pop_front().unwrap().0;
3212 let is_sampled = debug_log_sampling_rate.is_some_and(|sample_rate| send_count % sample_rate == 1);
3213 if is_sampled {
3214 if is_from_client {
3215 if let Ok((Frame::Message(_seq, _msg), _)) = bincode::serde::decode_from_slice::<Frame<M>, _>(&data, bincode::config::legacy()) {
3216 tracing::debug!("MockLink relays a msg from client. msg type: {}", std::any::type_name::<M>());
3217 }
3218 } else {
3219 let result = deserialize_response(data.clone());
3220 if let Ok(NetRxResponse::Ack(seq)) = result {
3221 tracing::debug!("MockLink relays an ack from server. seq: {}", seq);
3222 }
3223 }
3224 }
3225 let mut fw = FrameWrite::new(writer, data, hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH), 0).unwrap();
3226 if fw.send().await.is_err() {
3227 break;
3228 }
3229 writer = fw.complete();
3230 send_count += 1;
3231 }
3232 _ = task_coordination_token.cancelled() => break,
3233
3234 changed = disconnect_signal.changed() => {
3235 tracing::debug!("MockLink disconnects per disconnect_signal {:?}", changed);
3236 break;
3237 }
3238 }
3239 }
3240
3241 task_coordination_token.cancel();
3242 }
3243
3244 let (server, mut server_relay) = tokio::io::duplex(self.buffer_size);
3245 let (client, client_relay) = tokio::io::duplex(self.buffer_size);
3246
3247 write_link_init(&mut server_relay, session_id, 0, ProtocolKind::Simplex)
3251 .await
3252 .map_err(|err| ClientError::Io(self.dest(), err))?;
3253
3254 let (server_r, server_writer) = tokio::io::split(server_relay);
3255 let (client_r, client_writer) = tokio::io::split(client_relay);
3256
3257 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
3258 let server_reader = FrameReader::new(server_r, max_len);
3259 let client_reader = FrameReader::new(client_r, max_len);
3260
3261 let task_coordination_token = CancellationToken::new();
3262 let _server_relay_task_handle = tokio::spawn(relay_message::<M>(
3263 self.disconnect_signal.subscribe(),
3264 self.network_flakiness.clone(),
3265 self.disconnected_count.clone(),
3266 self.prev_disconnected_at.clone(),
3267 server_reader,
3268 client_writer,
3269 task_coordination_token.clone(),
3270 self.debug_log_sampling_rate,
3271 false,
3272 ));
3273 let _client_relay_task_handle = tokio::spawn(relay_message::<M>(
3274 self.disconnect_signal.subscribe(),
3275 self.network_flakiness.clone(),
3276 self.disconnected_count.clone(),
3277 self.prev_disconnected_at.clone(),
3278 client_reader,
3279 server_writer,
3280 task_coordination_token,
3281 self.debug_log_sampling_rate,
3282 true,
3283 ));
3284
3285 self.receiver_storage.put(server).await;
3286 Ok(client)
3287 }
3288 }
3289
3290 struct MockLinkListener {
3291 receiver_storage: Arc<MVar<DuplexStream>>,
3292 channel_addr: ChannelAddr,
3293 }
3294
3295 impl MockLinkListener {
3296 fn new(receiver_storage: Arc<MVar<DuplexStream>>, channel_addr: ChannelAddr) -> Self {
3297 Self {
3298 receiver_storage,
3299 channel_addr,
3300 }
3301 }
3302 }
3303
3304 impl fmt::Debug for MockLinkListener {
3305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3306 f.debug_struct("MockLinkListener")
3307 .field("channel_addr", &self.channel_addr)
3308 .finish()
3309 }
3310 }
3311
3312 #[async_trait]
3313 impl super::Listener for MockLinkListener {
3314 type Stream = DuplexStream;
3315
3316 async fn accept(&mut self) -> Result<(Self::Stream, ChannelAddr), ServerError> {
3317 let stream = self.receiver_storage.take().await;
3318 Ok((stream, self.channel_addr.clone()))
3319 }
3320 }
3321
3322 fn serve_acceptor_test<M: RemoteMessage>(
3326 session_id: SessionId,
3327 ) -> (
3328 JoinHandle<()>,
3329 mpsc::UnboundedSender<DuplexStream>,
3330 mpsc::Receiver<M>,
3331 CancellationToken,
3332 ) {
3333 let (acceptor_tx, acceptor_rx) = mpsc::unbounded_channel::<DuplexStream>();
3334 let cancel_token = CancellationToken::new();
3335 let link = AcceptorLink {
3336 dest: ChannelAddr::Local(u64::MAX),
3337 session_id,
3338 stream: acceptor_rx,
3339 cancel: cancel_token.clone(),
3340 };
3341 let (tx, rx) = mpsc::channel::<M>(1024);
3342 let ct = cancel_token.clone();
3343 let handle = tokio::spawn(async move {
3344 let mut session = Session::new(link);
3345 let mut next = session::Next { seq: 0, ack: 0 };
3346
3347 loop {
3348 let connected = match session.connect().await {
3349 Ok(s) => s,
3350 Err(_) => break,
3351 };
3352
3353 let result = {
3354 let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
3355 tokio::select! {
3356 r = session::recv_connected::<M, _, _>(&stream, &tx, &mut next) => r,
3357 _ = ct.cancelled() => Err(session::RecvLoopError::Cancelled),
3358 }
3359 };
3360
3361 if next.ack < next.seq {
3363 let ack = serialize_response(NetRxResponse::Ack(next.seq - 1)).unwrap();
3364 let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
3365 let mut completion = stream.write(ack);
3366 match completion.drive().await {
3367 Ok(()) => {
3368 next.ack = next.seq;
3369 }
3370 Err(e) => {
3371 tracing::debug!(
3372 error = %e,
3373 "failed to flush acks during cleanup"
3374 );
3375 }
3376 }
3377 }
3378
3379 let terminal_response = match &result {
3381 Err(session::RecvLoopError::SequenceError(reason)) => {
3382 Some(NetRxResponse::Reject(reason.clone()))
3383 }
3384 Err(session::RecvLoopError::Cancelled) => Some(NetRxResponse::Closed),
3385 _ => None,
3386 };
3387 if let Some(rsp) = terminal_response {
3388 let data = serialize_response(rsp).unwrap();
3389 let stream = connected.stream(INITIATOR_TO_ACCEPTOR);
3390 let mut completion = stream.write(data);
3391 let _ = completion.drive().await;
3392 }
3393
3394 let recoverable = matches!(&result, Ok(()) | Err(session::RecvLoopError::Io(_)));
3395 session = connected.release();
3396 if recoverable {
3397 continue;
3398 }
3399 break;
3400 }
3401 });
3402 (handle, acceptor_tx, rx, cancel_token)
3403 }
3404
3405 async fn write_stream<M, W>(
3406 mut writer: W,
3407 _session_id: u64,
3408 messages: &[(u64, M)],
3409 _init: bool,
3410 ) -> W
3411 where
3412 M: RemoteMessage + PartialEq + Clone,
3413 W: AsyncWrite + Unpin,
3414 {
3415 for (seq, message) in messages {
3416 let message =
3417 serde_multipart::serialize_bincode(&Frame::<M>::Message(*seq, message.clone()))
3418 .unwrap();
3419 let mut fw = FrameWrite::new(
3420 writer,
3421 message.framed(),
3422 hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3423 0,
3424 )
3425 .map_err(|(_w, e)| e)
3426 .unwrap();
3427 fw.send().await.unwrap();
3428 writer = fw.complete();
3429 }
3430
3431 writer
3432 }
3433
3434 #[async_timed_test(timeout_secs = 60)]
3435 async fn test_persistent_server_session() {
3436 let config = hyperactor_config::global::lock();
3437 let _guard = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1);
3438
3439 async fn verify_ack(reader: &mut FrameReader<ReadHalf<DuplexStream>>, expected_last: u64) {
3440 let mut last_acked: i128 = -1;
3441 loop {
3442 let (_, bytes) = reader.next().await.unwrap().unwrap();
3443 let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
3444 assert!(
3445 acked as i128 > last_acked,
3446 "acks should be delivered in ascending order"
3447 );
3448 last_acked = acked as i128;
3449 assert!(acked <= expected_last);
3450 if acked == expected_last {
3451 break;
3452 }
3453 }
3454 }
3455
3456 let session_id = SessionId(123);
3457 let (_handle, acceptor_tx, mut rx, cancel_token) = serve_acceptor_test::<u64>(session_id);
3458
3459 {
3461 let (sender, receiver) = tokio::io::duplex(5000);
3462 acceptor_tx.send(receiver).unwrap();
3463
3464 let (r, writer) = tokio::io::split(sender);
3465 let mut reader = FrameReader::new(
3466 r,
3467 hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3468 );
3469
3470 let _writer = write_stream(
3471 writer,
3472 123,
3473 &[
3474 (0u64, 100u64),
3475 (1u64, 101u64),
3476 (2u64, 102u64),
3477 (3u64, 103u64),
3478 ],
3479 true,
3480 )
3481 .await;
3482
3483 assert_eq!(rx.recv().await, Some(100));
3484 assert_eq!(rx.recv().await, Some(101));
3485 assert_eq!(rx.recv().await, Some(102));
3486 assert_eq!(rx.recv().await, Some(103));
3487
3488 verify_ack(&mut reader, 3).await;
3489 }
3491
3492 {
3494 let (sender2, receiver2) = tokio::io::duplex(5000);
3495 acceptor_tx.send(receiver2).unwrap();
3496
3497 let (r2, writer2) = tokio::io::split(sender2);
3498 let mut reader2 = FrameReader::new(
3499 r2,
3500 hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3501 );
3502
3503 let _ = write_stream(
3504 writer2,
3505 123,
3506 &[
3507 (2u64, 102u64),
3508 (3u64, 103u64),
3509 (4u64, 104u64),
3510 (5u64, 105u64),
3511 ],
3512 true,
3513 )
3514 .await;
3515
3516 assert_eq!(rx.recv().await, Some(104));
3518 assert_eq!(rx.recv().await, Some(105));
3519
3520 verify_ack(&mut reader2, 5).await;
3521
3522 cancel_token.cancel();
3523 }
3524 }
3525
3526 #[async_timed_test(timeout_secs = 60)]
3527 async fn test_ack_from_server_session() {
3528 let config = hyperactor_config::global::lock();
3529 let _guard = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1);
3530 let session_id = SessionId(123);
3531 let (_handle, acceptor_tx, mut rx, cancel_token) = serve_acceptor_test::<u64>(session_id);
3532
3533 let (sender, receiver) = tokio::io::duplex(5000);
3534 acceptor_tx.send(receiver).unwrap();
3535 let (r, mut writer) = tokio::io::split(sender);
3536 let mut reader = FrameReader::new(
3537 r,
3538 hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3539 );
3540
3541 for i in 0u64..100u64 {
3542 writer = write_stream(writer, 123, &[(i, 100u64 + i)], i == 0u64).await;
3543 assert_eq!(rx.recv().await, Some(100u64 + i));
3544 let (_, bytes) = reader.next().await.unwrap().unwrap();
3545 let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
3546 assert_eq!(acked, i);
3547 }
3548
3549 tokio::time::sleep(Duration::from_secs(5)).await;
3551
3552 cancel_token.cancel();
3553
3554 let (_, bytes) = reader.next().await.unwrap().unwrap();
3556 assert!(deserialize_response(bytes).unwrap().is_closed());
3557 }
3558
3559 #[tracing_test::traced_test]
3560 async fn verify_tx_closed(tx_status: &mut watch::Receiver<TxStatus>, expected_log: &str) {
3561 match tokio::time::timeout(Duration::from_secs(5), tx_status.changed()).await {
3562 Ok(Ok(())) => {
3563 let current_status = tx_status.borrow().clone();
3564 assert!(current_status.is_closed());
3565 logs_assert_unscoped(|logs| {
3566 if logs.iter().any(|log| log.contains(expected_log)) {
3567 Ok(())
3568 } else {
3569 Err("expected log not found".to_string())
3570 }
3571 });
3572 }
3573 Ok(Err(_)) => panic!("watch::Receiver::changed() failed because sender is dropped."),
3574 Err(_) => panic!("timeout before tx_status changed"),
3575 }
3576 }
3577
3578 #[tracing_test::traced_test]
3579 #[tokio::test]
3580 #[cfg_attr(not(fbcode_build), ignore)]
3582 async fn test_tcp_tx_delivery_timeout() {
3583 let link = MockLink::<u64>::fail_connects();
3585 let tx = spawn::<u64>(link);
3586 let config = hyperactor_config::global::lock();
3588 let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(1));
3589 let mut tx_receiver = tx.status().clone();
3590 let _receipt = tx.try_post(123);
3591 verify_tx_closed(&mut tx_receiver, "failed to deliver message within timeout").await;
3592 }
3593
3594 async fn take_receiver(
3595 receiver_storage: &MVar<DuplexStream>,
3596 ) -> (FrameReader<ReadHalf<DuplexStream>>, WriteHalf<DuplexStream>) {
3597 let mut receiver = receiver_storage.take().await;
3598 let _link_init = read_link_init(&mut receiver).await.expect("read LinkInit");
3600 let (r, writer) = tokio::io::split(receiver);
3601 let reader = FrameReader::new(
3602 r,
3603 hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3604 );
3605 (reader, writer)
3606 }
3607
3608 async fn verify_message<M: RemoteMessage + PartialEq + std::fmt::Debug>(
3609 reader: &mut FrameReader<ReadHalf<DuplexStream>>,
3610 expect: (u64, M),
3611 loc: u32,
3612 ) {
3613 let expected = Frame::Message(expect.0, expect.1);
3614 let (_, bytes) = reader.next().await.unwrap().expect("unexpected EOF");
3615 let message = serde_multipart::Message::from_framed(bytes).unwrap();
3616 let frame: Frame<M> = serde_multipart::deserialize_bincode(message).unwrap();
3617
3618 assert_eq!(frame, expected, "from ln={loc}");
3619 }
3620
3621 async fn verify_stream<M: RemoteMessage + PartialEq + std::fmt::Debug + Clone>(
3622 reader: &mut FrameReader<ReadHalf<DuplexStream>>,
3623 expects: &[(u64, M)],
3624 _expect_session_id: Option<u64>,
3625 loc: u32,
3626 ) {
3627 for expect in expects {
3628 verify_message(reader, expect.clone(), loc).await;
3629 }
3630 }
3631
3632 async fn net_tx_send(tx: &ChannelTx<u64>, msgs: &[u64]) {
3633 for msg in msgs {
3634 tx.post(*msg);
3635 }
3636 }
3637
3638 #[async_timed_test(timeout_secs = 30)]
3640 async fn test_ack_in_net_tx_basic() {
3641 let link = MockLink::<u64>::new();
3642 let receiver_storage = link.receiver_storage();
3643 let tx = spawn::<u64>(link);
3644
3645 net_tx_send(&tx, &[100, 101, 102, 103, 104]).await;
3647 {
3648 let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3649 verify_stream(
3650 &mut reader,
3651 &[
3652 (0u64, 100u64),
3653 (1u64, 101u64),
3654 (2u64, 102u64),
3655 (3u64, 103u64),
3656 (4u64, 104u64),
3657 ],
3658 None,
3659 line!(),
3660 )
3661 .await;
3662
3663 for i in 0u64..5u64 {
3664 writer = FrameWrite::write_frame(
3665 writer,
3666 serialize_response(NetRxResponse::Ack(i)).unwrap(),
3667 1024,
3668 0,
3669 )
3670 .await
3671 .map_err(|(_, e)| e)
3672 .unwrap();
3673 }
3674 tokio::time::sleep(Duration::from_secs(3)).await;
3676 drop(reader);
3678 drop(writer);
3679 };
3680
3681 net_tx_send(&tx, &[105u64]).await;
3683 {
3684 let (mut reader, _writer) = take_receiver(&receiver_storage).await;
3685 verify_stream(&mut reader, &[(5u64, 105u64)], None, line!()).await;
3686 };
3688 }
3689
3690 #[async_timed_test(timeout_secs = 60)]
3692 async fn test_persistent_net_tx() {
3693 let link = MockLink::<u64>::new();
3694 let receiver_storage = link.receiver_storage();
3695
3696 let tx = spawn::<u64>(link);
3697
3698 net_tx_send(&tx, &[100, 101, 102, 103, 104]).await;
3700
3701 let n = 3;
3705
3706 for i in 0..n {
3709 {
3710 let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3711 verify_stream(
3712 &mut reader,
3713 &[
3714 (0u64, 100u64),
3715 (1u64, 101u64),
3716 (2u64, 102u64),
3717 (3u64, 103u64),
3718 (4u64, 104u64),
3719 ],
3720 None,
3721 line!(),
3722 )
3723 .await;
3724
3725 if i == n - 1 {
3728 writer = FrameWrite::write_frame(
3729 writer,
3730 serialize_response(NetRxResponse::Ack(1)).unwrap(),
3731 1024,
3732 0,
3733 )
3734 .await
3735 .map_err(|(_, e)| e)
3736 .unwrap();
3737 tokio::time::sleep(Duration::from_secs(3)).await;
3739 }
3740 drop(reader);
3742 drop(writer);
3743 };
3744 }
3745
3746 for _ in 0..n {
3748 {
3749 let (mut reader, mut _writer) = take_receiver(&receiver_storage).await;
3750 verify_stream(
3751 &mut reader,
3752 &[(2u64, 102u64), (3u64, 103u64), (4u64, 104u64)],
3753 None,
3754 line!(),
3755 )
3756 .await;
3757 };
3759 }
3760
3761 net_tx_send(&tx, &[105u64, 106u64, 107u64, 108u64, 109u64]).await;
3763 for i in 0..n {
3766 {
3767 let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3768 verify_stream(
3769 &mut reader,
3770 &[
3771 (2u64, 102u64),
3773 (3u64, 103u64),
3774 (4u64, 104u64),
3775 (5u64, 105u64),
3777 (6u64, 106u64),
3778 (7u64, 107u64),
3779 (8u64, 108u64),
3780 (9u64, 109u64),
3781 ],
3782 None,
3783 line!(),
3784 )
3785 .await;
3786
3787 if i == n - 1 {
3790 writer = FrameWrite::write_frame(
3793 writer,
3794 serialize_response(NetRxResponse::Ack(1)).unwrap(),
3795 1024,
3796 0,
3797 )
3798 .await
3799 .map_err(|(_, e)| e)
3800 .unwrap();
3801 writer = FrameWrite::write_frame(
3802 writer,
3803 serialize_response(NetRxResponse::Ack(2)).unwrap(),
3804 1024,
3805 0,
3806 )
3807 .await
3808 .map_err(|(_, e)| e)
3809 .unwrap();
3810 writer = FrameWrite::write_frame(
3811 writer,
3812 serialize_response(NetRxResponse::Ack(3)).unwrap(),
3813 1024,
3814 0,
3815 )
3816 .await
3817 .map_err(|(_, e)| e)
3818 .unwrap();
3819 tokio::time::sleep(Duration::from_secs(3)).await;
3821 }
3822 drop(reader);
3824 drop(writer);
3825 };
3826 }
3827
3828 for i in 0..n {
3829 {
3830 let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3831 verify_stream(
3832 &mut reader,
3833 &[
3834 (4u64, 104),
3836 (5u64, 105u64),
3838 (6u64, 106u64),
3839 (7u64, 107u64),
3840 (8u64, 108u64),
3841 (9u64, 109u64),
3842 ],
3843 None,
3844 line!(),
3845 )
3846 .await;
3847
3848 if i == n - 1 {
3850 writer = FrameWrite::write_frame(
3851 writer,
3852 serialize_response(NetRxResponse::Ack(7)).unwrap(),
3853 1024,
3854 0,
3855 )
3856 .await
3857 .map_err(|(_, e)| e)
3858 .unwrap();
3859 tokio::time::sleep(Duration::from_secs(3)).await;
3861 }
3862 drop(reader);
3864 drop(writer);
3865 };
3866 }
3867
3868 for _ in 0..n {
3869 {
3870 let (mut reader, writer) = take_receiver(&receiver_storage).await;
3871 verify_stream(
3872 &mut reader,
3873 &[
3874 (8u64, 108u64),
3876 (9u64, 109u64),
3877 ],
3878 None,
3879 line!(),
3880 )
3881 .await;
3882 drop(reader);
3884 drop(writer);
3885 };
3886 }
3887 }
3888
3889 #[async_timed_test(timeout_secs = 15)]
3890 async fn test_ack_before_redelivery_in_net_tx() {
3891 let link = MockLink::<u64>::new();
3892 let receiver_storage = link.receiver_storage();
3893 let net_tx = spawn::<u64>(link);
3894
3895 let receipt = net_tx.try_post(100);
3898 let (mut reader, mut writer) = take_receiver(&receiver_storage).await;
3899 verify_stream(&mut reader, &[(0u64, 100u64)], None, line!()).await;
3900 writer = FrameWrite::write_frame(
3902 writer,
3903 serialize_response(NetRxResponse::Ack(0)).unwrap(),
3904 1024,
3905 0,
3906 )
3907 .await
3908 .map_err(|(_, e)| e)
3909 .unwrap();
3910 assert!(receipt.await.is_ok());
3912
3913 let _ = FrameWrite::write_frame(
3918 writer,
3919 serialize_response(NetRxResponse::Ack(1)).unwrap(),
3920 1024,
3921 0,
3922 )
3923 .await
3924 .map_err(|(_, e)| e)
3925 .unwrap();
3926
3927 let receipt = net_tx.try_post(101);
3928 verify_message(&mut reader, (1u64, 101u64), line!()).await;
3930 assert!(receipt.await.is_ok());
3935 }
3936
3937 async fn verify_ack_exceeded_limit(disconnect_before_ack: bool) {
3938 let config = hyperactor_config::global::lock();
3940 let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(2));
3941
3942 let link: MockLink<u64> = MockLink::<u64>::new();
3943 let disconnect_signal = link.disconnect_signal().clone();
3944 let fail_connect_switch = link.fail_connects_switch();
3945 let receiver_storage = link.receiver_storage();
3946 let tx = spawn::<u64>(link);
3947 let mut tx_status = tx.status().clone();
3948 tx.post(100);
3950 let (mut reader, writer) = take_receiver(&receiver_storage).await;
3951 verify_stream(&mut reader, &[(0u64, 100u64)], None, line!()).await;
3953 let _ = FrameWrite::write_frame(
3955 writer,
3956 serialize_response(NetRxResponse::Ack(0)).unwrap(),
3957 hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
3958 0,
3959 )
3960 .await
3961 .map_err(|(_, e)| e)
3962 .unwrap();
3963 tokio::time::sleep(Duration::from_secs(3)).await;
3964 assert!(!tx_status.has_changed().unwrap());
3966 assert_eq!(*tx_status.borrow(), TxStatus::Active);
3967
3968 tx.post(101);
3969 verify_message(&mut reader, (1u64, 101u64), line!()).await;
3971
3972 if disconnect_before_ack {
3973 fail_connect_switch.store(true, Ordering::Release);
3975 disconnect_signal.send(()).unwrap();
3977 }
3978
3979 let expected_log: &str = if disconnect_before_ack {
3981 "failed to receive ack within timeout 2s; link is currently broken"
3982 } else {
3983 "failed to receive ack within timeout 2s; link is currently connected"
3984 };
3985
3986 verify_tx_closed(&mut tx_status, expected_log).await;
3987 }
3988
3989 #[tracing_test::traced_test]
3990 #[async_timed_test(timeout_secs = 30)]
3991 #[cfg_attr(not(fbcode_build), ignore)]
3993 async fn test_ack_exceeded_limit_with_connected_link() {
3994 verify_ack_exceeded_limit(false).await;
3995 }
3996
3997 #[tracing_test::traced_test]
3998 #[async_timed_test(timeout_secs = 30)]
3999 #[cfg_attr(not(fbcode_build), ignore)]
4001 async fn test_ack_exceeded_limit_with_broken_link() {
4002 verify_ack_exceeded_limit(true).await;
4003 }
4004
4005 #[async_timed_test(timeout_secs = 60)]
4008 async fn test_network_flakiness_in_channel() {
4009 hyperactor_telemetry::initialize_logging_for_test();
4010
4011 let sampling_rate = 100;
4012 let mut link = MockLink::<u64>::with_network_flakiness(NetworkFlakiness {
4013 disconnect_params: Some((0.001, 15, Duration::from_millis(400))),
4014 latency_range: Some((Duration::from_millis(100), Duration::from_millis(200))),
4015 });
4016 link.set_sampling_rate(sampling_rate);
4017 link.set_buffer_size(1024000);
4019 let disconnected_count = link.disconnected_count();
4020 let receiver_storage = link.receiver_storage();
4021 let listener = MockLinkListener::new(receiver_storage.clone(), link.dest());
4022 let local_addr = listener.channel_addr.clone();
4023 let (_, mut nx): (ChannelAddr, ChannelRx<u64>) =
4024 super::server::serve_with_listener(listener, local_addr).unwrap();
4025 let tx = spawn::<u64>(link);
4026 let messages: Vec<_> = (0..10001).collect();
4027 let messages_clone = messages.clone();
4028 let send_task_handle = tokio::spawn(async move {
4031 for message in messages_clone {
4032 tokio::time::sleep(Duration::from_micros(rand::random::<u64>() % 100)).await;
4044 tx.post(message);
4045 }
4046 tracing::debug!("ChannelTx sent all messages");
4047 tx
4050 });
4051
4052 for message in &messages {
4053 if message % sampling_rate == 0 {
4054 tracing::debug!("ChannelRx received a message: {message}");
4055 }
4056 assert_eq!(nx.recv().await.unwrap(), *message);
4057 }
4058 tracing::debug!("ChannelRx received all messages");
4059
4060 let send_result = send_task_handle.await;
4061 assert!(send_result.is_ok());
4062
4063 tracing::debug!(
4064 "MockLink disconnected {} times.",
4065 disconnected_count.load(Ordering::SeqCst)
4066 );
4067 }
4070
4071 #[async_timed_test(timeout_secs = 60)]
4072 async fn test_ack_every_n_messages() {
4073 let config = hyperactor_config::global::lock();
4074 let _guard_message_ack = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 600);
4075 let _guard_time_interval =
4076 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(1000));
4077 sparse_ack().await;
4078 }
4079
4080 #[async_timed_test(timeout_secs = 60)]
4081 async fn test_ack_every_time_interval() {
4082 let config = hyperactor_config::global::lock();
4083 let _guard_message_ack =
4084 config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 100000000);
4085 let _guard_time_interval = config.override_key(
4086 config::MESSAGE_ACK_TIME_INTERVAL,
4087 Duration::from_millis(500),
4088 );
4089 sparse_ack().await;
4090 }
4091
4092 async fn sparse_ack() {
4093 let mut link = MockLink::<u64>::new();
4094 link.set_buffer_size(1024000);
4096 let disconnected_count = link.disconnected_count();
4097 let receiver_storage = link.receiver_storage();
4098 let listener = MockLinkListener::new(receiver_storage.clone(), link.dest());
4099 let local_addr = listener.channel_addr.clone();
4100 let (_, mut nx): (ChannelAddr, ChannelRx<u64>) =
4101 super::server::serve_with_listener(listener, local_addr).unwrap();
4102 let tx = spawn::<u64>(link);
4103 let messages: Vec<_> = (0..20001).collect();
4104 let messages_clone = messages.clone();
4105 let send_task_handle = tokio::spawn(async move {
4108 for message in messages_clone {
4109 tokio::time::sleep(Duration::from_micros(rand::random::<u64>() % 100)).await;
4110 tx.post(message);
4111 }
4112 tokio::time::sleep(Duration::from_secs(5)).await;
4113 tracing::debug!("ChannelTx sent all messages");
4114 tx
4115 });
4116
4117 for message in &messages {
4118 assert_eq!(nx.recv().await.unwrap(), *message);
4119 }
4120 tracing::debug!("ChannelRx received all messages");
4121
4122 let send_result = send_task_handle.await;
4123 assert!(send_result.is_ok());
4124
4125 tracing::debug!(
4126 "MockLink disconnected {} times.",
4127 disconnected_count.load(Ordering::SeqCst)
4128 );
4129 }
4130
4131 #[test]
4132 fn test_metatls_parsing() {
4133 let channel: ChannelAddr = "metatls!localhost:1234".parse().unwrap();
4135 assert_eq!(
4136 channel,
4137 ChannelAddr::MetaTls(TlsAddr::new("localhost", 1234))
4138 );
4139 let channel: ChannelAddr = "metatls!1.2.3.4:1234".parse().unwrap();
4141 assert_eq!(channel, ChannelAddr::MetaTls(TlsAddr::new("1.2.3.4", 1234)));
4142 let channel: ChannelAddr = "metatls!2401:db00:33c:6902:face:0:2a2:0:1234"
4144 .parse()
4145 .unwrap();
4146 assert_eq!(
4147 channel,
4148 ChannelAddr::MetaTls(TlsAddr::new("2401:db00:33c:6902:face:0:2a2:0", 1234))
4149 );
4150
4151 let channel: ChannelAddr = "metatls![::]:1234".parse().unwrap();
4152 assert_eq!(channel, ChannelAddr::MetaTls(TlsAddr::new("::", 1234)));
4153 }
4154
4155 #[async_timed_test(timeout_secs = 300)]
4156 #[cfg_attr(not(fbcode_build), ignore)]
4158 async fn test_tcp_throughput() {
4159 let config = hyperactor_config::global::lock();
4160 let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_mins(5));
4161
4162 let socket_addr: SocketAddr = "[::1]:0".parse().unwrap();
4163 let (local_addr, mut rx) =
4164 server::serve::<String>(ChannelAddr::Tcp(socket_addr), None).unwrap();
4165
4166 let total_num_msgs = 500000;
4168
4169 let receive_handle = tokio::spawn(async move {
4170 let mut num = 0;
4171 for _ in 0..10 * total_num_msgs {
4172 rx.recv().await.unwrap();
4173 num += 1;
4174
4175 if num % 100000 == 0 {
4176 tracing::info!("total number of received messages: {}", num);
4177 }
4178 }
4179 });
4180
4181 let mut tx_handles = vec![];
4182 let mut txs = vec![];
4183 for _ in 0..10 {
4184 let server_addr = local_addr.clone();
4185 let tx = Arc::new(channel::dial::<String>(server_addr).unwrap());
4186 let tx2 = Arc::clone(&tx);
4187 txs.push(tx);
4188 tx_handles.push(tokio::spawn(async move {
4189 let random_string = rand::rng()
4190 .sample_iter(&Alphanumeric)
4191 .take(2048)
4192 .map(char::from)
4193 .collect::<String>();
4194 for _ in 0..total_num_msgs {
4195 tx2.post(random_string.clone());
4196 }
4197 }));
4198 }
4199
4200 receive_handle.await.unwrap();
4201 for handle in tx_handles {
4202 handle.await.unwrap();
4203 }
4204 }
4205
4206 #[tracing_test::traced_test]
4207 #[async_timed_test(timeout_secs = 60)]
4208 #[cfg_attr(not(fbcode_build), ignore)]
4210 async fn test_net_tx_closed_on_server_reject() {
4211 let link = MockLink::<u64>::new();
4212 let receiver_storage = link.receiver_storage();
4213 let mut tx = spawn::<u64>(link);
4214 net_tx_send(&tx, &[100]).await;
4215
4216 {
4217 let (_reader, writer) = take_receiver(&receiver_storage).await;
4218 let _ = FrameWrite::write_frame(
4219 writer,
4220 serialize_response(NetRxResponse::Reject("testing".to_string())).unwrap(),
4221 1024,
4222 0,
4223 )
4224 .await
4225 .map_err(|(_, e)| e);
4226
4227 tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
4230 }
4231
4232 verify_tx_closed(&mut tx.status, "server rejected connection").await;
4233 }
4234
4235 #[async_timed_test(timeout_secs = 60)]
4236 async fn test_server_rejects_conn_on_out_of_sequence_message() {
4237 let config = hyperactor_config::global::lock();
4238 let _guard = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1);
4239 let session_id = SessionId(123);
4240 let (_handle, acceptor_tx, mut rx, _cancel_token) = serve_acceptor_test::<u64>(session_id);
4241
4242 let (sender, receiver) = tokio::io::duplex(5000);
4243 acceptor_tx.send(receiver).unwrap();
4244 let (r, writer) = tokio::io::split(sender);
4245 let mut reader = FrameReader::new(
4246 r,
4247 hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH),
4248 );
4249
4250 let _ = write_stream(writer, 123, &[(0, 100u64), (1, 101u64), (3, 103u64)], true).await;
4251 assert_eq!(rx.recv().await, Some(100u64));
4252 assert_eq!(rx.recv().await, Some(101u64));
4253 let (_, bytes) = reader.next().await.unwrap().unwrap();
4254 let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
4255 assert_eq!(acked, 0);
4256 let (_, bytes) = reader.next().await.unwrap().unwrap();
4257 let acked = deserialize_response(bytes).unwrap().into_ack().unwrap();
4258 assert_eq!(acked, 1);
4259 let (_, bytes) = reader.next().await.unwrap().unwrap();
4260 assert!(deserialize_response(bytes).unwrap().is_reject());
4261 }
4262
4263 #[async_timed_test(timeout_secs = 60)]
4264 #[cfg_attr(not(fbcode_build), ignore)]
4266 async fn test_stop_net_tx_after_stopping_net_rx() {
4267 hyperactor_telemetry::initialize_logging_for_test();
4268
4269 let config = hyperactor_config::global::lock();
4270 let _guard = config.override_key(config::MESSAGE_DELIVERY_TIMEOUT, Duration::from_mins(5));
4271 let (addr, mut rx) =
4272 server::serve::<u64>(ChannelAddr::Tcp("[::1]:0".parse().unwrap()), None).unwrap();
4273 let socket_addr = match addr {
4274 ChannelAddr::Tcp(a) => a,
4275 _ => panic!("unexpected channel type"),
4276 };
4277 let tx: ChannelTx<u64> = spawn(tcp::link(
4278 socket_addr,
4279 SessionId::random(),
4280 0,
4281 ProtocolKind::Simplex,
4282 ));
4283 tx.send(100).await.unwrap();
4288 assert_eq!(rx.recv().await.unwrap(), 100);
4289 rx.server.stop("testing");
4291 assert!(rx.recv().await.is_err());
4292
4293 tx.post(101);
4296 let mut watcher = tx.status().clone();
4297 let _ = watcher.wait_for(|val| val.is_closed()).await;
4299 assert!(watcher.borrow().is_closed());
4303 }
4304
4305 struct QueueListener {
4309 streams: std::collections::VecDeque<DuplexStream>,
4310 addr: ChannelAddr,
4311 }
4312
4313 #[async_trait]
4314 impl super::Listener for QueueListener {
4315 type Stream = DuplexStream;
4316
4317 async fn accept(&mut self) -> Result<(DuplexStream, ChannelAddr), ServerError> {
4318 match self.streams.pop_front() {
4319 Some(s) => Ok((s, self.addr.clone())),
4320 None => std::future::pending().await,
4321 }
4322 }
4323 }
4324
4325 #[derive(Debug)]
4331 pub(super) struct GatedWriteStream {
4332 inner: DuplexStream,
4333 gate: Arc<GateState>,
4334 }
4335
4336 #[derive(Debug)]
4337 pub(super) struct GateState {
4338 open: AtomicBool,
4339 waker: std::sync::Mutex<Option<std::task::Waker>>,
4340 }
4341
4342 impl GateState {
4343 pub(super) fn new() -> Arc<Self> {
4344 Arc::new(Self {
4345 open: AtomicBool::new(false),
4346 waker: std::sync::Mutex::new(None),
4347 })
4348 }
4349
4350 pub(super) fn open(&self) {
4352 self.open.store(true, Ordering::Release);
4353 if let Some(w) = self.waker.lock().unwrap().take() {
4354 w.wake();
4355 }
4356 }
4357 }
4358
4359 impl GatedWriteStream {
4360 pub(super) fn new(inner: DuplexStream, gate: Arc<GateState>) -> Self {
4361 Self { inner, gate }
4362 }
4363 }
4364
4365 impl AsyncRead for GatedWriteStream {
4366 fn poll_read(
4367 mut self: std::pin::Pin<&mut Self>,
4368 cx: &mut std::task::Context<'_>,
4369 buf: &mut tokio::io::ReadBuf<'_>,
4370 ) -> std::task::Poll<std::io::Result<()>> {
4371 std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
4372 }
4373 }
4374
4375 impl AsyncWrite for GatedWriteStream {
4376 fn poll_write(
4377 mut self: std::pin::Pin<&mut Self>,
4378 cx: &mut std::task::Context<'_>,
4379 buf: &[u8],
4380 ) -> std::task::Poll<std::io::Result<usize>> {
4381 if !self.gate.open.load(Ordering::Acquire) {
4382 *self.gate.waker.lock().unwrap() = Some(cx.waker().clone());
4383 if !self.gate.open.load(Ordering::Acquire) {
4384 return std::task::Poll::Pending;
4385 }
4386 }
4387 std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
4388 }
4389
4390 fn poll_flush(
4391 mut self: std::pin::Pin<&mut Self>,
4392 cx: &mut std::task::Context<'_>,
4393 ) -> std::task::Poll<std::io::Result<()>> {
4394 std::pin::Pin::new(&mut self.inner).poll_flush(cx)
4395 }
4396
4397 fn poll_shutdown(
4398 mut self: std::pin::Pin<&mut Self>,
4399 cx: &mut std::task::Context<'_>,
4400 ) -> std::task::Poll<std::io::Result<()>> {
4401 std::pin::Pin::new(&mut self.inner).poll_shutdown(cx)
4402 }
4403 }
4404
4405 struct GatedQueueListener {
4409 streams: std::collections::VecDeque<GatedWriteStream>,
4410 addr: ChannelAddr,
4411 }
4412
4413 #[async_trait]
4414 impl super::Listener for GatedQueueListener {
4415 type Stream = GatedWriteStream;
4416
4417 async fn accept(&mut self) -> Result<(GatedWriteStream, ChannelAddr), ServerError> {
4418 match self.streams.pop_front() {
4419 Some(s) => Ok((s, self.addr.clone())),
4420 None => std::future::pending().await,
4421 }
4422 }
4423 }
4424
4425 struct PreparedConnection {
4428 server_side: DuplexStream,
4429 _client_w: tokio::io::WriteHalf<DuplexStream>,
4433 client_r: ReadHalf<DuplexStream>,
4434 }
4435
4436 async fn prepare_connection(
4442 session_id: SessionId,
4443 stream_id: u8,
4444 kind: super::ProtocolKind,
4445 messages: &[(u64, u64)],
4446 ) -> PreparedConnection {
4447 let (client_side, server_side) = tokio::io::duplex(8192);
4448 let (client_r, mut client_w) = tokio::io::split(client_side);
4449
4450 super::write_link_init(&mut client_w, session_id, stream_id, kind)
4451 .await
4452 .unwrap();
4453 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4454 for (seq, value) in messages {
4455 let payload =
4456 serde_multipart::serialize_bincode(&Frame::<u64>::Message(*seq, *value)).unwrap();
4457 let mut fw = FrameWrite::new(client_w, payload.framed(), max_len, 0)
4458 .map_err(|(_w, e)| e)
4459 .unwrap();
4460 fw.send().await.unwrap();
4461 client_w = fw.complete();
4462 }
4463
4464 PreparedConnection {
4465 server_side,
4466 _client_w: client_w,
4467 client_r,
4468 }
4469 }
4470
4471 struct SeparateSessionPlan {
4474 session_id: SessionId,
4475 stream_id: u8,
4476 messages: Vec<(u64, u64)>,
4477 }
4478
4479 async fn run_separate_sessions_flush_test(
4490 plans: Vec<SeparateSessionPlan>,
4491 stream_id_label: &str,
4492 ) {
4493 let config = hyperactor_config::global::lock();
4494 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
4495 let _g_time =
4496 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
4497
4498 let mut conns: Vec<PreparedConnection> = Vec::with_capacity(plans.len());
4500 let mut expected_messages: std::collections::HashSet<u64> =
4501 std::collections::HashSet::new();
4502 let mut expected_acks: Vec<u64> = Vec::with_capacity(plans.len());
4503 for plan in &plans {
4504 for (_seq, value) in &plan.messages {
4505 expected_messages.insert(*value);
4506 }
4507 expected_acks.push(plan.messages.iter().map(|(s, _)| *s).max().unwrap());
4508 conns.push(
4509 prepare_connection(
4510 plan.session_id,
4511 plan.stream_id,
4512 super::ProtocolKind::Simplex,
4513 &plan.messages,
4514 )
4515 .await,
4516 );
4517 }
4518
4519 let addr = ChannelAddr::Local(u64::MAX);
4520 let listener = QueueListener {
4521 streams: conns
4522 .iter_mut()
4523 .map(|c| {
4524 std::mem::replace(&mut c.server_side, tokio::io::duplex(1).0)
4526 })
4527 .collect(),
4528 addr: addr.clone(),
4529 };
4530 let (_addr, mut rx) = super::server::serve_with_listener::<u64, _>(listener, addr).unwrap();
4531
4532 let mut received: std::collections::HashSet<u64> = std::collections::HashSet::new();
4534 for _ in 0..expected_messages.len() {
4535 received.insert(rx.recv().await.unwrap());
4536 }
4537 assert_eq!(
4538 received, expected_messages,
4539 "{stream_id_label}: every produced message should reach the application"
4540 );
4541
4542 tokio::time::sleep(Duration::from_millis(100)).await;
4549
4550 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4551 let mut readers: Vec<FrameReader<ReadHalf<DuplexStream>>> = conns
4552 .into_iter()
4553 .map(|c| FrameReader::new(c.client_r, max_len))
4554 .collect();
4555 for (idx, reader) in readers.iter_mut().enumerate() {
4556 match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
4557 Err(_) => {} Ok(Err(e)) => panic!(
4559 "{stream_id_label}: connection {idx} frame reader error before rx.join: {e}"
4560 ),
4561 Ok(Ok(None)) => {
4562 panic!("{stream_id_label}: connection {idx} closed before rx.join()")
4563 }
4564 Ok(Ok(Some((_, bytes)))) => {
4565 let resp = super::deserialize_response(bytes).unwrap();
4566 panic!(
4567 "{stream_id_label}: connection {idx} unexpectedly received {resp:?} \
4568 before rx.join()"
4569 );
4570 }
4571 }
4572 }
4573
4574 rx.join().await;
4575
4576 for (idx, (reader, expected_ack)) in readers.iter_mut().zip(&expected_acks).enumerate() {
4580 let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4581 .await
4582 .unwrap_or_else(|_| {
4583 panic!(
4584 "{stream_id_label}: connection {idx} produced no Ack frame within 50ms \
4585 after rx.join()"
4586 )
4587 })
4588 .expect("frame reader error")
4589 .expect("frame reader returned None");
4590 let acked = super::deserialize_response(bytes.1)
4591 .unwrap()
4592 .into_ack()
4593 .unwrap_or_else(|other| {
4594 panic!("{stream_id_label}: connection {idx} expected Ack, got {other:?}")
4595 });
4596 assert_eq!(
4597 acked, *expected_ack,
4598 "{stream_id_label}: connection {idx} ack mismatch"
4599 );
4600
4601 let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4602 .await
4603 .unwrap_or_else(|_| {
4604 panic!(
4605 "{stream_id_label}: connection {idx} produced no Closed frame within 50ms"
4606 )
4607 })
4608 .expect("frame reader error")
4609 .expect("frame reader returned None");
4610 assert!(
4611 super::deserialize_response(bytes.1).unwrap().is_closed(),
4612 "{stream_id_label}: connection {idx} expected Closed terminal frame"
4613 );
4614 }
4615 }
4616
4617 #[async_timed_test(timeout_secs = 30)]
4618 async fn rx_join_flushes_pending_ack_single_stream() {
4619 let plans = (1u64..=3)
4623 .map(|sid| SeparateSessionPlan {
4624 session_id: SessionId(sid),
4625 stream_id: 0,
4626 messages: (0u64..3).map(|seq| (seq, sid * 100 + seq)).collect(),
4627 })
4628 .collect();
4629 run_separate_sessions_flush_test(plans, "single-stream").await;
4630 }
4631
4632 #[async_timed_test(timeout_secs = 30)]
4633 async fn rx_join_flushes_pending_ack_multi_stream() {
4634 let plans = (1u64..=3)
4639 .map(|sid| SeparateSessionPlan {
4640 session_id: SessionId(sid),
4641 stream_id: 1,
4642 messages: (0u64..3).map(|seq| (seq, sid * 100 + seq)).collect(),
4643 })
4644 .collect();
4645 run_separate_sessions_flush_test(plans, "multi-stream").await;
4646 }
4647
4648 #[async_timed_test(timeout_secs = 30)]
4656 async fn rx_join_flushes_pending_ack_shared_multi_stream_session() {
4657 let config = hyperactor_config::global::lock();
4658 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
4659 let _g_time =
4660 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
4661
4662 let session_id = SessionId(99);
4663 let num_streams = 3u8;
4664 let msgs_per_stream = 3u64;
4665 let mut conns: Vec<PreparedConnection> = Vec::with_capacity(num_streams as usize);
4666 let mut expected_messages: std::collections::HashSet<u64> =
4667 std::collections::HashSet::new();
4668 for stream_id in 1..=num_streams {
4669 let messages: Vec<(u64, u64)> = (0u64..msgs_per_stream)
4670 .map(|i| {
4671 let seq = (stream_id as u64 - 1) * msgs_per_stream + i;
4672 (seq, 1000 + seq)
4673 })
4674 .collect();
4675 for (_, v) in &messages {
4676 expected_messages.insert(*v);
4677 }
4678 conns.push(
4679 prepare_connection(
4680 session_id,
4681 stream_id,
4682 super::ProtocolKind::Simplex,
4683 &messages,
4684 )
4685 .await,
4686 );
4687 }
4688 let highest_seq = num_streams as u64 * msgs_per_stream - 1;
4689
4690 let addr = ChannelAddr::Local(u64::MAX);
4691 let listener = QueueListener {
4692 streams: conns
4693 .iter_mut()
4694 .map(|c| std::mem::replace(&mut c.server_side, tokio::io::duplex(1).0))
4695 .collect(),
4696 addr: addr.clone(),
4697 };
4698 let (_addr, mut rx) = super::server::serve_with_listener::<u64, _>(listener, addr).unwrap();
4699
4700 let mut received: std::collections::HashSet<u64> = std::collections::HashSet::new();
4701 for _ in 0..expected_messages.len() {
4702 received.insert(rx.recv().await.unwrap());
4703 }
4704 assert_eq!(
4705 received, expected_messages,
4706 "shared-session multi-stream: every message reaches the application"
4707 );
4708
4709 tokio::time::sleep(Duration::from_millis(100)).await;
4710
4711 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4712 let mut readers: Vec<FrameReader<ReadHalf<DuplexStream>>> = conns
4713 .into_iter()
4714 .map(|c| FrameReader::new(c.client_r, max_len))
4715 .collect();
4716 for (idx, reader) in readers.iter_mut().enumerate() {
4717 match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
4718 Err(_) | Ok(Ok(None)) => {} Ok(Err(e)) => {
4720 panic!("shared-session multi-stream: stream {idx} frame reader error: {e}")
4721 }
4722 Ok(Ok(Some((_, bytes)))) => {
4723 let resp = super::deserialize_response(bytes).unwrap();
4724 panic!(
4725 "shared-session multi-stream: stream {idx} unexpectedly received \
4726 {resp:?} before rx.join()"
4727 );
4728 }
4729 }
4730 }
4731
4732 rx.join().await;
4733
4734 let mut ack_count = 0;
4739 let mut closed_count = 0;
4740 for (idx, reader) in readers.iter_mut().enumerate() {
4741 loop {
4742 match tokio::time::timeout(Duration::from_millis(50), reader.next()).await {
4743 Err(_) => panic!(
4744 "shared-session multi-stream: stream {idx} did not yield expected \
4745 frames within 50ms after rx.join()"
4746 ),
4747 Ok(Err(e)) => panic!("frame reader error: {e}"),
4748 Ok(Ok(None)) => break,
4749 Ok(Ok(Some((_, bytes)))) => {
4750 let resp = super::deserialize_response(bytes).unwrap();
4751 match resp {
4752 NetRxResponse::Ack(seq) => {
4753 assert_eq!(
4754 seq, highest_seq,
4755 "shared-session multi-stream: ack should cover the full \
4756 contiguous range 0..={highest_seq}"
4757 );
4758 ack_count += 1;
4759 }
4760 NetRxResponse::Closed => {
4761 closed_count += 1;
4762 break;
4763 }
4764 other => panic!(
4765 "shared-session multi-stream: stream {idx} unexpected {other:?}"
4766 ),
4767 }
4768 }
4769 }
4770 }
4771 }
4772 assert!(
4773 ack_count >= 1,
4774 "shared-session multi-stream: expected at least one Ack({highest_seq}); \
4775 got {ack_count}"
4776 );
4777 assert!(
4778 ack_count <= num_streams as usize,
4779 "shared-session multi-stream: expected at most {num_streams} Ack({highest_seq}) \
4780 frames; got {ack_count}"
4781 );
4782 assert_eq!(
4783 closed_count, num_streams as usize,
4784 "shared-session multi-stream: every stream should emit its own Closed frame"
4785 );
4786 }
4787
4788 #[async_timed_test(timeout_secs = 30)]
4795 async fn duplex_test_listener_rejects_simplex_link_init() {
4796 let conn =
4797 prepare_connection(SessionId(1), 0, super::ProtocolKind::Simplex, &[(0, 123)]).await;
4798 let addr = ChannelAddr::Local(u64::MAX);
4799 let listener = QueueListener {
4800 streams: std::collections::VecDeque::from([conn.server_side]),
4801 addr: addr.clone(),
4802 };
4803
4804 let mut server = super::duplex::serve_with_listener::<u64, u64, _>(listener, addr).unwrap();
4805
4806 assert!(
4807 tokio::time::timeout(Duration::from_millis(500), server.accept())
4808 .await
4809 .is_err(),
4810 "duplex test server accepted a simplex LinkInit",
4811 );
4812 }
4813
4814 #[async_timed_test(timeout_secs = 30)]
4821 async fn server_join_flushes_pending_ack_duplex_session() {
4822 let config = hyperactor_config::global::lock();
4823 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
4824 let _g_time =
4825 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
4826
4827 let session_count = 3u64;
4828 let msgs_per_session = 3u64;
4829 let mut conns: Vec<PreparedConnection> = Vec::with_capacity(session_count as usize);
4830 let mut expected_messages: std::collections::HashSet<u64> =
4831 std::collections::HashSet::new();
4832 let mut expected_acks: Vec<u64> = Vec::with_capacity(session_count as usize);
4833 for sid in 1..=session_count {
4834 let messages: Vec<(u64, u64)> = (0u64..msgs_per_session)
4835 .map(|seq| (seq, sid * 100 + seq))
4836 .collect();
4837 for (_, v) in &messages {
4838 expected_messages.insert(*v);
4839 }
4840 expected_acks.push(messages.iter().map(|(s, _)| *s).max().unwrap());
4841 conns.push(
4842 prepare_connection(SessionId(sid), 0, super::ProtocolKind::Duplex, &messages).await,
4843 );
4844 }
4845
4846 let addr = ChannelAddr::Local(u64::MAX);
4847 let listener = QueueListener {
4848 streams: conns
4849 .iter_mut()
4850 .map(|c| std::mem::replace(&mut c.server_side, tokio::io::duplex(1).0))
4851 .collect(),
4852 addr: addr.clone(),
4853 };
4854 let mut server = super::duplex::serve_with_listener::<u64, u64, _>(listener, addr).unwrap();
4855
4856 let mut all_rx: Vec<super::duplex::DuplexRx<u64>> =
4862 Vec::with_capacity(session_count as usize);
4863 let mut all_tx: Vec<super::duplex::DuplexTx<u64>> =
4864 Vec::with_capacity(session_count as usize);
4865 for _ in 0..session_count {
4866 let (rx, tx) = server.accept().await.unwrap();
4867 all_rx.push(rx);
4868 all_tx.push(tx);
4869 }
4870
4871 let mut received: std::collections::HashSet<u64> = std::collections::HashSet::new();
4876 for rx in all_rx.iter_mut() {
4877 for _ in 0..msgs_per_session {
4878 received.insert(rx.recv().await.unwrap());
4879 }
4880 }
4881 assert_eq!(
4882 received, expected_messages,
4883 "duplex: every produced message should reach the application"
4884 );
4885
4886 tokio::time::sleep(Duration::from_millis(100)).await;
4890
4891 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
4892 let mut readers: Vec<FrameReader<ReadHalf<DuplexStream>>> = conns
4893 .into_iter()
4894 .map(|c| FrameReader::new(c.client_r, max_len))
4895 .collect();
4896 for (idx, reader) in readers.iter_mut().enumerate() {
4897 match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
4898 Err(_) => {} Ok(Err(e)) => {
4900 panic!("duplex: connection {idx} frame reader error before join: {e}")
4901 }
4902 Ok(Ok(None)) => {
4903 panic!("duplex: connection {idx} closed before server.join()")
4904 }
4905 Ok(Ok(Some((_, bytes)))) => {
4906 let resp = super::deserialize_response(bytes).unwrap();
4907 panic!(
4908 "duplex: connection {idx} unexpectedly received {resp:?} \
4909 before server.join()"
4910 );
4911 }
4912 }
4913 }
4914
4915 server.join().await;
4921
4922 for (idx, (reader, expected_ack)) in readers.iter_mut().zip(&expected_acks).enumerate() {
4927 let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4928 .await
4929 .unwrap_or_else(|_| {
4930 panic!(
4931 "duplex: connection {idx} produced no Ack frame within 50ms after \
4932 server.join()"
4933 )
4934 })
4935 .expect("frame reader error")
4936 .expect("frame reader returned None");
4937 let acked = super::deserialize_response(bytes.1)
4938 .unwrap()
4939 .into_ack()
4940 .unwrap_or_else(|other| {
4941 panic!("duplex: connection {idx} expected Ack, got {other:?}")
4942 });
4943 assert_eq!(
4944 acked, *expected_ack,
4945 "duplex: connection {idx} ack mismatch"
4946 );
4947
4948 let bytes = tokio::time::timeout(Duration::from_millis(50), reader.next())
4949 .await
4950 .unwrap_or_else(|_| {
4951 panic!("duplex: connection {idx} produced no Closed frame within 50ms")
4952 })
4953 .expect("frame reader error")
4954 .expect("frame reader returned None");
4955 assert!(
4956 super::deserialize_response(bytes.1).unwrap().is_closed(),
4957 "duplex: connection {idx} expected Closed terminal frame"
4958 );
4959 }
4960 }
4961
4962 struct DuplexDialMockLink {
4967 session_id: SessionId,
4968 streams: std::collections::VecDeque<DuplexStream>,
4969 }
4970
4971 impl fmt::Debug for DuplexDialMockLink {
4972 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4973 f.debug_struct("DuplexDialMockLink")
4974 .field("session_id", &self.session_id)
4975 .field("remaining_streams", &self.streams.len())
4976 .finish()
4977 }
4978 }
4979
4980 #[async_trait]
4981 impl super::Link for DuplexDialMockLink {
4982 type Stream = DuplexStream;
4983
4984 fn dest(&self) -> ChannelAddr {
4985 ChannelAddr::Local(u64::MAX)
4986 }
4987
4988 fn link_id(&self) -> SessionId {
4989 self.session_id
4990 }
4991
4992 async fn next(&mut self) -> Result<DuplexStream, ClientError> {
4993 match self.streams.pop_front() {
4994 Some(mut stream) => {
4995 super::write_link_init(
4996 &mut stream,
4997 self.session_id,
4998 0,
4999 super::ProtocolKind::Duplex,
5000 )
5001 .await
5002 .map_err(|err| ClientError::Io(self.dest(), err))?;
5003 Ok(stream)
5004 }
5005 None => Err(ClientError::Connect(
5006 self.dest(),
5007 std::io::Error::other("mock link exhausted"),
5008 "no more streams".into(),
5009 )),
5010 }
5011 }
5012 }
5013
5014 #[async_timed_test(timeout_secs = 30)]
5023 async fn duplex_serve_flushes_pending_ack_on_app_closed() {
5024 let config = hyperactor_config::global::lock();
5025 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5026 let _g_time =
5027 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5028
5029 let session_id = SessionId(1);
5030 let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5031 let expected_ack: u64 = 2;
5032 let conn = prepare_connection(session_id, 0, super::ProtocolKind::Duplex, &messages).await;
5033
5034 let addr = ChannelAddr::Local(u64::MAX);
5035 let listener = QueueListener {
5036 streams: std::collections::VecDeque::from([conn.server_side]),
5037 addr: addr.clone(),
5038 };
5039
5040 let mut server = super::duplex::serve_with_listener::<u64, u64, _>(listener, addr).unwrap();
5041
5042 let (mut server_rx, server_tx) = server.accept().await.unwrap();
5043
5044 let mut received: Vec<u64> = Vec::with_capacity(messages.len());
5046 for _ in &messages {
5047 received.push(server_rx.recv().await.unwrap());
5048 }
5049 let expected_values: Vec<u64> = messages.iter().map(|(_, v)| *v).collect();
5050 assert_eq!(
5051 received, expected_values,
5052 "duplex serve: every message should reach the application"
5053 );
5054
5055 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5058 tokio::time::sleep(Duration::from_millis(100)).await;
5059 let mut reader = FrameReader::new(conn.client_r, max_len);
5060 match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5061 Err(_) => {} Ok(Err(e)) => panic!("duplex serve: frame reader error before app close: {e}"),
5063 Ok(Ok(None)) => panic!("duplex serve: wire closed before app close"),
5064 Ok(Ok(Some((_, bytes)))) => {
5065 let resp = super::deserialize_response(bytes).unwrap();
5066 panic!("duplex serve: unexpectedly received {resp:?} before app close");
5067 }
5068 }
5069
5070 drop(server_tx);
5076
5077 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5078 .await
5079 .unwrap_or_else(|_| panic!("duplex serve: produced no Ack frame within 100ms"))
5080 .expect("frame reader error")
5081 .expect("frame reader returned None");
5082 let acked = super::deserialize_response(bytes.1)
5083 .unwrap()
5084 .into_ack()
5085 .unwrap_or_else(|other| panic!("duplex serve: expected Ack, got {other:?}"));
5086 assert_eq!(
5087 acked, expected_ack,
5088 "duplex serve: ack should cover the highest seq received"
5089 );
5090
5091 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5092 .await
5093 .unwrap_or_else(|_| panic!("duplex serve: produced no Closed frame within 100ms"))
5094 .expect("frame reader error")
5095 .expect("frame reader returned None");
5096 assert!(
5097 super::deserialize_response(bytes.1).unwrap().is_closed(),
5098 "duplex serve: expected Closed terminal frame after Ack"
5099 );
5100
5101 drop(server_rx);
5102 server.join().await;
5107 }
5108
5109 #[async_timed_test(timeout_secs = 30)]
5119 async fn mux_duplex_join_flushes_pending_ack_for_session() {
5120 let config = hyperactor_config::global::lock();
5121 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5122 let _g_time =
5123 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5124
5125 let session_id = SessionId(1);
5126 let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5127 let expected_ack: u64 = 2;
5128 let conn = prepare_connection(session_id, 0, super::ProtocolKind::Duplex, &messages).await;
5129
5130 let addr = ChannelAddr::Local(u64::MAX);
5131 let listener = QueueListener {
5132 streams: std::collections::VecDeque::from([conn.server_side]),
5133 addr: addr.clone(),
5134 };
5135
5136 let mut parts =
5137 super::mux::serve_with_listener::<u64, u64, u64, _>(listener, addr).unwrap();
5138
5139 let (mut server_rx, _server_tx) = parts.duplex.accept().await.unwrap();
5144 for _ in &messages {
5145 server_rx.recv().await.unwrap();
5146 }
5147
5148 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5150 let mut reader = FrameReader::new(conn.client_r, max_len);
5151 tokio::time::sleep(Duration::from_millis(50)).await;
5152 match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5153 Err(_) => {} Ok(Err(e)) => panic!("mux: frame reader error before shutdown: {e}"),
5155 Ok(Ok(None)) => panic!("mux: wire closed before shutdown"),
5156 Ok(Ok(Some((_, bytes)))) => {
5157 let resp = super::deserialize_response(bytes).unwrap();
5158 panic!("mux: unexpectedly received {resp:?} before shutdown");
5159 }
5160 }
5161
5162 parts.duplex.stop("test mux duplex join flush");
5168 parts.duplex.join().await;
5169
5170 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5171 .await
5172 .unwrap_or_else(|_| panic!("mux: produced no Ack frame within 100ms after join"))
5173 .expect("frame reader error")
5174 .expect("frame reader returned None");
5175 let acked = super::deserialize_response(bytes.1)
5176 .unwrap()
5177 .into_ack()
5178 .unwrap_or_else(|other| panic!("mux: expected Ack, got {other:?}"));
5179 assert_eq!(
5180 acked, expected_ack,
5181 "mux: ack should cover the highest seq received"
5182 );
5183
5184 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5185 .await
5186 .unwrap_or_else(|_| panic!("mux: produced no Closed frame within 100ms after join"))
5187 .expect("frame reader error")
5188 .expect("frame reader returned None");
5189 assert!(
5190 super::deserialize_response(bytes.1).unwrap().is_closed(),
5191 "mux: expected Closed terminal frame after Ack"
5192 );
5193
5194 let _ = parts.simplex;
5196 let _ = parts.join_handle.await;
5197 }
5198
5199 #[async_timed_test(timeout_secs = 30)]
5209 async fn mux_duplex_join_blocks_until_terminal_write_completes() {
5210 let config = hyperactor_config::global::lock();
5211 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5212 let _g_time =
5213 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5214
5215 let session_id = SessionId(1);
5216 let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5217 let expected_ack: u64 = 2;
5218 let conn = prepare_connection(session_id, 0, super::ProtocolKind::Duplex, &messages).await;
5219
5220 let gate = GateState::new();
5223 let server_side_gated = GatedWriteStream::new(conn.server_side, gate.clone());
5224
5225 let addr = ChannelAddr::Local(u64::MAX);
5226 let listener = GatedQueueListener {
5227 streams: std::collections::VecDeque::from([server_side_gated]),
5228 addr: addr.clone(),
5229 };
5230
5231 let mut parts =
5232 super::mux::serve_with_listener::<u64, u64, u64, _>(listener, addr).unwrap();
5233
5234 let (mut server_rx, _server_tx) = parts.duplex.accept().await.unwrap();
5235 for _ in &messages {
5236 server_rx.recv().await.unwrap();
5237 }
5238
5239 parts
5245 .duplex
5246 .stop("test mux duplex join blocks until terminal write");
5247 let mut join_task = tokio::spawn(async move { parts.duplex.join().await });
5248
5249 let blocked = tokio::time::timeout(Duration::from_millis(200), &mut join_task).await;
5250 assert!(
5251 blocked.is_err(),
5252 "duplex.join() returned before the gated terminal write could complete"
5253 );
5254
5255 gate.open();
5258
5259 tokio::time::timeout(Duration::from_secs(5), join_task)
5260 .await
5261 .expect("duplex.join() did not resolve after gate opened")
5262 .expect("join task panicked");
5263
5264 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5266 let mut reader = FrameReader::new(conn.client_r, max_len);
5267
5268 let bytes = tokio::time::timeout(Duration::from_millis(200), reader.next())
5269 .await
5270 .expect("no Ack frame")
5271 .expect("frame reader error")
5272 .expect("frame reader returned None");
5273 let acked = super::deserialize_response(bytes.1)
5274 .unwrap()
5275 .into_ack()
5276 .unwrap_or_else(|other| panic!("expected Ack, got {other:?}"));
5277 assert_eq!(acked, expected_ack);
5278
5279 let bytes = tokio::time::timeout(Duration::from_millis(200), reader.next())
5280 .await
5281 .expect("no Closed frame")
5282 .expect("frame reader error")
5283 .expect("frame reader returned None");
5284 assert!(super::deserialize_response(bytes.1).unwrap().is_closed());
5285 }
5286
5287 #[async_timed_test(timeout_secs = 30)]
5294 async fn mux_split_simplex_join_flushes_final_ack() {
5295 let config = hyperactor_config::global::lock();
5296 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5297 let _g_time =
5298 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5299
5300 let session_id = SessionId(1);
5301 let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5302 let expected_ack: u64 = 2;
5303 let conn = prepare_connection(session_id, 0, super::ProtocolKind::Simplex, &messages).await;
5304
5305 let addr = ChannelAddr::Local(u64::MAX);
5306 let listener = QueueListener {
5307 streams: std::collections::VecDeque::from([conn.server_side]),
5308 addr: addr.clone(),
5309 };
5310
5311 let mut parts =
5312 super::mux::serve_with_listener::<u64, u64, u64, _>(listener, addr).unwrap();
5313
5314 for _ in &messages {
5316 parts.simplex.recv().await.unwrap();
5317 }
5318
5319 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5321 let mut reader = FrameReader::new(conn.client_r, max_len);
5322 tokio::time::sleep(Duration::from_millis(50)).await;
5323 match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5324 Err(_) => {}
5325 Ok(Err(e)) => panic!("mux split: frame reader error before join: {e}"),
5326 Ok(Ok(None)) => panic!("mux split: wire closed before join"),
5327 Ok(Ok(Some((_, bytes)))) => {
5328 let resp = super::deserialize_response(bytes).unwrap();
5329 panic!("mux split: unexpectedly received {resp:?} before join");
5330 }
5331 }
5332
5333 parts.simplex.join().await;
5338
5339 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5340 .await
5341 .unwrap_or_else(|_| panic!("mux split: produced no Ack frame within 100ms after join"))
5342 .expect("frame reader error")
5343 .expect("frame reader returned None");
5344 let acked = super::deserialize_response(bytes.1)
5345 .unwrap()
5346 .into_ack()
5347 .unwrap_or_else(|other| panic!("mux split: expected Ack, got {other:?}"));
5348 assert_eq!(
5349 acked, expected_ack,
5350 "mux split: simplex.join must flush the final ack before returning"
5351 );
5352
5353 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5354 .await
5355 .unwrap_or_else(|_| {
5356 panic!("mux split: produced no Closed frame within 100ms after join")
5357 })
5358 .expect("frame reader error")
5359 .expect("frame reader returned None");
5360 assert!(
5361 super::deserialize_response(bytes.1).unwrap().is_closed(),
5362 "mux split: expected Closed terminal frame after Ack"
5363 );
5364
5365 let _ = parts.duplex;
5367 parts.cancel.cancel();
5368 let _ = parts.join_handle.await;
5369 }
5370
5371 #[async_timed_test(timeout_secs = 30)]
5378 async fn duplex_client_stop_then_join_flushes_pending_ack() {
5379 let config = hyperactor_config::global::lock();
5380 let _g_msg = config.override_key(config::MESSAGE_ACK_EVERY_N_MESSAGES, 1_000_000);
5381 let _g_time =
5382 config.override_key(config::MESSAGE_ACK_TIME_INTERVAL, Duration::from_secs(3600));
5383
5384 let session_id = SessionId(123);
5385 let (client_side, server_side) = tokio::io::duplex(8192);
5386 let (mut test_r, mut test_w) = tokio::io::split(server_side);
5387
5388 let link = DuplexDialMockLink {
5389 session_id,
5390 streams: std::collections::VecDeque::from([client_side]),
5391 };
5392
5393 let mut dial_client = super::duplex::spawn::<u64, u64>(link);
5394 let _dial_tx = dial_client.tx();
5395 let mut dial_rx = dial_client.take_rx().unwrap();
5396
5397 super::read_link_init(&mut test_r).await.unwrap();
5399
5400 let max_len = hyperactor_config::global::get(config::CODEC_MAX_FRAME_LENGTH);
5405 let messages: Vec<(u64, u64)> = vec![(0, 100), (1, 200), (2, 300)];
5406 let expected_ack: u64 = 2;
5407 for (seq, value) in &messages {
5408 let payload =
5409 serde_multipart::serialize_bincode(&Frame::<u64>::Message(*seq, *value)).unwrap();
5410 let mut fw = FrameWrite::new(
5411 test_w,
5412 payload.framed(),
5413 max_len,
5414 super::ACCEPTOR_TO_INITIATOR,
5415 )
5416 .map_err(|(_w, e)| e)
5417 .unwrap();
5418 fw.send().await.unwrap();
5419 test_w = fw.complete();
5420 }
5421
5422 let mut received: Vec<u64> = Vec::with_capacity(messages.len());
5425 for _ in &messages {
5426 received.push(dial_rx.recv().await.unwrap());
5427 }
5428 let expected_values: Vec<u64> = messages.iter().map(|(_, v)| *v).collect();
5429 assert_eq!(
5430 received, expected_values,
5431 "dial: every message should reach the application"
5432 );
5433
5434 tokio::time::sleep(Duration::from_millis(100)).await;
5437 let mut reader = FrameReader::new(test_r, max_len);
5438 match tokio::time::timeout(Duration::from_millis(10), reader.next()).await {
5439 Err(_) => {} Ok(Err(e)) => panic!("dial: frame reader error before join: {e}"),
5441 Ok(Ok(None)) => panic!("dial: wire closed before join"),
5442 Ok(Ok(Some((_, bytes)))) => {
5443 let resp = super::deserialize_response(bytes).unwrap();
5444 panic!("dial: unexpectedly received {resp:?} before join");
5445 }
5446 }
5447
5448 dial_client.join().await;
5453
5454 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5455 .await
5456 .unwrap_or_else(|_| panic!("dial: produced no Ack frame within 100ms after join"))
5457 .expect("frame reader error")
5458 .expect("frame reader returned None");
5459 let acked = super::deserialize_response(bytes.1)
5460 .unwrap()
5461 .into_ack()
5462 .unwrap_or_else(|other| panic!("dial: expected Ack, got {other:?}"));
5463 assert_eq!(
5464 acked, expected_ack,
5465 "dial: ack should cover the highest seq received"
5466 );
5467
5468 let bytes = tokio::time::timeout(Duration::from_millis(100), reader.next())
5472 .await
5473 .unwrap_or_else(|_| panic!("dial: produced no Closed frame within 100ms after join"))
5474 .expect("frame reader error")
5475 .expect("frame reader returned None");
5476 assert!(
5477 super::deserialize_response(bytes.1).unwrap().is_closed(),
5478 "dial: expected Closed terminal frame after Ack"
5479 );
5480
5481 drop(dial_rx);
5482 drop(test_w);
5483 }
5484
5485 #[async_timed_test(timeout_secs = 30)]
5493 async fn duplex_client_stop_then_join_terminates_in_progress_recv() {
5494 let session_id = SessionId(123);
5495 let (client_side, server_side) = tokio::io::duplex(8192);
5496 let (mut test_r, _test_w) = tokio::io::split(server_side);
5497
5498 let link = DuplexDialMockLink {
5499 session_id,
5500 streams: std::collections::VecDeque::from([client_side]),
5501 };
5502
5503 let mut dial_client = super::duplex::spawn::<u64, u64>(link);
5504 let mut dial_rx = dial_client.take_rx().unwrap();
5505
5506 super::read_link_init(&mut test_r).await.unwrap();
5509
5510 let recv_handle: tokio::task::JoinHandle<Result<u64, ChannelError>> =
5515 tokio::spawn(async move { dial_rx.recv().await });
5516
5517 dial_client.join().await;
5521
5522 let result = tokio::time::timeout(Duration::from_millis(100), recv_handle)
5523 .await
5524 .expect("parked recv should resolve within 100ms after join")
5525 .expect("recv task should not panic");
5526 assert!(
5527 matches!(result, Err(ChannelError::Closed)),
5528 "in-progress recv should resolve with ChannelError::Closed after join, got {result:?}"
5529 );
5530 }
5531}