Skip to main content

hyperactor/
mailbox.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Mailboxes are the central message-passing mechanism in Hyperactor.
10//!
11//! Each actor owns a mailbox to which other actors can deliver messages.
12//! An actor can open one or more typed _ports_ in the mailbox; messages
13//! are in turn delivered to specific ports.
14//!
15//! Mailboxes are associated with an [`ActorAddr`] (given by `actor_id`
16//! in the following example):
17//!
18//! ```
19//! # use hyperactor::mailbox::Mailbox;
20//! # use hyperactor::Endpoint as _;
21//! # tokio_test::block_on(async {
22//! # let proc = hyperactor::Proc::current();
23//! # let client = hyperactor::client("client");
24//! # let actor_id = proc.proc_addr().actor_addr("actor");
25//! let mbox = Mailbox::new(actor_id);
26//! let (port, mut receiver) = mbox.open_port::<u64>();
27//!
28//! port.post(&client, 123);
29//! assert_eq!(receiver.recv().await.unwrap(), 123u64);
30//! # })
31//! ```
32//!
33//! Mailboxes also provide a form of one-shot ports, called [`OncePort`],
34//! that permits at most one message transmission:
35//!
36//! ```
37//! # use hyperactor::mailbox::Mailbox;
38//! # use hyperactor::Endpoint as _;
39//! # tokio_test::block_on(async {
40//! # let proc = hyperactor::Proc::current();
41//! # let client = hyperactor::client("client");
42//! # let actor_id = proc.proc_addr().actor_addr("actor");
43//! let mbox = Mailbox::new(actor_id);
44//!
45//! let (port, receiver) = mbox.open_once_port::<u64>();
46//!
47//! port.post(&client, 123u64);
48//! assert_eq!(receiver.recv().await.unwrap(), 123u64);
49//! # })
50//! ```
51//!
52//! [`OncePort`]s are correspondingly used for RPC replies in the actor
53//! system.
54//!
55//! ## Remote ports and serialization
56//!
57//! Mailboxes allow delivery of serialized messages to named ports:
58//!
59//! 1) Ports restrict message types to (serializable) [`Message`]s.
60//! 2) Each [`Port`] is associated with a [`PortAddr`] which globally names the port.
61//! 3) [`Mailbox`] provides interfaces to deliver serialized
62//!    messages to ports named by their [`PortAddr`].
63//!
64//! While this complicates the interface somewhat, it allows the
65//! implementation to avoid a serialization roundtrip when passing
66//! messages locally.
67//!
68//! ## Undeliverable-message log invariants (UM-*)
69//!
70//! The `undelivered_message_abandoned` log at
71//! `UndeliverableMailboxSender::post_unchecked` is a user-facing
72//! surface: it fires when a message could not be delivered *and*
73//! could not be returned to its sender. The following invariants
74//! govern its shape so the log stays scannable and its downstream
75//! consumers (Scuba, alerts) stay stable.
76//!
77//! - **UM-1 (bounded abandoned-message log).** The log must not emit
78//!   unbounded `envelope.headers().to_string()` or
79//!   `envelope.data().to_string()`. Payload observability is provided
80//!   by `message_type` (`data.typename()`) and `data_len`
81//!   (`data.len()`) — cheap, bounded, and type-safe.
82//!
83//! - **UM-2 (stable compatibility fields).** The `actor_name` and
84//!   `actor_id` fields stay on the log with their current values and
85//!   types. Readability improvements are strictly additive on this
86//!   surface; renames or removals require a separate migration diff
87//!   that coordinates with downstream consumers.
88//!
89//! - **UM-3a (destination naming).** When the envelope carries no
90//!   `OPERATION_ENDPOINT`, the format string names the transport
91//!   destination: `"message not delivered to <dest>"`.
92//!
93//! - **UM-3b (operation naming).** When the envelope carries
94//!   `OPERATION_ENDPOINT`, the format string names the operation:
95//!   `"abandoned message for <endpoint>"`.
96//!
97//!   `OPERATION_*` keys live in `hyperactor::mailbox::headers`
98//!   because the readers (this log, the undeliverable formatter)
99//!   live in `hyperactor` and can't depend upward on
100//!   `monarch_hyperactor`. Keys whose consumers are not at this
101//!   layer don't belong here.
102
103use std::any::Any;
104use std::collections::BTreeMap;
105use std::collections::BTreeSet;
106use std::fmt;
107use std::fmt::Debug;
108use std::future;
109use std::future::Future;
110use std::ops::Bound::Excluded;
111use std::pin::Pin;
112use std::sync::Arc;
113use std::sync::Condvar;
114use std::sync::Mutex;
115use std::sync::Once;
116use std::sync::OnceLock;
117use std::sync::RwLock;
118use std::sync::Weak;
119use std::sync::atomic::AtomicU64;
120use std::sync::atomic::AtomicUsize;
121use std::sync::atomic::Ordering;
122use std::task::Context;
123use std::task::Poll;
124
125use async_trait::async_trait;
126use dashmap::DashMap;
127use dashmap::mapref::entry::Entry;
128use enum_as_inner::EnumAsInner;
129use futures::Sink;
130use futures::Stream;
131use hyperactor_config::Flattrs;
132use hyperactor_telemetry::hash_to_u64;
133use serde::Deserialize;
134use serde::Serialize;
135use serde::de::DeserializeOwned;
136use tokio::sync::mpsc;
137use tokio::sync::oneshot;
138use tokio::sync::watch;
139use tokio::task::JoinHandle;
140use tokio_util::sync::CancellationToken;
141use tracing::Instrument;
142use typeuri::Named;
143
144use crate::ActorAddr;
145use crate::Addr;
146use crate::Endpoint;
147use crate::EndpointLocation;
148use crate::IdleFlushPortRef;
149// for macros
150use crate::OncePortRef;
151use crate::PortAddr;
152use crate::PortRef;
153use crate::ProcAddr;
154use crate::accum::Accumulator;
155use crate::accum::IdleFlushReducerOpts;
156use crate::accum::ReducerSpec;
157use crate::accum::StreamingReducerOpts;
158use crate::actor::ActorStatus;
159use crate::channel;
160use crate::channel::ChannelAddr;
161use crate::channel::ChannelError;
162use crate::channel::ChannelTransport;
163use crate::channel::CloseReason;
164use crate::channel::CompletionSink;
165use crate::channel::SendError;
166use crate::channel::SendErrorReason;
167use crate::channel::TxStatus;
168use crate::context;
169use crate::id::ActorId;
170use crate::metrics;
171use crate::ordering::OrderingSnapshot;
172use crate::ordering::SEQ_INFO;
173use crate::ordering::SeqInfo;
174use crate::port::ControlPort;
175use crate::port::Port;
176use crate::sequenced::SequencedEnvelope;
177use crate::sequenced::SequencedReceiver;
178use crate::sequenced::sequenced_unbounded;
179
180mod undeliverable;
181/// For [`Undeliverable`], a message type for delivery failures.
182pub use undeliverable::DeliveryFailureReport;
183pub use undeliverable::Undeliverable;
184pub use undeliverable::UndeliverableMessageError;
185pub use undeliverable::custom_monitored_return_handle;
186pub use undeliverable::monitored_return_handle; // TODO: Audit
187/// For [`MailboxAdminMessage`], a message type for mailbox administration.
188pub mod mailbox_admin_message;
189pub use mailbox_admin_message::MailboxAdminMessage;
190pub use mailbox_admin_message::MailboxAdminMessageHandler;
191/// For message headers and latency tracking.
192pub mod headers;
193
194/// Message collects the necessary requirements for messages that are deposited
195/// into mailboxes.
196pub trait Message: Send + Sync + 'static {}
197impl<M: Send + Sync + 'static> Message for M {}
198
199/// RemoteMessage extends [`Message`] by requiring that the messages
200/// also be serializable, and can thus traverse process boundaries.
201/// RemoteMessages must also specify a globally unique type name (a URI).
202pub trait RemoteMessage: Message + Named + Serialize + DeserializeOwned {}
203
204impl<M: Message + Named + Serialize + DeserializeOwned> RemoteMessage for M {}
205
206/// Type alias for bytestring data used throughout the system.
207pub type Data = Vec<u8>;
208
209const MAX_RENDERED_DELIVERY_FAILURE_ATTRS_LEN: usize = 1024;
210
211fn truncate_for_delivery_failure_rendering(value: String) -> String {
212    if value.len() <= MAX_RENDERED_DELIVERY_FAILURE_ATTRS_LEN {
213        return value;
214    }
215
216    let mut truncated = value;
217    let truncate_at = truncated
218        .char_indices()
219        .map(|(index, _)| index)
220        .take_while(|index| *index <= MAX_RENDERED_DELIVERY_FAILURE_ATTRS_LEN)
221        .last()
222        .unwrap_or(0);
223    truncated.truncate(truncate_at);
224    truncated.push_str("...");
225    truncated
226}
227
228/// A structured delivery failure with optional metadata.
229#[derive(thiserror::Error, Debug, Serialize, Deserialize, typeuri::Named, Clone)]
230#[error("{kind}")]
231pub struct DeliveryFailure {
232    /// The delivery failure kind.
233    pub kind: DeliveryFailureKind,
234
235    /// Additional keyed metadata for higher-level delivery features.
236    pub attrs: Flattrs,
237}
238
239impl DeliveryFailure {
240    /// Create a delivery failure with no additional metadata.
241    pub fn new(kind: impl Into<DeliveryFailureKind>) -> Self {
242        Self {
243            kind: kind.into(),
244            attrs: Flattrs::new(),
245        }
246    }
247
248    /// Create a delivery failure with additional keyed metadata.
249    pub fn with_attrs(kind: impl Into<DeliveryFailureKind>, attrs: Flattrs) -> Self {
250        Self {
251            kind: kind.into(),
252            attrs,
253        }
254    }
255
256    /// Render this failure for human-facing diagnostics.
257    pub fn render_bounded(&self) -> String {
258        let mut rendered = format!("delivery failure: {}", self.kind);
259        if !self.attrs.is_empty() {
260            rendered.push_str("; attrs: ");
261            rendered.push_str(&truncate_for_delivery_failure_rendering(
262                self.attrs.to_string(),
263            ));
264        }
265        rendered
266    }
267}
268
269/// The kind of delivery failure.
270#[derive(
271    thiserror::Error,
272    Debug,
273    Serialize,
274    Deserialize,
275    EnumAsInner,
276    typeuri::Named,
277    Clone,
278    PartialEq,
279    Eq
280)]
281pub enum DeliveryFailureKind {
282    /// The destination reference does not denote a valid recipient.
283    #[error("{0}")]
284    InvalidReference(#[from] InvalidReference),
285
286    /// The message could not be delivered for transport or receiver-lifecycle
287    /// reasons.
288    #[error("{0}")]
289    Undeliverable(#[from] UndeliverableReason),
290
291    /// The message exceeded its TTL.
292    #[error("{0}")]
293    Expired(#[from] ExpiredDelivery),
294}
295
296/// An invalid destination reference.
297#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
298#[error("invalid reference {target}: {reason}")]
299pub struct InvalidReference {
300    /// The invalid target.
301    pub target: Addr,
302
303    /// Why the reference is invalid.
304    pub reason: InvalidReferenceReason,
305}
306
307impl InvalidReference {
308    /// Create an invalid-reference failure.
309    pub fn new(target: impl Into<Addr>, reason: InvalidReferenceReason) -> Self {
310        Self {
311            target: target.into(),
312            reason,
313        }
314    }
315}
316
317/// Why a destination reference is invalid.
318#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
319pub enum InvalidReferenceReason {
320    /// The actor does not exist.
321    #[error("actor does not exist")]
322    ActorNotExist,
323
324    /// The handler port is not bound.
325    #[error("handler not bound")]
326    HandlerNotBound,
327
328    /// The actor stopped before delivery.
329    #[error("actor stopped")]
330    ActorStopped,
331
332    /// The actor failed before delivery.
333    #[error("actor failed")]
334    ActorFailed,
335
336    /// The port was never allocated.
337    #[error("port never allocated")]
338    PortNeverAllocated,
339
340    /// The message is incompatible with the destination.
341    #[error("protocol mismatch")]
342    ProtocolMismatch,
343
344    /// The envelope was delivered to the wrong mailbox owner.
345    #[error("wrong mailbox owner")]
346    WrongMailboxOwner,
347}
348
349/// A delivery failure caused by message expiration.
350#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
351#[error("ttl expired for {target}")]
352pub struct ExpiredDelivery {
353    /// The destination whose delivery expired.
354    pub target: PortAddr,
355}
356
357impl ExpiredDelivery {
358    /// Create an expired-delivery failure.
359    pub fn new(target: impl Into<PortAddr>) -> Self {
360        Self {
361            target: target.into(),
362        }
363    }
364}
365
366/// A non-invalid-reference delivery failure.
367#[derive(
368    thiserror::Error,
369    Debug,
370    Serialize,
371    Deserialize,
372    EnumAsInner,
373    Clone,
374    PartialEq,
375    Eq
376)]
377pub enum UndeliverableReason {
378    /// Delivery failed while carrying the message.
379    #[error("{0}")]
380    Transport(#[from] TransportFailure),
381
382    /// The destination port's ordinary recipient is gone.
383    #[error("{0}")]
384    PortGone(#[from] PortGone),
385}
386
387/// A transport delivery failure.
388#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
389#[error("transport failure to {target}: {reason}")]
390pub struct TransportFailure {
391    /// The delivery target.
392    pub target: Addr,
393
394    /// Why transport failed.
395    pub reason: TransportFailureReason,
396}
397
398impl TransportFailure {
399    /// Create a transport failure.
400    pub fn new(target: impl Into<Addr>, reason: TransportFailureReason) -> Self {
401        Self {
402            target: target.into(),
403            reason,
404        }
405    }
406}
407
408/// Why transport failed.
409#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
410pub enum TransportFailureReason {
411    /// The channel closed.
412    #[error("channel closed: {addr}")]
413    ChannelClosed {
414        /// The channel address.
415        addr: ChannelAddr,
416    },
417
418    /// Delivery acknowledgement timed out.
419    #[error("ack timed out: {addr}")]
420    AckTimedOut {
421        /// The channel address.
422        addr: ChannelAddr,
423    },
424
425    /// Dialing the destination failed.
426    #[error("dial failed: {addr}: {error}")]
427    DialFailed {
428        /// The channel address.
429        addr: ChannelAddr,
430
431        /// The dial error.
432        error: String,
433    },
434
435    /// The router has no route and is not authoritative for destination
436    /// existence.
437    #[error("no route")]
438    NoRoute,
439
440    /// The serialized frame exceeded the configured channel frame limit.
441    #[error(
442        "rejecting oversize frame: len={len} > max={max}. \
443        ack will not arrive before timeout; increase CODEC_MAX_FRAME_LENGTH to allow."
444    )]
445    OversizedFrame {
446        /// The serialized frame length.
447        len: usize,
448
449        /// The configured frame limit.
450        max: usize,
451    },
452
453    /// A weak reference in the delivery path could not be upgraded.
454    #[error("link unavailable: {0}")]
455    LinkUnavailable(String),
456
457    /// The forwarder is unavailable.
458    #[error("forwarder unavailable")]
459    ForwarderUnavailable,
460}
461
462/// A port whose ordinary recipient is gone.
463#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
464#[error("port gone: {port}")]
465pub struct PortGone {
466    /// The port whose recipient is gone.
467    pub port: PortAddr,
468
469    /// The message type, when known.
470    pub message_type: Option<String>,
471}
472
473impl PortGone {
474    /// Create a port-gone failure.
475    pub fn new(port: impl Into<PortAddr>, message_type: Option<String>) -> Self {
476        Self {
477            port: port.into(),
478            message_type,
479        }
480    }
481}
482
483/// An envelope that carries a message destined to a remote actor.
484/// The envelope contains a serialized message along with its destination
485/// and sender.
486#[derive(Debug, Serialize, Deserialize, Clone, typeuri::Named)]
487pub struct MessageEnvelope {
488    /// The sender of this message.
489    sender: ActorAddr,
490
491    /// The destination of the message.
492    dest: PortAddr,
493
494    /// The next hop used only for gateway routing.
495    #[serde(default)]
496    next_hop: Option<PortAddr>,
497
498    /// The serialized message.
499    data: wirevalue::Any,
500
501    /// Structured delivery failures. The first entry is the root delivery
502    /// failure; later entries record subsequent failures while returning or
503    /// forwarding the same envelope.
504    delivery_failures: Vec<DeliveryFailure>,
505
506    /// Additional context for this message.
507    headers: Flattrs,
508
509    /// Decremented at every `MailboxSender` hop.
510    ttl: u8,
511
512    /// If true, undeliverable messages should be returned to sender. Else, they
513    /// are dropped.
514    return_undeliverable: bool,
515    // TODO: add typename, source, seq, etc.
516}
517wirevalue::register_type!(MessageEnvelope);
518
519impl MessageEnvelope {
520    /// Create a new envelope with the provided sender, destination, and message.
521    pub fn new(
522        sender: impl Into<ActorAddr>,
523        dest: impl Into<PortAddr>,
524        data: wirevalue::Any,
525        headers: Flattrs,
526    ) -> Self {
527        let sender = sender.into();
528        let dest = dest.into();
529        Self {
530            sender,
531            dest,
532            next_hop: None,
533            data,
534            delivery_failures: Vec::new(),
535            headers,
536            ttl: hyperactor_config::global::get(crate::config::MESSAGE_TTL_DEFAULT),
537            // By default, all undeliverable messages should be returned to the sender.
538            return_undeliverable: true,
539        }
540    }
541
542    /// Create a new envelope whose sender ID is unknown.
543    pub(crate) fn new_unknown(dest: impl Into<PortAddr>, data: wirevalue::Any) -> Self {
544        // Create a synthetic "unknown" actor ID for messages with no known sender
545        let unknown_addr = ChannelAddr::any(ChannelTransport::Local);
546        let unknown_proc_ref = ProcAddr::instance(unknown_addr, "unknown");
547        let unknown_actor_ref =
548            ActorAddr::root(unknown_proc_ref, crate::id::Label::strip("unknown"));
549        Self::new(unknown_actor_ref, dest, data, Flattrs::new())
550    }
551
552    /// Construct a new serialized value by serializing the provided T-typed value.
553    pub fn serialize<T: Serialize + Named>(
554        source: impl Into<ActorAddr>,
555        dest: impl Into<PortAddr>,
556        value: &T,
557        headers: Flattrs,
558    ) -> Result<Self, wirevalue::Error> {
559        Ok(Self::new(
560            source,
561            dest,
562            wirevalue::Any::serialize(value)?,
563            headers,
564        ))
565    }
566
567    /// Returns the remaining time-to-live (TTL) for this message.
568    ///
569    /// The TTL is decremented at each `MailboxSender` hop. When it
570    /// reaches 0, the message is considered expired and is returned
571    /// to the sender as undeliverable.
572    pub fn ttl(&self) -> u8 {
573        self.ttl
574    }
575
576    /// Overrides the message’s time-to-live (TTL).
577    ///
578    /// This replaces the current TTL value (normally initialized from
579    /// `config::MESSAGE_TTL_DEFAULT`) with the provided `ttl`. The
580    /// updated envelope is returned for chaining.
581    ///
582    /// # Note
583    /// The TTL is decremented at each `MailboxSender` hop, and when
584    /// it reaches 0 the message will be treated as undeliverable.
585    pub fn set_ttl(mut self, ttl: u8) -> Self {
586        self.ttl = ttl;
587        self
588    }
589
590    /// Decrements the message's TTL by one hop.
591    ///
592    /// Decrement the TTL if the message has not already expired.
593    fn decrement_ttl(&mut self) -> bool {
594        if self.ttl == 0 {
595            false
596        } else {
597            self.ttl -= 1;
598            true
599        }
600    }
601
602    /// Deserialize the message in the envelope to the provided type T.
603    pub fn deserialized<T: DeserializeOwned + Named>(&self) -> Result<T, anyhow::Error> {
604        Ok(self.data.deserialized()?)
605    }
606
607    /// The serialized message.
608    pub fn data(&self) -> &wirevalue::Any {
609        &self.data
610    }
611
612    /// The message sender.
613    pub fn sender(&self) -> &ActorAddr {
614        &self.sender
615    }
616
617    /// The destination of the message.
618    pub fn dest(&self) -> &PortAddr {
619        &self.dest
620    }
621
622    /// The next hop that gateways use for routing.
623    pub(crate) fn next_hop(&self) -> &PortAddr {
624        self.next_hop.as_ref().unwrap_or(&self.dest)
625    }
626
627    /// Whether this envelope carries a next hop distinct from the
628    /// canonical destination.
629    pub(crate) fn has_next_hop(&self) -> bool {
630        self.next_hop.is_some()
631    }
632
633    /// Return this envelope with its destination replaced by `dest`.
634    pub fn with_dest(mut self, dest: PortAddr) -> Self {
635        self.dest = dest;
636        self.next_hop = None;
637        self
638    }
639
640    /// Return this envelope with its next hop replaced by `dest`.
641    pub(crate) fn with_next_hop(mut self, dest: PortAddr) -> Self {
642        self.next_hop = if dest == self.dest { None } else { Some(dest) };
643        self
644    }
645
646    /// The message headers.
647    pub fn headers(&self) -> &Flattrs {
648        &self.headers
649    }
650
651    /// Tells whether this is a signal message.
652    pub fn is_signal(&self) -> bool {
653        self.dest
654            .is_control_port_kind(crate::port::ControlPort::Signal)
655    }
656
657    /// Push a structured delivery failure onto this message's failure history.
658    pub fn push_delivery_failure(&mut self, failure: DeliveryFailure) {
659        self.delivery_failures.push(failure)
660    }
661
662    /// Push a structured delivery failure only when this envelope does not
663    /// already have a root failure.
664    pub fn ensure_root_delivery_failure(&mut self, failure: impl FnOnce() -> DeliveryFailure) {
665        if self.root_delivery_failure().is_none() {
666            self.push_delivery_failure(failure());
667        }
668    }
669
670    /// Change the sender on an envelope forwarded on behalf of another actor.
671    pub fn update_sender(&mut self, sender: impl Into<ActorAddr>) {
672        self.sender = sender.into();
673    }
674
675    /// Set to true if you want this message to be returned to sender if it cannot
676    /// reach dest. This is the default.
677    /// Set to false if you want the message to be dropped instead.
678    pub fn set_return_undeliverable(&mut self, return_undeliverable: bool) {
679        self.return_undeliverable = return_undeliverable;
680    }
681
682    /// The message has been determined to be undeliverable with the provided
683    /// failure. Mark the envelope with the failure and return it to the sender.
684    pub fn undeliverable(
685        mut self,
686        failure: DeliveryFailure,
687        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
688    ) {
689        let error = failure.render_bounded();
690        tracing::debug!(
691            name = "undelivered_message_attempt",
692            sender = self.sender.to_string(),
693            dest = self.dest.to_string(),
694            error = %error,
695            return_handle = %return_handle,
696        );
697        metrics::MAILBOX_UNDELIVERABLE_MESSAGES.add(
698            1,
699            hyperactor_telemetry::kv_pairs!(
700                "sender_actor_id" => self.sender.to_string(),
701                "dest_actor_id" => self.dest.to_string(),
702                "message_type" => self.data.typename().unwrap_or("unknown"),
703                "error_type" => error,
704            ),
705        );
706
707        self.push_delivery_failure(failure);
708        undeliverable::return_undeliverable(return_handle, self);
709    }
710
711    /// Get the structured delivery failures for this message. Empty means this
712    /// message was not determined as undeliverable through the structured path.
713    pub fn delivery_failures(&self) -> &[DeliveryFailure] {
714        &self.delivery_failures
715    }
716
717    /// Get the root structured delivery failure for this message.
718    pub fn root_delivery_failure(&self) -> Option<&DeliveryFailure> {
719        self.delivery_failures.first()
720    }
721
722    /// Get the root structured delivery failure mutably.
723    pub fn root_delivery_failure_mut(&mut self) -> Option<&mut DeliveryFailure> {
724        self.delivery_failures.first_mut()
725    }
726
727    /// Get the string representation of the errors of this message was
728    /// undeliverable. None means this message was not determined as
729    /// undeliverable.
730    pub fn error_msg(&self) -> Option<String> {
731        if !self.delivery_failures.is_empty() {
732            return Some(
733                self.delivery_failures
734                    .iter()
735                    .map(DeliveryFailure::render_bounded)
736                    .collect::<Vec<_>>()
737                    .join("; "),
738            );
739        }
740
741        None
742    }
743
744    fn open(self) -> (MessageMetadata, wirevalue::Any) {
745        let Self {
746            sender,
747            dest,
748            next_hop,
749            data,
750            delivery_failures,
751            headers,
752            ttl,
753            return_undeliverable,
754        } = self;
755
756        (
757            MessageMetadata {
758                sender,
759                dest,
760                next_hop,
761                delivery_failures,
762                headers,
763                ttl,
764                return_undeliverable,
765            },
766            data,
767        )
768    }
769
770    fn seal(metadata: MessageMetadata, data: wirevalue::Any) -> Self {
771        let MessageMetadata {
772            sender,
773            dest,
774            next_hop,
775            delivery_failures,
776            headers,
777            ttl,
778            return_undeliverable,
779        } = metadata;
780
781        Self {
782            sender,
783            dest,
784            next_hop,
785            data,
786            delivery_failures,
787            headers,
788            ttl,
789            return_undeliverable,
790        }
791    }
792
793    fn return_undeliverable(&self) -> bool {
794        self.return_undeliverable
795    }
796
797    /// Set a header value on this envelope.
798    pub fn set_header<T: Serialize>(&mut self, key: hyperactor_config::attrs::Key<T>, value: T) {
799        self.headers.set(key, value);
800    }
801}
802
803impl fmt::Display for MessageEnvelope {
804    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
805        match &self.error_msg() {
806            None => write!(
807                f,
808                "{} > {}: {} {{{}}}",
809                self.sender, self.dest, self.data, self.headers
810            ),
811            Some(err) => write!(
812                f,
813                "{} > {}: {} {{{}}}: delivery error: {}",
814                self.sender, self.dest, self.data, self.headers, err
815            ),
816        }
817    }
818}
819
820/// Metadata about a message sent via a MessageEnvelope.
821#[derive(Clone)]
822pub struct MessageMetadata {
823    sender: ActorAddr,
824    dest: PortAddr,
825    next_hop: Option<PortAddr>,
826    /// Structured delivery failures. The first entry is the root delivery
827    /// failure; later entries record subsequent failures while returning or
828    /// forwarding the same envelope.
829    delivery_failures: Vec<DeliveryFailure>,
830    headers: Flattrs,
831    ttl: u8,
832    return_undeliverable: bool,
833}
834
835/// Errors that occur during mailbox operations. Each error is associated
836/// with the mailbox's actor id.
837#[derive(Debug)]
838pub struct MailboxError {
839    actor_id: ActorAddr,
840    kind: MailboxErrorKind,
841}
842
843/// The kinds of mailbox errors. This enum is marked non-exhaustive to
844/// allow for extensibility.
845#[derive(thiserror::Error, Debug)]
846#[non_exhaustive]
847pub enum MailboxErrorKind {
848    /// An operation was attempted on a closed mailbox.
849    #[error("mailbox closed")]
850    Closed,
851
852    /// The port associated with an operation was invalid.
853    #[error("invalid port: {0}")]
854    InvalidPort(PortAddr),
855
856    /// There was no sender associated with the port.
857    #[error("no sender for port: {0}")]
858    NoSenderForPort(PortAddr),
859
860    /// There was no local sender associated with the port.
861    /// Returned by operations that require a local port.
862    #[error("no local sender for port: {0}")]
863    NoLocalSenderForPort(PortAddr),
864
865    /// The port was closed.
866    #[error("{0}: port closed")]
867    PortClosed(PortAddr),
868
869    /// An error occured during a send operation.
870    #[error("send {0}: {1}")]
871    Send(PortAddr, #[source] anyhow::Error),
872
873    /// An error occured during a receive operation.
874    #[error("recv {0}: {1}")]
875    Recv(PortAddr, #[source] anyhow::Error),
876
877    /// There was a serialization failure.
878    #[error("serialize: {0}")]
879    Serialize(#[source] anyhow::Error),
880
881    /// There was a deserialization failure.
882    #[error("deserialize {0}: {1}")]
883    Deserialize(&'static str, anyhow::Error),
884
885    /// There was an error during a channel operation.
886    #[error(transparent)]
887    Channel(#[from] ChannelError),
888
889    /// The owning actor terminated (either stopped or failed).
890    #[error("owner terminated: {0}")]
891    OwnerTerminated(ActorStatus),
892}
893
894impl MailboxError {
895    /// Create a new mailbox error associated with the provided actor
896    /// id and of the given kind.
897    pub fn new(actor_id: impl Into<ActorAddr>, kind: MailboxErrorKind) -> Self {
898        Self {
899            actor_id: actor_id.into(),
900            kind,
901        }
902    }
903
904    /// The address of the mailbox producing this error.
905    pub fn actor_addr(&self) -> &ActorAddr {
906        &self.actor_id
907    }
908
909    /// The error's kind.
910    pub fn kind(&self) -> &MailboxErrorKind {
911        &self.kind
912    }
913}
914
915impl fmt::Display for MailboxError {
916    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917        write!(f, "{}: ", self.actor_id)?;
918        fmt::Display::fmt(&self.kind, f)
919    }
920}
921
922impl std::error::Error for MailboxError {
923    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
924        self.kind.source()
925    }
926}
927
928/// PortLocation describes the location of a port.
929/// This is used in errors to provide a uniform data type
930/// for ports that may or may not be bound.
931#[derive(Debug, Clone)]
932pub enum PortLocation {
933    /// The port was bound: the location is its underlying bound ID.
934    Bound(PortAddr),
935    /// The port was not bound: we provide the actor ID and the message type.
936    Unbound(ActorAddr, &'static str),
937}
938
939impl PortLocation {
940    fn new_unbound<M: Message>(actor_id: ActorAddr) -> Self {
941        PortLocation::Unbound(actor_id, std::any::type_name::<M>())
942    }
943
944    #[allow(dead_code)]
945    fn new_unbound_type(actor_id: ActorAddr, ty: &'static str) -> Self {
946        PortLocation::Unbound(actor_id, ty)
947    }
948
949    /// The actor address of the location.
950    pub fn actor_addr(&self) -> ActorAddr {
951        match self {
952            PortLocation::Bound(port_addr) => port_addr.actor_addr(),
953            PortLocation::Unbound(actor_addr, _) => actor_addr.clone(),
954        }
955    }
956}
957
958impl fmt::Display for PortLocation {
959    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
960        match self {
961            PortLocation::Bound(port_ref) => write!(f, "{}", port_ref),
962            PortLocation::Unbound(actor_ref, name) => write!(f, "{}<{}>", actor_ref, name),
963        }
964    }
965}
966
967/// Errors that that occur during mailbox sending operations. Each error
968/// is associated with the port ID of the operation.
969#[derive(Debug)]
970pub struct MailboxSenderError {
971    location: Box<PortLocation>,
972    kind: Box<MailboxSenderErrorKind>,
973}
974
975/// The kind of mailbox sending errors.
976#[derive(thiserror::Error, Debug)]
977pub enum MailboxSenderErrorKind {
978    /// Error during serialization.
979    #[error("serialization error: {0}")]
980    Serialize(anyhow::Error),
981
982    /// Error during deserialization.
983    #[error("deserialization error for type {0}: {1}")]
984    Deserialize(&'static str, anyhow::Error),
985
986    /// A send to an invalid port.
987    #[error("invalid port")]
988    Invalid,
989
990    /// A send to a closed port.
991    #[error("port closed")]
992    Closed,
993
994    // The following pass through underlying errors:
995    /// An underlying mailbox error.
996    #[error(transparent)]
997    Mailbox(#[from] MailboxError),
998
999    /// An underlying channel error.
1000    #[error(transparent)]
1001    Channel(#[from] ChannelError),
1002
1003    /// An other, uncategorized error.
1004    #[error("send error: {0}")]
1005    Other(#[from] anyhow::Error),
1006
1007    /// The destination was unreachable.
1008    #[error("unreachable: {0}")]
1009    Unreachable(anyhow::Error),
1010}
1011
1012impl MailboxSenderError {
1013    /// Create a new mailbox sender error to an unbound port.
1014    pub fn new_unbound<M>(actor_id: impl Into<ActorAddr>, kind: MailboxSenderErrorKind) -> Self {
1015        Self {
1016            location: Box::new(PortLocation::Unbound(
1017                actor_id.into(),
1018                std::any::type_name::<M>(),
1019            )),
1020            kind: Box::new(kind),
1021        }
1022    }
1023
1024    /// Create a new mailbox sender, manually providing the type.
1025    pub fn new_unbound_type(
1026        actor_id: impl Into<ActorAddr>,
1027        kind: MailboxSenderErrorKind,
1028        ty: &'static str,
1029    ) -> Self {
1030        Self {
1031            location: Box::new(PortLocation::Unbound(actor_id.into(), ty)),
1032            kind: Box::new(kind),
1033        }
1034    }
1035
1036    /// Create a new mailbox sender error with the provided port ID and kind.
1037    pub fn new_bound(port_id: impl Into<PortAddr>, kind: MailboxSenderErrorKind) -> Self {
1038        Self {
1039            location: Box::new(PortLocation::Bound(port_id.into())),
1040            kind: Box::new(kind),
1041        }
1042    }
1043
1044    /// The location at which the error occured.
1045    pub fn location(&self) -> &PortLocation {
1046        &self.location
1047    }
1048
1049    /// The kind associated with the error.
1050    pub fn kind(&self) -> &MailboxSenderErrorKind {
1051        &self.kind
1052    }
1053}
1054
1055impl fmt::Display for MailboxSenderError {
1056    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1057        write!(f, "{}: ", self.location)?;
1058        fmt::Display::fmt(&self.kind, f)
1059    }
1060}
1061
1062impl std::error::Error for MailboxSenderError {
1063    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1064        self.kind.source()
1065    }
1066}
1067
1068/// MailboxSenders can send messages through ports to mailboxes. It
1069/// provides a unified interface for message delivery in the system.
1070#[async_trait]
1071pub trait MailboxSender: Send + Sync + Any {
1072    /// Apply hop semantics (TTL decrement; undeliverable on 0), then
1073    /// delegate to transport.
1074    fn post(
1075        &self,
1076        mut envelope: MessageEnvelope,
1077        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1078    ) {
1079        if !envelope.decrement_ttl() {
1080            let failure = DeliveryFailure::new(ExpiredDelivery::new(envelope.dest().clone()));
1081            envelope.undeliverable(failure, return_handle);
1082            return;
1083        }
1084        self.post_unchecked(envelope, return_handle);
1085    }
1086
1087    /// Raw transport: **no** policy.
1088    fn post_unchecked(
1089        &self,
1090        envelope: MessageEnvelope,
1091        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1092    );
1093
1094    /// Wait until all messages previously posted through this sender
1095    /// have been delivered (wire-acked) or confirmed undeliverable.
1096    /// The default implementation is a no-op, appropriate for senders
1097    /// whose `post` is synchronous (e.g. local in-process delivery).
1098    async fn flush(&self) -> Result<(), anyhow::Error> {
1099        Ok(())
1100    }
1101}
1102
1103/// PortSender extends [`MailboxSender`] by providing typed endpoints
1104/// for sending messages over ports
1105pub trait PortSender: MailboxSender {
1106    /// Deliver a message to the provided port.
1107    fn serialize_and_send<M: RemoteMessage>(
1108        &self,
1109        port: &PortRef<M>,
1110        message: M,
1111        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1112    ) -> Result<(), MailboxSenderError> {
1113        // TODO: convert this to a undeliverable error also
1114        let serialized = wirevalue::Any::serialize(&message).map_err(|err| {
1115            MailboxSenderError::new_bound(
1116                port.port_addr().clone(),
1117                MailboxSenderErrorKind::Serialize(err.into()),
1118            )
1119        })?;
1120        let mut envelope = MessageEnvelope::new_unknown(port.port_addr().clone(), serialized);
1121        envelope.set_header(SEQ_INFO, SeqInfo::Direct);
1122        envelope.set_return_undeliverable(port.get_return_undeliverable());
1123        self.post(envelope, return_handle);
1124        Ok(())
1125    }
1126
1127    /// Deliver a message to a one-shot port, consuming the provided port,
1128    /// which is not reusable.
1129    fn serialize_and_send_once<M: RemoteMessage>(
1130        &self,
1131        once_port: OncePortRef<M>,
1132        message: M,
1133        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1134    ) -> Result<(), MailboxSenderError> {
1135        let serialized = wirevalue::Any::serialize(&message).map_err(|err| {
1136            MailboxSenderError::new_bound(
1137                once_port.port_addr().clone(),
1138                MailboxSenderErrorKind::Serialize(err.into()),
1139            )
1140        })?;
1141        let mut envelope = MessageEnvelope::new_unknown(once_port.port_addr().clone(), serialized);
1142        envelope.set_header(SEQ_INFO, SeqInfo::Direct);
1143        envelope.set_return_undeliverable(once_port.get_return_undeliverable());
1144        self.post(envelope, return_handle);
1145        Ok(())
1146    }
1147}
1148
1149impl<T: ?Sized + MailboxSender> PortSender for T {}
1150
1151/// A perpetually closed mailbox sender. Panics if any messages are posted.
1152/// Useful for tests, or where there is no meaningful mailbox sender
1153/// implementation available.
1154#[derive(Debug, Clone)]
1155pub struct PanickingMailboxSender;
1156
1157#[async_trait]
1158impl MailboxSender for PanickingMailboxSender {
1159    fn post_unchecked(
1160        &self,
1161        envelope: MessageEnvelope,
1162        _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1163    ) {
1164        panic!("panic! in the mailbox! attempted post: {}", envelope)
1165    }
1166}
1167
1168/// A mailbox sender for undeliverable messages. This will simply record
1169/// any undelivered messages.
1170#[derive(Debug)]
1171pub struct UndeliverableMailboxSender;
1172
1173#[async_trait]
1174impl MailboxSender for UndeliverableMailboxSender {
1175    fn post_unchecked(
1176        &self,
1177        envelope: MessageEnvelope,
1178        _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1179    ) {
1180        let sender_name = envelope
1181            .sender
1182            .label()
1183            .map_or("?".to_string(), |l| l.to_string());
1184        let error_str = envelope.error_msg().unwrap_or("".to_string());
1185        let operation_endpoint = envelope.headers().get(headers::OPERATION_ENDPOINT);
1186        let operation_adverb = envelope.headers().get(headers::OPERATION_ADVERB);
1187        // See UM-1..UM-3b in module docs.
1188        match &operation_endpoint {
1189            Some(endpoint) => tracing::error!(
1190                name = "undelivered_message_abandoned",
1191                actor_name = sender_name,
1192                actor_id = envelope.sender.to_string(),
1193                dest = envelope.dest.to_string(),
1194                message_type = envelope.data().typename().unwrap_or("unknown"),
1195                data_len = envelope.data().len(),
1196                endpoint = %endpoint,
1197                adverb = operation_adverb.as_deref().unwrap_or(""),
1198                error = %error_str,
1199                "abandoned message for {}",
1200                endpoint,
1201            ),
1202            None => tracing::error!(
1203                name = "undelivered_message_abandoned",
1204                actor_name = sender_name,
1205                actor_id = envelope.sender.to_string(),
1206                dest = envelope.dest.to_string(),
1207                message_type = envelope.data().typename().unwrap_or("unknown"),
1208                data_len = envelope.data().len(),
1209                error = %error_str,
1210                "message not delivered to {}",
1211                envelope.dest,
1212            ),
1213        }
1214    }
1215}
1216
1217/// Convenience boxing implementation for MailboxSender. Most APIs
1218/// are parameterized on MailboxSender implementations, and it's thus
1219/// difficult to work with dyn values.  BoxedMailboxSender bridges this
1220/// gap by providing a concrete MailboxSender which dispatches using an
1221/// underlying (boxed) dyn.
1222#[derive(Clone)]
1223pub struct BoxedMailboxSender(Arc<dyn MailboxSender + Send + Sync + 'static>);
1224
1225impl fmt::Debug for BoxedMailboxSender {
1226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1227        f.debug_struct("BoxedMailboxSender")
1228            .field("sender", &"<dyn MailboxSender>")
1229            .finish()
1230    }
1231}
1232
1233impl BoxedMailboxSender {
1234    /// Create a new boxed sender given the provided sender implementation.
1235    pub fn new(sender: impl MailboxSender + 'static) -> Self {
1236        Self(Arc::new(sender))
1237    }
1238
1239    /// Attempts to downcast the inner sender to the given concrete
1240    /// type.
1241    pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
1242        (&*self.0 as &dyn Any).downcast_ref::<T>()
1243    }
1244}
1245
1246/// Extension trait that creates a boxed clone of a MailboxSender.
1247pub trait BoxableMailboxSender: MailboxSender + Clone + 'static {
1248    /// A boxed clone of this MailboxSender.
1249    fn boxed(&self) -> BoxedMailboxSender;
1250}
1251impl<T: MailboxSender + Clone + 'static> BoxableMailboxSender for T {
1252    fn boxed(&self) -> BoxedMailboxSender {
1253        BoxedMailboxSender::new(self.clone())
1254    }
1255}
1256
1257/// Extension trait that rehomes a MailboxSender into a BoxedMailboxSender.
1258pub trait IntoBoxedMailboxSender: MailboxSender {
1259    /// Rehome this MailboxSender into a BoxedMailboxSender.
1260    fn into_boxed(self) -> BoxedMailboxSender;
1261}
1262impl<T: MailboxSender + 'static> IntoBoxedMailboxSender for T {
1263    fn into_boxed(self) -> BoxedMailboxSender {
1264        BoxedMailboxSender::new(self)
1265    }
1266}
1267
1268#[async_trait]
1269impl MailboxSender for BoxedMailboxSender {
1270    fn post_unchecked(
1271        &self,
1272        envelope: MessageEnvelope,
1273        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1274    ) {
1275        self.0.post_unchecked(envelope, return_handle);
1276    }
1277
1278    async fn flush(&self) -> Result<(), anyhow::Error> {
1279        self.0.flush().await
1280    }
1281}
1282
1283/// Errors that occur during mailbox serving.
1284#[derive(thiserror::Error, Debug)]
1285pub enum MailboxServerError {
1286    /// An underlying channel error.
1287    #[error(transparent)]
1288    Channel(#[from] ChannelError),
1289
1290    /// An underlying mailbox sender error.
1291    #[error(transparent)]
1292    MailboxSender(#[from] MailboxSenderError),
1293}
1294
1295/// Represents a running [`MailboxServer`]. The handle composes a
1296/// ['tokio::task::JoinHandle'] and may be joined in the same manner.
1297#[derive(Debug)]
1298pub struct MailboxServerHandle {
1299    join_handle: JoinHandle<Result<(), MailboxServerError>>,
1300    stopped_tx: watch::Sender<bool>,
1301}
1302
1303impl MailboxServerHandle {
1304    /// Signal the server to stop serving the mailbox. The caller should
1305    /// join the handle by awaiting the [`MailboxServerHandle`] future.
1306    pub fn stop(&self, reason: &str) {
1307        tracing::info!("stopping mailbox server; reason: {}", reason);
1308        // The server task owns the only `stopped_rx`. A failed send means
1309        // the task already exited on its own — e.g. a `serve_via` session
1310        // whose remote duplex peer closed, which breaks the serve loop
1311        // gracefully — so there is nothing left to stop.
1312        let _ = self.stopped_tx.send(true);
1313    }
1314
1315    /// Construct a handle from an already-spawned server task and a
1316    /// stop signal. The task must observe `stopped_rx` (the receiver
1317    /// paired with `stopped_tx`) and complete once stop is requested,
1318    /// so callers can join the handle to confirm shutdown.
1319    pub fn from_parts(
1320        join_handle: JoinHandle<Result<(), MailboxServerError>>,
1321        stopped_tx: watch::Sender<bool>,
1322    ) -> Self {
1323        Self {
1324            join_handle,
1325            stopped_tx,
1326        }
1327    }
1328}
1329
1330/// Forward future implementation to underlying handle.
1331impl Future for MailboxServerHandle {
1332    type Output = <JoinHandle<Result<(), MailboxServerError>> as Future>::Output;
1333
1334    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1335        // SAFETY: This is safe to do because self is pinned.
1336        let join_handle_pinned =
1337            unsafe { self.map_unchecked_mut(|container| &mut container.join_handle) };
1338        join_handle_pinned.poll(cx)
1339    }
1340}
1341
1342/// Serve a port on the provided [`channel::Rx`]. This dispatches all
1343/// channel messages directly to the port.
1344pub trait MailboxServer: MailboxSender + Clone + Sized + 'static {
1345    /// Serve the provided port on the given channel on this sender on
1346    /// a background task which may be joined with the returned handle.
1347    /// The task fails on any send error.
1348    fn serve(
1349        self,
1350        mut rx: impl channel::Rx<MessageEnvelope> + Send + 'static,
1351    ) -> MailboxServerHandle {
1352        // A `MailboxServer` can receive a message that couldn't
1353        // reach its destination. We can use the fact that servers are
1354        // `MailboxSender`s to attempt to forward them back to their
1355        // senders.
1356        let (return_handle, mut undeliverable_rx) = undeliverable::new_undeliverable_port();
1357        tokio::task::spawn(async move {
1358            let client = crate::client("undeliverable_supervisor");
1359            while let Ok(undeliverable) = undeliverable_rx.recv().await {
1360                match undeliverable {
1361                    Undeliverable::Returned(mut envelope) => {
1362                        match envelope.deserialized::<Undeliverable<MessageEnvelope>>() {
1363                            Ok(Undeliverable::Returned(e)) => {
1364                                // A non-returnable undeliverable.
1365                                UndeliverableMailboxSender.post(e, monitored_return_handle());
1366                                continue;
1367                            }
1368                            Ok(Undeliverable::Report(report)) => {
1369                                tracing::error!(
1370                                    sender = %report.sender,
1371                                    dest = %report.dest,
1372                                    message_type = report.message_type.as_deref().unwrap_or("unknown"),
1373                                    error = %report.error_msg().unwrap_or_default(),
1374                                    "undeliverable message report was undeliverable"
1375                                );
1376                                continue;
1377                            }
1378                            Err(_) => {}
1379                        }
1380                        let target = envelope.dest().clone();
1381                        envelope.ensure_root_delivery_failure(|| {
1382                            DeliveryFailure::new(UndeliverableReason::Transport(
1383                                TransportFailure::new(
1384                                    target,
1385                                    TransportFailureReason::LinkUnavailable(
1386                                        "message was undeliverable".to_owned(),
1387                                    ),
1388                                ),
1389                            ))
1390                        });
1391                        let sender_id: ActorAddr = envelope.sender().clone();
1392                        let return_port =
1393                            PortRef::<Undeliverable<MessageEnvelope>>::attest_handler_port(
1394                                &sender_id,
1395                            );
1396                        return_port.post_serialized(
1397                            &client,
1398                            Flattrs::new(),
1399                            wirevalue::Any::serialize(&Undeliverable::Returned(envelope)).unwrap(),
1400                        );
1401                    }
1402                    Undeliverable::Report(report) => {
1403                        tracing::error!(
1404                            sender = %report.sender,
1405                            dest = %report.dest,
1406                            message_type = report.message_type.as_deref().unwrap_or("unknown"),
1407                            error = %report.error_msg().unwrap_or_default(),
1408                            "undeliverable message report was undeliverable"
1409                        );
1410                    }
1411                }
1412            }
1413        });
1414
1415        let (stopped_tx, mut stopped_rx) = watch::channel(false);
1416        let join_handle = tokio::spawn(async move {
1417            let mut detached = false;
1418
1419            let result = loop {
1420                if *stopped_rx.borrow_and_update() {
1421                    break Ok(());
1422                }
1423
1424                tokio::select! {
1425                    message = rx.recv() => {
1426                        match message {
1427                            // Relay the message to the port directly.
1428                            Ok(envelope) => self.post(envelope, return_handle.clone()),
1429
1430                            // Closed is a "graceful" error in this case.
1431                            // We simply stop serving.
1432                            Err(ChannelError::Closed) => break Ok(()),
1433                            Err(channel_err) => break Err(MailboxServerError::from(channel_err)),
1434                        }
1435                    }
1436                    result = stopped_rx.changed(), if !detached  => {
1437                        detached = result.is_err();
1438                        if detached {
1439                            tracing::debug!(
1440                                "the mailbox server is detached for Rx {}", rx.addr()
1441                            );
1442                        } else {
1443                            tracing::debug!(
1444                                "the mailbox server is stopped for Rx {}", rx.addr()
1445                            );
1446                        }
1447                    }
1448                }
1449            };
1450
1451            // Join the channel receiver to ensure pending acks are
1452            // sent before the underlying channel server is torn down.
1453            rx.join().await;
1454
1455            result
1456        }.instrument(tracing::debug_span!("MailboxServer")));
1457
1458        MailboxServerHandle {
1459            join_handle,
1460            stopped_tx,
1461        }
1462    }
1463}
1464
1465impl<T: MailboxSender + Clone + Sized + Sync + Send + 'static> MailboxServer for T {}
1466
1467struct Buffer<T: Message> {
1468    queue: mpsc::UnboundedSender<(T, PortHandle<Undeliverable<T>>)>,
1469    #[allow(dead_code)]
1470    processed: watch::Receiver<usize>,
1471    seq: AtomicUsize,
1472}
1473
1474impl<T: Message> Buffer<T> {
1475    fn new<Fut>(
1476        process: impl Fn(T, PortHandle<Undeliverable<T>>) -> Fut + Send + Sync + 'static,
1477    ) -> Self
1478    where
1479        Fut: Future<Output = ()> + Send + 'static,
1480    {
1481        let (queue, mut next) = mpsc::unbounded_channel();
1482        let (last_processed, processed) = watch::channel(0);
1483        crate::init::get_runtime().spawn(
1484            async move {
1485                let mut seq = 0;
1486                while let Some((msg, return_handle)) = next.recv().await {
1487                    process(msg, return_handle).await;
1488                    seq += 1;
1489                    let _ = last_processed.send(seq);
1490                }
1491            }
1492            .instrument(tracing::debug_span!("mailbox::Buffer")),
1493        );
1494        Self {
1495            queue,
1496            processed,
1497            seq: AtomicUsize::new(0),
1498        }
1499    }
1500
1501    fn send(
1502        &self,
1503        item: (T, PortHandle<Undeliverable<T>>),
1504    ) -> Result<(), Box<mpsc::error::SendError<(T, PortHandle<Undeliverable<T>>)>>> {
1505        self.seq.fetch_add(1, Ordering::SeqCst);
1506        self.queue.send(item).map_err(Box::new)?;
1507        Ok(())
1508    }
1509}
1510
1511/// A mailbox server client that transmits messages on a Tx channel.
1512pub struct MailboxClient {
1513    // The channel address.
1514    addr: ChannelAddr,
1515
1516    // The unbounded sender.
1517    buffer: Buffer<MessageEnvelope>,
1518
1519    // To cancel monitoring tx health.
1520    _tx_monitoring: CancellationToken,
1521
1522    // Flush tracking: counts messages successfully submitted to the buffer.
1523    submitted: Arc<AtomicUsize>,
1524    // Flush tracking: counts messages whose delivery oneshot has resolved
1525    // (acked or failed).
1526    completed: Arc<AtomicUsize>,
1527    // Notifies flush waiters when `completed` changes.
1528    completed_notify: Arc<tokio::sync::Notify>,
1529
1530    // Watcher exposing the underlying Tx's health. Callers can peek to detect
1531    // a closed client before submitting, e.g. for routing-cache eviction.
1532    tx_status: watch::Receiver<TxStatus>,
1533}
1534
1535impl fmt::Debug for MailboxClient {
1536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1537        f.debug_struct("MailboxClient")
1538            .field("buffer", &"<Buffer>")
1539            .finish()
1540    }
1541}
1542
1543impl MailboxClient {
1544    /// Create a new client that sends messages destined for a
1545    /// [`MailboxServer`] on the provided Tx channel.
1546    pub fn new(tx: impl channel::Tx<MessageEnvelope> + Send + Sync + 'static) -> Self {
1547        let addr = tx.addr();
1548        let tx = Arc::new(tx);
1549        let tx_status = tx.status().clone();
1550        let tx_monitoring = CancellationToken::new();
1551        let completed = Arc::new(AtomicUsize::new(0));
1552        let completed_notify = Arc::new(tokio::sync::Notify::new());
1553        let buffer = {
1554            let completed = completed.clone();
1555            let completed_notify = completed_notify.clone();
1556            let addr = addr.clone();
1557            Buffer::new(move |envelope, return_handle| {
1558                let tx = Arc::clone(&tx);
1559                let addr = addr.clone();
1560                // Set up for delivery failure.
1561                let return_handle_0 = return_handle.clone();
1562                let tracker =
1563                    channel::CompletionTracker::new(completed.clone(), completed_notify.clone());
1564                let completion = CompletionSink::tracked(
1565                    tracker,
1566                    move |send_error: SendError<MessageEnvelope>| {
1567                        let SendError {
1568                            error,
1569                            message,
1570                            reason,
1571                        } = send_error;
1572                        let target = message.dest().clone();
1573                        let reason_text = reason
1574                            .as_ref()
1575                            .map(ToString::to_string)
1576                            .unwrap_or_else(|| "channel closed".to_owned());
1577                        let reason = match reason {
1578                            Some(SendErrorReason::OversizedFrame { len, max }) => {
1579                                TransportFailureReason::OversizedFrame { len, max }
1580                            }
1581                            Some(SendErrorReason::Other(_)) | None => {
1582                                TransportFailureReason::ChannelClosed { addr }
1583                            }
1584                        };
1585                        let failure = DeliveryFailure::new(UndeliverableReason::Transport(
1586                            TransportFailure::new(target, reason.clone()),
1587                        ));
1588                        tracing::debug!(
1589                            %error,
1590                            send_error_reason = %reason_text,
1591                            ?reason,
1592                            "failed to enqueue in mailbox client while processing buffer",
1593                        );
1594                        message.undeliverable(failure, return_handle_0);
1595                    },
1596                );
1597                // Send the message for transmission.
1598                tx.do_post(envelope, completion);
1599                future::ready(())
1600            })
1601        };
1602        let this = Self {
1603            addr: addr.clone(),
1604            buffer,
1605            _tx_monitoring: tx_monitoring.clone(),
1606            submitted: Arc::new(AtomicUsize::new(0)),
1607            completed,
1608            completed_notify,
1609            tx_status: tx_status.clone(),
1610        };
1611        Self::monitor_tx_health(tx_status, tx_monitoring, addr);
1612        this
1613    }
1614
1615    /// A means to monitor the health of the underlying [`channel::Tx`]. The
1616    /// watcher transitions to [`TxStatus::Closed`] when the tx is no longer
1617    /// usable for message delivery (e.g. peer rejected the session).
1618    pub fn tx_status(&self) -> &watch::Receiver<TxStatus> {
1619        &self.tx_status
1620    }
1621
1622    /// Convenience constructor, to set up a mailbox client that forwards messages
1623    /// to the provided address.
1624    pub fn dial(addr: ChannelAddr) -> Result<MailboxClient, ChannelError> {
1625        Ok(MailboxClient::new(channel::dial(addr)?))
1626    }
1627
1628    // Set up a watch for the tx's health.
1629    fn monitor_tx_health(
1630        mut rx: watch::Receiver<TxStatus>,
1631        cancel_token: CancellationToken,
1632        addr: ChannelAddr,
1633    ) {
1634        crate::init::get_runtime().spawn(async move {
1635            loop {
1636                tokio::select! {
1637                    changed = rx.changed() => {
1638                        if changed.is_err() || rx.borrow().is_closed() {
1639                            let reason = rx.borrow().as_closed().map(|r| r.to_string()).unwrap_or_else(|| "unknown".to_string());
1640                            tracing::warn!("connection to {} lost: {}", addr, reason);
1641                            // TODO: Potential for supervision event
1642                            // interaction here.
1643                            break;
1644                        }
1645                    }
1646                    _ = cancel_token.cancelled() => {
1647                        break;
1648                    }
1649                }
1650            }
1651        });
1652    }
1653}
1654
1655#[async_trait]
1656impl MailboxSender for MailboxClient {
1657    // HOT PATH: Be mindful of performance when making changes here.
1658    // To test how performance is affected by a change, run the RPC benchmarks in
1659    // `monarch/python/benches/`.
1660    fn post_unchecked(
1661        &self,
1662        envelope: MessageEnvelope,
1663        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1664    ) {
1665        tracing::event!(target:"messages", tracing::Level::TRACE,  "size"=envelope.data.len(), "sender"= %envelope.sender, "dest" = %envelope.dest.actor_addr(), "port"= envelope.dest.index(), "message_type" = envelope.data.typename().unwrap_or("unknown"), "send_message");
1666        if let Err(err) = self.buffer.send((envelope, return_handle)) {
1667            let mpsc::error::SendError((envelope, return_handle)) = *err;
1668            let target = envelope.dest().clone();
1669            let failure =
1670                DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1671                    target,
1672                    TransportFailureReason::LinkUnavailable(format!(
1673                        "mailbox client buffer is closed for {}",
1674                        self.addr
1675                    )),
1676                )));
1677
1678            // Failed to enqueue.
1679            envelope.undeliverable(failure, return_handle);
1680        } else {
1681            self.submitted.fetch_add(1, Ordering::Relaxed);
1682        }
1683    }
1684
1685    async fn flush(&self) -> Result<(), anyhow::Error> {
1686        let target = self.submitted.load(Ordering::Relaxed);
1687        loop {
1688            // Register before checking so a completion cannot notify between
1689            // the check and the wait.
1690            let notified = self.completed_notify.notified();
1691            if self.completed.load(Ordering::Relaxed) >= target {
1692                return Ok(());
1693            }
1694            notified.await;
1695        }
1696    }
1697}
1698
1699/// Wrapper to turn `PortAddr` into a `Sink`.
1700pub struct PortSink<C: context::Actor, M: RemoteMessage> {
1701    cx: C,
1702    port: PortRef<M>,
1703}
1704
1705impl<C: context::Actor, M: RemoteMessage> PortSink<C, M> {
1706    /// Create new PortSink
1707    pub fn new(cx: C, port: PortRef<M>) -> Self {
1708        Self { cx, port }
1709    }
1710}
1711
1712impl<C: context::Actor, M: RemoteMessage> Sink<M> for PortSink<C, M> {
1713    type Error = MailboxSenderError;
1714
1715    fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1716        Poll::Ready(Ok(()))
1717    }
1718
1719    fn start_send(self: Pin<&mut Self>, item: M) -> Result<(), Self::Error> {
1720        crate::Endpoint::post(&self.port, &self.cx, item);
1721        Ok(())
1722    }
1723
1724    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1725        Poll::Ready(Ok(()))
1726    }
1727
1728    fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1729        Poll::Ready(Ok(()))
1730    }
1731}
1732
1733/// A mailbox coordinates message delivery to actors through typed
1734/// [`Port`]s associated with the mailbox.
1735#[derive(Clone, Debug)]
1736pub struct Mailbox {
1737    inner: Arc<State>,
1738}
1739
1740// This explicit impl is only a compiler performance optimization. It is safe
1741// to delete this module and cfg and let Rust derive the auto traits; see proc.rs.
1742#[cfg(not(hyperactor_verify_auto_traits))]
1743mod _send_sync_shortcut {
1744    use super::*;
1745
1746    // SAFETY: the verification build structurally checks this bound.
1747    unsafe impl Send for Mailbox {}
1748    // SAFETY: the verification build structurally checks this bound.
1749    unsafe impl Sync for Mailbox {}
1750}
1751
1752const _: fn() = || {
1753    fn assert<T: Send + Sync + ?Sized>() {}
1754    assert::<Mailbox>();
1755};
1756
1757impl Mailbox {
1758    /// Create a mailbox associated with the provided actor ID.
1759    pub fn new(actor_id: impl Into<ActorAddr>) -> Self {
1760        Self {
1761            inner: Arc::new(State::new(actor_id.into())),
1762        }
1763    }
1764
1765    /// The actor address associated with this mailbox.
1766    pub fn actor_addr(&self) -> &ActorAddr {
1767        &self.inner.actor_id
1768    }
1769
1770    /// Open a new port that accepts M-typed messages. The returned
1771    /// port may be freely cloned, serialized, and passed around. The
1772    /// returned receiver should only be retained by the actor responsible
1773    /// for processing the delivered messages.
1774    pub fn open_port<M: Message>(&self) -> (PortHandle<M>, PortReceiver<M>) {
1775        let port_index = self.inner.allocate_port();
1776        let (sender, receiver) = sequenced_unbounded::<SequencedEnvelope<M>>();
1777        let port_id = self.inner.actor_id.port_addr(Port::from(port_index));
1778        self.register_ordering_snapshot(&port_id, &receiver);
1779        tracing::trace!(
1780            name = "open_port",
1781            "opening port for {} at {}",
1782            self.inner.actor_id,
1783            port_id
1784        );
1785        (
1786            PortHandle::new(
1787                self.clone(),
1788                port_index,
1789                UnboundedPortSender::Sequenced(sender),
1790            ),
1791            PortReceiver::new(receiver, port_id, /*coalesce=*/ false, self.clone()),
1792        )
1793    }
1794
1795    /// Bind the handler port for message type `M` to this mailbox.
1796    /// This method is normally used:
1797    ///   1. when we need to intercept a message sent to a handler, and re-route
1798    ///      that message to the returned receiver;
1799    ///   2. mock this message's handler when it is not implemented for this actor
1800    ///      type, with the returned receiver.
1801    ///
1802    /// The returned receiver owns the binding. Dropping it removes the handler
1803    /// port from the mailbox, so callers that need the handler to stay live
1804    /// must retain the receiver.
1805    pub(crate) fn bind_handler_port<M: RemoteMessage>(&self) -> (PortHandle<M>, PortReceiver<M>) {
1806        let (sender, receiver) = sequenced_unbounded::<SequencedEnvelope<M>>();
1807        let port_id = self.inner.actor_id.port_addr(Port::handler::<M>());
1808        self.register_ordering_snapshot(&port_id, &receiver);
1809        let handle = PortHandle::new_full_with_target(
1810            self.clone(),
1811            UnboundedPortSender::Sequenced(sender),
1812            PortBindTarget::Handler,
1813            None,
1814            StreamingReducerOpts::default(),
1815        );
1816        handle.bind_handler_port();
1817        (
1818            handle,
1819            PortReceiver::new(receiver, port_id, /*coalesce=*/ false, self.clone()),
1820        )
1821    }
1822
1823    fn register_ordering_snapshot<M: Message>(
1824        &self,
1825        port: &PortAddr,
1826        receiver: &SequencedReceiver<SequencedEnvelope<M>>,
1827    ) {
1828        let snapshot = receiver.snapshot_handle();
1829        self.inner
1830            .ordering_snapshots
1831            .insert(port.port(), Arc::new(move || snapshot.snapshot()));
1832    }
1833
1834    pub(crate) fn ordering_snapshot(&self, port: &Port) -> Option<OrderingSnapshot> {
1835        self.inner
1836            .ordering_snapshots
1837            .get(port)
1838            .map(|snapshot| snapshot.value()())
1839    }
1840
1841    /// Open a new port with an accumulator with default reduce options.
1842    /// See [`open_accum_port_opts`] for more details.
1843    pub fn open_accum_port<A>(&self, accum: A) -> (PortHandle<A::Update>, PortReceiver<A::State>)
1844    where
1845        A: Accumulator + Send + Sync + 'static,
1846        A::Update: Message,
1847        A::State: Message + Default + Clone,
1848    {
1849        self.open_accum_port_opts(accum, StreamingReducerOpts::default())
1850    }
1851
1852    /// Open an accumulator port whose bound references use idle-flush reduction
1853    /// at cast-tree split ports.
1854    pub fn open_idle_flush_accum_port<A>(
1855        &self,
1856        accum: A,
1857        reducer_opts: IdleFlushReducerOpts,
1858    ) -> (IdleFlushPortHandle<A::Update>, PortReceiver<A::State>)
1859    where
1860        A: Accumulator + Send + Sync + 'static,
1861        A::Update: Message,
1862        A::State: Message + Default + Clone,
1863    {
1864        let (port, receiver) = self.open_accum_port(accum);
1865        (IdleFlushPortHandle { port, reducer_opts }, receiver)
1866    }
1867
1868    /// Open a new port with an accumulator. This port accepts A::Update type
1869    /// messages, accumulate them into A::State with the given accumulator.
1870    /// The latest changed state can be received from the returned receiver as
1871    /// a single A::State message. If there is no new update, the receiver will
1872    /// not receive any message.
1873    ///
1874    /// If provided, reducer mode controls reduce operations.
1875    pub fn open_accum_port_opts<A>(
1876        &self,
1877        accum: A,
1878        streaming_opts: StreamingReducerOpts,
1879    ) -> (PortHandle<A::Update>, PortReceiver<A::State>)
1880    where
1881        A: Accumulator + Send + Sync + 'static,
1882        A::Update: Message,
1883        A::State: Message + Default + Clone,
1884    {
1885        let port_index = self.inner.allocate_port();
1886        let (sender, receiver) = sequenced_unbounded::<SequencedEnvelope<A::State>>();
1887        let port_id = self.inner.actor_id.port_addr(Port::from(port_index));
1888        let state = Mutex::new(A::State::default());
1889        let reducer_spec = accum.reducer_spec();
1890        let enqueue = move |_, update: A::Update| {
1891            let mut state = state.lock().unwrap();
1892            accum.accumulate(&mut state, update)?;
1893            let _ = sender.send(SequencedEnvelope::new(SeqInfo::Direct, None, state.clone()));
1894            Ok(())
1895        };
1896        (
1897            PortHandle::new_full(
1898                self.clone(),
1899                port_index,
1900                UnboundedPortSender::Func(Arc::new(enqueue)),
1901                reducer_spec,
1902                streaming_opts,
1903            ),
1904            PortReceiver::new(receiver, port_id, /*coalesce=*/ true, self.clone()),
1905        )
1906    }
1907
1908    /// Open a port that accepts M-typed messages, using the provided function
1909    /// to enqueue.
1910    // TODO: consider making lifetime bound to Self instead.
1911    #[cfg(test)]
1912    pub(crate) fn open_enqueue_port<M: Message>(
1913        &self,
1914        enqueue: impl Fn(Flattrs, M) -> Result<(), anyhow::Error> + Send + Sync + 'static,
1915    ) -> PortHandle<M> {
1916        PortHandle::new_full(
1917            self.clone(),
1918            self.inner.allocate_port(),
1919            UnboundedPortSender::Func(Arc::new(enqueue)),
1920            None,
1921            StreamingReducerOpts::default(),
1922        )
1923    }
1924
1925    /// Open a runtime-dispatched handler port that accepts M-typed
1926    /// messages using the provided enqueue function.
1927    pub(crate) fn open_handler_enqueue_port<M: Message>(
1928        &self,
1929        enqueue: impl Fn(Flattrs, M) -> Result<(), anyhow::Error> + Send + Sync + 'static,
1930    ) -> PortHandle<M> {
1931        let enqueue = Arc::new(enqueue);
1932        let sender = Arc::new(HandlerPortSender::new(
1933            UnboundedPortSender::Func(enqueue),
1934            self.inner.handler_ingress.clone(),
1935        ));
1936        PortHandle::new_full_with_target(
1937            self.clone(),
1938            UnboundedPortSender::Handler(sender),
1939            PortBindTarget::Handler,
1940            None,
1941            StreamingReducerOpts::default(),
1942        )
1943    }
1944
1945    /// Open a new one-shot port that accepts M-typed messages. The
1946    /// returned port may be used to send a single message; ditto the
1947    /// receiver may receive a single message.
1948    pub fn open_once_port<M: Message>(&self) -> (OncePortHandle<M>, OncePortReceiver<M>) {
1949        let port_index = self.inner.allocate_port();
1950        let port_id = self.inner.actor_id.port_addr(Port::from(port_index));
1951        let (sender, receiver) = oneshot::channel::<M>();
1952        (
1953            OncePortHandle {
1954                mailbox: self.clone(),
1955                port_id: port_id.clone(),
1956                sender,
1957                reducer_spec: None,
1958            },
1959            OncePortReceiver {
1960                receiver: Some(receiver),
1961                port_id,
1962                mailbox: self.clone(),
1963            },
1964        )
1965    }
1966
1967    /// Open a new one-shot port with a reducer. This port is designed
1968    /// to be used with casting, where the port is split across multiple
1969    /// destinations and responses are accumulated using the reducer.
1970    /// The accumulator type must have a ReducerSpec.
1971    ///
1972    /// The returned handle can be bound and embedded in cast messages.
1973    /// When cast routing splits the message, each destination receives a
1974    /// split port. Responses to split ports are accumulated using the
1975    /// accumulator's reducer, and the final accumulated result is delivered
1976    /// to the returned receiver.
1977    ///
1978    /// Note: For accumulators used with casting, `Update` and `State` types
1979    /// must be the same (e.g., `sum<u64>` where both are `u64`).
1980    pub fn open_reduce_port<A, T>(
1981        &self,
1982        accum: A,
1983    ) -> (OncePortHandle<A::State>, OncePortReceiver<A::State>)
1984    where
1985        A: Accumulator<State = T, Update = T> + Send + Sync + 'static,
1986        T: Message + Default + Clone,
1987    {
1988        let port_index = self.inner.allocate_port();
1989        let (sender, receiver) = oneshot::channel::<T>();
1990        let port_id = self.inner.actor_id.port_addr(Port::from(port_index));
1991        let reducer_spec = accum.reducer_spec();
1992        assert!(
1993            reducer_spec.is_some(),
1994            "cannot use a reduce port without a ReducerSpec"
1995        );
1996
1997        (
1998            OncePortHandle {
1999                mailbox: self.clone(),
2000                port_id: port_id.clone(),
2001                sender,
2002                reducer_spec,
2003            },
2004            OncePortReceiver {
2005                receiver: Some(receiver),
2006                port_id,
2007                mailbox: self.clone(),
2008            },
2009        )
2010    }
2011
2012    #[allow(dead_code)]
2013    fn error(&self, err: MailboxErrorKind) -> MailboxError {
2014        MailboxError::new(self.inner.actor_id.clone(), err)
2015    }
2016
2017    fn lookup_sender<M: RemoteMessage>(&self) -> Option<UnboundedPortSender<M>> {
2018        let port = Port::handler::<M>();
2019        self.inner.ports.get(&port).and_then(|boxed| {
2020            boxed
2021                .as_any()
2022                .downcast_ref::<UnboundedSender<M>>()
2023                .map(|s| {
2024                    assert_eq!(
2025                        s.port_id,
2026                        self.actor_addr().port_addr(port.clone()),
2027                        "port_id mismatch in downcasted UnboundedSender"
2028                    );
2029                    s.sender.clone()
2030                })
2031        })
2032    }
2033
2034    /// Retrieve the bound undeliverable handler port handle.
2035    pub fn bound_return_handle(&self) -> Option<PortHandle<Undeliverable<MessageEnvelope>>> {
2036        self.lookup_sender::<Undeliverable<MessageEnvelope>>()
2037            .map(|sender| PortHandle::new(self.clone(), self.inner.allocate_port(), sender))
2038    }
2039
2040    /// Install the owning actor's lazy delivery-failure handler initializer.
2041    pub(crate) fn set_return_handler_hook(&self, hook: impl Fn() + Send + Sync + 'static) {
2042        assert!(
2043            self.inner.return_handler_hook.set(Box::new(hook)).is_ok(),
2044            "return handler hook should be installed once"
2045        );
2046    }
2047
2048    /// Bind the owning actor's delivery-failure handler, if one was installed.
2049    pub(crate) fn ensure_return_handler(&self) {
2050        if let Some(hook) = self.inner.return_handler_hook.get() {
2051            self.inner.return_handler_init.call_once(hook);
2052        }
2053    }
2054
2055    pub(crate) fn allocate_port(&self) -> u64 {
2056        self.inner.allocate_port()
2057    }
2058
2059    fn bind<M: RemoteMessage>(&self, handle: &PortHandle<M>) -> PortRef<M> {
2060        assert_eq!(
2061            handle.inner.mailbox.actor_addr(),
2062            self.actor_addr(),
2063            "port does not belong to mailbox"
2064        );
2065
2066        // TODO: don't even allocate a port until the port is bound. Possibly
2067        // have handles explicitly staged (unbound, bound).
2068        let port_ref = self
2069            .actor_addr()
2070            .port_addr(Port::from(handle.inner.bind_target.ephemeral_index()));
2071        match self.inner.ports.entry(port_ref.port()) {
2072            Entry::Vacant(entry) => {
2073                entry.insert(Arc::new(UnboundedSender::new(
2074                    handle.inner.sender.clone(),
2075                    port_ref.clone(),
2076                )));
2077            }
2078            Entry::Occupied(_entry) => {}
2079        }
2080
2081        PortRef::attest(port_ref)
2082    }
2083
2084    fn bind_to_handler_port<M: RemoteMessage>(&self, handle: &PortHandle<M>) {
2085        self.bind_to_port(handle, Port::handler::<M>());
2086    }
2087
2088    fn bind_to_control_port<M: RemoteMessage>(&self, handle: &PortHandle<M>, port: ControlPort) {
2089        self.bind_to_port(handle, Port::control(port));
2090    }
2091
2092    fn bind_to_port<M: RemoteMessage>(&self, handle: &PortHandle<M>, port: Port) {
2093        assert_eq!(
2094            handle.inner.mailbox.actor_addr(),
2095            self.actor_addr(),
2096            "port does not belong to mailbox"
2097        );
2098
2099        let port_ref = self.actor_addr().port_addr(port.clone());
2100        match self.inner.ports.entry(port) {
2101            Entry::Vacant(entry) => {
2102                entry.insert(Arc::new(UnboundedSender::new(
2103                    handle.inner.sender.clone(),
2104                    port_ref.clone(),
2105                )));
2106            }
2107            Entry::Occupied(_entry) => panic!("port {} already bound", port_ref),
2108        }
2109    }
2110
2111    fn bind_once<M: RemoteMessage>(&self, handle: OncePortHandle<M>) {
2112        let port_id = handle.port_addr().clone();
2113        match self.inner.ports.entry(port_id.port()) {
2114            Entry::Vacant(entry) => {
2115                entry.insert(Arc::new(OnceSender::new(handle.sender, port_id.clone())));
2116            }
2117            Entry::Occupied(_entry) => {}
2118        }
2119    }
2120
2121    pub(crate) fn bind_untyped(&self, port_id: &PortAddr, sender: UntypedUnboundedSender) {
2122        assert_eq!(
2123            port_id.actor_addr(),
2124            *self.actor_addr(),
2125            "port does not belong to mailbox"
2126        );
2127
2128        match self.inner.ports.entry(port_id.port()) {
2129            Entry::Vacant(entry) => {
2130                entry.insert(Arc::new(sender));
2131            }
2132            Entry::Occupied(_entry) => {}
2133        }
2134    }
2135
2136    pub(crate) fn unbind_untyped(&self, port_id: &PortAddr) {
2137        assert_eq!(
2138            port_id.actor_addr(),
2139            *self.actor_addr(),
2140            "port does not belong to mailbox"
2141        );
2142
2143        self.inner.ports.remove(&port_id.port());
2144    }
2145
2146    pub(crate) fn close(&self, status: ActorStatus) {
2147        let mut closed = self.inner.closed.write().unwrap();
2148        if closed.is_some() {
2149            panic!("mailbox with owner {} already closed", self.actor_addr());
2150        }
2151        let _ = closed.insert(status);
2152    }
2153
2154    /// Start draining handler ingress for this mailbox.
2155    ///
2156    /// Draining is a mailbox lifecycle property, but it is enforced
2157    /// only by runtime-dispatched handler ports. New handler work is
2158    /// rejected at the handler-port sender. Work that already entered
2159    /// a handler-port sender before draining began is allowed to finish
2160    /// enqueueing, and this method waits for those in-flight enqueue
2161    /// attempts before it returns. After this method returns, no handler
2162    /// work can still be entering the actor queue through a handler
2163    /// port.
2164    ///
2165    /// Runtime/control ports and ordinary mailbox ports remain usable
2166    /// while draining, so shutdown can continue and already accepted
2167    /// work can flush.
2168    ///
2169    /// This is distinct from [`Mailbox::close`], which marks the
2170    /// mailbox terminal and rejects all subsequent local delivery.
2171    pub(crate) fn drain(&self) {
2172        self.inner.handler_ingress.drain();
2173    }
2174}
2175
2176impl context::Mailbox for Mailbox {
2177    fn mailbox(&self) -> &Mailbox {
2178        self
2179    }
2180}
2181
2182// TODO: figure out what to do with these interfaces -- possibly these caps
2183// do not have to be private.
2184
2185/// Open a port given a capability.
2186pub fn open_port<M: Message>(cx: &impl context::Mailbox) -> (PortHandle<M>, PortReceiver<M>) {
2187    cx.mailbox().open_port()
2188}
2189
2190/// Open a one-shot port given a capability. This is a public method primarily to
2191/// enable macro-generated clients.
2192pub fn open_once_port<M: Message>(
2193    cx: &impl context::Mailbox,
2194) -> (OncePortHandle<M>, OncePortReceiver<M>) {
2195    cx.mailbox().open_once_port()
2196}
2197
2198#[async_trait]
2199impl MailboxSender for Mailbox {
2200    // HOT PATH: Be mindful of performance when making changes here.
2201    // To test how performance is affected by a change, run the RPC benchmarks in
2202    // `monarch/python/benches/`.
2203    /// Deliver a serialized message to the provided port ID. This method fails
2204    /// if the message does not deserialize into the expected type.
2205    fn post_unchecked(
2206        &self,
2207        envelope: MessageEnvelope,
2208        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
2209    ) {
2210        metrics::MAILBOX_POSTS.add(
2211            1,
2212            hyperactor_telemetry::kv_pairs!(
2213                "actor_id" => envelope.sender.to_string(),
2214                "dest_actor_id" => envelope.dest.actor_addr().to_string(),
2215            ),
2216        );
2217        tracing::trace!(
2218            name = "post",
2219            actor_name = envelope.sender.label().map_or("?", |l| l.as_str()),
2220            actor_id = envelope.sender.to_string(),
2221            "posting message to {}",
2222            envelope.dest
2223        );
2224
2225        if envelope.dest().actor_id() != self.inner.actor_id.id() {
2226            let failure = DeliveryFailure::new(InvalidReference::new(
2227                envelope.dest().actor_addr(),
2228                InvalidReferenceReason::WrongMailboxOwner,
2229            ));
2230            return envelope.undeliverable(failure, return_handle);
2231        }
2232
2233        let port = envelope.dest().port();
2234
2235        // Clone the Arc<dyn SerializedSender> out of the DashMap while holding
2236        // only a short-lived read lock, then release the lock before calling
2237        // send_serialized. This prevents a deadlock that occurs when
2238        // send_serialized itself tries to post a message to another port on the
2239        // same mailbox: if both ports hash to the same DashMap shard, acquiring
2240        // the shard's write lock a second time on the same thread deadlocks
2241        // (RwLock is not reentrant). DashMap uses a random per-process hasher,
2242        // so whether two port indices collide in the same shard varies across
2243        // test runs, explaining the longstanding flaky timeout failures.
2244        let port_sender = match self.inner.ports.get(&port) {
2245            None => {
2246                let failure = unbound_port_delivery_failure(
2247                    envelope.dest(),
2248                    envelope.data(),
2249                    self.inner.next_ephemeral_port.load(Ordering::SeqCst),
2250                );
2251                return envelope.undeliverable(failure, return_handle);
2252            }
2253            Some(ref_) => {
2254                let closed = self.inner.closed.read().unwrap();
2255                if let Some(status) = &*closed {
2256                    match status {
2257                        ActorStatus::Stopped(reason) => {
2258                            tracing::debug!(
2259                                owner=%self.inner.actor_id,
2260                                %reason,
2261                                "mailbox owner is stopped",
2262                            );
2263                            let failure = DeliveryFailure::new(InvalidReference::new(
2264                                envelope.dest().actor_addr(),
2265                                InvalidReferenceReason::ActorStopped,
2266                            ));
2267                            return envelope.undeliverable(failure, return_handle);
2268                        }
2269                        ActorStatus::Failed(actor_error) => {
2270                            tracing::debug!(
2271                                owner=%self.inner.actor_id,
2272                                %actor_error,
2273                                "mailbox owner failed",
2274                            );
2275                            let failure = DeliveryFailure::new(InvalidReference::new(
2276                                envelope.dest().actor_addr(),
2277                                InvalidReferenceReason::ActorFailed,
2278                            ));
2279                            return envelope.undeliverable(failure, return_handle);
2280                        }
2281                        _ => {
2282                            let failure = DeliveryFailure::new(UndeliverableReason::Transport(
2283                                TransportFailure::new(
2284                                    envelope.dest().actor_addr(),
2285                                    TransportFailureReason::LinkUnavailable(format!(
2286                                        "mailbox owner {} closed unexpectedly: {:?}",
2287                                        self.inner.actor_id, status
2288                                    )),
2289                                ),
2290                            ));
2291                            return envelope.undeliverable(failure, return_handle);
2292                        }
2293                    }
2294                }
2295                // Clone the Arc so we can release the shard read lock before
2296                // calling send_serialized, which may re-enter post_unchecked.
2297                Arc::clone(&*ref_)
2298            }
2299        };
2300        // Shard read lock is released here when `ref_` is dropped.
2301
2302        let (metadata, data) = envelope.open();
2303        let MessageMetadata {
2304            mut headers,
2305            sender,
2306            dest,
2307            next_hop,
2308            delivery_failures,
2309            ttl,
2310            return_undeliverable,
2311        } = metadata;
2312
2313        let to_actor_id = hash_to_u64(dest.actor_addr().id());
2314        let message_id = hyperactor_telemetry::generate_message_id(to_actor_id);
2315        headers.set(crate::mailbox::headers::TELEMETRY_MESSAGE_ID, message_id);
2316        // A cast leaf stamps its trusted logical origin before this boundary.
2317        // For other deliveries, the envelope sender is the logical origin.
2318        if !headers.contains_key(crate::mailbox::headers::SENDER_ACTOR_ID_HASH) {
2319            crate::mailbox::headers::stamp_sender_actor_id_hash(&mut headers, &sender);
2320        }
2321        headers.set(crate::mailbox::headers::TELEMETRY_PORT_INDEX, dest.index());
2322
2323        match port_sender.send_serialized(headers, data) {
2324            Ok(disposition) => {
2325                hyperactor_telemetry::notify_message_status(
2326                    hyperactor_telemetry::MessageStatusEvent {
2327                        timestamp: std::time::SystemTime::now(),
2328                        id: hyperactor_telemetry::generate_status_event_id(message_id),
2329                        message_id,
2330                        status: "queued".to_string(),
2331                    },
2332                );
2333
2334                if disposition == SerializedSendDisposition::DeliveredAndExhausted {
2335                    self.inner.ports.remove(&port);
2336                }
2337            }
2338            Err(SerializedSendFailure::Dead { data, headers }) => {
2339                self.inner.ports.remove(&port);
2340                let failure = port_gone_delivery_failure(&dest, &data);
2341
2342                MessageEnvelope::seal(
2343                    MessageMetadata {
2344                        headers,
2345                        sender,
2346                        dest,
2347                        next_hop,
2348                        delivery_failures,
2349                        ttl,
2350                        return_undeliverable,
2351                    },
2352                    data,
2353                )
2354                .undeliverable(failure, return_handle)
2355            }
2356            Err(SerializedSendFailure::Error(SerializedSendError {
2357                data,
2358                error: sender_error,
2359                headers,
2360            })) => {
2361                let failure = serialized_send_error_delivery_failure(&dest, &sender_error);
2362
2363                let envelope = MessageEnvelope::seal(
2364                    MessageMetadata {
2365                        headers,
2366                        sender,
2367                        dest,
2368                        next_hop,
2369                        delivery_failures,
2370                        ttl,
2371                        return_undeliverable,
2372                    },
2373                    data,
2374                );
2375                envelope.undeliverable(failure, return_handle)
2376            }
2377        }
2378    }
2379}
2380
2381fn unbound_port_delivery_failure(
2382    port: &PortAddr,
2383    data: &wirevalue::Any,
2384    next_ephemeral_port: u64,
2385) -> DeliveryFailure {
2386    if port.is_handler_port() {
2387        DeliveryFailure::new(InvalidReference::new(
2388            port.clone(),
2389            InvalidReferenceReason::HandlerNotBound,
2390        ))
2391    } else {
2392        match port.ephemeral_index() {
2393            Some(index) if index < next_ephemeral_port => port_gone_delivery_failure(port, data),
2394            _ => DeliveryFailure::new(InvalidReference::new(
2395                port.clone(),
2396                InvalidReferenceReason::PortNeverAllocated,
2397            )),
2398        }
2399    }
2400}
2401
2402fn serialized_send_error_delivery_failure(
2403    dest: &PortAddr,
2404    sender_error: &MailboxSenderError,
2405) -> DeliveryFailure {
2406    match sender_error.kind() {
2407        MailboxSenderErrorKind::Deserialize(_, _) => DeliveryFailure::new(InvalidReference::new(
2408            dest.clone(),
2409            InvalidReferenceReason::ProtocolMismatch,
2410        )),
2411        MailboxSenderErrorKind::Invalid => {
2412            let reason = if dest.is_handler_port() {
2413                InvalidReferenceReason::HandlerNotBound
2414            } else {
2415                InvalidReferenceReason::PortNeverAllocated
2416            };
2417            DeliveryFailure::new(InvalidReference::new(dest.clone(), reason))
2418        }
2419        MailboxSenderErrorKind::Closed => DeliveryFailure::new(UndeliverableReason::PortGone(
2420            PortGone::new(dest.clone(), None),
2421        )),
2422        _ => DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
2423            dest.clone(),
2424            TransportFailureReason::LinkUnavailable(sender_error.to_string()),
2425        ))),
2426    }
2427}
2428
2429fn port_gone_delivery_failure(port: &PortAddr, data: &wirevalue::Any) -> DeliveryFailure {
2430    DeliveryFailure::new(port_gone(port, data))
2431}
2432
2433fn port_gone(port: &PortAddr, data: &wirevalue::Any) -> UndeliverableReason {
2434    UndeliverableReason::PortGone(PortGone::new(
2435        port.clone(),
2436        data.typename().map(str::to_string),
2437    ))
2438}
2439
2440#[derive(Debug, Clone, Copy)]
2441enum PortBindTarget {
2442    Ephemeral(u64),
2443    Handler,
2444}
2445
2446impl PortBindTarget {
2447    fn ephemeral_index(self) -> u64 {
2448        match self {
2449            Self::Ephemeral(port_index) => port_index,
2450            Self::Handler => panic!("handler port handle has no ephemeral port index"),
2451        }
2452    }
2453}
2454
2455/// Inner state of a [`PortHandle`], shared via `Arc` to make cloning cheap
2456/// (single atomic refcount bump instead of cloning each field).
2457struct PortHandleInner<M: Message> {
2458    mailbox: Mailbox,
2459    sender: UnboundedPortSender<M>,
2460    bind_target: PortBindTarget,
2461    // We would like this to be a Arc<RwLock<Option<PortAddr<M>>>>, but we cannot
2462    // write down the type PortAddr<M> (M: Message), even though we cannot
2463    // legally construct such a value without M: RemoteMessage. We could consider
2464    // making PortAddr<M> valid for M: Message, but constructible only for
2465    // M: RemoteMessage, but the guarantees offered by the impossibilty of even
2466    // writing down the type are appealing.
2467    bound: Arc<RwLock<Option<PortAddr>>>,
2468    // Typehash of an optional reducer. When it's defined, we include it in port
2469    /// references to optionally enable incremental accumulation.
2470    reducer_spec: Option<ReducerSpec>,
2471    /// Streaming reducer options.
2472    streaming_opts: StreamingReducerOpts,
2473}
2474
2475impl<M: Message> fmt::Debug for PortHandleInner<M> {
2476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2477        f.debug_struct("PortHandleInner")
2478            .field("mailbox", &self.mailbox)
2479            .field("sender", &self.sender)
2480            .field("bind_target", &self.bind_target)
2481            .field("bound", &self.bound)
2482            .field("reducer_spec", &self.reducer_spec)
2483            .field("streaming_opts", &self.streaming_opts)
2484            .finish()
2485    }
2486}
2487
2488/// A port to which M-typed messages can be delivered. Ports may be
2489/// serialized to be sent to other actors. However, when a port is
2490/// deserialized, it may no longer be used to send messages directly
2491/// to a mailbox since it is no longer associated with a local mailbox
2492/// ([`Mailbox::send`] will fail). However, the runtime may accept
2493/// remote Ports, and arrange for these messages to be delivered
2494/// indirectly through inter-node message passing.
2495#[derive(Debug)]
2496pub struct PortHandle<M: Message> {
2497    inner: Arc<PortHandleInner<M>>,
2498}
2499
2500impl<M: Message> PortHandle<M> {
2501    fn new_full(
2502        mailbox: Mailbox,
2503        port_index: u64,
2504        sender: UnboundedPortSender<M>,
2505        reducer_spec: Option<ReducerSpec>,
2506        streaming_opts: StreamingReducerOpts,
2507    ) -> Self {
2508        Self::new_full_with_target(
2509            mailbox,
2510            sender,
2511            PortBindTarget::Ephemeral(port_index),
2512            reducer_spec,
2513            streaming_opts,
2514        )
2515    }
2516
2517    fn new_full_with_target(
2518        mailbox: Mailbox,
2519        sender: UnboundedPortSender<M>,
2520        bind_target: PortBindTarget,
2521        reducer_spec: Option<ReducerSpec>,
2522        streaming_opts: StreamingReducerOpts,
2523    ) -> Self {
2524        Self {
2525            inner: Arc::new(PortHandleInner {
2526                mailbox,
2527                sender,
2528                bind_target,
2529                bound: Arc::new(RwLock::new(None)),
2530                reducer_spec,
2531                streaming_opts,
2532            }),
2533        }
2534    }
2535
2536    fn new(mailbox: Mailbox, port_index: u64, sender: UnboundedPortSender<M>) -> Self {
2537        Self::new_full(
2538            mailbox,
2539            port_index,
2540            sender,
2541            None,
2542            StreamingReducerOpts::default(),
2543        )
2544    }
2545
2546    pub(crate) fn location(&self) -> PortLocation {
2547        match self.inner.bound.read().unwrap().as_ref() {
2548            Some(port_id) => PortLocation::Bound(port_id.clone()),
2549            None => PortLocation::new_unbound::<M>(self.inner.mailbox.actor_addr().clone()),
2550        }
2551    }
2552
2553    // HOT PATH: Be mindful of performance when making changes here.
2554    // To test how performance is affected by a change, run the RPC benchmarks in
2555    // `monarch/python/benches/`.
2556    /// Post `message` to this port, returning an error if delivery fails (the
2557    /// port is closed, its owner has terminated, or its underlying channel is
2558    /// disconnected). Unlike [`Endpoint::post`], the caller observes the
2559    /// failure instead of having it reported through the actor's lost-message
2560    /// channel.
2561    pub fn try_post<C>(&self, cx: &C, message: M) -> Result<(), MailboxSenderError>
2562    where
2563        C: context::Actor,
2564    {
2565        let closed = self.inner.mailbox.inner.closed.read().unwrap();
2566
2567        if let Some(status) = &*closed {
2568            let err = MailboxError {
2569                actor_id: self.inner.mailbox.actor_addr().clone(),
2570                kind: MailboxErrorKind::OwnerTerminated(status.clone()),
2571            };
2572            return Err(MailboxSenderError::new_unbound::<M>(
2573                self.inner.mailbox.actor_addr().clone(),
2574                MailboxSenderErrorKind::Mailbox(err),
2575            ));
2576        }
2577        let mut headers = Flattrs::new();
2578
2579        crate::mailbox::headers::set_send_timestamp(&mut headers);
2580        crate::mailbox::headers::set_rust_message_type::<M>(&mut headers);
2581        // Holding this read lock makes `bind()` a fence: unbound local sends
2582        // are enqueued as direct messages before the port is published, while
2583        // bound local sends share the same sequence domain as ref/mailbox
2584        // sends.
2585        let bound_guard = self.inner.bound.read().unwrap();
2586        if let Some(dest) = bound_guard.as_ref() {
2587            let sequencer = cx.instance().sequencer();
2588            let seq_info = sequencer.assign_seq(dest);
2589            // Pair SENDER_ACTOR_ID stamp with SEQ_INFO. PortHandle::try_post
2590            // starts with Flattrs::new(), so there's no caller-supplied stale
2591            // header to defend against — use the simpler "fresh" helper.
2592            if let SeqInfo::Session { seq, .. } = &seq_info {
2593                crate::mailbox::headers::stamp_sender_actor_id_fresh(
2594                    &mut headers,
2595                    *seq,
2596                    dest,
2597                    cx.mailbox().actor_addr(),
2598                );
2599            }
2600            headers.set(SEQ_INFO, seq_info);
2601        } else {
2602            headers.set(SEQ_INFO, SeqInfo::Direct);
2603        }
2604        // Encountering error means the port is closed. So we do not need to
2605        // rollback the seq, because no message can be delivered to it, and
2606        // subsequently do not need to worry about out-of-sequence for messages
2607        // after this seq.
2608        //
2609        // Theoretically, we could have deadlock if
2610        //   1. `sender.send` attempts to hold read lock of this PortHandle's
2611        //      `bound` field, and in the meantime,
2612        //   2.  another thread is trying to bind and thus waiting for write lock.
2613        // But we do not expect `sender.send` to use the same PortHandle, so this
2614        // deadlock scenario should not happen.
2615        self.inner.sender.send(headers, message).map_err(|err| {
2616            MailboxSenderError::new_unbound::<M>(
2617                self.inner.mailbox.actor_addr().clone(),
2618                classify_sender_error(err),
2619            )
2620        })
2621    }
2622}
2623
2624impl<M> Endpoint<M> for &PortHandle<M>
2625where
2626    M: Message,
2627{
2628    fn endpoint_location(&self) -> EndpointLocation {
2629        self.location().into()
2630    }
2631
2632    fn post<C>(self, cx: &C, message: M)
2633    where
2634        C: context::Actor,
2635    {
2636        if let Err(err) = self.try_post(cx, message) {
2637            cx.instance()
2638                .report_delivery_failure(DeliveryFailureReport::from_send_error::<M>(
2639                    cx.mailbox().actor_addr().clone(),
2640                    self.endpoint_location(),
2641                    &err,
2642                ));
2643        }
2644    }
2645}
2646
2647impl<M: Message> PortHandle<M> {
2648    /// A contravariant map: using the provided function to translate
2649    /// `R`-typed messages to `M`-typed ones, delivered on this port.
2650    pub fn contramap<R, F>(&self, unmap: F) -> PortHandle<R>
2651    where
2652        R: Message,
2653        F: Fn(R) -> M + Send + Sync + 'static,
2654    {
2655        let port_index = self.inner.mailbox.inner.allocate_port();
2656        let sender = self.inner.sender.clone();
2657        PortHandle::new(
2658            self.inner.mailbox.clone(),
2659            port_index,
2660            UnboundedPortSender::Func(Arc::new(move |headers, value: R| {
2661                sender.send(headers, unmap(value))
2662            })),
2663        )
2664    }
2665}
2666
2667impl<M: RemoteMessage> PortHandle<M> {
2668    /// Bind this port, making it accessible to remote actors.
2669    ///
2670    /// Ordinary ports bind to their allocated ephemeral port. Handler ports
2671    /// bind to the well-known handler port for `M`.
2672    pub fn bind(&self) -> PortRef<M> {
2673        match self.inner.bind_target {
2674            PortBindTarget::Ephemeral(_) => self.bind_ephemeral_port(),
2675            PortBindTarget::Handler => self.bind_handler_port(),
2676        }
2677    }
2678
2679    /// Bind this handle to the well-known handler port for message type `M`
2680    /// and return a `PortRef` to it.
2681    ///
2682    /// Binding to the same handler port again returns the existing binding.
2683    /// Binding a handle that is already bound to a different port panics.
2684    pub(crate) fn bind_handler_port(&self) -> PortRef<M> {
2685        self.bind_to_port(Port::handler::<M>(), |mailbox, handle| {
2686            mailbox.bind_to_handler_port(handle);
2687        })
2688    }
2689
2690    /// Bind this handle to a control port and return a `PortRef` to it.
2691    ///
2692    /// Binding to the same control port again returns the existing binding.
2693    /// Binding a handle that is already bound to a different port panics.
2694    pub(crate) fn bind_control_port(&self, port: ControlPort) -> PortRef<M> {
2695        self.bind_to_port(Port::control(port), |mailbox, handle| {
2696            mailbox.bind_to_control_port(handle, port);
2697        })
2698    }
2699
2700    fn bind_ephemeral_port(&self) -> PortRef<M> {
2701        let port_addr = {
2702            let mut guard = self.inner.bound.write().unwrap();
2703            match guard.as_ref() {
2704                Some(existing) => existing.clone(),
2705                None => {
2706                    let port_addr = self.inner.mailbox.bind(self).into_port_addr();
2707                    *guard = Some(port_addr.clone());
2708                    port_addr
2709                }
2710            }
2711        };
2712        self.port_ref(port_addr)
2713    }
2714
2715    fn bind_to_port(&self, port: Port, bind: impl FnOnce(&Mailbox, &PortHandle<M>)) -> PortRef<M> {
2716        let port_id = self.inner.mailbox.actor_addr().port_addr(port);
2717        {
2718            let mut guard = self.inner.bound.write().unwrap();
2719            match guard.as_ref() {
2720                Some(existing) if existing == &port_id => {}
2721                Some(existing) => panic!(
2722                    "could not bind port handle {:?} as {port_id}: already bound to {existing}",
2723                    self.inner.bind_target
2724                ),
2725                None => {
2726                    bind(&self.inner.mailbox, self);
2727                    *guard = Some(port_id.clone());
2728                }
2729            }
2730        }
2731        self.port_ref(port_id)
2732    }
2733
2734    fn port_ref(&self, port_addr: PortAddr) -> PortRef<M> {
2735        PortRef::attest_reducible(
2736            port_addr,
2737            self.inner.reducer_spec.clone(),
2738            self.inner.streaming_opts.clone(),
2739        )
2740    }
2741}
2742
2743impl<M: Message> Clone for PortHandle<M> {
2744    fn clone(&self) -> Self {
2745        Self {
2746            inner: Arc::clone(&self.inner),
2747        }
2748    }
2749}
2750
2751impl<M: Message> fmt::Display for PortHandle<M> {
2752    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2753        fmt::Display::fmt(&self.location(), f)
2754    }
2755}
2756
2757/// A handle for an accumulator port whose bound references use idle-flush
2758/// reduction at cast-tree split ports.
2759#[derive(Debug)]
2760pub struct IdleFlushPortHandle<M: Message> {
2761    port: PortHandle<M>,
2762    reducer_opts: IdleFlushReducerOpts,
2763}
2764
2765impl<M: RemoteMessage> IdleFlushPortHandle<M> {
2766    /// Bind this port and return an idle-flush reference that may be passed to
2767    /// remote actors.
2768    pub fn bind(self) -> IdleFlushPortRef<M> {
2769        self.port.bind().into_idle_flush(self.reducer_opts)
2770    }
2771}
2772
2773/// A one-shot port handle to which M-typed messages can be delivered.
2774#[derive(Debug)]
2775pub struct OncePortHandle<M: Message> {
2776    mailbox: Mailbox,
2777    port_id: PortAddr,
2778    sender: oneshot::Sender<M>,
2779    reducer_spec: Option<ReducerSpec>,
2780}
2781
2782impl<M: Message> OncePortHandle<M> {
2783    /// This port's address.
2784    // TODO: make value
2785    pub fn port_addr(&self) -> &PortAddr {
2786        &self.port_id
2787    }
2788
2789    /// Post `message` to this port, returning an error if delivery fails (the
2790    /// receiver has been dropped). Unlike [`Endpoint::post`], the caller
2791    /// observes the failure instead of having it reported through the actor's
2792    /// lost-message channel.
2793    pub fn try_post<C>(self, _cx: &C, message: M) -> Result<(), MailboxSenderError>
2794    where
2795        C: context::Actor,
2796    {
2797        // TODO: Assign seq to the message if the port is bound to a handler port
2798        // in the future.
2799        assert!(
2800            !self.port_addr().is_handler_port(),
2801            "OncePortHandle currently does not support handler ports; a \
2802            prerequisite of that support is to assign seq to messages \
2803            if the port is a handler port."
2804        );
2805
2806        let actor_id = self.mailbox.actor_addr().clone();
2807        self.sender.send(message).map_err(|_| {
2808            // Here, the value is returned when the port is
2809            // closed.  We should consider having a similar
2810            // API for send_once, though arguably it makes less
2811            // sense in this context.
2812            MailboxSenderError::new_unbound::<M>(actor_id, MailboxSenderErrorKind::Closed)
2813        })
2814    }
2815}
2816
2817impl<M> Endpoint<M> for OncePortHandle<M>
2818where
2819    M: Message,
2820{
2821    fn endpoint_location(&self) -> EndpointLocation {
2822        EndpointLocation::Port(self.port_id.clone())
2823    }
2824
2825    fn post<C>(self, cx: &C, message: M)
2826    where
2827        C: context::Actor,
2828    {
2829        let endpoint_location = self.endpoint_location();
2830        if let Err(err) = self.try_post(cx, message) {
2831            cx.instance()
2832                .report_delivery_failure(DeliveryFailureReport::from_send_error::<M>(
2833                    cx.mailbox().actor_addr().clone(),
2834                    endpoint_location,
2835                    &err,
2836                ));
2837        }
2838    }
2839}
2840
2841impl<M: RemoteMessage> OncePortHandle<M> {
2842    /// Turn this handle into a ref that may be passed to
2843    /// a remote actor. The remote actor can then use the
2844    /// ref to send a message to the port. Creating a ref also
2845    /// binds the port, so that it is remotely writable.
2846    pub fn bind(self) -> OncePortRef<M> {
2847        let port_id: PortAddr = self.port_addr().clone();
2848        let reducer_spec = self.reducer_spec.clone();
2849        self.mailbox.clone().bind_once(self);
2850        OncePortRef::attest_reducible(port_id, reducer_spec)
2851    }
2852}
2853
2854impl<M: Message> fmt::Display for OncePortHandle<M> {
2855    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2856        fmt::Display::fmt(&self.port_addr(), f)
2857    }
2858}
2859
2860/// A receiver of M-typed messages, used by actors to receive messages
2861/// on open ports.
2862#[derive(Debug)]
2863pub struct PortReceiver<M> {
2864    receiver: SequencedReceiver<SequencedEnvelope<M>>,
2865    port_id: PortAddr,
2866    /// When multiple messages are put in channel, only receive the latest one
2867    /// if coalesce is true. Other messages will be discarded.
2868    coalesce: bool,
2869    /// State is used to remove the port from service when the receiver
2870    /// is dropped.
2871    mailbox: Mailbox,
2872}
2873
2874impl<M> PortReceiver<M> {
2875    fn new(
2876        receiver: SequencedReceiver<SequencedEnvelope<M>>,
2877        port_id: PortAddr,
2878        coalesce: bool,
2879        mailbox: Mailbox,
2880    ) -> Self {
2881        Self {
2882            receiver,
2883            port_id,
2884            coalesce,
2885            mailbox,
2886        }
2887    }
2888
2889    /// Tries to receive the next value for this receiver.
2890    /// This function returns `Ok(None)` if the receiver is empty
2891    /// and returns a MailboxError if the receiver is disconnected.
2892    #[allow(clippy::result_large_err)] // TODO: Consider reducing the size of `MailboxError`.
2893    pub fn try_recv(&mut self) -> Result<Option<M>, MailboxError> {
2894        let mut next = self.receiver.try_recv();
2895        // To coalesce, drain the mpsc queue and only keep the last one.
2896        if self.coalesce
2897            && let Some(latest) = self.drain().pop()
2898        {
2899            next = Ok(latest);
2900        }
2901        match next {
2902            Ok(msg) => Ok(Some(msg)),
2903            Err(mpsc::error::TryRecvError::Empty) => Ok(None),
2904            Err(mpsc::error::TryRecvError::Disconnected) => Err(MailboxError::new(
2905                self.actor_addr().clone(),
2906                MailboxErrorKind::Closed,
2907            )),
2908        }
2909    }
2910
2911    /// Receive the next message from the port corresponding with this
2912    /// receiver.
2913    pub async fn recv(&mut self) -> Result<M, MailboxError> {
2914        let mut next = self.receiver.recv().await;
2915        // To coalesce, get the last message from the queue if there are
2916        // more on the mspc queue.
2917        if self.coalesce
2918            && let Some(latest) = self.drain().pop()
2919        {
2920            next = Some(latest);
2921        }
2922        next.ok_or(MailboxError::new(
2923            self.actor_addr().clone(),
2924            MailboxErrorKind::Closed,
2925        ))
2926    }
2927
2928    /// Drains all available messages from the port.
2929    pub fn drain(&mut self) -> Vec<M> {
2930        let mut drained: Vec<M> = Vec::new();
2931        while let Ok(msg) = self.receiver.try_recv() {
2932            // To coalesce, discard the old message if there is any.
2933            if self.coalesce {
2934                drained.pop();
2935            }
2936            drained.push(msg);
2937        }
2938        drained
2939    }
2940
2941    fn port(&self) -> Port {
2942        self.port_id.port()
2943    }
2944
2945    fn actor_addr(&self) -> ActorAddr {
2946        self.port_id.actor_addr()
2947    }
2948}
2949
2950impl<M> Drop for PortReceiver<M> {
2951    fn drop(&mut self) {
2952        // MARIUS: do we need to tombstone these? or should we
2953        // error out if we have removed the receiver before serializing the port ref?
2954        // ("no longer live")?
2955        self.mailbox.inner.ports.remove(&self.port());
2956        self.mailbox.inner.ordering_snapshots.remove(&self.port());
2957    }
2958}
2959
2960impl<M> Unpin for PortReceiver<M> {}
2961
2962impl<M> Stream for PortReceiver<M> {
2963    type Item = Result<M, MailboxError>;
2964
2965    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
2966        std::pin::pin!(self.recv()).poll(cx).map(Some)
2967    }
2968}
2969
2970/// A receiver of M-typed messages from [`OncePort`]s.
2971pub struct OncePortReceiver<M> {
2972    receiver: Option<oneshot::Receiver<M>>,
2973    port_id: PortAddr,
2974
2975    /// Mailbox is used to remove the port from service when the receiver
2976    /// is dropped.
2977    mailbox: Mailbox,
2978}
2979
2980impl<M> OncePortReceiver<M> {
2981    /// Receive message from the one-shot port associated with this
2982    /// receiver.  Recv consumes the receiver: it is no longer valid
2983    /// after this call.
2984    pub async fn recv(mut self) -> Result<M, MailboxError> {
2985        std::mem::take(&mut self.receiver)
2986            .unwrap()
2987            .await
2988            .map_err(|err| {
2989                MailboxError::new(
2990                    self.actor_addr().clone(),
2991                    MailboxErrorKind::Recv(self.port_id.clone(), err.into()),
2992                )
2993            })
2994    }
2995
2996    fn port(&self) -> Port {
2997        self.port_id.port()
2998    }
2999
3000    fn actor_addr(&self) -> ActorAddr {
3001        self.port_id.actor_addr()
3002    }
3003}
3004
3005impl<M> Drop for OncePortReceiver<M> {
3006    fn drop(&mut self) {
3007        // MARIUS: do we need to tombstone these? or should we
3008        // error out if we have removed the receiver before serializing the port ref?
3009        // ("no longer live")?
3010        self.mailbox.inner.ports.remove(&self.port());
3011    }
3012}
3013
3014#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3015pub(crate) enum SerializedSendDisposition {
3016    Delivered,
3017    DeliveredAndExhausted,
3018}
3019
3020/// Error that that occur during `SerializedSender::send_serialized`.
3021pub(crate) struct SerializedSendError {
3022    /// The headers associated with the message.
3023    pub(crate) headers: Flattrs,
3024    /// The message was tried to send.
3025    pub(crate) data: wirevalue::Any,
3026    /// The mailbox sender error that occurred.
3027    pub(crate) error: MailboxSenderError,
3028}
3029
3030pub(crate) enum SerializedSendFailure {
3031    Dead {
3032        headers: Flattrs,
3033        data: wirevalue::Any,
3034    },
3035    Error(SerializedSendError),
3036}
3037
3038/// SerializedSender encapsulates senders:
3039///   - It performs type erasure (and thus it is object-safe).
3040///   - It abstracts over [`Port`]s and [`OncePort`]s, by dynamically tracking the
3041///     validity of the underlying port.
3042trait SerializedSender: Send + Sync {
3043    /// Enables downcasting from `&dyn SerializedSender` to concrete
3044    /// types.
3045    ///
3046    /// Used by `Mailbox::lookup_sender` to downcast to
3047    /// `&UnboundedSender<M>` via `Any::downcast_ref`.
3048    fn as_any(&self) -> &dyn Any;
3049
3050    /// Send a serialized message. SerializedSender will deserialize the
3051    /// message (failing if it fails to deserialize), and then send the
3052    /// resulting message on the underlying port.
3053    ///
3054    /// The returned disposition describes successful delivery. Errors
3055    /// report both the failed message and whether the sender remains live.
3056    fn send_serialized(
3057        &self,
3058        headers: Flattrs,
3059        serialized: wirevalue::Any,
3060    ) -> Result<SerializedSendDisposition, SerializedSendFailure>;
3061}
3062
3063#[derive(Debug, thiserror::Error)]
3064#[error("handler port closed")]
3065struct HandlerPortClosedError;
3066
3067fn classify_sender_error(err: anyhow::Error) -> MailboxSenderErrorKind {
3068    if err.is::<HandlerPortClosedError>() {
3069        MailboxSenderErrorKind::Closed
3070    } else {
3071        MailboxSenderErrorKind::Other(err)
3072    }
3073}
3074
3075/// A sender to an M-typed unbounded port.
3076enum UnboundedPortSender<M: Message> {
3077    /// Send through a receiver-local sequencing domain.
3078    Sequenced(mpsc::UnboundedSender<SequencedEnvelope<M>>),
3079    /// Use the provided function to enqueue the item.
3080    Func(Arc<dyn Fn(Flattrs, M) -> Result<(), anyhow::Error> + Send + Sync>),
3081    /// A runtime-dispatched handler port that observes mailbox drain state.
3082    Handler(Arc<HandlerPortSender<M>>),
3083}
3084
3085impl<M: Message> UnboundedPortSender<M> {
3086    fn send(&self, headers: Flattrs, message: M) -> Result<(), anyhow::Error> {
3087        match self {
3088            Self::Sequenced(sender) => {
3089                let seq_info = headers.get(SEQ_INFO).unwrap_or(SeqInfo::Direct);
3090                if !seq_info.is_valid() {
3091                    return Err(anyhow::anyhow!("sequenced port send has invalid SEQ_INFO"));
3092                }
3093                let sender_addr = headers.get(crate::mailbox::headers::SENDER_ACTOR_ID);
3094                sender
3095                    .send(SequencedEnvelope::new(seq_info, sender_addr, message))
3096                    .map_err(anyhow::Error::from)
3097            }
3098            Self::Func(func) => func(headers, message),
3099            Self::Handler(sender) => sender.send(headers, message),
3100        }
3101    }
3102}
3103
3104// We implement Clone manually as derive(Clone) places unnecessarily
3105// strict bounds on the type parameter M.
3106impl<M: Message> Clone for UnboundedPortSender<M> {
3107    fn clone(&self) -> Self {
3108        match self {
3109            Self::Sequenced(sender) => Self::Sequenced(sender.clone()),
3110            Self::Func(func) => Self::Func(func.clone()),
3111            Self::Handler(sender) => Self::Handler(sender.clone()),
3112        }
3113    }
3114}
3115
3116impl<M: Message> Debug for UnboundedPortSender<M> {
3117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
3118        match self {
3119            Self::Sequenced(q) => f
3120                .debug_tuple("UnboundedPortSender::Sequenced")
3121                .field(q)
3122                .finish(),
3123            Self::Func(_) => f
3124                .debug_tuple("UnboundedPortSender::Func")
3125                .field(&"..")
3126                .finish(),
3127            Self::Handler(_) => f
3128                .debug_tuple("UnboundedPortSender::Handler")
3129                .field(&"..")
3130                .finish(),
3131        }
3132    }
3133}
3134
3135const HANDLER_INGRESS_DRAINING: usize = 1usize << (usize::BITS as usize - 1);
3136const HANDLER_INGRESS_ACTIVE_MASK: usize = !HANDLER_INGRESS_DRAINING;
3137
3138struct HandlerIngressGate {
3139    state: AtomicUsize,
3140    wait_lock: Mutex<()>,
3141    drained: Condvar,
3142}
3143
3144struct HandlerIngressGuard {
3145    gate: Arc<HandlerIngressGate>,
3146}
3147
3148impl HandlerIngressGate {
3149    fn new() -> Self {
3150        Self {
3151            state: AtomicUsize::new(0),
3152            wait_lock: Mutex::new(()),
3153            drained: Condvar::new(),
3154        }
3155    }
3156
3157    fn try_enter(self: &Arc<Self>) -> Result<HandlerIngressGuard, HandlerPortClosedError> {
3158        let mut state = self.state.load(Ordering::Acquire);
3159        loop {
3160            if state & HANDLER_INGRESS_DRAINING != 0 {
3161                return Err(HandlerPortClosedError);
3162            }
3163
3164            let active = state & HANDLER_INGRESS_ACTIVE_MASK;
3165            assert!(
3166                active < HANDLER_INGRESS_ACTIVE_MASK,
3167                "too many active handler ingress sends"
3168            );
3169
3170            match self.state.compare_exchange_weak(
3171                state,
3172                state + 1,
3173                Ordering::AcqRel,
3174                Ordering::Acquire,
3175            ) {
3176                Ok(_) => {
3177                    return Ok(HandlerIngressGuard {
3178                        gate: Arc::clone(self),
3179                    });
3180                }
3181                Err(next_state) => state = next_state,
3182            }
3183        }
3184    }
3185
3186    fn drain(&self) {
3187        let mut state = self.state.load(Ordering::Acquire);
3188        loop {
3189            if state & HANDLER_INGRESS_DRAINING != 0 {
3190                break;
3191            }
3192            match self.state.compare_exchange_weak(
3193                state,
3194                state | HANDLER_INGRESS_DRAINING,
3195                Ordering::AcqRel,
3196                Ordering::Acquire,
3197            ) {
3198                Ok(_) => break,
3199                Err(next_state) => state = next_state,
3200            }
3201        }
3202
3203        let mut wait_guard = self.wait_lock.lock().unwrap();
3204        while self.state.load(Ordering::Acquire) & HANDLER_INGRESS_ACTIVE_MASK != 0 {
3205            wait_guard = self.drained.wait(wait_guard).unwrap();
3206        }
3207    }
3208}
3209
3210impl Drop for HandlerIngressGuard {
3211    fn drop(&mut self) {
3212        let previous = self.gate.state.fetch_sub(1, Ordering::AcqRel);
3213        assert!(
3214            previous & HANDLER_INGRESS_ACTIVE_MASK != 0,
3215            "handler ingress active count underflow"
3216        );
3217        if previous & HANDLER_INGRESS_DRAINING != 0 && previous & HANDLER_INGRESS_ACTIVE_MASK == 1 {
3218            // Pair only the final active-count decrement during drain
3219            // with the drain waiter's condvar mutex. Ordinary send
3220            // completion stays on the atomic fast path, but the final
3221            // sender still cannot notify between the waiter's state
3222            // check and its transition to sleep.
3223            let _wait_guard = self.gate.wait_lock.lock().unwrap();
3224            self.gate.drained.notify_all();
3225        }
3226    }
3227}
3228
3229struct HandlerPortSender<M: Message> {
3230    sender: UnboundedPortSender<M>,
3231    gate: Arc<HandlerIngressGate>,
3232}
3233
3234impl<M: Message> HandlerPortSender<M> {
3235    fn new(sender: UnboundedPortSender<M>, gate: Arc<HandlerIngressGate>) -> Self {
3236        Self { sender, gate }
3237    }
3238
3239    fn send(&self, headers: Flattrs, message: M) -> Result<(), anyhow::Error> {
3240        let _guard = self.gate.try_enter()?;
3241        self.sender.send(headers, message)
3242    }
3243}
3244
3245struct UnboundedSender<M: Message> {
3246    sender: UnboundedPortSender<M>,
3247    port_id: PortAddr,
3248}
3249
3250impl<M: Message> UnboundedSender<M> {
3251    /// Create a new UnboundedSender encapsulating the provided
3252    /// sender.
3253    fn new(sender: UnboundedPortSender<M>, port_id: PortAddr) -> Self {
3254        Self { sender, port_id }
3255    }
3256
3257    #[allow(dead_code)]
3258    fn send(&self, headers: Flattrs, message: M) -> Result<(), MailboxSenderError> {
3259        self.sender.send(headers, message).map_err(|err| {
3260            MailboxSenderError::new_bound(self.port_id.clone(), classify_sender_error(err))
3261        })
3262    }
3263}
3264
3265// Clone is implemented explicitly because the derive macro demands M:
3266// Clone directly. In this case, it isn't needed because Arc<T> can
3267// clone for any T.
3268impl<M: Message> Clone for UnboundedSender<M> {
3269    fn clone(&self) -> Self {
3270        Self {
3271            sender: self.sender.clone(),
3272            port_id: self.port_id.clone(),
3273        }
3274    }
3275}
3276
3277impl<M: RemoteMessage> SerializedSender for UnboundedSender<M> {
3278    fn as_any(&self) -> &dyn Any {
3279        self
3280    }
3281
3282    fn send_serialized(
3283        &self,
3284        headers: Flattrs,
3285        serialized: wirevalue::Any,
3286    ) -> Result<SerializedSendDisposition, SerializedSendFailure> {
3287        // Here, the stack ensures that this port is only instantiated for M-typed messages.
3288        // This does not protect against bad senders (e.g., encoding wrongly-typed messages),
3289        // but it is required for serialized messages that have already been routed to the
3290        // destination's typed handler port.
3291        match serialized.deserialized_unchecked() {
3292            Ok(message) => match self.sender.send(headers.clone(), message) {
3293                Ok(()) => Ok(SerializedSendDisposition::Delivered),
3294                Err(_) if matches!(&self.sender, UnboundedPortSender::Sequenced(_)) => {
3295                    Err(SerializedSendFailure::Dead {
3296                        data: serialized,
3297                        headers,
3298                    })
3299                }
3300                Err(err) => Err(SerializedSendFailure::Error(SerializedSendError {
3301                    data: serialized,
3302                    error: MailboxSenderError::new_bound(
3303                        self.port_id.clone(),
3304                        classify_sender_error(err),
3305                    ),
3306                    headers,
3307                })),
3308            },
3309            Err(err) => Err(SerializedSendFailure::Error(SerializedSendError {
3310                data: serialized,
3311                error: MailboxSenderError::new_bound(
3312                    self.port_id.clone(),
3313                    MailboxSenderErrorKind::Deserialize(M::typename(), err.into()),
3314                ),
3315                headers,
3316            })),
3317        }
3318    }
3319}
3320
3321/// OnceSender encapsulates an underlying one-shot sender, dynamically
3322/// tracking its validity.
3323#[derive(Debug)]
3324struct OnceSender<M: Message> {
3325    sender: Arc<Mutex<Option<oneshot::Sender<M>>>>,
3326    port_id: PortAddr,
3327}
3328
3329impl<M: Message> OnceSender<M> {
3330    /// Create a new OnceSender encapsulating the provided one-shot
3331    /// sender.
3332    fn new(sender: oneshot::Sender<M>, port_id: PortAddr) -> Self {
3333        Self {
3334            sender: Arc::new(Mutex::new(Some(sender))),
3335            port_id,
3336        }
3337    }
3338
3339    fn send_once(&self, message: M) -> Result<SerializedSendDisposition, MailboxSenderError> {
3340        // TODO: we should replace the sender on error
3341        match self.sender.lock().unwrap().take() {
3342            None => Err(MailboxSenderError::new_bound(
3343                self.port_id.clone(),
3344                MailboxSenderErrorKind::Closed,
3345            )),
3346            Some(sender) => {
3347                sender.send(message).map_err(|_| {
3348                    // Here, the value is returned when the port is
3349                    // closed.  We should consider having a similar
3350                    // API for send_once, though arguably it makes less
3351                    // sense in this context.
3352                    MailboxSenderError::new_bound(
3353                        self.port_id.clone(),
3354                        MailboxSenderErrorKind::Closed,
3355                    )
3356                })?;
3357                Ok(SerializedSendDisposition::DeliveredAndExhausted)
3358            }
3359        }
3360    }
3361}
3362
3363// Clone is implemented explicitly because the derive macro demands M:
3364// Clone directly. In this case, it isn't needed because Arc<T> can
3365// clone for any T.
3366impl<M: Message> Clone for OnceSender<M> {
3367    fn clone(&self) -> Self {
3368        Self {
3369            sender: self.sender.clone(),
3370            port_id: self.port_id.clone(),
3371        }
3372    }
3373}
3374
3375impl<M: RemoteMessage> SerializedSender for OnceSender<M> {
3376    fn as_any(&self) -> &dyn Any {
3377        self
3378    }
3379
3380    fn send_serialized(
3381        &self,
3382        headers: Flattrs,
3383        serialized: wirevalue::Any,
3384    ) -> Result<SerializedSendDisposition, SerializedSendFailure> {
3385        match serialized.deserialized() {
3386            Ok(message) => self
3387                .send_once(message)
3388                .map_err(|_| SerializedSendFailure::Dead {
3389                    data: serialized,
3390                    headers,
3391                }),
3392            Err(err) => Err(SerializedSendFailure::Error(SerializedSendError {
3393                data: serialized,
3394                error: MailboxSenderError::new_bound(
3395                    self.port_id.clone(),
3396                    MailboxSenderErrorKind::Deserialize(M::typename(), err.into()),
3397                ),
3398                headers,
3399            })),
3400        }
3401    }
3402}
3403
3404/// Use the provided function to send untyped messages (i.e. Any objects).
3405pub(crate) struct UntypedUnboundedSender {
3406    pub(crate) sender: Box<
3407        dyn Fn(Flattrs, wirevalue::Any) -> Result<SerializedSendDisposition, SerializedSendFailure>
3408            + Send
3409            + Sync,
3410    >,
3411}
3412
3413impl SerializedSender for UntypedUnboundedSender {
3414    fn as_any(&self) -> &dyn Any {
3415        self
3416    }
3417
3418    fn send_serialized(
3419        &self,
3420        headers: Flattrs,
3421        serialized: wirevalue::Any,
3422    ) -> Result<SerializedSendDisposition, SerializedSendFailure> {
3423        (self.sender)(headers, serialized)
3424    }
3425}
3426
3427/// State is the internal state of the mailbox.
3428struct State {
3429    /// The ID of the mailbox owner.
3430    actor_id: ActorAddr,
3431
3432    // insert if it's serializable; otherwise don't.
3433    /// The set of active ports in the mailbox. All currently
3434    /// allocated ports are
3435    ports: DashMap<Port, Arc<dyn SerializedSender>>,
3436
3437    /// Receiver-local ordering snapshots for directly sequenced ports.
3438    ordering_snapshots: DashMap<Port, Arc<dyn Fn() -> OrderingSnapshot + Send + Sync>>,
3439
3440    /// The next ephemeral port ID to allocate.
3441    next_ephemeral_port: AtomicU64,
3442
3443    /// If a value is present, the mailbox has been closed with the provided
3444    /// status, and any subsequent `Mailbox::post_unchecked` calls will fail.
3445    closed: RwLock<Option<ActorStatus>>,
3446
3447    /// Gate that closes and drains runtime-dispatched handler ingress.
3448    handler_ingress: Arc<HandlerIngressGate>,
3449
3450    /// One-shot initialization of the owning actor's delivery-failure handler.
3451    return_handler_init: Once,
3452
3453    /// Actor-specific action that binds the delivery-failure handler.
3454    return_handler_hook: OnceLock<Box<dyn Fn() + Send + Sync + 'static>>,
3455}
3456
3457impl State {
3458    /// Create a new state with the provided owning ActorAddr.
3459    fn new(actor_id: ActorAddr) -> Self {
3460        Self {
3461            actor_id,
3462            ports: DashMap::new(),
3463            ordering_snapshots: DashMap::new(),
3464            next_ephemeral_port: AtomicU64::new(0),
3465            closed: RwLock::new(None),
3466            handler_ingress: Arc::new(HandlerIngressGate::new()),
3467            return_handler_init: Once::new(),
3468            return_handler_hook: OnceLock::new(),
3469        }
3470    }
3471
3472    /// Allocate a fresh port.
3473    fn allocate_port(&self) -> u64 {
3474        self.next_ephemeral_port.fetch_add(1, Ordering::SeqCst)
3475    }
3476}
3477
3478impl fmt::Debug for State {
3479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
3480        f.debug_struct("State")
3481            .field("actor_id", &self.actor_id)
3482            .field(
3483                "open_ports",
3484                &self
3485                    .ports
3486                    .iter()
3487                    .map(|e| e.key().clone())
3488                    .collect::<Vec<_>>(),
3489            )
3490            .field("next_ephemeral_port", &self.next_ephemeral_port)
3491            .finish()
3492    }
3493}
3494
3495// TODO: mux based on some parameterized type. (mux key).
3496/// An in-memory mailbox muxer. This is used to route messages to
3497/// different underlying senders.
3498///
3499/// Messages for an actor that has not yet bound are buffered and
3500/// delivered, in order, when it binds. If the actor does not bind
3501/// within [`crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT`], they are
3502/// returned as undeliverable with [`InvalidReferenceReason::ActorNotExist`].
3503#[derive(Clone)]
3504pub struct MailboxMuxer {
3505    mailboxes: Arc<DashMap<ActorId, ActorMailbox>>,
3506    status_sender: Arc<OnceLock<Box<dyn MailboxSender + Send + Sync>>>,
3507}
3508
3509type PendingMessage = (MessageEnvelope, PortHandle<Undeliverable<MessageEnvelope>>);
3510
3511enum ActorMailbox {
3512    /// The actor has bound; messages go straight to its sender.
3513    Bound(Arc<dyn MailboxSender + Send + Sync>),
3514    /// The actor has not bound yet. Its messages expire after
3515    /// [`crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT`].
3516    Buffering(Vec<PendingMessage>),
3517}
3518
3519impl Default for MailboxMuxer {
3520    fn default() -> Self {
3521        Self::new()
3522    }
3523}
3524
3525impl MailboxMuxer {
3526    /// Create a new, empty, muxer.
3527    pub fn new() -> Self {
3528        Self {
3529            mailboxes: Arc::new(DashMap::new()),
3530            status_sender: Arc::new(OnceLock::new()),
3531        }
3532    }
3533
3534    /// Route messages destined for the provided actor id to the provided
3535    /// sender, first delivering any messages buffered for the actor.
3536    /// Returns false if the actor is already bound. In this case, the
3537    /// sender is not replaced, and the caller must
3538    /// [`MailboxMuxer::unbind`] it first.
3539    pub fn bind(&self, actor_id: ActorId, sender: impl MailboxSender + 'static) -> bool {
3540        let sender: Arc<dyn MailboxSender + Send + Sync> = Arc::new(sender);
3541        match self.mailboxes.entry(actor_id) {
3542            Entry::Vacant(entry) => {
3543                entry.insert(ActorMailbox::Bound(sender));
3544                true
3545            }
3546            Entry::Occupied(mut entry) => {
3547                let ActorMailbox::Buffering(pending) = entry.get_mut() else {
3548                    return false;
3549                };
3550                // Holding the entry lock keeps later messages behind the
3551                // buffered ones. `post` must not re-enter the muxer.
3552                for (envelope, return_handle) in std::mem::take(pending) {
3553                    sender.post(envelope, return_handle);
3554                }
3555                entry.insert(ActorMailbox::Bound(sender));
3556                true
3557            }
3558        }
3559    }
3560
3561    /// Convenience function to bind a mailbox.
3562    pub fn bind_mailbox(&self, mailbox: Mailbox) -> bool {
3563        self.bind(mailbox.actor_addr().id().clone(), mailbox)
3564    }
3565
3566    /// Route status messages to the provided sender, regardless of the
3567    /// destination actor's liveness.
3568    pub fn bind_status(&self, sender: impl MailboxSender + 'static) -> bool {
3569        self.status_sender.set(Box::new(sender)).is_ok()
3570    }
3571
3572    /// Unbind the sender associated with the provided actor ID. After
3573    /// unbinding, the muxer will no longer be able to send messages to
3574    /// that actor.
3575    #[allow(dead_code)]
3576    pub(crate) fn unbind(&self, actor_id: &ActorId) {
3577        self.mailboxes.remove_if(actor_id, |_, mailbox| {
3578            matches!(mailbox, ActorMailbox::Bound(_))
3579        });
3580    }
3581
3582    /// Deliver a message for an actor that was not bound when
3583    /// [`MailboxSender::post_unchecked`] looked it up.
3584    fn post_unbound(
3585        &self,
3586        envelope: MessageEnvelope,
3587        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
3588    ) {
3589        let timeout = hyperactor_config::global::get(crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT);
3590        match self.mailboxes.entry(envelope.dest().actor_id().clone()) {
3591            Entry::Occupied(mut entry) => match entry.get_mut() {
3592                ActorMailbox::Bound(sender) => sender.post(envelope, return_handle),
3593                ActorMailbox::Buffering(pending) => pending.push((envelope, return_handle)),
3594            },
3595            Entry::Vacant(_) if timeout.is_zero() => {
3596                let failure = actor_not_exist(&envelope);
3597                envelope.undeliverable(failure, return_handle);
3598            }
3599            Entry::Vacant(entry) => {
3600                let actor_id = entry.key().clone();
3601                entry.insert(ActorMailbox::Buffering(vec![(envelope, return_handle)]));
3602                let mailboxes = Arc::downgrade(&self.mailboxes);
3603                crate::init::get_runtime().spawn(async move {
3604                    tokio::time::sleep(timeout).await;
3605                    let Some(mailboxes) = mailboxes.upgrade() else {
3606                        return;
3607                    };
3608                    let Some((actor_id, ActorMailbox::Buffering(expired))) = mailboxes
3609                        .remove_if(&actor_id, |_, mailbox| {
3610                            matches!(mailbox, ActorMailbox::Buffering(_))
3611                        })
3612                    else {
3613                        return;
3614                    };
3615                    tracing::warn!(
3616                        %actor_id,
3617                        count = expired.len(),
3618                        ?timeout,
3619                        "actor did not bind in time; returning buffered messages",
3620                    );
3621                    for (envelope, return_handle) in expired {
3622                        let failure = actor_not_exist(&envelope);
3623                        envelope.undeliverable(failure, return_handle);
3624                    }
3625                });
3626            }
3627        }
3628    }
3629}
3630
3631fn actor_not_exist(envelope: &MessageEnvelope) -> DeliveryFailure {
3632    DeliveryFailure::new(InvalidReference::new(
3633        envelope.dest().actor_addr(),
3634        InvalidReferenceReason::ActorNotExist,
3635    ))
3636}
3637
3638#[async_trait]
3639impl MailboxSender for MailboxMuxer {
3640    fn post_unchecked(
3641        &self,
3642        envelope: MessageEnvelope,
3643        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
3644    ) {
3645        if envelope.dest().is_control_port_kind(ControlPort::Status)
3646            && let Some(sender) = self.status_sender.get()
3647        {
3648            sender.post(envelope, return_handle);
3649            return;
3650        }
3651
3652        if let Some(mailbox) = self.mailboxes.get(envelope.dest().actor_id())
3653            && let ActorMailbox::Bound(sender) = mailbox.value()
3654        {
3655            return sender.post(envelope, return_handle);
3656        }
3657        self.post_unbound(envelope, return_handle);
3658    }
3659
3660    async fn flush(&self) -> Result<(), anyhow::Error> {
3661        let senders: Vec<_> = self
3662            .mailboxes
3663            .iter()
3664            .filter_map(|mailbox| match mailbox.value() {
3665                ActorMailbox::Bound(sender) => Some(Arc::clone(sender)),
3666                ActorMailbox::Buffering(_) => None,
3667            })
3668            .collect();
3669        for sender in senders {
3670            sender.flush().await?;
3671        }
3672        Ok(())
3673    }
3674}
3675
3676/// MailboxRouter routes messages to the sender that is bound to its
3677/// nearest prefix.
3678#[derive(Clone)]
3679pub struct MailboxRouter {
3680    entries: Arc<RwLock<BTreeMap<Addr, Arc<dyn MailboxSender + Send + Sync>>>>,
3681}
3682
3683impl Default for MailboxRouter {
3684    fn default() -> Self {
3685        Self::new()
3686    }
3687}
3688
3689impl MailboxRouter {
3690    /// Create a new, empty router.
3691    pub fn new() -> Self {
3692        Self {
3693            entries: Arc::new(RwLock::new(BTreeMap::new())),
3694        }
3695    }
3696
3697    /// Downgrade this router to a [`WeakMailboxRouter`].
3698    pub fn downgrade(&self) -> WeakMailboxRouter {
3699        WeakMailboxRouter(Arc::downgrade(&self.entries))
3700    }
3701
3702    /// Returns a boxed sender that first attempts to find a route in
3703    /// this router's table; otherwise posts the message to the provided
3704    /// fallback sender.
3705    pub fn fallback(&self, default: BoxedMailboxSender) -> BoxedMailboxSender {
3706        FallbackMailboxRouter {
3707            router: self.clone(),
3708            default,
3709        }
3710        .into_boxed()
3711    }
3712
3713    /// Bind the provided sender to the given reference. The destination
3714    /// is treated as a prefix to which messages can be routed, and
3715    /// messages are routed to their longest matching prefix.
3716    pub fn bind(&self, dest: impl Into<Addr>, sender: impl MailboxSender + 'static) {
3717        let dest = dest.into();
3718        let mut w = self.entries.write().unwrap();
3719        w.insert(dest, Arc::new(sender));
3720    }
3721
3722    /// Remove the binding for the given reference. Only the exact
3723    /// point is removed; other bindings under the same prefix are
3724    /// unaffected.
3725    pub fn unbind(&self, dest: &Addr) {
3726        let mut w = self.entries.write().unwrap();
3727        w.remove(dest);
3728    }
3729
3730    fn sender(&self, actor_ref: &ActorAddr) -> Option<Arc<dyn MailboxSender + Send + Sync>> {
3731        let reference = Addr::from(actor_ref.clone());
3732        match self
3733            .entries
3734            .read()
3735            .unwrap()
3736            .lower_bound(Excluded(&reference))
3737            .prev()
3738        {
3739            None => None,
3740            Some((key, sender)) if key.is_prefix_of(&reference) => Some(sender.clone()),
3741            Some(_) => None,
3742        }
3743    }
3744}
3745
3746#[async_trait]
3747impl MailboxSender for MailboxRouter {
3748    fn post_unchecked(
3749        &self,
3750        envelope: MessageEnvelope,
3751        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
3752    ) {
3753        let dest_actor_ref = envelope.dest().actor_addr();
3754        match self.sender(&dest_actor_ref) {
3755            None => {
3756                let target = envelope.dest().clone();
3757                let failure = DeliveryFailure::new(UndeliverableReason::Transport(
3758                    TransportFailure::new(target, TransportFailureReason::NoRoute),
3759                ));
3760                envelope.undeliverable(failure, return_handle)
3761            }
3762            Some(sender) => sender.post(envelope, return_handle),
3763        }
3764    }
3765
3766    async fn flush(&self) -> Result<(), anyhow::Error> {
3767        let senders: Vec<_> = self.entries.read().unwrap().values().cloned().collect();
3768        let futs: Vec<_> = senders.iter().map(|s| s.flush()).collect();
3769        futures::future::try_join_all(futs).await?;
3770        Ok(())
3771    }
3772}
3773
3774/// A router that first checks a [`MailboxRouter`] for a matching
3775/// prefix route, falling back to a default sender when none is found.
3776#[derive(Clone)]
3777pub struct FallbackMailboxRouter {
3778    router: MailboxRouter,
3779    default: BoxedMailboxSender,
3780}
3781
3782impl FallbackMailboxRouter {
3783    /// The fallback sender used when the router has no match.
3784    pub fn default_sender(&self) -> &BoxedMailboxSender {
3785        &self.default
3786    }
3787}
3788
3789#[async_trait]
3790impl MailboxSender for FallbackMailboxRouter {
3791    fn post_unchecked(
3792        &self,
3793        envelope: MessageEnvelope,
3794        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
3795    ) {
3796        let dest_actor_ref = envelope.dest().actor_addr();
3797        match self.router.sender(&dest_actor_ref) {
3798            Some(sender) => sender.post(envelope, return_handle),
3799            None => self.default.post(envelope, return_handle),
3800        }
3801    }
3802
3803    async fn flush(&self) -> Result<(), anyhow::Error> {
3804        let (r1, r2) = futures::future::join(self.router.flush(), self.default.flush()).await;
3805        r1?;
3806        r2?;
3807        Ok(())
3808    }
3809}
3810
3811/// A version of [`MailboxRouter`] that holds a weak reference to the underlying
3812/// state. This allows router references to be circular: an entity holding a reference
3813/// to the router may also contain the router itself.
3814///
3815/// TODO: this currently holds a weak reference to the entire router. This helps
3816/// prevent cycle leaks, but can cause excess memory usage as the cycle is at
3817/// the granularity of each entry. Possibly the router should allow weak references
3818/// on a per-entry basis.
3819#[derive(Debug, Clone)]
3820pub struct WeakMailboxRouter(Weak<RwLock<BTreeMap<Addr, Arc<dyn MailboxSender + Send + Sync>>>>);
3821
3822impl WeakMailboxRouter {
3823    /// Upgrade the weak router to a strong reference router.
3824    pub fn upgrade(&self) -> Option<MailboxRouter> {
3825        self.0.upgrade().map(|entries| MailboxRouter { entries })
3826    }
3827}
3828
3829#[async_trait]
3830impl MailboxSender for WeakMailboxRouter {
3831    fn post_unchecked(
3832        &self,
3833        envelope: MessageEnvelope,
3834        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
3835    ) {
3836        match self.upgrade() {
3837            Some(router) => router.post(envelope, return_handle),
3838            None => {
3839                let target = envelope.dest().clone();
3840                let failure =
3841                    DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
3842                        target,
3843                        TransportFailureReason::LinkUnavailable(
3844                            "mailbox router is gone".to_string(),
3845                        ),
3846                    )));
3847                envelope.undeliverable(failure, return_handle)
3848            }
3849        }
3850    }
3851
3852    async fn flush(&self) -> Result<(), anyhow::Error> {
3853        match self.upgrade() {
3854            Some(router) => router.flush().await,
3855            None => Ok(()),
3856        }
3857    }
3858}
3859
3860/// Returns true if `status` is `Closed` with a typed reason identifying a
3861/// stale session — the K8s "out-of-sequence message, expected seq 0, got N"
3862/// case where the peer's dispatcher GC'd the `SessionId` but our cached
3863/// `NetTx` still holds an `Outbox.next_seq` past 0. Re-dialing produces a
3864/// fresh session that the peer accepts.
3865///
3866/// Other close reasons (oversized frame, codec errors, etc.) intentionally
3867/// do not match: the message or peer is the problem and re-dialing would
3868/// just hit the same failure.
3869fn is_stale_session_close(status: &TxStatus) -> bool {
3870    matches!(status, TxStatus::Closed(CloseReason::SequenceMismatch(_)))
3871}
3872
3873/// A dynamic mailbox router that supports remote delivery.
3874///
3875/// `DialMailboxRouter` maintains a runtime address book mapping
3876/// references to `ChannelAddr`s. It holds a cache of active
3877/// connections and forwards messages to the appropriate
3878/// `MailboxClient`.
3879///
3880/// If a message destination is not bound, but is a "direct mode" address
3881/// (i.e., its proc id contains the channel address through which the proc
3882/// is reachable), then DialMailboxRouter dials the proc directly.
3883///
3884/// Messages sent to unknown destinations are routed to the `default`
3885/// sender, if present.
3886#[derive(Clone)]
3887pub struct DialMailboxRouter {
3888    address_book: Arc<RwLock<BTreeMap<Addr, ChannelAddr>>>,
3889    sender_cache: Arc<DashMap<ChannelAddr, Arc<MailboxClient>>>,
3890
3891    // The default sender, to which messages for unknown recipients
3892    // are sent. (This is like a default route in a routing table.)
3893    default: BoxedMailboxSender,
3894
3895    // When true, only dial direct-addressed procs if their transport
3896    // type is remote. Otherwise, fall back to the default sender.
3897    direct_addressed_remote_only: bool,
3898}
3899
3900impl Default for DialMailboxRouter {
3901    fn default() -> Self {
3902        Self::new()
3903    }
3904}
3905
3906impl DialMailboxRouter {
3907    /// Create a new [`DialMailboxRouter`] with an empty routing table.
3908    pub fn new() -> Self {
3909        Self::new_with_default(BoxedMailboxSender::new(UnroutableMailboxSender))
3910    }
3911
3912    /// Create a new [`DialMailboxRouter`] with an empty routing table,
3913    /// and a default sender. Any message with an unknown destination is
3914    /// dispatched on this default sender, unless the destination is
3915    /// direct-addressed, in which case it is dialed directly.
3916    pub fn new_with_default(default: BoxedMailboxSender) -> Self {
3917        Self {
3918            address_book: Arc::new(RwLock::new(BTreeMap::new())),
3919            sender_cache: Arc::new(DashMap::new()),
3920            default,
3921            direct_addressed_remote_only: false,
3922        }
3923    }
3924
3925    /// Create a new [`DialMailboxRouter`] with an empty routing table,
3926    /// and a default sender. Any message with an unknown destination is
3927    /// dispatched on this default sender, unless the destination is
3928    /// direct-addressed *and* has a remote channel transport type.
3929    pub fn new_with_default_direct_addressed_remote_only(default: BoxedMailboxSender) -> Self {
3930        Self {
3931            address_book: Arc::new(RwLock::new(BTreeMap::new())),
3932            sender_cache: Arc::new(DashMap::new()),
3933            default,
3934            direct_addressed_remote_only: true,
3935        }
3936    }
3937
3938    /// Binds a [`Addr`] to a [`ChannelAddr`], replacing any
3939    /// existing binding.
3940    ///
3941    /// If the address changes, the old sender is evicted from the
3942    /// cache to ensure fresh routing on next use.
3943    pub fn bind(&self, dest: impl Into<Addr>, addr: ChannelAddr) {
3944        let dest = dest.into();
3945        let addr = addr.into_dial_addr();
3946        if let Ok(mut w) = self.address_book.write() {
3947            if let Some(old_addr) = w.insert(dest.clone(), addr.clone())
3948                && old_addr != addr
3949            {
3950                tracing::info!("rebinding {:?} from {:?} to {:?}", dest, old_addr, addr);
3951                self.sender_cache.remove(&old_addr);
3952            }
3953        } else {
3954            tracing::error!("address book poisoned during bind of {:?}", dest);
3955        }
3956    }
3957
3958    /// Removes all address mappings with the given prefix from the
3959    /// router.
3960    ///
3961    /// Also evicts any corresponding cached senders to prevent reuse
3962    /// of stale connections.
3963    pub fn unbind(&self, dest: &Addr) {
3964        if let Ok(mut w) = self.address_book.write() {
3965            let to_remove: Vec<(Addr, ChannelAddr)> = w
3966                .range(dest..)
3967                .take_while(|(key, _)| dest.is_prefix_of(key))
3968                .map(|(key, addr)| (key.clone(), addr.clone()))
3969                .collect();
3970
3971            for (key, addr) in to_remove {
3972                tracing::info!("unbinding {:?} from {:?}", key, addr);
3973                w.remove(&key);
3974                self.sender_cache.remove(&addr);
3975            }
3976        } else {
3977            tracing::error!("address book poisoned during unbind of {:?}", dest);
3978        }
3979    }
3980
3981    /// Lookup an actor's channel in the router's address bok.
3982    pub fn lookup_addr(&self, actor_ref: &ActorAddr) -> Option<ChannelAddr> {
3983        let address_book = self.address_book.read().unwrap();
3984        let reference = Addr::from(actor_ref.clone());
3985        let found = address_book.lower_bound(Excluded(&reference)).prev();
3986
3987        // First try to look up the address in our address book; failing that,
3988        // extract the address from the ProcAddr (all procs are direct-addressed now).
3989        if let Some((key, addr)) = found
3990            && key.is_prefix_of(&reference)
3991        {
3992            Some(addr.clone().into_dial_addr())
3993        } else {
3994            let addr = actor_ref.addr().clone().into_dial_addr();
3995            if self.direct_addressed_remote_only {
3996                addr.transport().is_remote().then_some(addr)
3997            } else {
3998                Some(addr)
3999            }
4000        }
4001    }
4002
4003    /// Return all covering prefixes of this router. That is, all references that are not
4004    /// prefixed by another reference in the routing table
4005    pub fn prefixes(&self) -> BTreeSet<Addr> {
4006        let addrs = self.address_book.read().unwrap();
4007        let mut prefixes: BTreeSet<Addr> = BTreeSet::new();
4008        for reference in addrs.keys() {
4009            match prefixes.lower_bound(Excluded(reference)).peek_prev() {
4010                Some(candidate) if candidate.is_prefix_of(reference) => (),
4011                _ => {
4012                    prefixes.insert(reference.clone());
4013                }
4014            }
4015        }
4016
4017        prefixes
4018    }
4019
4020    fn dial(
4021        &self,
4022        addr: &ChannelAddr,
4023        actor_ref: &ActorAddr,
4024    ) -> Result<Arc<MailboxClient>, MailboxSenderError> {
4025        // The cache must self-heal when a peer rejects a stale session
4026        // (e.g. its dispatcher GC'd the SessionId after the prior connection
4027        // ended, but our cached NetTx has an Outbox.next_seq past 0). Without
4028        // eviction, the cached client is dead-on-arrival forever, since the
4029        // server rejects every reconnect with "out-of-sequence message,
4030        // expected seq 0, got N".
4031        //
4032        // Eviction is narrowly gated on this exact close reason. Re-dialing
4033        // a client closed for any other reason (oversized frame, codec
4034        // error, etc.) just produces a fresh session that fails the same
4035        // way and would turn the cache into an unbounded redial loop under
4036        // upstream retry. Other reasons stay cached so the existing
4037        // closed-channel fast-fail path can drain the retry budget cleanly.
4038        loop {
4039            match self.sender_cache.entry(addr.clone()) {
4040                Entry::Occupied(entry) => {
4041                    let status = entry.get().tx_status().borrow().clone();
4042                    if is_stale_session_close(&status) {
4043                        tracing::info!(
4044                            ?addr,
4045                            reason = ?status.as_closed(),
4046                            "evicting stale-session MailboxClient from DialMailboxRouter cache"
4047                        );
4048                        entry.remove();
4049                        continue;
4050                    }
4051                    return Ok(entry.get().clone());
4052                }
4053                Entry::Vacant(entry) => {
4054                    let tx = channel::dial(addr.clone()).map_err(|err| {
4055                        MailboxSenderError::new_unbound_type(
4056                            actor_ref.clone(),
4057                            MailboxSenderErrorKind::Channel(err),
4058                            "unknown",
4059                        )
4060                    })?;
4061                    let sender = Arc::new(MailboxClient::new(tx));
4062                    return Ok(entry.insert(sender).value().clone());
4063                }
4064            }
4065        }
4066    }
4067}
4068
4069#[async_trait]
4070impl MailboxSender for DialMailboxRouter {
4071    fn post_unchecked(
4072        &self,
4073        envelope: MessageEnvelope,
4074        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
4075    ) {
4076        let dest_actor_ref = envelope.dest().actor_addr();
4077        let Some(addr) = self.lookup_addr(&dest_actor_ref) else {
4078            self.default.post(envelope, return_handle);
4079            return;
4080        };
4081
4082        match self.dial(&addr, &dest_actor_ref) {
4083            Err(err) => {
4084                let target = envelope.dest().clone();
4085                let failure =
4086                    DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
4087                        target,
4088                        TransportFailureReason::DialFailed {
4089                            addr,
4090                            error: err.to_string(),
4091                        },
4092                    )));
4093                envelope.undeliverable(failure, return_handle)
4094            }
4095            Ok(sender) => sender.post(envelope, return_handle),
4096        }
4097    }
4098
4099    async fn flush(&self) -> Result<(), anyhow::Error> {
4100        let senders: Vec<_> = self
4101            .sender_cache
4102            .iter()
4103            .map(|entry| entry.value().clone())
4104            .collect();
4105        let mut futs: Vec<_> = senders.iter().map(|s| s.flush()).collect();
4106        futs.push(self.default.flush());
4107        futures::future::try_join_all(futs).await?;
4108        Ok(())
4109    }
4110}
4111
4112/// A MailboxSender that reports any envelope as undeliverable due to
4113/// routing failure.
4114#[derive(Debug)]
4115pub struct UnroutableMailboxSender;
4116
4117#[async_trait]
4118impl MailboxSender for UnroutableMailboxSender {
4119    fn post_unchecked(
4120        &self,
4121        envelope: MessageEnvelope,
4122        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
4123    ) {
4124        let target = envelope.dest().clone();
4125        let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
4126            target,
4127            TransportFailureReason::NoRoute,
4128        )));
4129        envelope.undeliverable(failure, return_handle);
4130    }
4131}
4132
4133#[cfg(test)]
4134mod tests {
4135
4136    use std::assert_matches;
4137    use std::mem::drop;
4138    use std::sync::atomic::AtomicUsize;
4139    use std::time::Duration;
4140
4141    use async_trait::async_trait;
4142    use hyperactor_config::NonZeroUsize;
4143    use timed_test::async_timed_test;
4144
4145    use super::*;
4146    use crate as hyperactor;
4147    use crate::Actor;
4148    use crate::ActorRef;
4149    use crate::Handler;
4150    use crate::accum;
4151    use crate::accum::ReducerMode;
4152    use crate::channel::ChannelTransport;
4153    use crate::context::Actor as _;
4154    use crate::context::Mailbox as MailboxContext;
4155    use crate::context::MailboxExt as _;
4156    use crate::endpoint::Endpoint as _;
4157    use crate::proc::Proc;
4158    use crate::testing::ids::test_actor_id;
4159    use crate::testing::ids::test_port_id;
4160    use crate::testing::ids::test_proc_id;
4161
4162    fn test_proc_ref(name: &str) -> Addr {
4163        Addr::Proc(test_proc_id(name))
4164    }
4165
4166    fn test_actor_ref(proc_name: &str, actor_name: &str) -> Addr {
4167        Addr::Actor(test_actor_id(proc_name, actor_name))
4168    }
4169
4170    fn root_transport_failure(envelope: &MessageEnvelope) -> &TransportFailure {
4171        let root_failure = envelope
4172            .root_delivery_failure()
4173            .expect("expected root delivery failure");
4174        let DeliveryFailureKind::Undeliverable(UndeliverableReason::Transport(transport)) =
4175            &root_failure.kind
4176        else {
4177            panic!("expected transport failure, got {root_failure}");
4178        };
4179        transport
4180    }
4181
4182    fn root_invalid_reference(envelope: &MessageEnvelope) -> &InvalidReference {
4183        let root_failure = envelope
4184            .root_delivery_failure()
4185            .expect("expected root delivery failure");
4186        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
4187            panic!("expected invalid reference, got {root_failure}");
4188        };
4189        invalid_reference
4190    }
4191
4192    struct ClosedChannelTx {
4193        addr: ChannelAddr,
4194        status: watch::Receiver<TxStatus>,
4195    }
4196
4197    impl ClosedChannelTx {
4198        fn new(addr: ChannelAddr) -> Self {
4199            let (_sender, status) = watch::channel(TxStatus::Closed(CloseReason::Other(
4200                "test channel closed".into(),
4201            )));
4202            Self { addr, status }
4203        }
4204    }
4205
4206    #[async_trait]
4207    impl channel::Tx<MessageEnvelope> for ClosedChannelTx {
4208        fn do_post(&self, message: MessageEnvelope, completion: CompletionSink<MessageEnvelope>) {
4209            completion.reject(SendError {
4210                error: ChannelError::Closed,
4211                message,
4212                reason: None,
4213            });
4214        }
4215
4216        fn addr(&self) -> ChannelAddr {
4217            self.addr.clone()
4218        }
4219
4220        fn status(&self) -> &watch::Receiver<TxStatus> {
4221            &self.status
4222        }
4223    }
4224
4225    #[test]
4226    fn test_error() {
4227        use crate::testing::ids::test_actor_id;
4228        let err = MailboxError::new(
4229            test_actor_id("myworld_2", "myactor"),
4230            MailboxErrorKind::Closed,
4231        );
4232        // ActorAddr display is now "actor_uid.proc_uid@location"
4233        let err_str = format!("{err}");
4234        assert!(
4235            err_str.contains("mailbox closed"),
4236            "expected error: {}",
4237            err_str
4238        );
4239        assert!(
4240            err_str.contains("@"),
4241            "expected ref-style location separator in {err_str}"
4242        );
4243    }
4244
4245    #[test]
4246    fn test_error_msg_renders_structured_delivery_failure() {
4247        let sender = test_actor_id("0", "sender");
4248        let dest = test_port_id("0", "dest", 42);
4249        let mut envelope = MessageEnvelope::serialize(sender, dest.clone(), &42u64, Flattrs::new())
4250            .expect("serialize");
4251        envelope.push_delivery_failure(DeliveryFailure::new(InvalidReference::new(
4252            dest,
4253            InvalidReferenceReason::PortNeverAllocated,
4254        )));
4255
4256        let error = envelope.error_msg().expect("expected error");
4257        assert!(error.contains("delivery failure: invalid reference"));
4258        assert!(error.contains("port never allocated"));
4259    }
4260
4261    #[test]
4262    fn test_delivery_failure_rendering_bounds_attrs() {
4263        use hyperactor_config::attrs::declare_attrs;
4264
4265        declare_attrs! {
4266            attr TEST_DELIVERY_FAILURE_ATTR: String;
4267        }
4268
4269        let target = test_port_id("0", "dest", 42);
4270        let mut attrs = Flattrs::new();
4271        let large_value = "x".repeat(MAX_RENDERED_DELIVERY_FAILURE_ATTRS_LEN * 2);
4272        attrs.set(TEST_DELIVERY_FAILURE_ATTR, large_value.clone());
4273        let failure = DeliveryFailure::with_attrs(
4274            InvalidReference::new(target, InvalidReferenceReason::PortNeverAllocated),
4275            attrs,
4276        );
4277        let rendered = failure.render_bounded();
4278
4279        assert!(rendered.contains("port never allocated"));
4280        assert!(rendered.contains("..."));
4281        assert!(
4282            !rendered.contains(&large_value),
4283            "rendering must not include unbounded attr values"
4284        );
4285    }
4286
4287    #[tokio::test]
4288    async fn test_mailbox_basic() {
4289        let mbox = Mailbox::new(test_actor_id("0", "test"));
4290        let (port, mut receiver) = mbox.open_port::<u64>();
4291        let port = port.bind();
4292
4293        mbox.serialize_and_send(&port, 123, monitored_return_handle())
4294            .unwrap();
4295        mbox.serialize_and_send(&port, 321, monitored_return_handle())
4296            .unwrap();
4297        assert_eq!(receiver.recv().await.unwrap(), 123u64);
4298        assert_eq!(receiver.recv().await.unwrap(), 321u64);
4299
4300        let serialized = wirevalue::Any::serialize(&999u64).unwrap();
4301        mbox.post(
4302            MessageEnvelope::new_unknown(port.port_addr().clone(), serialized),
4303            monitored_return_handle(),
4304        );
4305        assert_eq!(receiver.recv().await.unwrap(), 999u64);
4306    }
4307
4308    #[tokio::test]
4309    async fn test_mailbox_rejects_messages_for_other_actors() {
4310        let mbox = Mailbox::new(test_actor_id("0", "owner"));
4311        let dest = test_actor_id("0", "other").port_addr(Port::from(1234));
4312        let envelope =
4313            MessageEnvelope::serialize(mbox.actor_addr().clone(), dest, &42u64, Flattrs::new())
4314                .expect("serialize");
4315        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4316
4317        mbox.post(envelope, return_handle);
4318
4319        let Undeliverable::Returned(undelivered) =
4320            tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
4321                .await
4322                .expect("timed out waiting for undeliverable")
4323                .expect("return port closed")
4324        else {
4325            panic!("expected returned message");
4326        };
4327        assert!(
4328            undelivered
4329                .error_msg()
4330                .expect("expected error")
4331                .contains("wrong mailbox owner")
4332        );
4333        let root_failure = undelivered
4334            .root_delivery_failure()
4335            .expect("expected root delivery failure");
4336        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
4337            panic!("expected invalid reference, got {root_failure}");
4338        };
4339        assert_eq!(
4340            invalid_reference.reason,
4341            InvalidReferenceReason::WrongMailboxOwner
4342        );
4343    }
4344
4345    #[tokio::test]
4346    async fn test_ephemeral_port_orders_raw_and_serialized_sends() {
4347        let proc = Proc::isolated();
4348        let client = proc.client("client");
4349        let (port_handle, mut receiver) = client.open_port::<u64>();
4350        let port = port_handle.bind();
4351        let session_id = client.instance().sequencer().session_id();
4352
4353        let mut headers = Flattrs::new();
4354        headers.set(SEQ_INFO, SeqInfo::Session { session_id, seq: 2 });
4355        let envelope = MessageEnvelope::new(
4356            client.mailbox().actor_addr().clone(),
4357            port.port_addr().clone(),
4358            wirevalue::Any::serialize(&2u64).unwrap(),
4359            headers,
4360        );
4361        client.mailbox().post(envelope, monitored_return_handle());
4362
4363        port_handle.try_post(&client, 1u64).unwrap();
4364
4365        assert_eq!(receiver.recv().await.unwrap(), 1);
4366        assert_eq!(receiver.recv().await.unwrap(), 2);
4367    }
4368
4369    #[tokio::test]
4370    async fn test_ttl_expiration_records_root_delivery_failure() {
4371        let mbox = Mailbox::new(test_actor_id("0", "test"));
4372        let (port, _) = mbox.open_port::<u64>();
4373        let port_ref = port.bind();
4374        let envelope = MessageEnvelope::serialize(
4375            mbox.actor_addr().clone(),
4376            port_ref.port_addr().clone(),
4377            &42u64,
4378            Flattrs::new(),
4379        )
4380        .expect("serialize")
4381        .set_ttl(0);
4382        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4383
4384        mbox.post(envelope, return_handle);
4385
4386        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
4387            .await
4388            .expect("timed out waiting for undeliverable")
4389            .expect("return port closed")
4390            .into_message()
4391            .expect("expected returned envelope");
4392        let root_failure = undelivered
4393            .root_delivery_failure()
4394            .expect("expected root delivery failure");
4395        assert!(
4396            matches!(root_failure.kind, DeliveryFailureKind::Expired(_)),
4397            "expected expired delivery failure, got {root_failure}"
4398        );
4399    }
4400
4401    #[tokio::test]
4402    async fn test_missing_handler_port_records_invalid_reference() {
4403        let mbox = Mailbox::new(test_actor_id("0", "test"));
4404        let dest = mbox.actor_addr().port_addr(Port::handler::<TestMessage>());
4405        let envelope = MessageEnvelope::serialize(
4406            mbox.actor_addr().clone(),
4407            dest,
4408            &TestMessage,
4409            Flattrs::new(),
4410        )
4411        .expect("serialize");
4412        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4413
4414        mbox.post(envelope, return_handle);
4415
4416        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
4417            .await
4418            .expect("timed out waiting for undeliverable")
4419            .expect("return port closed")
4420            .into_message()
4421            .expect("expected returned envelope");
4422        let root_failure = undelivered
4423            .root_delivery_failure()
4424            .expect("expected root delivery failure");
4425        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
4426            panic!("expected invalid reference, got {root_failure}");
4427        };
4428        assert_eq!(
4429            invalid_reference.reason,
4430            InvalidReferenceReason::HandlerNotBound
4431        );
4432    }
4433
4434    #[tokio::test]
4435    async fn test_missing_dropped_port_records_recipient_gone() {
4436        let mbox = Mailbox::new(test_actor_id("0", "test"));
4437        let (port, receiver) = mbox.open_port::<u64>();
4438        let port_ref = port.bind();
4439        drop(receiver);
4440        let envelope = MessageEnvelope::serialize(
4441            mbox.actor_addr().clone(),
4442            port_ref.port_addr().clone(),
4443            &42u64,
4444            Flattrs::new(),
4445        )
4446        .expect("serialize");
4447        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4448
4449        mbox.post(envelope, return_handle);
4450
4451        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
4452            .await
4453            .expect("timed out waiting for undeliverable")
4454            .expect("return port closed")
4455            .into_message()
4456            .expect("expected returned envelope");
4457        let root_failure = undelivered
4458            .root_delivery_failure()
4459            .expect("expected root delivery failure");
4460        let DeliveryFailureKind::Undeliverable(UndeliverableReason::PortGone(port_gone)) =
4461            &root_failure.kind
4462        else {
4463            panic!("expected port gone, got {root_failure}");
4464        };
4465        assert_eq!(port_gone.port, *port_ref.port_addr());
4466    }
4467
4468    #[tokio::test]
4469    async fn test_missing_never_allocated_port_records_invalid_reference() {
4470        let mbox = Mailbox::new(test_actor_id("0", "test"));
4471        let dest = mbox.actor_addr().port_addr(Port::from(0));
4472        let envelope =
4473            MessageEnvelope::serialize(mbox.actor_addr().clone(), dest, &42u64, Flattrs::new())
4474                .expect("serialize");
4475        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4476
4477        mbox.post(envelope, return_handle);
4478
4479        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
4480            .await
4481            .expect("timed out waiting for undeliverable")
4482            .expect("return port closed")
4483            .into_message()
4484            .expect("expected returned envelope");
4485        let root_failure = undelivered
4486            .root_delivery_failure()
4487            .expect("expected root delivery failure");
4488        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
4489            panic!("expected invalid reference, got {root_failure}");
4490        };
4491        assert_eq!(
4492            invalid_reference.reason,
4493            InvalidReferenceReason::PortNeverAllocated
4494        );
4495    }
4496
4497    #[tokio::test]
4498    async fn test_mailbox_accum() {
4499        let proc = Proc::isolated();
4500        let client = proc.client("client");
4501        let (port, mut receiver) = client
4502            .mailbox()
4503            .open_accum_port(accum::join_semilattice::<accum::Max<i64>>());
4504
4505        for i in -3..4 {
4506            port.post(&client, accum::Max(i));
4507            let received: accum::Max<i64> = receiver.recv().await.unwrap();
4508            let msg = received.get();
4509            assert_eq!(msg, &i);
4510        }
4511        // Send a smaller or same value. Should still receive the previous max.
4512        for i in -3..4 {
4513            port.post(&client, accum::Max(i));
4514            assert_eq!(receiver.recv().await.unwrap().get(), &3);
4515        }
4516        // send a larger value. Should receive the new max.
4517        port.post(&client, accum::Max(4));
4518        assert_eq!(receiver.recv().await.unwrap().get(), &4);
4519
4520        // Send multiple updates. Should only receive the final change.
4521        for i in 5..10 {
4522            port.post(&client, accum::Max(i));
4523        }
4524        assert_eq!(receiver.recv().await.unwrap().get(), &9);
4525        port.post(&client, accum::Max(1));
4526        port.post(&client, accum::Max(3));
4527        port.post(&client, accum::Max(2));
4528        assert_eq!(receiver.recv().await.unwrap().get(), &9);
4529    }
4530
4531    #[test]
4532    fn test_port_and_reducer() {
4533        let mbox = Mailbox::new(test_actor_id("0", "test"));
4534        // accum port could have reducer typehash
4535        {
4536            let accumulator = accum::join_semilattice::<accum::Max<u64>>();
4537            let reducer_spec = accumulator.reducer_spec().unwrap();
4538            let (port, _) = mbox.open_accum_port(accum::join_semilattice::<accum::Max<u64>>());
4539            assert_eq!(port.inner.reducer_spec, Some(reducer_spec.clone()));
4540            let port_ref = port.bind();
4541            assert_eq!(port_ref.reducer_spec(), &Some(reducer_spec));
4542        }
4543        // idle-flush accum port should put its reducer and options on the bound ref
4544        {
4545            let accumulator = accum::join_semilattice::<accum::Max<u64>>();
4546            let reducer_spec = accumulator.reducer_spec().unwrap();
4547            let reducer_opts = IdleFlushReducerOpts {
4548                idle_timeout: Duration::from_millis(50),
4549                abandon_timeout: Duration::from_secs(30),
4550                expected_updates_per_destination: NonZeroUsize::MIN,
4551            };
4552            let (port, _) = mbox.open_idle_flush_accum_port(accumulator, reducer_opts.clone());
4553            let port_ref = port.bind();
4554            let repr = crate::ref_::IdleFlushPortRefRepr::try_from(&port_ref)
4555                .expect("idle-flush port reference should have a representation");
4556
4557            assert_eq!(port_ref.reducer_spec(), &Some(reducer_spec));
4558            assert_eq!(repr.reducer_opts(), &reducer_opts);
4559        }
4560        // normal port should not have reducer typehash
4561        {
4562            let (port, _) = mbox.open_port::<u64>();
4563            assert_eq!(port.inner.reducer_spec, None);
4564            let port_ref = port.bind();
4565            assert_eq!(port_ref.reducer_spec(), &None);
4566        }
4567    }
4568
4569    #[tokio::test]
4570    async fn test_mailbox_once() {
4571        let proc = Proc::isolated();
4572        let client = proc.client("client");
4573
4574        let (port, receiver) = client.open_once_port::<u64>();
4575
4576        // let port_id = port.port_addr().clone();
4577
4578        port.post(&client, 123u64);
4579        assert_eq!(receiver.recv().await.unwrap(), 123u64);
4580
4581        // // The borrow checker won't let us send again on the port
4582        // // (good!), but we stashed the port-id and so we can try on the
4583        // // serialized interface.
4584        // let Err(err) = mbox
4585        //     .send_serialized(&port_id, &wirevalue::Any(Vec::new()))
4586        //     .await
4587        // else {
4588        //     unreachable!()
4589        // };
4590        // assert_matches!(err.kind(), MailboxSenderErrorKind::Closed);
4591    }
4592
4593    #[cfg(any())]
4594    #[tokio::test]
4595    async fn test_mailbox_receiver_drop() {
4596        let mbox = Mailbox::new(test_actor_id("0", "test"));
4597        let (port, mut receiver) = mbox.open_port::<u64>();
4598        // Make sure we go through "remote" path.
4599        let port = port.bind();
4600        mbox.serialize_and_send(&port, 123u64, monitored_return_handle())
4601            .unwrap();
4602        assert_eq!(receiver.recv().await.unwrap(), 123u64);
4603        drop(receiver);
4604        let Err(err) = mbox.serialize_and_send(&port, 123u64, monitored_return_handle()) else {
4605            panic!();
4606        };
4607
4608        assert_matches!(err.kind(), MailboxSenderErrorKind::Closed);
4609        assert_matches!(err.location(), PortLocation::Bound(bound) if *bound == *port.port_addr());
4610    }
4611
4612    #[tokio::test]
4613    async fn test_mailbox_type_mismatch_does_not_evict_unbounded_port() {
4614        let mbox = Mailbox::new(test_actor_id("0", "test"));
4615        let (port, mut receiver) = mbox.open_port::<u64>();
4616        let port = port.bind();
4617        let port_index = port.port_addr().index();
4618        let target: Addr = port.port_addr().clone().into();
4619        let (return_handle, mut return_receiver) =
4620            crate::mailbox::undeliverable::new_undeliverable_port();
4621
4622        let wrong_message = wirevalue::Any::serialize(&TestMessage).unwrap();
4623        mbox.post(
4624            MessageEnvelope::new_unknown(port.port_addr().clone(), wrong_message),
4625            return_handle.clone(),
4626        );
4627
4628        let envelope = tokio::time::timeout(Duration::from_secs(1), return_receiver.recv())
4629            .await
4630            .expect("undeliverable mismatch should arrive")
4631            .unwrap()
4632            .into_message()
4633            .expect("expected returned envelope");
4634        assert!(
4635            envelope
4636                .error_msg()
4637                .is_some_and(|message| message.contains("protocol mismatch")),
4638            "expected protocol mismatch in {envelope}",
4639        );
4640        let invalid_reference = root_invalid_reference(&envelope);
4641        assert_eq!(invalid_reference.target, target);
4642        assert_eq!(
4643            invalid_reference.reason,
4644            InvalidReferenceReason::ProtocolMismatch
4645        );
4646        assert!(
4647            mbox.inner.ports.contains_key(&Port::from(port_index)),
4648            "deserialization mismatch should not evict reusable port",
4649        );
4650
4651        mbox.serialize_and_send(&port, 123u64, return_handle)
4652            .unwrap();
4653        assert_eq!(
4654            tokio::time::timeout(Duration::from_secs(1), receiver.recv())
4655                .await
4656                .expect("valid message should still be delivered")
4657                .unwrap(),
4658            123u64
4659        );
4660    }
4661
4662    #[tokio::test]
4663    async fn test_mailbox_closed_unbounded_port_is_removed_after_send_failure() {
4664        let mbox = Mailbox::new(test_actor_id("0", "test"));
4665        let port_index = mbox.allocate_port();
4666        let port_id = mbox.actor_addr().port_addr(Port::from(port_index));
4667        let port = crate::PortRef::attest(port_id.clone());
4668        let (return_handle, mut return_receiver) =
4669            crate::mailbox::undeliverable::new_undeliverable_port();
4670        let (sender, receiver) = sequenced_unbounded::<SequencedEnvelope<u64>>();
4671
4672        drop(receiver);
4673
4674        mbox.inner.ports.insert(
4675            Port::from(port_index),
4676            Arc::new(UnboundedSender::new(
4677                UnboundedPortSender::Sequenced(sender),
4678                port_id,
4679            )),
4680        );
4681
4682        mbox.serialize_and_send(&port, 123u64, return_handle.clone())
4683            .unwrap();
4684
4685        let envelope = tokio::time::timeout(Duration::from_secs(1), return_receiver.recv())
4686            .await
4687            .expect("closed port should produce undeliverable")
4688            .unwrap()
4689            .into_message()
4690            .expect("expected returned envelope");
4691        let first_error = envelope.error_msg().expect("expected delivery error");
4692        assert!(
4693            first_error.contains("port gone"),
4694            "expected port-gone error in {envelope}",
4695        );
4696        assert!(
4697            !mbox.inner.ports.contains_key(&Port::from(port_index)),
4698            "dead reusable port should be removed after send failure",
4699        );
4700
4701        mbox.serialize_and_send(&port, 456u64, return_handle)
4702            .unwrap();
4703        let envelope = tokio::time::timeout(Duration::from_secs(1), return_receiver.recv())
4704            .await
4705            .expect("removed port should produce unbound undeliverable")
4706            .unwrap()
4707            .into_message()
4708            .expect("expected returned envelope");
4709        let second_error = envelope.error_msg().expect("expected delivery error");
4710        assert_eq!(
4711            first_error, second_error,
4712            "dead-port undeliverable should match unbound-port undeliverable exactly",
4713        );
4714    }
4715
4716    #[tokio::test]
4717    async fn test_mailbox_once_type_mismatch_preserves_sender_until_delivery() {
4718        let mbox = Mailbox::new(test_actor_id("0", "test"));
4719        let (port, receiver) = mbox.open_once_port::<u64>();
4720        let port = port.bind();
4721        let port_index = port.port_addr().index();
4722        let target: Addr = port.port_addr().clone().into();
4723        let (return_handle, mut return_receiver) =
4724            crate::mailbox::undeliverable::new_undeliverable_port();
4725
4726        let wrong_message = wirevalue::Any::serialize(&TestMessage).unwrap();
4727        mbox.post(
4728            MessageEnvelope::new_unknown(port.port_addr().clone(), wrong_message),
4729            return_handle.clone(),
4730        );
4731
4732        let envelope = tokio::time::timeout(Duration::from_secs(1), return_receiver.recv())
4733            .await
4734            .expect("once-port mismatch should arrive")
4735            .unwrap()
4736            .into_message()
4737            .expect("expected returned envelope");
4738        assert!(
4739            envelope
4740                .error_msg()
4741                .is_some_and(|message| message.contains("protocol mismatch")),
4742            "expected protocol mismatch in {envelope}",
4743        );
4744        let invalid_reference = root_invalid_reference(&envelope);
4745        assert_eq!(invalid_reference.target, target);
4746        assert_eq!(
4747            invalid_reference.reason,
4748            InvalidReferenceReason::ProtocolMismatch
4749        );
4750        assert!(
4751            mbox.inner.ports.contains_key(&Port::from(port_index)),
4752            "once port should survive deserialization mismatch before delivery",
4753        );
4754
4755        mbox.serialize_and_send_once(port, 123u64, return_handle)
4756            .unwrap();
4757        assert_eq!(
4758            tokio::time::timeout(Duration::from_secs(1), receiver.recv())
4759                .await
4760                .expect("valid once message should still be delivered")
4761                .unwrap(),
4762            123u64
4763        );
4764        assert!(
4765            !mbox.inner.ports.contains_key(&Port::from(port_index)),
4766            "successful once send should remove the sender entry",
4767        );
4768    }
4769
4770    #[tokio::test]
4771    async fn test_drain() {
4772        let mbox = Mailbox::new(test_actor_id("0", "test"));
4773
4774        let (port, mut receiver) = mbox.open_port();
4775        let port = port.bind();
4776
4777        for i in 0..10 {
4778            mbox.serialize_and_send(&port, i, monitored_return_handle())
4779                .unwrap();
4780        }
4781
4782        for i in 0..10 {
4783            assert_eq!(receiver.recv().await.unwrap(), i);
4784        }
4785
4786        assert!(receiver.drain().is_empty());
4787    }
4788
4789    #[tokio::test]
4790    async fn test_mailbox_muxer() {
4791        let config = hyperactor_config::global::lock();
4792        let _config_guard = config.override_key(
4793            crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT,
4794            Duration::from_millis(100),
4795        );
4796        let muxer = MailboxMuxer::new();
4797
4798        let mbox0 = Mailbox::new(test_actor_id("0", "actor1"));
4799        let mbox1 = Mailbox::new(test_actor_id("0", "actor2"));
4800
4801        muxer.bind(mbox0.actor_addr().id().clone(), mbox0.clone());
4802        muxer.bind(mbox1.actor_addr().id().clone(), mbox1.clone());
4803
4804        let (port, receiver) = mbox0.open_once_port::<u64>();
4805
4806        let muxer_sender = muxer.clone();
4807        let proc = Proc::configured(test_proc_id("0"), BoxedMailboxSender::new(muxer));
4808        let client = proc.client("client");
4809
4810        port.post(&client, 123u64);
4811        assert_eq!(receiver.recv().await.unwrap(), 123u64);
4812
4813        let missing_actor = test_actor_id("0", "missing_actor");
4814        let missing_dest = missing_actor.port_addr(Port::from(1234));
4815        let envelope = MessageEnvelope::serialize(
4816            client.self_addr().clone(),
4817            missing_dest,
4818            &456u64,
4819            Flattrs::new(),
4820        )
4821        .expect("serialize");
4822        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4823
4824        muxer_sender.post(envelope, return_handle);
4825
4826        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
4827            .await
4828            .expect("timed out waiting for undeliverable")
4829            .expect("return port closed")
4830            .into_message()
4831            .expect("expected returned envelope");
4832        let root_failure = undelivered
4833            .root_delivery_failure()
4834            .expect("expected root delivery failure");
4835        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
4836            panic!("expected invalid reference, got {root_failure}");
4837        };
4838        assert_eq!(invalid_reference.target, Addr::Actor(missing_actor));
4839        assert_eq!(
4840            invalid_reference.reason,
4841            InvalidReferenceReason::ActorNotExist
4842        );
4843
4844        /*
4845        let (tx, rx) = channel::local::new::<u64>();
4846        let (port, _) = mbox0.open_port::<u64>();
4847        let handle = muxer.clone().serve_port(port, rx).unwrap();
4848        muxer.unbind(mbox0.actor_addr());
4849        tx.send(123u64).await.unwrap();
4850        let Ok(Err(err)) = handle.await else { panic!() };
4851        assert_eq!(err.actor_addr(), &actor_id(0));
4852        */
4853    }
4854
4855    fn muxer_envelope(dest: PortAddr, value: u64) -> MessageEnvelope {
4856        MessageEnvelope::serialize(test_actor_id("0", "sender"), dest, &value, Flattrs::new())
4857            .expect("serialize")
4858    }
4859
4860    #[tokio::test]
4861    async fn muxer_buffers_until_bind() {
4862        let muxer = MailboxMuxer::new();
4863        let mbox = Mailbox::new(test_actor_id("0", "late"));
4864        let (port, mut receiver) = mbox.open_port::<u64>();
4865        let dest = port.bind().port_addr().clone();
4866        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4867
4868        muxer.post(muxer_envelope(dest.clone(), 1), return_handle.clone());
4869        muxer.post(muxer_envelope(dest.clone(), 2), return_handle.clone());
4870        assert!(
4871            receiver.try_recv().unwrap().is_none(),
4872            "messages must wait for the mailbox to bind"
4873        );
4874
4875        assert!(muxer.bind_mailbox(mbox.clone()));
4876        muxer.post(muxer_envelope(dest, 3), return_handle.clone());
4877
4878        for expected in 1..=3u64 {
4879            assert_eq!(receiver.recv().await.unwrap(), expected);
4880        }
4881        assert!(
4882            return_rx.try_recv().unwrap().is_none(),
4883            "no message should bounce"
4884        );
4885        assert!(
4886            !muxer.bind_mailbox(mbox),
4887            "a second bind must not replace the first"
4888        );
4889    }
4890
4891    #[tokio::test]
4892    async fn muxer_zero_timeout_bounces_immediately() {
4893        let config = hyperactor_config::global::lock();
4894        let _config_guard = config.override_key(
4895            crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT,
4896            Duration::ZERO,
4897        );
4898        let muxer = MailboxMuxer::new();
4899        let missing_actor = test_actor_id("0", "missing_actor");
4900        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
4901
4902        muxer.post(
4903            muxer_envelope(missing_actor.port_addr(Port::from(1234)), 1),
4904            return_handle,
4905        );
4906
4907        let undelivered = return_rx
4908            .try_recv()
4909            .unwrap()
4910            .expect("zero timeout must bounce without buffering")
4911            .into_message()
4912            .expect("expected returned envelope");
4913        let root_failure = undelivered
4914            .root_delivery_failure()
4915            .expect("expected root delivery failure");
4916        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
4917            panic!("expected invalid reference, got {root_failure}");
4918        };
4919        assert_eq!(
4920            invalid_reference.reason,
4921            InvalidReferenceReason::ActorNotExist
4922        );
4923    }
4924
4925    #[tokio::test]
4926    async fn test_local_client_server() {
4927        let mbox = Mailbox::new(test_actor_id("0", "actor0"));
4928        let (addr, rx) =
4929            channel::serve(ChannelAddr::any(ChannelTransport::Local)).expect("serve local");
4930        let tx = channel::dial(addr).expect("dial local");
4931        let serve_handle = mbox.clone().serve(rx);
4932        let client = MailboxClient::new(tx);
4933
4934        let (port, receiver) = mbox.open_once_port::<u64>();
4935        let port = port.bind();
4936
4937        client
4938            .serialize_and_send_once(port, 123u64, monitored_return_handle())
4939            .unwrap();
4940        assert_eq!(receiver.recv().await.unwrap(), 123u64);
4941        serve_handle.stop("fromt test");
4942        serve_handle.await.unwrap().unwrap();
4943    }
4944
4945    #[tokio::test]
4946    async fn test_mailbox_client_records_channel_closed_failure() {
4947        let mbox = Mailbox::new(test_actor_id("0", "actor0"));
4948        let client = MailboxClient::new(ClosedChannelTx::new(ChannelAddr::Local(0)));
4949        let addr = client.addr.clone();
4950
4951        let (port, _receiver) = mbox.open_once_port::<u64>();
4952        let port = port.bind();
4953        let target: Addr = port.port_addr().clone().into();
4954        let (return_handle, mut return_receiver) =
4955            crate::mailbox::undeliverable::new_undeliverable_port();
4956
4957        client
4958            .serialize_and_send_once(port, 123u64, return_handle)
4959            .unwrap();
4960
4961        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_receiver.recv())
4962            .await
4963            .expect("timed out waiting for undeliverable")
4964            .expect("return port closed")
4965            .into_message()
4966            .expect("expected returned envelope");
4967        let root_failure = undelivered
4968            .root_delivery_failure()
4969            .expect("expected root delivery failure");
4970        let DeliveryFailureKind::Undeliverable(UndeliverableReason::Transport(transport)) =
4971            &root_failure.kind
4972        else {
4973            panic!("expected transport failure, got {root_failure}");
4974        };
4975        assert_eq!(transport.target, target);
4976        assert_eq!(
4977            transport.reason,
4978            TransportFailureReason::ChannelClosed { addr }
4979        );
4980    }
4981
4982    #[tokio::test]
4983    async fn test_mailbox_router() {
4984        let mbox0 = Mailbox::new(test_actor_id("world0_0", "actor0"));
4985        let mbox1 = Mailbox::new(test_actor_id("world1_0", "actor0"));
4986        let mbox2 = Mailbox::new(test_actor_id("world1_1", "actor0"));
4987        let mbox3 = Mailbox::new(test_actor_id("world1_1", "actor1"));
4988
4989        let comms: Vec<(OncePortRef<u64>, OncePortReceiver<u64>)> =
4990            [&mbox0, &mbox1, &mbox2, &mbox3]
4991                .into_iter()
4992                .map(|mbox| {
4993                    let (port, receiver) = mbox.open_once_port::<u64>();
4994                    (port.bind(), receiver)
4995                })
4996                .collect();
4997
4998        let router = MailboxRouter::new();
4999
5000        router.bind(test_proc_ref("world0_0"), mbox0);
5001        router.bind(test_proc_ref("world1_0"), mbox1);
5002        router.bind(test_proc_ref("world1_1"), mbox2);
5003        router.bind(test_actor_ref("world1_1", "actor1"), mbox3);
5004
5005        for (i, (port, receiver)) in comms.into_iter().enumerate() {
5006            router
5007                .serialize_and_send_once(port, i as u64, monitored_return_handle())
5008                .unwrap();
5009            assert_eq!(receiver.recv().await.unwrap(), i as u64);
5010        }
5011
5012        // Test undeliverable messages, and that it is delivered with the appropriate fallback.
5013
5014        let mbox4 = Mailbox::new(test_actor_id("fallback_0", "actor"));
5015
5016        let (return_handle, mut return_receiver) =
5017            crate::mailbox::undeliverable::new_undeliverable_port();
5018        let (port, _receiver) = mbox4.open_once_port();
5019        let port = port.bind();
5020        let target: Addr = port.port_addr().clone().into();
5021        router
5022            .serialize_and_send_once(port, 0, return_handle.clone())
5023            .unwrap();
5024        let undelivered = return_receiver
5025            .recv()
5026            .await
5027            .unwrap()
5028            .into_message()
5029            .expect("expected returned envelope");
5030        let transport = root_transport_failure(&undelivered);
5031        assert_eq!(transport.target, target);
5032        assert_eq!(transport.reason, TransportFailureReason::NoRoute);
5033
5034        let router = router.fallback(mbox4.clone().into_boxed());
5035        let (port, receiver) = mbox4.open_once_port();
5036        router
5037            .serialize_and_send_once(port.bind(), 0, return_handle)
5038            .unwrap();
5039        assert_eq!(receiver.recv().await.unwrap(), 0);
5040    }
5041
5042    #[tokio::test]
5043    async fn test_weak_mailbox_router_records_link_unavailable_failure() {
5044        let router = MailboxRouter::new();
5045        let weak_router = router.downgrade();
5046        drop(router);
5047
5048        let mbox = Mailbox::new(test_actor_id("0", "actor0"));
5049        let (port, _receiver) = mbox.open_once_port::<u64>();
5050        let port = port.bind();
5051        let target: Addr = port.port_addr().clone().into();
5052        let (return_handle, mut return_receiver) =
5053            crate::mailbox::undeliverable::new_undeliverable_port();
5054
5055        weak_router
5056            .serialize_and_send_once(port, 123u64, return_handle)
5057            .unwrap();
5058
5059        let undelivered = return_receiver
5060            .recv()
5061            .await
5062            .unwrap()
5063            .into_message()
5064            .expect("expected returned envelope");
5065        let transport = root_transport_failure(&undelivered);
5066        assert_eq!(transport.target, target);
5067        assert_eq!(
5068            transport.reason,
5069            TransportFailureReason::LinkUnavailable("mailbox router is gone".to_string())
5070        );
5071    }
5072
5073    #[tokio::test]
5074    async fn test_unroutable_mailbox_sender_records_no_route_failure() {
5075        let mbox = Mailbox::new(test_actor_id("0", "actor0"));
5076        let (port, _receiver) = mbox.open_once_port::<u64>();
5077        let port = port.bind();
5078        let target: Addr = port.port_addr().clone().into();
5079        let (return_handle, mut return_receiver) =
5080            crate::mailbox::undeliverable::new_undeliverable_port();
5081
5082        UnroutableMailboxSender
5083            .serialize_and_send_once(port, 123u64, return_handle)
5084            .unwrap();
5085
5086        let undelivered = return_receiver
5087            .recv()
5088            .await
5089            .unwrap()
5090            .into_message()
5091            .expect("expected returned envelope");
5092        let transport = root_transport_failure(&undelivered);
5093        assert_eq!(transport.target, target);
5094        assert_eq!(transport.reason, TransportFailureReason::NoRoute);
5095    }
5096
5097    #[tokio::test]
5098    async fn test_dial_mailbox_router() {
5099        let router = DialMailboxRouter::new();
5100
5101        router.bind(test_proc_ref("world0_0"), "unix!@1".parse().unwrap());
5102        router.bind(test_proc_ref("world1_0"), "unix!@2".parse().unwrap());
5103        router.bind(test_proc_ref("world1_1"), "unix!@3".parse().unwrap());
5104        router.bind(
5105            test_actor_ref("world1_1", "actor1"),
5106            "unix!@4".parse().unwrap(),
5107        );
5108        // Bind a direct address -- we should use its bound address!
5109        // The actor must be on unix:@4 so that after unbinding, the prefix
5110        // route for world1_1 (unix!@3) is the fallback, not world1_1/actor1 (unix!@4).
5111        let direct_actor_ref: ActorAddr =
5112            ProcAddr::singleton("unix:@4".parse().unwrap(), "my_proc").actor_addr("my_actor");
5113        router.bind(
5114            Addr::Actor(direct_actor_ref.clone()),
5115            "unix:@5".parse().unwrap(),
5116        );
5117
5118        // We should be able to lookup the ids
5119        router
5120            .lookup_addr(&test_actor_id("world0_0", "actor"))
5121            .unwrap();
5122        router
5123            .lookup_addr(&test_actor_id("world1_0", "actor"))
5124            .unwrap();
5125
5126        let actor_id = direct_actor_ref;
5127        assert_eq!(
5128            router.lookup_addr(&actor_id).unwrap(),
5129            "unix!@5".parse().unwrap(),
5130        );
5131        router.unbind(&actor_id.clone().into());
5132        assert_eq!(
5133            router.lookup_addr(&actor_id).unwrap(),
5134            "unix!@4".parse().unwrap(),
5135        );
5136
5137        // Unbind procs so lookups fall back to the proc's direct address
5138        // (all procs are direct-addressed now, so lookup_addr always returns
5139        // Some; we verify the bound address is gone by checking the returned
5140        // address is the local fallback, not the originally bound one).
5141        let fallback = ChannelAddr::any(ChannelTransport::Local);
5142        router.unbind(&test_proc_ref("world1_0"));
5143        router.unbind(&test_proc_ref("world1_1"));
5144        assert_eq!(
5145            router
5146                .lookup_addr(&test_actor_id("world1_0", "actor1"))
5147                .unwrap(),
5148            fallback,
5149        );
5150        assert_eq!(
5151            router
5152                .lookup_addr(&test_actor_id("world1_1", "actor1"))
5153                .unwrap(),
5154            fallback,
5155        );
5156        router
5157            .lookup_addr(&test_actor_id("world0_0", "actor"))
5158            .unwrap();
5159        router.unbind(&test_proc_ref("world0_0"));
5160        assert_eq!(
5161            router
5162                .lookup_addr(&test_actor_id("world0_0", "actor"))
5163                .unwrap(),
5164            fallback,
5165        );
5166    }
5167
5168    #[test]
5169    fn test_dial_mailbox_router_canonicalizes_alias_addresses() {
5170        let router = DialMailboxRouter::new();
5171        let dial_to = ChannelAddr::from_zmq_url("tcp://127.0.0.1:9000").unwrap();
5172        let alias = ChannelAddr::from_zmq_url("tcp://127.0.0.1:9000@tcp://0.0.0.0:9000").unwrap();
5173
5174        router.bind(test_proc_ref("world_alias"), alias.clone());
5175        assert_eq!(
5176            router
5177                .lookup_addr(&test_actor_id("world_alias", "actor"))
5178                .unwrap(),
5179            dial_to
5180        );
5181
5182        let direct_actor_ref = ProcAddr::singleton(alias, "direct_alias").actor_addr("actor");
5183        assert_eq!(router.lookup_addr(&direct_actor_ref).unwrap(), dial_to);
5184    }
5185
5186    #[tokio::test]
5187    async fn test_dial_mailbox_router_records_dial_failure() {
5188        let router = DialMailboxRouter::new();
5189        let addr = ChannelAddr::Local(9_876_543_210);
5190        let mbox = Mailbox::new(test_actor_id("world0_0", "actor0"));
5191        router.bind(test_proc_ref("world0_0"), addr.clone());
5192
5193        let (port, _receiver) = mbox.open_once_port::<u64>();
5194        let port = port.bind();
5195        let target: Addr = port.port_addr().clone().into();
5196        let (return_handle, mut return_receiver) =
5197            crate::mailbox::undeliverable::new_undeliverable_port();
5198
5199        router
5200            .serialize_and_send_once(port, 123u64, return_handle)
5201            .unwrap();
5202
5203        let undelivered = return_receiver
5204            .recv()
5205            .await
5206            .unwrap()
5207            .into_message()
5208            .expect("expected returned envelope");
5209        let transport = root_transport_failure(&undelivered);
5210        assert_eq!(transport.target, target);
5211        let TransportFailureReason::DialFailed {
5212            addr: failure_addr,
5213            error,
5214        } = &transport.reason
5215        else {
5216            panic!("expected dial failure, got {}", transport.reason);
5217        };
5218        assert_eq!(failure_addr, &addr);
5219        assert!(
5220            error.contains("channel closed"),
5221            "unexpected error: {error}"
5222        );
5223    }
5224
5225    #[cfg(any())]
5226    #[tokio::test]
5227    async fn test_dial_mailbox_router_default() {
5228        let mbox0 = Mailbox::new(test_actor_id("world0_0", "actor0"));
5229        let mbox1 = Mailbox::new(test_actor_id("world1_0", "actor0"));
5230        let mbox2 = Mailbox::new(test_actor_id("world1_1", "actor0"));
5231        let mbox3 = Mailbox::new(test_actor_id("world1_1", "actor1"));
5232
5233        // We don't need to dial here, since we gain direct access to the
5234        // underlying routers.
5235        let root = MailboxRouter::new();
5236        let world0_router = DialMailboxRouter::new_with_default(root.boxed());
5237        let world1_router = DialMailboxRouter::new_with_default(root.boxed());
5238
5239        root.bind(test_proc_ref("world0"), world0_router.clone());
5240        root.bind(test_proc_ref("world1"), world1_router.clone());
5241
5242        let mailboxes = [&mbox0, &mbox1, &mbox2, &mbox3];
5243
5244        let mut handles = Vec::new(); // hold on to handles, or channels get closed
5245        for mbox in mailboxes.iter() {
5246            let (addr, rx) = channel::serve(ChannelAddr::any(ChannelTransport::Local)).unwrap();
5247            let handle = (*mbox).clone().serve(rx);
5248            handles.push(handle);
5249
5250            eprintln!("{}: {}", mbox.actor_addr(), addr);
5251            if mbox
5252                .actor_addr()
5253                .proc_addr()
5254                .label()
5255                .is_some_and(|l| l.as_str().starts_with("world0"))
5256            {
5257                world0_router.bind(Addr::from(mbox.actor_addr().clone()), addr);
5258            } else {
5259                world1_router.bind(Addr::from(mbox.actor_addr().clone()), addr);
5260            }
5261        }
5262
5263        // Make sure nodes are fully connected.
5264        for router in [root.boxed(), world0_router.boxed(), world1_router.boxed()] {
5265            for mbox in mailboxes.iter() {
5266                let (port, receiver) = mbox.open_once_port::<u64>();
5267                let port = port.bind();
5268                router
5269                    .serialize_and_send_once(port, 123u64, monitored_return_handle())
5270                    .unwrap();
5271                assert_eq!(receiver.recv().await.unwrap(), 123u64);
5272            }
5273        }
5274    }
5275
5276    #[test]
5277    fn test_is_stale_session_close() {
5278        // Only the typed SequenceMismatch variant identifies a stale session.
5279        let stale = TxStatus::Closed(CloseReason::SequenceMismatch(
5280            "out-of-sequence message, expected seq 0, got 7".into(),
5281        ));
5282        assert!(is_stale_session_close(&stale));
5283
5284        // Other close reasons must NOT match — re-dialing would just hit the
5285        // same failure and create an unbounded evict/redial loop.
5286        let oversize = TxStatus::Closed(CloseReason::OversizedFrame {
5287            size: 55_001_392,
5288            max: 50_000_000,
5289        });
5290        assert!(!is_stale_session_close(&oversize));
5291
5292        let generic = TxStatus::Closed(CloseReason::Other("test teardown".into()));
5293        assert!(!is_stale_session_close(&generic));
5294
5295        // Active is never stale.
5296        assert!(!is_stale_session_close(&TxStatus::Active));
5297    }
5298
5299    #[tokio::test]
5300    async fn test_dial_router_keeps_client_closed_for_non_stale_reason() {
5301        // A client closed for any reason other than the K8s sequence-mismatch
5302        // pattern must stay cached — re-dialing on, say, an oversize-frame
5303        // rejection would just produce a fresh session that fails the same
5304        // way under upstream retry, turning the cache into an unbounded
5305        // evict/redial loop. The closed entry sticks; upstream retries
5306        // fast-fail via the existing channel-closed path.
5307        let mbox = Mailbox::new(test_actor_id("non_stale_close_0", "actor0"));
5308        let (addr, rx) =
5309            channel::serve::<MessageEnvelope>(ChannelAddr::any(ChannelTransport::Local)).unwrap();
5310        let h = mbox.clone().serve(rx);
5311
5312        let router = DialMailboxRouter::new();
5313        router.bind(Addr::from(mbox.actor_addr().clone()), addr.clone());
5314
5315        let client1 = router.dial(&addr, mbox.actor_addr()).unwrap();
5316        let mut status = client1.tx_status().clone();
5317
5318        // Local transport behaves like a socket: the dialer connects lazily
5319        // on the first send and only observes peer teardown over an
5320        // established connection. Post a message (routed through client1) and
5321        // confirm receipt so the session actually connects, then drain acks so
5322        // no unacked messages remain — this makes the post-teardown close take
5323        // the fast connect-failure path rather than the delivery-timeout path.
5324        let (port, receiver) = mbox.open_once_port::<u64>();
5325        let port = port.bind();
5326        router
5327            .serialize_and_send_once(port, 67u64, monitored_return_handle())
5328            .unwrap();
5329        assert_eq!(receiver.recv().await.unwrap(), 67u64);
5330        client1.flush().await.unwrap();
5331
5332        // Tearing down the local server breaks the established connection; the
5333        // next reconnect fails (port freed) and the session closes with a
5334        // non-stale reason, so it must not trigger eviction.
5335        h.stop("test teardown");
5336        let _ = h.await;
5337        while !status.borrow_and_update().is_closed() {
5338            status.changed().await.unwrap();
5339        }
5340        assert!(!is_stale_session_close(&status.borrow()));
5341
5342        let client2 = router.dial(&addr, mbox.actor_addr()).unwrap();
5343        assert!(
5344            Arc::ptr_eq(&client1, &client2),
5345            "router must not evict a client closed for a non-stale reason"
5346        );
5347        let client3 = router.dial(&addr, mbox.actor_addr()).unwrap();
5348        assert!(Arc::ptr_eq(&client1, &client3));
5349    }
5350
5351    #[tokio::test]
5352    async fn test_enqueue_port() {
5353        let proc = Proc::isolated();
5354        let client = proc.client("client");
5355
5356        let count = Arc::new(AtomicUsize::new(0));
5357        let count_clone = count.clone();
5358        let port = client.mailbox().open_enqueue_port(move |_, n| {
5359            count_clone.fetch_add(n, Ordering::SeqCst);
5360            Ok(())
5361        });
5362
5363        port.post(&client, 10);
5364        port.post(&client, 5);
5365        port.post(&client, 1);
5366        port.post(&client, 0);
5367
5368        assert_eq!(count.load(Ordering::SeqCst), 16);
5369    }
5370
5371    #[derive(Clone, Debug, Serialize, Deserialize, typeuri::Named)]
5372    struct TestMessage;
5373
5374    #[derive(Clone, Debug, Serialize, Deserialize, typeuri::Named)]
5375    #[named(name = "some::custom::path")]
5376    struct TestMessage2;
5377
5378    #[test]
5379    fn test_remote_message_macros() {
5380        assert_eq!(
5381            TestMessage::typename(),
5382            "hyperactor::mailbox::tests::TestMessage"
5383        );
5384        assert_eq!(TestMessage2::typename(), "some::custom::path");
5385    }
5386
5387    #[test]
5388    fn test_message_envelope_display() {
5389        #[derive(typeuri::Named, Serialize, Deserialize)]
5390        struct MyTest {
5391            a: u64,
5392            b: String,
5393        }
5394        wirevalue::register_type!(MyTest);
5395
5396        let envelope = MessageEnvelope::serialize(
5397            test_actor_id("source_0", "actor"),
5398            test_port_id("dest_1", "actor", 123),
5399            &MyTest {
5400                a: 123,
5401                b: "hello".into(),
5402            },
5403            Flattrs::new(),
5404        )
5405        .unwrap();
5406
5407        // Note: display format changed from "source[0].actor" to direct format
5408        assert!(format!("{}", envelope).contains("MyTest{\"a\":123,\"b\":\"hello\"}"));
5409    }
5410
5411    #[derive(Debug, Default)]
5412    struct Foo;
5413
5414    impl Actor for Foo {}
5415
5416    // Test that a message delivery failure causes the sending actor
5417    // to stop running.
5418    #[tokio::test]
5419    async fn test_actor_delivery_failure() {
5420        // This test involves making an actor fail and so we must set
5421        // a supervision coordinator.
5422        use crate::actor::ActorStatus;
5423        use crate::testing::proc_supervison::ProcSupervisionCoordinator;
5424
5425        let proc_forwarder = BoxedMailboxSender::new(DialMailboxRouter::new_with_default(
5426            BoxedMailboxSender::new(PanickingMailboxSender),
5427        ));
5428        let proc_id = test_proc_id("quux_0");
5429        let mut proc = Proc::configured(proc_id.clone(), proc_forwarder);
5430        let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
5431        let client = proc.client("client");
5432
5433        let foo = proc.spawn(Foo);
5434        let return_handle = foo.port::<Undeliverable<MessageEnvelope>>();
5435        let message = MessageEnvelope::new(
5436            foo.actor_addr().clone(),
5437            test_port_id("corge_0", "bar", 9999),
5438            wirevalue::Any::serialize(&1u64).unwrap(),
5439            Flattrs::new(),
5440        );
5441        return_handle.post(&client, Undeliverable::Returned(message));
5442
5443        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
5444
5445        let foo_status = foo.status();
5446        assert!(matches!(*foo_status.borrow(), ActorStatus::Failed(_)));
5447        let ActorStatus::Failed(ref msg) = *foo_status.borrow() else {
5448            unreachable!()
5449        };
5450        let msg_str = msg.to_string();
5451        // UE-3 (top-line shape): with no operation context, the top
5452        // line names the destination — `undeliverable message to
5453        // {dest}`. The retired neutral `undeliverable message error`
5454        // wording is no longer emitted.
5455        assert!(
5456            msg_str.contains("undeliverable message to"),
5457            "expected destination-named top line, got:\n{msg_str}"
5458        );
5459        assert!(
5460            !msg_str.contains("undeliverable message error"),
5461            "retired neutral fallback must not appear, got:\n{msg_str}"
5462        );
5463        assert!(msg_str.contains("sender:") && msg_str.contains("quux_0"));
5464        assert!(msg_str.contains("dest:") && msg_str.contains("corge_0"));
5465
5466        proc.destroy_and_wait(tokio::time::Duration::from_secs(1), "test cleanup")
5467            .await
5468            .unwrap();
5469    }
5470
5471    #[tokio::test]
5472    async fn test_detached_return_handle() {
5473        let (return_handle, mut return_receiver) =
5474            crate::mailbox::undeliverable::new_undeliverable_port();
5475        // Simulate an undelivered message return.
5476        let envelope = MessageEnvelope::new(
5477            test_actor_id("foo_0", "bar"),
5478            test_port_id("baz_0", "corge", 9999),
5479            wirevalue::Any::serialize(&1u64).unwrap(),
5480            Flattrs::new(),
5481        );
5482        let proc = Proc::isolated();
5483        let client = proc.client("client");
5484        return_handle.post(&client, Undeliverable::Returned(envelope.clone()));
5485        // Check we receive the undelivered message.
5486        assert!(
5487            tokio::time::timeout(tokio::time::Duration::from_secs(1), return_receiver.recv())
5488                .await
5489                .is_ok()
5490        );
5491        // Setup a monitor for the receiver and show that if there are
5492        // no outstanding return handles it terminates.
5493        let monitor_handle = tokio::spawn(async move {
5494            while let Ok(Undeliverable::Returned(mut envelope)) = return_receiver.recv().await {
5495                envelope.push_delivery_failure(DeliveryFailure::new(
5496                    UndeliverableReason::Transport(TransportFailure::new(
5497                        envelope.dest().clone(),
5498                        TransportFailureReason::LinkUnavailable(
5499                            "returned in unit test".to_string(),
5500                        ),
5501                    )),
5502                ));
5503                UndeliverableMailboxSender
5504                    .post(envelope, /*unused */ monitored_return_handle());
5505            }
5506        });
5507        drop(return_handle);
5508        assert!(
5509            tokio::time::timeout(tokio::time::Duration::from_secs(1), monitor_handle)
5510                .await
5511                .is_ok()
5512        );
5513    }
5514
5515    async fn verify_receiver(coalesce: bool, drop_sender: bool) {
5516        fn create_receiver<M>(
5517            coalesce: bool,
5518        ) -> (mpsc::UnboundedSender<SequencedEnvelope<M>>, PortReceiver<M>) {
5519            // Create dummy state and port_id to create PortReceiver. They are
5520            // not used in the test.
5521            let dummy_actor_ref: ActorAddr = test_actor_id("world_0", "actor");
5522            let dummy_state = State::new(dummy_actor_ref.clone());
5523            let dummy_port_id = dummy_actor_ref.port_addr(Port::from(0));
5524            let (sender, receiver) = sequenced_unbounded::<SequencedEnvelope<M>>();
5525            let receiver = PortReceiver::new(
5526                receiver,
5527                dummy_port_id,
5528                coalesce,
5529                Mailbox {
5530                    inner: Arc::new(dummy_state),
5531                },
5532            );
5533            (sender, receiver)
5534        }
5535
5536        fn send_direct<M>(sender: &mpsc::UnboundedSender<SequencedEnvelope<M>>, message: M) {
5537            sender
5538                .send(SequencedEnvelope::new(SeqInfo::Direct, None, message))
5539                .unwrap();
5540        }
5541
5542        // verify fn drain
5543        {
5544            let (sender, mut receiver) = create_receiver::<u64>(coalesce);
5545            assert!(receiver.drain().is_empty());
5546
5547            send_direct(&sender, 0);
5548            send_direct(&sender, 1);
5549            send_direct(&sender, 2);
5550            send_direct(&sender, 3);
5551            send_direct(&sender, 4);
5552            send_direct(&sender, 5);
5553            send_direct(&sender, 6);
5554            send_direct(&sender, 7);
5555
5556            if drop_sender {
5557                drop(sender);
5558            }
5559
5560            if !coalesce {
5561                assert_eq!(receiver.drain(), vec![0, 1, 2, 3, 4, 5, 6, 7]);
5562            } else {
5563                assert_eq!(receiver.drain(), vec![7]);
5564            }
5565
5566            assert!(receiver.drain().is_empty());
5567            assert!(receiver.drain().is_empty());
5568        }
5569
5570        // verify fn try_recv
5571        {
5572            let (sender, mut receiver) = create_receiver::<u64>(coalesce);
5573            assert!(receiver.try_recv().unwrap().is_none());
5574
5575            send_direct(&sender, 0);
5576            send_direct(&sender, 1);
5577            send_direct(&sender, 2);
5578            send_direct(&sender, 3);
5579
5580            if drop_sender {
5581                drop(sender);
5582            }
5583
5584            if !coalesce {
5585                assert_eq!(receiver.try_recv().unwrap().unwrap(), 0);
5586                assert_eq!(receiver.try_recv().unwrap().unwrap(), 1);
5587                assert_eq!(receiver.try_recv().unwrap().unwrap(), 2);
5588            }
5589            assert_eq!(receiver.try_recv().unwrap().unwrap(), 3);
5590            if drop_sender {
5591                assert_matches!(
5592                    receiver.try_recv().unwrap_err().kind(),
5593                    MailboxErrorKind::Closed
5594                );
5595                // Still Closed error
5596                assert_matches!(
5597                    receiver.try_recv().unwrap_err().kind(),
5598                    MailboxErrorKind::Closed
5599                );
5600            } else {
5601                assert!(receiver.try_recv().unwrap().is_none());
5602                // Still empty
5603                assert!(receiver.try_recv().unwrap().is_none());
5604            }
5605        }
5606        // verify fn recv
5607        {
5608            let (sender, mut receiver) = create_receiver::<u64>(coalesce);
5609            assert!(
5610                tokio::time::timeout(tokio::time::Duration::from_secs(1), receiver.recv())
5611                    .await
5612                    .is_err()
5613            );
5614
5615            send_direct(&sender, 4);
5616            send_direct(&sender, 5);
5617            send_direct(&sender, 6);
5618            send_direct(&sender, 7);
5619
5620            if drop_sender {
5621                drop(sender);
5622            }
5623
5624            if !coalesce {
5625                assert_eq!(receiver.recv().await.unwrap(), 4);
5626                assert_eq!(receiver.recv().await.unwrap(), 5);
5627                assert_eq!(receiver.recv().await.unwrap(), 6);
5628            }
5629            assert_eq!(receiver.recv().await.unwrap(), 7);
5630            if drop_sender {
5631                assert_matches!(
5632                    receiver.recv().await.unwrap_err().kind(),
5633                    MailboxErrorKind::Closed
5634                );
5635                // Still None
5636                assert_matches!(
5637                    receiver.recv().await.unwrap_err().kind(),
5638                    MailboxErrorKind::Closed
5639                );
5640            } else {
5641                assert!(
5642                    tokio::time::timeout(tokio::time::Duration::from_secs(1), receiver.recv())
5643                        .await
5644                        .is_err()
5645                );
5646            }
5647        }
5648    }
5649
5650    #[tokio::test]
5651    async fn test_receiver_basic_default() {
5652        verify_receiver(/*coalesce=*/ false, /*drop_sender=*/ false).await
5653    }
5654
5655    #[tokio::test]
5656    async fn test_receiver_basic_latest() {
5657        verify_receiver(/*coalesce=*/ true, /*drop_sender=*/ false).await
5658    }
5659
5660    #[tokio::test]
5661    async fn test_receiver_after_sender_drop_default() {
5662        verify_receiver(/*coalesce=*/ false, /*drop_sender=*/ true).await
5663    }
5664
5665    #[tokio::test]
5666    async fn test_receiver_after_sender_drop_latest() {
5667        verify_receiver(/*coalesce=*/ true, /*drop_sender=*/ true).await
5668    }
5669
5670    struct Setup {
5671        receiver: PortReceiver<u64>,
5672        actor0: crate::Client,
5673        actor1: crate::Client,
5674        port_id: PortAddr,
5675        port_id1: PortAddr,
5676        port_id2: PortAddr,
5677        port_id2_1: PortAddr,
5678    }
5679
5680    async fn setup_split_port_ids(
5681        reducer_spec: Option<ReducerSpec>,
5682        reducer_mode: ReducerMode,
5683    ) -> Setup {
5684        let proc = Proc::isolated();
5685        let actor0 = proc.client("actor0");
5686        let actor1 = proc.client("actor1");
5687
5688        // Open a port on actor0
5689        let (port_handle, receiver) = actor0.open_port::<u64>();
5690        let port_id = port_handle.bind().port_addr().clone();
5691
5692        // Split it twice on actor1
5693        let port_id1 = port_id
5694            .split(&actor1, reducer_spec.clone(), reducer_mode.clone(), true)
5695            .unwrap();
5696        let port_id2 = port_id
5697            .split(&actor1, reducer_spec.clone(), reducer_mode.clone(), true)
5698            .unwrap();
5699
5700        // A split port id can also be split
5701        let port_id2_1 = port_id2
5702            .split(&actor1, reducer_spec, reducer_mode.clone(), true)
5703            .unwrap();
5704
5705        Setup {
5706            receiver,
5707            actor0,
5708            actor1,
5709            port_id,
5710            port_id1,
5711            port_id2,
5712            port_id2_1,
5713        }
5714    }
5715
5716    fn post(cx: &impl context::Actor, port_id: PortAddr, msg: u64) {
5717        let serialized = wirevalue::Any::serialize(&msg).unwrap();
5718        port_id.send(cx, serialized);
5719    }
5720
5721    #[async_timed_test(timeout_secs = 30)]
5722    // TODO: OSS: this test is flaky in OSS. Need to repo and fix it.
5723    #[cfg_attr(not(fbcode_build), ignore)]
5724    async fn test_split_port_id_no_reducer() {
5725        let Setup {
5726            mut receiver,
5727            actor0,
5728            actor1,
5729            port_id,
5730            port_id1,
5731            port_id2,
5732            port_id2_1,
5733            ..
5734        } = setup_split_port_ids(None, ReducerMode::default()).await;
5735        // Can send messages to receiver from all port handles
5736        post(&actor0, port_id.clone(), 1);
5737        assert_eq!(receiver.recv().await.unwrap(), 1);
5738        post(&actor1, port_id1.clone(), 2);
5739        assert_eq!(receiver.recv().await.unwrap(), 2);
5740        post(&actor1, port_id2.clone(), 3);
5741        assert_eq!(receiver.recv().await.unwrap(), 3);
5742        post(&actor1, port_id2_1.clone(), 4);
5743        assert_eq!(receiver.recv().await.unwrap(), 4);
5744
5745        // no more messages
5746        tokio::time::sleep(Duration::from_secs(2)).await;
5747        let msg = receiver.try_recv().unwrap();
5748        assert_eq!(msg, None);
5749    }
5750
5751    async fn wait_for(
5752        receiver: &mut PortReceiver<u64>,
5753        expected_size: usize,
5754        timeout_duration: Duration,
5755    ) -> anyhow::Result<Vec<u64>> {
5756        let mut messeges = vec![];
5757
5758        tokio::time::timeout(timeout_duration, async {
5759            loop {
5760                let msg = receiver.recv().await.unwrap();
5761                messeges.push(msg);
5762                if messeges.len() == expected_size {
5763                    break;
5764                }
5765            }
5766        })
5767        .await?;
5768        Ok(messeges)
5769    }
5770
5771    #[async_timed_test(timeout_secs = 30)]
5772    async fn test_split_port_id_sum_reducer() {
5773        let config = hyperactor_config::global::lock();
5774        let _config_guard = config.override_key(crate::config::SPLIT_MAX_BUFFER_SIZE, 1);
5775
5776        let sum_accumulator = accum::sum::<u64>();
5777        let reducer_spec = sum_accumulator.reducer_spec();
5778        let Setup {
5779            mut receiver,
5780            actor0,
5781            actor1,
5782            port_id,
5783            port_id1,
5784            port_id2,
5785            port_id2_1,
5786            ..
5787        } = setup_split_port_ids(reducer_spec, ReducerMode::default()).await;
5788        post(&actor0, port_id.clone(), 4);
5789        post(&actor1, port_id1.clone(), 2);
5790        post(&actor1, port_id2.clone(), 3);
5791        post(&actor1, port_id2_1.clone(), 1);
5792        let mut messages = wait_for(&mut receiver, 4, Duration::from_secs(2))
5793            .await
5794            .unwrap();
5795        // Message might be received out of their sending out. So we sort the
5796        // messages here.
5797        messages.sort();
5798        assert_eq!(messages, vec![1, 2, 3, 4]);
5799
5800        // no more messages
5801        tokio::time::sleep(Duration::from_secs(2)).await;
5802        let msg = receiver.try_recv().unwrap();
5803        assert_eq!(msg, None);
5804    }
5805
5806    #[async_timed_test(timeout_secs = 30)]
5807    // TODO: OSS: this test is flaky in OSS. Need to repo and fix it.
5808    #[cfg_attr(not(fbcode_build), ignore)]
5809    async fn test_split_port_id_every_n_messages() {
5810        let config = hyperactor_config::global::lock();
5811        let _config_guard =
5812            config.override_key(crate::config::SPLIT_MAX_BUFFER_AGE, Duration::from_mins(10));
5813        let proc = Proc::isolated();
5814        let actor = proc.client("actor");
5815        let (port_handle, mut receiver) = actor.open_port::<u64>();
5816        let port_id = port_handle.bind().port_addr().clone();
5817        // Split it
5818        let reducer_spec = accum::sum::<u64>().reducer_spec();
5819        let split_port_id = port_id
5820            .split(
5821                &actor,
5822                reducer_spec,
5823                ReducerMode::Streaming(accum::StreamingReducerOpts {
5824                    max_update_interval: Some(Duration::from_mins(10)),
5825                    initial_update_interval: Some(Duration::from_mins(10)),
5826                }),
5827                true,
5828            )
5829            .unwrap();
5830
5831        // Send 9 messages.
5832        for msg in [1, 5, 3, 4, 2, 91, 92, 93, 94] {
5833            post(&actor, split_port_id.clone(), msg);
5834        }
5835        // The first 5 should be batched and reduced once due
5836        // to every_n_msgs = 5.
5837        let messages = wait_for(&mut receiver, 1, Duration::from_secs(2))
5838            .await
5839            .unwrap();
5840        assert_eq!(messages, vec![15]);
5841
5842        // the last message unfortranately will never come because they do not
5843        // reach batch size.
5844        tokio::time::sleep(Duration::from_secs(2)).await;
5845        let msg = receiver.try_recv().unwrap();
5846        assert_eq!(msg, None);
5847    }
5848
5849    #[async_timed_test(timeout_secs = 30)]
5850    async fn test_split_port_timeout_flush() {
5851        let config = hyperactor_config::global::lock();
5852        let _config_guard = config.override_key(crate::config::SPLIT_MAX_BUFFER_SIZE, 100);
5853
5854        let Setup {
5855            mut receiver,
5856            actor0: _actor0,
5857            actor1,
5858            port_id: _,
5859            port_id1,
5860            ..
5861        } = setup_split_port_ids(
5862            Some(accum::sum::<u64>().reducer_spec().unwrap()),
5863            ReducerMode::Streaming(accum::StreamingReducerOpts {
5864                max_update_interval: Some(Duration::from_millis(50)),
5865                initial_update_interval: Some(Duration::from_millis(50)),
5866            }),
5867        )
5868        .await;
5869
5870        post(&actor1, port_id1.clone(), 10);
5871        post(&actor1, port_id1.clone(), 20);
5872        post(&actor1, port_id1.clone(), 30);
5873
5874        // Messages should accumulate for 50ms.
5875        tokio::time::sleep(Duration::from_millis(10)).await;
5876        let msg = receiver.try_recv().unwrap();
5877        assert_eq!(msg, None);
5878
5879        // Wait until we are flushed.
5880        tokio::time::sleep(Duration::from_millis(100)).await;
5881
5882        // Now we are reduced and accumulated:
5883        let msg = receiver.recv().await.unwrap();
5884        assert_eq!(msg, 60); // 10 + 20 + 30
5885
5886        // No further messages:
5887        let msg = receiver.try_recv().unwrap();
5888        assert_eq!(msg, None);
5889    }
5890
5891    #[async_timed_test(timeout_secs = 30)]
5892    async fn test_split_port_timeout_and_size_flush() {
5893        let config = hyperactor_config::global::lock();
5894        let _config_guard = config.override_key(crate::config::SPLIT_MAX_BUFFER_SIZE, 3);
5895
5896        let Setup {
5897            mut receiver,
5898            actor0: _actor0,
5899            actor1,
5900            port_id: _,
5901            port_id1,
5902            ..
5903        } = setup_split_port_ids(
5904            Some(accum::sum::<u64>().reducer_spec().unwrap()),
5905            ReducerMode::Streaming(accum::StreamingReducerOpts {
5906                max_update_interval: Some(Duration::from_millis(50)),
5907                initial_update_interval: Some(Duration::from_millis(50)),
5908            }),
5909        )
5910        .await;
5911
5912        post(&actor1, port_id1.clone(), 10);
5913        post(&actor1, port_id1.clone(), 20);
5914        post(&actor1, port_id1.clone(), 30);
5915        post(&actor1, port_id1.clone(), 40);
5916
5917        // Should have flushed at the third message.
5918        let msg = receiver.recv().await.unwrap();
5919        assert_eq!(msg, 60);
5920
5921        // After 50ms, the next reduce will flush:
5922        let msg = receiver.recv().await.unwrap();
5923        assert_eq!(msg, 40);
5924
5925        // No further messages
5926        let msg = receiver.try_recv().unwrap();
5927        assert_eq!(msg, None);
5928    }
5929
5930    #[async_timed_test(timeout_secs = 30)]
5931    async fn test_split_port_idle_flush_complete() {
5932        let proc = Proc::isolated();
5933        let actor = proc.client("actor");
5934
5935        let (port_handle, mut receiver) = actor.open_port::<u64>();
5936
5937        let port_id = port_handle.bind().port_addr().clone();
5938
5939        let split_port_id = port_id
5940            .split(
5941                &actor,
5942                accum::sum::<u64>().reducer_spec(),
5943                ReducerMode::IdleFlush {
5944                    expected: NonZeroUsize::new(3).expect("3 is non-zero"),
5945                    idle_timeout: Duration::from_secs(10),
5946                    abandon_timeout: Duration::from_secs(30),
5947                },
5948                true,
5949            )
5950            .unwrap();
5951
5952        post(&actor, split_port_id.clone(), 10);
5953        post(&actor, split_port_id.clone(), 20);
5954
5955        assert_eq!(
5956            receiver.try_recv().unwrap(),
5957            None,
5958            "the reducer should not emit before receiving all expected updates"
5959        );
5960
5961        post(&actor, split_port_id, 30);
5962
5963        let reduced = tokio::time::timeout(Duration::from_secs(2), receiver.recv())
5964            .await
5965            .expect("complete idle-flush result should arrive")
5966            .unwrap();
5967
5968        assert_eq!(reduced, 60, "10 + 20 + 30 should reduce to 60");
5969    }
5970
5971    #[async_timed_test(timeout_secs = 30)]
5972    async fn test_split_port_idle_flush_partial() {
5973        let proc = Proc::isolated();
5974        let actor = proc.client("actor");
5975        let (port_handle, mut receiver) = actor.open_port::<u64>();
5976        let port_id = port_handle.bind().port_addr().clone();
5977        let split_port_id = port_id
5978            .split(
5979                &actor,
5980                accum::sum::<u64>().reducer_spec(),
5981                ReducerMode::IdleFlush {
5982                    expected: NonZeroUsize::new(3).expect("3 is non-zero"),
5983                    idle_timeout: Duration::from_millis(50),
5984                    abandon_timeout: Duration::from_secs(30),
5985                },
5986                true,
5987            )
5988            .unwrap();
5989
5990        post(&actor, split_port_id.clone(), 10);
5991        post(&actor, split_port_id, 20);
5992
5993        let reduced = tokio::time::timeout(Duration::from_secs(2), receiver.recv())
5994            .await
5995            .expect("partial idle-flush result should arrive after the idle interval")
5996            .unwrap();
5997        assert_eq!(reduced, 30, "10 + 20 should reduce to the partial value 30");
5998    }
5999
6000    #[tokio::test(start_paused = true)]
6001    async fn test_split_port_idle_flush_restarts_idle_timeout() {
6002        let proc = Proc::isolated();
6003        let actor = proc.client("actor");
6004        let (port_handle, mut receiver) = actor.open_port::<u64>();
6005        let port_id = port_handle.bind().port_addr().clone();
6006        let split_port_id = port_id
6007            .split(
6008                &actor,
6009                accum::sum::<u64>().reducer_spec(),
6010                ReducerMode::IdleFlush {
6011                    expected: NonZeroUsize::new(6).expect("6 is non-zero"),
6012                    idle_timeout: Duration::from_millis(50),
6013                    abandon_timeout: Duration::from_secs(30),
6014                },
6015                true,
6016            )
6017            .unwrap();
6018
6019        post(&actor, split_port_id.clone(), 10);
6020        tokio::task::yield_now().await;
6021
6022        for update in [20, 30, 40, 50] {
6023            tokio::time::advance(Duration::from_millis(25)).await;
6024            post(&actor, split_port_id.clone(), update);
6025            tokio::task::yield_now().await;
6026        }
6027
6028        assert_eq!(
6029            receiver.try_recv().unwrap(),
6030            None,
6031            "the reducer should not flush while replies arrive within the idle timeout"
6032        );
6033
6034        tokio::time::advance(Duration::from_millis(49)).await;
6035        tokio::task::yield_now().await;
6036        assert_eq!(
6037            receiver.try_recv().unwrap(),
6038            None,
6039            "the final reply should restart the full idle timeout"
6040        );
6041
6042        tokio::time::advance(Duration::from_millis(11)).await;
6043        let reduced = receiver.recv().await.unwrap();
6044        assert_eq!(reduced, 150, "10 + 20 + 30 + 40 + 50 should reduce to 150");
6045    }
6046
6047    #[tokio::test(start_paused = true)]
6048    async fn test_split_port_idle_flush_abandons_incomplete_port() {
6049        let proc = Proc::isolated();
6050        let actor = proc.client("actor");
6051        let (port_handle, mut receiver) = actor.open_port::<u64>();
6052        let port_id = port_handle.bind().port_addr().clone();
6053        let split_port_id = port_id
6054            .split(
6055                &actor,
6056                accum::sum::<u64>().reducer_spec(),
6057                ReducerMode::IdleFlush {
6058                    expected: NonZeroUsize::new(2).expect("2 is non-zero"),
6059                    idle_timeout: Duration::from_millis(50),
6060                    abandon_timeout: Duration::from_millis(200),
6061                },
6062                false,
6063            )
6064            .unwrap();
6065
6066        assert!(
6067            actor
6068                .mailbox()
6069                .inner
6070                .ports
6071                .contains_key(&split_port_id.port()),
6072            "the split port should initially be bound"
6073        );
6074
6075        post(&actor, split_port_id.clone(), 10);
6076        tokio::task::yield_now().await;
6077        tokio::time::advance(Duration::from_millis(50)).await;
6078        assert_eq!(receiver.recv().await.unwrap(), 10);
6079
6080        tokio::time::advance(Duration::from_millis(149)).await;
6081        tokio::task::yield_now().await;
6082        assert!(
6083            actor
6084                .mailbox()
6085                .inner
6086                .ports
6087                .contains_key(&split_port_id.port()),
6088            "the incomplete split port should remain bound before abandonment"
6089        );
6090
6091        tokio::time::advance(Duration::from_millis(2)).await;
6092        tokio::task::yield_now().await;
6093        assert!(
6094            !actor
6095                .mailbox()
6096                .inner
6097                .ports
6098                .contains_key(&split_port_id.port()),
6099            "the incomplete split port should be removed after abandonment"
6100        );
6101    }
6102
6103    #[async_timed_test(timeout_secs = 30)]
6104    async fn test_split_port_once_mode_basic() {
6105        let proc = Proc::isolated();
6106        let actor = proc.client("actor");
6107        let (port_handle, mut receiver) = actor.open_port::<u64>();
6108        let port_id = port_handle.bind().port_addr().clone();
6109
6110        // Split with Once(3) mode - accumulate 3 values then emit
6111        let reducer_spec = accum::sum::<u64>().reducer_spec();
6112        let split_port_id = port_id
6113            .split(&actor, reducer_spec, ReducerMode::Once(3), true)
6114            .unwrap();
6115
6116        // Send 3 messages
6117        post(&actor, split_port_id.clone(), 10);
6118        post(&actor, split_port_id.clone(), 20);
6119        post(&actor, split_port_id.clone(), 30);
6120
6121        // Should receive a single reduced message
6122        let msg = receiver.recv().await.unwrap();
6123        assert_eq!(msg, 60); // 10 + 20 + 30
6124
6125        // No further messages
6126        tokio::time::sleep(Duration::from_millis(100)).await;
6127        let msg = receiver.try_recv().unwrap();
6128        assert_eq!(msg, None);
6129    }
6130
6131    #[derive(Debug)]
6132    #[hyperactor::export(handlers = [u64])]
6133    struct SplitPortReceivingActor {
6134        received: PortRef<String>,
6135    }
6136
6137    impl Actor for SplitPortReceivingActor {}
6138
6139    #[async_trait]
6140    impl Handler<u64> for SplitPortReceivingActor {
6141        async fn handle(&mut self, cx: &crate::Context<Self>, msg: u64) -> anyhow::Result<()> {
6142            let endpoint = cx
6143                .headers()
6144                .get(headers::OPERATION_ENDPOINT)
6145                .unwrap_or_default();
6146            self.received
6147                .post(cx, format!("OPERATION_ENDPOINT={endpoint} sum={msg}"));
6148            Ok(())
6149        }
6150    }
6151
6152    #[async_timed_test(timeout_secs = 30)]
6153    async fn test_split_port_preserves_operation_context_headers() {
6154        let proc = Proc::isolated();
6155        let client = proc.client("client");
6156        let (received_handle, mut observed_rx) = client.open_port::<String>();
6157        let capture_handle = proc.spawn_with_label(
6158            "split_port_receiver",
6159            SplitPortReceivingActor {
6160                received: received_handle.bind(),
6161            },
6162        );
6163        let capture_ref: ActorRef<SplitPortReceivingActor> = capture_handle.bind();
6164        let port_id = capture_ref.port::<u64>().port_addr().clone();
6165
6166        let split_port_id = port_id
6167            .split(
6168                &client,
6169                // Accumulate 2 messages, sum the values, and send them
6170                accum::sum::<u64>().reducer_spec(),
6171                ReducerMode::Once(2),
6172                true,
6173            )
6174            .unwrap();
6175
6176        let mut headers = Flattrs::new();
6177        headers.set(headers::OPERATION_ENDPOINT, "endpoint.call()".to_string());
6178        client.post(
6179            split_port_id.clone(),
6180            headers.clone(),
6181            // Send "1"
6182            wirevalue::Any::serialize(&1u64).unwrap(),
6183            true,
6184            crate::context::SeqInfoPolicy::AssignNew,
6185        );
6186        client.post(
6187            split_port_id,
6188            headers,
6189            // Send "2"
6190            wirevalue::Any::serialize(&2u64).unwrap(),
6191            true,
6192            crate::context::SeqInfoPolicy::AssignNew,
6193        );
6194
6195        assert_eq!(
6196            observed_rx.recv().await.unwrap(),
6197            "OPERATION_ENDPOINT=endpoint.call() sum=3"
6198        );
6199    }
6200
6201    #[async_timed_test(timeout_secs = 30)]
6202    async fn test_split_port_once_mode_teardown() {
6203        let proc = Proc::isolated();
6204        let actor = proc.client("actor");
6205        let (port_handle, mut receiver) = actor.open_port::<u64>();
6206        let port_id = port_handle.bind().port_addr().clone();
6207
6208        // Set up an undeliverable receiver to capture messages sent to torn-down ports
6209        let (undeliverable_handle, mut undeliverable_receiver) =
6210            undeliverable::new_undeliverable_port();
6211
6212        // Split with Once(3) mode - accumulate 3 values then emit and tear down
6213        let reducer_spec = accum::sum::<u64>().reducer_spec();
6214        let split_port_id = port_id
6215            .split(&actor, reducer_spec, ReducerMode::Once(3), true)
6216            .unwrap();
6217
6218        // Send 3 messages to trigger reduction
6219        post(&actor, split_port_id.clone(), 10);
6220        post(&actor, split_port_id.clone(), 20);
6221        post(&actor, split_port_id.clone(), 30);
6222
6223        // Should receive a single reduced message
6224        let msg = receiver.recv().await.unwrap();
6225        assert_eq!(msg, 60); // 10 + 20 + 30
6226
6227        // Now send another message - it should fail because the port is torn down
6228        let serialized = wirevalue::Any::serialize(&100u64).unwrap();
6229        let envelope = MessageEnvelope::new(
6230            actor.mailbox().actor_addr().clone(),
6231            split_port_id.clone(),
6232            serialized,
6233            Flattrs::new(),
6234        );
6235        actor.mailbox().post(envelope, undeliverable_handle);
6236
6237        // Verify the message was returned as undeliverable
6238        let undeliverable =
6239            tokio::time::timeout(Duration::from_secs(2), undeliverable_receiver.recv())
6240                .await
6241                .expect("should receive undeliverable message")
6242                .expect("undeliverable receiver closed");
6243
6244        // Verify the undeliverable message has the correct destination
6245        let split_port_ref: PortAddr = split_port_id;
6246        assert_eq!(
6247            undeliverable
6248                .into_message()
6249                .expect("expected returned envelope")
6250                .dest(),
6251            &split_port_ref
6252        );
6253
6254        // Verify no additional messages arrived at the original receiver
6255        let msg = receiver.try_recv().unwrap();
6256        assert_eq!(msg, None);
6257    }
6258
6259    #[test]
6260    fn test_dial_mailbox_router_prefixes_empty() {
6261        assert_eq!(DialMailboxRouter::new().prefixes().len(), 0);
6262    }
6263
6264    #[test]
6265    fn test_dial_mailbox_router_prefixes_single_entry() {
6266        let router = DialMailboxRouter::new();
6267        router.bind(test_proc_ref("world0"), "unix!@1".parse().unwrap());
6268
6269        let prefixes: Vec<Addr> = router.prefixes().into_iter().collect();
6270        assert_eq!(prefixes.len(), 1);
6271        assert_eq!(prefixes[0], test_proc_ref("world0"));
6272    }
6273
6274    #[test]
6275    fn test_dial_mailbox_router_prefixes_no_overlap() {
6276        let router = DialMailboxRouter::new();
6277        router.bind(test_proc_ref("world0"), "unix!@1".parse().unwrap());
6278        router.bind(test_proc_ref("world1"), "unix!@2".parse().unwrap());
6279        router.bind(test_proc_ref("world2"), "unix!@3".parse().unwrap());
6280
6281        let mut prefixes: Vec<Addr> = router.prefixes().into_iter().collect();
6282        prefixes.sort();
6283
6284        let mut expected = vec![
6285            test_proc_ref("world0"),
6286            test_proc_ref("world1"),
6287            test_proc_ref("world2"),
6288        ];
6289        expected.sort();
6290
6291        assert_eq!(prefixes, expected);
6292    }
6293
6294    #[test]
6295    fn test_dial_mailbox_router_prefixes_with_overlaps() {
6296        let router = DialMailboxRouter::new();
6297        // Proc refs are all independent since they have different names.
6298        router.bind(test_proc_ref("world0"), "unix!@1".parse().unwrap());
6299        router.bind(test_proc_ref("world0_0"), "unix!@2".parse().unwrap());
6300        router.bind(test_proc_ref("world0_1"), "unix!@3".parse().unwrap());
6301        router.bind(test_proc_ref("world1"), "unix!@4".parse().unwrap());
6302        router.bind(test_proc_ref("world1_0"), "unix!@5".parse().unwrap());
6303
6304        let mut prefixes: Vec<Addr> = router.prefixes().into_iter().collect();
6305        prefixes.sort();
6306
6307        let mut expected = vec![
6308            test_proc_ref("world0"),
6309            test_proc_ref("world0_0"),
6310            test_proc_ref("world0_1"),
6311            test_proc_ref("world1"),
6312            test_proc_ref("world1_0"),
6313        ];
6314        expected.sort();
6315
6316        assert_eq!(prefixes, expected);
6317    }
6318
6319    #[test]
6320    fn test_dial_mailbox_router_prefixes_complex_hierarchy() {
6321        let router = DialMailboxRouter::new();
6322        // Proc refs still cover their own actors (same proc_id).
6323        router.bind(test_proc_ref("world0"), "unix!@1".parse().unwrap());
6324        router.bind(test_proc_ref("world0_0"), "unix!@2".parse().unwrap());
6325        router.bind(
6326            test_actor_ref("world0_0", "actor1"),
6327            "unix!@3".parse().unwrap(),
6328        );
6329        router.bind(test_proc_ref("world1_0"), "unix!@4".parse().unwrap());
6330        router.bind(test_proc_ref("world1_1"), "unix!@5".parse().unwrap());
6331        router.bind(
6332            test_actor_ref("world2_0", "actor0"),
6333            "unix!@6".parse().unwrap(),
6334        );
6335
6336        let mut prefixes: Vec<Addr> = router.prefixes().into_iter().collect();
6337        prefixes.sort();
6338
6339        // Covering prefixes:
6340        // - world0 (independent proc ref)
6341        // - world0_0 (covers world0_0.actor1 since same proc_id)
6342        // - world1_0 (not covered by anything else)
6343        // - world1_1 (not covered by anything else)
6344        // - world2_0.actor0 (not covered by anything else)
6345        let mut expected = vec![
6346            test_proc_ref("world0"),
6347            test_proc_ref("world0_0"),
6348            test_proc_ref("world1_0"),
6349            test_proc_ref("world1_1"),
6350            test_actor_ref("world2_0", "actor0"),
6351        ];
6352        expected.sort();
6353
6354        assert_eq!(prefixes, expected);
6355    }
6356
6357    #[test]
6358    fn test_dial_mailbox_router_prefixes_same_level() {
6359        let router = DialMailboxRouter::new();
6360        router.bind(test_proc_ref("world0_0"), "unix!@1".parse().unwrap());
6361        router.bind(test_proc_ref("world0_1"), "unix!@2".parse().unwrap());
6362        router.bind(test_proc_ref("world0_2"), "unix!@3".parse().unwrap());
6363
6364        let mut prefixes: Vec<Addr> = router.prefixes().into_iter().collect();
6365        prefixes.sort();
6366
6367        // All should be covering prefixes since none is a prefix of another
6368        let mut expected = vec![
6369            test_proc_ref("world0_0"),
6370            test_proc_ref("world0_1"),
6371            test_proc_ref("world0_2"),
6372        ];
6373        expected.sort();
6374
6375        assert_eq!(prefixes, expected);
6376    }
6377
6378    /// A forwarder that bounces messages back to the **same**
6379    /// mailbox, but does so on a task to avoid recursive stack
6380    /// growth.
6381    #[derive(Clone, Debug)]
6382    struct AsyncLoopForwarder;
6383
6384    #[async_trait]
6385    impl MailboxSender for AsyncLoopForwarder {
6386        fn post_unchecked(
6387            &self,
6388            envelope: MessageEnvelope,
6389            return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
6390        ) {
6391            let me = self.clone();
6392            tokio::spawn(async move {
6393                // Call `post` so each hop applies TTL exactly once.
6394                me.post(envelope, return_handle);
6395            });
6396        }
6397    }
6398
6399    #[tokio::test]
6400    async fn message_ttl_expires_in_routing_loop_returns_to_sender() {
6401        let actor_id = test_actor_id("world_0", "ttl_actor");
6402        let (ret_port, mut ret_rx) = undeliverable::new_undeliverable_port();
6403
6404        let remote_actor = test_actor_id("remote_world_1", "remote");
6405        let dest = remote_actor.port_addr(4242.into());
6406
6407        // Build an envelope (TTL is seeded in `MessageEnvelope::new` /
6408        // `::serialize`).
6409        let payload = 1234_u64;
6410        let envelope =
6411            MessageEnvelope::serialize(actor_id.clone(), dest.clone(), &payload, Flattrs::new())
6412                .expect("serialize");
6413
6414        AsyncLoopForwarder.post(envelope, ret_port.clone());
6415
6416        // We expect the undeliverable to come back once TTL expires.
6417        let undelivered = tokio::time::timeout(Duration::from_secs(5), ret_rx.recv())
6418            .await
6419            .expect("timed out waiting for undeliverable")
6420            .expect("channel closed")
6421            .into_message()
6422            .expect("expected returned envelope");
6423
6424        // Sanity: round-trip payload still deserializes.
6425        let got: u64 = undelivered.deserialized().expect("deserialize");
6426        assert_eq!(got, payload, "payload preserved");
6427    }
6428
6429    #[tokio::test]
6430    async fn message_ttl_success_local_delivery() {
6431        let actor_id = test_actor_id("world_0", "ttl_actor");
6432        let mailbox = Mailbox::new(actor_id.clone());
6433        let (_undeliverable_tx, mut undeliverable_rx) =
6434            mailbox.bind_handler_port::<Undeliverable<MessageEnvelope>>();
6435
6436        // Open a local user u64 port.
6437        let (user_port, mut user_rx) = mailbox.open_port::<u64>();
6438
6439        // Build an envelope destined for this mailbox's own port.
6440        let payload = 0xC0FFEE_u64;
6441        let envelope = MessageEnvelope::serialize(
6442            actor_id.clone(),
6443            user_port.bind().port_addr().clone(),
6444            &payload,
6445            Flattrs::new(),
6446        )
6447        .expect("serialize");
6448
6449        // Post the message using the mailbox (local path). TTL will
6450        // not expire.
6451        let return_handle = mailbox
6452            .bound_return_handle()
6453            .unwrap_or(monitored_return_handle());
6454        mailbox.post(envelope, return_handle);
6455
6456        // We should receive the payload locally.
6457        let got = tokio::time::timeout(Duration::from_secs(1), user_rx.recv())
6458            .await
6459            .expect("timed out waiting for local delivery")
6460            .expect("user port closed");
6461        assert_eq!(got, payload);
6462
6463        // There should be no undeliverables arriving.
6464        let no_undeliverable =
6465            tokio::time::timeout(Duration::from_millis(100), undeliverable_rx.recv()).await;
6466        assert!(
6467            no_undeliverable.is_err(),
6468            "unexpected undeliverable returned on successful local delivery"
6469        );
6470    }
6471
6472    #[tokio::test]
6473    async fn test_port_contramap() {
6474        let proc = Proc::isolated();
6475        let client = proc.client("client");
6476        let (handle, mut rx) = client.open_port();
6477
6478        handle
6479            .contramap(|m| (1, m))
6480            .post(&client, "hello".to_string());
6481        assert_eq!(rx.recv().await.unwrap(), (1, "hello".to_string()));
6482    }
6483
6484    #[test]
6485    fn test_bind_open_port_uses_ephemeral_port() {
6486        let mbox = Mailbox::new(test_actor_id("0", "test"));
6487        let (handle, _rx) = mbox.open_port::<String>();
6488        let ephemeral_port = mbox
6489            .actor_addr()
6490            .port_addr(Port::from(handle.inner.bind_target.ephemeral_index()));
6491        let handler_port = mbox.actor_addr().port_addr(Port::handler::<String>());
6492
6493        let port_ref = handle.bind();
6494
6495        assert_eq!(port_ref.port_addr(), &ephemeral_port);
6496        assert_ne!(port_ref.port_addr(), &handler_port);
6497    }
6498
6499    #[test]
6500    fn test_bind_handler_port_handle_twice_is_idempotent() {
6501        let mbox = Mailbox::new(test_actor_id("0", "test"));
6502        let default_port = mbox.actor_addr().port_addr(Port::handler::<String>());
6503        let handle = mbox.open_handler_enqueue_port(|_, _message: String| Ok(()));
6504        assert_matches!(handle.inner.bind_target, PortBindTarget::Handler);
6505
6506        let first = handle.bind();
6507        let second = handle.bind();
6508
6509        assert_eq!(first.port_addr(), &default_port);
6510        assert_eq!(second.port_addr(), first.port_addr());
6511        assert_matches!(handle.location(), PortLocation::Bound(port) if port == default_port);
6512    }
6513
6514    #[test]
6515    fn test_bind_handler_port_helper_returns_handler_bound_handle() {
6516        let mbox = Mailbox::new(test_actor_id("0", "test"));
6517        let default_port = mbox.actor_addr().port_addr(Port::handler::<String>());
6518        let (handle, _rx) = mbox.bind_handler_port::<String>();
6519        assert_matches!(handle.inner.bind_target, PortBindTarget::Handler);
6520
6521        let port_ref = handle.bind();
6522
6523        assert_eq!(port_ref.port_addr(), &default_port);
6524        assert_matches!(handle.location(), PortLocation::Bound(port) if port == default_port);
6525    }
6526
6527    #[test]
6528    #[should_panic(expected = "already bound")]
6529    fn test_bind_port_handle_to_handler_port_when_already_bound() {
6530        let mbox = Mailbox::new(test_actor_id("0", "test"));
6531        let (handle, _rx) = mbox.open_port::<String>();
6532        // Bound handle to the port allocated by mailbox.
6533        handle.bind();
6534        assert_matches!(handle.location(), PortLocation::Bound(port) if port.index() == handle.inner.bind_target.ephemeral_index());
6535        // Rebinding the same handle to a different port should panic.
6536        handle.bind_handler_port();
6537    }
6538
6539    #[tokio::test]
6540    async fn test_mailbox_post_fails_when_actor_stopped() {
6541        let actor_id = test_actor_id("0", "stopped_actor");
6542
6543        let mailbox = Mailbox::new(actor_id.clone());
6544
6545        mailbox.close(ActorStatus::Stopped("test stop".to_string()));
6546
6547        let (user_port, _user_rx) = mailbox.open_port::<u64>();
6548
6549        // Use a separate return mailbox since
6550        // the main mailbox is stopped and won't accept messages.
6551        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
6552
6553        let envelope = MessageEnvelope::serialize(
6554            actor_id.clone(),
6555            user_port.bind().port_addr().clone(),
6556            &42u64,
6557            Flattrs::new(),
6558        )
6559        .expect("serialize");
6560
6561        mailbox.post(envelope, return_handle);
6562
6563        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
6564            .await
6565            .expect("timed out waiting for undeliverable")
6566            .expect("return port closed")
6567            .into_message()
6568            .expect("expected returned envelope");
6569
6570        let err = undelivered.error_msg().expect("expected error");
6571        assert!(
6572            err.contains("actor stopped"),
6573            "error should indicate actor stopped: {}",
6574            err
6575        );
6576        let root_failure = undelivered
6577            .root_delivery_failure()
6578            .expect("expected root delivery failure");
6579        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
6580            panic!("expected invalid reference, got {root_failure}");
6581        };
6582        assert_eq!(
6583            invalid_reference.reason,
6584            InvalidReferenceReason::ActorStopped
6585        );
6586    }
6587
6588    #[tokio::test]
6589    async fn test_mailbox_post_fails_when_actor_failed() {
6590        use crate::actor::ActorErrorKind;
6591
6592        let actor_id = test_actor_id("0", "failed_actor");
6593
6594        let mailbox = Mailbox::new(actor_id.clone());
6595
6596        let (user_port, _user_rx) = mailbox.open_port::<u64>();
6597
6598        mailbox.close(ActorStatus::Failed(ActorErrorKind::Generic(
6599            "test failure".to_string(),
6600        )));
6601
6602        // Use a separate return mailbox since
6603        // the main mailbox is failed and won't accept messages.
6604        let (return_handle, mut return_rx) = undeliverable::new_undeliverable_port();
6605
6606        let envelope = MessageEnvelope::serialize(
6607            actor_id.clone(),
6608            user_port.bind().port_addr().clone(),
6609            &42u64,
6610            Flattrs::new(),
6611        )
6612        .expect("serialize");
6613
6614        mailbox.post(envelope, return_handle);
6615
6616        let undelivered = tokio::time::timeout(Duration::from_secs(1), return_rx.recv())
6617            .await
6618            .expect("timed out waiting for undeliverable")
6619            .expect("return port closed")
6620            .into_message()
6621            .expect("expected returned envelope");
6622
6623        let err = undelivered.error_msg().expect("expected error");
6624        assert!(
6625            err.contains("actor failed"),
6626            "error should indicate actor failed: {}",
6627            err
6628        );
6629        let root_failure = undelivered
6630            .root_delivery_failure()
6631            .expect("expected root delivery failure");
6632        let DeliveryFailureKind::InvalidReference(invalid_reference) = &root_failure.kind else {
6633            panic!("expected invalid reference, got {root_failure}");
6634        };
6635        assert_eq!(
6636            invalid_reference.reason,
6637            InvalidReferenceReason::ActorFailed
6638        );
6639    }
6640
6641    #[tokio::test]
6642    async fn test_port_handle_send_fails_when_actor_stopped() {
6643        let actor_id = test_actor_id("0", "stopped_actor");
6644
6645        let mailbox = Mailbox::new(actor_id.clone());
6646
6647        let (port_handle, _rx) = mailbox.open_port::<u64>();
6648        let proc = Proc::isolated();
6649        let client = proc.client("client");
6650
6651        mailbox.close(ActorStatus::Stopped("test stop".to_string()));
6652
6653        let err = port_handle.try_post(&client, 42u64).unwrap_err();
6654        assert_matches!(
6655            err.kind(),
6656            MailboxSenderErrorKind::Mailbox(mailbox_err)
6657                if matches!(mailbox_err.kind(), MailboxErrorKind::OwnerTerminated(ActorStatus::Stopped(reason)) if reason == "test stop")
6658        );
6659    }
6660
6661    #[tokio::test]
6662    async fn test_port_handle_send_fails_when_actor_failed() {
6663        use crate::actor::ActorErrorKind;
6664
6665        let actor_id = test_actor_id("0", "failed_actor");
6666
6667        let mailbox = Mailbox::new(actor_id.clone());
6668
6669        let (port_handle, _rx) = mailbox.open_port::<u64>();
6670        let proc = Proc::isolated();
6671        let client = proc.client("client");
6672
6673        mailbox.close(ActorStatus::Failed(ActorErrorKind::Generic(
6674            "test failure".to_string(),
6675        )));
6676
6677        let err = port_handle.try_post(&client, 42u64).unwrap_err();
6678        assert_matches!(
6679            err.kind(),
6680            MailboxSenderErrorKind::Mailbox(mailbox_err)
6681                if matches!(mailbox_err.kind(), MailboxErrorKind::OwnerTerminated(ActorStatus::Failed(ActorErrorKind::Generic(msg))) if msg == "test failure")
6682        );
6683    }
6684
6685    #[async_timed_test(timeout_secs = 30)]
6686    async fn test_open_reduce_port() {
6687        let proc = Proc::isolated();
6688        let client = proc.client("client");
6689
6690        // Open an accumulator port with sum reducer
6691        let (port_handle, receiver) = client.mailbox().open_reduce_port(accum::sum::<u64>());
6692
6693        // Verify the reducer_spec is set
6694        let port_ref = port_handle.bind();
6695        assert!(port_ref.reducer_spec().is_some());
6696
6697        // Send a single value via the bound port
6698        port_ref.post(&client, 42);
6699
6700        // Should receive the value
6701        let result = receiver.recv().await.unwrap();
6702        assert_eq!(result, 42);
6703    }
6704
6705    #[async_timed_test(timeout_secs = 30)]
6706    async fn test_open_reduce_port_reducer_spec_preserved() {
6707        let proc = Proc::isolated();
6708        let client = proc.client("client");
6709
6710        // Test that different accumulators produce different reducer_specs
6711        let (sum_handle, _) = client.mailbox().open_reduce_port(accum::sum::<u64>());
6712        let sum_ref = sum_handle.bind();
6713        let sum_typehash = sum_ref.reducer_spec().as_ref().unwrap().typehash;
6714
6715        let (max_handle, _) = client
6716            .mailbox()
6717            .open_reduce_port(accum::join_semilattice::<accum::Max<u64>>());
6718        let max_ref = max_handle.bind();
6719        let max_typehash = max_ref.reducer_spec().as_ref().unwrap().typehash;
6720
6721        // Different accumulators should have different reducer typehashes
6722        assert_ne!(sum_typehash, max_typehash);
6723    }
6724
6725    /// Test that `MailboxClient::flush()` waits until messages are wire-acked
6726    /// over a unix domain socket channel. We send messages, flush, and then
6727    /// confirm that the messages have already been delivered to the receiving
6728    /// mailbox.
6729    #[tokio::test]
6730    async fn test_flush_over_unix_channel() {
6731        let mbox = Mailbox::new(test_actor_id("0", "actor0"));
6732
6733        // Serve the mailbox on a unix domain socket channel.
6734        let (addr, rx) = channel::serve(ChannelAddr::any(ChannelTransport::Unix)).unwrap();
6735        let serve_handle = mbox.clone().serve(rx);
6736
6737        // Dial the unix address to get a MailboxClient.
6738        let client = MailboxClient::dial(addr).unwrap();
6739
6740        // Open a streaming port so we can receive multiple messages.
6741        let (port, mut receiver) = mbox.open_port::<u64>();
6742        let port = port.bind();
6743
6744        // Send several messages without awaiting delivery.
6745        for i in 0..10u64 {
6746            client
6747                .serialize_and_send(&port, i, monitored_return_handle())
6748                .unwrap();
6749        }
6750
6751        // Flush: this should block until all 10 messages are wire-acked,
6752        // meaning they've been enqueued into the receiving mailbox.
6753        client.flush().await.unwrap();
6754
6755        // After flush, all messages should already be available.
6756        for i in 0..10u64 {
6757            let msg = receiver
6758                .try_recv()
6759                .expect("message should be available after flush")
6760                .expect("receiver should not be empty after flush");
6761            assert_eq!(msg, i);
6762        }
6763
6764        serve_handle.stop("test done");
6765        serve_handle.await.unwrap().unwrap();
6766    }
6767
6768    #[test]
6769    fn test_drain_waits_for_active_handler_enqueue() {
6770        let mailbox = Mailbox::new(test_actor_id("drain", "actor"));
6771        let (entered_tx, entered_rx) = std::sync::mpsc::channel();
6772        let release = Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new()));
6773        let delivered = Arc::new(AtomicUsize::new(0));
6774
6775        let port = mailbox.open_handler_enqueue_port({
6776            let release = Arc::clone(&release);
6777            let delivered = Arc::clone(&delivered);
6778            move |_headers, _message: u64| {
6779                entered_tx.send(()).unwrap();
6780                let (lock, cvar) = &*release;
6781                let mut released = lock.lock().unwrap();
6782                while !*released {
6783                    released = cvar.wait(released).unwrap();
6784                }
6785                delivered.fetch_add(1, Ordering::SeqCst);
6786                Ok(())
6787            }
6788        });
6789
6790        let sender = port.inner.sender.clone();
6791        let sender_thread = std::thread::spawn(move || sender.send(Flattrs::new(), 1u64).unwrap());
6792        entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
6793
6794        let (drained_tx, drained_rx) = std::sync::mpsc::channel();
6795        let drain_thread = std::thread::spawn({
6796            let mailbox = mailbox.clone();
6797            move || {
6798                mailbox.drain();
6799                drained_tx.send(()).unwrap();
6800            }
6801        });
6802
6803        let deadline = std::time::Instant::now() + Duration::from_secs(1);
6804        while mailbox.inner.handler_ingress.state.load(Ordering::Acquire) & HANDLER_INGRESS_DRAINING
6805            == 0
6806        {
6807            assert!(std::time::Instant::now() < deadline, "drain did not start");
6808            std::thread::yield_now();
6809        }
6810        assert_matches!(
6811            drained_rx.try_recv(),
6812            Err(std::sync::mpsc::TryRecvError::Empty)
6813        );
6814
6815        let (lock, cvar) = &*release;
6816        *lock.lock().unwrap() = true;
6817        cvar.notify_all();
6818
6819        sender_thread.join().unwrap();
6820        drained_rx.recv_timeout(Duration::from_secs(1)).unwrap();
6821        drain_thread.join().unwrap();
6822        assert_eq!(delivered.load(Ordering::SeqCst), 1);
6823
6824        let err = port.inner.sender.send(Flattrs::new(), 2u64).unwrap_err();
6825        assert!(err.is::<HandlerPortClosedError>());
6826    }
6827
6828    /// Helper: build a `MessageEnvelope` with a recognizable payload
6829    /// and non-empty headers, then feed it through
6830    /// `UndeliverableMailboxSender::post_unchecked`. Returns the
6831    /// sender + destination so tests can assert against the values we
6832    /// know will end up on the log.
6833    fn drive_abandonment_log(payload_sentinel: &str) -> (crate::ActorAddr, crate::PortAddr) {
6834        use hyperactor_config::declare_attrs;
6835
6836        declare_attrs! {
6837            // Any non-empty entry works; UM-1 only asserts the log
6838            // does not dump `headers` inline.
6839            attr UM_TEST_HEADER: u64;
6840        }
6841
6842        let sender = test_actor_id("um_proc", "um_sender");
6843        let dest = test_port_id("um_dest_proc", "um_dest", 42);
6844
6845        let mut headers = Flattrs::new();
6846        headers.set(UM_TEST_HEADER, 0xC0FFEEu64);
6847
6848        let envelope = MessageEnvelope::new(
6849            sender.clone(),
6850            dest.clone(),
6851            wirevalue::Any::serialize(&payload_sentinel.to_string()).unwrap(),
6852            headers,
6853        );
6854
6855        let (return_handle, _rx) = crate::mailbox::undeliverable::new_undeliverable_port();
6856        UndeliverableMailboxSender.post_unchecked(envelope, return_handle);
6857        (sender, dest)
6858    }
6859
6860    /// UM-1: the log does not render unbounded `headers` or `data`
6861    /// fields, and reports bounded `message_type` + `data_len`
6862    /// summaries instead.
6863    #[tracing_test::traced_test]
6864    #[test]
6865    fn test_um1_bounded_fields() {
6866        let payload_sentinel = "um1_payload_sentinel_5b7a9c3d";
6867        let (_sender, _dest) = drive_abandonment_log(payload_sentinel);
6868
6869        let buf = tracing_test::internal::global_buf().lock().unwrap();
6870        let logs = std::str::from_utf8(&buf).expect("logs are utf-8");
6871
6872        // Bounded summaries are present.
6873        assert!(
6874            logs.contains("message_type="),
6875            "UM-1: expected message_type field, got:\n{logs}"
6876        );
6877        assert!(
6878            logs.contains("data_len="),
6879            "UM-1: expected data_len field, got:\n{logs}"
6880        );
6881        // The payload body must not appear (no `data=<full body>`
6882        // dump).
6883        assert!(
6884            !logs.contains(payload_sentinel),
6885            "UM-1: payload body leaked into the log:\n{logs}"
6886        );
6887        // The `headers=` field must not appear. Use a space-prefixed
6888        // match to avoid matching e.g. the word "headers" in free
6889        // prose.
6890        assert!(
6891            !logs.contains(" headers="),
6892            "UM-1: unbounded headers field leaked into the log:\n{logs}"
6893        );
6894    }
6895
6896    /// UM-2: the `actor_name` and `actor_id` fields are preserved
6897    /// on the log for downstream Scuba / alert compatibility — same
6898    /// field names, same values, same types as the prior shape.
6899    #[tracing_test::traced_test]
6900    #[test]
6901    fn test_um2_compat_fields_preserved() {
6902        let (sender, _dest) = drive_abandonment_log("um2_payload");
6903
6904        let buf = tracing_test::internal::global_buf().lock().unwrap();
6905        let logs = std::str::from_utf8(&buf).expect("logs are utf-8");
6906
6907        // Decouple field-presence from value-presence so the test
6908        // does not depend on the tracing formatter's quoting rules.
6909        let actor_name = sender.log_name();
6910        let actor_id = sender.to_string();
6911        assert!(
6912            logs.contains("actor_name=") && logs.contains(actor_name),
6913            "UM-2: expected actor_name={actor_name} on the log, got:\n{logs}"
6914        );
6915        assert!(
6916            logs.contains("actor_id=") && logs.contains(&actor_id),
6917            "UM-2: expected actor_id={actor_id} on the log, got:\n{logs}"
6918        );
6919    }
6920
6921    /// UM-3: the format string names the transport destination.
6922    #[tracing_test::traced_test]
6923    #[test]
6924    fn test_um3_destination_format() {
6925        let (_sender, dest) = drive_abandonment_log("um3_payload");
6926
6927        let buf = tracing_test::internal::global_buf().lock().unwrap();
6928        let logs = std::str::from_utf8(&buf).expect("logs are utf-8");
6929
6930        assert!(
6931            logs.contains(&format!("message not delivered to {}", dest)),
6932            "UM-3: expected destination-naming format string, got:\n{logs}"
6933        );
6934    }
6935
6936    /// UM-3b: when the envelope carries operation-context headers, the
6937    /// format string names the user operation and the log carries
6938    /// the structured `endpoint` / `adverb` fields.
6939    #[tracing_test::traced_test]
6940    #[test]
6941    fn test_um3b_operation_format_with_operation_context() {
6942        let sender = test_actor_id("um_proc", "um_sender");
6943        let dest = test_port_id("um_dest_proc", "um_dest", 42);
6944
6945        let mut headers = Flattrs::new();
6946        headers.set(
6947            headers::OPERATION_ENDPOINT,
6948            "training.buffer.sample()".to_string(),
6949        );
6950        headers.set(headers::OPERATION_ADVERB, "call_one".to_string());
6951
6952        let envelope = MessageEnvelope::new(
6953            sender,
6954            dest,
6955            wirevalue::Any::serialize(&"um3b_payload".to_string()).unwrap(),
6956            headers,
6957        );
6958
6959        let (return_handle, _rx) = crate::mailbox::undeliverable::new_undeliverable_port();
6960        UndeliverableMailboxSender.post_unchecked(envelope, return_handle);
6961
6962        let buf = tracing_test::internal::global_buf().lock().unwrap();
6963        let logs = std::str::from_utf8(&buf).expect("logs are utf-8");
6964
6965        assert!(
6966            logs.contains("abandoned message for training.buffer.sample()"),
6967            "UM-3b: expected operation-naming format string, got:\n{logs}"
6968        );
6969        assert!(
6970            logs.contains("endpoint=") && logs.contains("training.buffer.sample()"),
6971            "UM-3b: expected endpoint field with the caller's operation, got:\n{logs}"
6972        );
6973        assert!(
6974            logs.contains("adverb=") && logs.contains("call_one"),
6975            "UM-3b: expected adverb field, got:\n{logs}"
6976        );
6977        // The UM-3a destination-naming shape must not appear when
6978        // operation-context headers are stamped on the envelope.
6979        assert!(
6980            !logs.contains("message not delivered to"),
6981            "UM-3b: unexpected destination-naming format string:\n{logs}"
6982        );
6983    }
6984}