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