Skip to main content

hyperactor_mesh/
global_context.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//! Process-global context, root client actor, and supervision bridge.
10//!
11//! This module provides the Rust equivalent of Python's `context()`,
12//! `this_host()`, and `this_proc()`. A singleton [`Host`] is lazily
13//! created with the [`GlobalClientActor`] on its `local_proc`:
14//!
15//! ```rust,ignore
16//! let cx = context().await;
17//! cx.actor_instance    // c.f. Python: context().actor_instance
18//! this_host().await    // c.f. Python: this_host()
19//! this_proc().await    // c.f Python: this_proc()
20//! ```
21//!
22//! ## Undeliverables → supervision
23//!
24//! When the runtime detects that a message cannot be delivered, it
25//! produces an [`Undeliverable<MessageEnvelope>`]. The global root
26//! client observes these failures, converts them into
27//! [`ActorSupervisionEvent`]s, and forwards them to the currently
28//! active mesh supervision sink.
29//!
30//! **GC-1 (undeliverable routing):** Any
31//! `Undeliverable<MessageEnvelope>` observed by the global root
32//! client must be reported as an [`ActorSupervisionEvent`] to the
33//! active `ProcMesh`, and handling that failure must never crash the
34//! global client. The root client acts as a monitor, not a
35//! participant: routing failures are treated as signals to be
36//! reported, not fatal errors.
37//!
38//! ## Multiple ProcMeshes
39//!
40//! A process may allocate more than one `ProcMesh` (e.g.
41//! internal/controller meshes plus an application mesh). The root
42//! client is a process-wide singleton, so its supervision sink is
43//! also process-global.
44//!
45//! The active mesh is defined using **last-sink-wins** semantics:
46//! each newly allocated `ProcMesh` installs its sink, overriding the
47//! previous one.
48//!
49//! If no sink has been installed yet (early/late binding),
50//! undeliverables are logged and dropped, preserving forward progress
51//! until a mesh becomes available.
52
53use std::sync::OnceLock;
54use std::sync::RwLock;
55
56use async_trait::async_trait;
57use hyperactor::Actor;
58use hyperactor::ActorHandle;
59use hyperactor::Context;
60use hyperactor::Endpoint as _;
61use hyperactor::Handler;
62use hyperactor::Instance;
63use hyperactor::PortRef;
64use hyperactor::actor::ActorError;
65use hyperactor::actor::ActorErrorKind;
66use hyperactor::actor::ActorStatus;
67use hyperactor::actor::Signal;
68use hyperactor::id::Label;
69use hyperactor::id::Uid;
70use hyperactor::mailbox::DeliveryFailure;
71use hyperactor::mailbox::MessageEnvelope;
72use hyperactor::mailbox::TransportFailure;
73use hyperactor::mailbox::TransportFailureReason;
74use hyperactor::mailbox::Undeliverable;
75use hyperactor::mailbox::UndeliverableReason;
76use hyperactor::proc::ActorWorkReceiver;
77use hyperactor::proc::Proc;
78use hyperactor::supervision::ActorSupervisionEvent;
79use hyperactor_cast::cast_actor::CAST_ACTOR_NAME;
80use tokio::sync::mpsc;
81use tokio::task::JoinHandle;
82
83use crate::HostMeshRef;
84use crate::host::Host;
85use crate::host::LocalProcManager;
86use crate::host_mesh::host_agent::GetLocalProcClient;
87use crate::host_mesh::host_agent::HOST_MESH_AGENT_ACTOR_NAME;
88use crate::host_mesh::host_agent::HostAgent;
89use crate::host_mesh::host_agent::ProcManagerSpawnFn;
90use crate::mesh_id::HostMeshId;
91use crate::mesh_id::ProcMeshId;
92use crate::proc_agent::GetProcClient;
93use crate::proc_agent::ProcAgent;
94use crate::proc_mesh::ProcMeshRef;
95use crate::proc_mesh::ProcRef;
96use crate::supervision::MeshFailure;
97use crate::transport::default_bind_spec;
98
99/// Single, process-wide supervision sink storage.
100///
101/// Routes undeliverables observed by the process-global root client
102/// (c.f. [`context()`]) to the *currently active* `ProcMesh`'s
103/// agent. Newer meshes override older ones ("last sink wins").
104///
105/// Uses `PortRef` (not `PortHandle`) because the sink target
106/// (`ProcAgent`) runs in a remote worker process.
107static GLOBAL_SUPERVISION_SINK: OnceLock<RwLock<Option<PortRef<ActorSupervisionEvent>>>> =
108    OnceLock::new();
109
110/// Returns the lazily-initialized container that holds the current
111/// process-global supervision sink.
112fn sink_cell() -> &'static RwLock<Option<PortRef<ActorSupervisionEvent>>> {
113    GLOBAL_SUPERVISION_SINK.get_or_init(|| RwLock::new(None))
114}
115
116/// Install (or replace) the process-global supervision sink used by
117/// the [`context()`] undeliverable → supervision bridge.
118///
119/// This uses **last-sink-wins** semantics: if multiple `ProcMesh`
120/// instances are created in the same process (e.g. controller meshes
121/// plus an application mesh), the most recently installed sink
122/// becomes the active destination for forwarded
123/// [`ActorSupervisionEvent`]s.
124///
125/// Returns the previously installed sink, if any, to allow callers to
126/// log/inspect overrides.
127///
128/// Note: the sink is a [`PortRef`] (not a `PortHandle`) because the
129/// destination [`ProcAgent`] may live in a different
130/// process/rank.
131pub(crate) fn set_global_supervision_sink(
132    sink: PortRef<ActorSupervisionEvent>,
133) -> Option<PortRef<ActorSupervisionEvent>> {
134    let cell = sink_cell();
135    let mut guard = cell.write().unwrap();
136    let prev = guard.take();
137    *guard = Some(sink);
138    prev
139}
140
141/// Get the current process-global supervision sink used by the
142/// [`context()`] undeliverable → supervision bridge.
143///
144/// Returns `None` until some mesh creation path installs a sink
145/// (early/late binding). Callers should treat this as "no active mesh
146/// yet": log and drop undeliverables rather than crashing the global
147/// root client.
148///
149/// Cloning a [`PortRef`] is cheap.
150///
151/// Used only by the process-global root client.
152fn get_global_supervision_sink() -> Option<PortRef<ActorSupervisionEvent>> {
153    sink_cell().read().unwrap().clone()
154}
155
156/// Process-global "root client" actor.
157///
158/// This actor lives on the `local_proc` of the singleton [`Host`]
159/// created by [`context()`], symmetric with Python's
160/// `RootClientActor` on `bootstrap_host()`'s local proc.
161///
162/// It acts as a *monitor* for routing failures observed at the
163/// process boundary: undeliverable messages are treated as signals to
164/// be reported via mesh supervision (when a sink is installed), not
165/// as fatal errors.
166///
167/// The actor is driven by `run()`, which `select!`s over:
168/// - `work_rx`: the primary dispatch queue for bound handler work
169///   items (including `Undeliverable<MessageEnvelope>` and
170///   `MeshFailure>`),
171/// - `supervision_rx`: supervision events delivered to this actor,
172///   and
173/// - `signal_rx`: control signals (currently minimal handling).
174#[derive(Debug)]
175#[hyperactor::export(handlers = [MeshFailure])]
176pub struct GlobalClientActor {
177    /// Control signals for the actor's proc (shutdown, etc.).
178    signal_rx: mpsc::UnboundedReceiver<Signal>,
179    /// Supervision events delivered to this actor instance.
180    ///
181    /// The root client is a monitor, so it should process these
182    /// events without crashing on routine routing/delivery failures
183    /// it observes.
184    supervision_rx: mpsc::UnboundedReceiver<ActorSupervisionEvent>,
185    /// Primary work queue for handler dispatch.
186    ///
187    /// Any bound handler message (e.g. `MeshFailure`,
188    /// `Undeliverable<MessageEnvelope>`, introspection, etc.) is
189    /// received here and executed via `WorkCell::handle`.
190    work_rx: ActorWorkReceiver<Self>,
191}
192
193impl GlobalClientActor {
194    fn run(mut self, instance: &'static Instance<Self>) -> JoinHandle<()> {
195        tokio::spawn(async move {
196            #[allow(unused_labels)]
197            let err = 'messages: loop {
198                tokio::select! {
199                    work = self.work_rx.recv() => {
200                        let work = work.expect("inconsistent work queue state");
201                        if let Err(err) = work.handle(&mut self, instance).await {
202                            while let Ok(supervision_event) = self.supervision_rx.try_recv() {
203                                instance.handle_supervision_event(&mut self, supervision_event).await
204                                    .expect("GlobalClientActor::handle_supervision_event is infallible");
205                            }
206                            let kind = ActorErrorKind::processing(err);
207                            break ActorError {
208                                actor_id: Box::new(instance.self_addr().clone()),
209                                kind: Box::new(kind),
210                            };
211                        }
212                    }
213                    Some(_) = self.signal_rx.recv() => {
214                        // TODO: do we need any signal handling for the root client?
215                    }
216                    Some(supervision_event) = self.supervision_rx.recv() => {
217                        instance.handle_supervision_event(&mut self, supervision_event).await
218                            .expect("GlobalClientActor::handle_supervision_event is infallible");
219                    }
220                };
221            };
222            let event = match *err.kind {
223                ActorErrorKind::UnhandledSupervisionEvent(event) => *event,
224                _ => {
225                    let status = ActorStatus::generic_failure(err.kind.to_string());
226                    ActorSupervisionEvent::new(
227                        instance.self_addr().clone(),
228                        Some("testclient".into()),
229                        status,
230                        None,
231                    )
232                }
233            };
234            instance
235                .proc()
236                .handle_unhandled_supervision_event(instance, event);
237        })
238    }
239
240    async fn report_delivery_failure(
241        &mut self,
242        cx: &Instance<Self>,
243        undeliverable: Undeliverable<MessageEnvelope>,
244    ) -> Result<(), anyhow::Error> {
245        let mut env = match undeliverable {
246            Undeliverable::Returned(env) => env,
247            Undeliverable::Report(report) => {
248                let actor_ref = report.dest.actor_addr();
249                let error = report.error_msg().unwrap_or_default();
250                let event = ActorSupervisionEvent::new(
251                    actor_ref.clone(),
252                    None,
253                    ActorStatus::generic_failure(format!(
254                        "message not delivered to {}: {}",
255                        report.dest, error
256                    )),
257                    None,
258                );
259                match get_global_supervision_sink() {
260                    Some(sink) => {
261                        sink.post(cx, event);
262                    }
263                    None => {
264                        tracing::warn!(
265                            actor=%actor_ref,
266                            error=%error,
267                            "no supervision sink; delivery failure report logged but not forwarded"
268                        );
269                    }
270                }
271                return Ok(());
272            }
273        };
274        env.push_delivery_failure(DeliveryFailure::new(UndeliverableReason::Transport(
275            TransportFailure::new(
276                env.dest().clone(),
277                TransportFailureReason::LinkUnavailable(
278                    "message returned to global root client".to_string(),
279                ),
280            ),
281        )));
282        let actor_ref = env.dest().actor_addr();
283        let headers = env.headers().clone();
284        let event = ActorSupervisionEvent::new(
285            actor_ref.clone(),
286            None,
287            ActorStatus::generic_failure(format!("message not delivered: {}", env)),
288            Some(headers),
289        );
290
291        match get_global_supervision_sink() {
292            Some(sink) => {
293                sink.post(cx, event);
294            }
295            None => {
296                tracing::warn!(
297                    actor=%actor_ref,
298                    error=%env.error_msg().unwrap_or_default(),
299                    "no supervision sink; undeliverable message logged but not forwarded"
300                );
301            }
302        }
303        Ok(())
304    }
305}
306
307/// Handle a returned (undeliverable) message observed by the
308/// process-global root client.
309///
310/// The global root client is a **monitor**, not a participant: it
311/// must not crash or propagate failures just because a routed message
312/// could not be delivered.
313///
314/// Instead, we translate the undeliverable into an
315/// `ActorSupervisionEvent` and forward it to the **active**
316/// `ProcMesh` via the process-global supervision sink ("last sink
317/// wins"). If no sink has been installed yet (e.g., before the first
318/// `ProcMesh` allocation completes), we log and drop the event.
319#[async_trait]
320impl Actor for GlobalClientActor {
321    /// The global root client is the root of the supervision tree:
322    /// there is no parent to escalate to. Child-actor failures (e.g.
323    /// ActorMeshControllers detecting dead procs after mesh teardown)
324    /// are expected and must not crash the process.
325    async fn handle_supervision_event(
326        &mut self,
327        _this: &Instance<Self>,
328        event: &ActorSupervisionEvent,
329    ) -> Result<bool, anyhow::Error> {
330        tracing::warn!(
331            %event,
332            "global root client absorbed child supervision event",
333        );
334        Ok(true)
335    }
336
337    async fn handle_delivery_failure_event(
338        &mut self,
339        cx: &Instance<Self>,
340        undeliverable: Undeliverable<MessageEnvelope>,
341    ) -> Result<(), anyhow::Error> {
342        self.report_delivery_failure(cx, undeliverable).await
343    }
344
345    async fn handle_undeliverable_message(
346        &mut self,
347        cx: &Instance<Self>,
348        _reason: UndeliverableReason,
349        undeliverable: Undeliverable<MessageEnvelope>,
350    ) -> Result<(), anyhow::Error> {
351        self.report_delivery_failure(cx, undeliverable).await
352    }
353
354    async fn handle_invalid_reference(
355        &mut self,
356        cx: &Instance<Self>,
357        _invalid: hyperactor::mailbox::InvalidReference,
358        undeliverable: Undeliverable<MessageEnvelope>,
359    ) -> Result<(), anyhow::Error> {
360        self.report_delivery_failure(cx, undeliverable).await
361    }
362}
363
364/// `MeshFailure` is a terminal supervision signal for an `ActorMesh`.
365///
366/// The process-global root client should never be a consumer of
367/// mesh-level supervision failures during normal operation: those
368/// events are expected to be observed and handled by the owning
369/// mesh/controller, not by the global client.
370///
371/// In processes that create and destroy multiple meshes (e.g.,
372/// benchmarks), `MeshFailure` events may arrive here during or after
373/// mesh teardown. Log loudly but do not crash — the global client is
374/// a monitor and must preserve forward progress.
375#[async_trait]
376impl Handler<MeshFailure> for GlobalClientActor {
377    async fn handle(&mut self, _cx: &Context<Self>, message: MeshFailure) -> anyhow::Result<()> {
378        tracing::error!("supervision failure reached global client: {}", message);
379        Ok(())
380    }
381}
382
383struct GlobalState {
384    actor_instance: &'static Instance<GlobalClientActor>,
385    host_mesh: HostMeshRef,
386    proc_mesh: ProcMeshRef,
387}
388
389/// Process-global, lazily-initialized Monarch context.
390///
391/// Backed by a `tokio::sync::OnceCell` so initialization is async and
392/// runs at most once per process. The first caller bootstraps the
393/// singleton host and root client actor (mirroring Python's
394/// `bootstrap_host()` / `context()`), and subsequent callers reuse
395/// the same `GlobalState`.
396///
397/// This provides a stable root `actor_instance` plus `this_host()` /
398/// `this_proc()` accessors.
399static GLOBAL_CONTEXT: tokio::sync::OnceCell<GlobalState> = tokio::sync::OnceCell::const_new();
400
401/// Bootstrap the singleton Host and GlobalClientActor. Mirrors
402/// Python's `bootstrap_host()` (monarch_hyperactor/src/host_mesh.rs).
403async fn bootstrap_host() -> GlobalState {
404    // 1. Create Host with LocalProcManager. The spawn closure is the
405    // ProcAgent boot function, called by HostAgent on GetLocalProc.
406    let spawn: ProcManagerSpawnFn =
407        Box::new(|proc| Box::pin(std::future::ready(ProcAgent::boot_v1(proc, None))));
408    let manager: LocalProcManager<ProcManagerSpawnFn> = LocalProcManager::new(spawn);
409    let host = Host::new(manager, default_bind_spec().binding_addr())
410        .await
411        .expect("failed to create global host");
412
413    // 2. Extract system_proc before moving Host into HostAgent.
414    let system_proc = host.system_proc().clone();
415
416    // 3. Spawn HostAgent on system_proc (takes ownership of Host).
417    let host_agent = system_proc
418        .spawn_with_uid(
419            Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
420            HostAgent::new_local(host),
421        )
422        .expect("failed to spawn host agent");
423    HostAgent::wait_initialized(&host_agent)
424        .await
425        .expect("failed to initialize host agent");
426
427    let cast_handle = system_proc
428        .spawn_with_uid(
429            Uid::singleton(Label::strip(CAST_ACTOR_NAME)),
430            hyperactor_cast::cast_actor::CastActor::default(),
431        )
432        .expect("failed to spawn cast actor");
433
434    cast_handle.bind::<hyperactor_cast::cast_actor::CastActor>();
435
436    // 4. Build HostMeshRef.
437    let host_mesh = HostMeshRef::from_host_agent(
438        HostMeshId::singleton(Label::new("local").unwrap()),
439        host_agent.bind(),
440    )
441    .expect("failed to create host mesh ref");
442
443    // 5. Get local_proc via HostAgent (lazily boots ProcAgent).
444    //
445    // We use a throwaway Proc::isolated() for the bootstrap request-reply
446    // calls, matching Python's bootstrap_host() (host_mesh.rs:330-333).
447    // This creates a temporary in-process-only proc context during init
448    // — intentionally acceptable for cross-language symmetry and easier
449    // reasoning about the bootstrap sequence.
450    let temp_proc = Proc::isolated();
451    let bootstrap_cx = temp_proc.client("bootstrap");
452    let local_proc_agent: ActorHandle<ProcAgent> = host_agent
453        .get_local_proc(&bootstrap_cx)
454        .await
455        .expect("failed to get local proc agent");
456
457    // 6. Get the actual Proc object.
458    let local_proc = local_proc_agent
459        .get_proc(&bootstrap_cx)
460        .await
461        .expect("failed to get local proc");
462
463    // 7. Build ProcMeshRef.
464    let proc_mesh = ProcMeshRef::new_singleton(
465        ProcMeshId::singleton(Label::new("local").unwrap()),
466        ProcRef::new(
467            local_proc_agent.actor_addr().proc_addr(),
468            0,
469            local_proc_agent.bind(),
470        ),
471    )
472    .expect("failed to create proc mesh ref");
473    let actor_instance = local_proc
474        .actor_instance::<GlobalClientActor>("client")
475        .expect("failed to create root client instance");
476
477    let hyperactor::proc::ActorInstance {
478        instance: client_instance,
479        handle,
480        supervision,
481        signal,
482        work,
483    } = actor_instance;
484
485    // GlobalClientActor uses a custom run loop that bypasses
486    // Actor::init, so set_system() must be called explicitly.
487    client_instance.set_system();
488    handle.bind::<GlobalClientActor>();
489
490    // Use a static OnceLock to get 'static lifetime for the instance.
491    static INSTANCE: OnceLock<(Instance<GlobalClientActor>, ActorHandle<GlobalClientActor>)> =
492        OnceLock::new();
493    INSTANCE
494        .set((client_instance, handle))
495        .map_err(|_| "already initialized root client instance")
496        .unwrap();
497    let (instance, _handle) = INSTANCE.get().unwrap();
498
499    let client = GlobalClientActor {
500        signal_rx: signal,
501        supervision_rx: supervision,
502        work_rx: work,
503    };
504    client.run(instance);
505
506    GlobalState {
507        actor_instance: instance,
508        host_mesh,
509        proc_mesh,
510    }
511}
512
513/// Process-global Monarch context for Rust programs. Symmetric with
514/// Python's `context()`.
515pub struct GlobalContext {
516    /// Consistent with Python's `context().actor_instance`
517    pub actor_instance: &'static Instance<GlobalClientActor>,
518    /// The singleton HostMesh. See also [`this_host()`].
519    pub host_mesh: &'static HostMeshRef,
520    /// The local ProcMesh. See also [`this_proc()`].
521    pub proc_mesh: &'static ProcMeshRef,
522}
523
524/// Returns the process-global Monarch context, lazily initialized.
525///
526/// On first call, creates a singleton [`Host`] and bootstraps
527/// [`GlobalClientActor`] on its `local_proc` — symmetric with
528/// Python's `bootstrap_host()`. Subsequent calls return immediately.
529///
530/// ```rust,ignore
531/// let cx = context().await;
532/// cx.actor_instance    // c.f. Python: context().actor_instance
533/// ```
534///
535/// **Python programs do not use this.** Python has its own root
536/// client actor bootstrapped separately.
537pub async fn context() -> GlobalContext {
538    let state = GLOBAL_CONTEXT.get_or_init(bootstrap_host).await;
539    GlobalContext {
540        actor_instance: state.actor_instance,
541        host_mesh: &state.host_mesh,
542        proc_mesh: &state.proc_mesh,
543    }
544}
545
546/// Returns the singleton HostMesh c.f. Python's `this_host()`.
547pub async fn this_host() -> &'static HostMeshRef {
548    &GLOBAL_CONTEXT.get_or_init(bootstrap_host).await.host_mesh
549}
550
551/// Returns the local ProcMesh c.f Python's `this_proc()`.
552pub async fn this_proc() -> &'static ProcMeshRef {
553    &GLOBAL_CONTEXT.get_or_init(bootstrap_host).await.proc_mesh
554}
555
556/// Separate storage for client host registered by non-Rust runtimes
557/// (e.g. Python's `bootstrap_host()`). Checked by `try_this_host()`
558/// alongside `GLOBAL_CONTEXT`.
559static REGISTERED_CLIENT_HOST: std::sync::OnceLock<HostMeshRef> = std::sync::OnceLock::new();
560
561/// Register the client host mesh from an external runtime (Python).
562/// Called by Python's `bootstrap_host()` so that `try_this_host()`
563/// can discover C for the A/C invariant.
564pub fn register_client_host(host_mesh: HostMeshRef) {
565    let _ = REGISTERED_CLIENT_HOST.set(host_mesh);
566}
567
568/// Returns the client host mesh if available, without triggering
569/// lazy bootstrap. Checks both the Rust global context and the
570/// external registration (Python). Used by `MeshAdminAgent` to
571/// discover C at query time (A/C invariant).
572pub fn try_this_host() -> Option<&'static HostMeshRef> {
573    GLOBAL_CONTEXT
574        .get()
575        .map(|state| &state.host_mesh)
576        .or_else(|| REGISTERED_CLIENT_HOST.get())
577}
578
579#[cfg(test)]
580mod tests {
581    use std::time::Duration;
582
583    use hyperactor::testing::ids::test_actor_id;
584    use hyperactor_config::Flattrs;
585    #[cfg(fbcode_build)]
586    use ndslice::view::Extent;
587    #[cfg(fbcode_build)]
588    use timed_test::async_timed_test;
589
590    use super::*;
591    #[cfg(fbcode_build)]
592    use crate::testing;
593
594    /// Helper: send an `Undeliverable<MessageEnvelope>` to the global
595    /// root client's well-known undeliverable port via the runtime's
596    /// routing/dispatch path.
597    ///
598    /// This exercises the full integration boundary: serialisation →
599    /// routing → work_rx dispatch → `handle_undeliverable_message`.
600    ///
601    /// Uses the provided `dest_actor` so callers can distinguish
602    /// events from different injections (important because the global
603    /// sink is shared across tests running in the same process).
604    fn inject_undeliverable(
605        client: &'static Instance<GlobalClientActor>,
606        dest_actor: hyperactor::ActorAddr,
607    ) {
608        let env = MessageEnvelope::new(
609            client.self_addr().clone(),
610            dest_actor.port_addr(0.into()),
611            wirevalue::Any::serialize(&0u64).unwrap(),
612            Flattrs::new(),
613        );
614        // Target the global root client's well-known Undeliverable port.
615        let client_actor_id: hyperactor::ActorAddr = client.self_addr().clone();
616        let undeliverable_port =
617            PortRef::<Undeliverable<MessageEnvelope>>::attest_handler_port(&client_actor_id);
618        undeliverable_port.post(client, Undeliverable::Returned(env));
619    }
620
621    /// Verifies that creating a `ProcMesh` installs the
622    /// process-global supervision sink used by the global root
623    /// client.
624    #[async_timed_test(timeout_secs = 30)]
625    #[cfg(fbcode_build)]
626    async fn test_sink_installed_after_mesh_creation() {
627        let instance = testing::instance();
628        let mut hm = testing::host_mesh(2).await;
629        let _mesh = hm
630            .spawn(instance, "test", Extent::unity(), None, None)
631            .await
632            .unwrap();
633        assert!(
634            get_global_supervision_sink().is_some(),
635            "supervision sink must be set after ProcMesh creation"
636        );
637        let _ = hm.shutdown(instance).await;
638    }
639
640    /// Proves the full forwarding pipeline:
641    ///
642    ///   Undeliverable<MessageEnvelope>
643    ///       → GlobalClientActor::handle_undeliverable_message
644    ///       → GLOBAL_SUPERVISION_SINK (PortRef)
645    ///       → ActorSupervisionEvent delivered
646    ///
647    /// Installs a local port as the sink and verifies that the
648    /// `ActorSupervisionEvent` arrives there.
649    #[tokio::test]
650    async fn test_undeliverable_forwarded_to_sink() {
651        let cx = context().await;
652        let client = cx.actor_instance;
653
654        // Install a test sink we control.
655        let (sink_handle, mut sink_rx) = client.open_port::<ActorSupervisionEvent>();
656        set_global_supervision_sink(sink_handle.bind());
657
658        let marker = test_actor_id("fwd_test", "marker_actor");
659        inject_undeliverable(client, marker.clone());
660
661        // The handler runs asynchronously via work_rx; wait for the
662        // forwarded event with our marker.
663        let event = tokio::time::timeout(Duration::from_secs(5), async {
664            loop {
665                let ev = sink_rx.recv().await.expect("sink channel closed");
666                if ev.actor_id == marker {
667                    return ev;
668                }
669                // Discard stale events from other tests sharing the
670                // global sink.
671            }
672        })
673        .await
674        .expect("timed out waiting for supervision event");
675
676        assert_eq!(
677            event.actor_id, marker,
678            "forwarded event must reference the undeliverable's destination actor"
679        );
680    }
681
682    /// Proves last-sink-wins: when two sinks are installed in
683    /// sequence, only the second receives the forwarded event.
684    #[tokio::test]
685    async fn test_last_sink_wins() {
686        let cx = context().await;
687        let client = cx.actor_instance;
688
689        // Install sink A.
690        let (sink_a_handle, _sink_a_rx) = client.open_port::<ActorSupervisionEvent>();
691        set_global_supervision_sink(sink_a_handle.bind());
692
693        // Install sink B (overrides A).
694        let (sink_b_handle, mut sink_b_rx) = client.open_port::<ActorSupervisionEvent>();
695        set_global_supervision_sink(sink_b_handle.bind());
696
697        let marker = test_actor_id("last_wins", "marker_actor");
698        inject_undeliverable(client, marker.clone());
699
700        // B should receive our marked event.
701        let event = tokio::time::timeout(Duration::from_secs(5), async {
702            loop {
703                let ev = sink_b_rx.recv().await.expect("sink B channel closed");
704                if ev.actor_id == marker {
705                    return ev;
706                }
707            }
708        })
709        .await
710        .expect("timed out waiting for supervision event on sink B");
711        assert_eq!(event.actor_id, marker);
712    }
713
714    /// Proves the global client does not crash when no sink is
715    /// installed (early/late binding). The handler must log and
716    /// drop gracefully, and the client must remain usable
717    /// afterward.
718    #[tokio::test]
719    async fn test_no_crash_without_sink() {
720        let cx = context().await;
721        let client = cx.actor_instance;
722
723        // Clear any previously installed sink.
724        *sink_cell().write().unwrap() = None;
725
726        // Inject an undeliverable — should not panic.
727        inject_undeliverable(client, test_actor_id("no_sink", "marker_actor"));
728
729        // Give the async handler time to run.
730        tokio::time::sleep(Duration::from_millis(100)).await;
731
732        // The global client must still be alive and usable.
733        // Verify by installing a new sink and sending another
734        // undeliverable that arrives correctly.
735        let (sink_handle, mut sink_rx) = client.open_port::<ActorSupervisionEvent>();
736        set_global_supervision_sink(sink_handle.bind());
737
738        let marker = test_actor_id("no_sink_recovery", "marker_actor");
739        inject_undeliverable(client, marker.clone());
740
741        let event = tokio::time::timeout(Duration::from_secs(5), async {
742            loop {
743                let ev = sink_rx.recv().await.expect("sink channel closed");
744                if ev.actor_id == marker {
745                    return ev;
746                }
747            }
748        })
749        .await
750        .expect("timed out: global client crashed or stopped processing");
751        assert_eq!(event.actor_id, marker);
752    }
753}