Skip to main content

hyperactor/
proc.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//! [`Proc`] is an addressable actor-runtime boundary.
10//!
11//! It owns actor lifecycle (spawn, run, terminate), delivers messages
12//! to its local actors, and hosts supervision state. A proc is not a
13//! network endpoint by itself: it reaches—and is reached by—other procs
14//! through a [`Gateway`](crate::gateway::Gateway), its connectivity
15//! boundary (see [`Proc::gateway`]). Outbound messages for remote
16//! destinations flow through that gateway, which delivers locally,
17//! forwards to a known peer, or routes onward. A host attaches all of
18//! its procs to one shared gateway.
19//!
20//! It also stores bounded snapshots of terminated actors for
21//! post-mortem introspection.
22//!
23//! ## Client instance invariants (CI-*)
24//!
25//! - **CI-1 (client status):** `IntrospectMessage::Query` on an
26//!   introspectable instance returns `status: "client"` and
27//!   `actor_type: "()"` in attrs.
28//! - **CI-2 (snapshot on drop):** Dropping the returned `Instance<()>`
29//!   transitions its status to terminal, causing the introspect task
30//!   to store a terminated snapshot.
31//!
32//! ## Actor identity invariants (AI-*)
33//!
34//! - **AI-1 (named-child uid):** Each child gets a globally unique
35//!   random uid. Named children carry a label for display purposes.
36//! - **AI-3 (controller ActorAddr uniqueness):** Each named child gets
37//!   a unique uid; the label is informational only.
38//!
39//! ## Flight recorder span invariants (FR-*)
40//!
41//! - **FR-1 (recording-span route equivalence):**
42//!   `Instance::recording_span()` returns a span bound to the same
43//!   actor-local `Recording` consumed by handler instrumentation and
44//!   introspection. Events emitted under that span land in the same
45//!   flight-recorder ring buffer returned by `introspect_payload()`.
46//! - **FR-2 (recording-span rootness):** Every span returned by
47//!   `Instance::recording_span()` is a fresh root span (`parent:
48//!   None`). Ambient tracing context does not cause events emitted
49//!   under that span to route into a parent actor's flight recorder.
50//! - **FR-3 (fresh-handle, stable-destination):** Repeated calls to
51//!   `Instance::recording_span()` return distinct span handles, but
52//!   all target the same underlying actor recording.
53//!
54//! ## Queue depth accounting invariants (PD-5*)
55//!
56//! - **PD-5a:** Per-actor queue depth counts accepted handler work
57//!   not yet dequeued by the actor loop. Accounting increments in
58//!   `HandlerPorts::get`'s enqueue closure *before* the reorder-buffer
59//!   decision, so this counter includes both in-order messages waiting
60//!   in `work_rx` AND out-of-order messages held in the receiver-local
61//!   reorder buffer. (Reflected at the introspection layer in IO-3 of
62//!   the `introspect` module doc.)
63//! - **PD-5b:** Queue depth is incremented exactly once per accepted
64//!   message in the enqueue closure of `HandlerPorts::get`, before
65//!   the in-order / out-of-order branch.
66//! - **PD-5c:** Queue depth is decremented exactly once on every
67//!   dequeue from `work_rx` (in the actor `run` loop).
68//! - **PD-5d:** Queue depth is intended to be non-negative; tests
69//!   must cover ordered/buffered delivery paths to validate the
70//!   accounting.
71//! - **PD-5e:** `queue_depth` and the OTel `ACTOR_MESSAGE_QUEUE_SIZE`
72//!   counter are two consumers of one accounting path. The
73//!   `account_enqueue` / `account_dequeue` helpers update both
74//!   together so they cannot drift.
75//!
76//! ## Retained queue-pressure invariants (PD-6 through PD-9)
77//!
78//! `ProcQueueStats` holds proc-level retained evidence of queue
79//! pressure. These are runtime-driven (not publish-time sampled)
80//! so they capture between-publish bursts.
81//!
82//! - **PD-6:** `high_water_mark >= running_total` eventually.
83//!   Because `running_total` is incremented before `high_water_mark`
84//!   is updated, a concurrent reader may transiently observe
85//!   `total > high_water_mark`. This is a sampling artifact, not
86//!   an accounting error.
87//! - **PD-7:** `last_nonzero_age_ms() == None` iff proc queue
88//!   depth has never been non-zero since startup. The timestamp
89//!   is updated on enqueue and on dequeue when the queue remains
90//!   non-zero, so it reflects the last observed non-zero state.
91//! - **PD-8:** transient bursts that drain before publish still
92//!   update both the high-water mark and the last-nonzero state.
93//! - **PD-9:** `last_nonzero_age_ms()` is expected to be
94//!   non-decreasing during quiet periods, but this is not a hard
95//!   guarantee — the implementation uses `SystemTime` (wall clock),
96//!   which can move backward on NTP adjustments. Callers should
97//!   treat the age as best-effort telemetry, not a monotonic
98//!   invariant.
99
100use std::any::Any;
101use std::any::TypeId;
102use std::collections::BTreeMap;
103use std::collections::HashMap;
104use std::fmt;
105use std::future::Future;
106use std::ops::Deref;
107use std::panic;
108use std::panic::AssertUnwindSafe;
109use std::panic::Location as PanicLocation;
110use std::pin::Pin;
111use std::sync::Arc;
112use std::sync::Condvar;
113use std::sync::Mutex;
114use std::sync::OnceLock;
115use std::sync::RwLock;
116use std::sync::Weak;
117use std::sync::atomic::AtomicBool;
118use std::sync::atomic::AtomicU64;
119use std::sync::atomic::AtomicUsize;
120use std::sync::atomic::Ordering;
121use std::time::Duration;
122use std::time::Instant;
123use std::time::SystemTime;
124
125use async_trait::async_trait;
126use dashmap::DashMap;
127use dashmap::DashSet;
128use dashmap::mapref::entry::Entry;
129use dashmap::mapref::multiple::RefMulti;
130use futures::FutureExt;
131use hyperactor_config::Flattrs;
132use hyperactor_telemetry::ActorStatusEvent;
133use hyperactor_telemetry::generate_actor_status_event_id;
134use hyperactor_telemetry::hash_to_u64;
135use hyperactor_telemetry::notify_actor_status_changed;
136use hyperactor_telemetry::notify_message;
137use hyperactor_telemetry::notify_message_status;
138use hyperactor_telemetry::recorder::Recording;
139use serde::Deserialize;
140use serde::Serialize;
141use tokio::sync::Notify;
142use tokio::sync::mpsc;
143use tokio::sync::oneshot;
144use tokio::sync::watch;
145use tokio::task::JoinHandle;
146use tracing::Instrument;
147use tracing::Span;
148use typeuri::Named;
149use uuid::Uuid;
150use wirevalue::TypeInfo;
151
152use crate as hyperactor;
153use crate::Actor;
154use crate::ActorAddr;
155use crate::ActorRef;
156use crate::Addr;
157use crate::Data;
158use crate::Handler;
159use crate::Location;
160use crate::Message;
161use crate::PortAddr;
162use crate::PortRef;
163use crate::ProcAddr;
164use crate::ProcId;
165use crate::RemoteMessage;
166use crate::actor::ActorError;
167use crate::actor::ActorErrorKind;
168use crate::actor::ActorHandle;
169use crate::actor::ActorStatus;
170use crate::actor::ActorStoppingReason;
171use crate::actor::AnyActorHandle;
172use crate::actor::Binds;
173use crate::actor::HandlerInfo;
174use crate::actor::Referable;
175use crate::actor::RemoteHandles;
176use crate::actor::Signal;
177use crate::actor::StopMode;
178use crate::actor_local::ActorLocalStorage;
179use crate::channel;
180use crate::channel::ChannelAddr;
181use crate::channel::ChannelError;
182use crate::client::Client;
183use crate::client::ClientActor;
184use crate::config;
185use crate::context;
186use crate::context::Mailbox as _;
187use crate::endpoint::Endpoint as _;
188use crate::gateway::Gateway;
189use crate::id::ActorId;
190use crate::id::Label;
191use crate::id::Uid;
192use crate::introspect::IntrospectMessage;
193use crate::introspect::IntrospectResult;
194use crate::mailbox::BoxedMailboxSender;
195use crate::mailbox::DeliveryFailure;
196use crate::mailbox::DialMailboxRouter;
197use crate::mailbox::IntoBoxedMailboxSender as _;
198use crate::mailbox::Mailbox;
199use crate::mailbox::MailboxMuxer;
200use crate::mailbox::MailboxSender;
201use crate::mailbox::MessageEnvelope;
202use crate::mailbox::OncePortHandle;
203use crate::mailbox::OncePortReceiver;
204use crate::mailbox::PortGone;
205use crate::mailbox::PortHandle;
206use crate::mailbox::PortReceiver;
207use crate::mailbox::PortSender as _;
208use crate::mailbox::TransportFailure;
209use crate::mailbox::TransportFailureReason;
210use crate::mailbox::Undeliverable;
211use crate::mailbox::UndeliverableReason;
212use crate::metrics::ACTOR_MESSAGE_HANDLER_DURATION;
213use crate::metrics::ACTOR_MESSAGE_QUEUE_SIZE;
214use crate::metrics::ACTOR_MESSAGES_RECEIVED;
215use crate::port::Port;
216use crate::subject::AsSubject as _;
217
218tokio::task_local! {
219    static CURRENT_TASK_PROC: Proc;
220}
221
222/// Legacy singleton proc name used for host-local client actors.
223///
224/// This is not a true singleton: every host may have a `local` proc, so local
225/// delivery must compare both proc id and location for this id.
226pub const LEGACY_LOCAL_PROC_NAME: &str = "local";
227
228/// Legacy singleton proc name used for host system actors.
229///
230/// This is not a true singleton: every host may have a `service` proc, so
231/// local delivery must compare both proc id and location for this id.
232pub const LEGACY_SERVICE_PROC_NAME: &str = "service";
233
234/// Returns current epoch-millis from wall clock. Used by
235/// `ProcQueueStats` for timestamp recording. In tests, override
236/// via `ProcQueueStats::with_clock` to get deterministic behavior.
237fn wall_clock_epoch_ms() -> u64 {
238    std::time::SystemTime::now()
239        .duration_since(std::time::UNIX_EPOCH)
240        .unwrap_or_default()
241        .as_millis() as u64
242}
243
244/// Proc-level retained queue-pressure state (PD-6 through PD-9).
245///
246/// Runtime-driven and updated from the enqueue/dequeue accounting
247/// path, not from publish-time sampling. These metrics preserve
248/// between-publish queue-pressure evidence that instantaneous
249/// sampling misses.
250pub(crate) struct ProcQueueStats {
251    /// Proc-wide running total of queued work items. Incremented on
252    /// enqueue, decremented on dequeue. O(1) alternative to iterating
253    /// per-actor depths.
254    running_total: AtomicU64,
255    /// Maximum proc-wide queue depth observed since startup (PD-6).
256    high_water_mark: AtomicU64,
257    /// Epoch-millis of the most recent moment when proc-wide queue
258    /// depth was observed non-zero (PD-7). Sentinel 0 means never.
259    /// Updated on enqueue and on dequeue when the queue remains
260    /// non-zero, so the age reflects the last observed non-zero
261    /// state rather than merely the last enqueue.
262    last_nonzero_epoch_ms: AtomicU64,
263    /// Clock function for timestamps. Defaults to `wall_clock_epoch_ms`.
264    /// Tests can override via `with_clock` for deterministic behavior.
265    clock: fn() -> u64,
266}
267
268impl ProcQueueStats {
269    fn new() -> Self {
270        Self {
271            running_total: AtomicU64::new(0),
272            high_water_mark: AtomicU64::new(0),
273            last_nonzero_epoch_ms: AtomicU64::new(0),
274            clock: wall_clock_epoch_ms,
275        }
276    }
277
278    /// Create with a custom clock for testing.
279    #[cfg(test)]
280    fn with_clock(clock: fn() -> u64) -> Self {
281        Self {
282            running_total: AtomicU64::new(0),
283            high_water_mark: AtomicU64::new(0),
284            last_nonzero_epoch_ms: AtomicU64::new(0),
285            clock,
286        }
287    }
288
289    /// Current epoch-millis from this instance's clock.
290    fn now_ms(&self) -> u64 {
291        (self.clock)()
292    }
293
294    /// Current proc-wide running total.
295    pub(crate) fn running_total(&self) -> u64 {
296        self.running_total.load(Ordering::Relaxed)
297    }
298
299    /// Maximum proc-wide queue depth since startup (PD-6).
300    pub(crate) fn high_water_mark(&self) -> u64 {
301        self.high_water_mark.load(Ordering::Relaxed)
302    }
303
304    /// How long ago proc-wide queue depth was last observed non-zero
305    /// (PD-7). `None` means no counted actor work has traversed the
306    /// queue accounting path since startup. Uses the configured clock
307    /// (wall clock in production, injectable in tests).
308    pub(crate) fn last_nonzero_age_ms(&self) -> Option<u64> {
309        let ts = self.last_nonzero_epoch_ms.load(Ordering::Relaxed);
310        if ts == 0 {
311            return None;
312        }
313        Some(self.now_ms().saturating_sub(ts))
314    }
315}
316
317/// Single accounting path for actor work-queue enqueue.
318///
319/// Updates three consumers together: per-actor `queue_depth`,
320/// proc-level retained queue-pressure state (`ProcQueueStats`),
321/// and OTel `ACTOR_MESSAGE_QUEUE_SIZE`. Unifying the update
322/// here ensures they cannot drift.
323fn account_enqueue(queue_depth: &AtomicU64, proc_stats: &ProcQueueStats, actor_id: &str) {
324    queue_depth.fetch_add(1, Ordering::Relaxed);
325    let new_total = proc_stats.running_total.fetch_add(1, Ordering::Relaxed) + 1;
326    // PD-6: update high-water mark.
327    proc_stats
328        .high_water_mark
329        .fetch_max(new_total, Ordering::Relaxed);
330    // PD-7: record that the proc is non-zero right now.
331    proc_stats
332        .last_nonzero_epoch_ms
333        .store(proc_stats.now_ms(), Ordering::Relaxed);
334    ACTOR_MESSAGE_QUEUE_SIZE.add(
335        1,
336        hyperactor_telemetry::kv_pairs!("actor_id" => actor_id.to_owned()),
337    );
338}
339
340/// Single accounting path for actor work-queue dequeue.
341///
342/// Updates per-actor `queue_depth`, proc-level running total,
343/// OTel `ACTOR_MESSAGE_QUEUE_SIZE`, and the last-nonzero
344/// timestamp when the proc-wide queue remains non-zero after
345/// this dequeue.
346fn account_dequeue(queue_depth: &AtomicU64, proc_stats: &ProcQueueStats, actor_id: &str) {
347    queue_depth.fetch_sub(1, Ordering::Relaxed);
348    let prev_total = proc_stats.running_total.fetch_sub(1, Ordering::Relaxed);
349    // PD-7: if the queue is still non-zero after this dequeue,
350    // update the timestamp so last_nonzero_age_ms reflects
351    // "last observed non-zero state," not just "last enqueue."
352    if prev_total > 1 {
353        proc_stats
354            .last_nonzero_epoch_ms
355            .store(proc_stats.now_ms(), Ordering::Relaxed);
356    }
357    ACTOR_MESSAGE_QUEUE_SIZE.add(
358        -1,
359        hyperactor_telemetry::kv_pairs!("actor_id" => actor_id.to_owned()),
360    );
361}
362
363/// Roll back an accounted enqueue when the underlying send fails.
364///
365/// Must be paired with a prior `account_enqueue` that has not yet
366/// been balanced by `account_dequeue`. Decrements per-actor
367/// `queue_depth`, proc-level `running_total`, and OTel
368/// `ACTOR_MESSAGE_QUEUE_SIZE` symmetrically. Leaves
369/// `high_water_mark` alone (monotonic by design) and does not
370/// touch `last_nonzero_epoch_ms` (best-effort observational
371/// timestamp; brief overcount on failed sends is acceptable).
372fn account_cancel_enqueue(queue_depth: &AtomicU64, proc_stats: &ProcQueueStats, actor_id: &str) {
373    queue_depth.fetch_sub(1, Ordering::Relaxed);
374    proc_stats.running_total.fetch_sub(1, Ordering::Relaxed);
375    ACTOR_MESSAGE_QUEUE_SIZE.add(
376        -1,
377        hyperactor_telemetry::kv_pairs!("actor_id" => actor_id.to_owned()),
378    );
379}
380
381use crate::ordering::SEQ_INFO;
382use crate::ordering::SeqInfo;
383use crate::ordering::Sequencer;
384use crate::panic_handler;
385use crate::sequenced::SequencedEnvelope;
386use crate::sequenced::SequencedReceiver;
387use crate::sequenced::sequenced_unbounded_with_buffering;
388use crate::supervision::ActorSupervisionEvent;
389
390/// A proc instance is the runtime managing a single proc in Hyperactor.
391/// It is responsible for spawning actors in the proc, multiplexing messages
392/// to/within actors in the proc, and providing fallback routing to external
393/// procs.
394///
395/// Procs are also responsible for maintaining the local supervision hierarchy.
396#[derive(Clone)]
397pub struct Proc {
398    inner: Arc<ProcState>,
399}
400
401impl fmt::Debug for Proc {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        f.debug_struct("Proc")
404            .field("proc_id", &self.inner.proc_id)
405            .finish()
406    }
407}
408
409struct ProcState {
410    /// The proc's runtime identity. This should be globally unique,
411    /// but is not (yet) for local-only procs.
412    proc_id: ProcId,
413
414    /// Shared ingress, egress, and advertised reachability state.
415    gateway: Gateway,
416
417    /// A muxer instance that has entries for every actor managed by
418    /// the proc.
419    proc_muxer: MailboxMuxer,
420
421    /// Reserved root actor uids. Prevents races between concurrent
422    /// `allocate_root_id` callers — insert returns false if the uid
423    /// was already reserved.
424    reserved_roots: DashSet<crate::id::Uid>,
425
426    /// Reserved explicit child actor uids. Prevents races between concurrent
427    /// `gspawn_uid` callers with the same uid.
428    reserved_child_uids: DashSet<crate::id::Uid>,
429
430    /// All actor instances in this proc.
431    instances: DashMap<ActorId, WeakInstanceCell>,
432
433    /// Root actor ids in this proc, tracked independently from uid shape.
434    root_actors: DashSet<ActorId>,
435
436    /// Proc-level queue-pressure accounting (PD-6 through PD-9).
437    /// Runtime-driven — updated from `account_enqueue` /
438    /// `account_dequeue`, not from publish-time sampling.
439    /// `Arc`-wrapped so `HandlerPorts<A>` enqueue closures can share it.
440    queue_stats: Arc<ProcQueueStats>,
441
442    /// Snapshots of terminated actors for post-mortem introspection.
443    /// Populated by the introspect task just before it exits on
444    /// terminal status. Bounded by
445    /// [`config::TERMINATED_SNAPSHOT_RETENTION`].
446    terminated_snapshots: DashMap<ActorId, TerminatedSnapshot>,
447
448    /// Terminal statuses for actors that existed on this proc.
449    /// Note: this map is retained for the lifetime of the process; thus
450    /// tombstones will grow with the number of actors that have ever existed
451    /// in the proc.
452    actor_tombstones: DashMap<ActorId, ActorStatus>,
453
454    /// Used by root actors to send events to the actor coordinating
455    /// supervision of root actors in this proc. Stored as a [`PortRef`] so
456    /// the coordinator may live in another proc — e.g. a process monitor
457    /// supervising this proc from its parent — reached over the gateway.
458    supervision_coordinator_port: OnceLock<PortRef<ActorSupervisionEvent>>,
459
460    /// The actor ID of the supervision coordinator, if it lives on this proc.
461    /// Used to ensure the coordinator is shut down last during proc teardown.
462    supervision_coordinator_actor_id: OnceLock<ActorAddr>,
463
464    /// Handle to the mailbox server task, if this proc was created with
465    /// `Proc::direct()` or had `serve()` called on it. Used to
466    /// gracefully stop the server and join it (flushing receive-side
467    /// acks) during shutdown.
468    mailbox_server_handle: std::sync::Mutex<Option<crate::mailbox::MailboxServerHandle>>,
469
470    /// Detach guard for this proc's attachment to `gateway`. Held here
471    /// so that dropping the proc removes its entry from the gateway
472    /// without a separate explicit step. Wrapped in a `OnceLock`
473    /// because the guard is constructed *after* the `ProcState` Arc
474    /// exists (so we can pass `&proc` to `gateway.attach_proc`); it is
475    /// set exactly once during construction and never read by anyone
476    /// outside of drop ordering.
477    _attached_proc_guard: OnceLock<crate::gateway::AttachedProcGuard>,
478}
479
480struct TerminatedSnapshot {
481    actor_addr: ActorAddr,
482    payload: crate::introspect::IntrospectResult,
483}
484
485/// Actor status control-plane message.
486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Named)]
487pub enum StatusMessage {
488    /// Return the destination actor's current or tombstoned status.
489    GetStatus {
490        /// Reply port receiving `None` for an unknown actor and
491        /// `Some(status)` for a known actor.
492        reply: crate::OncePortRef<Option<ActorStatus>>,
493    },
494}
495wirevalue::register_type!(StatusMessage);
496
497struct StatusSender(WeakProc);
498
499impl StatusSender {
500    fn new(weak_proc: WeakProc) -> Self {
501        Self(weak_proc)
502    }
503}
504
505#[async_trait]
506impl MailboxSender for StatusSender {
507    fn post_unchecked(
508        &self,
509        envelope: MessageEnvelope,
510        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
511    ) {
512        let reply = match envelope.deserialized() {
513            Ok(StatusMessage::GetStatus { reply }) => reply,
514            Err(err) => {
515                let target = envelope.dest().clone();
516                let failure =
517                    DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
518                        target,
519                        TransportFailureReason::LinkUnavailable(format!(
520                            "status message deserialization failed: {err}"
521                        )),
522                    )));
523                envelope.undeliverable(failure, return_handle);
524                return;
525            }
526        };
527
528        let Some(proc) = self.0.upgrade() else {
529            let failure = DeliveryFailure::new(UndeliverableReason::PortGone(PortGone::new(
530                envelope.dest().clone(),
531                envelope.data().typename().map(str::to_string),
532            )));
533            envelope.undeliverable(failure, return_handle);
534            return;
535        };
536
537        let actor_id = envelope.dest().actor_id().clone();
538        let status = proc.status_for_actor(&actor_id);
539
540        if let Err(err) =
541            proc.serialize_and_send_once(reply, status, crate::mailbox::monitored_return_handle())
542        {
543            tracing::error!("status reply failed: {err}");
544        }
545    }
546}
547
548impl Drop for ProcState {
549    fn drop(&mut self) {
550        // We only want log ProcStatus::Dropped when ProcState is dropped,
551        // rather than Proc is dropped. This is because we need to wait for
552        // Proc::inner's ref count becomes 0.
553        let proc_addr = self.proc_addr();
554        tracing::info!(
555            subject = %proc_addr.subject(),
556            name = "ProcStatus",
557            status = "Dropped"
558        );
559    }
560}
561
562impl ProcState {
563    /// The proc's effective advertised location.
564    fn default_location(&self) -> Location {
565        self.gateway.default_location()
566    }
567
568    fn set_default_location(&self, location: Location) {
569        self.gateway.set_default_location(location)
570    }
571
572    fn proc_addr(&self) -> ProcAddr {
573        ProcAddr::new(self.proc_id.clone(), self.gateway.default_location())
574    }
575}
576
577/// Structured return type for [`Proc::actor_instance`].
578///
579/// Groups the instance, handle, and per-channel receivers that an
580/// "inverted" actor caller needs to drive the actor manually.
581pub struct ActorInstance<A: Actor> {
582    /// The actor instance (used for sending/receiving messages, spawning children, etc.).
583    pub instance: Instance<A>,
584    /// Handle to the actor (used for lifecycle control and port access).
585    pub handle: ActorHandle<A>,
586    /// Supervision events delivered to this actor.
587    pub supervision: mpsc::UnboundedReceiver<ActorSupervisionEvent>,
588    /// Control signals for the actor.
589    pub signal: mpsc::UnboundedReceiver<Signal>,
590    /// Primary work queue for handler dispatch.
591    pub work: ActorWorkReceiver<A>,
592}
593
594/// Receiver for actor handler work.
595pub struct ActorWorkReceiver<A: Actor> {
596    inner: SequencedReceiver<SequencedEnvelope<WorkCell<A>>>,
597}
598
599impl<A: Actor> fmt::Debug for ActorWorkReceiver<A> {
600    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601        f.debug_struct("ActorWorkReceiver").finish_non_exhaustive()
602    }
603}
604
605impl<A: Actor> ActorWorkReceiver<A> {
606    fn new(inner: SequencedReceiver<SequencedEnvelope<WorkCell<A>>>) -> Self {
607        Self { inner }
608    }
609
610    /// Receive the next deliverable handler work item.
611    pub async fn recv(&mut self) -> Option<WorkCell<A>> {
612        self.inner.recv().await
613    }
614
615    /// Try to receive the next deliverable handler work item without waiting.
616    pub fn try_recv(&mut self) -> Result<WorkCell<A>, mpsc::error::TryRecvError> {
617        self.inner.try_recv()
618    }
619}
620
621/// Builder for constructing a [`Proc`] with explicit identity and connectivity.
622pub struct Builder<State = GlobalGateway> {
623    proc_id: Option<ProcId>,
624    state: State,
625}
626
627/// Builder state that attaches the proc to the process-wide global gateway.
628pub struct GlobalGateway;
629
630/// Builder state that attaches the proc to a shared gateway.
631pub struct SharedGateway {
632    gateway: Gateway,
633}
634
635/// Builder state that creates a private gateway with a custom forwarder.
636pub struct PrivateGateway {
637    forwarder: BoxedMailboxSender,
638}
639
640impl Builder<GlobalGateway> {
641    /// Create a new proc builder.
642    pub fn new() -> Self {
643        Self {
644            proc_id: None,
645            state: GlobalGateway,
646        }
647    }
648
649    /// Attach the proc to a shared gateway.
650    pub fn shared_gateway(self, gateway: Gateway) -> Builder<SharedGateway> {
651        Builder {
652            proc_id: self.proc_id,
653            state: SharedGateway { gateway },
654        }
655    }
656
657    /// Use a private gateway with the provided forwarder.
658    pub fn private_gateway(self, forwarder: BoxedMailboxSender) -> Builder<PrivateGateway> {
659        Builder {
660            proc_id: self.proc_id,
661            state: PrivateGateway { forwarder },
662        }
663    }
664
665    /// Build the proc.
666    ///
667    /// Procs built on the global gateway must not use legacy
668    /// pseudo-singleton ids — those ids are reserved for in-host
669    /// service/local procs whose location is bound to the host's
670    /// frontend address. Use [`Builder::shared_gateway`] (passing the
671    /// host's gateway) to construct legacy-id procs.
672    pub fn build(self) -> Result<Proc, anyhow::Error> {
673        let proc_id = self
674            .proc_id
675            .unwrap_or_else(|| ProcId::new(Uid::anonymous(), None));
676        if is_legacy_pseudo_singleton_proc_id(&proc_id) {
677            anyhow::bail!(
678                "legacy pseudo-singleton proc id '{}' is reserved for host-scoped construction; use Builder::shared_gateway",
679                proc_id
680            );
681        }
682        Ok(Proc::from_parts_unchecked(
683            proc_id,
684            Gateway::global().clone(),
685        ))
686    }
687}
688
689impl<State> Builder<State> {
690    /// Set the proc identity.
691    pub fn proc_id(mut self, proc_id: ProcId) -> Self {
692        self.proc_id = Some(proc_id);
693        self
694    }
695
696    fn build_proc(proc_id: Option<ProcId>, gateway: Gateway) -> Result<Proc, anyhow::Error> {
697        let proc_id = proc_id.unwrap_or_else(ProcId::anonymous);
698        Ok(Proc::from_parts_unchecked(proc_id, gateway))
699    }
700}
701
702impl Builder<SharedGateway> {
703    /// Build the proc. Accepts any proc id, including legacy
704    /// pseudo-singleton ids, because the caller has explicit control
705    /// over the gateway's default location.
706    pub fn build(self) -> Result<Proc, anyhow::Error> {
707        let Builder {
708            proc_id,
709            state: SharedGateway { gateway },
710        } = self;
711        Self::build_proc(proc_id, gateway)
712    }
713}
714
715impl Builder<PrivateGateway> {
716    /// Build the proc.
717    pub fn build(self) -> Result<Proc, anyhow::Error> {
718        let Builder {
719            proc_id,
720            state: PrivateGateway { forwarder },
721        } = self;
722        let gateway = Gateway::configured(channel::reserve_local_addr().into(), forwarder);
723        Self::build_proc(proc_id, gateway)
724    }
725}
726
727impl Proc {
728    fn from_parts_unchecked(proc_id: ProcId, gateway: Gateway) -> Self {
729        let proc_addr = ProcAddr::new(proc_id.clone(), gateway.default_location());
730        tracing::info!(
731            subject = %proc_addr.subject(),
732            name = "ProcStatus",
733            status = "Created"
734        );
735
736        let proc = Self {
737            inner: Arc::new(ProcState {
738                proc_id: proc_id.clone(),
739                gateway: gateway.clone(),
740                proc_muxer: MailboxMuxer::new(),
741                reserved_roots: DashSet::new(),
742                reserved_child_uids: DashSet::new(),
743                instances: DashMap::new(),
744                root_actors: DashSet::new(),
745                queue_stats: Arc::new(ProcQueueStats::new()),
746                terminated_snapshots: DashMap::new(),
747                actor_tombstones: DashMap::new(),
748                supervision_coordinator_port: OnceLock::new(),
749                supervision_coordinator_actor_id: OnceLock::new(),
750                mailbox_server_handle: std::sync::Mutex::new(None),
751                _attached_proc_guard: OnceLock::new(),
752            }),
753        };
754        let bound = proc
755            .inner
756            .proc_muxer
757            .bind_status(StatusSender::new(proc.downgrade()));
758        assert!(
759            bound,
760            "fresh proc muxer must not have a status control port"
761        );
762        // Attach to the gateway now that the `Arc<ProcState>` exists;
763        // the returned guard's drop will remove the entry when the last
764        // `Proc` referencing this state is dropped.
765        let guard = gateway.attach_proc(&proc);
766        proc.inner
767            ._attached_proc_guard
768            .set(guard)
769            .expect("fresh ProcState's attached-proc guard slot is empty");
770        proc
771    }
772
773    fn status_for_actor(&self, actor_id: &ActorId) -> Option<ActorStatus> {
774        let live_status = self
775            .inner
776            .instances
777            .get(actor_id)
778            .and_then(|entry| entry.value().upgrade())
779            .map(|cell| cell.status().borrow().clone());
780        live_status.or_else(|| {
781            self.inner
782                .actor_tombstones
783                .get(actor_id)
784                .map(|entry| entry.value().clone())
785        })
786    }
787
788    fn from_parts(proc_id: ProcId, gateway: Gateway) -> Self {
789        assert_not_legacy_pseudo_singleton_proc_id(&proc_id);
790        Self::from_parts_unchecked(proc_id, gateway)
791    }
792
793    /// Create the legacy host-local client proc pseudo-singleton on
794    /// a fresh gateway whose forwarder is `forwarder`.
795    pub fn legacy_local_pseudo_singleton(addr: ChannelAddr, forwarder: BoxedMailboxSender) -> Self {
796        Self::legacy_local_pseudo_singleton_on_gateway(Gateway::configured(addr.into(), forwarder))
797    }
798
799    /// Create the legacy host system proc pseudo-singleton on a
800    /// fresh gateway whose forwarder is `forwarder`.
801    pub fn legacy_service_pseudo_singleton(
802        addr: ChannelAddr,
803        forwarder: BoxedMailboxSender,
804    ) -> Self {
805        Self::legacy_service_pseudo_singleton_on_gateway(Gateway::configured(
806            addr.into(),
807            forwarder,
808        ))
809    }
810
811    /// Create the legacy host-local client proc pseudo-singleton on
812    /// the provided shared gateway.
813    pub fn legacy_local_pseudo_singleton_on_gateway(gateway: Gateway) -> Self {
814        Self::legacy_pseudo_singleton_on_gateway(LEGACY_LOCAL_PROC_NAME, gateway)
815    }
816
817    /// Create the legacy host system proc pseudo-singleton on the
818    /// provided shared gateway.
819    pub fn legacy_service_pseudo_singleton_on_gateway(gateway: Gateway) -> Self {
820        Self::legacy_pseudo_singleton_on_gateway(LEGACY_SERVICE_PROC_NAME, gateway)
821    }
822
823    fn legacy_pseudo_singleton_on_gateway(name: &'static str, gateway: Gateway) -> Self {
824        let proc_id = ProcId::singleton(Label::strip(name));
825        Self::from_parts_unchecked(proc_id, gateway)
826    }
827
828    /// Create a proc with an anonymous instance id on the default gateway.
829    pub fn anonymous() -> Self {
830        Self::builder()
831            .build()
832            .expect("anonymous proc builder is valid")
833    }
834
835    /// Create a proc with an instance id and display label on the default gateway.
836    pub fn instance(label: impl AsRef<str>) -> Self {
837        Self::builder()
838            .proc_id(ProcId::instance(Label::strip(label.as_ref())))
839            .build()
840            .expect("instance proc builder is valid")
841    }
842
843    /// Create a proc with a singleton id on the default gateway.
844    pub fn singleton(name: impl AsRef<str>) -> Self {
845        Self::builder()
846            .proc_id(ProcId::singleton(Label::strip(name.as_ref())))
847            .build()
848            .expect("singleton proc builder is valid")
849    }
850
851    /// Create a proc with a random id on a fresh local-only gateway.
852    pub fn isolated() -> Self {
853        Self::builder()
854            .shared_gateway(Gateway::isolated())
855            .build()
856            .expect("isolated proc builder is valid")
857    }
858
859    /// Create a proc builder.
860    pub fn builder() -> Builder {
861        Builder::new()
862    }
863
864    /// Create a pre-configured proc with the given proc id and forwarder.
865    pub fn configured(proc_id: impl Into<ProcAddr>, forwarder: BoxedMailboxSender) -> Self {
866        let proc_addr = proc_id.into();
867        Self::from_parts(
868            proc_addr.id().clone(),
869            Gateway::configured(proc_addr.location().clone(), forwarder),
870        )
871    }
872
873    /// Create a new direct-addressed proc.
874    ///
875    /// The provided name is a display label. Direct procs are otherwise
876    /// independent instances, so each one receives a unique proc id.
877    pub fn direct(addr: ChannelAddr, name: String) -> Result<Self, ChannelError> {
878        let (addr, rx) = channel::serve(addr)?;
879        let proc_id = ProcAddr::instance(addr, name);
880        let proc = Self::builder()
881            .proc_id(proc_id.id().clone())
882            .shared_gateway(Gateway::configured(
883                proc_id.location().clone(),
884                DialMailboxRouter::new().into_boxed(),
885            ))
886            .build()
887            .expect("direct proc builder is valid");
888        let handle = proc.gateway().serve_rx(rx);
889        *proc.inner.mailbox_server_handle.lock().unwrap() = Some(handle);
890        Ok(proc)
891    }
892
893    /// Set the supervision coordinator's port for this proc. Return Err if it is
894    /// already set.
895    pub fn set_supervision_coordinator(
896        &self,
897        port: PortRef<ActorSupervisionEvent>,
898    ) -> Result<(), anyhow::Error> {
899        let actor_ref: ActorAddr = port.port_addr().actor_addr();
900        self.state()
901            .supervision_coordinator_port
902            .set(port)
903            .map_err(|existing| anyhow::anyhow!("coordinator port is already set to {existing}"))?;
904        let _ = self.state().supervision_coordinator_actor_id.set(actor_ref);
905        Ok(())
906    }
907
908    /// The actor address of the supervision coordinator, if one is set and
909    /// lives on this proc.
910    pub fn supervision_coordinator_actor_addr(&self) -> Option<&ActorAddr> {
911        self.state().supervision_coordinator_actor_id.get()
912    }
913
914    /// Handle a supervision event received by the proc. Attempt to forward it to the
915    /// supervision coordinator port if one is set, otherwise crash the process.
916    pub fn handle_unhandled_supervision_event(
917        &self,
918        cx: &impl context::Actor,
919        event: ActorSupervisionEvent,
920    ) {
921        let result = match self.state().supervision_coordinator_port.get() {
922            Some(port) => {
923                port.post(cx, event.clone());
924                Ok(())
925            }
926            None => {
927                if !event.is_error() {
928                    // Normal lifecycle events (e.g. clean stop) without a coordinator
929                    // are silently dropped.
930                    return;
931                }
932                Err(anyhow::anyhow!(
933                    "coordinator port is not set for proc {}",
934                    self.proc_addr(),
935                ))
936            }
937        };
938        if let Err(err) = result {
939            if !event.is_error() {
940                // Normal lifecycle events that fail to send (e.g. coordinator
941                // mailbox already closed during shutdown) are silently dropped.
942                tracing::debug!(
943                    subject = %self.proc_addr().subject(),
944                    "dropping non-error supervision event {}: {:?}",
945                    event,
946                    err
947                );
948                return;
949            }
950            tracing::error!(
951                subject = %self.proc_addr().subject(),
952                "could not propagate supervision event {} due to error: {:?}: crashing",
953                event,
954                err
955            );
956
957            std::process::exit(1);
958        }
959    }
960
961    /// The proc's runtime identity.
962    pub fn proc_id(&self) -> &ProcId {
963        &self.state().proc_id
964    }
965
966    /// The proc's default advertised location.
967    pub fn default_location(&self) -> Location {
968        self.state().default_location()
969    }
970
971    /// Set the proc's default advertised location.
972    pub fn set_default_location(&self, location: Location) {
973        self.state().set_default_location(location)
974    }
975
976    /// The proc's routeable address using its default advertised location.
977    pub fn proc_addr(&self) -> ProcAddr {
978        self.state().proc_addr()
979    }
980
981    /// The proc's connectivity boundary.
982    pub fn gateway(&self) -> Gateway {
983        self.state().gateway.clone()
984    }
985
986    /// Return the process-global proc.
987    pub fn global() -> Self {
988        static GLOBAL_PROC: OnceLock<Proc> = OnceLock::new();
989        GLOBAL_PROC
990            .get_or_init(|| {
991                let label = global_proc_label();
992                Proc::instance(label.as_str())
993            })
994            .clone()
995    }
996
997    /// Return the proc for the current execution context.
998    ///
999    /// Actor callbacks run with their owning proc installed as the current
1000    /// proc. Outside an actor callback, this returns the process-global proc.
1001    pub fn current() -> Self {
1002        CURRENT_TASK_PROC
1003            .try_with(Clone::clone)
1004            .unwrap_or_else(|_| Self::global())
1005    }
1006
1007    async fn with_current<F>(&self, future: F) -> F::Output
1008    where
1009        F: Future,
1010    {
1011        CURRENT_TASK_PROC.scope(self.clone(), future).await
1012    }
1013
1014    /// Shared sender used by the proc to forward messages to remote
1015    /// destinations.
1016    pub fn forwarder(&self) -> BoxedMailboxSender {
1017        self.state().gateway.forwarder()
1018    }
1019
1020    /// The proc's mailbox muxer, which routes messages to actors
1021    /// registered on this proc.
1022    pub fn muxer(&self) -> &MailboxMuxer {
1023        &self.inner.proc_muxer
1024    }
1025
1026    /// Convenience accessor for state.
1027    fn state(&self) -> &ProcState {
1028        self.inner.as_ref()
1029    }
1030
1031    /// Attach a mailbox to the proc with the provided root name.
1032    pub fn attach(&self, name: &str) -> Result<Mailbox, anyhow::Error> {
1033        let actor_id: ActorAddr = self.allocate_root_id(name)?;
1034        Ok(self.bind_mailbox(actor_id))
1035    }
1036
1037    /// Attach a mailbox to the proc as a child actor.
1038    pub fn attach_child(&self, parent_id: &ActorAddr) -> Result<Mailbox, anyhow::Error> {
1039        let actor_id: ActorAddr = self.allocate_anonymous_child_id(parent_id);
1040        Ok(self.bind_mailbox(actor_id))
1041    }
1042
1043    /// Bind a mailbox to the proc.
1044    fn bind_mailbox(&self, actor_id: ActorAddr) -> Mailbox {
1045        let mbox = Mailbox::new(actor_id);
1046
1047        // TODO: T210748165 tie the muxer entry to the lifecycle of the mailbox held
1048        // by the caller. This will likely require a weak reference.
1049        self.state().proc_muxer.bind_mailbox(mbox.clone());
1050        mbox
1051    }
1052
1053    /// Attach a mailbox to the proc with the provided root name, and bind an [`ActorAddr`].
1054    /// This is intended only for testing, and will be replaced by simpled utilities.
1055    pub fn attach_actor<R, M>(
1056        &self,
1057        name: &str,
1058    ) -> Result<(Client, ActorRef<R>, PortReceiver<M>), anyhow::Error>
1059    where
1060        M: RemoteMessage,
1061        R: Referable + RemoteHandles<M>,
1062    {
1063        let client = self.client(name);
1064        let (_handle, rx) = client.bind_handler_port::<M>();
1065        let actor_ref = ActorRef::attest(client.self_addr().clone());
1066        Ok((client, actor_ref, rx))
1067    }
1068
1069    /// Spawn a root actor with a fresh uid labeled from the actor type.
1070    pub fn spawn<A: Actor>(&self, actor: A) -> ActorHandle<A> {
1071        let actor_id: ActorAddr = self.allocate_root_type::<A>();
1072        self.spawn_inner(actor_id, actor, None)
1073    }
1074
1075    /// Spawn a root actor with a fresh uid carrying a display label.
1076    ///
1077    /// The label is descriptive only and does not participate in actor
1078    /// identity.
1079    pub fn spawn_with_label<A: Actor>(&self, label: &str, actor: A) -> ActorHandle<A> {
1080        let actor_id: ActorAddr = self.allocate_root_label(label);
1081        self.spawn_inner(actor_id, actor, None)
1082    }
1083
1084    /// Spawn a root actor on this proc using an explicit uid.
1085    ///
1086    /// This is the explicit identity API, and the only root spawn API that
1087    /// permits singleton actor identity. The uid must be unique among root
1088    /// actors on this proc. Instance labels, if present, are descriptive only
1089    /// and do not affect uniqueness.
1090    pub fn spawn_with_uid<A: Actor>(
1091        &self,
1092        uid: Uid,
1093        actor: A,
1094    ) -> Result<ActorHandle<A>, anyhow::Error> {
1095        let actor_id: ActorAddr = self.allocate_root_uid(uid)?;
1096        Ok(self.spawn_inner(actor_id, actor, None))
1097    }
1098
1099    /// Common spawn logic for both root and child actors.
1100    fn spawn_inner<A: Actor>(
1101        &self,
1102        actor_id: ActorAddr,
1103        actor: A,
1104        parent: Option<InstanceCell>,
1105    ) -> ActorHandle<A> {
1106        let (instance, receivers) = Instance::new(self.clone(), actor_id, false, parent);
1107        instance.start(actor, receivers)
1108    }
1109
1110    /// Create a lightweight client instance (no actor loop, no
1111    /// introspect task).  This is safe to call outside a Tokio
1112    /// runtime — unlike [`actor_instance`], it never calls
1113    /// `tokio::spawn`.
1114    pub fn client(&self, label: &str) -> Client {
1115        let actor_id = self.allocate_client_id(label);
1116        let (instance, _receivers) =
1117            Instance::<ClientActor>::new(self.clone(), actor_id, false, None);
1118        instance.change_status(ActorStatus::Client);
1119        Client::new(instance)
1120    }
1121
1122    /// Create a lightweight client instance that handles
1123    /// [`IntrospectMessage`].
1124    ///
1125    /// Like [`client`](Self::client), this creates a client-mode
1126    /// instance with no actor message loop. Unlike `client`, it
1127    /// spawns a dedicated introspect task, so the instance responds
1128    /// to `IntrospectMessage::Query` and is visible and navigable in
1129    /// admin tooling such as the mesh TUI.
1130    ///
1131    /// See CI-1, CI-2 in module doc.
1132    ///
1133    /// Requires an active Tokio runtime (calls `tokio::spawn`).
1134    pub fn introspectable_instance(
1135        &self,
1136        name: &str,
1137    ) -> Result<(Instance<()>, ActorHandle<()>), anyhow::Error> {
1138        let actor_id: ActorAddr = self.allocate_root_id(name)?;
1139        let (instance, receivers) = Instance::new(self.clone(), actor_id, false, None);
1140        let handle = ActorHandle::new(instance.inner.cell.clone(), instance.inner.ports.clone());
1141        instance.change_status(ActorStatus::Client);
1142        instance.spawn_detached_introspect(receivers.introspect);
1143        Ok((instance, handle))
1144    }
1145
1146    /// Create and return an actor instance, its handle, and its
1147    /// receivers. This allows actors to be "inverted": the caller can
1148    /// use the returned [`Instance`] to send and receive messages,
1149    /// launch child actors, etc. The actor itself does not handle any
1150    /// messages unless driven by the caller.
1151    pub fn actor_instance<A: Actor>(&self, name: &str) -> Result<ActorInstance<A>, anyhow::Error> {
1152        let actor_id: ActorAddr = self.allocate_root_id(name)?;
1153        let span = tracing::debug_span!(
1154            "actor_instance",
1155            subject = %actor_id.subject(),
1156        );
1157        let _guard = span.enter();
1158        let (instance, receivers) = Instance::new(self.clone(), actor_id.clone(), false, None);
1159        let handle = ActorHandle::new(instance.inner.cell.clone(), instance.inner.ports.clone());
1160        instance.change_status(ActorStatus::Client);
1161
1162        instance.spawn_detached_introspect(receivers.introspect);
1163
1164        let (signal_rx, supervision_rx) = receivers.actor_loop.unwrap();
1165        Ok(ActorInstance {
1166            instance,
1167            handle,
1168            supervision: supervision_rx,
1169            signal: signal_rx,
1170            work: receivers.work,
1171        })
1172    }
1173
1174    /// Traverse all actor trees in this proc, starting from root actors.
1175    pub fn traverse<F>(&self, f: &mut F)
1176    where
1177        F: FnMut(&InstanceCell, usize),
1178    {
1179        for entry in self.state().root_actors.iter() {
1180            if let Some(cell) = self.get_instance_by_id(entry.key()) {
1181                cell.traverse(f);
1182            }
1183        }
1184    }
1185
1186    /// Proc-wide running total of queued work items.
1187    pub fn queue_depth_total(&self) -> u64 {
1188        self.state().queue_stats.running_total()
1189    }
1190
1191    /// Maximum proc-wide queue depth observed since startup (PD-6).
1192    pub fn queue_depth_high_water_mark(&self) -> u64 {
1193        self.state().queue_stats.high_water_mark()
1194    }
1195
1196    /// How long ago proc-wide queue depth was last non-zero (PD-7).
1197    pub fn last_nonzero_queue_depth_age_ms(&self) -> Option<u64> {
1198        self.state().queue_stats.last_nonzero_age_ms()
1199    }
1200
1201    /// Look up an instance by ActorAddr.
1202    pub fn get_instance(&self, actor_id: &ActorAddr) -> Option<InstanceCell> {
1203        self.get_instance_by_id(actor_id.id())
1204    }
1205
1206    /// Look up an instance by ActorId.
1207    pub fn get_instance_by_id(&self, actor_id: &ActorId) -> Option<InstanceCell> {
1208        self.state()
1209            .instances
1210            .get(actor_id)
1211            .and_then(|cell| cell.upgrade())
1212    }
1213
1214    /// Returns the ActorAddrs of all root actors in this proc.
1215    pub fn root_actor_ids(&self) -> Vec<ActorAddr> {
1216        self.state()
1217            .root_actors
1218            .iter()
1219            .filter_map(|entry| {
1220                self.get_instance_by_id(entry.key())
1221                    .map(|cell| cell.actor_addr().clone())
1222            })
1223            .collect()
1224    }
1225
1226    /// Returns the ActorAddrs of all live actors in this proc, including
1227    /// dynamically spawned children.
1228    ///
1229    /// An actor is considered live if its weak reference is
1230    /// upgradeable and its status is not terminal. This excludes
1231    /// actors whose `InstanceCell` has been dropped and actors that
1232    /// have stopped or failed but whose Arc is still held (e.g. by
1233    /// the introspect task during teardown).
1234    pub fn all_actor_ids(&self) -> Vec<ActorAddr> {
1235        self.state()
1236            .instances
1237            .iter()
1238            .filter_map(|entry| {
1239                let cell = entry.value().upgrade()?;
1240                (!cell.status().borrow().is_terminal()).then(|| cell.actor_addr().clone())
1241            })
1242            .collect()
1243    }
1244
1245    /// Snapshot all instance ids from the DashMap without inspecting
1246    /// values. Each shard read lock is held only long enough to clone
1247    /// the id — no `Weak::upgrade()`, no `watch::borrow()`, no
1248    /// `is_terminal()` check. This minimises shard lock hold time to
1249    /// avoid convoy starvation with concurrent `insert`/`remove`
1250    /// operations during rapid actor churn.
1251    ///
1252    /// The returned list may include actors that are terminal or whose
1253    /// `WeakInstanceCell` no longer upgrades. Callers should tolerate stale
1254    /// ids (e.g. by handling "not found" on subsequent per-actor lookups).
1255    pub fn all_instance_keys(&self) -> Vec<ActorId> {
1256        self.state()
1257            .instances
1258            .iter()
1259            .map(|entry| entry.key().clone())
1260            .collect()
1261    }
1262
1263    /// Look up a terminated actor's snapshot by ID.
1264    pub fn terminated_snapshot(
1265        &self,
1266        actor_id: &ActorAddr,
1267    ) -> Option<crate::introspect::IntrospectResult> {
1268        self.state()
1269            .terminated_snapshots
1270            .get(actor_id.id())
1271            .map(|entry| entry.value().payload.clone())
1272    }
1273
1274    /// Return all terminated actor IDs currently retained.
1275    pub fn all_terminated_actor_ids(&self) -> Vec<ActorAddr> {
1276        self.state()
1277            .terminated_snapshots
1278            .iter()
1279            .map(|entry| entry.value().actor_addr.clone())
1280            .collect()
1281    }
1282
1283    /// Create a child instance. Called from `Instance`.
1284    fn child_instance(&self, parent: InstanceCell) -> (Instance<()>, ActorHandle<()>) {
1285        let actor_id = self.allocate_anonymous_child_id(parent.actor_addr());
1286        let _ = tracing::debug_span!(
1287            "child_actor_instance",
1288            subject = %actor_id.subject(),
1289        );
1290
1291        let (instance, _receivers) = Instance::new(self.clone(), actor_id, false, Some(parent));
1292        // Client-mode instance: no actor loop, no introspect task.
1293        // Receivers are intentionally dropped.
1294        let handle = ActorHandle::new(instance.inner.cell.clone(), instance.inner.ports.clone());
1295        instance.change_status(ActorStatus::Client);
1296        (instance, handle)
1297    }
1298
1299    /// Spawn a child actor from the provided parent on this proc. The parent actor
1300    /// must already belong to this proc, a fact which is asserted in code.
1301    ///
1302    /// When spawn_child returns, the child has an associated cell and is linked
1303    /// with its parent.
1304    pub(crate) fn spawn_child<A: Actor>(&self, parent: InstanceCell, actor: A) -> ActorHandle<A> {
1305        let actor_id = self.allocate_child_id::<A>(parent.actor_addr());
1306        self.spawn_inner(actor_id, actor, Some(parent))
1307    }
1308
1309    /// Spawn a child actor from the provided parent using an explicit uid.
1310    pub(crate) fn spawn_child_with_uid<A: Actor>(
1311        &self,
1312        parent: InstanceCell,
1313        uid: Uid,
1314        actor: A,
1315    ) -> Result<ActorHandle<A>, anyhow::Error> {
1316        let actor_id = self.ensure_child_uid(parent.actor_addr(), uid)?;
1317        Ok(self.spawn_inner(actor_id, actor, Some(parent)))
1318    }
1319
1320    /// Spawn a named child actor. Same as `spawn_child` but the child
1321    /// gets a descriptive name instead of inheriting the parent's.
1322    /// Supervision linkage to parent is preserved.
1323    pub(crate) fn spawn_named_child<A: Actor>(
1324        &self,
1325        parent: InstanceCell,
1326        name: &str,
1327        actor: A,
1328    ) -> ActorHandle<A> {
1329        let actor_id = self.allocate_named_child_id(parent.actor_addr(), name);
1330        self.spawn_inner(actor_id, actor, Some(parent))
1331    }
1332
1333    /// Call `abort` on the `JoinHandle` associated with the given
1334    /// root actor. If successful return `Some(root.clone())` else
1335    /// `None`.
1336    pub fn abort_root_actor(&self, root: &ActorId) -> Option<impl Future<Output = ActorAddr>> {
1337        self.state()
1338            .instances
1339            .get(root)
1340            .into_iter()
1341            .flat_map(|entry| entry.value().upgrade())
1342            .map(|cell| {
1343                let actor_addr = cell.actor_addr().clone();
1344                let r1 = actor_addr.clone();
1345                let r2 = actor_addr;
1346                // `Instance::start()` is infallible and should
1347                // complete quickly, so calling `wait()` on `actor_task_handle`
1348                // should be safe (i.e., not hang forever).
1349                async move {
1350                    tokio::task::spawn_blocking(move || {
1351                        let h = cell.inner.actor_task_handle.wait();
1352                        tracing::debug!("{}: aborting {:?}", r1, h);
1353                        h.abort();
1354                    })
1355                    .await
1356                    .unwrap();
1357                    r2
1358                }
1359            })
1360            .next()
1361    }
1362
1363    /// Signals to a root actor to stop,
1364    /// returning a status observer if successful.
1365    pub fn stop_actor(
1366        &self,
1367        actor_id: &ActorId,
1368        reason: String,
1369    ) -> Option<watch::Receiver<ActorStatus>> {
1370        // Upgrade the weak ref and immediately drop the DashMap entry (read
1371        // guard) before doing anything with `cell`. InstanceCellState::drop
1372        // calls instances.remove(), which needs a write lock on the same shard.
1373        // Holding the read guard while cell drops would self-deadlock.
1374        let cell = match self.state().instances.get(actor_id) {
1375            None => {
1376                tracing::error!(subject = %self.proc_addr().subject(), "no actor {} found", actor_id);
1377                return None;
1378            }
1379            Some(entry) => entry.value().upgrade(),
1380        }; // entry (shard read lock) dropped here
1381        match cell {
1382            None => None, // the actor's cell has been dropped
1383            Some(cell) => {
1384                tracing::info!("sending stop signal to {}", cell.actor_addr());
1385                if let Err(err) = cell.signal(Signal::DrainAndStop(reason)) {
1386                    tracing::error!(
1387                        "failed to send stop signal to uid {}: {:?}",
1388                        cell.uid(),
1389                        err
1390                    );
1391                    None
1392                } else {
1393                    Some(cell.status().clone())
1394                }
1395            }
1396        }
1397    }
1398
1399    /// Stop the proc. Returns a pair of:
1400    /// - the actors observed to stop;
1401    /// - the actors not observed to stop when timeout.
1402    #[hyperactor::instrument(fields(subject = self.proc_addr().subject().to_string()))]
1403    pub async fn destroy_and_wait(
1404        &mut self,
1405        timeout: Duration,
1406        reason: &str,
1407    ) -> Result<(Vec<ActorAddr>, Vec<ActorAddr>), anyhow::Error> {
1408        tracing::debug!("proc stopping");
1409
1410        let coordinator_id = self.supervision_coordinator_actor_addr().cloned();
1411
1412        // Phase 1: stop all root actors except the supervision coordinator
1413        // (which must stay alive to receive stop events from the others).
1414        let mut statuses = HashMap::new();
1415        for actor_id in self
1416            .state()
1417            .root_actors
1418            .iter()
1419            .filter_map(|entry| self.get_instance_by_id(entry.key()))
1420            .filter(|cell| !matches!(*cell.status().borrow(), ActorStatus::Client))
1421            .map(|cell| cell.actor_addr().clone())
1422            .collect::<Vec<_>>()
1423        {
1424            if coordinator_id.as_ref() == Some(&actor_id) {
1425                continue;
1426            }
1427            if let Some(status) = self.stop_actor(actor_id.id(), reason.to_string()) {
1428                statuses.insert(actor_id, status);
1429            }
1430        }
1431        tracing::debug!("non-coordinator actors stopped");
1432
1433        let waits: Vec<_> = statuses
1434            .iter_mut()
1435            .map(|(actor_id, root)| {
1436                let actor_id = actor_id.clone();
1437                async move {
1438                    tokio::time::timeout(
1439                        timeout,
1440                        root.wait_for(|state: &ActorStatus| state.is_terminal()),
1441                    )
1442                    .await
1443                    .ok()
1444                    .map(|_| actor_id)
1445                }
1446            })
1447            .collect();
1448
1449        let results = futures::future::join_all(waits).await;
1450        let mut stopped_actors: Vec<_> = results
1451            .iter()
1452            .filter_map(|actor_id| actor_id.as_ref())
1453            .cloned()
1454            .collect();
1455        let aborted_actors: Vec<_> = statuses
1456            .iter()
1457            .filter(|(actor_id, _)| !stopped_actors.contains(actor_id))
1458            .map(|(actor_id, _)| {
1459                let f = self.abort_root_actor(actor_id.id());
1460                async move {
1461                    let _ = if let Some(f) = f { Some(f.await) } else { None };
1462                    // If `is_none(&_)` then the associated actor's
1463                    // instance cell was already dropped when we went
1464                    // to call `abort()` on the cell's task handle.
1465
1466                    actor_id.clone()
1467                }
1468            })
1469            .collect();
1470        let mut aborted_actors = futures::future::join_all(aborted_actors).await;
1471
1472        // Phase 2: now that all other actors have stopped, request the
1473        // supervision coordinator to stop. Their terminal supervision
1474        // events have already been enqueued by this point, and the
1475        // coordinator's DrainAndStop path drains queued supervision
1476        // events before exiting.
1477        if let Some(ref coord_id) = coordinator_id
1478            && let Some(mut status) = self.stop_actor(coord_id.id(), reason.to_string())
1479        {
1480            let stopped =
1481                tokio::time::timeout(timeout, status.wait_for(|s: &ActorStatus| s.is_terminal()))
1482                    .await
1483                    .is_ok();
1484            if stopped {
1485                stopped_actors.push(coord_id.clone());
1486            } else {
1487                if let Some(f) = self.abort_root_actor(coord_id.id()) {
1488                    f.await;
1489                }
1490                aborted_actors.push(coord_id.clone());
1491            }
1492        }
1493
1494        // Flush the gateway so that any messages posted during
1495        // teardown (e.g. supervision events) are wire-delivered
1496        // before we tear down the proc's networking. The flush is
1497        // best-effort: if the remote side has already torn down its
1498        // networking, acks may never arrive and flush would hang
1499        // indefinitely, so we bound it with a configurable timeout.
1500        let flush_timeout = hyperactor_config::global::get(crate::config::FORWARDER_FLUSH_TIMEOUT);
1501        let gateway = self.gateway();
1502        match tokio::time::timeout(flush_timeout, gateway.flush()).await {
1503            Ok(Err(err)) => {
1504                tracing::warn!("gateway flush failed during proc exit: {:?}", err);
1505            }
1506            Err(_elapsed) => {
1507                tracing::warn!("gateway flush timed out during proc exit");
1508            }
1509            Ok(Ok(())) => {}
1510        }
1511
1512        tracing::info!(
1513            "destroy_and_wait: {} actors stopped, {} actors aborted",
1514            stopped_actors.len(),
1515            aborted_actors.len()
1516        );
1517        Ok((stopped_actors, aborted_actors))
1518    }
1519
1520    /// Resolve an actor reference to a **live** actor on this proc.
1521    ///
1522    /// Returns `None` if:
1523    /// - the actor was never spawned here,
1524    /// - the actor's `InstanceCell` has been dropped, or
1525    /// - the actor's status is terminal (stopped or failed).
1526    ///
1527    /// The terminal-status check makes the live/dead boundary explicit:
1528    /// terminal status is published only after runtime epilogue tasks
1529    /// such as introspection have completed, so a terminal actor must
1530    /// not be returned even if another local handle still keeps its cell
1531    /// alive.
1532    ///
1533    /// Bounds:
1534    /// - `R: Actor` — must be a real actor that can live in this
1535    ///   proc.
1536    /// - `R: Referable` — required because the input is an
1537    ///   `ActorRef<R>`.
1538    pub fn resolve_actor_ref<R: Actor + Referable>(
1539        &self,
1540        actor_ref: &ActorRef<R>,
1541    ) -> Option<ActorHandle<R>> {
1542        let cell = self
1543            .inner
1544            .instances
1545            .get(actor_ref.actor_addr().id())?
1546            .upgrade()?;
1547        // An actor whose status is terminal has stopped processing
1548        // messages even if its InstanceCell Arc is still alive (e.g.
1549        // held by the introspect task during teardown).
1550        if cell.status().borrow().is_terminal() {
1551            return None;
1552        }
1553        cell.downcast_handle()
1554    }
1555
1556    /// Create a root allocation in the proc.
1557    ///
1558    /// Uses `reserved_roots` to prevent races between concurrent callers.
1559    fn allocate_root_id(&self, name: &str) -> Result<ActorAddr, anyhow::Error> {
1560        self.reserve_root(Uid::singleton(Label::strip(name)))
1561    }
1562
1563    /// Create a root allocation with fresh identity and the actor type label.
1564    fn allocate_root_type<A: Actor>(&self) -> ActorAddr {
1565        self.root_addr(Uid::instance(default_actor_label::<A>()))
1566    }
1567
1568    /// Create a root allocation with a display label and fresh identity.
1569    fn allocate_root_label(&self, label: &str) -> ActorAddr {
1570        self.root_addr(Uid::instance(Label::strip(label)))
1571    }
1572
1573    /// Create a root allocation in the proc from an explicit uid.
1574    fn allocate_root_uid(&self, uid: Uid) -> Result<ActorAddr, anyhow::Error> {
1575        self.reserve_root(uid)
1576    }
1577
1578    fn allocate_client_id(&self, label: &str) -> ActorAddr {
1579        let actor_id = if label.is_empty() {
1580            ActorId::anonymous(self.proc_id().clone())
1581        } else {
1582            ActorId::instance(Label::strip(label), self.proc_id().clone())
1583        };
1584        ActorAddr::new(actor_id, self.default_location())
1585    }
1586
1587    fn reserve_root(&self, uid: Uid) -> Result<ActorAddr, anyhow::Error> {
1588        let actor_id = ActorId::new(uid.clone(), self.proc_id().clone(), None);
1589        if !self.state().reserved_roots.insert(uid) {
1590            anyhow::bail!("an actor with id '{}' has already been spawned", actor_id)
1591        }
1592        Ok(ActorAddr::new(actor_id, self.default_location()))
1593    }
1594
1595    fn root_addr(&self, uid: Uid) -> ActorAddr {
1596        ActorAddr::new(
1597            ActorId::new(uid, self.proc_id().clone(), None),
1598            self.default_location(),
1599        )
1600    }
1601
1602    /// Create a child allocation in the proc.
1603    pub(crate) fn allocate_anonymous_child_id(&self, parent_id: &ActorAddr) -> ActorAddr {
1604        assert_eq!(parent_id.proc_id(), self.proc_id());
1605        ActorAddr::new(
1606            ActorId::anonymous(self.proc_id().clone()),
1607            self.default_location(),
1608        )
1609    }
1610
1611    /// Create a child allocation in the proc using the actor type label.
1612    pub(crate) fn allocate_child_id<A: Actor>(&self, parent_id: &ActorAddr) -> ActorAddr {
1613        assert_eq!(parent_id.proc_id(), self.proc_id());
1614        let actor_id = ActorId::instance(default_actor_label::<A>(), self.proc_id().clone());
1615        ActorAddr::new(actor_id, self.default_location())
1616    }
1617
1618    /// Ensure that the requested child uid is available in this proc.
1619    fn ensure_child_uid(
1620        &self,
1621        parent_id: &ActorAddr,
1622        uid: Uid,
1623    ) -> Result<ActorAddr, anyhow::Error> {
1624        assert_eq!(parent_id.proc_id(), self.proc_id());
1625        let actor_id = ActorId::new(uid.clone(), self.proc_id().clone(), None);
1626        let actor_addr = ActorAddr::new(actor_id, self.default_location());
1627        if !self.state().reserved_child_uids.insert(uid) {
1628            anyhow::bail!("an actor with id {} has already been spawned", actor_addr);
1629        }
1630        Ok(actor_addr)
1631    }
1632
1633    /// Allocate an actor ID with a custom name on this proc.
1634    pub(crate) fn allocate_named_child_id(&self, parent_id: &ActorAddr, name: &str) -> ActorAddr {
1635        assert_eq!(parent_id.proc_id(), self.proc_id());
1636        let proc_id = self.proc_id().clone();
1637        let actor_id = crate::id::ActorId::instance(crate::id::Label::strip(name), proc_id);
1638        ActorAddr::new(actor_id, self.default_location())
1639    }
1640
1641    /// Downgrade to a weak reference that doesn't prevent the proc from being dropped.
1642    pub fn downgrade(&self) -> WeakProc {
1643        WeakProc::new(self)
1644    }
1645
1646    /// Flush the gateway so that any buffered messages are
1647    /// wire-delivered before the proc's networking is torn down.
1648    pub async fn flush(&self) -> Result<(), anyhow::Error> {
1649        self.gateway().flush().await
1650    }
1651
1652    /// Stop and join the mailbox server, flushing receive-side acks.
1653    ///
1654    /// This stops the `MailboxServer::serve` loop and awaits its
1655    /// completion, which runs `Rx::join()` to flush any pending
1656    /// transport-level acks before the channel is torn down.
1657    ///
1658    /// No-op if no mailbox server handle is stored (e.g. for
1659    /// `Proc::configured` or `Proc::isolated` procs that don't serve).
1660    pub async fn join_mailbox_server(&self) {
1661        let handle = self.inner.mailbox_server_handle.lock().unwrap().take();
1662        if let Some(handle) = handle {
1663            handle.stop("proc shutting down");
1664            let _ = handle.await;
1665        }
1666    }
1667
1668    pub(crate) fn is_local_delivery_target(&self, dest_proc: &ProcAddr) -> bool {
1669        self.is_local_delivery_target_at(dest_proc, &[self.default_location()])
1670    }
1671
1672    pub(crate) fn is_local_delivery_target_at(
1673        &self,
1674        dest_proc: &ProcAddr,
1675        local_locations: &[Location],
1676    ) -> bool {
1677        dest_proc.id() == self.proc_id()
1678            && (!requires_location_for_local_delivery_identity(dest_proc.id())
1679                || local_locations
1680                    .iter()
1681                    .any(|location| dest_proc.location() == location))
1682    }
1683}
1684
1685fn requires_location_for_local_delivery_identity(proc_id: &ProcId) -> bool {
1686    // Temporary hyperactor_mesh compatibility hack: host bootstrap
1687    // still creates a `service` proc and a `local` proc in every host
1688    // process, so those proc ids are not globally unique. Until those
1689    // construction paths are assigned instance ids, local delivery for
1690    // those two ids also compares the terminal channel address.
1691    is_legacy_pseudo_singleton_proc_id(proc_id)
1692}
1693
1694fn default_actor_label<A>() -> Label {
1695    let type_name = std::any::type_name::<A>();
1696    let type_name = type_name
1697        .split_once('<')
1698        .map_or(type_name, |(base, _)| base);
1699    Label::strip(type_name.rsplit("::").next().unwrap_or(type_name))
1700}
1701
1702fn global_proc_label() -> Label {
1703    let hostname = hostname::get().expect("hostname should be available");
1704    global_proc_label_from(&hostname.to_string_lossy(), std::process::id())
1705}
1706
1707fn global_proc_label_from(hostname: &str, pid: u32) -> Label {
1708    let short_hostname = hostname
1709        .split_once('.')
1710        .map_or(hostname, |(short, _)| short);
1711    Label::strip(&format!("{}-{}", short_hostname, pid))
1712}
1713
1714fn assert_not_legacy_pseudo_singleton_proc_id(proc_id: &ProcId) {
1715    if is_legacy_pseudo_singleton_proc_id(proc_id) {
1716        panic!(
1717            "legacy pseudo-singleton proc id '{}' must be constructed with a dedicated Proc constructor",
1718            proc_id
1719        );
1720    }
1721}
1722
1723fn is_legacy_pseudo_singleton_proc_id(proc_id: &ProcId) -> bool {
1724    matches!(
1725        proc_id.uid(),
1726        Uid::Singleton(label) if is_legacy_pseudo_singleton_label(label)
1727    )
1728}
1729
1730fn is_legacy_pseudo_singleton_label(label: &Label) -> bool {
1731    matches!(
1732        label.as_str(),
1733        LEGACY_SERVICE_PROC_NAME | LEGACY_LOCAL_PROC_NAME
1734    )
1735}
1736
1737#[async_trait]
1738impl MailboxSender for Proc {
1739    fn post_unchecked(
1740        &self,
1741        envelope: MessageEnvelope,
1742        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1743    ) {
1744        let dest_proc = envelope.dest().actor_addr().proc_addr();
1745        if self.is_local_delivery_target(&dest_proc) {
1746            self.state().proc_muxer.post(envelope, return_handle);
1747            return;
1748        }
1749        // Route through the gateway as a [`MailboxSender`] (not its
1750        // raw forwarder) so peers sharing the same gateway —
1751        // typically a host's `service_proc` / `local_proc` — are
1752        // reached by an in-gateway lookup rather than bouncing out
1753        // through the forwarder.
1754        self.state().gateway.post(envelope, return_handle);
1755    }
1756
1757    async fn flush(&self) -> Result<(), anyhow::Error> {
1758        self.gateway().flush().await
1759    }
1760}
1761
1762/// A weak reference to a Proc that doesn't prevent it from being dropped.
1763#[derive(Clone, Debug)]
1764pub struct WeakProc(Weak<ProcState>);
1765
1766impl WeakProc {
1767    fn new(proc: &Proc) -> Self {
1768        Self(Arc::downgrade(&proc.inner))
1769    }
1770
1771    /// Upgrade to a strong Proc reference, if the proc is still alive.
1772    pub fn upgrade(&self) -> Option<Proc> {
1773        self.0.upgrade().map(|inner| Proc { inner })
1774    }
1775
1776    /// Whether two weak handles refer to the same underlying proc.
1777    pub(crate) fn ptr_eq(&self, other: &WeakProc) -> bool {
1778        self.0.ptr_eq(&other.0)
1779    }
1780}
1781
1782#[async_trait]
1783impl MailboxSender for WeakProc {
1784    fn post_unchecked(
1785        &self,
1786        envelope: MessageEnvelope,
1787        return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1788    ) {
1789        match self.upgrade() {
1790            Some(proc) => proc.post(envelope, return_handle),
1791            None => {
1792                let target = envelope.dest().clone();
1793                let failure =
1794                    DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1795                        target,
1796                        TransportFailureReason::LinkUnavailable("proc is gone".to_string()),
1797                    )));
1798                envelope.undeliverable(failure, return_handle)
1799            }
1800        }
1801    }
1802
1803    async fn flush(&self) -> Result<(), anyhow::Error> {
1804        match self.upgrade() {
1805            Some(proc) => proc.flush().await,
1806            None => Ok(()),
1807        }
1808    }
1809}
1810
1811/// Represents a single work item used by the instance to dispatch to
1812/// actor handles. Specifically, this enables handler polymorphism.
1813pub struct WorkCell<A: Actor + Send>(
1814    Box<
1815        dyn for<'a> FnOnce(
1816                &'a mut A,
1817                &'a Instance<A>,
1818            )
1819                -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + 'a + Send>>
1820            + Send
1821            + Sync,
1822    >,
1823);
1824
1825impl<A: Actor + Send> WorkCell<A> {
1826    /// Create a new WorkCell from a concrete function (closure).
1827    fn new(
1828        f: impl for<'a> FnOnce(
1829            &'a mut A,
1830            &'a Instance<A>,
1831        )
1832            -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + 'a + Send>>
1833        + Send
1834        + Sync
1835        + 'static,
1836    ) -> Self {
1837        Self(Box::new(f))
1838    }
1839
1840    /// Handle the message represented by this work cell.
1841    pub fn handle<'a>(
1842        self,
1843        actor: &'a mut A,
1844        instance: &'a Instance<A>,
1845    ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + Send + 'a>> {
1846        (self.0)(actor, instance)
1847    }
1848}
1849
1850/// Context for a message currently being handled by an Instance.
1851pub struct Context<'a, A: Actor> {
1852    instance: &'a Instance<A>,
1853    headers: Flattrs,
1854}
1855
1856impl<'a, A: Actor> Context<'a, A> {
1857    /// Construct a new Context.
1858    pub fn new(instance: &'a Instance<A>, headers: Flattrs) -> Self {
1859        Self { instance, headers }
1860    }
1861
1862    /// Get a reference to the message headers.
1863    pub fn headers(&self) -> &Flattrs {
1864        &self.headers
1865    }
1866}
1867
1868impl<A: Actor> Deref for Context<'_, A> {
1869    type Target = Instance<A>;
1870
1871    fn deref(&self) -> &Self::Target {
1872        self.instance
1873    }
1874}
1875
1876/// An actor instance. This is responsible for managing a running actor, including
1877/// its full lifecycle, supervision, signal management, etc. Instances can represent
1878/// a managed actor or a "client" actor that has joined the proc.
1879pub struct Instance<A: Actor> {
1880    inner: Arc<InstanceState<A>>,
1881}
1882
1883impl<A: Actor> fmt::Debug for Instance<A> {
1884    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1885        f.debug_struct("Instance").field("inner", &"..").finish()
1886    }
1887}
1888
1889struct InstanceState<A: Actor> {
1890    /// The proc that owns this instance.
1891    proc: Proc,
1892
1893    /// The instance cell that manages instance hierarchy.
1894    cell: InstanceCell,
1895
1896    /// The mailbox associated with the actor.
1897    mailbox: Mailbox,
1898
1899    ports: Arc<HandlerPorts<A>>,
1900
1901    /// Runtime-owned delayed-post scheduler.
1902    delayed_posts: DelayedPosts<A>,
1903
1904    /// Shutdown signal for the runtime-owned introspection task.
1905    introspect_shutdown_tx: Mutex<Option<oneshot::Sender<ActorStatus>>>,
1906
1907    /// Join handle for the runtime-owned introspection task.
1908    introspect_task_handle: Mutex<Option<JoinHandle<()>>>,
1909
1910    /// Shutdown signal for proc-managed detached introspection
1911    /// lifecycle. Used by client/inverted instances that have no actor
1912    /// serving loop to join introspection.
1913    detached_introspect_shutdown_tx: Mutex<Option<oneshot::Sender<ActorStatus>>>,
1914
1915    /// This instance's globally unique ID.
1916    id: Uuid,
1917
1918    /// Used to assign sequence numbers for messages sent from this actor.
1919    sequencer: Sequencer,
1920
1921    /// Per-instance local storage.
1922    instance_locals: ActorLocalStorage,
1923}
1924
1925struct ActorStopped {
1926    reason: String,
1927    stop_mode: StopMode,
1928}
1929
1930#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1931enum ChildTeardown {
1932    Cooperative(StopMode),
1933    Kill,
1934}
1935
1936impl ChildTeardown {
1937    fn from_run_result(result: &Result<ActorStopped, ActorError>) -> Self {
1938        match result {
1939            Ok(stopped) => Self::Cooperative(stopped.stop_mode),
1940            Err(err) if matches!(err.kind.as_ref(), ActorErrorKind::Aborted(_)) => Self::Kill,
1941            Err(_) => Self::Cooperative(StopMode::Stop),
1942        }
1943    }
1944
1945    fn cooperative_signal(mode: StopMode) -> Signal {
1946        match mode {
1947            StopMode::DrainAndStop => Signal::DrainAndStop("parent draining".to_string()),
1948            StopMode::Stop => Signal::Stop("parent stopping".to_string()),
1949        }
1950    }
1951}
1952
1953type DelayedPost<A> = Box<dyn FnOnce(&Instance<A>) + Send>;
1954
1955trait PostAfterEndpoint<A: Actor, M: Message>: Send {
1956    fn endpoint_location(&self) -> crate::EndpointLocation;
1957
1958    fn into_delayed_post(self, message: M) -> DelayedPost<A>;
1959}
1960
1961impl<A, M> PostAfterEndpoint<A, M> for &Instance<A>
1962where
1963    A: Actor + Handler<M>,
1964    M: Message,
1965{
1966    fn endpoint_location(&self) -> crate::EndpointLocation {
1967        crate::EndpointLocation::Actor(self.self_addr().clone())
1968    }
1969
1970    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
1971        let dest = self.clone_for_py();
1972        Box::new(move |this| crate::Endpoint::post(&dest, this, message))
1973    }
1974}
1975
1976impl<A, M> PostAfterEndpoint<A, M> for &Context<'_, A>
1977where
1978    A: Actor + Handler<M>,
1979    M: Message,
1980{
1981    fn endpoint_location(&self) -> crate::EndpointLocation {
1982        crate::EndpointLocation::Actor(self.self_addr().clone())
1983    }
1984
1985    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
1986        let dest = self.clone_for_py();
1987        Box::new(move |this| crate::Endpoint::post(&dest, this, message))
1988    }
1989}
1990
1991impl<A, M> PostAfterEndpoint<A, M> for Instance<A>
1992where
1993    A: Actor + Handler<M>,
1994    M: Message,
1995{
1996    fn endpoint_location(&self) -> crate::EndpointLocation {
1997        crate::EndpointLocation::Actor(self.self_addr().clone())
1998    }
1999
2000    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2001        Box::new(move |this| crate::Endpoint::post(&self, this, message))
2002    }
2003}
2004
2005impl<A, B, M> PostAfterEndpoint<A, M> for ActorHandle<B>
2006where
2007    A: Actor,
2008    B: Actor + Handler<M>,
2009    M: Message,
2010{
2011    fn endpoint_location(&self) -> crate::EndpointLocation {
2012        crate::Endpoint::endpoint_location(&self)
2013    }
2014
2015    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2016        Box::new(move |this| crate::Endpoint::post(&self, this, message))
2017    }
2018}
2019
2020impl<A, M> PostAfterEndpoint<A, M> for PortHandle<M>
2021where
2022    A: Actor,
2023    M: Message,
2024{
2025    fn endpoint_location(&self) -> crate::EndpointLocation {
2026        crate::Endpoint::endpoint_location(&self)
2027    }
2028
2029    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2030        Box::new(move |this| crate::Endpoint::post(&self, this, message))
2031    }
2032}
2033
2034impl<A, M> PostAfterEndpoint<A, M> for OncePortHandle<M>
2035where
2036    A: Actor,
2037    M: Message,
2038{
2039    fn endpoint_location(&self) -> crate::EndpointLocation {
2040        crate::Endpoint::endpoint_location(self)
2041    }
2042
2043    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2044        Box::new(move |this| crate::Endpoint::post(self, this, message))
2045    }
2046}
2047
2048impl<A, B, M> PostAfterEndpoint<A, M> for ActorRef<B>
2049where
2050    A: Actor,
2051    B: Referable + RemoteHandles<M>,
2052    M: RemoteMessage,
2053{
2054    fn endpoint_location(&self) -> crate::EndpointLocation {
2055        crate::Endpoint::endpoint_location(&self)
2056    }
2057
2058    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2059        Box::new(move |this| crate::Endpoint::post(&self, this, message))
2060    }
2061}
2062
2063impl<A, M> PostAfterEndpoint<A, M> for crate::PortRef<M>
2064where
2065    A: Actor,
2066    M: RemoteMessage,
2067{
2068    fn endpoint_location(&self) -> crate::EndpointLocation {
2069        crate::Endpoint::endpoint_location(&self)
2070    }
2071
2072    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2073        Box::new(move |this| crate::Endpoint::post(&self, this, message))
2074    }
2075}
2076
2077impl<A, M> PostAfterEndpoint<A, M> for crate::OncePortRef<M>
2078where
2079    A: Actor,
2080    M: RemoteMessage,
2081{
2082    fn endpoint_location(&self) -> crate::EndpointLocation {
2083        crate::Endpoint::endpoint_location(self)
2084    }
2085
2086    fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2087        Box::new(move |this| crate::Endpoint::post(self, this, message))
2088    }
2089}
2090
2091struct DelayedPosts<A: Actor> {
2092    ingress: Arc<DelayedPostIngressGate>,
2093    state: Mutex<DelayedPostState<A>>,
2094    notify: Notify,
2095}
2096
2097struct DelayedPostState<A: Actor> {
2098    queue: BTreeMap<(tokio::time::Instant, u64), DelayedPost<A>>,
2099    next_order: u64,
2100}
2101
2102impl<A: Actor> DelayedPosts<A> {
2103    fn new() -> Self {
2104        Self {
2105            ingress: Arc::new(DelayedPostIngressGate::new()),
2106            state: Mutex::new(DelayedPostState {
2107                queue: BTreeMap::new(),
2108                next_order: 0,
2109            }),
2110            notify: Notify::new(),
2111        }
2112    }
2113
2114    fn push(&self, deadline: tokio::time::Instant, post: DelayedPost<A>) {
2115        let mut state = self.state.lock().unwrap();
2116        let order = state.next_order;
2117        state.next_order = state.next_order.wrapping_add(1);
2118        state.queue.insert((deadline, order), post);
2119        drop(state);
2120        self.notify.notify_one();
2121    }
2122
2123    fn next_deadline(&self) -> Option<tokio::time::Instant> {
2124        self.state
2125            .lock()
2126            .unwrap()
2127            .queue
2128            .keys()
2129            .next()
2130            .map(|(deadline, _)| *deadline)
2131    }
2132
2133    fn pop_due(&self, now: tokio::time::Instant) -> Vec<DelayedPost<A>> {
2134        let mut posts = Vec::new();
2135        let mut state = self.state.lock().unwrap();
2136        while let Some((&(deadline, _), _)) = state.queue.first_key_value() {
2137            if deadline > now {
2138                break;
2139            }
2140            let (_, post) = state.queue.pop_first().expect("delayed post should exist");
2141            posts.push(post);
2142        }
2143        posts
2144    }
2145
2146    fn drain(&self) {
2147        self.ingress.drain();
2148    }
2149
2150    fn is_draining(&self) -> bool {
2151        self.ingress.is_draining()
2152    }
2153}
2154
2155const DELAYED_POST_INGRESS_DRAINING: usize = 1usize << (usize::BITS as usize - 1);
2156const DELAYED_POST_INGRESS_ACTIVE_MASK: usize = !DELAYED_POST_INGRESS_DRAINING;
2157
2158struct DelayedPostIngressGate {
2159    state: AtomicUsize,
2160    wait_lock: Mutex<()>,
2161    drained: Condvar,
2162}
2163
2164struct DelayedPostIngressGuard {
2165    gate: Arc<DelayedPostIngressGate>,
2166}
2167
2168impl DelayedPostIngressGate {
2169    fn new() -> Self {
2170        Self {
2171            state: AtomicUsize::new(0),
2172            wait_lock: Mutex::new(()),
2173            drained: Condvar::new(),
2174        }
2175    }
2176
2177    fn try_enter(self: &Arc<Self>) -> Result<DelayedPostIngressGuard, ()> {
2178        let mut state = self.state.load(Ordering::Acquire);
2179        loop {
2180            if state & DELAYED_POST_INGRESS_DRAINING != 0 {
2181                return Err(());
2182            }
2183
2184            let active = state & DELAYED_POST_INGRESS_ACTIVE_MASK;
2185            assert!(
2186                active < DELAYED_POST_INGRESS_ACTIVE_MASK,
2187                "too many active delayed post sends"
2188            );
2189
2190            match self.state.compare_exchange_weak(
2191                state,
2192                state + 1,
2193                Ordering::AcqRel,
2194                Ordering::Acquire,
2195            ) {
2196                Ok(_) => {
2197                    return Ok(DelayedPostIngressGuard {
2198                        gate: Arc::clone(self),
2199                    });
2200                }
2201                Err(next_state) => state = next_state,
2202            }
2203        }
2204    }
2205
2206    fn drain(&self) {
2207        let mut state = self.state.load(Ordering::Acquire);
2208        loop {
2209            if state & DELAYED_POST_INGRESS_DRAINING != 0 {
2210                break;
2211            }
2212            match self.state.compare_exchange_weak(
2213                state,
2214                state | DELAYED_POST_INGRESS_DRAINING,
2215                Ordering::AcqRel,
2216                Ordering::Acquire,
2217            ) {
2218                Ok(_) => break,
2219                Err(next_state) => state = next_state,
2220            }
2221        }
2222
2223        let mut wait_guard = self.wait_lock.lock().unwrap();
2224        while self.state.load(Ordering::Acquire) & DELAYED_POST_INGRESS_ACTIVE_MASK != 0 {
2225            wait_guard = self.drained.wait(wait_guard).unwrap();
2226        }
2227    }
2228
2229    fn is_draining(&self) -> bool {
2230        self.state.load(Ordering::Acquire) & DELAYED_POST_INGRESS_DRAINING != 0
2231    }
2232}
2233
2234impl Drop for DelayedPostIngressGuard {
2235    fn drop(&mut self) {
2236        let previous = self.gate.state.fetch_sub(1, Ordering::AcqRel);
2237        assert!(
2238            previous & DELAYED_POST_INGRESS_ACTIVE_MASK != 0,
2239            "delayed post ingress active count underflow"
2240        );
2241        if previous & DELAYED_POST_INGRESS_DRAINING != 0
2242            && previous & DELAYED_POST_INGRESS_ACTIVE_MASK == 1
2243        {
2244            let _wait_guard = self.gate.wait_lock.lock().unwrap();
2245            self.gate.drained.notify_all();
2246        }
2247    }
2248}
2249
2250impl<A: Actor> InstanceState<A> {
2251    fn self_addr(&self) -> &ActorAddr {
2252        self.mailbox.actor_addr()
2253    }
2254}
2255
2256impl<A: Actor> Drop for InstanceState<A> {
2257    fn drop(&mut self) {
2258        let terminal_status = ActorStatus::Stopped("instance is dropped".into());
2259        if let Some(shutdown_tx) = self.detached_introspect_shutdown_tx.lock().unwrap().take() {
2260            let _ = shutdown_tx.send(terminal_status.clone());
2261            return;
2262        }
2263
2264        if let Some(shutdown_tx) = self.introspect_shutdown_tx.lock().unwrap().take() {
2265            let _ = shutdown_tx.send(terminal_status.clone());
2266        }
2267        let _ = self.introspect_task_handle.lock().unwrap().take();
2268
2269        self.cell.publish_dropped_status(terminal_status);
2270    }
2271}
2272
2273/// Receivers created by [`Instance::new`] that must be threaded to
2274/// their respective consumers (actor loop, introspect task, etc.).
2275///
2276/// # Invariant
2277///
2278/// See S10 in `introspect` module doc.
2279pub struct InstanceReceivers<A: Actor> {
2280    /// Signal and supervision receivers for the actor loop. `None`
2281    /// for detached/client instances that don't run an actor loop.
2282    actor_loop: Option<(
2283        mpsc::UnboundedReceiver<Signal>,
2284        mpsc::UnboundedReceiver<ActorSupervisionEvent>,
2285    )>,
2286    /// Work queue for dispatching messages to actor handlers.
2287    work: ActorWorkReceiver<A>,
2288    /// Introspect message receiver for the dedicated introspect task.
2289    introspect: PortReceiver<IntrospectMessage>,
2290}
2291
2292impl<A: Actor> Instance<A> {
2293    /// Create a new actor instance in Created state.
2294    fn new(
2295        proc: Proc,
2296        actor_id: ActorAddr,
2297        detached: bool,
2298        parent: Option<InstanceCell>,
2299    ) -> (Self, InstanceReceivers<A>) {
2300        // Set up messaging
2301        let mailbox = Mailbox::new(actor_id.clone());
2302        let enable_buffering =
2303            hyperactor_config::global::get(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER);
2304        let (work_tx, work_rx) = sequenced_unbounded_with_buffering(enable_buffering);
2305        let inbound_ordering_snapshot_handle = work_rx.snapshot_handle();
2306        let queue_depth = Arc::new(AtomicU64::new(0));
2307        let proc_stats = Arc::clone(&proc.state().queue_stats);
2308        let ports: Arc<HandlerPorts<A>> = Arc::new(HandlerPorts::new(
2309            mailbox.clone(),
2310            work_tx,
2311            enable_buffering,
2312            Arc::clone(&queue_depth),
2313            proc_stats,
2314        ));
2315        proc.state().proc_muxer.bind_mailbox(mailbox.clone());
2316        let (status_tx, status_rx) = watch::channel(ActorStatus::Created);
2317
2318        let actor_type = match TypeInfo::of::<A>() {
2319            Some(info) => ActorType::Named(info),
2320            None => ActorType::Anonymous(std::any::type_name::<A>()),
2321        };
2322        let actor_loop_ports = if detached {
2323            None
2324        } else {
2325            let (signal_tx, signal_receiver) = mpsc::unbounded_channel::<Signal>();
2326            let (supervision_tx, supervision_receiver) =
2327                mpsc::unbounded_channel::<ActorSupervisionEvent>();
2328            Some((
2329                (signal_tx, supervision_tx),
2330                (signal_receiver, supervision_receiver),
2331            ))
2332        };
2333
2334        let (actor_loop, actor_loop_receivers) = actor_loop_ports.unzip();
2335
2336        // Introspect port: a separate channel handled by a dedicated
2337        // tokio task (not the actor's message loop). bind_control_port()
2338        // registers it in the mailbox dispatch table.
2339        //
2340        // Exercises S3, S4, S9 (see introspect module doc).
2341        let (introspect_port, introspect_receiver) = mailbox.open_port::<IntrospectMessage>();
2342        introspect_port.bind_control_port(crate::port::ControlPort::Introspect);
2343
2344        let instance_id = Uuid::now_v7();
2345
2346        // Type-erased snapshot callback: captures only the receiver-local
2347        // sequencing snapshot handle. Nothing cyclic is captured.
2348        let inbound_ordering_snapshot: Option<
2349            Box<dyn Fn() -> crate::ordering::OrderingSnapshot + Send + Sync>,
2350        > = Some(Box::new(move || {
2351            inbound_ordering_snapshot_handle.snapshot()
2352        }));
2353
2354        let cell = InstanceCell::new(
2355            actor_id,
2356            instance_id,
2357            actor_type,
2358            proc.clone(),
2359            actor_loop,
2360            status_tx,
2361            status_rx,
2362            parent,
2363            ports.clone(),
2364            queue_depth,
2365            inbound_ordering_snapshot,
2366        );
2367        let inner = Arc::new(InstanceState {
2368            proc,
2369            cell,
2370            mailbox,
2371            ports,
2372            delayed_posts: DelayedPosts::new(),
2373            introspect_shutdown_tx: Mutex::new(None),
2374            introspect_task_handle: Mutex::new(None),
2375            detached_introspect_shutdown_tx: Mutex::new(None),
2376            sequencer: Sequencer::new(instance_id),
2377            id: instance_id,
2378            instance_locals: ActorLocalStorage::new(),
2379        });
2380        (
2381            Self { inner },
2382            InstanceReceivers {
2383                actor_loop: actor_loop_receivers,
2384                work: ActorWorkReceiver::new(work_rx),
2385                introspect: introspect_receiver,
2386            },
2387        )
2388    }
2389
2390    fn spawn_introspect(&self, receiver: PortReceiver<IntrospectMessage>) {
2391        let (shutdown_tx, shutdown_rx) = oneshot::channel();
2392        let handle = tokio::spawn(crate::introspect::serve_introspect(
2393            self.inner.cell.clone(),
2394            receiver,
2395            shutdown_rx,
2396        ));
2397
2398        let mut shutdown = self.inner.introspect_shutdown_tx.lock().unwrap();
2399        assert!(
2400            shutdown.is_none(),
2401            "introspection shutdown handle already set"
2402        );
2403        let mut task = self.inner.introspect_task_handle.lock().unwrap();
2404        assert!(task.is_none(), "introspection task already set");
2405        *shutdown = Some(shutdown_tx);
2406        *task = Some(handle);
2407    }
2408
2409    fn spawn_detached_introspect(&self, receiver: PortReceiver<IntrospectMessage>) {
2410        let (detached_shutdown_tx, detached_shutdown_rx) = oneshot::channel::<ActorStatus>();
2411        let (introspect_shutdown_tx, introspect_shutdown_rx) = oneshot::channel();
2412        let introspect_handle = tokio::spawn(crate::introspect::serve_introspect(
2413            self.inner.cell.clone(),
2414            receiver,
2415            introspect_shutdown_rx,
2416        ));
2417
2418        let cell = self.inner.cell.clone();
2419        tokio::spawn(async move {
2420            let Ok(terminal_status) = detached_shutdown_rx.await else {
2421                return;
2422            };
2423            let _ = introspect_shutdown_tx.send(terminal_status.clone());
2424            if let Err(err) = introspect_handle.await {
2425                tracing::debug!("introspect task join failed: {:?}", err);
2426            }
2427            cell.publish_dropped_status(terminal_status);
2428        });
2429
2430        let mut shutdown = self.inner.detached_introspect_shutdown_tx.lock().unwrap();
2431        assert!(
2432            shutdown.is_none(),
2433            "detached introspection shutdown handle already set"
2434        );
2435        *shutdown = Some(detached_shutdown_tx);
2436    }
2437
2438    fn signal_introspect_stop(&self, terminal_status: ActorStatus) -> Option<JoinHandle<()>> {
2439        if let Some(shutdown_tx) = self.inner.introspect_shutdown_tx.lock().unwrap().take() {
2440            let _ = shutdown_tx.send(terminal_status);
2441        }
2442        self.inner.introspect_task_handle.lock().unwrap().take()
2443    }
2444
2445    async fn stop_introspect(&self, terminal_status: ActorStatus) {
2446        if let Some(handle) = self.signal_introspect_stop(terminal_status)
2447            && let Err(err) = handle.await
2448        {
2449            tracing::debug!("introspect task join failed: {:?}", err);
2450        }
2451    }
2452
2453    /// Notify subscribers of a change in the actors status and bump counters with the duration which
2454    /// the last status was active for.
2455    #[track_caller]
2456    pub fn change_status(&self, new: ActorStatus) {
2457        self.inner.cell.change_status(new);
2458    }
2459
2460    fn is_terminal(&self) -> bool {
2461        self.inner.cell.status().borrow().is_terminal()
2462    }
2463
2464    fn is_stopping(&self) -> bool {
2465        self.inner.cell.status().borrow().is_stopping()
2466    }
2467
2468    /// This instance's actor address.
2469    pub fn self_addr(&self) -> &ActorAddr {
2470        self.inner.self_addr()
2471    }
2472
2473    /// Report a delivery failure whose original payload is unavailable.
2474    pub(crate) fn report_delivery_failure(&self, report: crate::mailbox::DeliveryFailureReport) {
2475        static REPORT_WARNED_MAILBOXES: OnceLock<DashSet<ActorAddr>> = OnceLock::new();
2476
2477        let mailbox = &self.inner.mailbox;
2478        let return_handle = mailbox.bound_return_handle().unwrap_or_else(|| {
2479            let actor_id = mailbox.actor_addr();
2480            if REPORT_WARNED_MAILBOXES
2481                .get_or_init(DashSet::new)
2482                .insert(actor_id.clone())
2483            {
2484                let bt = std::backtrace::Backtrace::force_capture();
2485                tracing::warn!(
2486                    actor_id = ?actor_id,
2487                    backtrace = ?bt,
2488                    "actor attempted to report delivery failure without binding Undeliverable<MessageEnvelope>"
2489                );
2490            }
2491            crate::mailbox::monitored_return_handle()
2492        });
2493
2494        if let Err(error) =
2495            return_handle.try_post(self, crate::mailbox::Undeliverable::report(report.clone()))
2496        {
2497            tracing::error!(
2498                sender = %report.sender,
2499                dest = %report.dest,
2500                message_type = report.message_type.as_deref().unwrap_or("unknown"),
2501                error = %report.error_msg().unwrap_or_default(),
2502                return_error = %error,
2503                "delivery failure report could not be returned"
2504            );
2505        }
2506    }
2507
2508    /// Snapshot of this actor's introspection payload.
2509    ///
2510    /// Returns an [`IntrospectResult`] built from live [`InstanceCell`]
2511    /// state, without going through the actor message loop. This is
2512    /// safe to call from within a handler on the same actor (no
2513    /// self-send deadlock).
2514    ///
2515    /// The snapshot is best-effort: it reflects framework-owned state
2516    /// (status, message count, flight recorder, supervision children)
2517    /// at the instant of the call. `parent` is left as `None` —
2518    /// callers are responsible for setting topology context.
2519    ///
2520    /// Note: this acquires a write lock on the flight recorder spool
2521    /// and clones its contents. Suitable for occasional introspection
2522    /// requests, not for hot paths.
2523    pub fn introspect_payload(&self) -> crate::introspect::IntrospectResult {
2524        crate::introspect::live_actor_payload(&self.inner.cell)
2525    }
2526
2527    /// Return a fresh tracing span bound to this actor's flight
2528    /// recorder, with this actor as the subject. See FR-1, FR-2, FR-3
2529    /// in module doc.
2530    pub fn recording_span(&self) -> tracing::Span {
2531        use crate::subject::AsSubject;
2532        self.inner
2533            .cell
2534            .recording()
2535            .span(&self.self_addr().subject().to_string())
2536    }
2537
2538    /// Publish domain-specific properties for introspection.
2539    ///
2540    /// Publish a complete Attrs bag for introspection. Replaces any
2541    /// previously published attrs.
2542    ///
2543    /// Debug builds assert that every key in the bag is tagged with
2544    /// the `INTROSPECT` meta-attribute.
2545    pub fn publish_attrs(&self, attrs: hyperactor_config::Attrs) {
2546        #[cfg(debug_assertions)]
2547        {
2548            use std::collections::HashSet;
2549            use std::sync::OnceLock;
2550
2551            use hyperactor_config::attrs::AttrKeyInfo;
2552
2553            static INTROSPECT_KEYS: OnceLock<HashSet<&'static str>> = OnceLock::new();
2554            let allowed = INTROSPECT_KEYS.get_or_init(|| {
2555                inventory::iter::<AttrKeyInfo>()
2556                    .filter(|info| info.meta.get(hyperactor_config::INTROSPECT).is_some())
2557                    .map(|info| info.name)
2558                    .collect()
2559            });
2560            for (name, _) in attrs.iter() {
2561                debug_assert!(
2562                    allowed.contains(name),
2563                    "publish_attrs: key {:?} is not tagged with INTROSPECT",
2564                    name
2565                );
2566            }
2567        }
2568        self.inner.cell.set_published_attrs(attrs);
2569    }
2570
2571    /// Publish a single attr key-value pair for introspection. Merges
2572    /// into existing published attrs (insert or overwrite).
2573    ///
2574    /// Debug builds assert that the key is tagged with the
2575    /// `INTROSPECT` meta-attribute.
2576    pub fn publish_attr<T: hyperactor_config::AttrValue>(
2577        &self,
2578        key: hyperactor_config::Key<T>,
2579        value: T,
2580    ) {
2581        debug_assert!(
2582            key.attrs().get(hyperactor_config::INTROSPECT).is_some(),
2583            "publish_attr called with non-introspection key: {}",
2584            key.name()
2585        );
2586        self.inner.cell.merge_published_attr(key, value);
2587    }
2588
2589    /// Install an actor-supplied introspection-attrs snapshot callback.
2590    ///
2591    /// The callback is invoked by the introspect task (outside the actor
2592    /// loop) when building this actor's node payload; its `Attrs` are
2593    /// merged into the Actor view, core keys winning on collision (AS-2).
2594    /// It must be `Send + Sync`, non-blocking (`try_lock`), and
2595    /// infallible. Core does not interpret the keys — the actor owns its
2596    /// own introspection vocabulary (AS-1). See the AS-* family in
2597    /// `introspect.rs`.
2598    pub fn set_attrs_snapshot(
2599        &self,
2600        callback: impl Fn() -> hyperactor_config::Attrs + Send + Sync + 'static,
2601    ) {
2602        self.inner.cell.set_attrs_snapshot(callback);
2603    }
2604
2605    /// Mark this actor as system/infrastructure. System actors are
2606    /// hidden by default in the TUI (toggled via `s`).
2607    pub fn set_system(&self) {
2608        self.inner
2609            .cell
2610            .inner
2611            .is_system
2612            .store(true, Ordering::Relaxed);
2613    }
2614
2615    /// Register a callback for resolving non-addressable children.
2616    ///
2617    /// The callback runs on the actor's introspect task (not the
2618    /// actor loop), so it must be `Send + Sync` and must not access
2619    /// actor-mutable state. Capture cloned `Proc` references.
2620    ///
2621    /// Only `HostAgent` uses this today — for resolving system
2622    /// procs that have no independent `ProcAgent`.
2623    pub fn set_query_child_handler(
2624        &self,
2625        handler: impl (Fn(&Addr) -> IntrospectResult) + Send + Sync + 'static,
2626    ) {
2627        self.inner.cell.set_query_child_handler(handler);
2628    }
2629
2630    /// Signal the actor to stop.
2631    pub fn stop(&self, reason: &str) -> Result<(), ActorError> {
2632        tracing::info!(
2633            actor_id = %self.inner.cell.actor_addr(),
2634            reason,
2635            "instance stop called",
2636        );
2637        self.inner.cell.signal(Signal::Stop(reason.to_string()))
2638    }
2639
2640    /// Signal the actor to drain current ordinary work and then stop.
2641    pub fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
2642        tracing::info!(
2643            actor_id = %self.inner.cell.actor_addr(),
2644            reason,
2645            "instance drain_and_stop called",
2646        );
2647        self.inner
2648            .cell
2649            .signal(Signal::DrainAndStop(reason.to_string()))
2650    }
2651
2652    /// Signal the actor to terminate immediately with a provided reason.
2653    pub fn kill(&self, reason: &str) -> Result<(), ActorError> {
2654        tracing::info!(
2655            actor_id = %self.inner.cell.actor_addr(),
2656            reason,
2657            "instance kill called",
2658        );
2659        self.inner.cell.signal(Signal::Kill(reason.to_string()))
2660    }
2661
2662    /// Backward-compatible alias for `kill()`.
2663    pub fn abort(&self, reason: &str) -> Result<(), ActorError> {
2664        tracing::info!(
2665            actor_id = %self.inner.cell.actor_addr(),
2666            reason,
2667            "instance abort called",
2668        );
2669        self.kill(reason)
2670    }
2671
2672    /// Close handler ingress for this actor.
2673    pub fn close(&self) {
2674        self.inner.delayed_posts.drain();
2675        self.inner.mailbox.drain();
2676    }
2677
2678    pub(crate) fn status(&self) -> watch::Receiver<ActorStatus> {
2679        self.inner.cell.status().clone()
2680    }
2681
2682    pub(crate) fn close_client(&self, reason: &str) {
2683        let status = ActorStatus::Stopped(reason.to_string());
2684        self.inner.mailbox.close(status.clone());
2685        self.change_status(status);
2686    }
2687
2688    /// Request immediate actor exit with the provided stop reason.
2689    pub fn exit(&self, reason: &str) -> Result<(), ActorError> {
2690        self.inner
2691            .cell
2692            .signal(Signal::ExitRequested(reason.to_string()))
2693    }
2694
2695    /// Queue an internal exit request after already accepted handler work.
2696    ///
2697    /// This is intentionally a small runtime special case for now.
2698    /// The long-term goal is to make "exit after drain" fall out of
2699    /// ordinary self-messaging semantics rather than requiring a
2700    /// dedicated internal path here.
2701    pub fn exit_after_drain(&self, reason: &str) -> Result<(), ActorError> {
2702        let this = self.clone_for_py();
2703        let reason = reason.to_string();
2704        let work = WorkCell::new(move |_actor: &mut A, _instance: &Instance<A>| {
2705            Box::pin(async move {
2706                this.exit(&reason).map_err(anyhow::Error::from)?;
2707                Ok(())
2708            })
2709        });
2710        self.enqueue_runtime_work(work)
2711    }
2712
2713    /// Open a new port that accepts M-typed messages. The returned
2714    /// port may be freely cloned, serialized, and passed around. The
2715    /// returned receiver should only be retained by the actor responsible
2716    /// for processing the delivered messages.
2717    pub fn open_port<M: Message>(&self) -> (PortHandle<M>, PortReceiver<M>) {
2718        self.inner.mailbox.open_port()
2719    }
2720
2721    /// Open a new one-shot port that accepts M-typed messages. The
2722    /// returned port may be used to send a single message; ditto the
2723    /// receiver may receive a single message.
2724    pub fn open_once_port<M: Message>(&self) -> (OncePortHandle<M>, OncePortReceiver<M>) {
2725        self.inner.mailbox.open_once_port()
2726    }
2727
2728    /// Return this actor's runtime signal sender.
2729    #[doc(hidden)]
2730    pub fn signal_sender(&self) -> mpsc::UnboundedSender<Signal> {
2731        self.inner.cell.signal_sender()
2732    }
2733
2734    /// Get the per-instance local storage.
2735    pub fn locals(&self) -> &ActorLocalStorage {
2736        &self.inner.instance_locals
2737    }
2738
2739    /// Send a message to the actor running on the proc.
2740    pub fn post(&self, port_id: impl Into<PortAddr>, headers: Flattrs, message: wirevalue::Any) {
2741        let port_id: PortAddr = port_id.into();
2742        <Self as context::MailboxExt>::post(
2743            self,
2744            port_id,
2745            headers,
2746            message,
2747            true,
2748            context::SeqInfoPolicy::AssignNew,
2749        )
2750    }
2751
2752    /// Post a message with pre-set SEQ_INFO. Only for internal use by CommActor.
2753    ///
2754    /// # Warning
2755    /// This method bypasses the SEQ_INFO assertion. Do not use unless you are
2756    /// implementing mesh-level message routing (CommActor).
2757    #[doc(hidden)]
2758    pub fn post_with_external_seq_info(
2759        &self,
2760        port_id: impl Into<PortAddr>,
2761        headers: Flattrs,
2762        message: wirevalue::Any,
2763    ) {
2764        <Self as context::MailboxExt>::post(
2765            self,
2766            port_id.into(),
2767            headers,
2768            message,
2769            true,
2770            context::SeqInfoPolicy::AllowExternal,
2771        )
2772    }
2773
2774    fn enqueue_runtime_work(&self, work: WorkCell<A>) -> Result<(), ActorError> {
2775        let actor_id_str = self.self_addr().to_string();
2776        account_enqueue(
2777            &self.inner.cell.inner.queue_depth,
2778            &self.inner.proc.state().queue_stats,
2779            &actor_id_str,
2780        );
2781        let result = self
2782            .inner
2783            .ports
2784            .workq
2785            .send(SequencedEnvelope::new(SeqInfo::Direct, None, work))
2786            .map_err(anyhow::Error::from);
2787        if result.is_err() {
2788            account_cancel_enqueue(
2789                &self.inner.cell.inner.queue_depth,
2790                &self.inner.proc.state().queue_stats,
2791                &actor_id_str,
2792            );
2793        }
2794        result.map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::processing(err)))
2795    }
2796
2797    /// Return a static client instance that can be used to send
2798    /// messages to port handles from outside an actor context
2799    /// (e.g. from background tokio tasks).
2800    // TODO: replace with a proper mechanism for sending to port
2801    // handles without an actor context.
2802    pub fn self_client() -> &'static Client {
2803        static CLIENT: OnceLock<Client> = OnceLock::new();
2804        CLIENT.get_or_init(|| Proc::global().client("self_message_client"))
2805    }
2806
2807    /// Post `message` to `dest` after `delay`.
2808    ///
2809    /// Delayed posts are owned by the actor runtime. They are best-effort:
2810    /// messages are posted no earlier than `delay`, and any delayed posts that
2811    /// have not fired when the actor shuts down are discarded.
2812    #[allow(private_bounds)]
2813    pub fn post_after<D, M>(&self, dest: D, message: M, delay: Duration)
2814    where
2815        M: Message,
2816        D: PostAfterEndpoint<A, M>,
2817    {
2818        let dest_location = dest.endpoint_location();
2819        if matches!(*self.inner.cell.status().borrow(), ActorStatus::Client) {
2820            self.report_delivery_failure(
2821                crate::mailbox::DeliveryFailureReport::link_unavailable::<M>(
2822                    self.mailbox().actor_addr().clone(),
2823                    dest_location,
2824                    "delayed posts require an actor runtime",
2825                ),
2826            );
2827            return;
2828        }
2829        let Ok(_guard) = self.inner.delayed_posts.ingress.try_enter() else {
2830            self.report_delivery_failure(
2831                crate::mailbox::DeliveryFailureReport::link_unavailable::<M>(
2832                    self.mailbox().actor_addr().clone(),
2833                    dest_location,
2834                    "actor runtime is stopping",
2835                ),
2836            );
2837            return;
2838        };
2839        if self.is_stopping() || self.is_terminal() {
2840            self.report_delivery_failure(
2841                crate::mailbox::DeliveryFailureReport::link_unavailable::<M>(
2842                    self.mailbox().actor_addr().clone(),
2843                    dest_location,
2844                    "actor runtime is stopping",
2845                ),
2846            );
2847            return;
2848        }
2849
2850        self.inner.delayed_posts.push(
2851            tokio::time::Instant::now() + delay,
2852            dest.into_delayed_post(message),
2853        );
2854    }
2855
2856    /// Start an A-typed actor onto this instance with the provided params. When spawn returns,
2857    /// the actor has been linked with its parent, if it has one.
2858    fn start(self, actor: A, receivers: InstanceReceivers<A>) -> ActorHandle<A> {
2859        let instance_cell = self.inner.cell.clone();
2860        let actor_id = self.inner.cell.actor_addr().clone();
2861        let actor_handle = ActorHandle::new(self.inner.cell.clone(), self.inner.ports.clone());
2862
2863        // Spawn the introspect task — a separate tokio task that
2864        // reads InstanceCell directly and replies through the owning Proc. The
2865        // actor loop never sees IntrospectMessage.
2866        self.spawn_introspect(receivers.introspect);
2867
2868        let actor_loop_receivers = receivers
2869            .actor_loop
2870            .expect("non-detached instance must have actor loop receivers");
2871        let actor_task_handle = A::spawn_server_task(
2872            panic_handler::with_backtrace_tracking(self.serve(
2873                actor,
2874                actor_loop_receivers,
2875                receivers.work,
2876            ))
2877            .instrument(Span::current()),
2878        );
2879        tracing::debug!("{}: spawned with {:?}", actor_id, actor_task_handle);
2880        instance_cell
2881            .inner
2882            .actor_task_handle
2883            .set(actor_task_handle)
2884            .unwrap_or_else(|_| panic!("{}: task handle store failed", actor_id));
2885
2886        actor_handle
2887    }
2888
2889    async fn serve(
2890        mut self,
2891        mut actor: A,
2892        actor_loop_receivers: (
2893            mpsc::UnboundedReceiver<Signal>,
2894            mpsc::UnboundedReceiver<ActorSupervisionEvent>,
2895        ),
2896        mut work_rx: ActorWorkReceiver<A>,
2897    ) {
2898        let result = self
2899            .run_actor_tree(&mut actor, actor_loop_receivers, &mut work_rx)
2900            .await;
2901
2902        assert!(self.is_stopping());
2903        // Compute the terminal status and supervision event, but defer
2904        // change_status until AFTER the event is delivered. If we flip
2905        // the status to terminal first, a concurrent destroy_and_wait
2906        // observer can release Phase 1 and stop the coordinator before
2907        // the event lands in its mailbox — dropping the event.
2908        let (terminal_status, event) = match result {
2909            Ok(stop_reason) => {
2910                let status = ActorStatus::Stopped(stop_reason);
2911                let event = ActorSupervisionEvent::new(
2912                    self.inner.cell.actor_addr().clone(),
2913                    actor.display_name(),
2914                    status.clone(),
2915                    None,
2916                );
2917                (status, Some(event))
2918            }
2919            Err(err) => match *err.kind {
2920                ActorErrorKind::UnhandledSupervisionEvent(box event) => {
2921                    // We use the event's actor_status as this actor's terminal status.
2922                    assert!(event.actor_status.is_terminal());
2923                    let status = event.actor_status.clone();
2924                    (status, Some(event))
2925                }
2926                _ => {
2927                    let error_kind = ActorErrorKind::Generic(err.kind.to_string());
2928                    let status = ActorStatus::Failed(error_kind);
2929                    let event = ActorSupervisionEvent::new(
2930                        self.inner.cell.actor_addr().clone(),
2931                        actor.display_name(),
2932                        status.clone(),
2933                        None,
2934                    );
2935                    (status, Some(event))
2936                }
2937            },
2938        };
2939
2940        // Supervision policy is driven by the event status, not the live cell status.
2941        // After teardown marks an actor zombie, publish that lifecycle verdict instead
2942        // of a late task failure so default supervision treats it as non-error.
2943        let current_status = self.inner.cell.status().borrow().clone();
2944        let event = if current_status.is_zombie() {
2945            Some(ActorSupervisionEvent::new(
2946                self.inner.cell.actor_addr().clone(),
2947                actor.display_name(),
2948                current_status,
2949                None,
2950            ))
2951        } else {
2952            event
2953        };
2954
2955        self.mailbox().close(terminal_status.clone());
2956        // FI-1: store supervision_event BEFORE change_status.
2957        if let Some(event) = &event {
2958            *self.inner.cell.inner.supervision_event.lock().unwrap() = Some(event.clone());
2959        }
2960
2961        // Deliver the supervision event to the parent/proc BEFORE
2962        // change_status so that any observer waiting for this actor's
2963        // terminal state can only see it once the event has been
2964        // enqueued at its destination.
2965        if let Some(parent) = self.inner.cell.maybe_unlink_parent() {
2966            if let Some(event) = event {
2967                // Parent exists, failure should be propagated to the parent.
2968                parent.send_supervision_event_or_crash(event);
2969            }
2970            // TODO: we should get rid of this signal, and use *only* supervision events for
2971            // the purpose of conveying lifecycle changes
2972            if let Err(err) = parent.signal(Signal::ChildStopped(self.inner.cell.uid().clone())) {
2973                tracing::error!(
2974                    "{}: failed to send stop message to parent uid {}: {:?}",
2975                    self.self_addr(),
2976                    parent.uid(),
2977                    err
2978                );
2979            }
2980        } else {
2981            // Failure happened to the root actor or orphaned child actors.
2982            // In either case, the failure should be propagated to proc.
2983            //
2984            // Note that orphaned actor is unexpected and would only happen if
2985            // there is a bug.
2986            if let Some(event) = event {
2987                self.inner
2988                    .proc
2989                    .handle_unhandled_supervision_event(&self, event);
2990            }
2991        }
2992
2993        self.stop_introspect(terminal_status.clone()).await;
2994        self.change_status(terminal_status);
2995    }
2996
2997    /// Runs the actor, and manages its supervision tree. When the function returns,
2998    /// the whole tree rooted at this actor has stopped. On success, returns the reason
2999    /// why the actor stopped. On failure, returns the error that caused the failure.
3000    async fn run_actor_tree(
3001        &mut self,
3002        actor: &mut A,
3003        mut actor_loop_receivers: (
3004            mpsc::UnboundedReceiver<Signal>,
3005            mpsc::UnboundedReceiver<ActorSupervisionEvent>,
3006        ),
3007        work_rx: &mut ActorWorkReceiver<A>,
3008    ) -> Result<String, ActorError> {
3009        // It is okay to catch all panics here, because we are in a tokio task,
3010        // and tokio will catch the panic anyway:
3011        // https://docs.rs/tokio/latest/tokio/task/struct.JoinError.html#method.is_panic
3012        // What we do here is just to catch it early so we can handle it.
3013
3014        let mut did_panic = false;
3015        let result = match AssertUnwindSafe(self.run(actor, &mut actor_loop_receivers, work_rx))
3016            .catch_unwind()
3017            .await
3018        {
3019            Ok(result) => result,
3020            Err(_) => {
3021                did_panic = true;
3022                let panic_info = panic_handler::take_panic_info()
3023                    .map(|info| info.to_string())
3024                    .unwrap_or_else(|e| format!("Cannot take backtrace due to: {:?}", e));
3025                Err(ActorError::new(
3026                    self.self_addr(),
3027                    ActorErrorKind::panic(anyhow::anyhow!(panic_info)),
3028                ))
3029            }
3030        };
3031
3032        assert!(!self.is_terminal());
3033        // `Zombie` is an out-of-band teardown verdict. Preserve it until the actor task
3034        // publishes its true terminal status.
3035        if !self.inner.cell.status().borrow().is_zombie() {
3036            self.change_status(ActorStatus::stopping());
3037        }
3038        if let Err(err) = &result {
3039            tracing::error!("{}: actor failure: {}", self.self_addr(), err);
3040        }
3041
3042        // After this point, we know we won't spawn any more children,
3043        // so we can safely read the current child keys.
3044        let mut to_unlink = Vec::new();
3045        let child_signal = match ChildTeardown::from_run_result(&result) {
3046            ChildTeardown::Cooperative(mode) => ChildTeardown::cooperative_signal(mode),
3047            ChildTeardown::Kill => {
3048                // TODO: fan out kill once child teardown can detach
3049                // unresponsive children without blocking this parent.
3050                ChildTeardown::cooperative_signal(StopMode::Stop)
3051            }
3052        };
3053        for child in self.inner.cell.child_iter() {
3054            if let Err(err) = child.value().signal(child_signal.clone()) {
3055                tracing::error!(
3056                    "{}: failed to send stop signal to child pid {}: {:?}",
3057                    self.self_addr(),
3058                    child.key(),
3059                    err
3060                );
3061                to_unlink.push(child.value().clone());
3062            }
3063        }
3064        // Manually unlink children that have already been stopped.
3065        for child in to_unlink {
3066            self.inner.cell.unlink(&child);
3067        }
3068
3069        let (mut signal_receiver, _) = actor_loop_receivers;
3070        while self.inner.cell.child_count() > 0 {
3071            match tokio::time::timeout(Duration::from_millis(500), signal_receiver.recv()).await {
3072                Ok(Some(Signal::ChildStopped(uid))) => {
3073                    assert!(self.inner.cell.get_child(&uid).is_none());
3074                }
3075                // Drain only tracks child termination; other signals are
3076                // intentionally swallowed here.
3077                Ok(Some(_)) => {}
3078                Ok(None) => {
3079                    // Signal channel closed: no further ChildStopped will
3080                    // arrive, so we can no longer track child termination.
3081                    // Drop remaining links and exit the drain loop, mirroring
3082                    // the timeout branch below.
3083                    self.inner.cell.unlink_all();
3084                    break;
3085                }
3086                Err(_) => {
3087                    tracing::warn!(
3088                        "timeout waiting for ChildStopped signal from child on actor: {}, ignoring",
3089                        self.self_addr()
3090                    );
3091                    // No more waiting to receive messages. Unlink all remaining
3092                    // children.
3093                    self.inner.cell.unlink_all();
3094                    break;
3095                }
3096            }
3097        }
3098        // Run the actor cleanup function before the actor stops to delete
3099        // resources. If it times out, continue with stopping the actor.
3100        // Don't call it if there was a panic, because the actor may
3101        // be in an invalid state and unable to access anything, for example
3102        // the GIL.
3103        let cleanup_result = if !did_panic {
3104            let cleanup_timeout = hyperactor_config::global::get(config::CLEANUP_TIMEOUT);
3105            match tokio::time::timeout(
3106                cleanup_timeout,
3107                self.inner
3108                    .proc
3109                    .with_current(actor.cleanup(self, result.as_ref().err())),
3110            )
3111            .await
3112            {
3113                Ok(Ok(x)) => Ok(x),
3114                Ok(Err(e)) => Err(ActorError::new(
3115                    self.self_addr(),
3116                    ActorErrorKind::cleanup(e),
3117                )),
3118                Err(e) => Err(ActorError::new(
3119                    self.self_addr(),
3120                    ActorErrorKind::cleanup(e.into()),
3121                )),
3122            }
3123        } else {
3124            Ok(())
3125        };
3126        if let Err(ref actor_err) = result {
3127            // The original result error takes precedence over the cleanup error,
3128            // so make sure the cleanup error is still logged in that case.
3129            if let Err(ref err) = cleanup_result {
3130                tracing::warn!(
3131                    cleanup_err = %err,
3132                    %actor_err,
3133                    "ignoring cleanup error after actor error",
3134                );
3135            }
3136        }
3137        // If the original exit was not an error, let cleanup errors be
3138        // surfaced.
3139        result.and_then(|stopped| cleanup_result.map(|_| stopped.reason))
3140    }
3141
3142    /// Initialize and run the actor until it fails or is stopped. On success,
3143    /// returns why the actor stopped and the mode that child actors inherit.
3144    /// On failure, returns the error that caused the failure.
3145    async fn run(
3146        &mut self,
3147        actor: &mut A,
3148        actor_loop_receivers: &mut (
3149            mpsc::UnboundedReceiver<Signal>,
3150            mpsc::UnboundedReceiver<ActorSupervisionEvent>,
3151        ),
3152        work_rx: &mut ActorWorkReceiver<A>,
3153    ) -> Result<ActorStopped, ActorError> {
3154        let (signal_receiver, supervision_event_receiver) = actor_loop_receivers;
3155        let mut stop_mode = StopMode::Stop;
3156
3157        self.change_status(ActorStatus::Initializing);
3158        self.inner
3159            .proc
3160            .with_current(actor.init(self))
3161            .await
3162            .map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::init(err)))?;
3163        let actor_id_str = self.self_addr().to_string();
3164        let stop_reason = 'messages: loop {
3165            if !self.is_stopping() {
3166                self.change_status(ActorStatus::Idle);
3167            }
3168            let next_delayed_deadline = self.inner.delayed_posts.next_deadline();
3169            let metric_pairs = hyperactor_telemetry::kv_pairs!("actor_id" => actor_id_str.clone());
3170            tokio::select! {
3171                biased;
3172                signal = signal_receiver.recv() => {
3173                    let signal = signal.ok_or_else(|| {
3174                        ActorError::new(self.self_addr(), ActorErrorKind::SignalChannelClosed)
3175                    })?;
3176                    tracing::debug!("received signal {signal:?}");
3177                    match signal {
3178                        Signal::Stop(reason) => {
3179                            stop_mode = StopMode::Stop;
3180                            self.change_status(ActorStatus::stopping());
3181                            self.inner
3182                                .proc
3183                                .with_current(actor.handle_stop(self, StopMode::Stop, &reason))
3184                                .await
3185                                .map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::processing(err)))?;
3186                        },
3187                        Signal::DrainAndStop(reason) => {
3188                            stop_mode = StopMode::DrainAndStop;
3189                            self.change_status(ActorStatus::stopping());
3190                            self.inner
3191                                .proc
3192                                .with_current(actor.handle_stop(self, StopMode::DrainAndStop, &reason))
3193                                .await
3194                                .map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::processing(err)))?;
3195                        },
3196                        Signal::ChildStopped(uid) => {
3197                            assert!(self.inner.cell.get_child(&uid).is_none());
3198                        },
3199                        Signal::ExitRequested(reason) => {
3200                            break 'messages reason;
3201                        }
3202                        Signal::Kill(reason) => {
3203                            return Err(ActorError { actor_id: Box::new(self.self_addr().clone()), kind: Box::new(ActorErrorKind::Aborted(reason)) });
3204                        }
3205                    }
3206                }
3207                work = work_rx.recv() => {
3208                    ACTOR_MESSAGES_RECEIVED.add(1, metric_pairs);
3209                    account_dequeue(&self.inner.cell.inner.queue_depth, &self.inner.proc.state().queue_stats, &actor_id_str);
3210                    let _ = ACTOR_MESSAGE_HANDLER_DURATION.start(metric_pairs);
3211                    let work = work.expect("inconsistent work queue state");
3212                    if let Err(err) = work.handle(actor, self).await {
3213                        while let Ok(supervision_event) = supervision_event_receiver.try_recv() {
3214                            self.handle_supervision_event(actor, supervision_event).await?;
3215                        }
3216                        let kind = ActorErrorKind::processing(err);
3217                        return Err(ActorError {
3218                            actor_id: Box::new(self.self_addr().clone()),
3219                            kind: Box::new(kind),
3220                        });
3221                    }
3222                }
3223                _ = self.inner.delayed_posts.notify.notified(), if !self.is_stopping() && !self.inner.delayed_posts.is_draining() => {
3224                }
3225                _ = async {
3226                    match next_delayed_deadline {
3227                        Some(deadline) => tokio::time::sleep_until(deadline).await,
3228                        None => std::future::pending::<()>().await,
3229                    }
3230                }, if !self.is_stopping() && !self.inner.delayed_posts.is_draining() && next_delayed_deadline.is_some() => {
3231                    let now = tokio::time::Instant::now();
3232                    if let Ok(_guard) = self.inner.delayed_posts.ingress.try_enter() {
3233                        for post in self.inner.delayed_posts.pop_due(now) {
3234                            post(self);
3235                        }
3236                    }
3237                }
3238                Some(supervision_event) = supervision_event_receiver.recv() => {
3239                    self.handle_supervision_event(actor, supervision_event).await?;
3240                }
3241            }
3242            self.inner
3243                .cell
3244                .inner
3245                .num_processed_messages
3246                .fetch_add(1, Ordering::SeqCst);
3247        };
3248        tracing::debug!(
3249            actor_id = %self.self_addr(),
3250            reason = stop_reason,
3251            "exited actor loop",
3252        );
3253        Ok(ActorStopped {
3254            reason: stop_reason,
3255            stop_mode,
3256        })
3257    }
3258
3259    /// Handle a supervision event using the provided actor.
3260    pub async fn handle_supervision_event(
3261        &self,
3262        actor: &mut A,
3263        supervision_event: ActorSupervisionEvent,
3264    ) -> Result<(), ActorError> {
3265        // Handle the supervision event with the current actor.
3266        match self
3267            .inner
3268            .proc
3269            .with_current(actor.handle_supervision_event(self, &supervision_event))
3270            .await
3271        {
3272            Ok(true) => {
3273                // The supervision event was handled by this actor, nothing more to do.
3274                Ok(())
3275            }
3276            Ok(false) => {
3277                let kind = ActorErrorKind::UnhandledSupervisionEvent(Box::new(supervision_event));
3278                Err(ActorError::new(self.self_addr(), kind))
3279            }
3280            Err(err) => {
3281                // The actor failed to handle the supervision event, it should die.
3282                // Create a new supervision event for this failure and propagate it.
3283                let kind = ActorErrorKind::ErrorDuringHandlingSupervision(
3284                    err.to_string(),
3285                    Box::new(supervision_event),
3286                );
3287                Err(ActorError::new(self.self_addr(), kind))
3288            }
3289        }
3290    }
3291
3292    async unsafe fn handle_message<M: Message>(
3293        &self,
3294        actor: &mut A,
3295        type_info: Option<&'static TypeInfo>,
3296        headers: Flattrs,
3297        message: M,
3298    ) -> Result<(), anyhow::Error>
3299    where
3300        A: Handler<M>,
3301    {
3302        // Build HandlerInfo from TypeInfo (zero-copy) or fall back to type_name.
3303        let handler_info = match type_info {
3304            Some(info) => {
3305                // SAFETY: The caller promises to pass the correct type info.
3306                let arm = unsafe { info.arm_unchecked(&message as *const M as *const ()) };
3307                HandlerInfo::from_static(info.typename(), arm)
3308            }
3309            None => {
3310                // Fall back to std::any::type_name (also static, zero-copy).
3311                HandlerInfo::from_static(std::any::type_name::<M>(), None)
3312            }
3313        };
3314
3315        let endpoint = type_info.and_then(|info| {
3316            // SAFETY: The caller promises to pass the correct type info.
3317            unsafe { info.endpoint_name(&message as *const M as *const ()) }
3318        });
3319
3320        // Use a helper function for a better instrument log.
3321        self.handle_message_with_handler_info(actor, handler_info, headers, message, endpoint)
3322            .await
3323    }
3324
3325    #[tracing::instrument(level = "debug", name = "handle_message", skip_all, fields(message_type = %handler_info))]
3326    async fn handle_message_with_handler_info<M: Message>(
3327        &self,
3328        actor: &mut A,
3329        handler_info: HandlerInfo,
3330        headers: Flattrs,
3331        message: M,
3332        endpoint: Option<String>,
3333    ) -> Result<(), anyhow::Error>
3334    where
3335        A: Handler<M>,
3336    {
3337        let now = std::time::SystemTime::now();
3338        let handler_info = Some(handler_info);
3339        self.change_status(ActorStatus::Processing(now, handler_info.clone()));
3340        crate::mailbox::headers::log_message_latency_if_sampling(
3341            &headers,
3342            self.self_addr().to_string(),
3343        );
3344
3345        let message_id = headers.get(crate::mailbox::headers::TELEMETRY_MESSAGE_ID);
3346
3347        if let Some(message_id) = message_id {
3348            let from_actor_id = headers
3349                .get(crate::mailbox::headers::SENDER_ACTOR_ID_HASH)
3350                .unwrap_or(0);
3351            let to_actor_id = hash_to_u64(self.self_addr().id());
3352            let port_index = headers.get(crate::mailbox::headers::TELEMETRY_PORT_INDEX);
3353
3354            notify_message(hyperactor_telemetry::MessageEvent {
3355                timestamp: now,
3356                id: message_id,
3357                from_actor_id,
3358                to_actor_id,
3359                endpoint,
3360                port_index,
3361            });
3362
3363            notify_message_status(hyperactor_telemetry::MessageStatusEvent {
3364                timestamp: now,
3365                id: hyperactor_telemetry::generate_status_event_id(message_id),
3366                message_id,
3367                status: "active".to_string(),
3368            });
3369        }
3370
3371        // Record the message handler being invoked.
3372        *self.inner.cell.inner.last_message_handler.write().unwrap() = handler_info;
3373
3374        let context = Context::new(self, headers);
3375        // Pass a reference to the context to the handler, so that deref
3376        // coercion allows the `this` argument to be treated exactly like
3377        // &Instance<A>.
3378        let start = Instant::now();
3379        let subject_str = self.self_addr().subject().to_string();
3380        let result = self
3381            .inner
3382            .proc
3383            .with_current(actor.handle(&context, message))
3384            .instrument(self.inner.cell.inner.recording.span(&subject_str))
3385            .await;
3386        let elapsed_us = start.elapsed().as_micros() as u64;
3387        self.inner
3388            .cell
3389            .inner
3390            .total_processing_time_us
3391            .fetch_add(elapsed_us, Ordering::SeqCst);
3392
3393        if let Some(message_id) = message_id {
3394            notify_message_status(hyperactor_telemetry::MessageStatusEvent {
3395                timestamp: std::time::SystemTime::now(),
3396                id: hyperactor_telemetry::generate_status_event_id(message_id),
3397                message_id,
3398                status: "complete".to_string(),
3399            });
3400        }
3401
3402        result
3403    }
3404
3405    /// Spawn a child actor with a fresh uid labeled from the actor type.
3406    pub fn spawn<C: Actor>(&self, actor: C) -> ActorHandle<C> {
3407        self.inner.proc.spawn_child(self.inner.cell.clone(), actor)
3408    }
3409
3410    /// Spawn a named child actor on this instance. The child gets a
3411    /// descriptive name in its ActorId instead of inheriting this
3412    /// instance's name. Supervision linkage is preserved.
3413    pub fn spawn_with_name<C: Actor>(&self, name: &str, actor: C) -> ActorHandle<C> {
3414        self.inner
3415            .proc
3416            .spawn_named_child(self.inner.cell.clone(), name, actor)
3417    }
3418
3419    /// Spawn a child actor with a fresh uid carrying a display label.
3420    ///
3421    /// The label is descriptive only and does not participate in actor
3422    /// identity. Supervision linkage to this instance is preserved.
3423    pub fn spawn_with_label<C: Actor>(&self, label: &str, actor: C) -> ActorHandle<C> {
3424        self.inner
3425            .proc
3426            .spawn_named_child(self.inner.cell.clone(), label, actor)
3427    }
3428
3429    /// Spawn a child actor on this instance using an explicit uid.
3430    ///
3431    /// This is the explicit identity API, and the only child spawn API that
3432    /// permits singleton actor identity. Instance labels, if present, are
3433    /// descriptive only and do not affect uniqueness.
3434    pub fn spawn_with_uid<C: Actor>(&self, uid: Uid, actor: C) -> anyhow::Result<ActorHandle<C>> {
3435        self.inner
3436            .proc
3437            .spawn_child_with_uid(self.inner.cell.clone(), uid, actor)
3438    }
3439
3440    /// Create a new direct child instance.
3441    pub fn child(&self) -> (Instance<()>, ActorHandle<()>) {
3442        self.inner.proc.child_instance(self.inner.cell.clone())
3443    }
3444
3445    /// Spawn a registered actor as this instance's child.
3446    ///
3447    /// The actor type is resolved through the remote spawn registry. The child
3448    /// receives an empty environment.
3449    pub async fn gspawn(&self, actor_type: &str, params: Data) -> anyhow::Result<AnyActorHandle> {
3450        self.gspawn_uid(actor_type, crate::id::Uid::anonymous(), params)
3451            .await
3452    }
3453
3454    /// Spawn a registered actor as this instance's child using an explicit uid.
3455    ///
3456    /// The actor type is resolved through the remote spawn registry. The child
3457    /// receives an empty environment.
3458    pub async fn gspawn_uid(
3459        &self,
3460        actor_type: &str,
3461        uid: crate::id::Uid,
3462        params: Data,
3463    ) -> anyhow::Result<AnyActorHandle> {
3464        crate::actor::remote::Remote::global()
3465            .gspawn_child(
3466                &self.inner.proc,
3467                self.inner.cell.clone(),
3468                actor_type,
3469                uid,
3470                params,
3471                Flattrs::default(),
3472            )
3473            .await
3474    }
3475
3476    /// Return a handler port handle representing the actor's message
3477    /// handler for M-typed messages.
3478    pub fn port<M: Message>(&self) -> PortHandle<M>
3479    where
3480        A: Handler<M>,
3481    {
3482        self.inner.ports.get()
3483    }
3484
3485    /// The [`ActorHandle`] corresponding to this instance.
3486    pub fn handle(&self) -> ActorHandle<A> {
3487        ActorHandle::new(self.inner.cell.clone(), Arc::clone(&self.inner.ports))
3488    }
3489
3490    /// The owning actor ref.
3491    pub fn bind<R: Binds<A>>(&self) -> ActorRef<R> {
3492        self.inner.cell.bind(self.inner.ports.as_ref())
3493    }
3494
3495    // Temporary in order to support python bindings.
3496    #[doc(hidden)]
3497    pub fn mailbox_for_py(&self) -> &Mailbox {
3498        &self.inner.mailbox
3499    }
3500
3501    /// The owning proc.
3502    pub fn proc(&self) -> &Proc {
3503        &self.inner.proc
3504    }
3505
3506    /// Clone this Instance to get an owned struct that can be
3507    /// plumbed through python. This should really only be called
3508    /// for the explicit purpose of being passed into python
3509    #[doc(hidden)]
3510    pub fn clone_for_py(&self) -> Self {
3511        Self {
3512            inner: Arc::clone(&self.inner),
3513        }
3514    }
3515
3516    /// Get the join handle associated with this actor.
3517    fn actor_task_handle(&self) -> Option<&JoinHandle<()>> {
3518        self.inner.cell.inner.actor_task_handle.get()
3519    }
3520
3521    /// Return this instance's sequencer.
3522    pub fn sequencer(&self) -> &Sequencer {
3523        &self.inner.sequencer
3524    }
3525
3526    /// Reserve (consume) the next `count` ordering sequence numbers for
3527    /// the given destination without posting any messages. Subsequent
3528    /// normal sends to this destination pick up at `last_reserved + 1`,
3529    /// creating a deterministic gap from the receiver's perspective.
3530    ///
3531    /// Test/demo only. Production code should not call this; misuse will
3532    /// produce stalled receivers. Marked `#[doc(hidden)]`; review
3533    /// discipline is the misuse defense.
3534    #[doc(hidden)]
3535    pub fn debug_skip_next_ordering_seq(&self, dest: &PortAddr, count: u64) {
3536        let sequencer = self.sequencer();
3537        for _ in 0..count {
3538            let _ = sequencer.assign_seq(dest);
3539        }
3540    }
3541
3542    /// Return this instance's ID.
3543    pub fn instance_id(&self) -> Uuid {
3544        self.inner.id
3545    }
3546
3547    /// Return a handle to this instance's parent actor, if it has one.
3548    pub fn parent_handle<P: Actor>(&self) -> Option<ActorHandle<P>> {
3549        let parent_cell = self.inner.cell.inner.parent.upgrade()?;
3550        let ports = if let Ok(ports) = parent_cell.inner.ports.clone().downcast() {
3551            ports
3552        } else {
3553            return None;
3554        };
3555        Some(ActorHandle::new(parent_cell, ports))
3556    }
3557}
3558
3559impl Instance<ClientActor> {
3560    pub(crate) fn child_client(&self) -> Client {
3561        let actor_id = self
3562            .inner
3563            .proc
3564            .allocate_anonymous_child_id(self.inner.cell.actor_addr());
3565        let (instance, _receivers) = Instance::new(
3566            self.inner.proc.clone(),
3567            actor_id,
3568            false,
3569            Some(self.inner.cell.clone()),
3570        );
3571        instance.change_status(ActorStatus::Client);
3572        Client::new(instance)
3573    }
3574}
3575
3576impl<A: Actor> context::Mailbox for Instance<A> {
3577    fn mailbox(&self) -> &Mailbox {
3578        &self.inner.mailbox
3579    }
3580}
3581
3582impl<A: Actor> context::Mailbox for Context<'_, A> {
3583    fn mailbox(&self) -> &Mailbox {
3584        &self.instance.inner.mailbox
3585    }
3586}
3587
3588impl<A: Actor> context::Mailbox for &Instance<A> {
3589    fn mailbox(&self) -> &Mailbox {
3590        &self.inner.mailbox
3591    }
3592}
3593
3594impl<A: Actor> context::Mailbox for &Context<'_, A> {
3595    fn mailbox(&self) -> &Mailbox {
3596        &self.instance.inner.mailbox
3597    }
3598}
3599
3600impl<A: Actor> context::Actor for Instance<A> {
3601    type A = A;
3602    fn instance(&self) -> &Instance<A> {
3603        self
3604    }
3605}
3606
3607impl<A: Actor> context::Actor for Context<'_, A> {
3608    type A = A;
3609    fn instance(&self) -> &Instance<A> {
3610        self
3611    }
3612
3613    fn headers(&self) -> &Flattrs {
3614        Context::headers(self)
3615    }
3616}
3617
3618impl<A: Actor> context::Actor for &Instance<A> {
3619    type A = A;
3620    fn instance(&self) -> &Instance<A> {
3621        self
3622    }
3623}
3624
3625impl<A: Actor> context::Actor for &Context<'_, A> {
3626    type A = A;
3627    fn instance(&self) -> &Instance<A> {
3628        self
3629    }
3630
3631    fn headers(&self) -> &Flattrs {
3632        Context::headers(self)
3633    }
3634}
3635
3636impl<A, M> crate::Endpoint<M> for &Instance<A>
3637where
3638    A: Actor + Handler<M>,
3639    M: Message,
3640{
3641    fn endpoint_location(&self) -> crate::EndpointLocation {
3642        crate::EndpointLocation::Actor(self.self_addr().clone())
3643    }
3644
3645    fn post<C>(self, cx: &C, message: M)
3646    where
3647        C: context::Actor,
3648    {
3649        let port = self.port();
3650        crate::Endpoint::post(&port, cx, message)
3651    }
3652}
3653
3654impl<A, M> crate::Endpoint<M> for &Context<'_, A>
3655where
3656    A: Actor + Handler<M>,
3657    M: Message,
3658{
3659    fn endpoint_location(&self) -> crate::EndpointLocation {
3660        crate::EndpointLocation::Actor(self.self_addr().clone())
3661    }
3662
3663    fn post<C>(self, cx: &C, message: M)
3664    where
3665        C: context::Actor,
3666    {
3667        crate::Endpoint::post(self.instance, cx, message)
3668    }
3669}
3670
3671impl<A, M> crate::Endpoint<M> for Instance<A>
3672where
3673    A: Actor + Handler<M>,
3674    M: Message,
3675{
3676    fn endpoint_location(&self) -> crate::EndpointLocation {
3677        crate::EndpointLocation::Actor(self.self_addr().clone())
3678    }
3679
3680    fn post<C>(self, cx: &C, message: M)
3681    where
3682        C: context::Actor,
3683    {
3684        crate::Endpoint::post(&self, cx, message)
3685    }
3686}
3687
3688impl Instance<()> {
3689    /// See [Mailbox::bind_handler_port] for details.
3690    pub fn bind_handler_port<M: RemoteMessage>(&self) -> (PortHandle<M>, PortReceiver<M>) {
3691        assert!(
3692            self.actor_task_handle().is_none(),
3693            "can only bind handler port on instance with no running actor task"
3694        );
3695        self.inner.mailbox.bind_handler_port()
3696    }
3697}
3698
3699#[derive(Debug)]
3700enum ActorType {
3701    Named(&'static TypeInfo),
3702    Anonymous(&'static str),
3703}
3704
3705impl ActorType {
3706    fn type_name(&self) -> &str {
3707        match self {
3708            ActorType::Named(info) => info.typename(),
3709            ActorType::Anonymous(name) => name,
3710        }
3711    }
3712}
3713
3714/// InstanceCell contains all of the type-erased, shareable state of an instance.
3715/// Specifically, InstanceCells form a supervision tree, and is used by ActorHandle
3716/// to access the underlying instance.
3717///
3718/// InstanceCell is reference counted and cloneable.
3719#[derive(Clone)]
3720pub struct InstanceCell {
3721    inner: Arc<InstanceCellState>,
3722}
3723
3724impl fmt::Debug for InstanceCell {
3725    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3726        f.debug_struct("InstanceCell")
3727            .field("actor_id", &self.inner.actor_id)
3728            .field("actor_type", &self.inner.actor_type)
3729            .finish()
3730    }
3731}
3732
3733struct InstanceCellState {
3734    /// The actor's id.
3735    actor_id: ActorAddr,
3736
3737    /// The actor instance's `Uuid::now_v7()` identity. Stable for the
3738    /// lifetime of this instance; surfaced via `InstanceCell::instance_id`
3739    /// and the `INSTANCE_ID` introspection attr.
3740    instance_id: Uuid,
3741
3742    /// Actor info contains the actor's type information.
3743    actor_type: ActorType,
3744
3745    /// The proc in which the actor is running.
3746    proc: Proc,
3747
3748    /// Control plane message senders to the actor loop, if one is running.
3749    actor_loop: Option<(
3750        mpsc::UnboundedSender<Signal>,
3751        mpsc::UnboundedSender<ActorSupervisionEvent>,
3752    )>,
3753
3754    /// A watch for communicating the actor's state.
3755    status_tx: watch::Sender<ActorStatus>,
3756
3757    /// An observer that stores the current status of the actor.
3758    status: watch::Receiver<ActorStatus>,
3759
3760    /// A weak reference to this instance's parent.
3761    parent: WeakInstanceCell,
3762
3763    /// This instance's children by their uids.
3764    children: DashMap<crate::id::Uid, InstanceCell>,
3765
3766    /// Access to the spawned actor's join handle.
3767    actor_task_handle: OnceLock<JoinHandle<()>>,
3768
3769    /// The set of named ports that are exported by this actor.
3770    exported_named_ports: DashMap<Port, &'static str>,
3771
3772    /// The number of messages processed by this actor.
3773    num_processed_messages: AtomicU64,
3774
3775    /// When this actor was created.
3776    created_at: SystemTime,
3777
3778    /// Name of the last message handler invoked.
3779    last_message_handler: RwLock<Option<HandlerInfo>>,
3780
3781    /// Total time spent processing messages, in microseconds.
3782    total_processing_time_us: AtomicU64,
3783
3784    /// Current actor work-queue depth.
3785    ///
3786    /// Two consumers of one accounting path (PD-5e): this field is
3787    /// the introspection-readable state; the OTel
3788    /// `ACTOR_MESSAGE_QUEUE_SIZE` counter is the telemetry export.
3789    /// Both are updated together by `account_enqueue` /
3790    /// `account_dequeue`.
3791    ///
3792    /// Shared with `HandlerPorts<A>`: incremented at enqueue in the send
3793    /// path, decremented when the actor loop receives from `work_rx`.
3794    queue_depth: Arc<AtomicU64>,
3795
3796    /// The log recording associated with this actor. It is used to
3797    /// store a 'flight record' of events while the actor is running.
3798    recording: Recording,
3799
3800    /// Attrs-based introspection data published by the actor. Written
3801    /// by the actor via `Instance::publish_attrs()` /
3802    /// `Instance::publish_attr()`, and read by the introspection
3803    /// runtime handler when building node payloads.
3804    ///
3805    /// This bag may contain both mesh-level keys (`node_type`,
3806    /// `addr`, `num_procs`, ...) and actor-runtime keys (`status`,
3807    /// `messages_processed`, ...).
3808    published_attrs: RwLock<Option<hyperactor_config::Attrs>>,
3809
3810    /// Optional callback for resolving non-addressable children
3811    /// (e.g., system procs). Registered by infrastructure actors
3812    /// like `HostAgent` in `Actor::init`. Invoked by the
3813    /// introspection runtime handler for `QueryChild` messages.
3814    /// `None` means `QueryChild` returns a "not_found" error.
3815    ///
3816    /// See S7 in `introspect` module doc.
3817    query_child_handler: RwLock<Option<Box<dyn (Fn(&Addr) -> IntrospectResult) + Send + Sync>>>,
3818
3819    /// The supervision event for this actor's failure, if any.
3820    /// See FI-1, FI-2 in `introspect` module doc.
3821    supervision_event: std::sync::Mutex<Option<crate::supervision::ActorSupervisionEvent>>,
3822
3823    /// Whether this actor is infrastructure/system (hidden by default
3824    /// in the TUI `s` toggle). Set by spawning code via
3825    /// `Instance::set_system()`.
3826    is_system: AtomicBool,
3827
3828    /// A type-erased reference to HandlerPorts<A>, which allows us to
3829    /// recover an ActorHandle<A> by downcasting.
3830    ports: Arc<dyn Any + Send + Sync>,
3831
3832    /// Type-erased snapshot callback for inbound ordering state.
3833    /// Captured at `Instance::new` from the typed `Arc<HandlerPorts<A>>`
3834    /// (where `A` is in scope), erased to `dyn Fn() -> OrderingSnapshot`
3835    /// so non-generic code in `InstanceCellState` can invoke it.
3836    ///
3837    /// Hygiene: the closure captures ONLY `Arc<HandlerPorts<A>>::clone()`
3838    /// — never `Instance<A>` or `InstanceCell` — to avoid cyclic refs
3839    /// back to the cell that holds this callback. Body is a single
3840    /// `workq.snapshot()` call; bounded work, `try_lock`-based, never
3841    /// blocks. See IO-1/IO-2 in `introspect` module doc.
3842    ///
3843    /// `None` only for hand-built fixtures or future code paths that
3844    /// construct an `InstanceCellState` without going through
3845    /// `Instance::new`; production live actors always install Some.
3846    inbound_ordering_snapshot:
3847        Option<Box<dyn Fn() -> crate::ordering::OrderingSnapshot + Send + Sync>>,
3848
3849    /// Type-erased snapshot callback for actor-supplied introspection
3850    /// attrs. Installed by the actor (e.g. in `Actor::init`) via
3851    /// `Instance::set_attrs_snapshot`; invoked by the introspect task
3852    /// (outside the actor loop) in `build_actor_attrs`, where the
3853    /// returned `Attrs` are merged into the Actor view with core keys
3854    /// winning on collision (AS-2).
3855    ///
3856    /// Generic: core does not interpret the keys (AS-1). The callback
3857    /// must be non-blocking (`try_lock` internally); it is
3858    /// `catch_unwind`-guarded at the call site so a panic degrades to no
3859    /// extra attrs rather than breaking introspection (AS-3). `None`
3860    /// means the actor publishes no extra attrs.
3861    actor_attrs_snapshot: RwLock<Option<Box<dyn Fn() -> hyperactor_config::Attrs + Send + Sync>>>,
3862}
3863
3864impl InstanceCellState {
3865    /// Unlink this instance from its parent, if it has one. If it was unlinked,
3866    /// the parent is returned.
3867    fn maybe_unlink_parent(&self) -> Option<InstanceCell> {
3868        self.parent
3869            .upgrade()
3870            .filter(|parent| parent.inner.unlink(self))
3871    }
3872
3873    /// Unlink this instance from a child.
3874    fn unlink(&self, child: &InstanceCellState) -> bool {
3875        assert_eq!(self.actor_id.proc_id(), child.actor_id.proc_id());
3876        self.children.remove(child.actor_id.uid()).is_some()
3877    }
3878}
3879
3880/// Select which terminated snapshots to evict when the retention cap
3881/// is exceeded.
3882///
3883/// Each entry is `(actor_id, Option<occurred_at>)` where `Some` means
3884/// the actor has `failure_info` (i.e. it failed), and `None` means a
3885/// clean stop.
3886///
3887/// Eviction priority:
3888/// 1. Cleanly-stopped actors are evicted first (arbitrary order).
3889/// 2. If more evictions are needed, failed actors are evicted
3890///    newest-first (descending `occurred_at`), preserving the
3891///    earliest failures which are closest to the root cause.
3892fn select_eviction_candidates(
3893    entries: &[(ActorAddr, Option<String>)],
3894    excess: usize,
3895) -> Vec<ActorAddr> {
3896    let mut clean: Vec<&ActorAddr> = Vec::new();
3897    let mut failed: Vec<(&ActorAddr, &str)> = Vec::new();
3898    for (id, occurred_at) in entries {
3899        match occurred_at {
3900            Some(ts) => failed.push((id, ts.as_str())),
3901            None => clean.push(id),
3902        }
3903    }
3904
3905    let mut to_remove: Vec<ActorAddr> = Vec::new();
3906    let mut remaining = excess;
3907
3908    // Evict cleanly-stopped first.
3909    for id in clean {
3910        if remaining == 0 {
3911            break;
3912        }
3913        to_remove.push(id.clone());
3914        remaining -= 1;
3915    }
3916
3917    // If still over cap, evict most-recent failures first.
3918    if remaining > 0 {
3919        failed.sort_by(|a, b| b.1.cmp(a.1));
3920        for (id, _) in failed.into_iter().take(remaining) {
3921            to_remove.push(id.clone());
3922        }
3923    }
3924
3925    to_remove
3926}
3927
3928#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3929enum IllegalStatusChange {
3930    LinkedZombie,
3931    TerminalRewrite,
3932}
3933
3934#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3935enum StatusChange {
3936    Apply,
3937    Ignore,
3938    Illegal(IllegalStatusChange),
3939}
3940
3941fn classify_status_change(old: &ActorStatus, new: &ActorStatus) -> StatusChange {
3942    // This classifier only covers status ordering. Actor-topology checks belong
3943    // at the mutation site that can inspect the instance tree.
3944    if old == new || old.is_zombie() && !new.is_terminal() || old.is_terminal() && new.is_zombie() {
3945        StatusChange::Ignore
3946    } else if old.is_terminal() {
3947        StatusChange::Illegal(IllegalStatusChange::TerminalRewrite)
3948    } else {
3949        StatusChange::Apply
3950    }
3951}
3952
3953impl InstanceCell {
3954    /// Creates a new instance cell with the provided internal state. If a parent
3955    /// is provided, it is linked to this cell.
3956    #[allow(clippy::too_many_arguments)]
3957    fn new(
3958        actor_id: ActorAddr,
3959        instance_id: Uuid,
3960        actor_type: ActorType,
3961        proc: Proc,
3962        actor_loop: Option<(
3963            mpsc::UnboundedSender<Signal>,
3964            mpsc::UnboundedSender<ActorSupervisionEvent>,
3965        )>,
3966        status_tx: watch::Sender<ActorStatus>,
3967        status: watch::Receiver<ActorStatus>,
3968        parent: Option<InstanceCell>,
3969        ports: Arc<dyn Any + Send + Sync>,
3970        queue_depth: Arc<AtomicU64>,
3971        inbound_ordering_snapshot: Option<
3972            Box<dyn Fn() -> crate::ordering::OrderingSnapshot + Send + Sync>,
3973        >,
3974    ) -> Self {
3975        let is_root = parent.is_none();
3976        let _ais = actor_id.to_string();
3977        let cell = Self {
3978            inner: Arc::new(InstanceCellState {
3979                actor_id: actor_id.clone(),
3980                instance_id,
3981                actor_type,
3982                proc: proc.clone(),
3983                actor_loop,
3984                status_tx,
3985                status,
3986                parent: parent.map_or_else(WeakInstanceCell::new, |cell| cell.downgrade()),
3987                children: DashMap::new(),
3988                actor_task_handle: OnceLock::new(),
3989                exported_named_ports: DashMap::new(),
3990                num_processed_messages: AtomicU64::new(0),
3991                created_at: std::time::SystemTime::now(),
3992                last_message_handler: RwLock::new(None),
3993                total_processing_time_us: AtomicU64::new(0),
3994                queue_depth,
3995                recording: hyperactor_telemetry::recorder().record(64),
3996                published_attrs: RwLock::new(None),
3997                query_child_handler: RwLock::new(None),
3998                supervision_event: std::sync::Mutex::new(None),
3999                is_system: AtomicBool::new(false),
4000                ports,
4001                inbound_ordering_snapshot,
4002                actor_attrs_snapshot: RwLock::new(None),
4003            }),
4004        };
4005        cell.maybe_link_parent();
4006        // TODO: disallow reuse; maybe only for instance ids.
4007        proc.inner.actor_tombstones.remove(actor_id.id());
4008        proc.inner
4009            .instances
4010            .insert(actor_id.id().clone(), cell.downgrade());
4011        if is_root {
4012            proc.inner.root_actors.insert(actor_id.id().clone());
4013        }
4014        cell
4015    }
4016
4017    fn wrap(inner: Arc<InstanceCellState>) -> Self {
4018        Self { inner }
4019    }
4020
4021    /// The actor's address.
4022    pub fn actor_addr(&self) -> &ActorAddr {
4023        &self.inner.actor_id
4024    }
4025
4026    /// The proc in which this actor is running.
4027    pub(crate) fn proc(&self) -> &Proc {
4028        &self.inner.proc
4029    }
4030
4031    /// The actor's uid.
4032    pub(crate) fn uid(&self) -> &crate::id::Uid {
4033        self.inner.actor_id.uid()
4034    }
4035
4036    /// The actor's join handle.
4037    #[allow(dead_code)]
4038    pub(crate) fn actor_task_handle(&self) -> Option<&JoinHandle<()>> {
4039        self.inner.actor_task_handle.get()
4040    }
4041
4042    /// The instance's status observer.
4043    pub fn status(&self) -> &watch::Receiver<ActorStatus> {
4044        &self.inner.status
4045    }
4046
4047    /// Notify subscribers of a change in the actors status and bump counters with the duration which
4048    /// the last status was active for.
4049    #[track_caller]
4050    fn change_status(&self, new: ActorStatus) {
4051        let mut old_status = None;
4052        let mut illegal_status = None;
4053        let actor_id = self.actor_addr().id().clone();
4054        let changed = self.inner.status_tx.send_if_modified(|status| {
4055            let old = status.clone();
4056            let mut status_change = classify_status_change(&old, &new);
4057            if status_change == StatusChange::Apply && new.is_zombie() && self.parent().is_some() {
4058                status_change = StatusChange::Illegal(IllegalStatusChange::LinkedZombie);
4059            }
4060
4061            match status_change {
4062                StatusChange::Apply => {}
4063                StatusChange::Ignore => return false,
4064                StatusChange::Illegal(reason) => {
4065                    // Leave the watch update closure before panicking so the
4066                    // status lock is not held during unwind.
4067                    illegal_status = Some((old, reason));
4068                    return false;
4069                }
4070            }
4071
4072            if new.is_terminal() {
4073                self.inner
4074                    .proc
4075                    .inner
4076                    .actor_tombstones
4077                    .insert(actor_id.clone(), new.clone());
4078            }
4079            old_status = Some(old);
4080            *status = new.clone();
4081            true
4082        });
4083
4084        if let Some((old, reason)) = illegal_status {
4085            match reason {
4086                IllegalStatusChange::LinkedZombie => {
4087                    panic!(
4088                        "zombie actor must be detached from parent before status update: actor_id={}, prev_status={}, status={}",
4089                        self.actor_addr(),
4090                        old,
4091                        new
4092                    );
4093                }
4094                IllegalStatusChange::TerminalRewrite => {
4095                    panic!(
4096                        "actor changing status illegally, only allow non-terminal -> non-terminal \
4097                        and non-terminal -> terminal statuses. actor_id={}, prev_status={}, status={}",
4098                        self.actor_addr(),
4099                        old,
4100                        new
4101                    );
4102                }
4103            }
4104        }
4105
4106        if !changed {
4107            return;
4108        }
4109        let old = old_status.expect("status change should capture previous status");
4110
4111        // Actor status changes between Idle and Processing when handling every
4112        // message. It creates too many logs if we want to log these 2 states.
4113        // Also, sometimes the actor transitions from Processing -> Processing.
4114        // Therefore we skip the status changes between them.
4115        if !((old.is_idle() && new.is_processing())
4116            || (old.is_processing() && new.is_idle())
4117            || old == new)
4118        {
4119            let new_status = new.arm().unwrap_or("unknown");
4120            let change_reason = match &new {
4121                ActorStatus::Failed(reason) => reason.to_string(),
4122                ActorStatus::Stopping(ActorStoppingReason::Zombie(reason)) => reason.clone(),
4123                ActorStatus::Stopped(reason) => reason.clone(),
4124                _ => "".to_string(),
4125            };
4126            tracing::info!(
4127                name = "ActorStatus",
4128                actor_id = %self.actor_addr(),
4129                actor_name = self.actor_addr().log_name(),
4130                status = new_status,
4131                prev_status = old.arm().unwrap_or("unknown"),
4132                caller = %PanicLocation::caller(),
4133                change_reason,
4134            );
4135            let actor_id = hash_to_u64(self.actor_addr().id());
4136            notify_actor_status_changed(ActorStatusEvent {
4137                id: generate_actor_status_event_id(actor_id),
4138                timestamp: std::time::SystemTime::now(),
4139                actor_id,
4140                new_status: new_status.to_string(),
4141                reason: if change_reason.is_empty() {
4142                    None
4143                } else {
4144                    Some(change_reason)
4145                },
4146            });
4147        }
4148    }
4149
4150    fn publish_dropped_status(&self, terminal_status: ActorStatus) {
4151        let actor_id = self.actor_addr().id().clone();
4152        let actor_addr = self.actor_addr().clone();
4153        self.inner.status_tx.send_if_modified(|status| {
4154            if status.is_terminal() {
4155                false
4156            } else {
4157                self.inner
4158                    .proc
4159                    .inner
4160                    .actor_tombstones
4161                    .insert(actor_id.clone(), terminal_status.clone());
4162                tracing::info!(
4163                    name = "ActorStatus",
4164                    actor_id = %actor_addr,
4165                    actor_name = actor_addr.log_name(),
4166                    status = "Stopped",
4167                    prev_status = status.arm().unwrap_or("unknown"),
4168                    "instance is dropped",
4169                );
4170                *status = terminal_status.clone();
4171                true
4172            }
4173        });
4174    }
4175
4176    /// The supervision event stored when this actor failed.
4177    /// `None` for actors that stopped cleanly or are still running.
4178    pub fn supervision_event(&self) -> Option<crate::supervision::ActorSupervisionEvent> {
4179        self.inner.supervision_event.lock().unwrap().clone()
4180    }
4181
4182    fn signal_sender(&self) -> mpsc::UnboundedSender<Signal> {
4183        self.inner
4184            .actor_loop
4185            .as_ref()
4186            .map(|(signal_tx, _)| signal_tx.clone())
4187            .unwrap_or_else(|| panic!("{} has no runtime signal sender", self.actor_addr()))
4188    }
4189
4190    /// Send a signal to the actor.
4191    pub fn signal(&self, signal: Signal) -> Result<(), ActorError> {
4192        if let Some((signal_tx, _)) = &self.inner.actor_loop {
4193            signal_tx.send(signal).map_err(|_| {
4194                ActorError::new(self.actor_addr(), ActorErrorKind::SignalChannelClosed)
4195            })
4196        } else {
4197            tracing::warn!(
4198                "{}: attempted to send signal {} to detached actor",
4199                self.inner.actor_id,
4200                signal
4201            );
4202            Ok(())
4203        }
4204    }
4205
4206    /// Used by this actor's children to send a supervision event to this actor.
4207    /// When it fails to send, we will crash the process. As part of the crash,
4208    /// all the procs and actors running on this process will be terminated
4209    /// forcefully.
4210    ///
4211    /// Note that "let it crash" is the default behavior when a supervision event
4212    /// cannot be delivered upstream. It is the upstream's responsibility to
4213    /// detect and handle crashes.
4214    pub fn send_supervision_event_or_crash(&self, event: ActorSupervisionEvent) {
4215        match &self.inner.actor_loop {
4216            Some((_, supervision_tx)) => {
4217                if let Err(err) = supervision_tx.send(event.clone()) {
4218                    if !event.is_error() {
4219                        // Normal lifecycle events (e.g. clean stop) that fail to
4220                        // send are silently dropped. This happens when a child
4221                        // stops after the parent's mailbox has been closed or its
4222                        // supervision port receiver has been dropped (e.g. client
4223                        // instances created via Proc::client()).
4224                        tracing::debug!(
4225                            "{}: dropping non-error supervision event {}: {:?}",
4226                            self.actor_addr(),
4227                            event,
4228                            err
4229                        );
4230                        return;
4231                    }
4232                    tracing::error!(
4233                        "{}: failed to send supervision event to actor: {:?}. Crash the process.",
4234                        self.actor_addr(),
4235                        err
4236                    );
4237                    std::process::exit(1);
4238                }
4239            }
4240            None => {
4241                if !event.is_error() {
4242                    tracing::debug!(
4243                        "{}: dropping non-error supervision event {} to detached actor",
4244                        self.actor_addr(),
4245                        event,
4246                    );
4247                    return;
4248                }
4249                tracing::error!(
4250                    "{}: failed: {}: cannot send supervision event to detached actor: crashing",
4251                    self.actor_addr(),
4252                    event,
4253                );
4254                std::process::exit(1);
4255            }
4256        }
4257    }
4258
4259    /// Downgrade this InstanceCell to a weak reference.
4260    pub fn downgrade(&self) -> WeakInstanceCell {
4261        WeakInstanceCell {
4262            inner: Arc::downgrade(&self.inner),
4263        }
4264    }
4265
4266    /// Link this instance to a new child.
4267    fn link(&self, child: InstanceCell) {
4268        assert_eq!(self.actor_addr().proc_id(), child.actor_addr().proc_id());
4269        self.inner.children.insert(child.uid().clone(), child);
4270    }
4271
4272    /// Unlink this instance from a child.
4273    fn unlink(&self, child: &InstanceCell) {
4274        assert_eq!(self.actor_addr().proc_id(), child.actor_addr().proc_id());
4275        self.inner.children.remove(child.uid());
4276    }
4277
4278    /// Unlink this instance from all children.
4279    fn unlink_all(&self) {
4280        self.inner.children.clear();
4281    }
4282
4283    /// Link this instance to its parent, if it has one.
4284    fn maybe_link_parent(&self) {
4285        if let Some(parent) = self.inner.parent.upgrade() {
4286            parent.link(self.clone());
4287        }
4288    }
4289
4290    /// Unlink this instance from its parent, if it has one. If it was unlinked,
4291    /// the parent is returned.
4292    fn maybe_unlink_parent(&self) -> Option<InstanceCell> {
4293        self.inner.maybe_unlink_parent()
4294    }
4295
4296    /// Return an iterator over this instance's children. This may deadlock if the
4297    /// caller already holds a reference to any item in map.
4298    fn child_iter(&self) -> impl Iterator<Item = RefMulti<'_, crate::id::Uid, InstanceCell>> {
4299        self.inner.children.iter()
4300    }
4301
4302    /// The number of children this instance has.
4303    pub fn child_count(&self) -> usize {
4304        self.inner.children.len()
4305    }
4306
4307    /// Returns the ActorAddrs of this instance's direct children.
4308    pub fn child_actor_ids(&self) -> Vec<ActorAddr> {
4309        self.inner
4310            .children
4311            .iter()
4312            .map(|entry| entry.value().actor_addr().clone())
4313            .collect()
4314    }
4315
4316    /// Get a child by its uid.
4317    fn get_child(&self, uid: &crate::id::Uid) -> Option<InstanceCell> {
4318        self.inner.children.get(uid).map(|child| child.clone())
4319    }
4320
4321    /// Access the flight recorder for this actor.
4322    pub fn recording(&self) -> &Recording {
4323        &self.inner.recording
4324    }
4325
4326    /// When this actor was created.
4327    pub fn created_at(&self) -> SystemTime {
4328        self.inner.created_at
4329    }
4330
4331    /// The number of messages processed by this actor.
4332    pub fn num_processed_messages(&self) -> u64 {
4333        self.inner.num_processed_messages.load(Ordering::SeqCst)
4334    }
4335
4336    /// The last message handler invoked by this actor.
4337    pub fn last_message_handler(&self) -> Option<HandlerInfo> {
4338        self.inner.last_message_handler.read().unwrap().clone()
4339    }
4340
4341    /// Total time spent processing messages, in microseconds.
4342    pub fn total_processing_time_us(&self) -> u64 {
4343        self.inner.total_processing_time_us.load(Ordering::SeqCst)
4344    }
4345
4346    /// Current actor work-queue depth (PD-5).
4347    pub fn queue_depth(&self) -> u64 {
4348        self.inner.queue_depth.load(Ordering::Relaxed)
4349    }
4350
4351    /// Stable per-instance identifier (`Uuid::now_v7`) assigned at
4352    /// `Instance::new` and threaded through to the cell at construction.
4353    pub fn instance_id(&self) -> Uuid {
4354        self.inner.instance_id
4355    }
4356
4357    /// Out-of-band inbound ordering snapshot. Returns `None` when no
4358    /// snapshot callback was installed (see IO-1 in `introspect` module
4359    /// doc). The callback uses the sequenced receiver's snapshot handle
4360    /// (`try_lock`, non-blocking) and never perturbs ordering state.
4361    pub fn inbound_ordering_snapshot(&self) -> Option<crate::ordering::OrderingSnapshot> {
4362        self.inner.inbound_ordering_snapshot.as_ref().map(|f| f())
4363    }
4364
4365    /// Install the actor-supplied introspection-attrs snapshot callback.
4366    /// Called by the actor (e.g. in `Actor::init`). The callback runs on
4367    /// the introspect task (outside the actor loop), so it must be
4368    /// `Send + Sync`, non-blocking (`try_lock`), and must not access
4369    /// actor-mutable state.
4370    pub fn set_attrs_snapshot(
4371        &self,
4372        callback: impl Fn() -> hyperactor_config::Attrs + Send + Sync + 'static,
4373    ) {
4374        *self.inner.actor_attrs_snapshot.write().unwrap() = Some(Box::new(callback));
4375    }
4376
4377    /// Out-of-band actor-supplied introspection attrs. `None` when no
4378    /// callback was installed (the actor publishes no extra attrs).
4379    /// `catch_unwind`-guarded: a panicking callback degrades to `None`
4380    /// rather than killing the introspect task (AS-3).
4381    pub fn actor_attrs_snapshot(&self) -> Option<hyperactor_config::Attrs> {
4382        let guard = self.inner.actor_attrs_snapshot.read().unwrap();
4383        let callback = guard.as_ref()?;
4384        let attrs = std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback))
4385            .map_err(|_| {
4386                tracing::warn!(
4387                    actor_id = %self.actor_addr(),
4388                    "actor_attrs_snapshot callback panicked; omitting actor attrs",
4389                );
4390            })
4391            .ok()?;
4392        // AS-1 discipline: actor-supplied keys must be INTROSPECT-tagged
4393        // (mirrors the `publish_attr` guard), so they carry a stable HTTP
4394        // short-name and schema entry rather than forming an ungoverned
4395        // second introspection channel.
4396        #[cfg(debug_assertions)]
4397        for (name, _) in attrs.iter() {
4398            debug_assert!(
4399                inventory::iter::<hyperactor_config::attrs::AttrKeyInfo>().any(|info| {
4400                    info.name == name && info.meta.get(hyperactor_config::INTROSPECT).is_some()
4401                }),
4402                "actor_attrs_snapshot callback returned non-INTROSPECT key `{name}`; \
4403                 actor introspection keys must carry @meta(INTROSPECT)"
4404            );
4405        }
4406        Some(attrs)
4407    }
4408
4409    /// Get parent instance cell, if it exists.
4410    pub fn parent(&self) -> Option<InstanceCell> {
4411        self.inner.parent.upgrade()
4412    }
4413
4414    /// The actor's type name.
4415    pub fn actor_type_name(&self) -> &str {
4416        self.inner.actor_type.type_name()
4417    }
4418
4419    /// Replace the published introspection attrs with a new bag.
4420    pub fn set_published_attrs(&self, attrs: hyperactor_config::Attrs) {
4421        *self.inner.published_attrs.write().unwrap() = Some(attrs);
4422    }
4423
4424    /// Set a single introspection attr, merging into the existing bag
4425    /// (or creating one if none exists).
4426    pub fn merge_published_attr<T: hyperactor_config::AttrValue>(
4427        &self,
4428        key: hyperactor_config::Key<T>,
4429        value: T,
4430    ) {
4431        self.inner
4432            .published_attrs
4433            .write()
4434            .unwrap()
4435            .get_or_insert_with(hyperactor_config::Attrs::new)
4436            .set(key, value);
4437    }
4438
4439    /// Read the published introspection attrs, if any.
4440    pub fn published_attrs(&self) -> Option<hyperactor_config::Attrs> {
4441        self.inner.published_attrs.read().unwrap().clone()
4442    }
4443
4444    /// Register a callback for resolving non-addressable children
4445    /// via `IntrospectMessage::QueryChild`.
4446    ///
4447    /// The callback runs on the actor's introspect task (a separate
4448    /// tokio task, not the actor's message loop), so it must be
4449    /// `Send + Sync` and must not access actor-mutable state.
4450    /// Capture cloned `Proc` references, not `&mut self`.
4451    pub fn set_query_child_handler(
4452        &self,
4453        handler: impl (Fn(&Addr) -> IntrospectResult) + Send + Sync + 'static,
4454    ) {
4455        *self.inner.query_child_handler.write().unwrap() = Some(Box::new(handler));
4456    }
4457
4458    /// Invoke the registered QueryChild handler, if any.
4459    pub fn query_child(&self, child_ref: &Addr) -> Option<IntrospectResult> {
4460        let guard = self.inner.query_child_handler.read().unwrap();
4461        guard.as_ref().map(|handler| handler(child_ref))
4462    }
4463
4464    /// Whether this actor is infrastructure/system.
4465    pub fn is_system(&self) -> bool {
4466        self.inner.is_system.load(Ordering::Relaxed)
4467    }
4468
4469    /// Store a post-mortem snapshot for this actor in the proc's
4470    /// `terminated_snapshots` map. Called by the introspect task
4471    /// just before exiting on terminal status.
4472    ///
4473    /// Eviction policy when the retention cap is exceeded:
4474    /// 1. Evict cleanly-stopped actors first (no `failure_info`).
4475    /// 2. When only failed actors remain, evict the most recent
4476    ///    (by `occurred_at`), preserving the earliest failures
4477    ///    which are closest to the root cause.
4478    pub fn store_terminated_snapshot(&self, payload: crate::introspect::IntrospectResult) {
4479        let snapshots = &self.inner.proc.inner.terminated_snapshots;
4480        snapshots.insert(
4481            self.actor_addr().id().clone(),
4482            TerminatedSnapshot {
4483                actor_addr: self.actor_addr().clone(),
4484                payload,
4485            },
4486        );
4487        let max = hyperactor_config::global::get(crate::config::TERMINATED_SNAPSHOT_RETENTION);
4488        let excess = snapshots.len().saturating_sub(max);
4489        if excess > 0 {
4490            // Build entries for the eviction selector.
4491            let entries: Vec<_> = snapshots
4492                .iter()
4493                .map(|entry| {
4494                    let occurred_at = serde_json::from_str::<hyperactor_config::Attrs>(
4495                        &entry.value().payload.attrs,
4496                    )
4497                    .ok()
4498                    .and_then(|attrs| {
4499                        // Presence of FAILURE_ERROR_MESSAGE means the actor failed.
4500                        attrs
4501                            .get(crate::introspect::FAILURE_ERROR_MESSAGE)
4502                            .cloned()?;
4503                        // Extract occurred_at timestamp for sorting.
4504                        attrs
4505                            .get(crate::introspect::FAILURE_OCCURRED_AT)
4506                            .map(|t| humantime::format_rfc3339(*t).to_string())
4507                    });
4508                    (entry.value().actor_addr.clone(), occurred_at)
4509                })
4510                .collect();
4511
4512            for key in select_eviction_candidates(&entries, excess) {
4513                snapshots.remove(key.id());
4514            }
4515        }
4516    }
4517
4518    /// This is temporary so that we can share binding code between handle and instance.
4519    /// We should find some (better) way to consolidate the two.
4520    pub(crate) fn bind<A: Actor, R: Binds<A>>(&self, ports: &HandlerPorts<A>) -> ActorRef<R> {
4521        <R as Binds<A>>::bind(ports);
4522        // Undeliverable: dispatched through the work queue to the
4523        // actor's Handler<Undeliverable<MessageEnvelope>>.
4524        //
4525        // IntrospectMessage: registered directly in Instance::new()
4526        // and handled by a dedicated introspect task.
4527        ports.bind::<Undeliverable<MessageEnvelope>>();
4528        // TODO: consider sharing `ports.bound` directly.
4529        for entry in ports.bound.iter() {
4530            self.inner
4531                .exported_named_ports
4532                .insert(entry.key().clone(), entry.value());
4533        }
4534        ActorRef::attest(ActorAddr::new(
4535            self.actor_addr().id().clone(),
4536            self.inner.proc.default_location(),
4537        ))
4538    }
4539
4540    /// Attempt to downcast this cell to a concrete actor handle.
4541    pub(crate) fn downcast_handle<A: Actor>(&self) -> Option<ActorHandle<A>> {
4542        let ports = Arc::clone(&self.inner.ports)
4543            .downcast::<HandlerPorts<A>>()
4544            .ok()?;
4545        Some(ActorHandle::new(self.clone(), ports))
4546    }
4547
4548    /// Traverse the subtree rooted at this instance in pre-order.
4549    /// The callback receives each InstanceCell and its depth (root = 0).
4550    /// Children are visited in pid order for deterministic traversal.
4551    pub fn traverse<F>(&self, f: &mut F)
4552    where
4553        F: FnMut(&InstanceCell, usize),
4554    {
4555        self.traverse_inner(0, f);
4556    }
4557
4558    fn traverse_inner<F>(&self, depth: usize, f: &mut F)
4559    where
4560        F: FnMut(&InstanceCell, usize),
4561    {
4562        f(self, depth);
4563        // Collect and sort children by uid for deterministic traversal order
4564        let mut children: Vec<_> = self.child_iter().map(|r| r.value().clone()).collect();
4565        children.sort_by_key(|c| c.uid().clone());
4566        for child in children {
4567            child.traverse_inner(depth + 1, f);
4568        }
4569    }
4570}
4571
4572impl Drop for InstanceCellState {
4573    fn drop(&mut self) {
4574        if let Some(parent) = self.maybe_unlink_parent() {
4575            tracing::debug!(
4576                "instance {} was dropped with parent {} still linked",
4577                self.actor_id,
4578                parent.actor_addr()
4579            );
4580        }
4581        if self
4582            .proc
4583            .inner
4584            .instances
4585            .remove(self.actor_id.id())
4586            .is_none()
4587        {
4588            tracing::error!("instance {} was dropped but not in proc", self.actor_id);
4589        }
4590        self.proc.inner.root_actors.remove(self.actor_id.id());
4591    }
4592}
4593
4594/// A weak version of the InstanceCell. This is used to provide cyclical
4595/// linkage between actors without creating a strong reference cycle.
4596#[derive(Debug, Clone)]
4597pub struct WeakInstanceCell {
4598    inner: Weak<InstanceCellState>,
4599}
4600
4601impl Default for WeakInstanceCell {
4602    fn default() -> Self {
4603        Self::new()
4604    }
4605}
4606
4607impl WeakInstanceCell {
4608    /// Create a new weak instance cell that is never upgradeable.
4609    pub fn new() -> Self {
4610        Self { inner: Weak::new() }
4611    }
4612
4613    /// Upgrade this weak instance cell to a strong reference, if possible.
4614    pub fn upgrade(&self) -> Option<InstanceCell> {
4615        self.inner.upgrade().map(InstanceCell::wrap)
4616    }
4617}
4618
4619/// A polymorphic dictionary that stores runtime-dispatched handler ports.
4620/// The interface memoizes the ports so that they are reused. We do not
4621/// (yet) support stable identifiers across multiple instances of the same
4622/// actor.
4623pub struct HandlerPorts<A: Actor> {
4624    ports: DashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>,
4625    bound: DashMap<Port, &'static str>,
4626    mailbox: Mailbox,
4627    workq: mpsc::UnboundedSender<SequencedEnvelope<WorkCell<A>>>,
4628    enable_buffering: bool,
4629    /// Per-actor queue depth (PD-5). Shared with `InstanceCellState`.
4630    queue_depth: Arc<AtomicU64>,
4631    /// Proc-level queue-pressure stats (PD-6 through PD-9).
4632    proc_stats: Arc<ProcQueueStats>,
4633}
4634
4635impl<A: Actor> HandlerPorts<A> {
4636    fn new(
4637        mailbox: Mailbox,
4638        workq: mpsc::UnboundedSender<SequencedEnvelope<WorkCell<A>>>,
4639        enable_buffering: bool,
4640        queue_depth: Arc<AtomicU64>,
4641        proc_stats: Arc<ProcQueueStats>,
4642    ) -> Self {
4643        Self {
4644            ports: DashMap::new(),
4645            bound: DashMap::new(),
4646            mailbox,
4647            workq,
4648            enable_buffering,
4649            queue_depth,
4650            proc_stats,
4651        }
4652    }
4653
4654    /// Get a port for the Handler<M> of actor A.
4655    pub(crate) fn get<M: Message>(&self) -> PortHandle<M>
4656    where
4657        A: Handler<M>,
4658    {
4659        let key = TypeId::of::<M>();
4660        match self.ports.entry(key) {
4661            Entry::Vacant(entry) => {
4662                // Runtime control-plane ports are provisioned directly, not
4663                // through HandlerPorts, nor wired to the work queue. So they
4664                // should never hit this code path.
4665                assert!(
4666                    !crate::ordering::is_bypass_workq_type_id(key),
4667                    "cannot provision bypass-workq port {} through `Ports::get`; \
4668                     it must be pre-registered via `open_message_port` in `Instance::new`",
4669                    std::any::type_name::<M>()
4670                );
4671
4672                let type_info = TypeInfo::get_by_typeid(key);
4673                let workq = self.workq.clone();
4674                let enable_buffering = self.enable_buffering;
4675                let actor_id = self.mailbox.actor_addr().to_string();
4676                let enqueue_depth = Arc::clone(&self.queue_depth);
4677                let enqueue_proc_stats = Arc::clone(&self.proc_stats);
4678                // Handler-port draining holds an ingress guard while this
4679                // closure runs. Therefore, the drain guarantee depends on this
4680                // closure synchronously finishing all work that it admits into
4681                // the actor work queue before it returns. That includes the
4682                // sequenced path: enqueue synchronously hands accepted work to
4683                // the receiver's channel. Messages already held in the
4684                // receiver-local reorder buffer but still waiting on a future
4685                // sequence are not considered drainable accepted work; after
4686                // draining begins, that missing future sequence is rejected.
4687                let enqueue = move |headers: Flattrs, msg: M| {
4688                    // Extract values from headers BEFORE they're moved into
4689                    // WorkCell — Flattrs::get returns owned typed values, so
4690                    // these bindings don't borrow from `headers` and `headers`
4691                    // can be moved into WorkCell freely.
4692                    let seq_info = if enable_buffering {
4693                        match headers.get(SEQ_INFO) {
4694                            Some(seq_info) => seq_info,
4695                            None => {
4696                                let error_msg = format!(
4697                                    "in enqueue func for {}, buffering is enabled, but SEQ_INFO is not set for message type {}",
4698                                    actor_id,
4699                                    std::any::type_name::<M>(),
4700                                );
4701                                tracing::error!(error_msg);
4702                                return Err(anyhow::anyhow!(error_msg));
4703                            }
4704                        }
4705                    } else {
4706                        SeqInfo::Direct
4707                    };
4708                    if !seq_info.is_valid() {
4709                        let error_msg = format!(
4710                            "in enqueue func for {}, got seq 0 for message type {}",
4711                            actor_id,
4712                            std::any::type_name::<M>(),
4713                        );
4714                        tracing::error!(error_msg);
4715                        return Err(anyhow::anyhow!(error_msg));
4716                    }
4717                    let sender = headers.get(crate::mailbox::headers::SENDER_ACTOR_ID);
4718
4719                    let work = WorkCell::new(move |actor: &mut A, instance: &Instance<A>| {
4720                        Box::pin(async move {
4721                            // SAFETY: we guarantee that the passed type_info is for type M.
4722                            unsafe {
4723                                instance
4724                                    .handle_message(actor, type_info, headers, msg)
4725                                    .await
4726                            }
4727                        })
4728                    });
4729                    // PD-5b: account the enqueue BEFORE handing the work
4730                    // to the queue. Otherwise the consumer can race and
4731                    // call `account_dequeue` before this thread accounts
4732                    // the enqueue, underflowing `running_total`. On send
4733                    // failure, `account_cancel_enqueue` rolls back the
4734                    // counters so `queue_depth` does not drift.
4735                    account_enqueue(&enqueue_depth, &enqueue_proc_stats, &actor_id);
4736                    // TODO: return the message contained in the error instead of dropping them when converting
4737                    // to anyhow::Error. In that way, the message can be picked up by mailbox and returned to sender.
4738                    let result = workq
4739                        .send(SequencedEnvelope::new(seq_info, sender, work))
4740                        .map_err(anyhow::Error::from);
4741                    if result.is_err() {
4742                        account_cancel_enqueue(&enqueue_depth, &enqueue_proc_stats, &actor_id);
4743                    }
4744                    result
4745                };
4746                let port = self.mailbox.open_handler_enqueue_port(enqueue);
4747                entry.insert(Box::new(port.clone()));
4748                port
4749            }
4750            Entry::Occupied(entry) => {
4751                let port = entry.get();
4752                port.downcast_ref::<PortHandle<M>>().unwrap().clone()
4753            }
4754        }
4755    }
4756
4757    /// Bind the given message type to its handler port.
4758    pub fn bind<M: RemoteMessage>(&self)
4759    where
4760        A: Handler<M>,
4761    {
4762        let port = Port::handler::<M>();
4763        match self.bound.entry(port.clone()) {
4764            Entry::Vacant(entry) => {
4765                let _ = self.get::<M>().bind();
4766                entry.insert(M::typename());
4767            }
4768            Entry::Occupied(entry) => {
4769                assert_eq!(
4770                    *entry.get(),
4771                    M::typename(),
4772                    "bind {}: port {} already bound to type {}",
4773                    M::typename(),
4774                    port,
4775                    entry.get(),
4776                );
4777            }
4778        }
4779    }
4780}
4781
4782#[cfg(test)]
4783mod tests {
4784    use std::assert_matches;
4785    use std::sync::atomic::AtomicBool;
4786
4787    use hyperactor_macros::export;
4788    use serde_json::json;
4789    use timed_test::async_timed_test;
4790    use tokio::sync::Barrier;
4791    use tokio::sync::oneshot;
4792    use tracing::Level;
4793    use tracing_subscriber::layer::SubscriberExt;
4794    use tracing_test::internal::logs_with_scope_contain;
4795
4796    use super::*;
4797    // needed for in-crate macro expansion
4798    use crate as hyperactor;
4799    use crate::HandleClient;
4800    use crate::Handler;
4801    use crate::OncePortRef;
4802    use crate::PortRef;
4803    use crate::channel::ChannelTransport;
4804    use crate::mailbox::MailboxClient;
4805    use crate::mailbox::PanickingMailboxSender;
4806    use crate::port::Port;
4807    use crate::testing::ids::test_actor_id;
4808    use crate::testing::proc_supervison::ProcSupervisionCoordinator;
4809    use crate::testing::process_assertion::assert_termination;
4810
4811    #[derive(Debug, Default)]
4812    #[export]
4813    struct TestActor;
4814
4815    impl Actor for TestActor {}
4816
4817    async fn get_status(client: &Client, actor_addr: &ActorAddr) -> Option<ActorStatus> {
4818        let (reply_port, reply_rx) = client.open_once_port::<Option<ActorStatus>>();
4819        actor_addr.status_port().post(
4820            client,
4821            StatusMessage::GetStatus {
4822                reply: reply_port.bind(),
4823            },
4824        );
4825        tokio::time::timeout(Duration::from_secs(5), reply_rx.recv())
4826            .await
4827            .expect("status reply should arrive")
4828            .expect("status reply port should remain open")
4829    }
4830
4831    #[test]
4832    fn classify_status_change_table() {
4833        let cases = [
4834            (
4835                ActorStatus::Idle,
4836                ActorStatus::Processing(SystemTime::UNIX_EPOCH, None),
4837                StatusChange::Apply,
4838                "idle to processing",
4839            ),
4840            (
4841                ActorStatus::Processing(SystemTime::UNIX_EPOCH, None),
4842                ActorStatus::Idle,
4843                StatusChange::Apply,
4844                "processing to idle",
4845            ),
4846            (
4847                ActorStatus::Idle,
4848                ActorStatus::Stopped("done".to_string()),
4849                StatusChange::Apply,
4850                "non-terminal to terminal",
4851            ),
4852            (
4853                ActorStatus::stopping(),
4854                ActorStatus::zombie("hard kill did not finish"),
4855                StatusChange::Apply,
4856                "non-terminal to zombie",
4857            ),
4858            (
4859                ActorStatus::zombie("hard kill did not finish"),
4860                ActorStatus::Stopped("done".to_string()),
4861                StatusChange::Apply,
4862                "zombie to terminal",
4863            ),
4864            (
4865                ActorStatus::zombie("hard kill did not finish"),
4866                ActorStatus::Idle,
4867                StatusChange::Ignore,
4868                "zombie to idle",
4869            ),
4870            (
4871                ActorStatus::zombie("hard kill did not finish"),
4872                ActorStatus::stopping(),
4873                StatusChange::Ignore,
4874                "zombie to stopping",
4875            ),
4876            (
4877                ActorStatus::Stopped("done".to_string()),
4878                ActorStatus::zombie("hard kill did not finish"),
4879                StatusChange::Ignore,
4880                "terminal to zombie",
4881            ),
4882            (
4883                ActorStatus::Stopped("done".to_string()),
4884                ActorStatus::Idle,
4885                StatusChange::Illegal(IllegalStatusChange::TerminalRewrite),
4886                "terminal to idle",
4887            ),
4888            (
4889                ActorStatus::Stopped("done".to_string()),
4890                ActorStatus::Stopped("other".to_string()),
4891                StatusChange::Illegal(IllegalStatusChange::TerminalRewrite),
4892                "terminal to terminal",
4893            ),
4894            (
4895                ActorStatus::Idle,
4896                ActorStatus::Idle,
4897                StatusChange::Ignore,
4898                "same status",
4899            ),
4900        ];
4901
4902        for (old, new, expected, label) in cases {
4903            assert_eq!(
4904                classify_status_change(&old, &new),
4905                expected,
4906                "{label}: {old} -> {new}"
4907            );
4908        }
4909    }
4910
4911    #[derive(Debug)]
4912    struct ChildLabelActor;
4913
4914    impl Actor for ChildLabelActor {}
4915
4916    #[async_timed_test(timeout_secs = 30)]
4917    async fn test_status_control_port_reports_live_actor() {
4918        let proc = Proc::isolated();
4919        let client = proc.client("client");
4920        let handle = proc.spawn(TestActor);
4921
4922        let mut status_rx = handle.status();
4923        status_rx
4924            .wait_for(|status| matches!(status, ActorStatus::Idle))
4925            .await
4926            .expect("actor should become idle");
4927
4928        let status = get_status(&client, handle.actor_addr()).await;
4929        assert_eq!(status, Some(ActorStatus::Idle));
4930
4931        handle.drain_and_stop("test").unwrap();
4932        handle.await;
4933    }
4934
4935    #[async_timed_test(timeout_secs = 30)]
4936    async fn test_status_control_port_reports_tombstoned_actor() {
4937        let proc = Proc::isolated();
4938        let client = proc.client("client");
4939        let handle = proc.spawn(TestActor);
4940        let actor_addr = handle.actor_addr().clone();
4941
4942        handle.drain_and_stop("test").unwrap();
4943        let terminal_status = handle.await;
4944
4945        assert_eq!(
4946            get_status(&client, &actor_addr).await,
4947            Some(terminal_status)
4948        );
4949    }
4950
4951    #[async_timed_test(timeout_secs = 30)]
4952    async fn test_status_control_port_reports_unknown_actor() {
4953        let proc = Proc::isolated();
4954        let client = proc.client("client");
4955        let missing = proc.root_addr(Uid::instance(Label::strip("missing")));
4956
4957        assert_eq!(get_status(&client, &missing).await, None);
4958    }
4959
4960    #[derive(Debug)]
4961    struct DelayedSelfActor {
4962        ready: Option<OncePortRef<()>>,
4963        fired: Option<OncePortRef<()>>,
4964        delay: Duration,
4965    }
4966
4967    #[derive(Debug)]
4968    struct DelayedSelfTick;
4969
4970    #[async_trait]
4971    impl Actor for DelayedSelfActor {
4972        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
4973            if let Some(ready) = self.ready.take() {
4974                ready.post(this, ());
4975            }
4976            this.post_after(this, DelayedSelfTick, self.delay);
4977            Ok(())
4978        }
4979    }
4980
4981    #[async_trait]
4982    impl Handler<DelayedSelfTick> for DelayedSelfActor {
4983        async fn handle(
4984            &mut self,
4985            cx: &crate::Context<Self>,
4986            _message: DelayedSelfTick,
4987        ) -> anyhow::Result<()> {
4988            if let Some(fired) = self.fired.take() {
4989                fired.post(cx, ());
4990            }
4991            Ok(())
4992        }
4993    }
4994
4995    #[derive(Debug)]
4996    struct DelayedPortActor {
4997        reply: Option<PortRef<u64>>,
4998        delay: Duration,
4999    }
5000
5001    #[async_trait]
5002    impl Actor for DelayedPortActor {
5003        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
5004            this.post_after(
5005                self.reply.take().expect("reply port should be present"),
5006                123u64,
5007                self.delay,
5008            );
5009            Ok(())
5010        }
5011    }
5012
5013    #[derive(Handler, HandleClient, Debug)]
5014    enum TestActorMessage {
5015        Reply(oneshot::Sender<()>),
5016        Wait(oneshot::Sender<()>, oneshot::Receiver<()>),
5017        Forward(ActorHandle<TestActor>, Box<TestActorMessage>),
5018        Noop(),
5019        Fail(anyhow::Error),
5020        Panic(String),
5021        Spawn(oneshot::Sender<ActorHandle<TestActor>>),
5022    }
5023
5024    impl TestActor {
5025        async fn spawn_child(
5026            cx: &impl context::Actor,
5027            parent: &ActorHandle<TestActor>,
5028        ) -> ActorHandle<TestActor> {
5029            let (tx, rx) = oneshot::channel();
5030            parent.post(cx, TestActorMessage::Spawn(tx));
5031            rx.await.unwrap()
5032        }
5033    }
5034
5035    #[test]
5036    fn test_proc_identity_constructors() {
5037        let anonymous = Proc::anonymous();
5038        assert!(
5039            matches!(anonymous.proc_id().uid(), crate::id::Uid::Instance(_, None)),
5040            "anonymous proc must have an unlabeled instance id"
5041        );
5042        assert_eq!(anonymous.proc_id().label(), None);
5043
5044        let instance = Proc::instance("worker");
5045        assert!(
5046            matches!(
5047                instance.proc_id().uid(),
5048                crate::id::Uid::Instance(_, Some(label)) if label.as_str() == "worker"
5049            ),
5050            "instance proc must have a labeled instance id"
5051        );
5052        assert_eq!(
5053            instance.proc_id().label().map(|label| label.as_str()),
5054            Some("worker")
5055        );
5056
5057        let singleton = Proc::singleton("controller");
5058        assert!(
5059            matches!(
5060                singleton.proc_id().uid(),
5061                crate::id::Uid::Singleton(label) if label.as_str() == "controller"
5062            ),
5063            "singleton proc must have a singleton id"
5064        );
5065        assert_eq!(
5066            singleton.proc_id().label().map(|label| label.as_str()),
5067            Some("controller")
5068        );
5069    }
5070
5071    #[test]
5072    fn test_default_actor_label_uses_label_compatible_type_basename() {
5073        assert_eq!(default_actor_label::<TestActor>().as_str(), "testactor");
5074        assert_eq!(
5075            default_actor_label::<std::collections::HashMap<String, u64>>().as_str(),
5076            "hashmap"
5077        );
5078        assert_eq!(default_actor_label::<()>().as_str(), "nil");
5079    }
5080
5081    #[test]
5082    fn test_global_proc_label_uses_short_hostname_and_pid() {
5083        assert_eq!(
5084            global_proc_label_from("devvm34959.nha0.facebook.com", 123555).as_str(),
5085            "devvm34959-123555"
5086        );
5087        assert_eq!(
5088            global_proc_label_from("DevVM34959.nha0.facebook.com", 7).as_str(),
5089            "devvm34959-7"
5090        );
5091    }
5092
5093    #[async_timed_test(timeout_secs = 30)]
5094    async fn test_spawn_uses_actor_type_label_for_root_actor() {
5095        let proc = Proc::isolated();
5096        let handle = proc.spawn(TestActor);
5097
5098        assert_eq!(
5099            handle.actor_addr().label().map(Label::as_str),
5100            Some("testactor")
5101        );
5102        assert!(matches!(
5103            handle.actor_addr().uid(),
5104            Uid::Instance(_, Some(label)) if label.as_str() == "testactor"
5105        ));
5106
5107        handle.drain_and_stop("test").unwrap();
5108        handle.await;
5109    }
5110
5111    #[async_timed_test(timeout_secs = 30)]
5112    async fn test_spawn_uses_actor_type_label_for_child_actor() {
5113        let proc = Proc::isolated();
5114        let parent = proc.spawn(TestActor);
5115        let child = proc.spawn_child(parent.cell().clone(), ChildLabelActor);
5116
5117        assert!(!child.actor_addr().is_root());
5118        assert_eq!(
5119            child.actor_addr().label().map(Label::as_str),
5120            Some("childlabelactor")
5121        );
5122        assert!(matches!(
5123            child.actor_addr().uid(),
5124            Uid::Instance(_, Some(label)) if label.as_str() == "childlabelactor"
5125        ));
5126
5127        child.drain_and_stop("test").unwrap();
5128        parent.drain_and_stop("test").unwrap();
5129        child.await;
5130        parent.await;
5131    }
5132
5133    #[async_timed_test(timeout_secs = 30)]
5134    async fn test_root_tracking_does_not_depend_on_singleton_uids() {
5135        let proc = Proc::isolated();
5136        let parent = proc.spawn(TestActor);
5137        let child = proc.spawn_child(parent.cell().clone(), TestActor);
5138
5139        assert!(parent.actor_addr().uid().is_instance());
5140        let roots = proc.root_actor_ids();
5141        assert!(
5142            roots
5143                .iter()
5144                .any(|root| root.id() == parent.actor_addr().id())
5145        );
5146        assert!(
5147            !roots
5148                .iter()
5149                .any(|root| root.id() == child.actor_addr().id())
5150        );
5151
5152        let mut traversed = Vec::new();
5153        proc.traverse(&mut |cell, _depth| {
5154            traversed.push(cell.actor_addr().id().clone());
5155        });
5156        assert!(traversed.contains(parent.actor_addr().id()));
5157        assert!(traversed.contains(child.actor_addr().id()));
5158
5159        child.drain_and_stop("test").unwrap();
5160        parent.drain_and_stop("test").unwrap();
5161        child.await;
5162        parent.await;
5163    }
5164
5165    #[async_timed_test(timeout_secs = 30)]
5166    async fn test_client_spawn_api_labels_and_explicit_uid() {
5167        let proc = Proc::isolated();
5168        let client = proc.client("client");
5169
5170        let spawned = client.spawn(TestActor);
5171        assert_eq!(
5172            spawned.actor_addr().label().map(Label::as_str),
5173            Some("testactor")
5174        );
5175
5176        let labeled = client.spawn_with_label("custom", TestActor);
5177        assert_eq!(
5178            labeled.actor_addr().label().map(Label::as_str),
5179            Some("custom")
5180        );
5181
5182        let uid = Uid::instance(Label::new("explicit").unwrap());
5183        let explicit = client.spawn_with_uid(uid.clone(), TestActor).unwrap();
5184        assert_eq!(explicit.actor_addr().uid(), &uid);
5185
5186        let child = client.child();
5187        assert!(!child.self_addr().is_root());
5188        assert!(matches!(child.self_addr().uid(), Uid::Instance(_, None)));
5189        assert_eq!(child.self_addr().label(), None);
5190        let child_spawned = child.spawn(TestActor);
5191        assert_eq!(
5192            child_spawned.actor_addr().label().map(Label::as_str),
5193            Some("testactor")
5194        );
5195
5196        spawned.drain_and_stop("test").unwrap();
5197        labeled.drain_and_stop("test").unwrap();
5198        explicit.drain_and_stop("test").unwrap();
5199        child_spawned.drain_and_stop("test").unwrap();
5200        spawned.await;
5201        labeled.await;
5202        explicit.await;
5203        child_spawned.await;
5204    }
5205
5206    #[test]
5207    fn test_current_proc_uses_stable_global_proc_outside_actor_context() {
5208        let first = Proc::current();
5209        let second = Proc::current();
5210
5211        assert_eq!(first.proc_id(), second.proc_id());
5212        assert_eq!(
5213            ProcAddr::new(
5214                first.proc_id().clone(),
5215                Gateway::current().default_location()
5216            ),
5217            first.proc_addr()
5218        );
5219    }
5220
5221    #[async_trait]
5222    #[crate::handle(TestActorMessage)]
5223    impl TestActorMessageHandler for TestActor {
5224        async fn reply(
5225            &mut self,
5226            _cx: &crate::Context<Self>,
5227            sender: oneshot::Sender<()>,
5228        ) -> Result<(), anyhow::Error> {
5229            sender.send(()).unwrap();
5230            Ok(())
5231        }
5232
5233        async fn wait(
5234            &mut self,
5235            _cx: &crate::Context<Self>,
5236            sender: oneshot::Sender<()>,
5237            receiver: oneshot::Receiver<()>,
5238        ) -> Result<(), anyhow::Error> {
5239            sender.send(()).unwrap();
5240            receiver.await.unwrap();
5241            Ok(())
5242        }
5243
5244        async fn forward(
5245            &mut self,
5246            cx: &crate::Context<Self>,
5247            destination: ActorHandle<TestActor>,
5248            message: Box<TestActorMessage>,
5249        ) -> Result<(), anyhow::Error> {
5250            // TODO: this needn't be async
5251            destination.post(cx, *message);
5252            Ok(())
5253        }
5254
5255        async fn noop(&mut self, _cx: &crate::Context<Self>) -> Result<(), anyhow::Error> {
5256            Ok(())
5257        }
5258
5259        async fn fail(
5260            &mut self,
5261            _cx: &crate::Context<Self>,
5262            err: anyhow::Error,
5263        ) -> Result<(), anyhow::Error> {
5264            Err(err)
5265        }
5266
5267        async fn panic(
5268            &mut self,
5269            _cx: &crate::Context<Self>,
5270            err_msg: String,
5271        ) -> Result<(), anyhow::Error> {
5272            panic!("{}", err_msg);
5273        }
5274
5275        async fn spawn(
5276            &mut self,
5277            cx: &crate::Context<Self>,
5278            reply: oneshot::Sender<ActorHandle<TestActor>>,
5279        ) -> Result<(), anyhow::Error> {
5280            let handle = cx.spawn(TestActor);
5281            reply.send(handle).unwrap();
5282            Ok(())
5283        }
5284    }
5285
5286    #[derive(Debug)]
5287    struct CurrentProcActor;
5288
5289    impl Actor for CurrentProcActor {}
5290
5291    #[derive(Handler, Debug)]
5292    enum CurrentProcMessage {
5293        Check(oneshot::Sender<CurrentProcSnapshot>),
5294    }
5295
5296    #[derive(Debug)]
5297    struct CurrentProcSnapshot {
5298        current_proc_id: ProcId,
5299        current_gateway_proc_addr: ProcAddr,
5300        spawned_handle: ActorHandle<TestActor>,
5301        client_proc_id: ProcId,
5302    }
5303
5304    #[async_trait]
5305    #[crate::handle(CurrentProcMessage)]
5306    impl CurrentProcMessageHandler for CurrentProcActor {
5307        async fn check(
5308            &mut self,
5309            _cx: &crate::Context<Self>,
5310            reply: oneshot::Sender<CurrentProcSnapshot>,
5311        ) -> Result<(), anyhow::Error> {
5312            let current = Proc::current();
5313            let spawned_handle = crate::spawn(TestActor);
5314            let client = crate::client("current_client");
5315            reply
5316                .send(CurrentProcSnapshot {
5317                    current_proc_id: current.proc_id().clone(),
5318                    current_gateway_proc_addr: ProcAddr::new(
5319                        current.proc_id().clone(),
5320                        Gateway::current().default_location(),
5321                    ),
5322                    spawned_handle,
5323                    client_proc_id: client.self_addr().proc_id().clone(),
5324                })
5325                .unwrap();
5326            Ok(())
5327        }
5328    }
5329
5330    #[tokio::test]
5331    async fn test_current_proc_tracks_actor_context() {
5332        let proc = Proc::isolated();
5333        let client = proc.client("client");
5334        let actor = proc.spawn(CurrentProcActor);
5335        let (tx, rx) = oneshot::channel();
5336
5337        crate::Endpoint::post(&actor, &client, CurrentProcMessage::Check(tx));
5338        let snapshot = rx.await.unwrap();
5339
5340        assert_eq!(&snapshot.current_proc_id, proc.proc_id());
5341        assert_eq!(snapshot.current_gateway_proc_addr, proc.proc_addr());
5342        assert_eq!(
5343            snapshot.spawned_handle.actor_addr().proc_id(),
5344            proc.proc_id()
5345        );
5346        assert_eq!(&snapshot.client_proc_id, proc.proc_id());
5347
5348        snapshot
5349            .spawned_handle
5350            .drain_and_stop("test complete")
5351            .unwrap();
5352        snapshot.spawned_handle.await;
5353        actor.drain_and_stop("test complete").unwrap();
5354        actor.await;
5355    }
5356
5357    #[expect(
5358        clippy::await_holding_invalid_type,
5359        reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
5360    )]
5361    #[tracing_test::traced_test]
5362    #[async_timed_test(timeout_secs = 30)]
5363    async fn test_spawn_actor() {
5364        let proc = Proc::isolated();
5365        let client = proc.client("client");
5366        let handle = proc.spawn(TestActor);
5367
5368        // Check on the join handle.
5369        assert!(logs_contain(
5370            format!(
5371                "{}: spawned with {:?}",
5372                handle.actor_addr(),
5373                handle.cell().actor_task_handle().unwrap(),
5374            )
5375            .as_str()
5376        ));
5377
5378        let mut state = handle.status().clone();
5379
5380        // Send a ping-pong to the actor. Wait for the actor to become idle.
5381
5382        let (tx, rx) = oneshot::channel::<()>();
5383        handle.post(&client, TestActorMessage::Reply(tx));
5384        rx.await.unwrap();
5385
5386        state
5387            .wait_for(|state: &ActorStatus| matches!(*state, ActorStatus::Idle))
5388            .await
5389            .unwrap();
5390
5391        // Make sure we enter processing state while the actor is handling a message.
5392        let (enter_tx, enter_rx) = oneshot::channel::<()>();
5393        let (exit_tx, exit_rx) = oneshot::channel::<()>();
5394
5395        handle.post(&client, TestActorMessage::Wait(enter_tx, exit_rx));
5396        enter_rx.await.unwrap();
5397        assert_matches!(*state.borrow(), ActorStatus::Processing(instant, _) if instant <= std::time::SystemTime::now());
5398        exit_tx.send(()).unwrap();
5399
5400        state
5401            .wait_for(|state| matches!(*state, ActorStatus::Idle))
5402            .await
5403            .unwrap();
5404
5405        handle.drain_and_stop("test").unwrap();
5406        handle.await;
5407        assert_matches!(&*state.borrow(), ActorStatus::Stopped(reason) if reason == "test");
5408    }
5409
5410    #[async_timed_test(timeout_secs = 30)]
5411    async fn test_proc_actors_messaging() {
5412        let proc = Proc::isolated();
5413        let client = proc.client("client");
5414        let first = proc.spawn_with_label::<TestActor>("first", TestActor);
5415        let second = proc.spawn_with_label::<TestActor>("second", TestActor);
5416        let (tx, rx) = oneshot::channel::<()>();
5417        let reply_message = TestActorMessage::Reply(tx);
5418        first.post(
5419            &client,
5420            TestActorMessage::Forward(second, Box::new(reply_message)),
5421        );
5422        rx.await.unwrap();
5423    }
5424
5425    /// Proc ownership is based on `ProcId`, not the routeable
5426    /// `ProcAddr`. A proc may be reached through multiple locations,
5427    /// but a different proc id must still forward even when the
5428    /// location matches.
5429    #[tokio::test]
5430    async fn test_post_routes_by_proc_id() {
5431        use crate::mailbox::monitored_return_handle;
5432
5433        #[derive(Clone)]
5434        struct CountingSender(Arc<AtomicUsize>);
5435
5436        #[async_trait]
5437        impl MailboxSender for CountingSender {
5438            fn post_unchecked(
5439                &self,
5440                _envelope: MessageEnvelope,
5441                _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
5442            ) {
5443                self.0.fetch_add(1, Ordering::SeqCst);
5444            }
5445        }
5446
5447        // Distinct in-process local addresses; `ChannelAddr::any` would
5448        // hand out the same `Local(0)` sentinel both times.
5449        let local_addr = ChannelAddr::Local(1);
5450        let remote_addr = ChannelAddr::Local(2);
5451
5452        let proc_local = ProcAddr::instance(local_addr.clone(), "shared");
5453        let proc_same_id_other_location =
5454            ProcAddr::new(proc_local.id().clone(), remote_addr.into());
5455        let proc_other_id_same_location = ProcAddr::instance(local_addr, "other");
5456        assert_eq!(
5457            proc_local.id(),
5458            proc_same_id_other_location.id(),
5459            "test setup: both procs must share a ProcId"
5460        );
5461        assert_ne!(
5462            proc_local.id(),
5463            proc_other_id_same_location.id(),
5464            "test setup: the remote proc must have a distinct ProcId"
5465        );
5466
5467        let forwarded = Arc::new(AtomicUsize::new(0));
5468        let proc = Proc::configured(
5469            proc_local.clone(),
5470            BoxedMailboxSender::new(CountingSender(forwarded.clone())),
5471        );
5472        let sender = test_actor_id("sender", "client");
5473
5474        // Same ProcId, same location: route locally; the forwarder must not see it.
5475        let local_dest = proc_local.actor_addr("worker").port_addr(Port::from(1234));
5476        proc.post(
5477            MessageEnvelope::new(
5478                sender.clone(),
5479                local_dest,
5480                wirevalue::Any::serialize(&1u64).unwrap(),
5481                Flattrs::new(),
5482            ),
5483            monitored_return_handle(),
5484        );
5485        assert_eq!(forwarded.load(Ordering::SeqCst), 0);
5486
5487        // Same instance ProcId, different location: still local ownership.
5488        let same_id_other_location_dest = proc_same_id_other_location
5489            .actor_addr("worker")
5490            .port_addr(Port::from(1234));
5491        proc.post(
5492            MessageEnvelope::new(
5493                sender.clone(),
5494                same_id_other_location_dest,
5495                wirevalue::Any::serialize(&1u64).unwrap(),
5496                Flattrs::new(),
5497            ),
5498            monitored_return_handle(),
5499        );
5500        assert_eq!(forwarded.load(Ordering::SeqCst), 0);
5501
5502        // Different ProcId, same location: forward.
5503        let other_id_same_location_dest = proc_other_id_same_location
5504            .actor_addr("worker")
5505            .port_addr(Port::from(1234));
5506        proc.post(
5507            MessageEnvelope::new(
5508                sender,
5509                other_id_same_location_dest,
5510                wirevalue::Any::serialize(&1u64).unwrap(),
5511                Flattrs::new(),
5512            ),
5513            monitored_return_handle(),
5514        );
5515        assert_eq!(forwarded.load(Ordering::SeqCst), 1);
5516    }
5517
5518    /// `Instance::post` (-> `MailboxExt::post`) must stamp `SENDER_ACTOR_ID`
5519    /// alongside the `SEQ_INFO` it assigns when the destination is a handler
5520    /// port. Verified by forwarding to a different `ProcId` and capturing the
5521    /// outbound envelope.
5522    #[tokio::test]
5523    async fn test_mailbox_ext_post_stamps_sender_actor_id() {
5524        use crate::mailbox::headers::SENDER_ACTOR_ID;
5525
5526        #[derive(typeuri::Named)]
5527        struct DestHandlerMsg;
5528
5529        #[derive(Clone, Default)]
5530        struct CapturingSender(Arc<Mutex<Vec<MessageEnvelope>>>);
5531
5532        #[async_trait]
5533        impl MailboxSender for CapturingSender {
5534            fn post_unchecked(
5535                &self,
5536                envelope: MessageEnvelope,
5537                _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
5538            ) {
5539                self.0.lock().unwrap().push(envelope);
5540            }
5541        }
5542
5543        let proc_addr = ProcAddr::instance(ChannelAddr::Local(1), "stamping_test");
5544        let captured: Arc<Mutex<Vec<MessageEnvelope>>> = Arc::new(Mutex::new(Vec::new()));
5545        let proc = Proc::configured(
5546            proc_addr,
5547            BoxedMailboxSender::new(CapturingSender(captured.clone())),
5548        );
5549
5550        let client = proc.client("client");
5551        let client_addr = client.mailbox().actor_addr().clone();
5552
5553        // Distinct ProcId so the envelope routes through the configured
5554        // forwarder (CapturingSender), where we can inspect the headers.
5555        let remote_dest = ProcAddr::instance(ChannelAddr::Local(2), "remote")
5556            .actor_addr("worker")
5557            .port_addr(Port::handler::<DestHandlerMsg>());
5558
5559        // UFCS to select MailboxExt::post over Endpoint::post (also in
5560        // scope at module level via `use ... as _`). Client implements
5561        // `context::Actor` so the MailboxExt blanket impl applies.
5562        <Client as context::MailboxExt>::post(
5563            &client,
5564            remote_dest,
5565            Flattrs::new(),
5566            wirevalue::Any::serialize(&1u64).unwrap(),
5567            false,
5568            context::SeqInfoPolicy::AssignNew,
5569        );
5570
5571        let captured = captured.lock().unwrap();
5572        assert_eq!(
5573            captured.len(),
5574            1,
5575            "exactly one envelope should be forwarded"
5576        );
5577        assert_eq!(
5578            captured[0].headers().get(SENDER_ACTOR_ID),
5579            Some(client_addr),
5580            "MailboxExt::post must stamp SENDER_ACTOR_ID with the client's actor_addr"
5581        );
5582    }
5583
5584    #[test]
5585    fn test_local_delivery_service_and_local_compare_full_proc_addr() {
5586        for name in [LEGACY_SERVICE_PROC_NAME, LEGACY_LOCAL_PROC_NAME] {
5587            let local = ProcAddr::singleton(ChannelAddr::Local(1), name);
5588            let same_id_other_location = ProcAddr::singleton(ChannelAddr::Local(2), name);
5589            let proc = match name {
5590                LEGACY_SERVICE_PROC_NAME => Proc::legacy_service_pseudo_singleton(
5591                    ChannelAddr::Local(1),
5592                    BoxedMailboxSender::new(PanickingMailboxSender),
5593                ),
5594                LEGACY_LOCAL_PROC_NAME => Proc::legacy_local_pseudo_singleton(
5595                    ChannelAddr::Local(1),
5596                    BoxedMailboxSender::new(PanickingMailboxSender),
5597                ),
5598                _ => unreachable!("test only covers legacy pseudo-singletons"),
5599            };
5600
5601            assert_eq!(local.id(), same_id_other_location.id());
5602            assert!(proc.is_local_delivery_target(&local));
5603            assert!(!proc.is_local_delivery_target(&same_id_other_location));
5604        }
5605
5606        let shared = ProcAddr::singleton(ChannelAddr::Local(1), "shared");
5607        let shared_other_location = ProcAddr::singleton(ChannelAddr::Local(2), "shared");
5608        let proc = Proc::configured(
5609            shared.clone(),
5610            BoxedMailboxSender::new(PanickingMailboxSender),
5611        );
5612        assert!(proc.is_local_delivery_target(&shared_other_location));
5613
5614        let service_instance = ProcAddr::instance(ChannelAddr::Local(1), "service");
5615        let service_instance_other_location =
5616            ProcAddr::new(service_instance.id().clone(), ChannelAddr::Local(2).into());
5617        let proc = Proc::configured(
5618            service_instance,
5619            BoxedMailboxSender::new(PanickingMailboxSender),
5620        );
5621        assert!(proc.is_local_delivery_target(&service_instance_other_location));
5622    }
5623
5624    #[test]
5625    fn test_legacy_pseudo_singletons_use_dedicated_constructors() {
5626        for name in [LEGACY_SERVICE_PROC_NAME, LEGACY_LOCAL_PROC_NAME] {
5627            let result = std::panic::catch_unwind(|| {
5628                Proc::configured(
5629                    ProcAddr::singleton(ChannelAddr::Local(1), name),
5630                    BoxedMailboxSender::new(PanickingMailboxSender),
5631                );
5632            });
5633            assert!(result.is_err());
5634        }
5635
5636        let service = Proc::legacy_service_pseudo_singleton(
5637            ChannelAddr::Local(1),
5638            BoxedMailboxSender::new(PanickingMailboxSender),
5639        );
5640        assert_eq!(
5641            service.proc_addr().id().uid().to_string(),
5642            LEGACY_SERVICE_PROC_NAME
5643        );
5644
5645        let local = Proc::legacy_local_pseudo_singleton(
5646            ChannelAddr::Local(2),
5647            BoxedMailboxSender::new(PanickingMailboxSender),
5648        );
5649        assert_eq!(
5650            local.proc_addr().id().uid().to_string(),
5651            LEGACY_LOCAL_PROC_NAME
5652        );
5653    }
5654
5655    #[tokio::test]
5656    async fn test_mailbox_muxer_delivers_by_actor_id() {
5657        use crate::mailbox::PortLocation;
5658        use crate::mailbox::monitored_return_handle;
5659
5660        let proc = Proc::isolated();
5661        let instance = proc.client("worker");
5662        let (port, mut receiver) = instance.bind_handler_port::<u64>();
5663
5664        let PortLocation::Bound(default_dest) = port.location() else {
5665            panic!("handler port must be bound");
5666        };
5667        let alternate_dest =
5668            PortAddr::new(default_dest.id().clone(), ChannelAddr::Local(9876).into());
5669
5670        proc.post(
5671            MessageEnvelope::serialize(
5672                test_actor_id("sender", "client"),
5673                alternate_dest,
5674                &123u64,
5675                Flattrs::new(),
5676            )
5677            .unwrap(),
5678            monitored_return_handle(),
5679        );
5680
5681        assert_eq!(receiver.recv().await.unwrap(), 123);
5682    }
5683
5684    #[test]
5685    fn test_default_location_changes_new_bindings_not_lookup() {
5686        let proc = Proc::isolated();
5687        let gateway = proc.gateway();
5688        let client = proc.client("worker");
5689
5690        let first_ref: ActorRef<()> = client.bind();
5691        let new_location = ChannelAddr::Local(9876).into();
5692        gateway.set_default_location(new_location);
5693        let second_ref: ActorRef<()> = client.bind();
5694
5695        assert_eq!(first_ref.actor_addr().id(), second_ref.actor_addr().id());
5696        assert_ne!(
5697            first_ref.actor_addr().location(),
5698            second_ref.actor_addr().location()
5699        );
5700        assert_eq!(second_ref.actor_addr().location(), &proc.default_location());
5701        assert_eq!(proc.default_location(), gateway.default_location());
5702        assert_eq!(
5703            proc.proc_addr(),
5704            ProcAddr::new(proc.proc_id().clone(), gateway.default_location())
5705        );
5706        assert!(proc.get_instance(second_ref.actor_addr()).is_some());
5707    }
5708
5709    /// Concurrent `set_default_location` and `handle.bind()` must not
5710    /// corrupt the bindings. Every bound ref carries one of the racing
5711    /// locations, and every bound ref is still resolvable via
5712    /// `get_instance` (which keys on identity, not location).
5713    #[async_timed_test(timeout_secs = 10)]
5714    async fn test_default_location_concurrent_with_bind() {
5715        let proc = Proc::isolated();
5716        let gateway = proc.gateway();
5717        let handle = proc.client("worker");
5718
5719        let loc_a: Location = ChannelAddr::Local(40001).into();
5720        let loc_b: Location = ChannelAddr::Local(40002).into();
5721
5722        // Pre-set to loc_a so binds never observe the initial default
5723        // location. Without this, a bind that runs before the setter's
5724        // first write could see the initial location and fail the
5725        // "one of two locations" assertion.
5726        gateway.set_default_location(loc_a.clone());
5727
5728        let barrier = std::sync::Arc::new(Barrier::new(2));
5729
5730        let setter = {
5731            let gateway = gateway.clone();
5732            let loc_a = loc_a.clone();
5733            let loc_b = loc_b.clone();
5734            let barrier = barrier.clone();
5735            tokio::spawn(async move {
5736                barrier.wait().await;
5737                for i in 0..100 {
5738                    let loc = if i % 2 == 0 {
5739                        loc_a.clone()
5740                    } else {
5741                        loc_b.clone()
5742                    };
5743                    gateway.set_default_location(loc);
5744                    tokio::task::yield_now().await;
5745                }
5746            })
5747        };
5748
5749        let binder = {
5750            // Clone `handle` into the binder so the outer `handle` stays
5751            // alive after the spawned task finishes. Without this, the
5752            // only strong reference to the client's instance drops when
5753            // the binder task ends and the `proc.get_instance(...)` checks
5754            // below return None.
5755            let handle = handle.clone();
5756            let barrier = barrier.clone();
5757            tokio::spawn(async move {
5758                barrier.wait().await;
5759                let mut refs = Vec::with_capacity(100);
5760                for _ in 0..100 {
5761                    refs.push(handle.bind::<()>());
5762                    tokio::task::yield_now().await;
5763                }
5764                refs
5765            })
5766        };
5767
5768        setter.await.unwrap();
5769        let refs = binder.await.unwrap();
5770
5771        // Every ref carries one of the two racing locations.
5772        for r in &refs {
5773            let loc = r.actor_addr().location();
5774            assert!(
5775                loc == &loc_a || loc == &loc_b,
5776                "ref location {loc:?} is neither {loc_a:?} nor {loc_b:?}",
5777            );
5778        }
5779
5780        // Every ref is still resolvable via get_instance (identity-based).
5781        for r in &refs {
5782            assert!(
5783                proc.get_instance(r.actor_addr()).is_some(),
5784                "ref {:?} no longer resolves",
5785                r.actor_addr(),
5786            );
5787        }
5788    }
5789
5790    #[test]
5791    fn test_builder_procs_can_share_gateway_with_distinct_ids() {
5792        let gateway = Gateway::new();
5793        let first = Proc::builder()
5794            .proc_id(ProcId::instance(Label::strip("first")))
5795            .shared_gateway(gateway.clone())
5796            .build()
5797            .unwrap();
5798        let second = Proc::builder()
5799            .proc_id(ProcId::instance(Label::strip("second")))
5800            .shared_gateway(gateway.clone())
5801            .build()
5802            .unwrap();
5803
5804        assert_ne!(first.proc_id(), second.proc_id());
5805        assert_eq!(first.default_location(), second.default_location());
5806
5807        let new_location = ChannelAddr::Local(9876).into();
5808        gateway.set_default_location(new_location);
5809
5810        assert_eq!(first.default_location(), gateway.default_location());
5811        assert_eq!(second.default_location(), gateway.default_location());
5812        assert_eq!(
5813            first.proc_addr(),
5814            ProcAddr::new(first.proc_id().clone(), gateway.default_location())
5815        );
5816        assert_eq!(
5817            second.proc_addr(),
5818            ProcAddr::new(second.proc_id().clone(), gateway.default_location())
5819        );
5820    }
5821
5822    #[test]
5823    fn test_isolated_procs_use_distinct_gateways() {
5824        let first = Proc::isolated();
5825        let second = Proc::isolated();
5826        let second_location = second.default_location();
5827
5828        first
5829            .gateway()
5830            .set_default_location(ChannelAddr::Local(9876).into());
5831
5832        assert_ne!(first.proc_id(), second.proc_id());
5833        assert_ne!(first.default_location(), second_location);
5834        assert_eq!(second.default_location(), second_location);
5835    }
5836
5837    #[tokio::test]
5838    async fn test_gateway_serve_updates_location_and_stops() {
5839        use crate::mailbox::PortLocation;
5840        use crate::mailbox::monitored_return_handle;
5841
5842        let proc = Proc::isolated();
5843        let gateway = proc.gateway();
5844        let initial_location = proc.default_location();
5845        let client = proc.client("client");
5846        let (port, mut receiver) = client.bind_handler_port::<u64>();
5847        let PortLocation::Bound(default_dest) = port.location() else {
5848            panic!("handler port must be bound");
5849        };
5850
5851        async fn send_to_location(
5852            location: Location,
5853            default_dest: &PortAddr,
5854            value: u64,
5855            receiver: &mut PortReceiver<u64>,
5856        ) {
5857            let dest = PortAddr::new(default_dest.id().clone(), location.clone());
5858            let sender = MailboxClient::dial(location.addr().clone()).unwrap();
5859            sender.post(
5860                MessageEnvelope::serialize(
5861                    test_actor_id("sender", "client"),
5862                    dest,
5863                    &value,
5864                    Flattrs::new(),
5865                )
5866                .unwrap(),
5867                monitored_return_handle(),
5868            );
5869            sender.flush().await.unwrap();
5870            let received = tokio::time::timeout(Duration::from_secs(5), receiver.recv())
5871                .await
5872                .unwrap()
5873                .unwrap();
5874            assert_eq!(received, value);
5875        }
5876
5877        let mut server =
5878            Gateway::serve(&gateway, ChannelAddr::any(ChannelTransport::Local)).unwrap();
5879
5880        assert_eq!(proc.default_location(), initial_location);
5881        assert_eq!(proc.default_location(), gateway.default_location());
5882        assert_eq!(
5883            proc.proc_addr(),
5884            ProcAddr::new(proc.proc_id().clone(), gateway.default_location())
5885        );
5886        send_to_location(initial_location.clone(), &default_dest, 1, &mut receiver).await;
5887
5888        let mut next_server =
5889            Gateway::serve(&gateway, ChannelAddr::any(ChannelTransport::Local)).unwrap();
5890        let next_location = proc.default_location();
5891
5892        assert_ne!(proc.default_location(), initial_location);
5893        assert_eq!(proc.default_location(), gateway.default_location());
5894        assert_eq!(
5895            proc.proc_addr(),
5896            ProcAddr::new(proc.proc_id().clone(), gateway.default_location())
5897        );
5898        send_to_location(next_location.clone(), &default_dest, 2, &mut receiver).await;
5899        send_to_location(initial_location.clone(), &default_dest, 3, &mut receiver).await;
5900
5901        next_server.stop("test complete");
5902        next_server.join().await.unwrap();
5903
5904        assert_eq!(proc.default_location(), initial_location);
5905        assert_eq!(proc.default_location(), gateway.default_location());
5906        assert!(MailboxClient::dial(next_location.addr().clone()).is_err());
5907        send_to_location(initial_location.clone(), &default_dest, 4, &mut receiver).await;
5908
5909        server.stop("test complete");
5910        server.join().await.unwrap();
5911
5912        assert_eq!(proc.default_location(), initial_location);
5913        assert_eq!(proc.default_location(), gateway.default_location());
5914        assert!(MailboxClient::dial(initial_location.addr().clone()).is_err());
5915    }
5916
5917    #[tokio::test]
5918    async fn test_direct_proc_server_stops_via_join_mailbox_server() {
5919        let proc = Proc::direct(
5920            ChannelAddr::any(ChannelTransport::Local),
5921            "direct".to_string(),
5922        )
5923        .unwrap();
5924
5925        assert_eq!(
5926            proc.proc_addr(),
5927            ProcAddr::new(proc.proc_id().clone(), proc.gateway().default_location())
5928        );
5929
5930        proc.join_mailbox_server().await;
5931    }
5932
5933    #[tokio::test]
5934    async fn test_local_only_gateway_returns_undeliverable_messages() {
5935        let proc = Proc::isolated();
5936        let client = proc.client("client");
5937        let (return_handle, mut undeliverable_rx) =
5938            client.open_port::<Undeliverable<MessageEnvelope>>();
5939        let remote_proc = ProcAddr::instance(ChannelAddr::Local(1234), "remote");
5940        let remote_dest = remote_proc.actor_addr("worker").port_addr(Port::from(0));
5941
5942        proc.post(
5943            MessageEnvelope::serialize(
5944                test_actor_id("sender", "client"),
5945                remote_dest.clone(),
5946                &123u64,
5947                Flattrs::new(),
5948            )
5949            .unwrap(),
5950            return_handle,
5951        );
5952
5953        let Undeliverable::Returned(envelope) = undeliverable_rx.recv().await.unwrap() else {
5954            panic!("expected returned message");
5955        };
5956        assert_eq!(envelope.dest(), &remote_dest);
5957    }
5958
5959    #[tokio::test]
5960    async fn test_gateway_attach_peer_routes_via_through_sender() {
5961        use tokio::sync::mpsc::UnboundedSender;
5962        use tokio::sync::mpsc::unbounded_channel;
5963
5964        use crate::mailbox::IntoBoxedMailboxSender;
5965        use crate::mailbox::MailboxSender as _;
5966        use crate::mailbox::monitored_return_handle;
5967
5968        #[derive(Clone)]
5969        struct MpscSender(UnboundedSender<MessageEnvelope>);
5970
5971        #[async_trait]
5972        impl crate::mailbox::MailboxSender for MpscSender {
5973            fn post_unchecked(
5974                &self,
5975                envelope: MessageEnvelope,
5976                _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
5977            ) {
5978                self.0.send(envelope).unwrap();
5979            }
5980        }
5981
5982        let proc = Proc::isolated();
5983        let gateway = proc.gateway();
5984
5985        // Advertise the peer with a `Via(remote_uid, ...)` prefix,
5986        // mirroring the host-spawn convention so the peer route
5987        // matches the outermost via hop.
5988        let remote_uid = Uid::Instance(0xabc123, Some(Label::strip("remote")));
5989        let remote_inner_addr = ChannelAddr::Local(1234);
5990        let remote_location =
5991            Location::from(remote_inner_addr.clone()).with_via(remote_uid.clone());
5992        let remote_proc = ProcAddr::new(ProcId::new(remote_uid.clone(), None), remote_location);
5993        let remote_dest = remote_proc.actor_addr("worker").port_addr(Port::from(0));
5994
5995        let (tx, mut rx) = unbounded_channel::<MessageEnvelope>();
5996        let route_guard = gateway
5997            .attach_peer(remote_uid.clone(), MpscSender(tx).into_boxed())
5998            .unwrap();
5999
6000        gateway.post(
6001            MessageEnvelope::serialize(
6002                test_actor_id("sender", "client"),
6003                remote_dest.clone(),
6004                &42u64,
6005                Flattrs::new(),
6006            )
6007            .unwrap(),
6008            monitored_return_handle(),
6009        );
6010
6011        let routed = tokio::time::timeout(Duration::from_secs(5), rx.recv())
6012            .await
6013            .expect("attach_peer did not route envelope before timeout")
6014            .expect("recording sender closed unexpectedly");
6015        // The outer via is peeled from the next hop before
6016        // forwarding; the canonical destination is preserved.
6017        assert_eq!(routed.dest(), &remote_dest);
6018        assert_eq!(routed.next_hop().id(), remote_dest.id());
6019        assert_eq!(routed.next_hop().location().addr(), &remote_inner_addr);
6020        assert!(
6021            !routed.next_hop().location().is_via(),
6022            "outer via hop must be peeled from the next hop"
6023        );
6024
6025        drop(route_guard);
6026
6027        let client = proc.client("client");
6028        let (return_handle, mut undeliverable_rx) =
6029            client.open_port::<Undeliverable<MessageEnvelope>>();
6030        gateway.post(
6031            MessageEnvelope::serialize(
6032                test_actor_id("sender", "client"),
6033                remote_dest.clone(),
6034                &43u64,
6035                Flattrs::new(),
6036            )
6037            .unwrap(),
6038            return_handle,
6039        );
6040        let Undeliverable::Returned(envelope) = undeliverable_rx.recv().await.unwrap() else {
6041            panic!("expected Undeliverable::Returned variant");
6042        };
6043        assert_eq!(envelope.dest(), &remote_dest);
6044    }
6045
6046    #[derive(Debug, Default)]
6047    #[export]
6048    struct LookupTestActor;
6049
6050    impl Actor for LookupTestActor {}
6051
6052    #[derive(Handler, HandleClient, Debug)]
6053    enum LookupTestMessage {
6054        ActorExists(ActorRef<TestActor>, #[reply] OncePortRef<bool>),
6055    }
6056
6057    #[async_trait]
6058    #[crate::handle(LookupTestMessage)]
6059    impl LookupTestMessageHandler for LookupTestActor {
6060        async fn actor_exists(
6061            &mut self,
6062            cx: &crate::Context<Self>,
6063            actor_ref: ActorRef<TestActor>,
6064        ) -> Result<bool, anyhow::Error> {
6065            Ok(actor_ref.downcast_handle(cx).is_some())
6066        }
6067    }
6068
6069    #[async_timed_test(timeout_secs = 30)]
6070    async fn test_actor_lookup() {
6071        let proc = Proc::isolated();
6072        let client = proc.client("client");
6073
6074        let target_actor = proc.spawn(TestActor);
6075        let target_actor_ref = target_actor.bind();
6076        let lookup_actor = proc.spawn(LookupTestActor);
6077
6078        assert!(
6079            lookup_actor
6080                .actor_exists(&client, target_actor_ref.clone())
6081                .await
6082                .unwrap()
6083        );
6084
6085        // Make up a child actor. It shouldn't exist.
6086        assert!(
6087            !lookup_actor
6088                .actor_exists(
6089                    &client,
6090                    ActorRef::attest(target_actor.actor_addr().anonymous_child())
6091                )
6092                .await
6093                .unwrap()
6094        );
6095        // A wrongly-typed actor ref should also not obtain.
6096        assert!(
6097            !lookup_actor
6098                .actor_exists(&client, ActorRef::attest(lookup_actor.actor_addr().clone()))
6099                .await
6100                .unwrap()
6101        );
6102
6103        target_actor.drain_and_stop("test").unwrap();
6104        target_actor.await;
6105
6106        assert!(
6107            !lookup_actor
6108                .actor_exists(&client, target_actor_ref)
6109                .await
6110                .unwrap()
6111        );
6112
6113        lookup_actor.drain_and_stop("test").unwrap();
6114        lookup_actor.await;
6115    }
6116
6117    fn validate_link(child: &InstanceCell, parent: &InstanceCell) {
6118        assert_eq!(
6119            child.actor_addr().proc_addr(),
6120            parent.actor_addr().proc_addr()
6121        );
6122        assert_eq!(
6123            child.inner.parent.upgrade().unwrap().actor_addr(),
6124            parent.actor_addr()
6125        );
6126        assert_matches!(
6127            parent.inner.children.get(child.uid()),
6128            Some(node) if node.actor_addr() == child.actor_addr()
6129        );
6130    }
6131
6132    #[expect(
6133        clippy::await_holding_invalid_type,
6134        reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
6135    )]
6136    #[tracing_test::traced_test]
6137    #[async_timed_test(timeout_secs = 30)]
6138    async fn test_spawn_child() {
6139        let proc = Proc::isolated();
6140        let client = proc.client("client");
6141
6142        let first = proc.spawn_with_label::<TestActor>("first", TestActor);
6143        let second = TestActor::spawn_child(&client, &first).await;
6144        let third = TestActor::spawn_child(&client, &second).await;
6145
6146        // Check we've got the join handles.
6147        assert!(logs_with_scope_contain(
6148            "hyperactor::proc",
6149            format!(
6150                "{}: spawned with {:?}",
6151                first.actor_addr(),
6152                first.cell().actor_task_handle().unwrap()
6153            )
6154            .as_str()
6155        ));
6156        assert!(logs_with_scope_contain(
6157            "hyperactor::proc",
6158            format!(
6159                "{}: spawned with {:?}",
6160                second.actor_addr(),
6161                second.cell().actor_task_handle().unwrap()
6162            )
6163            .as_str()
6164        ));
6165        assert!(logs_with_scope_contain(
6166            "hyperactor::proc",
6167            format!(
6168                "{}: spawned with {:?}",
6169                third.actor_addr(),
6170                third.cell().actor_task_handle().unwrap()
6171            )
6172            .as_str()
6173        ));
6174
6175        // All actors are in the same proc:
6176        assert_eq!(first.actor_addr().proc_addr(), proc.proc_addr());
6177        assert_eq!(second.actor_addr().proc_addr(), proc.proc_addr());
6178        assert_eq!(third.actor_addr().proc_addr(), proc.proc_addr());
6179
6180        // Supervision tree is constructed correctly.
6181        validate_link(third.cell(), second.cell());
6182        validate_link(second.cell(), first.cell());
6183        assert!(first.cell().inner.parent.upgrade().is_none());
6184
6185        // Supervision tree is torn down correctly.
6186        // Once each actor is stopped, it should have no linked children.
6187        let third_cell = third.cell().clone();
6188        third.drain_and_stop("test").unwrap();
6189        third.await;
6190        assert!(third_cell.inner.children.is_empty());
6191        drop(third_cell);
6192        validate_link(second.cell(), first.cell());
6193
6194        let second_cell = second.cell().clone();
6195        second.drain_and_stop("test").unwrap();
6196        second.await;
6197        assert!(second_cell.inner.children.is_empty());
6198        drop(second_cell);
6199
6200        let first_cell = first.cell().clone();
6201        first.drain_and_stop("test").unwrap();
6202        first.await;
6203        assert!(first_cell.inner.children.is_empty());
6204    }
6205
6206    #[async_timed_test(timeout_secs = 30)]
6207    async fn zombie_status_sets_unless_terminal() {
6208        let proc = Proc::isolated();
6209        let alive = proc.spawn_with_label::<TestActor>("alive", TestActor);
6210        alive
6211            .status()
6212            .clone()
6213            .wait_for(ActorStatus::is_idle)
6214            .await
6215            .unwrap();
6216        alive
6217            .cell()
6218            .change_status(ActorStatus::zombie("hard kill did not finish"));
6219        assert!(alive.cell().status().borrow().is_zombie());
6220
6221        alive.cell().change_status(ActorStatus::Idle);
6222        assert!(alive.cell().status().borrow().is_zombie());
6223
6224        let stopped = proc.spawn_with_label::<TestActor>("stopped", TestActor);
6225        let stopped_cell = stopped.cell().clone();
6226        stopped.drain_and_stop("test").unwrap();
6227        let terminal_status = stopped.await;
6228        assert!(terminal_status.is_terminal());
6229
6230        stopped_cell.change_status(ActorStatus::zombie("hard kill did not finish"));
6231        assert_eq!(*stopped_cell.status().borrow(), terminal_status);
6232
6233        alive.drain_and_stop("test").unwrap();
6234        alive.await;
6235    }
6236
6237    #[async_timed_test(timeout_secs = 30)]
6238    async fn change_status_rejects_zombie_for_linked_child() {
6239        let proc = Proc::isolated();
6240        let client = proc.client("client");
6241        let parent = proc.spawn_with_label::<TestActor>("parent", TestActor);
6242        let child = TestActor::spawn_child(&client, &parent).await;
6243        child
6244            .status()
6245            .clone()
6246            .wait_for(ActorStatus::is_idle)
6247            .await
6248            .unwrap();
6249
6250        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6251            child
6252                .cell()
6253                .change_status(ActorStatus::zombie("hard kill did not finish"));
6254        }));
6255        assert!(
6256            result.is_err(),
6257            "`change_status` must reject zombie status for linked children"
6258        );
6259
6260        parent.drain_and_stop("test").unwrap();
6261        assert_matches!(parent.await, ActorStatus::Stopped(reason) if reason == "test");
6262        assert_matches!(child.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6263    }
6264
6265    #[async_timed_test(timeout_secs = 30)]
6266    async fn zombie_status_does_not_complete_actor_handle() {
6267        let proc = Proc::isolated();
6268        let handle = proc.spawn_with_label::<TestActor>("alive", TestActor);
6269        handle
6270            .status()
6271            .clone()
6272            .wait_for(ActorStatus::is_idle)
6273            .await
6274            .unwrap();
6275        handle
6276            .cell()
6277            .change_status(ActorStatus::zombie("hard kill did not finish"));
6278
6279        let await_handle = handle.clone();
6280        let result = tokio::time::timeout(
6281            Duration::from_millis(100),
6282            async move { await_handle.await },
6283        )
6284        .await;
6285        assert!(
6286            result.is_err(),
6287            "zombie actor must not complete ActorHandle::await"
6288        );
6289        handle.drain_and_stop("test").unwrap();
6290        assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6291    }
6292
6293    #[async_timed_test(timeout_secs = 30)]
6294    async fn zombie_task_failure_reports_non_error_supervision_event() {
6295        let proc = Proc::isolated();
6296        let client = proc.client("client");
6297        let (mut reported_event, _coordinator) =
6298            ProcSupervisionCoordinator::set(&proc).await.unwrap();
6299        let handle = proc.spawn_with_label::<TestActor>("alive", TestActor);
6300        handle
6301            .status()
6302            .clone()
6303            .wait_for(ActorStatus::is_idle)
6304            .await
6305            .unwrap();
6306        handle
6307            .cell()
6308            .change_status(ActorStatus::zombie("hard kill did not finish"));
6309
6310        handle
6311            .fail(&client, anyhow::anyhow!("zombie failure"))
6312            .await
6313            .unwrap();
6314        assert_matches!(handle.await, ActorStatus::Failed(_));
6315
6316        let event = tokio::time::timeout(Duration::from_secs(1), reported_event.recv())
6317            .await
6318            .expect("zombie supervision event should arrive");
6319        assert!(
6320            event.actor_status.is_zombie(),
6321            "zombie actor task failure should report zombie status"
6322        );
6323        assert!(
6324            !event.is_error(),
6325            "zombie actor task failure should report a non-error supervision event"
6326        );
6327    }
6328
6329    #[async_timed_test(timeout_secs = 30)]
6330    async fn test_child_lifecycle() {
6331        let proc = Proc::isolated();
6332        let client = proc.client("client");
6333
6334        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
6335        let root_1 = TestActor::spawn_child(&client, &root).await;
6336        let root_2 = TestActor::spawn_child(&client, &root).await;
6337        let root_2_1 = TestActor::spawn_child(&client, &root_2).await;
6338
6339        root.drain_and_stop("test").unwrap();
6340        root.await;
6341
6342        for actor in [root_1, root_2, root_2_1] {
6343            assert!(
6344                actor
6345                    .port::<TestActorMessage>()
6346                    .try_post(&client, TestActorMessage::Noop())
6347                    .is_err()
6348            );
6349            assert_matches!(actor.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6350        }
6351    }
6352
6353    #[derive(Debug)]
6354    struct DrainCountingActor {
6355        handled: Arc<AtomicUsize>,
6356    }
6357
6358    impl Actor for DrainCountingActor {}
6359
6360    #[derive(Handler, Debug)]
6361    enum DrainCountingMessage {
6362        Block(oneshot::Sender<()>, oneshot::Receiver<()>),
6363        Count(),
6364    }
6365
6366    #[async_trait]
6367    #[crate::handle(DrainCountingMessage)]
6368    impl DrainCountingMessageHandler for DrainCountingActor {
6369        async fn block(
6370            &mut self,
6371            _cx: &crate::Context<Self>,
6372            entered: oneshot::Sender<()>,
6373            release: oneshot::Receiver<()>,
6374        ) -> Result<(), anyhow::Error> {
6375            entered.send(()).unwrap();
6376            release.await.unwrap();
6377            Ok(())
6378        }
6379
6380        async fn count(&mut self, _cx: &crate::Context<Self>) -> Result<(), anyhow::Error> {
6381            self.handled.fetch_add(1, Ordering::SeqCst);
6382            Ok(())
6383        }
6384    }
6385
6386    async fn block_and_queue_counts(
6387        client: &Client,
6388        handle: &ActorHandle<DrainCountingActor>,
6389        count: usize,
6390    ) -> oneshot::Sender<()> {
6391        let (entered_tx, entered_rx) = oneshot::channel();
6392        let (release_tx, release_rx) = oneshot::channel();
6393        handle.post(client, DrainCountingMessage::Block(entered_tx, release_rx));
6394        entered_rx.await.unwrap();
6395        for _ in 0..count {
6396            handle.post(client, DrainCountingMessage::Count());
6397        }
6398        release_tx
6399    }
6400
6401    #[async_timed_test(timeout_secs = 30)]
6402    async fn child_inherits_drain_mode() {
6403        let proc = Proc::isolated();
6404        let client = proc.client("client");
6405        let parent = proc.spawn(TestActor);
6406        let handled = Arc::new(AtomicUsize::new(0));
6407        let child = proc.spawn_child(
6408            parent.cell().clone(),
6409            DrainCountingActor {
6410                handled: handled.clone(),
6411            },
6412        );
6413        let release = block_and_queue_counts(&client, &child, 5).await;
6414
6415        parent.drain_and_stop("test").unwrap();
6416        release.send(()).unwrap();
6417
6418        assert_matches!(parent.await, ActorStatus::Stopped(reason) if reason == "test");
6419        assert_matches!(child.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6420        assert_eq!(handled.load(Ordering::SeqCst), 5);
6421    }
6422
6423    #[async_timed_test(timeout_secs = 30)]
6424    async fn tree_inherits_drain_mode() {
6425        let proc = Proc::isolated();
6426        let client = proc.client("client");
6427        let grandparent = proc.spawn(TestActor);
6428        let parent = proc.spawn_child(grandparent.cell().clone(), TestActor);
6429        let handled = Arc::new(AtomicUsize::new(0));
6430        let grandchild = proc.spawn_child(
6431            parent.cell().clone(),
6432            DrainCountingActor {
6433                handled: handled.clone(),
6434            },
6435        );
6436        let release = block_and_queue_counts(&client, &grandchild, 7).await;
6437
6438        grandparent.drain_and_stop("test").unwrap();
6439        release.send(()).unwrap();
6440
6441        assert_matches!(grandparent.await, ActorStatus::Stopped(reason) if reason == "test");
6442        assert_matches!(parent.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6443        assert_matches!(grandchild.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6444        assert_eq!(handled.load(Ordering::SeqCst), 7);
6445    }
6446
6447    #[derive(Debug)]
6448    struct StopFailingActor;
6449
6450    #[async_trait]
6451    impl Actor for StopFailingActor {
6452        async fn handle_stop(
6453            &mut self,
6454            _this: &Instance<Self>,
6455            _mode: StopMode,
6456            _reason: &str,
6457        ) -> Result<(), anyhow::Error> {
6458            Err(anyhow::anyhow!("stop failed"))
6459        }
6460    }
6461
6462    #[async_timed_test(timeout_secs = 30)]
6463    async fn failed_drain_stop_stops_children_immediately() {
6464        let proc = Proc::isolated();
6465        let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
6466        let parent = proc.spawn(StopFailingActor);
6467        let child = proc.spawn_child(parent.cell().clone(), TestActor);
6468
6469        parent.drain_and_stop("test").unwrap();
6470
6471        assert_matches!(child.await, ActorStatus::Stopped(reason) if reason == "parent stopping");
6472        assert_matches!(
6473            parent.await,
6474            ActorStatus::Failed(err) if err.to_string().contains("stop failed")
6475        );
6476    }
6477
6478    #[derive(Debug)]
6479    struct DeferredStopActor {
6480        stop_started: Arc<tokio::sync::Notify>,
6481        release_stop: Arc<tokio::sync::Notify>,
6482    }
6483
6484    #[async_trait]
6485    impl Actor for DeferredStopActor {
6486        async fn handle_stop(
6487            &mut self,
6488            this: &Instance<Self>,
6489            mode: StopMode,
6490            reason: &str,
6491        ) -> Result<(), anyhow::Error> {
6492            let this = this.clone_for_py();
6493            let release_stop = Arc::clone(&self.release_stop);
6494            let reason = reason.to_string();
6495            this.close();
6496            self.stop_started.notify_one();
6497            tokio::spawn(async move {
6498                release_stop.notified().await;
6499                match mode {
6500                    StopMode::Stop => this.exit(&reason).unwrap(),
6501                    StopMode::DrainAndStop => this.exit_after_drain(&reason).unwrap(),
6502                }
6503            });
6504            Ok(())
6505        }
6506    }
6507
6508    #[async_trait]
6509    impl Handler<()> for DeferredStopActor {
6510        async fn handle(&mut self, _cx: &crate::Context<Self>, _message: ()) -> anyhow::Result<()> {
6511            Ok(())
6512        }
6513    }
6514
6515    #[async_timed_test(timeout_secs = 30)]
6516    async fn test_handle_stop_can_defer_exit() {
6517        let proc = Proc::isolated();
6518        let stop_started = Arc::new(tokio::sync::Notify::new());
6519        let release_stop = Arc::new(tokio::sync::Notify::new());
6520        let handle = proc.spawn(DeferredStopActor {
6521            stop_started: Arc::clone(&stop_started),
6522            release_stop: Arc::clone(&release_stop),
6523        });
6524
6525        let mut status = handle.status();
6526        handle.stop("test").unwrap();
6527        stop_started.notified().await;
6528        status.wait_for(ActorStatus::is_stopping).await.unwrap();
6529
6530        release_stop.notify_one();
6531        assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6532    }
6533
6534    #[async_timed_test(timeout_secs = 30)]
6535    async fn test_drain_and_stop_closes_handler_ingress() {
6536        let proc = Proc::isolated();
6537        let client = proc.client("client");
6538        let stop_started = Arc::new(tokio::sync::Notify::new());
6539        let release_stop = Arc::new(tokio::sync::Notify::new());
6540        let handle = proc.spawn(DeferredStopActor {
6541            stop_started: Arc::clone(&stop_started),
6542            release_stop: Arc::clone(&release_stop),
6543        });
6544
6545        handle.drain_and_stop("test").unwrap();
6546        stop_started.notified().await;
6547
6548        // Drain closes runtime-dispatched handler ingress, so new
6549        // sends to the actor's handler port are rejected.
6550        let err = handle.port::<()>().try_post(&client, ()).unwrap_err();
6551        assert_matches!(err.kind(), crate::mailbox::MailboxSenderErrorKind::Closed);
6552
6553        release_stop.notify_one();
6554        assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6555    }
6556
6557    #[async_timed_test(timeout_secs = 30)]
6558    async fn test_parent_failure() {
6559        let proc = Proc::isolated();
6560        let client = proc.client("client");
6561        // Need to set a supervison coordinator for this Proc because there will
6562        // be actor failure(s) in this test which trigger supervision.
6563        let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
6564
6565        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
6566        let root_1 = TestActor::spawn_child(&client, &root).await;
6567        let root_2 = TestActor::spawn_child(&client, &root).await;
6568        let root_2_1 = TestActor::spawn_child(&client, &root_2).await;
6569
6570        root_2.post(
6571            &client,
6572            TestActorMessage::Fail(anyhow::anyhow!("some random failure")),
6573        );
6574        let _root_2_actor_id = root_2.actor_addr().clone();
6575        assert_matches!(
6576            root_2.await,
6577            ActorStatus::Failed(err) if err.to_string() == "some random failure"
6578        );
6579
6580        // TODO: should we provide finer-grained stop reasons, e.g., to indicate it was
6581        // stopped by a parent failure?
6582        // Currently the parent fails with an error related to the child's failure.
6583        assert_matches!(
6584            root.await,
6585            ActorStatus::Failed(err) if err.to_string().contains("some random failure")
6586        );
6587        assert_matches!(root_2_1.await, ActorStatus::Stopped(_));
6588        assert_matches!(root_1.await, ActorStatus::Stopped(_));
6589    }
6590
6591    #[async_timed_test(timeout_secs = 30)]
6592    async fn test_multi_handler() {
6593        // TEMPORARY: This test is currently a bit awkward since we don't yet expose
6594        // public interfaces to multi-handlers. This will be fixed shortly.
6595
6596        #[derive(Debug)]
6597        struct TestActor(Arc<AtomicUsize>);
6598
6599        #[async_trait]
6600        impl Actor for TestActor {}
6601
6602        #[async_trait]
6603        impl Handler<OncePortHandle<PortHandle<usize>>> for TestActor {
6604            async fn handle(
6605                &mut self,
6606                cx: &crate::Context<Self>,
6607                message: OncePortHandle<PortHandle<usize>>,
6608            ) -> anyhow::Result<()> {
6609                message.post(cx, cx.port());
6610                Ok(())
6611            }
6612        }
6613
6614        #[async_trait]
6615        impl Handler<usize> for TestActor {
6616            async fn handle(
6617                &mut self,
6618                _cx: &crate::Context<Self>,
6619                message: usize,
6620            ) -> anyhow::Result<()> {
6621                self.0.fetch_add(message, Ordering::SeqCst);
6622                Ok(())
6623            }
6624        }
6625
6626        let proc = Proc::isolated();
6627        let state = Arc::new(AtomicUsize::new(0));
6628        let actor = TestActor(state.clone());
6629        let handle = proc.spawn(actor);
6630        let client = proc.client("client");
6631        let (tx, rx) = client.open_once_port();
6632        handle.post(&client, tx);
6633        let usize_handle = rx.recv().await.unwrap();
6634        usize_handle.post(&client, 123);
6635
6636        handle.drain_and_stop("test").unwrap();
6637        handle.await;
6638
6639        assert_eq!(state.load(Ordering::SeqCst), 123);
6640    }
6641
6642    #[async_timed_test(timeout_secs = 30)]
6643    async fn test_post_after_self_message() {
6644        let proc = Proc::isolated();
6645        let client = proc.client("client");
6646        let (ready, ready_rx) = client.open_once_port();
6647        let (fired, fired_rx) = client.open_once_port();
6648        let delay = Duration::from_millis(50);
6649        let start = tokio::time::Instant::now();
6650        let handle = proc.spawn(DelayedSelfActor {
6651            ready: Some(ready.bind()),
6652            fired: Some(fired.bind()),
6653            delay,
6654        });
6655
6656        ready_rx.recv().await.unwrap();
6657        fired_rx.recv().await.unwrap();
6658
6659        assert!(start.elapsed() >= delay);
6660        handle.drain_and_stop("test").unwrap();
6661        handle.await;
6662    }
6663
6664    #[async_timed_test(timeout_secs = 30)]
6665    async fn test_post_after_port_ref() {
6666        let proc = Proc::isolated();
6667        let client = proc.client("client");
6668        let (reply, mut reply_rx) = client.open_port();
6669        let delay = Duration::from_millis(50);
6670        let start = tokio::time::Instant::now();
6671        let handle = proc.spawn(DelayedPortActor {
6672            reply: Some(reply.bind()),
6673            delay,
6674        });
6675
6676        assert_eq!(reply_rx.recv().await.unwrap(), 123);
6677        assert!(start.elapsed() >= delay);
6678        handle.drain_and_stop("test").unwrap();
6679        handle.await;
6680    }
6681
6682    #[async_timed_test(timeout_secs = 30)]
6683    async fn test_post_after_discards_pending_messages_on_shutdown() {
6684        let proc = Proc::isolated();
6685        let client = proc.client("client");
6686        let (ready, ready_rx) = client.open_once_port();
6687        let (fired, fired_rx) = client.open_once_port();
6688        let handle = proc.spawn(DelayedSelfActor {
6689            ready: Some(ready.bind()),
6690            fired: Some(fired.bind()),
6691            delay: Duration::from_secs(60),
6692        });
6693
6694        ready_rx.recv().await.unwrap();
6695        handle.drain_and_stop("test").unwrap();
6696        assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6697
6698        let result = tokio::time::timeout(Duration::from_millis(100), fired_rx.recv()).await;
6699        assert!(!matches!(result, Ok(Ok(()))));
6700    }
6701
6702    #[async_timed_test(timeout_secs = 30)]
6703    async fn test_actor_panic() {
6704        // Need this custom hook to store panic backtrace in task_local.
6705        panic_handler::set_panic_hook();
6706
6707        let proc = Proc::isolated();
6708        // Need to set a supervison coordinator for this Proc because there will
6709        // be actor failure(s) in this test which trigger supervision.
6710        let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
6711
6712        let client = proc.client("client");
6713        let actor_handle = proc.spawn(TestActor);
6714        actor_handle
6715            .panic(&client, "some random failure".to_string())
6716            .await
6717            .unwrap();
6718        let actor_status = actor_handle.await;
6719
6720        // Note: even when the test passes, the panic stacktrace will still be
6721        // printed to stderr because that is the behavior controlled by the panic
6722        // hook.
6723        assert_matches!(actor_status, ActorStatus::Failed(_));
6724        if let ActorStatus::Failed(err) = actor_status {
6725            let error_msg = err.to_string();
6726            // Verify panic message is captured
6727            assert!(error_msg.contains("some random failure"));
6728            // Verify backtrace is captured. Note the backtrace message might
6729            // change in the future. If that happens, we need to update this
6730            // statement with something up-to-date.
6731            assert!(error_msg.contains("library/std/src/panicking.rs"));
6732        }
6733    }
6734
6735    // Two independent supervision trees on one proc exercise both
6736    // propagation outcomes concurrently:
6737    //   - tree 1 (`root` -> `root_1` -> `root_1_1` -> `root_1_1_1`): a leaf
6738    //     failure bubbles up and is *contained* at `root_1`, the one actor
6739    //     that handles supervision events.
6740    //   - tree 2 (`root_2` -> `root_2_1`): a leaf failure has no handler and
6741    //     bubbles all the way to the `ProcSupervisionCoordinator`.
6742    //
6743    // The trees must not share an ancestor. If tree 2 hung off `root`, then
6744    // failing `root_2_1` would fail `root` (which does not handle events),
6745    // and a failed parent tears down its whole subtree — including `root_1`
6746    // — before `root_1` could handle its own subtree's failure. Whichever
6747    // chain reached `root` first won that race; when tree 2 won, `root_1`
6748    // was stopped without ever handling, its subtree's failure was rerouted
6749    // to the coordinator, and the test blocked forever on `root_1`'s Notify.
6750    // Independent trees make both outcomes deterministic regardless of
6751    // scheduling.
6752    #[async_timed_test(timeout_secs = 30)]
6753    async fn test_local_supervision_propagation() {
6754        hyperactor_telemetry::initialize_logging_for_test();
6755
6756        #[derive(Debug)]
6757        struct TestActor {
6758            handled: Arc<AtomicBool>,
6759            notify: Arc<tokio::sync::Notify>,
6760            should_handle: bool,
6761        }
6762
6763        #[async_trait]
6764        impl Actor for TestActor {
6765            async fn handle_supervision_event(
6766                &mut self,
6767                _this: &Instance<Self>,
6768                _event: &ActorSupervisionEvent,
6769            ) -> Result<bool, anyhow::Error> {
6770                if !self.should_handle {
6771                    return Ok(false);
6772                }
6773
6774                tracing::error!(
6775                    "{}: supervision event received: {:?}",
6776                    _this.self_addr(),
6777                    _event
6778                );
6779                self.handled.store(true, Ordering::SeqCst);
6780                self.notify.notify_one();
6781                Ok(true)
6782            }
6783        }
6784
6785        #[async_trait]
6786        impl Handler<String> for TestActor {
6787            async fn handle(
6788                &mut self,
6789                cx: &crate::Context<Self>,
6790                message: String,
6791            ) -> anyhow::Result<()> {
6792                tracing::info!("{} received message: {}", cx.self_addr(), message);
6793                Err(anyhow::anyhow!(message))
6794            }
6795        }
6796
6797        let make_actor = |handled: &Arc<AtomicBool>, should_handle: bool| TestActor {
6798            handled: handled.clone(),
6799            notify: Arc::new(tokio::sync::Notify::new()),
6800            should_handle,
6801        };
6802
6803        let proc = Proc::isolated();
6804        let client = proc.client("client");
6805        let (mut reported_event, _coordinator) =
6806            ProcSupervisionCoordinator::set(&proc).await.unwrap();
6807
6808        let root_state = Arc::new(AtomicBool::new(false));
6809        let root_1_state = Arc::new(AtomicBool::new(false));
6810        let root_1_notify = Arc::new(tokio::sync::Notify::new());
6811        let root_1_1_state = Arc::new(AtomicBool::new(false));
6812        let root_1_1_1_state = Arc::new(AtomicBool::new(false));
6813        let root_2_state = Arc::new(AtomicBool::new(false));
6814        let root_2_1_state = Arc::new(AtomicBool::new(false));
6815
6816        let root = proc.spawn_with_label::<TestActor>("root", make_actor(&root_state, false));
6817        let root_1 = proc.spawn_child::<TestActor>(
6818            root.cell().clone(),
6819            TestActor {
6820                handled: root_1_state.clone(),
6821                notify: root_1_notify.clone(),
6822                should_handle: true, // children's event stops here
6823            },
6824        );
6825        let root_1_1 = proc
6826            .spawn_child::<TestActor>(root_1.cell().clone(), make_actor(&root_1_1_state, false));
6827        let root_1_1_1 = proc.spawn_child::<TestActor>(
6828            root_1_1.cell().clone(),
6829            make_actor(&root_1_1_1_state, false),
6830        );
6831        // `root_2` is a second, independent root — deliberately not a child
6832        // of `root` — so failing its subtree cannot tear down tree 1.
6833        let root_2 = proc.spawn_with_label::<TestActor>("root_2", make_actor(&root_2_state, false));
6834        let root_2_1 = proc
6835            .spawn_child::<TestActor>(root_2.cell().clone(), make_actor(&root_2_1_state, false));
6836
6837        // fail `root_1_1_1`, the supervision msg should be propagated to
6838        // `root_1` because `root_1` has set `true` to `handle_supervision_event`.
6839        root_1_1_1.post(&client, "some random failure".to_string());
6840
6841        // fail `root_2_1`, the supervision msg should be propagated to
6842        // ProcSupervisionCoordinator.
6843        let root_2_1_id = root_2_1.actor_addr().clone();
6844        root_2_1.post(&client, "some random failure".to_string());
6845
6846        // Wait for root_1 to handle the supervision event from the
6847        // root_1_1_1 -> root_1_1 -> root_1 chain. The Notify provides
6848        // a deterministic signal — no polling or timing needed.
6849        root_1_notify.notified().await;
6850
6851        // Wait for the supervision event from root_2_1's failure to
6852        // reach the ProcSupervisionCoordinator.
6853        let event = reported_event.recv().await;
6854        assert_eq!(event.actor_id, root_2_1_id);
6855
6856        assert!(!root_state.load(Ordering::SeqCst));
6857        assert!(root_1_state.load(Ordering::SeqCst));
6858        assert!(!root_1_1_state.load(Ordering::SeqCst));
6859        assert!(!root_1_1_1_state.load(Ordering::SeqCst));
6860        assert!(!root_2_state.load(Ordering::SeqCst));
6861        assert!(!root_2_1_state.load(Ordering::SeqCst));
6862    }
6863
6864    #[async_timed_test(timeout_secs = 30)]
6865    async fn test_instance() {
6866        #[derive(Debug, Default)]
6867        struct TestActor;
6868
6869        impl Actor for TestActor {}
6870
6871        #[async_trait]
6872        impl Handler<(String, PortRef<String>)> for TestActor {
6873            async fn handle(
6874                &mut self,
6875                cx: &crate::Context<Self>,
6876                (message, port): (String, PortRef<String>),
6877            ) -> anyhow::Result<()> {
6878                port.post(cx, message);
6879                Ok(())
6880            }
6881        }
6882
6883        let proc = Proc::isolated();
6884
6885        let client = proc.client("my_test_actor");
6886        let status = client.status();
6887
6888        let child_actor = client.spawn(TestActor);
6889
6890        let (port, mut receiver) = client.open_port();
6891        child_actor.post(&client, ("hello".to_string(), port.bind()));
6892
6893        let message = receiver.recv().await.unwrap();
6894        assert_eq!(message, "hello");
6895
6896        child_actor.drain_and_stop("test").unwrap();
6897        child_actor.await;
6898
6899        assert_eq!(*status.borrow(), ActorStatus::Client);
6900        drop(client);
6901        assert_matches!(*status.borrow(), ActorStatus::Stopped(_));
6902    }
6903
6904    // Tokio's I/O driver is not fork-safe on macOS, and this test intentionally
6905    // validates process termination by forking without a coordinator.
6906    #[cfg_attr(target_os = "macos", ignore = "tokio runtime fork assertion on macOS")]
6907    #[tokio::test]
6908    async fn test_proc_terminate_without_coordinator() {
6909        if std::env::var("CARGO_TEST").is_ok() {
6910            eprintln!("test skipped as it hangs when run by cargo in sandcastle");
6911            return;
6912        }
6913
6914        let process = async {
6915            let proc = Proc::isolated();
6916            // Intentionally not setting a proc supervison coordinator. This
6917            // should cause the process to terminate.
6918            // ProcSupervisionCoordinator::set(&proc).await.unwrap();
6919            let root = proc.spawn_with_label("root", TestActor);
6920            let client = proc.client("client");
6921            root.fail(&client, anyhow::anyhow!("some random failure"))
6922                .await
6923                .unwrap();
6924            // It is okay to sleep a long time here, because we expect this
6925            // process to be terminated way before the sleep ends due to the
6926            // missing proc supervison coordinator.
6927            tokio::time::sleep(Duration::from_secs(30)).await;
6928        };
6929
6930        assert_termination(|| process, 1).await.unwrap();
6931    }
6932
6933    fn trace_and_block(fut: impl Future) {
6934        tracing::subscriber::with_default(
6935            tracing_subscriber::Registry::default().with(hyperactor_telemetry::recorder().layer()),
6936            || {
6937                tokio::runtime::Builder::new_current_thread()
6938                    .enable_all()
6939                    .build()
6940                    .unwrap()
6941                    .block_on(fut)
6942            },
6943        );
6944    }
6945
6946    #[test]
6947    fn test_handler_logging() {
6948        #[derive(Debug, Default)]
6949        struct LoggingActor;
6950
6951        impl Actor for LoggingActor {}
6952
6953        impl LoggingActor {
6954            async fn wait(cx: &impl context::Actor, handle: &ActorHandle<Self>) {
6955                let barrier = Arc::new(Barrier::new(2));
6956                handle.post(cx, barrier.clone());
6957                barrier.wait().await;
6958            }
6959        }
6960
6961        #[async_trait]
6962        impl Handler<String> for LoggingActor {
6963            async fn handle(
6964                &mut self,
6965                _cx: &crate::Context<Self>,
6966                message: String,
6967            ) -> anyhow::Result<()> {
6968                tracing::info!("{}", message);
6969                Ok(())
6970            }
6971        }
6972
6973        #[async_trait]
6974        impl Handler<u64> for LoggingActor {
6975            async fn handle(
6976                &mut self,
6977                _cx: &crate::Context<Self>,
6978                message: u64,
6979            ) -> anyhow::Result<()> {
6980                tracing::event!(Level::INFO, number = message);
6981                Ok(())
6982            }
6983        }
6984
6985        #[async_trait]
6986        impl Handler<Arc<Barrier>> for LoggingActor {
6987            async fn handle(
6988                &mut self,
6989                _cx: &crate::Context<Self>,
6990                message: Arc<Barrier>,
6991            ) -> anyhow::Result<()> {
6992                message.wait().await;
6993                Ok(())
6994            }
6995        }
6996
6997        #[async_trait]
6998        impl Handler<Arc<(Barrier, Barrier)>> for LoggingActor {
6999            #[expect(
7000                clippy::await_holding_invalid_type,
7001                reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
7002            )]
7003            async fn handle(
7004                &mut self,
7005                _cx: &crate::Context<Self>,
7006                barriers: Arc<(Barrier, Barrier)>,
7007            ) -> anyhow::Result<()> {
7008                let inner = tracing::span!(Level::INFO, "child_span");
7009                let _inner_guard = inner.enter();
7010                barriers.0.wait().await;
7011                barriers.1.wait().await;
7012                Ok(())
7013            }
7014        }
7015
7016        trace_and_block(async {
7017            let proc = Proc::isolated();
7018            let client = proc.client("client");
7019            let handle = hyperactor::spawn(LoggingActor).into_guard();
7020            handle.post(&client, "hello world".to_string());
7021            handle.post(&client, "hello world again".to_string());
7022            handle.post(&client, 123u64);
7023
7024            LoggingActor::wait(&client, &handle).await;
7025
7026            let events = handle.cell().inner.recording.tail();
7027            assert_eq!(events.len(), 3);
7028            assert_eq!(events[0].json_value(), json!({ "message": "hello world" }));
7029            assert_eq!(
7030                events[1].json_value(),
7031                json!({ "message": "hello world again" })
7032            );
7033            assert_eq!(events[2].json_value(), json!({ "number": 123 }));
7034
7035            let stacks = {
7036                let barriers = Arc::new((Barrier::new(2), Barrier::new(2)));
7037                handle.post(&client, Arc::clone(&barriers));
7038                barriers.0.wait().await;
7039                let stacks = handle.cell().inner.recording.stacks();
7040                barriers.1.wait().await;
7041                stacks
7042            };
7043            assert_eq!(stacks.len(), 1);
7044            assert_eq!(stacks[0].len(), 1);
7045            assert_eq!(stacks[0][0].name(), "child_span");
7046        })
7047    }
7048
7049    #[async_timed_test(timeout_secs = 30)]
7050    async fn test_mailbox_closed_with_owner_stopped_reason() {
7051        let proc = Proc::isolated();
7052        let client = proc.client("client");
7053        let actor_handle = proc.spawn(TestActor);
7054
7055        // Clone the handle before awaiting since await consumes the handle
7056        let handle_for_send = actor_handle.clone();
7057
7058        // Stop the actor gracefully
7059        actor_handle.drain_and_stop("healthy shutdown").unwrap();
7060        actor_handle.await;
7061
7062        // Try to send a message to the stopped actor
7063        let result = handle_for_send
7064            .port::<TestActorMessage>()
7065            .try_post(&client, TestActorMessage::Noop());
7066
7067        assert!(result.is_err(), "send should fail when actor is stopped");
7068        let err = result.unwrap_err();
7069        assert_matches!(
7070            err.kind(),
7071            crate::mailbox::MailboxSenderErrorKind::Mailbox(mailbox_err)
7072                if matches!(
7073                    mailbox_err.kind(),
7074                    crate::mailbox::MailboxErrorKind::OwnerTerminated(ActorStatus::Stopped(reason)) if reason == "healthy shutdown"
7075                )
7076        );
7077    }
7078
7079    #[async_timed_test(timeout_secs = 30)]
7080    async fn test_mailbox_closed_with_owner_failed_reason() {
7081        let proc = Proc::isolated();
7082        let client = proc.client("client");
7083        // Need to set a supervison coordinator for this Proc because there will
7084        // be actor failure(s) in this test which trigger supervision.
7085        let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
7086
7087        let actor_handle = proc.spawn(TestActor);
7088
7089        // Clone the handle before awaiting since await consumes the handle
7090        let handle_for_send = actor_handle.clone();
7091
7092        // Cause the actor to fail
7093        actor_handle.post(
7094            &client,
7095            TestActorMessage::Fail(anyhow::anyhow!("intentional failure")),
7096        );
7097        actor_handle.await;
7098
7099        // Try to send a message to the failed actor
7100        let result = handle_for_send
7101            .port::<TestActorMessage>()
7102            .try_post(&client, TestActorMessage::Noop());
7103
7104        assert!(result.is_err(), "send should fail when actor has failed");
7105        let err = result.unwrap_err();
7106        assert_matches!(
7107            err.kind(),
7108            crate::mailbox::MailboxSenderErrorKind::Mailbox(mailbox_err)
7109                if matches!(
7110                    mailbox_err.kind(),
7111                    crate::mailbox::MailboxErrorKind::OwnerTerminated(ActorStatus::Failed(ActorErrorKind::Generic(msg)))
7112                        if msg.contains("intentional failure")
7113                )
7114        );
7115    }
7116
7117    /// Wait for a terminated snapshot to appear for the given actor.
7118    /// The introspect task runs in a separate tokio task and may not
7119    /// have stored the snapshot by the time `handle.await` returns.
7120    async fn wait_for_terminated_snapshot(
7121        proc: &Proc,
7122        actor_id: &ActorAddr,
7123    ) -> crate::introspect::IntrospectResult {
7124        // Yield to let the introspect task run, then poll. Use a
7125        // combination of yields (for fast paths) and sleeps (to
7126        // avoid busy-spinning if the scheduler is loaded).
7127        for i in 0..1000 {
7128            if let Some(snapshot) = proc.terminated_snapshot(actor_id) {
7129                return snapshot;
7130            }
7131            if i < 50 {
7132                tokio::task::yield_now().await;
7133            } else {
7134                tokio::time::sleep(Duration::from_millis(50)).await;
7135            }
7136        }
7137        panic!("timed out waiting for terminated snapshot for {}", actor_id);
7138    }
7139
7140    // Verifies that when an actor is stopped, the proc eventually
7141    // records a "terminated snapshot" for it (written by the
7142    // introspect task, which runs asynchronously). The test asserts
7143    // the snapshot is absent while the actor is live, then stops the
7144    // actor, waits for the introspect task to observe the terminal
7145    // state, and confirms:
7146    //   - the stored snapshot reports a `stopped:*` actor_status, and
7147    //   - the actor id moves from the live set to the terminated set.
7148    #[async_timed_test(timeout_secs = 60)]
7149    async fn test_terminated_snapshot_stored_on_stop() {
7150        let proc = Proc::isolated();
7151        let _client = proc.client("client");
7152
7153        let handle = proc.spawn(TestActor);
7154        let actor_id = handle.actor_addr().clone();
7155
7156        // Actor is live — no terminated snapshot yet.
7157        assert!(proc.terminated_snapshot(&actor_id).is_none());
7158        assert!(!proc.all_terminated_actor_ids().contains(&actor_id));
7159
7160        // Stop the actor and wait for it to fully terminate.
7161        handle.drain_and_stop("test").unwrap();
7162        handle.await;
7163
7164        // The introspect task runs in a separate tokio task; wait for
7165        // it to observe the terminal status and store the snapshot.
7166        let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7167        let attrs: hyperactor_config::Attrs =
7168            serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7169        let status = attrs
7170            .get(crate::introspect::STATUS)
7171            .expect("must have status");
7172        assert!(
7173            status.starts_with("stopped"),
7174            "expected stopped status, got: {}",
7175            status
7176        );
7177
7178        // Actor should appear in terminated IDs but not in live IDs.
7179        assert!(proc.all_terminated_actor_ids().contains(&actor_id));
7180        assert!(
7181            !proc.all_actor_ids().contains(&actor_id),
7182            "stopped actor should not appear in live actor IDs"
7183        );
7184    }
7185
7186    // Verifies that an actor failure results in a terminated snapshot
7187    // being stored. The test installs a ProcSupervisionCoordinator
7188    // (required for failure handling), spawns an actor, triggers a
7189    // failure via a message, waits for the actor to terminate, then
7190    // waits for the introspect task to persist the terminal snapshot
7191    // and asserts the snapshot reports a `failed:*` actor_status.
7192    #[async_timed_test(timeout_secs = 60)]
7193    async fn test_terminated_snapshot_stored_on_failure() {
7194        let proc = Proc::isolated();
7195        let client = proc.client("client");
7196        // Supervision coordinator required for actor failure handling.
7197        ProcSupervisionCoordinator::set(&proc).await.unwrap();
7198
7199        let handle = proc.spawn(TestActor);
7200        let actor_id = handle.actor_addr().clone();
7201
7202        // Trigger a failure.
7203        handle.post(&client, TestActorMessage::Fail(anyhow::anyhow!("boom")));
7204        handle.await;
7205
7206        let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7207        let attrs: hyperactor_config::Attrs =
7208            serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7209        let status = attrs
7210            .get(crate::introspect::STATUS)
7211            .expect("must have status");
7212        assert!(
7213            status.starts_with("failed"),
7214            "expected failed status, got: {}",
7215            status
7216        );
7217    }
7218
7219    // Exercises FI-1/FI-2 (see introspect.rs module-scope comment).
7220    #[async_timed_test(timeout_secs = 30)]
7221    async fn test_supervision_event_stored_on_failure() {
7222        let proc = Proc::isolated();
7223        let client = proc.client("client");
7224        ProcSupervisionCoordinator::set(&proc).await.unwrap();
7225
7226        let handle = proc.spawn(TestActor);
7227        let actor_id = handle.actor_addr().clone();
7228        let cell = handle.cell().clone();
7229
7230        handle.post(&client, TestActorMessage::Fail(anyhow::anyhow!("boom")));
7231        handle.await;
7232
7233        let event = cell
7234            .supervision_event()
7235            .expect("failed actor must have supervision_event");
7236        assert_eq!(event.actor_id, actor_id);
7237        assert!(event.actor_status.is_failed());
7238        // Originated here, not propagated.
7239        assert_eq!(event.actually_failing_actor().unwrap().actor_id, actor_id);
7240    }
7241
7242    // Exercises FI-2 (see introspect.rs module-scope comment).
7243    #[async_timed_test(timeout_secs = 30)]
7244    async fn test_supervision_event_on_clean_stop() {
7245        let proc = Proc::isolated();
7246        let _client = proc.client("client");
7247
7248        let handle = proc.spawn(TestActor);
7249        let cell = handle.cell().clone();
7250
7251        handle.drain_and_stop("test").unwrap();
7252        handle.await;
7253
7254        let event = cell
7255            .supervision_event()
7256            .expect("clean stop must store supervision event");
7257        assert!(
7258            matches!(event.actor_status, ActorStatus::Stopped(_)),
7259            "expected Stopped status, got {:?}",
7260            event.actor_status
7261        );
7262        assert!(!event.is_error());
7263    }
7264
7265    #[async_timed_test(timeout_secs = 30)]
7266    async fn test_supervision_coordinator_receives_clean_stop() {
7267        let proc = Proc::isolated();
7268        let _client = proc.client("client");
7269        let (mut reported_event, _coordinator_handle) =
7270            ProcSupervisionCoordinator::set(&proc).await.unwrap();
7271
7272        let handle = proc.spawn(TestActor);
7273        let actor_id = handle.actor_addr().clone();
7274
7275        handle.drain_and_stop("test").unwrap();
7276        handle.await;
7277
7278        let event = reported_event.recv().await;
7279        assert_eq!(event.actor_id, actor_id);
7280        assert!(
7281            matches!(event.actor_status, ActorStatus::Stopped(_)),
7282            "expected Stopped status, got {:?}",
7283            event.actor_status
7284        );
7285        assert!(!event.is_error());
7286    }
7287
7288    #[async_timed_test(timeout_secs = 30)]
7289    async fn test_coordinator_shuts_down_last_during_destroy() {
7290        let mut proc = Proc::isolated();
7291        let _client = proc.client("client");
7292        let (mut reported_event, _coordinator_handle) =
7293            ProcSupervisionCoordinator::set(&proc).await.unwrap();
7294
7295        // Spawn several actors that will all stop during destroy_and_wait.
7296        let mut actor_ids = Vec::new();
7297        for i in 0..3 {
7298            let handle = proc.spawn_with_label::<TestActor>(&format!("actor_{i}"), TestActor);
7299            actor_ids.push(handle.actor_addr().clone());
7300        }
7301
7302        // destroy_and_wait stops all actors. If the coordinator were stopped
7303        // simultaneously, supervision event delivery would fail and crash
7304        // the process. The fact that this completes without crashing proves
7305        // the coordinator outlived the other actors.
7306        proc.destroy_and_wait(Duration::from_secs(5), "test")
7307            .await
7308            .unwrap();
7309
7310        // Verify the coordinator received stop events from all three actors.
7311        let mut received_ids = Vec::new();
7312        for _ in 0..actor_ids.len() {
7313            let event = reported_event.recv().await;
7314            assert!(
7315                matches!(event.actor_status, ActorStatus::Stopped(_)),
7316                "expected Stopped, got {:?}",
7317                event.actor_status
7318            );
7319            received_ids.push(event.actor_id);
7320        }
7321        received_ids.sort();
7322        actor_ids.sort();
7323        assert_eq!(received_ids, actor_ids);
7324    }
7325
7326    // Exercises FI-4 (see introspect.rs module-scope comment).
7327    #[async_timed_test(timeout_secs = 30)]
7328    async fn test_supervision_event_on_propagated_failure() {
7329        let proc = Proc::isolated();
7330        let client = proc.client("client");
7331        ProcSupervisionCoordinator::set(&proc).await.unwrap();
7332
7333        let parent = proc.spawn_with_label::<TestActor>("parent", TestActor);
7334        let parent_cell = parent.cell().clone();
7335        // Spawn child under parent.
7336        let (tx, rx) = oneshot::channel();
7337        parent.post(&client, TestActorMessage::Spawn(tx));
7338        let child = rx.await.unwrap();
7339        let child_id = child.actor_addr().clone();
7340
7341        // Fail the child — parent doesn't handle supervision, so it
7342        // propagates and terminates too.
7343        child.post(
7344            &client,
7345            TestActorMessage::Fail(anyhow::anyhow!("child boom")),
7346        );
7347        parent.await;
7348
7349        let event = parent_cell.supervision_event();
7350        assert!(
7351            event.is_some(),
7352            "parent must have supervision_event from propagated failure"
7353        );
7354        let event = event.unwrap();
7355        // Root cause is the child, not the parent.
7356        assert_eq!(event.actually_failing_actor().unwrap().actor_id, child_id);
7357    }
7358
7359    // Exercises S11 (see introspect.rs module doc).
7360    //
7361    // A live actor is resolvable. After drain_and_stop + await, the
7362    // actor's status is terminal and resolve_actor_ref must return
7363    // None — even though the introspect task may still hold a strong
7364    // InstanceCell Arc (it drops the Arc only after observing
7365    // terminal status asynchronously). The is_terminal() check in
7366    // resolve_actor_ref closes that race window.
7367    #[async_timed_test(timeout_secs = 30)]
7368    async fn test_resolve_actor_ref_none_for_terminal_actor() {
7369        let proc = Proc::isolated();
7370        let _client = proc.client("client");
7371
7372        let handle = proc.spawn(TestActor);
7373        let actor_ref: ActorRef<TestActor> = handle.bind();
7374
7375        // Actor is live — resolve should succeed.
7376        assert!(
7377            proc.resolve_actor_ref(&actor_ref).is_some(),
7378            "live actor should be resolvable"
7379        );
7380
7381        handle.drain_and_stop("test").unwrap();
7382        handle.await;
7383
7384        // Actor is terminal — resolve must return None regardless of
7385        // whether the introspect task has dropped its Arc yet.
7386        assert!(
7387            proc.resolve_actor_ref(&actor_ref).is_none(),
7388            "terminal actor must not be resolvable"
7389        );
7390    }
7391
7392    // Exercises FI-3 (see introspect module doc).
7393    #[async_timed_test(timeout_secs = 60)]
7394    async fn test_terminated_snapshot_has_failure_info() {
7395        let proc = Proc::isolated();
7396        let client = proc.client("client");
7397        ProcSupervisionCoordinator::set(&proc).await.unwrap();
7398
7399        let handle = proc.spawn(TestActor);
7400        let actor_id = handle.actor_addr().clone();
7401
7402        handle.post(&client, TestActorMessage::Fail(anyhow::anyhow!("kaboom")));
7403        handle.await;
7404
7405        let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7406        let attrs: hyperactor_config::Attrs =
7407            serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7408        let status = attrs
7409            .get(crate::introspect::STATUS)
7410            .expect("must have status");
7411        assert!(
7412            status.starts_with("failed"),
7413            "expected failed status, got: {}",
7414            status
7415        );
7416        let err_msg = attrs
7417            .get(crate::introspect::FAILURE_ERROR_MESSAGE)
7418            .expect("failed actor must have failure_error_message");
7419        assert!(!err_msg.is_empty());
7420        let root_cause = attrs
7421            .get(crate::introspect::FAILURE_ROOT_CAUSE_ACTOR)
7422            .expect("must have root_cause_actor");
7423        assert_eq!(root_cause, &actor_id);
7424        assert_eq!(
7425            attrs.get(crate::introspect::FAILURE_IS_PROPAGATED),
7426            Some(&false)
7427        );
7428        assert!(
7429            attrs.get(crate::introspect::FAILURE_OCCURRED_AT).is_some(),
7430            "failed actor must have occurred_at"
7431        );
7432    }
7433
7434    // Exercises FI-4 (see introspect module doc).
7435    #[async_timed_test(timeout_secs = 60)]
7436    async fn test_propagated_failure_info() {
7437        let proc = Proc::isolated();
7438        let client = proc.client("client");
7439        ProcSupervisionCoordinator::set(&proc).await.unwrap();
7440
7441        let parent = proc.spawn_with_label::<TestActor>("parent", TestActor);
7442        let parent_id = parent.actor_addr().clone();
7443
7444        let (tx, rx) = oneshot::channel();
7445        parent.post(&client, TestActorMessage::Spawn(tx));
7446        let child = rx.await.unwrap();
7447        let child_id = child.actor_addr().clone();
7448
7449        child.post(
7450            &client,
7451            TestActorMessage::Fail(anyhow::anyhow!("child fail")),
7452        );
7453        parent.await;
7454
7455        let snapshot = wait_for_terminated_snapshot(&proc, &parent_id).await;
7456        let attrs: hyperactor_config::Attrs =
7457            serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7458        let root_cause = attrs
7459            .get(crate::introspect::FAILURE_ROOT_CAUSE_ACTOR)
7460            .expect("propagated failure must have root_cause_actor");
7461        assert_eq!(root_cause, &child_id);
7462        assert_eq!(
7463            attrs.get(crate::introspect::FAILURE_IS_PROPAGATED),
7464            Some(&true)
7465        );
7466    }
7467
7468    /// Exercises AI-1 (see module doc).
7469    #[async_timed_test(timeout_secs = 30)]
7470    async fn test_spawn_with_name_creates_descriptive_name() {
7471        let proc = Proc::isolated();
7472        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7473        let handle = proc.spawn_named_child(root.cell().clone(), "my_controller", TestActor);
7474        assert_eq!(
7475            handle.actor_addr().label().unwrap().as_str(),
7476            "my_controller"
7477        );
7478        assert!(!handle.actor_addr().is_root());
7479    }
7480
7481    /// Exercises AI-1 (see module doc).
7482    #[async_timed_test(timeout_secs = 30)]
7483    async fn test_spawn_with_name_increments_index() {
7484        let proc = Proc::isolated();
7485        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7486        let first = proc.spawn_named_child(root.cell().clone(), "my_controller", TestActor);
7487        let second = proc.spawn_named_child(root.cell().clone(), "my_controller", TestActor);
7488        assert_ne!(first.actor_addr().uid(), second.actor_addr().uid());
7489    }
7490
7491    /// Exercises AI-1 (see module doc).
7492    /// spawn_named_child passes Some(parent) to spawn_inner.
7493    #[async_timed_test(timeout_secs = 30)]
7494    async fn test_spawn_with_name_preserves_supervision() {
7495        let proc = Proc::isolated();
7496        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7497        let child = proc.spawn_named_child(root.cell().clone(), "supervised_child", TestActor);
7498        let child_cell = child.cell();
7499        let parent = child_cell.parent().expect("child must have parent");
7500        assert_eq!(parent.actor_addr(), root.actor_addr());
7501    }
7502
7503    /// Exercises AI-1 (see module doc).
7504    #[async_timed_test(timeout_secs = 30)]
7505    async fn test_spawn_unchanged() {
7506        let proc = Proc::isolated();
7507        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7508        let child = proc.spawn_child(root.cell().clone(), TestActor);
7509        assert!(!child.actor_addr().is_root());
7510    }
7511
7512    /// Exercises AI-1 (see module doc).
7513    #[async_timed_test(timeout_secs = 30)]
7514    async fn test_spawn_with_name_different_names_different_pids() {
7515        let proc = Proc::isolated();
7516        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7517        let a = proc.spawn_named_child(root.cell().clone(), "controller_a", TestActor);
7518        let b = proc.spawn_named_child(root.cell().clone(), "controller_b", TestActor);
7519        assert_ne!(a.actor_addr().uid(), b.actor_addr().uid());
7520        assert_eq!(a.actor_addr().label().unwrap().as_str(), "controller_a");
7521        assert_eq!(b.actor_addr().label().unwrap().as_str(), "controller_b");
7522    }
7523
7524    /// Exercises AI-1 (see module doc).
7525    #[async_timed_test(timeout_secs = 30)]
7526    async fn test_spawn_with_name_no_child_overwrite() {
7527        let proc = Proc::isolated();
7528        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7529        let _a = proc.spawn_named_child(root.cell().clone(), "ctrl", TestActor);
7530        let _b = proc.spawn_named_child(root.cell().clone(), "ctrl", TestActor);
7531        let _c = proc.spawn_child(root.cell().clone(), TestActor);
7532        assert_eq!(root.cell().child_count(), 3);
7533    }
7534
7535    /// Exercises AI-1 (see module doc).
7536    #[async_timed_test(timeout_secs = 30)]
7537    async fn test_spawn_with_name_does_not_pollute_roots() {
7538        let proc = Proc::isolated();
7539        let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7540        let _child = proc.spawn_named_child(root.cell().clone(), "foo", TestActor);
7541        // "foo" was used as a named child name but should NOT
7542        // prevent spawning a root actor with that name.
7543        let _root = proc.spawn_with_label::<TestActor>("foo", TestActor);
7544    }
7545
7546    /// Exercises AI-3 (see module doc).
7547    #[async_timed_test(timeout_secs = 30)]
7548    async fn test_ai3_controller_actor_ids_unique_across_parents_same_proc() {
7549        let proc = Proc::isolated();
7550        let parent_a = proc.spawn_with_label::<TestActor>("parent_a", TestActor);
7551        let parent_b = proc.spawn_with_label::<TestActor>("parent_b", TestActor);
7552
7553        // Simulate the correct pattern: include mesh identity in name.
7554        let ctrl_a =
7555            proc.spawn_named_child(parent_a.cell().clone(), "controller_mesh_a", TestActor);
7556        let ctrl_b =
7557            proc.spawn_named_child(parent_b.cell().clone(), "controller_mesh_b", TestActor);
7558
7559        assert_ne!(
7560            ctrl_a.actor_addr(),
7561            ctrl_b.actor_addr(),
7562            "controller ActorAddrs must be unique across parents"
7563        );
7564    }
7565
7566    /// Exercises AI-3 (see module doc).
7567    #[async_timed_test(timeout_secs = 30)]
7568    async fn test_ai3_no_controller_overwrite_in_parent_or_proc_maps() {
7569        let proc = Proc::isolated();
7570        let parent_a = proc.spawn_with_label::<TestActor>("parent_a", TestActor);
7571        let parent_b = proc.spawn_with_label::<TestActor>("parent_b", TestActor);
7572
7573        let ctrl_a =
7574            proc.spawn_named_child(parent_a.cell().clone(), "controller_mesh_a", TestActor);
7575        let ctrl_b =
7576            proc.spawn_named_child(parent_b.cell().clone(), "controller_mesh_b", TestActor);
7577
7578        // Both must be independently resolvable via the proc's instances.
7579        assert!(
7580            proc.get_instance(ctrl_a.actor_addr()).is_some(),
7581            "ctrl_a must be resolvable"
7582        );
7583        assert!(
7584            proc.get_instance(ctrl_b.actor_addr()).is_some(),
7585            "ctrl_b must be resolvable"
7586        );
7587        // Parents each see exactly one child.
7588        assert_eq!(parent_a.cell().child_count(), 1);
7589        assert_eq!(parent_b.cell().child_count(), 1);
7590    }
7591
7592    // Exercises FI-6 (see introspect module doc).
7593    #[async_timed_test(timeout_secs = 60)]
7594    async fn test_stopped_snapshot_has_no_failure_info() {
7595        let proc = Proc::isolated();
7596        let _client = proc.client("client");
7597
7598        let handle = proc.spawn(TestActor);
7599        let actor_id = handle.actor_addr().clone();
7600
7601        handle.drain_and_stop("test").unwrap();
7602        handle.await;
7603
7604        let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7605        let attrs: hyperactor_config::Attrs =
7606            serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7607        let status = attrs
7608            .get(crate::introspect::STATUS)
7609            .expect("must have status");
7610        assert!(
7611            status.starts_with("stopped"),
7612            "expected stopped, got: {}",
7613            status
7614        );
7615        assert!(
7616            attrs
7617                .get(crate::introspect::FAILURE_ERROR_MESSAGE)
7618                .is_none(),
7619            "stopped actor must not have failure attrs"
7620        );
7621    }
7622
7623    // ── PD-5: queue depth accounting ────────────────────────────
7624
7625    // PD-5b/PD-5c: queue depth increments on enqueue, decrements on
7626    // dequeue, and returns to zero after the message is handled. This
7627    // tests that the introspection-readable queue_depth is aligned
7628    // with the existing OTel ACTOR_MESSAGE_QUEUE_SIZE accounting.
7629    #[async_timed_test(timeout_secs = 10)]
7630    async fn test_queue_depth_increment_decrement() {
7631        let proc = Proc::isolated();
7632        let client = proc.client("client");
7633        let handle = proc.spawn_with_label("qd_test", TestActor);
7634        let actor_ref: crate::ActorRef<TestActor> = handle.bind();
7635        let actor_id = actor_ref.actor_addr().clone();
7636
7637        // Before any message: queue depth should be 0.
7638        let cell = proc.get_instance(&actor_id).expect("actor must exist");
7639        assert_eq!(cell.queue_depth(), 0, "initial queue depth should be 0");
7640
7641        // Send a message that blocks until we signal it. This lets
7642        // us observe queue depth > 0 while the actor is busy.
7643        let (reply_tx, reply_rx) = oneshot::channel();
7644        let (gate_tx, gate_rx) = oneshot::channel::<()>();
7645        handle.wait(&client, reply_tx, gate_rx).await.unwrap();
7646
7647        // Wait for the actor to start processing (it sends reply_tx).
7648        reply_rx.await.unwrap();
7649
7650        // Now send a second message — it should be queued.
7651        let (reply2_tx, reply2_rx) = oneshot::channel();
7652        handle.reply(&client, reply2_tx).await.unwrap();
7653
7654        // Give the enqueue a moment to propagate.
7655        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7656
7657        // Queue depth should be >= 1 (the Reply message is queued).
7658        let depth = cell.queue_depth();
7659        assert!(
7660            depth >= 1,
7661            "expected queue depth >= 1 while actor is busy, got {depth}"
7662        );
7663
7664        // Unblock the first message.
7665        let _ = gate_tx.send(());
7666
7667        // Wait for the second message to be handled.
7668        reply2_rx.await.unwrap();
7669
7670        // Give the dequeue a moment to propagate.
7671        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7672
7673        // Queue depth should return to 0.
7674        let depth = cell.queue_depth();
7675        assert_eq!(
7676            depth, 0,
7677            "queue depth should return to 0 after all messages handled"
7678        );
7679    }
7680
7681    // Test-only Named message + dedicated actor with explicit handler
7682    // export, so the integration test can bind the BufferTestMsg
7683    // handler port (`handle.bind()`) and drive ordered traffic through
7684    // the sequenced receiver. Without the bind, `PortHandle::try_post`
7685    // stamps `SeqInfo::Direct`, and the enqueue closure bypasses
7686    // sequencing, leaving the snapshot empty.
7687    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, typeuri::Named)]
7688    struct BufferTestMsg;
7689
7690    #[derive(Debug, Default)]
7691    #[hyperactor::export(handlers = [BufferTestMsg])]
7692    struct BufferTestActor;
7693
7694    #[async_trait]
7695    impl Actor for BufferTestActor {}
7696
7697    #[async_trait]
7698    impl Handler<BufferTestMsg> for BufferTestActor {
7699        async fn handle(
7700            &mut self,
7701            _cx: &crate::Context<Self>,
7702            _msg: BufferTestMsg,
7703        ) -> anyhow::Result<()> {
7704            Ok(())
7705        }
7706    }
7707
7708    /// Exercises IO-1 ("active" branch: snapshot is `Some({enabled:
7709    /// true, ...})`), IO-2 (publish-time state via `try_lock` populates
7710    /// the session's buffered fields), and IO-3 (asserts `queue_depth`
7711    /// is a propagated `u64` but makes NO arithmetic claim relating it
7712    /// to `buffered_count`). End-to-end wiring proof: `Instance::new`
7713    /// installs the snapshot callback, `InstanceCell::inbound_ordering_snapshot()`
7714    /// invokes it, the resulting snapshot includes the buffered session
7715    /// with its sender, `expected_next_seq`, and `buffered_count`, AND
7716    /// `build_actor_attrs` (via `live_actor_payload`) publishes
7717    /// `INBOUND_ORDERING` so it round-trips through `ActorAttrsView`.
7718    /// The attrs-publish assertion catches accidental removal of the
7719    /// `attrs.set(INBOUND_ORDERING, snapshot)` call site. Drives a
7720    /// deterministic gap via `debug_skip_next_ordering_seq` so the test
7721    /// is not timing-sensitive.
7722    #[async_timed_test(timeout_secs = 30)]
7723    async fn test_inbound_ordering_snapshot_callback_publishes_session() {
7724        // Pin reorder buffering ON regardless of any global config
7725        // overrides set by other tests in the same binary. Without this
7726        // guard the snapshot would observe `enabled = false` and the
7727        // assertions below would fail when tests run in interleaved
7728        // order.
7729        let config = hyperactor_config::global::lock();
7730        let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
7731
7732        let proc = Proc::isolated();
7733        let client = proc.client("client");
7734        let handle = proc.spawn_with_label("a", BufferTestActor);
7735        let actor_id = handle.actor_addr().clone();
7736
7737        // Bind the handler ports so `handle.post` uses `SeqInfo::Session`
7738        // (not `SeqInfo::Direct`). This is the equivalent of asking
7739        // for a typed actor reference; for our purposes we only need
7740        // the side effect of binding handler ports.
7741        let _actor_ref: crate::ActorRef<BufferTestActor> = handle.bind();
7742
7743        // Reserve seq 1 on the actor's BufferTestMsg handler port so
7744        // subsequent client posts get seqs 2..=N which then buffer
7745        // (waiting for seq 1, which will never arrive).
7746        let handler_port = actor_id.port_addr(Port::handler::<BufferTestMsg>());
7747        // Reserve one seq directly on the client's sequencer. Equivalent
7748        // to `Instance::debug_skip_next_ordering_seq(dest, 1)`, but
7749        // inlined here so the test does not depend on a Client-side
7750        // convenience method.
7751        let _ = client.sequencer().assign_seq(&handler_port);
7752
7753        // Post three messages. These flow through MailboxExt::post ->
7754        // HandlerPorts enqueue closure -> SequencedReceiver, where they
7755        // buffer (out of order from seq 1's perspective).
7756        for _ in 0..3 {
7757            handle.post(&client, BufferTestMsg);
7758        }
7759        // Direct accessor: cell.inbound_ordering_snapshot() invokes the
7760        // type-erased callback installed at Instance::new. Sequencing is
7761        // receiver-local, so wait until the actor loop has polled the
7762        // work receiver and populated the buffered session.
7763        let cell = proc.get_instance(&actor_id).expect("actor exists");
7764        let snapshot = tokio::time::timeout(Duration::from_secs(5), async {
7765            loop {
7766                let snapshot = cell
7767                    .inbound_ordering_snapshot()
7768                    .expect("snapshot callback should be installed for live actors");
7769                if snapshot
7770                    .sessions
7771                    .first()
7772                    .is_some_and(|session| session.buffered_count == 3)
7773                {
7774                    break snapshot;
7775                }
7776                tokio::task::yield_now().await;
7777            }
7778        })
7779        .await
7780        .expect("receiver-local sequencing state should be populated");
7781
7782        assert!(snapshot.enabled, "buffering enabled by override");
7783        assert_eq!(snapshot.sessions.len(), 1, "one client session expected");
7784        let session = &snapshot.sessions[0];
7785        assert_eq!(session.expected_next_seq, 1);
7786        assert_eq!(session.buffered_count, 3);
7787        assert_eq!(session.oldest_buffered_seq, Some(2));
7788        assert_eq!(session.newest_buffered_seq, Some(4));
7789        assert_eq!(
7790            session.sender.as_ref(),
7791            Some(client.mailbox().actor_addr()),
7792            "session owner should be the posting client",
7793        );
7794
7795        // Attrs-publish wiring: build_actor_attrs (via live_actor_payload)
7796        // must call attrs.set(INBOUND_ORDERING, snapshot). Round-trip
7797        // through ActorAttrsView and assert the same session.
7798        let payload = crate::introspect::live_actor_payload(&cell);
7799        let attrs: hyperactor_config::Attrs =
7800            serde_json::from_str(&payload.attrs).expect("payload.attrs is well-formed JSON");
7801        let view = crate::introspect::ActorAttrsView::from_attrs(&attrs)
7802            .expect("attrs decode through ActorAttrsView");
7803        let view_snapshot = view
7804            .inbound_ordering
7805            .as_ref()
7806            .expect("INBOUND_ORDERING attr should be set by build_actor_attrs");
7807        assert_eq!(view_snapshot, &snapshot);
7808
7809        // queue_depth scalar is propagated (no arithmetic relation to
7810        // buffered_count asserted; IO-3 in introspect module doc).
7811        let _: u64 = cell.queue_depth();
7812        let _: u64 = view.queue_depth;
7813    }
7814
7815    // PD-4/PD-5: proc-level queue pressure aggregation reports
7816    // non-zero under induced load. Queue depth is an instantaneous
7817    // snapshot of currently queued work, not backlog history.
7818    #[async_timed_test(timeout_secs = 10)]
7819    async fn test_proc_queue_depth_aggregation_under_pressure() {
7820        let proc = Proc::isolated();
7821        let client = proc.client("client");
7822
7823        // Spawn two actors.
7824        let h1 = proc.spawn_with_label("a1", TestActor);
7825        let h2 = proc.spawn_with_label("a2", TestActor);
7826
7827        // Block both actors with a Wait message.
7828        let (reply1, rx1) = oneshot::channel();
7829        let (gate1, grx1) = oneshot::channel::<()>();
7830        h1.wait(&client, reply1, grx1).await.unwrap();
7831        rx1.await.unwrap();
7832
7833        let (reply2, rx2) = oneshot::channel();
7834        let (gate2, grx2) = oneshot::channel::<()>();
7835        h2.wait(&client, reply2, grx2).await.unwrap();
7836        rx2.await.unwrap();
7837
7838        // Queue additional messages while actors are blocked.
7839        h1.noop(&client).await.unwrap();
7840        h1.noop(&client).await.unwrap();
7841        h2.noop(&client).await.unwrap();
7842
7843        // Poll until aggregated queue depth reaches the expected
7844        // level, with a bounded timeout to avoid flakes.
7845        let aggregate = || -> (u64, u64) {
7846            let mut total: u64 = 0;
7847            let mut max: u64 = 0;
7848            for actor_id in proc.all_instance_keys() {
7849                if let Some(cell) = proc.get_instance_by_id(&actor_id) {
7850                    let depth = cell.queue_depth();
7851                    total = total.saturating_add(depth);
7852                    max = max.max(depth);
7853                }
7854            }
7855            (total, max)
7856        };
7857
7858        // Same aggregation logic used by
7859        // ProcAgent::publish_introspect_properties.
7860        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7861        loop {
7862            let (total, max) = aggregate();
7863            if total >= 3 {
7864                assert!(max >= 1, "expected max >= 1, got {max}");
7865                assert!(max <= total, "PD-1: max ({max}) <= total ({total})");
7866                break;
7867            }
7868            assert!(
7869                tokio::time::Instant::now() < deadline,
7870                "timed out waiting for queue depth >= 3, got {total}",
7871            );
7872            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7873        }
7874
7875        // Unblock both actors.
7876        let _ = gate1.send(());
7877        let _ = gate2.send(());
7878
7879        // Poll until aggregated depth returns to 0.
7880        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7881        loop {
7882            let (total, _) = aggregate();
7883            if total == 0 {
7884                break;
7885            }
7886            assert!(
7887                tokio::time::Instant::now() < deadline,
7888                "timed out waiting for queue depth to return to 0, got {total}",
7889            );
7890            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7891        }
7892    }
7893
7894    // ── PD-6 through PD-9: retained queue-pressure evidence ───
7895
7896    // PD-7: cold start — no queue traffic means last-nonzero is None
7897    // and watermark is 0.
7898    #[async_timed_test(timeout_secs = 5)]
7899    async fn test_retained_queue_stats_cold_start() {
7900        let proc = Proc::isolated();
7901        assert_eq!(proc.queue_depth_total(), 0);
7902        assert_eq!(proc.queue_depth_high_water_mark(), 0);
7903        assert_eq!(proc.last_nonzero_queue_depth_age_ms(), None);
7904    }
7905
7906    // PD-6/PD-8: after induced pressure drains, high-water mark
7907    // retains the peak and last-nonzero is Some.
7908    #[async_timed_test(timeout_secs = 10)]
7909    async fn test_retained_queue_stats_burst_then_drain() {
7910        let proc = Proc::isolated();
7911        let client = proc.client("client");
7912        let h = proc.spawn_with_label("ret_test", TestActor);
7913
7914        // Block the actor.
7915        let (ready_tx, ready_rx) = oneshot::channel();
7916        let (gate_tx, gate_rx) = oneshot::channel::<()>();
7917        h.wait(&client, ready_tx, gate_rx).await.unwrap();
7918        ready_rx.await.unwrap();
7919
7920        // Queue work behind it.
7921        h.noop(&client).await.unwrap();
7922        h.noop(&client).await.unwrap();
7923
7924        // Poll until watermark is updated.
7925        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7926        loop {
7927            let hwm = proc.queue_depth_high_water_mark();
7928            if hwm >= 2 {
7929                // PD-6: watermark >= current total.
7930                assert!(hwm >= proc.queue_depth_total());
7931                // Active pressure: last-nonzero should be near zero.
7932                let age = proc.last_nonzero_queue_depth_age_ms();
7933                assert!(
7934                    age.is_some(),
7935                    "last-nonzero should be Some while pressure is active"
7936                );
7937                assert!(age.unwrap() < 2000, "last-nonzero age should be near zero");
7938                break;
7939            }
7940            assert!(
7941                tokio::time::Instant::now() < deadline,
7942                "timed out waiting for watermark >= 2",
7943            );
7944            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7945        }
7946
7947        // Unblock and drain.
7948        let _ = gate_tx.send(());
7949        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7950        loop {
7951            if proc.queue_depth_total() == 0 {
7952                break;
7953            }
7954            assert!(
7955                tokio::time::Instant::now() < deadline,
7956                "timed out waiting for total to drain",
7957            );
7958            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7959        }
7960
7961        // PD-8: watermark retained after drain.
7962        assert!(
7963            proc.queue_depth_high_water_mark() >= 2,
7964            "watermark should retain the peak after drain",
7965        );
7966
7967        // PD-7: last-nonzero is Some (not None) after pressure.
7968        let age = proc.last_nonzero_queue_depth_age_ms();
7969        assert!(age.is_some(), "last-nonzero should be Some after pressure");
7970    }
7971
7972    // PD-7: deterministic test of dequeue-side timestamp refresh
7973    // using a fake clock. Proves "last observed non-zero" semantics
7974    // without timing-dependent sleeps.
7975    #[test]
7976    fn test_last_nonzero_refreshed_on_dequeue_deterministic() {
7977        use std::sync::atomic::AtomicU64;
7978
7979        static FAKE_NOW: AtomicU64 = AtomicU64::new(0);
7980        fn fake_clock() -> u64 {
7981            FAKE_NOW.load(Ordering::Relaxed)
7982        }
7983
7984        let stats = ProcQueueStats::with_clock(fake_clock);
7985        let depth = Arc::new(AtomicU64::new(0));
7986
7987        // Cold start: no activity.
7988        assert_eq!(stats.last_nonzero_age_ms(), None);
7989
7990        // t=1000: enqueue two items.
7991        FAKE_NOW.store(1000, Ordering::Relaxed);
7992        account_enqueue(&depth, &stats, "a");
7993        account_enqueue(&depth, &stats, "a");
7994        assert_eq!(stats.running_total(), 2);
7995        assert_eq!(stats.high_water_mark(), 2);
7996
7997        // t=2000: read age — should be 1000ms since last nonzero.
7998        FAKE_NOW.store(2000, Ordering::Relaxed);
7999        assert_eq!(stats.last_nonzero_age_ms(), Some(1000));
8000
8001        // t=3000: dequeue one item. Queue still non-zero (1 left).
8002        // This should refresh the timestamp to 3000.
8003        FAKE_NOW.store(3000, Ordering::Relaxed);
8004        account_dequeue(&depth, &stats, "a");
8005        assert_eq!(stats.running_total(), 1);
8006
8007        // t=4000: read age — should be 1000ms (4000 - 3000), not
8008        // 3000ms (4000 - 1000). This proves the dequeue refreshed
8009        // the timestamp.
8010        FAKE_NOW.store(4000, Ordering::Relaxed);
8011        assert_eq!(stats.last_nonzero_age_ms(), Some(1000));
8012
8013        // t=5000: dequeue last item. Queue is now zero.
8014        // prev_total was 1, so prev_total > 1 is false — timestamp
8015        // is NOT refreshed. It stays at 3000.
8016        FAKE_NOW.store(5000, Ordering::Relaxed);
8017        account_dequeue(&depth, &stats, "a");
8018        assert_eq!(stats.running_total(), 0);
8019
8020        // t=6000: age should be 3000ms (6000 - 3000).
8021        FAKE_NOW.store(6000, Ordering::Relaxed);
8022        assert_eq!(stats.last_nonzero_age_ms(), Some(3000));
8023
8024        // Watermark retained.
8025        assert_eq!(stats.high_water_mark(), 2);
8026    }
8027
8028    // account_cancel_enqueue must symmetrically reverse
8029    // account_enqueue on queue_depth and running_total so that a
8030    // send failure after accounting cannot leave the proc-wide
8031    // counter at u64::MAX (which would panic the next enqueue via
8032    // the `fetch_add(1) + 1` path).
8033    #[test]
8034    fn test_account_cancel_enqueue_restores_counters() {
8035        let stats = ProcQueueStats::new();
8036        let depth = Arc::new(AtomicU64::new(0));
8037
8038        account_enqueue(&depth, &stats, "a");
8039        assert_eq!(stats.running_total(), 1);
8040        assert_eq!(depth.load(Ordering::Relaxed), 1);
8041
8042        account_cancel_enqueue(&depth, &stats, "a");
8043        assert_eq!(
8044            stats.running_total(),
8045            0,
8046            "cancel must restore running_total"
8047        );
8048        assert_eq!(
8049            depth.load(Ordering::Relaxed),
8050            0,
8051            "cancel must restore queue_depth"
8052        );
8053
8054        // high_water_mark is monotonic by design; cancel does not reset it.
8055        assert_eq!(stats.high_water_mark(), 1);
8056
8057        // A subsequent enqueue must not observe underflow: fetch_add(1) + 1
8058        // would panic in debug builds if running_total had wrapped to u64::MAX.
8059        account_enqueue(&depth, &stats, "a");
8060        assert_eq!(stats.running_total(), 1);
8061    }
8062
8063    #[test]
8064    fn child_teardown_distinguishes_kill_from_failure() {
8065        let actor_addr = test_actor_id("proc", "actor");
8066
8067        let stopped = Ok(ActorStopped {
8068            reason: "test".to_string(),
8069            stop_mode: StopMode::DrainAndStop,
8070        });
8071        assert_eq!(
8072            ChildTeardown::from_run_result(&stopped),
8073            ChildTeardown::Cooperative(StopMode::DrainAndStop)
8074        );
8075
8076        let killed = Err(ActorError::new(
8077            &actor_addr,
8078            ActorErrorKind::Aborted("test kill".to_string()),
8079        ));
8080        assert_eq!(ChildTeardown::from_run_result(&killed), ChildTeardown::Kill);
8081
8082        let failed = Err(ActorError::new(
8083            &actor_addr,
8084            ActorErrorKind::Generic("test failure".to_string()),
8085        ));
8086        assert_eq!(
8087            ChildTeardown::from_run_result(&failed),
8088            ChildTeardown::Cooperative(StopMode::Stop)
8089        );
8090    }
8091}