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