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