Skip to main content

hyperactor/
actor.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#![allow(dead_code)] // Allow until this is used outside of tests.
10
11//! This module contains all the core traits required to define and manage actors.
12
13use std::any::TypeId;
14use std::borrow::Cow;
15use std::fmt;
16use std::fmt::Debug;
17use std::future::Future;
18use std::future::IntoFuture;
19use std::ops::Deref;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::time::SystemTime;
23
24use async_trait::async_trait;
25use enum_as_inner::EnumAsInner;
26use futures::FutureExt;
27use futures::future::BoxFuture;
28use hyperactor_config::Flattrs;
29use serde::Deserialize;
30use serde::Serialize;
31use tokio::sync::watch;
32use tokio::task::JoinHandle;
33use typeuri::Named;
34
35use crate as hyperactor; // for macros
36use crate::ActorAddr;
37use crate::ActorRef;
38use crate::Addr;
39#[cfg(test)]
40use crate::Client;
41use crate::Data;
42use crate::EndpointLocation;
43use crate::Message;
44use crate::RemoteMessage;
45use crate::context;
46use crate::endpoint::Endpoint;
47use crate::mailbox::DeliveryFailure;
48use crate::mailbox::DeliveryFailureKind;
49use crate::mailbox::ExpiredDelivery;
50use crate::mailbox::InvalidReference;
51use crate::mailbox::MailboxError;
52use crate::mailbox::MailboxSenderError;
53use crate::mailbox::MessageEnvelope;
54use crate::mailbox::PortHandle;
55use crate::mailbox::TransportFailureReason;
56use crate::mailbox::Undeliverable;
57use crate::mailbox::UndeliverableReason;
58use crate::proc::Context;
59use crate::proc::HandlerPorts;
60use crate::proc::Instance;
61use crate::proc::InstanceCell;
62use crate::proc::Proc;
63use crate::supervision::ActorSupervisionEvent;
64
65pub mod remote;
66
67/// The shutdown mode requested for an actor.
68#[derive(
69    Clone,
70    Copy,
71    Debug,
72    Serialize,
73    Deserialize,
74    PartialEq,
75    Eq,
76    typeuri::Named
77)]
78pub enum StopMode {
79    /// Stop without draining ordinary queued work first.
80    Stop,
81    /// Stop after draining already accepted ordinary queued work.
82    DrainAndStop,
83}
84wirevalue::register_type!(StopMode);
85
86/// An Actor is an independent, asynchronous thread of execution. Each
87/// actor instance has a mailbox, whose messages are delivered through
88/// the method [`Actor::handle`].
89///
90/// Actors communicate with each other by way of message passing.
91/// Actors are assumed to be _deterministic_: that is, the state of an
92/// actor is determined by the set (and order) of messages it receives.
93#[async_trait]
94pub trait Actor: Sized + Send + 'static {
95    /// Initialize the actor, after the runtime has been fully initialized.
96    /// Init thus provides a mechanism by which an actor can reliably and always
97    /// receive some initial event that can be used to kick off further
98    /// (potentially delayed) processing.
99    async fn init(&mut self, _this: &Instance<Self>) -> Result<(), anyhow::Error> {
100        // Default implementation: no init.
101        Ok(())
102    }
103
104    /// Handle a stop request from the runtime.
105    ///
106    /// The default implementation closes handler ingress and then
107    /// either exits immediately or queues an exit after already
108    /// accepted handler work drains. Actors that need to coordinate
109    /// asynchronous shutdown work can override this method and call
110    /// `Instance::exit()` / `Instance::exit_after_drain()` later,
111    /// once they are ready to terminate.
112    async fn handle_stop(
113        &mut self,
114        this: &Instance<Self>,
115        mode: StopMode,
116        reason: &str,
117    ) -> Result<(), anyhow::Error> {
118        handle_stop(this, mode, reason)
119    }
120
121    /// Cleanup things used by this actor before shutting down. Notably this function
122    /// is async and allows more complex cleanup. Simpler cleanup can be handled
123    /// by the impl Drop for this Actor.
124    /// If err is not None, it is the error that this actor is failing with. Any
125    /// errors returned by this function will be logged and ignored.
126    /// If err is None, any errors returned by this function will be propagated
127    /// as an ActorError.
128    /// This function is not called if there is a panic in the actor, as the
129    /// actor may be in an indeterminate state. It is also not called if the
130    /// process is killed, there is no atexit handler or signal handler.
131    async fn cleanup(
132        &mut self,
133        _this: &Instance<Self>,
134        _err: Option<&ActorError>,
135    ) -> Result<(), anyhow::Error> {
136        // Default implementation: no cleanup.
137        Ok(())
138    }
139
140    /// This method is used by the runtime to spawn the actor server. It can be
141    /// used by actors that require customized runtime setups
142    /// (e.g., dedicated actor threads), or want to use a custom tokio runtime.
143    #[hyperactor::instrument_infallible]
144    fn spawn_server_task<F>(future: F) -> JoinHandle<F::Output>
145    where
146        F: Future + Send + 'static,
147        F::Output: Send + 'static,
148    {
149        tokio::spawn(future)
150    }
151
152    /// Handle actor supervision event. Return `Ok(true)`` if the event is handled here.
153    async fn handle_supervision_event(
154        &mut self,
155        _this: &Instance<Self>,
156        event: &ActorSupervisionEvent,
157    ) -> Result<bool, anyhow::Error> {
158        // Error events are not handled by default and bubble up to the parent.
159        // Normal lifecycle events (e.g. clean stop) are absorbed.
160        Ok(!event.is_error())
161    }
162
163    /// Default delivery-failure event handling behavior.
164    async fn handle_delivery_failure_event(
165        &mut self,
166        cx: &Instance<Self>,
167        undeliverable: Undeliverable<MessageEnvelope>,
168    ) -> Result<(), anyhow::Error> {
169        handle_delivery_failure_event(self, cx, undeliverable).await
170    }
171
172    /// Default undeliverable message handling behavior.
173    async fn handle_undeliverable_message(
174        &mut self,
175        cx: &Instance<Self>,
176        reason: UndeliverableReason,
177        undeliverable: Undeliverable<MessageEnvelope>,
178    ) -> Result<(), anyhow::Error> {
179        handle_undeliverable_message(cx, reason, undeliverable)
180    }
181
182    /// Default invalid-reference handling behavior.
183    async fn handle_invalid_reference(
184        &mut self,
185        cx: &Instance<Self>,
186        invalid: InvalidReference,
187        undeliverable: Undeliverable<MessageEnvelope>,
188    ) -> Result<(), anyhow::Error> {
189        handle_invalid_reference(cx, invalid, undeliverable)
190    }
191
192    /// Default expired-delivery handling behavior.
193    async fn handle_expired_delivery(
194        &mut self,
195        cx: &Instance<Self>,
196        expired: ExpiredDelivery,
197        undeliverable: Undeliverable<MessageEnvelope>,
198    ) -> Result<(), anyhow::Error> {
199        handle_expired_delivery(cx, expired, undeliverable)
200    }
201
202    /// If overridden, we will use this name in place of the
203    /// ActorAddr for talking about this actor in supervision error
204    /// messages.
205    fn display_name(&self) -> Option<String> {
206        None
207    }
208}
209
210/// Default implementation of [`Actor::handle_delivery_failure_event`]. Defined
211/// as a free function so that `Actor` implementations that override
212/// [`Actor::handle_delivery_failure_event`] can fallback to this default.
213pub async fn handle_delivery_failure_event<A: Actor>(
214    actor: &mut A,
215    cx: &Instance<A>,
216    undeliverable: Undeliverable<MessageEnvelope>,
217) -> Result<(), anyhow::Error> {
218    match undeliverable
219        .root_delivery_failure()
220        .map(|failure| failure.kind.clone())
221    {
222        Some(DeliveryFailureKind::InvalidReference(invalid)) => {
223            actor
224                .handle_invalid_reference(cx, invalid, undeliverable)
225                .await
226        }
227        Some(DeliveryFailureKind::Expired(expired)) => {
228            actor
229                .handle_expired_delivery(cx, expired, undeliverable)
230                .await
231        }
232        Some(DeliveryFailureKind::Undeliverable(reason)) => {
233            actor
234                .handle_undeliverable_message(cx, reason, undeliverable)
235                .await
236        }
237        None => anyhow::bail!(undeliverable.into_error()),
238    }
239}
240
241fn delivery_failure_event_target(undeliverable: &Undeliverable<MessageEnvelope>) -> Addr {
242    match undeliverable {
243        Undeliverable::Returned(envelope) => envelope.dest().clone().into(),
244        Undeliverable::Report(report) => match &report.dest {
245            EndpointLocation::Actor(actor) => actor.clone().into(),
246            EndpointLocation::Port(port) => port.clone().into(),
247            EndpointLocation::Local { actor, .. } => actor.clone().into(),
248        },
249    }
250}
251
252/// Default implementation of [`Actor::handle_undeliverable_message`]. Defined
253/// as a free function so that `Actor` implementations that override
254/// [`Actor::handle_undeliverable_message`] can fallback to this default.
255pub fn handle_undeliverable_message<A: Actor>(
256    _cx: &Instance<A>,
257    reason: UndeliverableReason,
258    undeliverable: Undeliverable<MessageEnvelope>,
259) -> Result<(), anyhow::Error> {
260    if undeliverable_reason_fails_actor(&reason) {
261        anyhow::bail!(undeliverable.into_error());
262    }
263    Ok(())
264}
265
266fn undeliverable_reason_fails_actor(reason: &UndeliverableReason) -> bool {
267    matches!(
268        reason,
269        UndeliverableReason::Transport(transport)
270            if matches!(
271                &transport.reason,
272                TransportFailureReason::OversizedFrame { .. }
273            )
274    )
275}
276
277/// Default implementation of [`Actor::handle_invalid_reference`]. Defined
278/// as a free function so that `Actor` implementations that override
279/// [`Actor::handle_invalid_reference`] can fallback to this default.
280pub fn handle_invalid_reference<A: Actor>(
281    _cx: &Instance<A>,
282    _invalid: InvalidReference,
283    undeliverable: Undeliverable<MessageEnvelope>,
284) -> Result<(), anyhow::Error> {
285    anyhow::bail!(undeliverable.into_error())
286}
287
288/// Default implementation of [`Actor::handle_expired_delivery`]. Defined
289/// as a free function so that `Actor` implementations that override
290/// [`Actor::handle_expired_delivery`] can fallback to this default.
291pub fn handle_expired_delivery<A: Actor>(
292    _cx: &Instance<A>,
293    _expired: ExpiredDelivery,
294    undeliverable: Undeliverable<MessageEnvelope>,
295) -> Result<(), anyhow::Error> {
296    anyhow::bail!(undeliverable.into_error())
297}
298
299/// Default implementation of [`Actor::handle_stop`]. Defined as a free
300/// function so that `Actor` implementations that override
301/// [`Actor::handle_stop`] can fall back to this default.
302pub fn handle_stop<A: Actor>(
303    this: &Instance<A>,
304    mode: StopMode,
305    reason: &str,
306) -> Result<(), anyhow::Error> {
307    // After `close`, no more messages may be enqueued.
308    // exit_after_drain will drain any pending messages before exiting.
309    this.close();
310    match mode {
311        StopMode::Stop => this.exit(reason).map_err(anyhow::Error::from),
312        StopMode::DrainAndStop => this.exit_after_drain(reason).map_err(anyhow::Error::from),
313    }
314}
315
316/// An actor that does nothing. It is used to represent "client only" actors,
317/// returned by [`Proc::client`].
318#[async_trait]
319impl Actor for () {}
320
321impl Referable for () {}
322
323impl Binds<()> for () {
324    fn bind(_ports: &HandlerPorts<Self>) {
325        // Binds no ports.
326    }
327}
328
329/// A Handler allows an actor to handle a specific message type.
330#[async_trait]
331pub trait Handler<M>: Actor {
332    /// Handle the next M-typed message.
333    async fn handle(&mut self, cx: &Context<Self>, message: M) -> Result<(), anyhow::Error>;
334}
335
336/// Blanket Handler impls for bypass-workq message types. Since these messages
337/// bypass workq, they will never be sent to actor's handler.
338///
339/// These exist solely to lock the `Handler<M>` coherence slot for each bypass
340/// type, so no specific `impl Handler<BypassType> for SomeActor` can be written.
341/// The actual delivery for these types goes through dedicated channels set up
342/// in `Instance::new`, not through Handler. See the matching sender-side check
343/// in [crate::ordering::Sequencer::assign_seq] and the registry of bypass types
344/// in [crate::ordering::is_bypass_workq_type_id].
345#[async_trait]
346impl<A: Actor> Handler<crate::introspect::IntrospectMessage> for A {
347    async fn handle(
348        &mut self,
349        _cx: &Context<Self>,
350        _message: crate::introspect::IntrospectMessage,
351    ) -> Result<(), anyhow::Error> {
352        unimplemented!("introspect message handler should not be called directly")
353    }
354}
355
356#[cfg(test)]
357#[derive(Debug)]
358enum DeliveryFailurePolicy {
359    InvalidReference,
360    Expired,
361    Undeliverable,
362}
363
364#[cfg(test)]
365fn delivery_failure_policy(message: &Undeliverable<MessageEnvelope>) -> DeliveryFailurePolicy {
366    match message.root_delivery_failure().map(|failure| &failure.kind) {
367        Some(DeliveryFailureKind::InvalidReference(_)) => DeliveryFailurePolicy::InvalidReference,
368        Some(DeliveryFailureKind::Expired(_)) => DeliveryFailurePolicy::Expired,
369        Some(DeliveryFailureKind::Undeliverable(_)) | None => DeliveryFailurePolicy::Undeliverable,
370    }
371}
372
373struct DeliveryFailureLogFields {
374    sender: ActorAddr,
375    dest: EndpointLocation,
376    error: DeliveryFailureLogError,
377}
378
379impl DeliveryFailureLogFields {
380    fn new(message: &Undeliverable<MessageEnvelope>) -> Self {
381        match message {
382            Undeliverable::Returned(envelope) => Self {
383                sender: envelope.sender().clone(),
384                dest: EndpointLocation::Port(envelope.dest().clone()),
385                error: DeliveryFailureLogError::DeliveryFailures(
386                    envelope.delivery_failures().to_vec(),
387                ),
388            },
389            Undeliverable::Report(report) => Self {
390                sender: report.sender.clone(),
391                dest: report.dest.clone(),
392                error: DeliveryFailureLogError::DeliveryFailures(report.delivery_failures.clone()),
393            },
394        }
395    }
396}
397
398enum DeliveryFailureLogError {
399    DeliveryFailures(Vec<DeliveryFailure>),
400}
401
402impl fmt::Display for DeliveryFailureLogError {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            Self::DeliveryFailures(failures) => {
406                if failures.is_empty() {
407                    return write!(f, "<none>");
408                }
409                for (index, failure) in failures.iter().enumerate() {
410                    if index > 0 {
411                        write!(f, "; ")?;
412                    }
413                    write!(f, "{}", failure)?;
414                }
415                Ok(())
416            }
417        }
418    }
419}
420
421/// This handler provides a default behavior when a message sent by
422/// the actor to another is returned due to delivery failure.
423#[async_trait]
424impl<A: Actor> Handler<Undeliverable<MessageEnvelope>> for A {
425    async fn handle(
426        &mut self,
427        cx: &Context<Self>,
428        message: Undeliverable<MessageEnvelope>,
429    ) -> Result<(), anyhow::Error> {
430        let log_fields = (tracing::enabled!(tracing::Level::DEBUG)
431            || tracing::enabled!(tracing::Level::ERROR))
432        .then(|| DeliveryFailureLogFields::new(&message));
433        let result = self.handle_delivery_failure_event(cx, message).await;
434        match result {
435            Ok(_) => {
436                if let Some(log_fields) = log_fields {
437                    tracing::debug!(
438                        actor_id = %cx.self_addr(),
439                        name = "undeliverable_message_handled",
440                        sender = %log_fields.sender,
441                        dest = %log_fields.dest,
442                        error = %log_fields.error,
443                    );
444                }
445                Ok(())
446            }
447            Err(e) => {
448                if let Some(log_fields) = log_fields {
449                    tracing::error!(
450                        actor_id = %cx.self_addr(),
451                        name = "undeliverable_message",
452                        sender = %log_fields.sender,
453                        dest = %log_fields.dest,
454                        error = %log_fields.error,
455                        handler_error = %e,
456                    );
457                } else {
458                    tracing::error!(
459                        actor_id = %cx.self_addr(),
460                        name = "undeliverable_message",
461                        handler_error = %e,
462                    );
463                }
464                Err(e)
465            }
466        }
467    }
468}
469
470/// An `Actor` that can be spawned remotely.
471///
472/// Bounds explained:
473/// - `Actor`: only actors may be remotely spawned.
474/// - `Referable`: marks the type as eligible for typed remote
475///   references (`ActorRef<A>`); required because remote spawn
476///   ultimately hands back an `ActorAddr` that higher-level APIs may
477///   re-type as `ActorRef<A>`.
478/// - `Binds<Self>`: lets the runtime wire this actor's handler ports
479///   when it is spawned (the blanket impl calls `handle.bind::<Self>()`).
480///
481/// `gspawn_root_bind` is a type-erased entry point used by the remote
482/// spawn/registry machinery. It takes serialized params and returns
483/// the new actor's `ActorAddr`; application code shouldn't call it
484/// directly.
485#[async_trait]
486pub trait RemoteSpawn: Actor + Referable + Binds<Self> {
487    /// The type of parameters used to instantiate the actor remotely.
488    type Params: RemoteMessage;
489
490    /// Creates a new actor instance given its instantiation parameters.
491    /// The `environment` allows whoever is responsible for spawning this actor
492    /// to pass in additional context that may be useful.
493    async fn new(params: Self::Params, environment: Flattrs) -> anyhow::Result<Self>;
494
495    /// A type-erased entry point to spawn this actor as a root. This is
496    /// primarily used by hyperactor's remote actor registration
497    /// mechanism.
498    // TODO: consider making this 'private' -- by moving it into a non-public trait as in [`cap`].
499    fn gspawn_root_bind(
500        proc: &Proc,
501        uid: crate::id::Uid,
502        serialized_params: Data,
503        environment: Flattrs,
504    ) -> Pin<Box<dyn Future<Output = Result<ActorAddr, anyhow::Error>> + Send>> {
505        let proc = proc.clone();
506        Box::pin(async move {
507            let params =
508                bincode::serde::decode_from_slice(&serialized_params, bincode::config::legacy())
509                    .map(|(v, _)| v)?;
510            let actor = Self::new(params, environment).await?;
511            let handle = proc.spawn_with_uid(uid, actor)?;
512            // We return only the ActorAddr, not a typed ActorRef.
513            // Callers that hold this ID can interact with the actor
514            // only via the serialized/opaque messaging path, which
515            // makes it safe to export across process boundaries.
516            //
517            // Note: the actor itself is still `A`-typed here; we
518            // merely restrict the *capability* we hand out to an
519            // untyped identifier.
520            //
521            // This will be replaced by a proper export/registry
522            // mechanism.
523            Ok(handle.bind::<Self>().into_actor_addr())
524        })
525    }
526
527    /// A type-erased entry point to spawn this actor as a child.
528    ///
529    /// The returned handle is lifecycle-only; callers that know the concrete
530    /// actor type can recover a typed handle with [`AnyActorHandle::downcast`].
531    fn gspawn_child(
532        proc: &Proc,
533        parent: InstanceCell,
534        uid: crate::id::Uid,
535        serialized_params: Data,
536        environment: Flattrs,
537    ) -> Pin<Box<dyn Future<Output = Result<AnyActorHandle, anyhow::Error>> + Send>> {
538        let proc = proc.clone();
539        Box::pin(async move {
540            let params =
541                bincode::serde::decode_from_slice(&serialized_params, bincode::config::legacy())
542                    .map(|(v, _)| v)?;
543            let actor = Self::new(params, environment).await?;
544            let handle = proc.spawn_child_with_uid(parent, uid, actor)?;
545            handle.bind::<Self>();
546            Ok(handle.into_any())
547        })
548    }
549
550    /// The type ID of this actor.
551    fn get_type_id() -> TypeId {
552        TypeId::of::<Self>()
553    }
554}
555
556/// If an actor implements Default, we use this as the
557/// `RemoteSpawn` implementation, too.
558#[async_trait]
559impl<A: Actor + Referable + Binds<Self> + Default> RemoteSpawn for A {
560    type Params = ();
561
562    async fn new(_params: Self::Params, _environment: Flattrs) -> anyhow::Result<Self> {
563        Ok(Default::default())
564    }
565}
566
567/// Errors that occur while serving actors. Each error is associated
568/// with the ID of the actor being served.
569#[derive(Debug)]
570pub struct ActorError {
571    /// The ActorAddr for the actor that generated this error.
572    pub actor_id: Box<ActorAddr>,
573    /// The kind of error that occurred.
574    pub kind: Box<ActorErrorKind>,
575}
576
577/// The kinds of actor serving errors.
578#[derive(thiserror::Error, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
579pub enum ActorErrorKind {
580    /// Generic error with a formatted message.
581    #[error("{0}")]
582    Generic(String),
583
584    /// An error that occurred while trying to handle a supervision event.
585    #[error("{0} while handling {1}")]
586    ErrorDuringHandlingSupervision(String, Box<ActorSupervisionEvent>),
587
588    /// The actor did not attempt to handle
589    #[error("{0}")]
590    UnhandledSupervisionEvent(Box<ActorSupervisionEvent>),
591
592    /// The actor was explicitly aborted with the provided reason.
593    #[error("actor explicitly aborted due to: {0}")]
594    Aborted(String),
595
596    /// The actor's signal channel was closed before the actor loop exited
597    /// normally.
598    #[error("signal channel closed")]
599    SignalChannelClosed,
600}
601
602impl ActorErrorKind {
603    /// Error while processing actor, i.e., returned by the actor's
604    /// processing method.
605    pub fn processing(err: anyhow::Error) -> Self {
606        // Unbox err from the anyhow err. Check if it is an ActorErrorKind object.
607        // If it is directly use it as the new ActorError's ActorErrorKind.
608        // This lets us directly pass the ActorErrorKind::UnhandledSupervisionEvent
609        // up the handling infrastructure.
610        err.downcast::<ActorErrorKind>()
611            .unwrap_or_else(|err| Self::Generic(err.to_string()))
612    }
613
614    /// Unwound stracktrace of a panic.
615    pub fn panic(err: anyhow::Error) -> Self {
616        Self::Generic(format!("panic: {}", err))
617    }
618
619    /// Error during actor initialization.
620    pub fn init(err: anyhow::Error) -> Self {
621        Self::Generic(format!("initialization error: {}", err))
622    }
623
624    /// Error during actor cleanup.
625    pub fn cleanup(err: anyhow::Error) -> Self {
626        Self::Generic(format!("cleanup error: {}", err))
627    }
628
629    /// An underlying mailbox error.
630    pub fn mailbox(err: MailboxError) -> Self {
631        Self::Generic(err.to_string())
632    }
633
634    /// An underlying mailbox sender error.
635    pub fn mailbox_sender(err: MailboxSenderError) -> Self {
636        Self::Generic(err.to_string())
637    }
638
639    /// The actor's state could not be determined.
640    pub fn indeterminate_state() -> Self {
641        Self::Generic("actor is in an indeterminate state".to_string())
642    }
643}
644
645impl ActorError {
646    /// Create a new actor server error with the provided id and kind.
647    pub(crate) fn new(actor_id: &ActorAddr, kind: ActorErrorKind) -> Self {
648        Self {
649            actor_id: Box::new(actor_id.clone()),
650            kind: Box::new(kind),
651        }
652    }
653}
654
655impl fmt::Display for ActorError {
656    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657        write!(f, "serving {}: ", self.actor_id)?;
658        fmt::Display::fmt(&self.kind, f)
659    }
660}
661
662impl std::error::Error for ActorError {
663    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
664        self.kind.source()
665    }
666}
667
668impl From<MailboxError> for ActorError {
669    fn from(inner: MailboxError) -> Self {
670        Self {
671            actor_id: Box::new(inner.actor_addr().clone()),
672            kind: Box::new(ActorErrorKind::mailbox(inner)),
673        }
674    }
675}
676
677impl From<MailboxSenderError> for ActorError {
678    fn from(inner: MailboxSenderError) -> Self {
679        Self {
680            actor_id: Box::new(inner.location().actor_addr()),
681            kind: Box::new(ActorErrorKind::mailbox_sender(inner)),
682        }
683    }
684}
685
686/// A collection of signals to control the behavior of the actor.
687/// Signals are internal runtime control plane messages and should not be
688/// sent outside of the runtime.
689///
690/// These messages are not handled directly by actors; instead, the runtime
691/// handles the various signals.
692#[derive(Clone, Debug, Serialize, Deserialize, typeuri::Named)]
693pub enum Signal {
694    /// Stop the actor, after draining messages.
695    DrainAndStop(String),
696
697    /// Stop the actor immediately.
698    Stop(String),
699
700    /// Exit the actor loop with the provided stop reason.
701    ExitRequested(String),
702
703    /// The direct child with the given uid was stopped.
704    ChildStopped(crate::id::Uid),
705
706    /// Kill the actor. This will exit the actor loop with an error,
707    /// causing a supervision event to propagate up the supervision
708    /// hierarchy.
709    Kill(String),
710}
711
712impl fmt::Display for Signal {
713    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714        match self {
715            Signal::DrainAndStop(reason) => write!(f, "DrainAndStop({})", reason),
716            Signal::Stop(reason) => write!(f, "Stop({})", reason),
717            Signal::ExitRequested(reason) => write!(f, "ExitRequested({})", reason),
718            Signal::ChildStopped(uid) => write!(f, "ChildStopped({})", uid),
719            Signal::Kill(reason) => write!(f, "Kill({})", reason),
720        }
721    }
722}
723
724/// Information about a message handler being processed.
725///
726/// Uses `Cow<'static, str>` to avoid string copies on the hot path.
727/// The typename and arm are typically static strings from `TypeInfo`.
728#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
729pub struct HandlerInfo {
730    /// The type name of the message being handled.
731    pub typename: Cow<'static, str>,
732    /// The enum arm being handled, if the message is an enum.
733    pub arm: Option<Cow<'static, str>>,
734}
735
736impl HandlerInfo {
737    /// Create a new `HandlerInfo` from static strings (zero-copy).
738    pub fn from_static(typename: &'static str, arm: Option<&'static str>) -> Self {
739        Self {
740            typename: Cow::Borrowed(typename),
741            arm: arm.map(Cow::Borrowed),
742        }
743    }
744
745    /// Create a new `HandlerInfo` from owned strings.
746    pub fn from_owned(typename: String, arm: Option<String>) -> Self {
747        Self {
748            typename: Cow::Owned(typename),
749            arm: arm.map(Cow::Owned),
750        }
751    }
752}
753
754impl fmt::Display for HandlerInfo {
755    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
756        match &self.arm {
757            Some(arm) => write!(f, "{}.{}", self.typename, arm),
758            None => write!(f, "{}", self.typename),
759        }
760    }
761}
762
763/// Why an actor is stopping.
764#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
765pub enum ActorStoppingReason {
766    /// The actor is stopping through the normal cooperative shutdown path.
767    Requested,
768    /// The actor did not respond to hard kill, and teardown stopped waiting on
769    /// it normally.
770    Zombie(String),
771}
772
773impl fmt::Display for ActorStoppingReason {
774    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775        match self {
776            Self::Requested => write!(f, "requested"),
777            Self::Zombie(reason) => write!(f, "zombie: {}", reason),
778        }
779    }
780}
781
782/// The runtime status of an actor.
783#[derive(
784    Debug,
785    Serialize,
786    Deserialize,
787    PartialEq,
788    Eq,
789    Clone,
790    typeuri::Named,
791    EnumAsInner
792)]
793pub enum ActorStatus {
794    /// The actor status is unknown.
795    Unknown,
796    /// The actor was created, but not yet started.
797    Created,
798    /// The actor is initializing. It is not yet ready to receive messages.
799    Initializing,
800    /// The actor is in "client" state: the user is managing the actor's
801    /// mailboxes manually.
802    Client,
803    /// The actor is ready to receive messages, but is currently idle.
804    Idle,
805    /// The actor has been processing a message, beginning at the specified
806    /// instant. The message handler info is included.
807    Processing(SystemTime, Option<HandlerInfo>),
808    /// The actor is stopping. It is draining messages.
809    Stopping(ActorStoppingReason),
810    /// The actor is stopped with a provided reason.
811    /// It is no longer processing messages.
812    Stopped(String),
813    /// The actor failed with the provided actor error.
814    Failed(ActorErrorKind),
815}
816
817impl ActorStatus {
818    /// Tells whether the status is a terminal state.
819    pub fn is_terminal(&self) -> bool {
820        self.is_stopped() || self.is_failed()
821    }
822
823    /// Create a normal stopping status.
824    pub fn stopping() -> Self {
825        Self::Stopping(ActorStoppingReason::Requested)
826    }
827
828    /// Create a zombie stopping status.
829    pub fn zombie(reason: impl Into<String>) -> Self {
830        Self::Stopping(ActorStoppingReason::Zombie(reason.into()))
831    }
832
833    /// Tells whether the status is a zombie stopping state.
834    pub fn is_zombie(&self) -> bool {
835        matches!(self, Self::Stopping(ActorStoppingReason::Zombie(_)))
836    }
837
838    /// Create a generic failure status with the provided error message.
839    pub fn generic_failure(message: impl Into<String>) -> Self {
840        Self::Failed(ActorErrorKind::Generic(message.into()))
841    }
842
843    fn span_string(&self) -> &'static str {
844        self.arm().unwrap_or_default()
845    }
846}
847
848impl fmt::Display for ActorStatus {
849    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
850        match self {
851            Self::Unknown => write!(f, "unknown"),
852            Self::Created => write!(f, "created"),
853            Self::Initializing => write!(f, "initializing"),
854            Self::Client => write!(f, "client"),
855            Self::Idle => write!(f, "idle"),
856            Self::Processing(instant, None) => {
857                write!(
858                    f,
859                    "processing for {}ms",
860                    std::time::SystemTime::now()
861                        .duration_since(*instant)
862                        .unwrap_or_default()
863                        .as_millis()
864                )
865            }
866            Self::Processing(instant, Some(handler_info)) => {
867                write!(
868                    f,
869                    "{}: processing for {}ms",
870                    handler_info,
871                    std::time::SystemTime::now()
872                        .duration_since(*instant)
873                        .unwrap_or_default()
874                        .as_millis()
875                )
876            }
877            Self::Stopping(ActorStoppingReason::Requested) => write!(f, "stopping"),
878            Self::Stopping(ActorStoppingReason::Zombie(reason)) => write!(f, "zombie: {}", reason),
879            Self::Stopped(reason) => write!(f, "stopped: {}", reason),
880            Self::Failed(err) => write!(f, "failed: {}", err),
881        }
882    }
883}
884
885/// ActorHandles represent a (local) serving actor. It is used to access
886/// its messaging and signal ports, as well as to synchronize with its
887/// lifecycle (e.g., providing joins).  Once dropped, the handle is
888/// detached from the underlying actor instance, and there is no longer
889/// any way to join it.
890///
891/// Correspondingly, [`crate::ActorAddr`]s refer to (possibly) remote
892/// actors.
893pub struct ActorHandle<A: Actor> {
894    cell: InstanceCell,
895    ports: Arc<HandlerPorts<A>>,
896}
897
898/// A handle to a running (local) actor.
899impl<A: Actor> ActorHandle<A> {
900    pub(crate) fn new(cell: InstanceCell, ports: Arc<HandlerPorts<A>>) -> Self {
901        Self { cell, ports }
902    }
903
904    /// The actor's cell. Used primarily for testing.
905    /// TODO: this should not be a public API.
906    pub(crate) fn cell(&self) -> &InstanceCell {
907        &self.cell
908    }
909
910    /// The [`ActorAddr`] of the actor represented by this handle.
911    pub fn actor_addr(&self) -> &ActorAddr {
912        self.cell.actor_addr()
913    }
914
915    /// Signal the actor to drain its current messages and then stop.
916    pub fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
917        tracing::info!("ActorHandle::drain_and_stop called: {}", self.actor_addr());
918        self.cell.signal(Signal::DrainAndStop(reason.to_string()))
919    }
920
921    /// Signal the actor to stop without draining ordinary queued
922    /// work first.
923    pub fn stop(&self, reason: &str) -> Result<(), ActorError> {
924        tracing::info!("actor handle stop called: {}", self.actor_addr());
925        self.cell.signal(Signal::Stop(reason.to_string()))
926    }
927
928    /// Signal the actor to terminate immediately.
929    pub fn kill(&self, reason: &str) -> Result<(), ActorError> {
930        tracing::info!("actor handle kill called: {}", self.actor_addr());
931        self.cell.signal(Signal::Kill(reason.to_string()))
932    }
933
934    /// A watch that observes the lifecycle state of the actor.
935    pub fn status(&self) -> watch::Receiver<ActorStatus> {
936        self.cell.status().clone()
937    }
938
939    /// Return a port for the provided message type handled by the actor.
940    pub fn port<M: Message>(&self) -> PortHandle<M>
941    where
942        A: Handler<M>,
943    {
944        self.ports.get()
945    }
946
947    /// Post `message` to this actor's handler port for `M`, returning an error
948    /// if delivery fails (the actor has stopped, its mailbox is closed, or the
949    /// underlying channel is disconnected). Unlike [`Endpoint::post`], the
950    /// caller observes the failure instead of having it reported through the
951    /// actor's lost-message channel.
952    pub fn try_post<C, M>(&self, cx: &C, message: M) -> Result<(), MailboxSenderError>
953    where
954        C: context::Actor,
955        M: Message,
956        A: Handler<M>,
957    {
958        self.ports.get::<M>().try_post(cx, message)
959    }
960
961    /// TEMPORARY: bind...
962    /// TODO: we shoudl also have a default binding(?)
963    pub fn bind<R: Binds<A>>(&self) -> ActorRef<R> {
964        self.cell.bind(self.ports.as_ref())
965    }
966
967    /// Erase this handle's actor type, preserving only lifecycle access.
968    pub fn into_any(self) -> AnyActorHandle {
969        AnyActorHandle { cell: self.cell }
970    }
971
972    /// Convert this handle into a guard that stops the actor when dropped.
973    ///
974    /// Dropping the returned guard sends a normal stop signal. The guard does
975    /// not wait for the actor to stop.
976    pub fn into_guard(self) -> ActorGuard<A> {
977        ActorGuard { handle: Some(self) }
978    }
979}
980
981/// A guard that stops an actor when dropped.
982pub struct ActorGuard<A: Actor> {
983    handle: Option<ActorHandle<A>>,
984}
985
986impl<A: Actor> ActorGuard<A> {
987    /// Return the actor handle without stopping the actor.
988    pub fn into_inner(mut self) -> ActorHandle<A> {
989        self.handle
990            .take()
991            .expect("actor guard must contain a handle")
992    }
993}
994
995impl<A: Actor> Deref for ActorGuard<A> {
996    type Target = ActorHandle<A>;
997
998    fn deref(&self) -> &Self::Target {
999        self.handle
1000            .as_ref()
1001            .expect("actor guard must contain a handle")
1002    }
1003}
1004
1005impl<A: Actor> Drop for ActorGuard<A> {
1006    fn drop(&mut self) {
1007        if let Some(handle) = self.handle.take()
1008            && let Err(err) = handle.stop("actor guard dropped")
1009        {
1010            tracing::debug!(
1011                actor_id = %handle.actor_addr(),
1012                "actor guard failed to stop actor: {}",
1013                err
1014            );
1015        }
1016    }
1017}
1018
1019impl<A: Actor> Debug for ActorGuard<A> {
1020    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1021        f.debug_struct("ActorGuard")
1022            .field(
1023                "actor_id",
1024                &self.handle.as_ref().map(|handle| handle.actor_addr()),
1025            )
1026            .finish()
1027    }
1028}
1029
1030/// A type-erased handle to a running actor whose concrete type is erased.
1031///
1032/// This handle intentionally does not expose typed messaging or binding APIs.
1033/// Use [`AnyActorHandle::downcast`] to recover a typed [`ActorHandle`] when the
1034/// concrete actor type is known.
1035pub struct AnyActorHandle {
1036    cell: InstanceCell,
1037}
1038
1039impl AnyActorHandle {
1040    /// The [`ActorAddr`] of the actor represented by this handle.
1041    pub fn actor_id(&self) -> &ActorAddr {
1042        self.cell.actor_addr()
1043    }
1044
1045    /// Signal the actor to drain its current messages and then stop.
1046    pub fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
1047        self.cell.signal(Signal::DrainAndStop(reason.to_string()))
1048    }
1049
1050    /// Signal the actor to stop without draining ordinary queued work first.
1051    pub fn stop(&self, reason: &str) -> Result<(), ActorError> {
1052        self.cell.signal(Signal::Stop(reason.to_string()))
1053    }
1054
1055    /// Signal the actor to terminate immediately.
1056    pub fn kill(&self, reason: &str) -> Result<(), ActorError> {
1057        self.cell.signal(Signal::Kill(reason.to_string()))
1058    }
1059
1060    /// A watch that observes the lifecycle state of the actor.
1061    pub fn status(&self) -> watch::Receiver<ActorStatus> {
1062        self.cell.status().clone()
1063    }
1064
1065    /// Attempt to recover a typed actor handle.
1066    pub fn downcast<A: Actor>(&self) -> Option<ActorHandle<A>> {
1067        self.cell.downcast_handle()
1068    }
1069
1070    /// Convert this handle into a guard that stops the actor when dropped.
1071    ///
1072    /// Dropping the returned guard sends a normal stop signal. The guard does
1073    /// not wait for the actor to stop.
1074    pub fn into_guard(self) -> AnyActorGuard {
1075        AnyActorGuard { handle: Some(self) }
1076    }
1077}
1078
1079/// A type-erased guard that stops an actor when dropped.
1080pub struct AnyActorGuard {
1081    handle: Option<AnyActorHandle>,
1082}
1083
1084impl AnyActorGuard {
1085    /// Return the actor handle without stopping the actor.
1086    pub fn into_inner(mut self) -> AnyActorHandle {
1087        self.handle
1088            .take()
1089            .expect("actor guard must contain a handle")
1090    }
1091}
1092
1093impl Deref for AnyActorGuard {
1094    type Target = AnyActorHandle;
1095
1096    fn deref(&self) -> &Self::Target {
1097        self.handle
1098            .as_ref()
1099            .expect("actor guard must contain a handle")
1100    }
1101}
1102
1103impl Drop for AnyActorGuard {
1104    fn drop(&mut self) {
1105        if let Some(handle) = self.handle.take()
1106            && let Err(err) = handle.stop("actor guard dropped")
1107        {
1108            tracing::debug!(
1109                actor_id = %handle.actor_id(),
1110                "actor guard failed to stop actor: {}",
1111                err
1112            );
1113        }
1114    }
1115}
1116
1117impl Debug for AnyActorGuard {
1118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1119        f.debug_struct("AnyActorGuard")
1120            .field(
1121                "actor_id",
1122                &self.handle.as_ref().map(|handle| handle.actor_id()),
1123            )
1124            .finish()
1125    }
1126}
1127
1128/// IntoFuture allows users to await the handle to join it. The future
1129/// resolves when the actor runtime has fully stopped.
1130/// The future resolves to the actor's final status.
1131impl IntoFuture for AnyActorHandle {
1132    type Output = ActorStatus;
1133    type IntoFuture = BoxFuture<'static, Self::Output>;
1134
1135    fn into_future(self) -> Self::IntoFuture {
1136        let future = async move {
1137            let mut status_receiver = self.cell.status().clone();
1138            let result = status_receiver.wait_for(ActorStatus::is_terminal).await;
1139            match result {
1140                Err(_) => ActorStatus::Unknown,
1141                Ok(status) => status.clone(),
1142            }
1143        };
1144
1145        future.boxed()
1146    }
1147}
1148
1149impl Debug for AnyActorHandle {
1150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1151        f.debug_struct("AnyActorHandle")
1152            .field("cell", &"..")
1153            .finish()
1154    }
1155}
1156
1157impl Clone for AnyActorHandle {
1158    fn clone(&self) -> Self {
1159        Self {
1160            cell: self.cell.clone(),
1161        }
1162    }
1163}
1164
1165/// IntoFuture allows users to await the handle to join it. The future
1166/// resolves when the actor runtime has fully stopped.
1167/// The future resolves to the actor's final status.
1168impl<A: Actor> IntoFuture for ActorHandle<A> {
1169    type Output = ActorStatus;
1170    type IntoFuture = BoxFuture<'static, Self::Output>;
1171
1172    fn into_future(self) -> Self::IntoFuture {
1173        let future = async move {
1174            let mut status_receiver = self.cell.status().clone();
1175            let result = status_receiver.wait_for(ActorStatus::is_terminal).await;
1176            match result {
1177                Err(_) => ActorStatus::Unknown,
1178                Ok(status) => status.clone(),
1179            }
1180        };
1181
1182        future.boxed()
1183    }
1184}
1185
1186impl<A: Actor> Debug for ActorHandle<A> {
1187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1188        f.debug_struct("ActorHandle").field("cell", &"..").finish()
1189    }
1190}
1191
1192impl<A, M> Endpoint<M> for &ActorHandle<A>
1193where
1194    A: Actor + Handler<M>,
1195    M: Message,
1196{
1197    fn endpoint_location(&self) -> EndpointLocation {
1198        EndpointLocation::Actor(self.actor_addr().clone())
1199    }
1200
1201    fn post<C>(self, cx: &C, message: M)
1202    where
1203        C: context::Actor,
1204    {
1205        Endpoint::post(&self.ports.get(), cx, message)
1206    }
1207}
1208
1209impl<A: Actor> Clone for ActorHandle<A> {
1210    fn clone(&self) -> Self {
1211        Self {
1212            cell: self.cell.clone(),
1213            ports: self.ports.clone(),
1214        }
1215    }
1216}
1217
1218/// `Referable` is a marker trait for types that can appear as
1219/// remote references across process boundaries.
1220///
1221/// It is not limited to concrete [`Actor`] implementations. For
1222/// example, façade types generated by [`behavior!`] implement
1223/// `Referable` so that you can hand out restricted or stable APIs
1224/// while still using the same remote messaging machinery.
1225///
1226/// Implementing this trait means the type can be identified (`Named`)
1227/// so the runtime knows what it is.
1228///
1229///  In contrast, [`RemoteSpawn`] is the trait that marks *actors*
1230/// that can actually be **spawned remotely**. A behavior may be a
1231/// `Referable` but is never a `RemoteSpawn`.
1232pub trait Referable: Named {}
1233
1234/// Binds determines how an actor's ports are bound to a specific
1235/// reference type.
1236pub trait Binds<A: Actor>: Referable {
1237    /// Bind ports in this actor.
1238    fn bind(ports: &HandlerPorts<A>);
1239}
1240
1241/// Handles is a marker trait specifying that message type [`M`]
1242/// is handled by a specific actor type.
1243pub trait RemoteHandles<M: RemoteMessage>: Referable {}
1244
1245/// Check if the actor behaves-as the a given behavior (defined by [`behavior!`]).
1246///
1247/// ```
1248/// # use serde::Serialize;
1249/// # use serde::Deserialize;
1250/// # use typeuri::Named;
1251/// # use hyperactor::Actor;
1252///
1253/// // First, define a behavior, based on handling a single message type `()`.
1254/// hyperactor::behavior!(UnitBehavior, ());
1255///
1256/// #[derive(Debug, Default)]
1257/// struct MyActor;
1258///
1259/// impl Actor for MyActor {}
1260///
1261/// #[async_trait::async_trait]
1262/// impl hyperactor::Handler<()> for MyActor {
1263///     async fn handle(
1264///         &mut self,
1265///         _cx: &hyperactor::Context<Self>,
1266///         _message: (),
1267///     ) -> Result<(), anyhow::Error> {
1268///         // no-op
1269///         Ok(())
1270///     }
1271/// }
1272///
1273/// hyperactor::assert_behaves!(MyActor as UnitBehavior);
1274/// ```
1275#[macro_export]
1276macro_rules! assert_behaves {
1277    ($ty:ty as $behavior:ty) => {
1278        const _: fn() = || {
1279            fn check<B: hyperactor::actor::Binds<$ty>>() {}
1280            check::<$behavior>();
1281        };
1282    };
1283}
1284
1285#[cfg(test)]
1286mod tests {
1287    use std::assert_matches;
1288    use std::sync::Mutex;
1289    use std::time::Duration;
1290
1291    use rand::seq::SliceRandom;
1292    use timed_test::async_timed_test;
1293    use tokio::sync::mpsc;
1294    use tokio::time::timeout;
1295
1296    use super::*;
1297    use crate as hyperactor;
1298    use crate::Actor;
1299    use crate::ActorRef;
1300    use crate::Addr;
1301    use crate::EndpointLocation;
1302    use crate::OncePortHandle;
1303    use crate::PortAddr;
1304    use crate::PortRef;
1305    use crate::config;
1306    use crate::context::Mailbox as _;
1307    use crate::introspect::IntrospectMessage;
1308    use crate::introspect::IntrospectResult;
1309    use crate::introspect::IntrospectView;
1310    use crate::mailbox::BoxableMailboxSender as _;
1311    use crate::mailbox::DeliveryFailure;
1312    use crate::mailbox::DeliveryFailureReport;
1313    use crate::mailbox::ExpiredDelivery;
1314    use crate::mailbox::InvalidReference;
1315    use crate::mailbox::InvalidReferenceReason;
1316    use crate::mailbox::MailboxSender;
1317    use crate::mailbox::PortGone;
1318    use crate::mailbox::PortLocation;
1319    use crate::mailbox::TransportFailure;
1320    use crate::mailbox::TransportFailureReason;
1321    use crate::mailbox::UndeliverableReason;
1322    use crate::mailbox::monitored_return_handle;
1323    use crate::ordering::SEQ_INFO;
1324    use crate::ordering::SeqInfo;
1325    use crate::port::Port;
1326    use crate::testing::ids::test_proc_id;
1327    use crate::testing::pingpong::PingPongActor;
1328    use crate::testing::pingpong::PingPongMessage;
1329    use crate::testing::proc_supervison::ProcSupervisionCoordinator; // for macros
1330
1331    #[derive(Debug)]
1332    struct EchoActor(PortRef<u64>);
1333
1334    #[async_trait]
1335    impl Actor for EchoActor {}
1336
1337    #[async_trait]
1338    impl Handler<u64> for EchoActor {
1339        async fn handle(&mut self, cx: &Context<Self>, message: u64) -> Result<(), anyhow::Error> {
1340            let Self(port) = self;
1341            port.post(cx, message);
1342            Ok(())
1343        }
1344    }
1345
1346    #[derive(Debug)]
1347    struct DeliveryPolicyActor(PortRef<()>);
1348
1349    #[async_trait]
1350    impl Actor for DeliveryPolicyActor {}
1351
1352    #[async_trait]
1353    impl Handler<()> for DeliveryPolicyActor {
1354        async fn handle(&mut self, cx: &Context<Self>, _message: ()) -> Result<(), anyhow::Error> {
1355            self.0.post(cx, ());
1356            Ok(())
1357        }
1358    }
1359
1360    fn delivery_policy_envelope(
1361        sender: &ActorAddr,
1362        dest: PortAddr,
1363        failure: DeliveryFailure,
1364    ) -> MessageEnvelope {
1365        let mut envelope =
1366            MessageEnvelope::serialize(sender.clone(), dest, &(), Flattrs::new()).unwrap();
1367        envelope.push_delivery_failure(failure);
1368        envelope
1369    }
1370
1371    fn delivery_policy_report(
1372        sender: ActorAddr,
1373        dest: PortAddr,
1374        failure: DeliveryFailure,
1375    ) -> DeliveryFailureReport {
1376        DeliveryFailureReport::new(
1377            sender,
1378            EndpointLocation::Port(dest),
1379            Some("()".to_string()),
1380            failure,
1381        )
1382    }
1383
1384    async fn assert_delivery_policy_actor_remains_live(
1385        make_undeliverable: impl FnOnce(&ActorAddr, PortAddr) -> Undeliverable<MessageEnvelope>,
1386    ) {
1387        let proc = Proc::isolated();
1388        let client = proc.client("client");
1389        let (sync_port, mut sync_rx) = client.open_port::<()>();
1390        let actor = DeliveryPolicyActor(sync_port.bind());
1391        let handle = proc.spawn_with_label("delivery_policy", actor);
1392        let dest = handle.actor_addr().port_addr(Port::from(1234));
1393
1394        handle.post(
1395            &client,
1396            make_undeliverable(handle.actor_addr(), dest.clone()),
1397        );
1398        handle.post(&client, ());
1399
1400        tokio::time::timeout(Duration::from_secs(1), sync_rx.recv())
1401            .await
1402            .expect("actor should remain live")
1403            .expect("sync port should receive response");
1404        handle.drain_and_stop("test").unwrap();
1405        assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
1406    }
1407
1408    async fn assert_delivery_policy_actor_fails(
1409        make_undeliverable: impl FnOnce(&ActorAddr, PortAddr) -> Undeliverable<MessageEnvelope>,
1410    ) {
1411        let proc = Proc::isolated();
1412        let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
1413        let client = proc.client("client");
1414        let (sync_port, _sync_rx) = client.open_port::<()>();
1415        let actor = DeliveryPolicyActor(sync_port.bind());
1416        let handle = proc.spawn_with_label("delivery_policy", actor);
1417        let dest = handle.actor_addr().port_addr(Port::from(1234));
1418
1419        handle.post(
1420            &client,
1421            make_undeliverable(handle.actor_addr(), dest.clone()),
1422        );
1423
1424        assert_matches!(handle.await, ActorStatus::Failed(_));
1425    }
1426
1427    #[tokio::test]
1428    async fn test_default_transport_undeliverable_policy_does_not_fail_actor() {
1429        let target = Addr::Proc(test_proc_id("target"));
1430        let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1431            target,
1432            TransportFailureReason::NoRoute,
1433        )));
1434        assert_delivery_policy_actor_remains_live(|sender, dest| {
1435            Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1436        })
1437        .await;
1438    }
1439
1440    #[tokio::test]
1441    async fn test_default_transport_report_policy_does_not_fail_actor() {
1442        let target = Addr::Proc(test_proc_id("target"));
1443        let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1444            target,
1445            TransportFailureReason::NoRoute,
1446        )));
1447        assert_delivery_policy_actor_remains_live(|sender, dest| {
1448            Undeliverable::Report(delivery_policy_report(sender.clone(), dest, failure))
1449        })
1450        .await;
1451    }
1452
1453    #[tokio::test]
1454    async fn test_default_oversized_frame_transport_policy_fails_actor() {
1455        let target = Addr::Proc(test_proc_id("target"));
1456        let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1457            target,
1458            TransportFailureReason::OversizedFrame {
1459                len: 55001392,
1460                max: 50000000,
1461            },
1462        )));
1463        assert_delivery_policy_actor_fails(|sender, dest| {
1464            Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1465        })
1466        .await;
1467    }
1468
1469    #[tokio::test]
1470    async fn test_default_oversized_frame_report_policy_fails_actor() {
1471        let target = Addr::Proc(test_proc_id("target"));
1472        let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1473            target,
1474            TransportFailureReason::OversizedFrame {
1475                len: 55001392,
1476                max: 50000000,
1477            },
1478        )));
1479        assert_delivery_policy_actor_fails(|sender, dest| {
1480            Undeliverable::Report(delivery_policy_report(sender.clone(), dest, failure))
1481        })
1482        .await;
1483    }
1484
1485    #[tokio::test]
1486    async fn test_default_port_gone_policy_does_not_fail_actor() {
1487        let port = test_proc_id("target")
1488            .actor_addr("actor")
1489            .port_addr(Port::from(1234));
1490        let failure = DeliveryFailure::new(UndeliverableReason::PortGone(PortGone::new(
1491            port,
1492            Some("()".to_string()),
1493        )));
1494        assert_delivery_policy_actor_remains_live(|sender, dest| {
1495            Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1496        })
1497        .await;
1498    }
1499
1500    #[tokio::test]
1501    async fn test_default_invalid_reference_policy_fails_actor() {
1502        let target = test_proc_id("target").actor_addr("actor");
1503        let failure = DeliveryFailure::new(InvalidReference::new(
1504            target,
1505            InvalidReferenceReason::ActorNotExist,
1506        ));
1507        assert_delivery_policy_actor_fails(|sender, dest| {
1508            Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1509        })
1510        .await;
1511    }
1512
1513    #[tokio::test]
1514    async fn test_default_invalid_reference_policy_allows_return_handle_sender_mismatch() {
1515        let target = test_proc_id("target").actor_addr("actor");
1516        let failure = DeliveryFailure::new(InvalidReference::new(
1517            target,
1518            InvalidReferenceReason::ActorNotExist,
1519        ));
1520        assert_delivery_policy_actor_fails(|_actor, dest| {
1521            Undeliverable::Returned(delivery_policy_envelope(
1522                &test_proc_id("sender").actor_addr("actor"),
1523                dest,
1524                failure,
1525            ))
1526        })
1527        .await;
1528    }
1529
1530    #[tokio::test]
1531    async fn test_default_expired_delivery_policy_fails_actor() {
1532        let port = test_proc_id("target")
1533            .actor_addr("actor")
1534            .port_addr(Port::from(1234));
1535        let failure = DeliveryFailure::new(ExpiredDelivery::new(port));
1536        assert_delivery_policy_actor_fails(|sender, dest| {
1537            Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1538        })
1539        .await;
1540    }
1541
1542    #[test]
1543    fn test_delivery_failure_policy_ignores_attrs() {
1544        hyperactor_config::attrs::declare_attrs! {
1545            attr TEST_DELIVERY_FAILURE_ATTR: String;
1546        }
1547
1548        let sender = test_proc_id("sender").actor_addr("actor");
1549        let dest = sender.port_addr(Port::from(1234));
1550        let mut attrs = Flattrs::new();
1551        attrs.set(TEST_DELIVERY_FAILURE_ATTR, "context".to_string());
1552
1553        let transport = delivery_policy_envelope(
1554            &sender,
1555            dest.clone(),
1556            DeliveryFailure::with_attrs(
1557                UndeliverableReason::Transport(TransportFailure::new(
1558                    dest.clone(),
1559                    TransportFailureReason::NoRoute,
1560                )),
1561                attrs.clone(),
1562            ),
1563        );
1564        assert_matches!(
1565            delivery_failure_policy(&Undeliverable::Returned(transport)),
1566            DeliveryFailurePolicy::Undeliverable
1567        );
1568
1569        let invalid_reference = delivery_policy_envelope(
1570            &sender,
1571            dest.clone(),
1572            DeliveryFailure::with_attrs(
1573                InvalidReference::new(dest.clone(), InvalidReferenceReason::PortNeverAllocated),
1574                attrs.clone(),
1575            ),
1576        );
1577        assert_matches!(
1578            delivery_failure_policy(&Undeliverable::Returned(invalid_reference)),
1579            DeliveryFailurePolicy::InvalidReference
1580        );
1581
1582        let expired = delivery_policy_envelope(
1583            &sender,
1584            dest.clone(),
1585            DeliveryFailure::with_attrs(ExpiredDelivery::new(dest), attrs),
1586        );
1587        assert_matches!(
1588            delivery_failure_policy(&Undeliverable::Returned(expired)),
1589            DeliveryFailurePolicy::Expired
1590        );
1591    }
1592
1593    #[test]
1594    fn test_delivery_failure_policy_uses_report_root_failure() {
1595        let sender = test_proc_id("sender").actor_addr("actor");
1596        let dest = sender.port_addr(Port::from(1234));
1597        let report = DeliveryFailureReport::new(
1598            sender,
1599            EndpointLocation::Port(dest.clone()),
1600            Some("()".to_string()),
1601            DeliveryFailure::new(ExpiredDelivery::new(dest)),
1602        );
1603
1604        assert_matches!(
1605            delivery_failure_policy(&Undeliverable::Report(report)),
1606            DeliveryFailurePolicy::Expired
1607        );
1608    }
1609
1610    #[tokio::test]
1611    async fn test_server_basic() {
1612        let proc = Proc::isolated();
1613        let client = proc.client("client");
1614        let (tx, mut rx) = client.open_port();
1615        let actor = EchoActor(tx.bind());
1616        let handle = proc.spawn(actor);
1617        handle.post(&client, 123u64);
1618        handle.drain_and_stop("test").unwrap();
1619        handle.await;
1620
1621        assert_eq!(rx.drain(), vec![123u64]);
1622    }
1623
1624    #[tokio::test]
1625    async fn test_actor_handle_guard_stops_actor_on_drop() {
1626        let proc = Proc::isolated();
1627        let handle = proc.spawn(());
1628        let mut status = handle.status();
1629
1630        {
1631            let _guard = handle.into_guard();
1632        }
1633
1634        let stopped = timeout(
1635            Duration::from_secs(5),
1636            status.wait_for(|status| {
1637                matches!(status, ActorStatus::Stopped(reason) if reason == "actor guard dropped")
1638            }),
1639        )
1640        .await
1641        .unwrap()
1642        .unwrap()
1643        .clone();
1644
1645        match stopped {
1646            ActorStatus::Stopped(reason) => assert_eq!(reason, "actor guard dropped"),
1647            status => panic!("actor guard should stop actor, got {status}"),
1648        }
1649    }
1650
1651    #[tokio::test]
1652    async fn test_actor_handle_guard_into_inner_disarms_stop() {
1653        let proc = Proc::isolated();
1654        let handle = proc.spawn(());
1655        let mut status = handle.status();
1656
1657        let guard = handle.into_guard();
1658        let _ = guard.status();
1659        let guarded_handle = guard.into_inner();
1660        let result = timeout(
1661            Duration::from_millis(100),
1662            status.wait_for(ActorStatus::is_terminal),
1663        )
1664        .await;
1665        assert!(result.is_err());
1666
1667        guarded_handle.drain_and_stop("test").unwrap();
1668        guarded_handle.await;
1669    }
1670
1671    #[tokio::test]
1672    async fn test_ping_pong() {
1673        let proc = Proc::isolated();
1674        let client = proc.client("client");
1675        let (undeliverable_msg_tx, _) = client.open_port();
1676
1677        let ping_actor = PingPongActor::new(Some(undeliverable_msg_tx.bind()), None, None);
1678        let pong_actor = PingPongActor::new(Some(undeliverable_msg_tx.bind()), None, None);
1679        let ping_handle = proc.spawn_with_label::<PingPongActor>("ping", ping_actor);
1680        let pong_handle = proc.spawn_with_label::<PingPongActor>("pong", pong_actor);
1681
1682        let (local_port, local_receiver) = client.open_once_port();
1683
1684        ping_handle.post(
1685            &client,
1686            PingPongMessage(10, pong_handle.bind(), local_port.bind()),
1687        );
1688
1689        assert!(local_receiver.recv().await.unwrap());
1690    }
1691
1692    #[tokio::test]
1693    async fn test_ping_pong_on_handler_error() {
1694        let proc = Proc::isolated();
1695        let client = proc.client("client");
1696        let (undeliverable_msg_tx, _) = client.open_port();
1697
1698        // Need to set a supervison coordinator for this Proc because there will
1699        // be actor failure(s) in this test which trigger supervision.
1700        let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
1701
1702        let error_ttl = 66;
1703
1704        let ping_actor =
1705            PingPongActor::new(Some(undeliverable_msg_tx.bind()), Some(error_ttl), None);
1706        let pong_actor =
1707            PingPongActor::new(Some(undeliverable_msg_tx.bind()), Some(error_ttl), None);
1708        let ping_handle = proc.spawn_with_label::<PingPongActor>("ping", ping_actor);
1709        let pong_handle = proc.spawn_with_label::<PingPongActor>("pong", pong_actor);
1710
1711        let (local_port, local_receiver) = client.open_once_port();
1712
1713        ping_handle.post(
1714            &client,
1715            PingPongMessage(
1716                error_ttl + 1, // will encounter an error at TTL=66
1717                pong_handle.bind(),
1718                local_port.bind(),
1719            ),
1720        );
1721
1722        // TODO: Fix this receiver hanging issue in T200423722.
1723        let res: Result<Result<bool, MailboxError>, tokio::time::error::Elapsed> =
1724            timeout(Duration::from_secs(5), local_receiver.recv()).await;
1725        assert!(res.is_err());
1726    }
1727
1728    #[derive(Debug)]
1729    struct InitActor(bool);
1730
1731    #[async_trait]
1732    impl Actor for InitActor {
1733        async fn init(&mut self, _this: &Instance<Self>) -> Result<(), anyhow::Error> {
1734            self.0 = true;
1735            Ok(())
1736        }
1737    }
1738
1739    #[async_trait]
1740    impl Handler<OncePortHandle<bool>> for InitActor {
1741        async fn handle(
1742            &mut self,
1743            cx: &Context<Self>,
1744            port: OncePortHandle<bool>,
1745        ) -> Result<(), anyhow::Error> {
1746            port.post(cx, self.0);
1747            Ok(())
1748        }
1749    }
1750
1751    #[tokio::test]
1752    async fn test_init() {
1753        let proc = Proc::isolated();
1754        let actor = InitActor(false);
1755        let handle = proc.spawn(actor);
1756        let client = proc.client("client");
1757
1758        let (port, receiver) = client.open_once_port();
1759        handle.post(&client, port);
1760        assert!(receiver.recv().await.unwrap());
1761
1762        handle.drain_and_stop("test").unwrap();
1763        handle.await;
1764    }
1765
1766    type MultiValues = Arc<Mutex<(u64, String)>>;
1767
1768    struct MultiValuesTest {
1769        proc: Proc,
1770        values: MultiValues,
1771        handle: ActorHandle<MultiActor>,
1772        client: Client,
1773    }
1774
1775    impl MultiValuesTest {
1776        async fn new() -> Self {
1777            let proc = Proc::isolated();
1778            let values: MultiValues = Arc::new(Mutex::new((0, "".to_string())));
1779            let actor = MultiActor(values.clone());
1780            let handle = proc.spawn(actor);
1781            let client = proc.client("client");
1782            Self {
1783                proc,
1784                values,
1785                handle,
1786                client,
1787            }
1788        }
1789
1790        fn send<M>(&self, message: M)
1791        where
1792            M: RemoteMessage,
1793            MultiActor: Handler<M>,
1794        {
1795            self.handle.post(&self.client, message)
1796        }
1797
1798        async fn sync(&self) {
1799            let (port, done) = self.client.open_once_port::<bool>();
1800            self.handle.post(&self.client, port);
1801            assert!(done.recv().await.unwrap());
1802        }
1803
1804        fn get_values(&self) -> (u64, String) {
1805            self.values.lock().unwrap().clone()
1806        }
1807    }
1808
1809    #[derive(Debug)]
1810    #[hyperactor::export(handlers = [u64, String])]
1811    struct MultiActor(MultiValues);
1812
1813    #[async_trait]
1814    impl Actor for MultiActor {}
1815
1816    #[async_trait]
1817    impl Handler<u64> for MultiActor {
1818        async fn handle(&mut self, _cx: &Context<Self>, message: u64) -> Result<(), anyhow::Error> {
1819            let mut vals = self.0.lock().unwrap();
1820            vals.0 = message;
1821            Ok(())
1822        }
1823    }
1824
1825    #[async_trait]
1826    impl Handler<String> for MultiActor {
1827        async fn handle(
1828            &mut self,
1829            _cx: &Context<Self>,
1830            message: String,
1831        ) -> Result<(), anyhow::Error> {
1832            let mut vals = self.0.lock().unwrap();
1833            vals.1 = message;
1834            Ok(())
1835        }
1836    }
1837
1838    #[async_trait]
1839    impl Handler<OncePortHandle<bool>> for MultiActor {
1840        async fn handle(
1841            &mut self,
1842            cx: &Context<Self>,
1843            message: OncePortHandle<bool>,
1844        ) -> Result<(), anyhow::Error> {
1845            message.post(cx, true);
1846            Ok(())
1847        }
1848    }
1849
1850    #[tokio::test]
1851    async fn test_multi_handler_refs() {
1852        let test = MultiValuesTest::new().await;
1853
1854        test.send(123u64);
1855        test.send("foo".to_string());
1856        test.sync().await;
1857        assert_eq!(test.get_values(), (123u64, "foo".to_string()));
1858
1859        let myref: ActorRef<MultiActor> = test.handle.bind();
1860
1861        myref.port().post(&test.client, 321u64);
1862        test.sync().await;
1863        assert_eq!(test.get_values(), (321u64, "foo".to_string()));
1864
1865        myref.port().post(&test.client, "bar".to_string());
1866        test.sync().await;
1867        assert_eq!(test.get_values(), (321u64, "bar".to_string()));
1868    }
1869
1870    #[tokio::test]
1871    async fn test_ref_behavior() {
1872        let test = MultiValuesTest::new().await;
1873
1874        test.send(123u64);
1875        test.send("foo".to_string());
1876
1877        hyperactor::behavior!(MyActorBehavior, u64, String);
1878
1879        let myref: ActorRef<MyActorBehavior> = test.handle.bind();
1880        myref.port().post(&test.client, "biz".to_string());
1881        myref.port().post(&test.client, 999u64);
1882
1883        test.sync().await;
1884        assert_eq!(test.get_values(), (999u64, "biz".to_string()));
1885    }
1886
1887    #[tokio::test]
1888    async fn test_actor_handle_downcast() {
1889        #[derive(Debug, Default)]
1890        struct NothingActor;
1891
1892        impl Actor for NothingActor {}
1893
1894        // Just test that we can round-trip the handle through a downcast.
1895
1896        let proc = Proc::isolated();
1897        let handle = proc.spawn(NothingActor);
1898        let cell = handle.cell();
1899
1900        // Invalid actor doesn't succeed.
1901        assert!(cell.downcast_handle::<EchoActor>().is_none());
1902
1903        let handle = cell.downcast_handle::<NothingActor>().unwrap();
1904        handle.drain_and_stop("test").unwrap();
1905        handle.await;
1906    }
1907
1908    // Returning the sequence number assigned to the message.
1909    #[derive(Debug)]
1910    #[hyperactor::export(handlers = [String, Callback])]
1911    struct GetSeqActor(PortRef<(String, SeqInfo)>);
1912
1913    #[async_trait]
1914    impl Actor for GetSeqActor {}
1915
1916    #[async_trait]
1917    impl Handler<String> for GetSeqActor {
1918        async fn handle(
1919            &mut self,
1920            cx: &Context<Self>,
1921            message: String,
1922        ) -> Result<(), anyhow::Error> {
1923            let Self(port) = self;
1924            let seq_info = cx.headers().get(SEQ_INFO).unwrap();
1925            port.post(cx, (message, seq_info.clone()));
1926            Ok(())
1927        }
1928    }
1929
1930    // Unlike Handler<String>, where the sender provides the string message
1931    // directly, in Handler<Callback>, sender needs to provide a port, and
1932    // handler will reply that port with its own callback port. Then sender can
1933    // send the string message through this callback port.
1934    #[derive(Clone, Debug, Serialize, Deserialize, Named)]
1935    struct Callback(PortRef<PortRef<String>>);
1936
1937    #[async_trait]
1938    impl Handler<Callback> for GetSeqActor {
1939        async fn handle(
1940            &mut self,
1941            cx: &Context<Self>,
1942            message: Callback,
1943        ) -> Result<(), anyhow::Error> {
1944            let (handle, mut receiver) = cx.open_port::<String>();
1945            let callback_ref = handle.bind();
1946            message.0.post(cx, callback_ref);
1947            let msg = receiver.recv().await.unwrap();
1948            self.handle(cx, msg).await
1949        }
1950    }
1951
1952    #[async_timed_test(timeout_secs = 30)]
1953    async fn test_sequencing_actor_handle_basic() {
1954        let proc = Proc::isolated();
1955        let client = proc.client("client");
1956        let (tx, mut rx) = client.open_port();
1957
1958        let actor_handle = proc.spawn(GetSeqActor(tx.bind()));
1959
1960        // Verify that unbound handle can send message.
1961        actor_handle.post(&client, "unbound".to_string());
1962        assert_eq!(
1963            rx.recv().await.unwrap(),
1964            ("unbound".to_string(), SeqInfo::Direct)
1965        );
1966
1967        let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
1968
1969        let session_id = client.sequencer().session_id();
1970        let mut expected_seq = 0;
1971        // Interleave messages sent through the handle and the reference.
1972        for m in 0..10 {
1973            actor_handle.post(&client, format!("{m}"));
1974            expected_seq += 1;
1975            assert_eq!(
1976                rx.recv().await.unwrap(),
1977                (
1978                    format!("{m}"),
1979                    SeqInfo::Session {
1980                        session_id,
1981                        seq: expected_seq,
1982                    }
1983                )
1984            );
1985
1986            for n in 0..2 {
1987                actor_ref.port().post(&client, format!("{m}-{n}"));
1988                expected_seq += 1;
1989                assert_eq!(
1990                    rx.recv().await.unwrap(),
1991                    (
1992                        format!("{m}-{n}"),
1993                        SeqInfo::Session {
1994                            session_id,
1995                            seq: expected_seq,
1996                        }
1997                    )
1998                );
1999            }
2000        }
2001    }
2002
2003    // Test that handler ports share a sequence while non-handler ports get their own.
2004    #[async_timed_test(timeout_secs = 30)]
2005    async fn test_sequencing_mixed_handler_and_non_handler_ports() {
2006        let proc = Proc::isolated();
2007        let client = proc.client("client");
2008
2009        // Port for receiving seq info from actor handler
2010        let (actor_tx, mut actor_rx) = client.open_port();
2011
2012        // Channel for receiving seq info from non-handler port
2013        let (non_handler_tx, mut non_handler_rx) = mpsc::unbounded_channel::<Option<SeqInfo>>();
2014
2015        let actor_handle = proc.spawn(GetSeqActor(actor_tx.bind()));
2016        let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
2017
2018        // Create a non-handler port using open_enqueue_port
2019        let non_handler_tx_clone = non_handler_tx.clone();
2020        let non_handler_port_handle =
2021            client
2022                .mailbox()
2023                .open_enqueue_port(move |headers: Flattrs, _m: ()| {
2024                    let seq_info = headers.get(SEQ_INFO);
2025                    non_handler_tx_clone.send(seq_info).unwrap();
2026                    Ok(())
2027                });
2028
2029        // Bind the port to get a port ID
2030        non_handler_port_handle.bind();
2031        let non_handler_port_id = match non_handler_port_handle.location() {
2032            PortLocation::Bound(port_id) => port_id,
2033            _ => panic!("port_handle should be bound"),
2034        };
2035        assert!(!non_handler_port_id.is_handler_port());
2036
2037        let session_id = client.sequencer().session_id();
2038
2039        // Send to handler ports via ActorHandle - seq 1
2040        actor_handle.post(&client, "msg1".to_string());
2041        assert_eq!(
2042            actor_rx.recv().await.unwrap().1,
2043            SeqInfo::Session { session_id, seq: 1 }
2044        );
2045
2046        // Send to handler ports via ActorRef - seq 2 (shared with ActorHandle)
2047        actor_ref.port().post(&client, "msg2".to_string());
2048        assert_eq!(
2049            actor_rx.recv().await.unwrap().1,
2050            SeqInfo::Session { session_id, seq: 2 }
2051        );
2052
2053        // Send to non-handler port - has its own sequence starting at 1
2054        non_handler_port_handle.post(&client, ());
2055        assert_eq!(
2056            non_handler_rx.recv().await.unwrap(),
2057            Some(SeqInfo::Session { session_id, seq: 1 })
2058        );
2059
2060        // Send more to handler ports via ActorHandle - seq continues at 3
2061        actor_handle.post(&client, "msg3".to_string());
2062        assert_eq!(
2063            actor_rx.recv().await.unwrap().1,
2064            SeqInfo::Session { session_id, seq: 3 }
2065        );
2066
2067        // Send more to non-handler port - its sequence continues at 2
2068        non_handler_port_handle.post(&client, ());
2069        assert_eq!(
2070            non_handler_rx.recv().await.unwrap(),
2071            Some(SeqInfo::Session { session_id, seq: 2 })
2072        );
2073
2074        // Send via ActorRef again - seq 4
2075        actor_ref.port().post(&client, "msg4".to_string());
2076        assert_eq!(
2077            actor_rx.recv().await.unwrap().1,
2078            SeqInfo::Session { session_id, seq: 4 }
2079        );
2080
2081        actor_handle.drain_and_stop("test cleanup").unwrap();
2082        actor_handle.await;
2083    }
2084
2085    // Test that messages from different clients get independent sequence schemes.
2086    #[async_timed_test(timeout_secs = 30)]
2087    async fn test_sequencing_multiple_clients() {
2088        let proc = Proc::isolated();
2089        let client1 = proc.client("client1");
2090        let client2 = proc.client("client2");
2091
2092        // Port for receiving seq info from actor handler
2093        let (tx, mut rx) = client1.open_port();
2094
2095        let actor_handle = proc.spawn(GetSeqActor(tx.bind()));
2096        let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
2097
2098        // Each client should have a different session_id
2099        let session_id_1 = client1.sequencer().session_id();
2100        let session_id_2 = client2.sequencer().session_id();
2101        assert_ne!(session_id_1, session_id_2);
2102
2103        // Send from client1 via ActorHandle - seq 1 for session_id_1
2104        actor_handle.post(&client1, "c1_msg1".to_string());
2105        assert_eq!(
2106            rx.recv().await.unwrap().1,
2107            SeqInfo::Session {
2108                session_id: session_id_1,
2109                seq: 1
2110            }
2111        );
2112
2113        // Send from client2 via ActorHandle - seq 1 for session_id_2 (independent)
2114        actor_handle.post(&client2, "c2_msg1".to_string());
2115        assert_eq!(
2116            rx.recv().await.unwrap().1,
2117            SeqInfo::Session {
2118                session_id: session_id_2,
2119                seq: 1
2120            }
2121        );
2122
2123        // Send from client1 via ActorRef - seq 2 for session_id_1
2124        actor_ref.port().post(&client1, "c1_msg2".to_string());
2125        assert_eq!(
2126            rx.recv().await.unwrap().1,
2127            SeqInfo::Session {
2128                session_id: session_id_1,
2129                seq: 2
2130            }
2131        );
2132
2133        // Send from client2 via ActorRef - seq 2 for session_id_2
2134        actor_ref.port().post(&client2, "c2_msg2".to_string());
2135        assert_eq!(
2136            rx.recv().await.unwrap().1,
2137            SeqInfo::Session {
2138                session_id: session_id_2,
2139                seq: 2
2140            }
2141        );
2142
2143        // Interleave more messages to further verify independence
2144        actor_handle.post(&client1, "c1_msg3".to_string());
2145        assert_eq!(
2146            rx.recv().await.unwrap().1,
2147            SeqInfo::Session {
2148                session_id: session_id_1,
2149                seq: 3
2150            }
2151        );
2152
2153        actor_ref.port().post(&client2, "c2_msg3".to_string());
2154        assert_eq!(
2155            rx.recv().await.unwrap().1,
2156            SeqInfo::Session {
2157                session_id: session_id_2,
2158                seq: 3
2159            }
2160        );
2161
2162        actor_handle.drain_and_stop("test cleanup").unwrap();
2163        actor_handle.await;
2164    }
2165
2166    // Verify that ordering is guarranteed based on
2167    //   * (sender actor , client actor, port stream)
2168    // not
2169    //   * (sender actor, client actor)
2170    //
2171    // For "port stream",
2172    //   * handler ports of the same actor belongs to the same stream;
2173    //   * non-handler port has its independent stream.
2174    //
2175    // Specifically, in this test,
2176    //   * client sends a Callback message to dest actor's handler;
2177    //   * while dest actor is still processing that message, client sends
2178    //     another non-handler message to dest actor.
2179    //
2180    // If the ordering is based on (sender actor, client actor), this test would
2181    // hang, since dest actor is deadlock on waiting for the 2nd message while
2182    // still processing the 2nd message.
2183    //
2184    // But since port stream is also part of the ordering guarrantee, such
2185    // deadlock should not happen.
2186    #[async_timed_test(timeout_secs = 30)]
2187    async fn test_sequencing_actor_handle_callback() {
2188        let config = hyperactor_config::global::lock();
2189        let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
2190
2191        let proc = Proc::isolated();
2192        let client = proc.client("client");
2193        let (tx, mut rx) = client.open_port();
2194
2195        let actor_handle = proc.spawn(GetSeqActor(tx.bind()));
2196        let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
2197
2198        let (callback_tx, mut callback_rx) = client.open_port();
2199        // Client sends the 1st message
2200        actor_ref.post(&client, Callback(callback_tx.bind()));
2201        let msg_port_ref = callback_rx.recv().await.unwrap();
2202        // client sends the 2nd message. At this time, GetSeqActor is still
2203        // processing the 1st message, and waiting for the 2nd message.
2204        msg_port_ref.post(&client, "finally".to_string());
2205
2206        let session_id = client.sequencer().session_id();
2207        // passing this assert means GetSeqActor processed the 2nd message.
2208        assert_eq!(
2209            rx.recv().await.unwrap(),
2210            (
2211                "finally".to_string(),
2212                SeqInfo::Session { session_id, seq: 1 }
2213            )
2214        );
2215    }
2216
2217    // Adding a delay before sending the destination proc. Useful for tests
2218    // requiring latency injection.
2219    #[derive(Clone, Debug)]
2220    struct DelayedMailboxSender {
2221        relay_tx: mpsc::UnboundedSender<MessageEnvelope>,
2222    }
2223
2224    impl DelayedMailboxSender {
2225        // Use a random latency between 0 and 1 second if the plan is empty.
2226        fn new(
2227            // The proc that hosts the dest actor. By posting envelope to this
2228            // proc, this proc will route that evenlope to the dest actor.
2229            dest_proc: Proc,
2230            // Vec index is the message seq - 1, value is the order this message
2231            // would be relayed to the dest actor. Endpoint actor is responsible to
2232            // ensure itself processes these messages in order.
2233            relay_orders: Vec<usize>,
2234        ) -> Self {
2235            let (relay_tx, mut relay_rx) = mpsc::unbounded_channel::<MessageEnvelope>();
2236
2237            tokio::spawn(async move {
2238                let mut buffer = Vec::new();
2239
2240                for _ in 0..relay_orders.len() {
2241                    let envelope = relay_rx.recv().await.unwrap();
2242                    buffer.push(envelope);
2243                }
2244
2245                for m in buffer.clone() {
2246                    let seq = match m.headers().get(SEQ_INFO) {
2247                        Some(SeqInfo::Session { seq, .. }) => seq as usize,
2248                        Some(SeqInfo::Direct) => panic!("expected Session variant"),
2249                        None => panic!("expected seq info"),
2250                    };
2251                    // seq no is one-based.
2252                    let order = relay_orders[seq - 1];
2253                    buffer[order] = m;
2254                }
2255
2256                let dest_proc_clone = dest_proc.clone();
2257                for msg in buffer {
2258                    dest_proc_clone.post(msg, monitored_return_handle());
2259                }
2260            });
2261
2262            Self { relay_tx }
2263        }
2264    }
2265
2266    #[async_trait]
2267    impl MailboxSender for DelayedMailboxSender {
2268        fn post_unchecked(
2269            &self,
2270            envelope: MessageEnvelope,
2271            _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
2272        ) {
2273            self.relay_tx.send(envelope).unwrap();
2274        }
2275    }
2276
2277    async fn assert_out_of_order_delivery(expected: Vec<(String, u64)>, relay_orders: Vec<usize>) {
2278        let local_proc: Proc = Proc::isolated();
2279        let client = local_proc.client("local");
2280        let (tx, mut rx) = client.open_port();
2281
2282        let handle = local_proc.spawn(GetSeqActor(tx.bind()));
2283        let actor_ref: ActorRef<GetSeqActor> = handle.bind();
2284
2285        let remote_proc = Proc::configured(
2286            test_proc_id("remote_0"),
2287            DelayedMailboxSender::new(local_proc.clone(), relay_orders).boxed(),
2288        );
2289        let remote_client = remote_proc.client("remote");
2290        // Send the messages out in the order of their expected sequence numbers.
2291        let mut messages = expected.clone();
2292        messages.sort_by_key(|v| v.1);
2293        for (message, _seq) in messages {
2294            actor_ref.post(&remote_client, message);
2295        }
2296        let session_id = remote_client.sequencer().session_id();
2297        for expect in expected {
2298            let expected = (
2299                expect.0,
2300                SeqInfo::Session {
2301                    session_id,
2302                    seq: expect.1,
2303                },
2304            );
2305            assert_eq!(rx.recv().await.unwrap(), expected);
2306        }
2307
2308        handle.drain_and_stop("test cleanup").unwrap();
2309        handle.await;
2310    }
2311
2312    // Send several messages, use DelayedMailboxSender and the relay orders to
2313    // ensure these messages will arrive at handler's workq out-of-order.
2314    // Then verify the actor handler will still process these messages based on
2315    // their sending order if reordering buffer is enabled.
2316    #[async_timed_test(timeout_secs = 30)]
2317    async fn test_sequencing_actor_ref_known_delivery_order() {
2318        let config = hyperactor_config::global::lock();
2319
2320        // relay order is second, third, first
2321        let relay_orders = vec![2, 0, 1];
2322
2323        // By disabling the actor side re-ordering buffer, the mssages will
2324        // be processed in the same order as they sent out.
2325        let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, false);
2326        assert_out_of_order_delivery(
2327            vec![
2328                ("second".to_string(), 2),
2329                ("third".to_string(), 3),
2330                ("first".to_string(), 1),
2331            ],
2332            relay_orders.clone(),
2333        )
2334        .await;
2335
2336        // By enabling the actor side re-ordering buffer, the mssages will
2337        // be re-ordered before being processed.
2338        let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
2339        assert_out_of_order_delivery(
2340            vec![
2341                ("first".to_string(), 1),
2342                ("second".to_string(), 2),
2343                ("third".to_string(), 3),
2344            ],
2345            relay_orders.clone(),
2346        )
2347        .await;
2348    }
2349
2350    // Send a large nubmer of messages, use DelayedMailboxSender to ensure these
2351    // messages will arrive at handler's workq in a random order. Then verify the
2352    // actor handler will still process these messages based on their sending
2353    // order with reordering buffer enabled.
2354    #[async_timed_test(timeout_secs = 30)]
2355    async fn test_sequencing_actor_ref_random_delivery_order() {
2356        let config = hyperactor_config::global::lock();
2357
2358        // By enabling the actor side re-ordering buffer, the mssages will
2359        // be re-ordered before being processed.
2360        let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
2361        let expected = (0..10000)
2362            .map(|i| (format!("msg{i}"), i + 1))
2363            .collect::<Vec<_>>();
2364
2365        let mut relay_orders: Vec<usize> = (0..10000).collect();
2366        relay_orders.shuffle(&mut rand::rng());
2367        assert_out_of_order_delivery(expected, relay_orders).await;
2368    }
2369
2370    /// Verifies the default blanket introspection handler for a plain
2371    /// actor.
2372    ///
2373    /// This test spawns a simple `EchoActor`, sends it
2374    /// `IntrospectMessage::Query`, and checks that the returned
2375    /// `IntrospectResult` matches the framework’s structural default:
2376    ///
2377    /// - `identity` matches the actor id
2378    /// - `attrs` contains actor-runtime keys (status, actor_type, etc.)
2379    /// - no supervision children are reported
2380    /// - `supervisor` is None because this actor is spawned as a
2381    ///   root/top-level actor in the proc (only supervised child actors
2382    ///   report a supervisor id).
2383    ///
2384    /// This exercises the end-to-end introspect task path rather than
2385    /// calling `live_actor_payload` directly, ensuring the runtime
2386    /// wiring behaves as expected.
2387    #[tokio::test]
2388    async fn test_introspect_query_default_payload() {
2389        let proc = Proc::isolated();
2390        let client = proc.client("client");
2391        let (tx, _rx) = client.open_port::<u64>();
2392        let actor = EchoActor(tx.bind());
2393        let handle = proc.spawn(actor);
2394
2395        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2396        handle.actor_addr().introspect_port().post(
2397            &client,
2398            IntrospectMessage::Query {
2399                view: IntrospectView::Actor,
2400                reply: reply_port.bind(),
2401            },
2402        );
2403        let payload = reply_rx.recv().await.unwrap();
2404
2405        assert_eq!(
2406            payload.identity,
2407            crate::introspect::IntrospectRef::Actor(handle.actor_addr().clone())
2408        );
2409        assert_valid_attrs(&payload);
2410        assert_has_attr(&payload, "status");
2411        assert_has_attr(&payload, "actor_type");
2412        assert_has_attr(&payload, "created_at");
2413        assert!(payload.children.is_empty());
2414        assert!(payload.parent.is_none());
2415
2416        handle.drain_and_stop("test").unwrap();
2417        handle.await;
2418    }
2419
2420    /// Helper: look up an attr in the attrs JSON by short name.
2421    fn attrs_get(attrs_json: &str, short_name: &str) -> Option<serde_json::Value> {
2422        use hyperactor_config::INTROSPECT;
2423        use hyperactor_config::attrs::AttrKeyInfo;
2424        let fq_name = inventory::iter::<AttrKeyInfo>()
2425            .find(|info| {
2426                info.meta
2427                    .get(INTROSPECT)
2428                    .is_some_and(|ia| ia.name == short_name)
2429            })
2430            .map(|info| info.name)?;
2431        let obj: serde_json::Value = serde_json::from_str(attrs_json).ok()?;
2432        obj.get(fq_name).cloned()
2433    }
2434
2435    /// Assert that an IntrospectResult has valid JSON attrs (IA-1).
2436    fn assert_valid_attrs(result: &IntrospectResult) {
2437        let parsed: serde_json::Value =
2438            serde_json::from_str(&result.attrs).expect("attrs must be valid JSON");
2439        assert!(parsed.is_object(), "IA-1: attrs must be a JSON object");
2440    }
2441
2442    /// Assert the actor status attr matches expected value.
2443    fn assert_status(result: &IntrospectResult, expected: &str) {
2444        let status = attrs_get(&result.attrs, "status")
2445            .and_then(|v| v.as_str().map(String::from))
2446            .expect("attrs must contain status");
2447        assert_eq!(status, expected, "unexpected actor status");
2448    }
2449
2450    /// Assert the actor has a specific handler (or None).
2451    fn assert_handler(result: &IntrospectResult, expected: Option<&str>) {
2452        let handler =
2453            attrs_get(&result.attrs, "last_handler").and_then(|v| v.as_str().map(String::from));
2454        assert_eq!(handler.as_deref(), expected);
2455    }
2456
2457    /// Assert the error code attr matches expected value.
2458    fn assert_error_code(result: &IntrospectResult, expected: &str) {
2459        let code = attrs_get(&result.attrs, "error_code")
2460            .and_then(|v| v.as_str().map(String::from))
2461            .expect("attrs must contain error_code");
2462        assert_eq!(code, expected);
2463    }
2464
2465    /// Assert handler does NOT contain a substring.
2466    fn assert_handler_not_contains(result: &IntrospectResult, forbidden: &str) {
2467        if let Some(handler) =
2468            attrs_get(&result.attrs, "last_handler").and_then(|v| v.as_str().map(String::from))
2469        {
2470            assert!(
2471                !handler.contains(forbidden),
2472                "handler should not contain '{}'; got: {}",
2473                forbidden,
2474                handler
2475            );
2476        }
2477    }
2478
2479    /// Assert an attr is present by short name.
2480    fn assert_has_attr(result: &IntrospectResult, short_name: &str) {
2481        assert!(
2482            attrs_get(&result.attrs, short_name).is_some(),
2483            "attrs must contain '{}'",
2484            short_name
2485        );
2486    }
2487
2488    /// Assert status contains a substring (for non-exact checks
2489    /// like "processing" on wedged actors).
2490    fn assert_status_contains(result: &IntrospectResult, substring: &str) {
2491        let status = attrs_get(&result.attrs, "status")
2492            .and_then(|v| v.as_str().map(String::from))
2493            .expect("attrs must contain status");
2494        assert!(
2495            status.contains(substring),
2496            "status should contain '{}'; got: {}",
2497            substring,
2498            status
2499        );
2500    }
2501
2502    /// Assert no status_reason attr (IA-3: non-terminal status).
2503    fn assert_no_status_reason(result: &IntrospectResult) {
2504        assert!(
2505            attrs_get(&result.attrs, "status_reason").is_none(),
2506            "IA-3: must not have status_reason"
2507        );
2508    }
2509
2510    /// Assert a handler is present (any value).
2511    fn assert_has_handler(result: &IntrospectResult) {
2512        assert!(
2513            attrs_get(&result.attrs, "last_handler").is_some(),
2514            "must have a handler"
2515        );
2516    }
2517
2518    /// Assert no failure attrs are present (IA-4).
2519    fn assert_no_failure_attrs(result: &IntrospectResult) {
2520        assert!(
2521            attrs_get(&result.attrs, "failure_error_message").is_none(),
2522            "IA-4: must not have failure attrs"
2523        );
2524    }
2525
2526    /// Establishes IA-1 (attrs-json), IA-3 (status-shape), and
2527    /// IA-4 (failure-shape) for the running-actor path only.
2528    /// Stopped/failed paths need separate tests (see proc.rs
2529    /// terminated snapshot tests).
2530    #[tokio::test]
2531    async fn test_ia1_ia4_running_actor_attrs() {
2532        let proc = Proc::isolated();
2533        let client = proc.client("client");
2534        let (tx, _rx) = client.open_port::<u64>();
2535        let actor = EchoActor(tx.bind());
2536        let handle = proc.spawn(actor);
2537
2538        let payload = crate::introspect::live_actor_payload(handle.cell());
2539
2540        // IA-1: valid JSON.
2541        assert_valid_attrs(&payload);
2542
2543        // IA-3: non-terminal status, no status_reason.
2544        assert_has_attr(&payload, "status");
2545        assert_no_status_reason(&payload);
2546
2547        // IA-4: no failure attrs.
2548        assert_no_failure_attrs(&payload);
2549
2550        handle.drain_and_stop("test").unwrap();
2551        handle.await;
2552    }
2553
2554    /// AS-1 (snapshot-opacity): an INTROSPECT-tagged actor-supplied attr
2555    /// is transported into the Actor view verbatim. `LAST_HANDLER` is
2556    /// INTROSPECT-tagged and left unset by the core builder for a
2557    /// message-free actor, so it shows additive transport cleanly (and
2558    /// satisfies the accessor's INTROSPECT-tagging guard).
2559    #[tokio::test]
2560    async fn test_actor_attrs_snapshot_appears_verbatim() {
2561        let proc = Proc::isolated();
2562        let client = proc.client("client");
2563        let (tx, _rx) = client.open_port::<u64>();
2564        let handle = proc.spawn(EchoActor(tx.bind()));
2565
2566        handle.cell().set_attrs_snapshot(|| {
2567            let mut attrs = hyperactor_config::Attrs::new();
2568            attrs.set(crate::introspect::LAST_HANDLER, "seam-probe".to_string());
2569            attrs
2570        });
2571
2572        let payload = crate::introspect::live_actor_payload(handle.cell());
2573        assert_valid_attrs(&payload);
2574        assert_eq!(
2575            attrs_get(&payload.attrs, "last_handler").and_then(|v| v.as_str().map(String::from)),
2576            Some("seam-probe".to_string()),
2577            "AS-1: actor-supplied attr must appear verbatim"
2578        );
2579
2580        handle.drain_and_stop("test").unwrap();
2581        handle.await;
2582    }
2583
2584    /// AS-2 (core-precedence): on a key collision, the core/runtime
2585    /// value wins over the actor-supplied snapshot.
2586    #[tokio::test]
2587    async fn test_actor_attrs_snapshot_core_wins_on_collision() {
2588        let proc = Proc::isolated();
2589        let client = proc.client("client");
2590        let (tx, _rx) = client.open_port::<u64>();
2591        let handle = proc.spawn(EchoActor(tx.bind()));
2592
2593        // The snapshot tries to clobber a core key with a bogus value;
2594        // `build_actor_attrs` sets `messages_processed` unconditionally,
2595        // so core must win.
2596        handle.cell().set_attrs_snapshot(|| {
2597            let mut attrs = hyperactor_config::Attrs::new();
2598            attrs.set(crate::introspect::MESSAGES_PROCESSED, 999u64);
2599            attrs
2600        });
2601
2602        let payload = crate::introspect::live_actor_payload(handle.cell());
2603        let messages = attrs_get(&payload.attrs, "messages_processed")
2604            .and_then(|v| v.as_u64())
2605            .expect("attrs must contain messages_processed");
2606        // Fresh actor processed no messages: the exact core value is 0,
2607        // and it wins over the snapshot's bogus 999.
2608        assert_eq!(messages, 0, "AS-2: core value must win over the snapshot");
2609
2610        handle.drain_and_stop("test").unwrap();
2611        handle.await;
2612    }
2613
2614    /// AS-3 (snapshot-non-fatal): a panicking snapshot degrades to the
2615    /// core-only Actor view rather than emptying or invalidating attrs.
2616    #[tokio::test]
2617    async fn test_actor_attrs_snapshot_panic_is_non_fatal() {
2618        let proc = Proc::isolated();
2619        let client = proc.client("client");
2620        let (tx, _rx) = client.open_port::<u64>();
2621        let handle = proc.spawn(EchoActor(tx.bind()));
2622
2623        handle
2624            .cell()
2625            .set_attrs_snapshot(|| -> hyperactor_config::Attrs {
2626                panic!("snapshot callback boom")
2627            });
2628
2629        let payload = crate::introspect::live_actor_payload(handle.cell());
2630        // Core view survives a panicking snapshot intact (IA-1, IA-5, AS-3).
2631        assert_valid_attrs(&payload);
2632        assert_has_attr(&payload, "status");
2633        assert_has_attr(&payload, "actor_type");
2634
2635        handle.drain_and_stop("test").unwrap();
2636        handle.await;
2637    }
2638
2639    // Verifies that QueryChild returns an error for actors without
2640    // a registered query_child_handler callback. The runtime
2641    // introspect task responds with the error sentinel payload
2642    // (`identity == ""`, error attrs with code "not_found",
2643    // .. }`).
2644    #[tokio::test]
2645    async fn test_introspect_query_child_not_found() {
2646        let proc = Proc::isolated();
2647        let client = proc.client("client");
2648        let (tx, _rx) = client.open_port::<u64>();
2649        let actor = EchoActor(tx.bind());
2650        let handle = proc.spawn(actor);
2651
2652        let child_ref = crate::Addr::Actor(test_proc_id("nonexistent").actor_addr("child"));
2653        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2654        handle.actor_addr().introspect_port().post(
2655            &client,
2656            IntrospectMessage::QueryChild {
2657                child_ref,
2658                reply: reply_port.bind(),
2659            },
2660        );
2661        let payload = reply_rx.recv().await.unwrap();
2662
2663        assert_eq!(
2664            payload.identity,
2665            crate::introspect::IntrospectRef::Actor(
2666                test_proc_id("nonexistent").actor_addr("child")
2667            )
2668        );
2669        assert_error_code(&payload, "not_found");
2670
2671        handle.drain_and_stop("test").unwrap();
2672        handle.await;
2673    }
2674
2675    // Verifies that with the runtime introspect task, custom
2676    // `handle_introspect` overrides are not called. The runtime
2677    // task intercepts IntrospectMessage before it reaches the
2678    // actor's work queue. An actor with an override still gets
2679    // standard Actor properties from the runtime task.
2680    #[tokio::test]
2681    async fn test_introspect_override() {
2682        #[derive(Debug, Default)]
2683        #[hyperactor::export(handlers = [])]
2684        struct CustomIntrospectActor;
2685
2686        #[async_trait]
2687        impl Actor for CustomIntrospectActor {}
2688
2689        let proc = Proc::isolated();
2690        let client = proc.client("client");
2691        let handle = proc.spawn(CustomIntrospectActor);
2692
2693        handle
2694            .status()
2695            .wait_for(|s| matches!(s, ActorStatus::Idle))
2696            .await
2697            .unwrap();
2698
2699        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2700        handle.actor_addr().introspect_port().post(
2701            &client,
2702            IntrospectMessage::Query {
2703                view: IntrospectView::Actor,
2704                reply: reply_port.bind(),
2705            },
2706        );
2707        let payload = reply_rx.recv().await.unwrap();
2708
2709        // The runtime task returns actor attrs (with status), NOT
2710        // the override's Host properties.
2711        assert_has_attr(&payload, "status");
2712
2713        handle.drain_and_stop("test").unwrap();
2714        handle.await;
2715    }
2716
2717    /// Verifies that a child actor spawned via `spawn_child` reports
2718    /// its parent as `supervisor` in the introspection payload, and
2719    /// that the parent's payload lists the child in `children`.
2720    #[tokio::test]
2721    async fn test_introspect_query_supervision_child() {
2722        let proc = Proc::isolated();
2723        let client = proc.client("client");
2724
2725        // Spawn parent.
2726        let (tx_parent, _rx_parent) = client.open_port::<u64>();
2727        let parent_handle =
2728            proc.spawn_with_label::<EchoActor>("parent", EchoActor(tx_parent.bind()));
2729
2730        // Spawn child under parent.
2731        let (tx_child, _rx_child) = client.open_port::<u64>();
2732        let child_handle =
2733            proc.spawn_child::<EchoActor>(parent_handle.cell().clone(), EchoActor(tx_child.bind()));
2734
2735        // Query the child — supervisor should be the parent.
2736        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2737        child_handle.actor_addr().introspect_port().post(
2738            &client,
2739            IntrospectMessage::Query {
2740                view: IntrospectView::Actor,
2741                reply: reply_port.bind(),
2742            },
2743        );
2744        let child_payload = reply_rx.recv().await.unwrap();
2745
2746        assert_eq!(
2747            child_payload.identity,
2748            crate::introspect::IntrospectRef::Actor(child_handle.actor_addr().clone()),
2749        );
2750        // Verify it has actor attrs (status present).
2751        assert!(
2752            attrs_get(&child_payload.attrs, "status").is_some(),
2753            "child should have actor attrs"
2754        );
2755        assert_eq!(
2756            child_payload.parent,
2757            Some(crate::introspect::IntrospectRef::Actor(
2758                parent_handle.actor_addr().clone()
2759            )),
2760        );
2761
2762        // Query the parent — children should include the child.
2763        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2764        parent_handle.actor_addr().introspect_port().post(
2765            &client,
2766            IntrospectMessage::Query {
2767                view: IntrospectView::Actor,
2768                reply: reply_port.bind(),
2769            },
2770        );
2771        let parent_payload = reply_rx.recv().await.unwrap();
2772
2773        assert!(parent_payload.parent.is_none());
2774        assert!(
2775            parent_payload
2776                .children
2777                .contains(&crate::introspect::IntrospectRef::Actor(
2778                    child_handle.actor_addr().clone()
2779                )),
2780        );
2781
2782        child_handle.drain_and_stop("test").unwrap();
2783        child_handle.await;
2784        parent_handle.drain_and_stop("test").unwrap();
2785        parent_handle.await;
2786    }
2787
2788    /// A freshly spawned actor that has received no user messages
2789    /// reports `last_message_handler == None` — the introspect
2790    /// handler does not leak through. Status is `"idle"` once
2791    /// initialization completes.
2792    #[tokio::test]
2793    async fn test_introspect_fresh_actor_status() {
2794        let proc = Proc::isolated();
2795        let client = proc.client("client");
2796        let (tx, _rx) = client.open_port::<u64>();
2797        let actor = EchoActor(tx.bind());
2798        let handle = proc.spawn(actor);
2799
2800        // Wait for the actor to finish initialization.
2801        handle
2802            .status()
2803            .wait_for(|s| matches!(s, ActorStatus::Idle))
2804            .await
2805            .unwrap();
2806
2807        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2808        handle.actor_addr().introspect_port().post(
2809            &client,
2810            IntrospectMessage::Query {
2811                view: IntrospectView::Actor,
2812                reply: reply_port.bind(),
2813            },
2814        );
2815        let payload = reply_rx.recv().await.unwrap();
2816
2817        assert_status(&payload, "idle");
2818        assert_handler(&payload, None);
2819
2820        handle.drain_and_stop("test").unwrap();
2821        handle.await;
2822    }
2823
2824    /// After processing a user message, the introspect payload reports
2825    /// the user message's handler and post-completion status — not
2826    /// the introspect handler itself (one-behind invariant,
2827    /// after-user-traffic case).
2828    #[tokio::test]
2829    async fn test_introspect_after_user_message() {
2830        let proc = Proc::isolated();
2831        let client = proc.client("client");
2832        let (tx, mut rx) = client.open_port::<u64>();
2833        let actor = EchoActor(tx.bind());
2834        let handle = proc.spawn(actor);
2835
2836        // Send a user message and wait for it to be processed.
2837        handle.post(&client, 42u64);
2838        let _ = rx.recv().await.unwrap();
2839
2840        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2841        handle.actor_addr().introspect_port().post(
2842            &client,
2843            IntrospectMessage::Query {
2844                view: IntrospectView::Actor,
2845                reply: reply_port.bind(),
2846            },
2847        );
2848        let payload = reply_rx.recv().await.unwrap();
2849
2850        assert_status(&payload, "idle");
2851        assert_has_handler(&payload);
2852        assert_handler_not_contains(&payload, "IntrospectMessage");
2853
2854        handle.drain_and_stop("test").unwrap();
2855        handle.await;
2856    }
2857
2858    /// Two consecutive introspect queries: with the runtime
2859    /// introspect task, neither perturbs the actor's state (S2).
2860    /// Both report the same `last_message_handler` for a fresh
2861    /// actor — `None`, not `IntrospectMessage`.
2862    #[tokio::test]
2863    async fn test_introspect_consecutive_queries() {
2864        let proc = Proc::isolated();
2865        let client = proc.client("client");
2866        let (tx, _rx) = client.open_port::<u64>();
2867        let actor = EchoActor(tx.bind());
2868        let handle = proc.spawn(actor);
2869
2870        handle
2871            .status()
2872            .wait_for(|s| matches!(s, ActorStatus::Idle))
2873            .await
2874            .unwrap();
2875
2876        // First introspect query.
2877        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2878        handle.actor_addr().introspect_port().post(
2879            &client,
2880            IntrospectMessage::Query {
2881                view: IntrospectView::Actor,
2882                reply: reply_port.bind(),
2883            },
2884        );
2885        let payload1 = reply_rx.recv().await.unwrap();
2886
2887        // Second introspect query.
2888        let (reply_port2, reply_rx2) = client.open_once_port::<IntrospectResult>();
2889        handle.actor_addr().introspect_port().post(
2890            &client,
2891            IntrospectMessage::Query {
2892                view: IntrospectView::Actor,
2893                reply: reply_port2.bind(),
2894            },
2895        );
2896        let payload2 = reply_rx2.recv().await.unwrap();
2897
2898        // Neither should show IntrospectMessage as the handler.
2899        assert_handler(&payload1, None);
2900        assert_handler(&payload2, None);
2901
2902        handle.drain_and_stop("test").unwrap();
2903        handle.await;
2904    }
2905
2906    // test_published_properties_round_trip removed — replaced by
2907    // test_publish_attrs_round_trip which tests the Attrs-based API.
2908
2909    /// Verify InstanceCell Attrs storage: `set_published_attrs`
2910    /// replaces the whole bag, `merge_published_attr` merges a single
2911    /// key incrementally. (Instance methods are thin wrappers over
2912    /// these.)
2913    #[tokio::test]
2914    async fn test_publish_attrs_round_trip() {
2915        use hyperactor_config::Attrs;
2916        use hyperactor_config::declare_attrs;
2917
2918        declare_attrs! {
2919            attr TEST_KEY_A: String;
2920            attr TEST_KEY_B: u64;
2921        }
2922
2923        let proc = Proc::isolated();
2924        let client = proc.client("client");
2925        let (tx, _rx) = client.open_port::<u64>();
2926        let actor = EchoActor(tx.bind());
2927        let handle = proc.spawn(actor);
2928
2929        // Before publishing, attrs are None.
2930        assert!(handle.cell().published_attrs().is_none());
2931
2932        // publish_attrs: replace entire bag.
2933        let mut attrs = Attrs::new();
2934        attrs.set(TEST_KEY_A, "hello".to_string());
2935        handle.cell().set_published_attrs(attrs);
2936        let published = handle.cell().published_attrs().unwrap();
2937        assert_eq!(published.get(TEST_KEY_A), Some(&"hello".to_string()));
2938
2939        // publish_attr: merge single key into existing bag.
2940        handle.cell().merge_published_attr(TEST_KEY_B, 42u64);
2941        let published = handle.cell().published_attrs().unwrap();
2942        assert_eq!(published.get(TEST_KEY_A), Some(&"hello".to_string()));
2943        assert_eq!(published.get(TEST_KEY_B), Some(&42u64));
2944
2945        // publish_attr: overwrite existing key.
2946        handle
2947            .cell()
2948            .merge_published_attr(TEST_KEY_A, "world".to_string());
2949        let published = handle.cell().published_attrs().unwrap();
2950        assert_eq!(published.get(TEST_KEY_A), Some(&"world".to_string()));
2951
2952        handle.drain_and_stop("test").unwrap();
2953        handle.await;
2954    }
2955
2956    /// Verify the query_child_handler callback: register a callback,
2957    /// invoke it via `query_child()`, and confirm the response.
2958    #[tokio::test]
2959    async fn test_query_child_handler_round_trip() {
2960        let proc = Proc::isolated();
2961        let client = proc.client("client");
2962        let (tx, _rx) = client.open_port::<u64>();
2963        let actor = EchoActor(tx.bind());
2964        let handle = proc.spawn(actor);
2965
2966        // Before registering, query_child returns None.
2967        let test_ref = Addr::Actor(test_proc_id("test").actor_addr("child"));
2968        assert!(handle.cell().query_child(&test_ref).is_none());
2969
2970        // Register a callback.
2971        handle.cell().set_query_child_handler(|child_ref| {
2972            use crate::introspect::IntrospectRef;
2973            let identity = match child_ref {
2974                Addr::Proc(p) => IntrospectRef::Proc(p.clone()),
2975                Addr::Actor(a) => IntrospectRef::Actor(a.clone()),
2976                Addr::Port(p) => IntrospectRef::Actor(p.actor_addr()),
2977            };
2978            IntrospectResult {
2979                identity,
2980                attrs: serde_json::json!({
2981                    "proc_name": "test_proc",
2982                    "num_actors": 42,
2983                })
2984                .to_string(),
2985                children: Vec::new(),
2986                parent: None,
2987                as_of: std::time::SystemTime::now(),
2988            }
2989        });
2990
2991        // Now query_child returns the callback's response.
2992        let payload = handle
2993            .cell()
2994            .query_child(&test_ref)
2995            .expect("query_child must return payload");
2996        assert_eq!(
2997            payload.identity,
2998            crate::introspect::IntrospectRef::Actor(test_proc_id("test").actor_addr("child"))
2999        );
3000        let attrs: serde_json::Value =
3001            serde_json::from_str(&payload.attrs).expect("attrs must be valid JSON");
3002        assert_eq!(
3003            attrs.get("proc_name").and_then(|v| v.as_str()),
3004            Some("test_proc")
3005        );
3006        assert_eq!(attrs.get("num_actors").and_then(|v| v.as_u64()), Some(42));
3007
3008        handle.drain_and_stop("test").unwrap();
3009        handle.await;
3010    }
3011
3012    /// Exercises S1 (see `introspect` module doc).
3013    ///
3014    /// Sends a wedging message, then queries introspect while the
3015    /// actor is blocked. The response must arrive and report live
3016    /// processing status.
3017    #[tokio::test]
3018    async fn test_introspect_wedged() {
3019        #[derive(Debug, Default)]
3020        #[hyperactor::export(handlers = [u64])]
3021        struct WedgedActor;
3022
3023        #[async_trait]
3024        impl Actor for WedgedActor {}
3025
3026        #[async_trait]
3027        impl Handler<u64> for WedgedActor {
3028            async fn handle(
3029                &mut self,
3030                _cx: &Context<Self>,
3031                _message: u64,
3032            ) -> Result<(), anyhow::Error> {
3033                // Block forever.
3034                std::future::pending::<()>().await;
3035                Ok(())
3036            }
3037        }
3038
3039        let proc = Proc::isolated();
3040        let client = proc.client("client");
3041        let handle = proc.spawn(WedgedActor);
3042
3043        // Wait for idle before sending the wedging message.
3044        handle
3045            .status()
3046            .wait_for(|s| matches!(s, ActorStatus::Idle))
3047            .await
3048            .unwrap();
3049
3050        // Send a u64 to wedge the actor in its handler.
3051        handle.post(&client, 1u64);
3052
3053        // Wait for the handler to start blocking.
3054        tokio::time::sleep(Duration::from_millis(50)).await;
3055
3056        // Send introspect query via the dedicated introspect port.
3057        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
3058        handle.actor_addr().introspect_port().post(
3059            &client,
3060            IntrospectMessage::Query {
3061                view: IntrospectView::Actor,
3062                reply: reply_port.bind(),
3063            },
3064        );
3065
3066        // Must not hang — the introspect task runs independently.
3067        let payload = tokio::time::timeout(Duration::from_secs(5), reply_rx.recv())
3068            .await
3069            .expect("introspect should not hang on a wedged actor")
3070            .unwrap();
3071
3072        assert_status_contains(&payload, "processing");
3073        assert_handler_not_contains(&payload, "IntrospectMessage");
3074    }
3075
3076    /// Exercises S2 (see `introspect` module doc).
3077    ///
3078    /// After a user message, two consecutive introspect queries both
3079    /// report the user message handler.
3080    #[tokio::test]
3081    async fn test_introspect_no_perturbation() {
3082        let proc = Proc::isolated();
3083        let client = proc.client("client");
3084        let (tx, mut rx) = client.open_port::<u64>();
3085        let actor = EchoActor(tx.bind());
3086        let handle = proc.spawn(actor);
3087
3088        // Wait for idle before sending the user message.
3089        handle
3090            .status()
3091            .wait_for(|s| matches!(s, ActorStatus::Idle))
3092            .await
3093            .unwrap();
3094
3095        // Send a user message and wait for it to be processed.
3096        handle.post(&client, 42u64);
3097        let _ = rx.recv().await.unwrap();
3098
3099        // First introspect query.
3100        let (reply_port1, reply_rx1) = client.open_once_port::<IntrospectResult>();
3101        handle.actor_addr().introspect_port().post(
3102            &client,
3103            IntrospectMessage::Query {
3104                view: IntrospectView::Actor,
3105                reply: reply_port1.bind(),
3106            },
3107        );
3108        let payload1 = reply_rx1.recv().await.unwrap();
3109
3110        // Second introspect query.
3111        let (reply_port2, reply_rx2) = client.open_once_port::<IntrospectResult>();
3112        handle.actor_addr().introspect_port().post(
3113            &client,
3114            IntrospectMessage::Query {
3115                view: IntrospectView::Actor,
3116                reply: reply_port2.bind(),
3117            },
3118        );
3119        let payload2 = reply_rx2.recv().await.unwrap();
3120
3121        // Both should report the user message handler, not IntrospectMessage.
3122        assert_handler_not_contains(&payload1, "IntrospectMessage");
3123        assert_handler_not_contains(&payload2, "IntrospectMessage");
3124        // Consecutive queries must agree (compare parsed, not raw
3125        // strings — HashMap key ordering is non-deterministic).
3126        let attrs1: serde_json::Value = serde_json::from_str(&payload1.attrs).unwrap();
3127        let attrs2: serde_json::Value = serde_json::from_str(&payload2.attrs).unwrap();
3128        assert_eq!(attrs1, attrs2, "consecutive queries should be identical");
3129
3130        handle.drain_and_stop("test").unwrap();
3131        handle.await;
3132    }
3133
3134    /// Exercises CI-1 (see `proc` module doc).
3135    ///
3136    /// Unlike a plain `client()`, which drops the introspect
3137    /// receiver so queries are silently discarded, an
3138    /// `introspectable_instance` has a live `serve_introspect` task
3139    /// and is fully navigable in admin tooling.
3140    #[tokio::test]
3141    async fn test_introspectable_instance_responds_to_query() {
3142        let proc = Proc::isolated();
3143        let (bridge, handle) = proc.introspectable_instance("bridge").unwrap();
3144        let actor_id: crate::ActorAddr = handle.actor_addr().clone();
3145
3146        let (reply_port, reply_rx) = bridge.open_once_port::<IntrospectResult>();
3147        actor_id.introspect_port().post(
3148            &bridge,
3149            IntrospectMessage::Query {
3150                view: IntrospectView::Actor,
3151                reply: reply_port.bind(),
3152            },
3153        );
3154        let payload = reply_rx.recv().await.unwrap();
3155
3156        // CI-1: introspectable_instance reports status "client"
3157        // and actor_type "()" (the unit type).
3158        assert_eq!(
3159            payload.identity,
3160            crate::introspect::IntrospectRef::Actor(actor_id.clone())
3161        );
3162        assert_status(&payload, "client");
3163        let actor_type =
3164            attrs_get(&payload.attrs, "actor_type").and_then(|v| v.as_str().map(String::from));
3165        assert_eq!(
3166            actor_type.as_deref(),
3167            Some("()"),
3168            "CI-1: actor_type must be \"()\""
3169        );
3170    }
3171
3172    /// Contrast with CI-1: a plain `client()` does NOT respond to
3173    /// `IntrospectMessage::Query`. Its introspect receiver is dropped
3174    /// in `Proc::client()`, so the message is silently discarded
3175    /// and the reply port never receives a value.
3176    ///
3177    /// Callers that need TUI visibility must use
3178    /// `introspectable_instance` instead.
3179    #[tokio::test]
3180    async fn test_instance_does_not_respond_to_query() {
3181        let proc = Proc::isolated();
3182        let client = proc.client("client");
3183        let mailbox = proc.client("mailbox");
3184        let mailbox_id: crate::ActorAddr = mailbox.self_addr().clone();
3185
3186        let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
3187        mailbox_id.introspect_port().post(
3188            &client,
3189            IntrospectMessage::Query {
3190                view: IntrospectView::Actor,
3191                reply: reply_port.bind(),
3192            },
3193        );
3194
3195        // The introspect receiver was dropped in `client()`, so the
3196        // message is silently discarded and the reply never arrives.
3197        let result = tokio::time::timeout(Duration::from_millis(100), reply_rx.recv()).await;
3198        assert!(
3199            result.is_err(),
3200            "client() must not respond to IntrospectMessage (introspect receiver dropped)"
3201        );
3202    }
3203
3204    /// Exercises CI-2 (see `proc` module doc).
3205    ///
3206    /// Dropping the instance shuts down and joins `serve_introspect`
3207    /// before terminal status is published.
3208    #[tokio::test]
3209    async fn test_introspectable_instance_snapshot_on_drop() {
3210        let proc = Proc::isolated();
3211        let (instance, handle) = proc.introspectable_instance("bridge").unwrap();
3212        let actor_id = handle.actor_addr().clone();
3213
3214        assert!(
3215            proc.all_actor_ids().contains(&actor_id),
3216            "should appear in all_actor_ids while live"
3217        );
3218
3219        drop(instance);
3220        handle.await;
3221
3222        let snapshot = proc.terminated_snapshot(&actor_id).unwrap();
3223        let actor_status = attrs_get(&snapshot.attrs, "status")
3224            .and_then(|v| v.as_str().map(String::from))
3225            .expect("snapshot attrs must contain status");
3226        assert!(
3227            actor_status.starts_with("stopped"),
3228            "CI-2: snapshot actor_status should be stopped, got: {}",
3229            actor_status
3230        );
3231    }
3232
3233    #[test]
3234    fn zombie_status_is_not_terminal() {
3235        let status = ActorStatus::zombie("hard kill did not finish");
3236
3237        assert!(!status.is_terminal(), "zombie status is not terminal");
3238        assert!(status.is_stopping(), "zombie status is stopping");
3239        assert!(status.is_zombie(), "zombie predicate should match");
3240    }
3241}