1use std::collections::HashMap;
24use std::fmt;
25use std::hash::Hash;
26use std::hash::Hasher;
27use std::ops::Deref;
28use std::sync::Arc;
29use std::sync::OnceLock as OnceCell;
30use std::time::Duration;
31
32use hyperactor::ActorAddr;
33use hyperactor::ActorLocal;
34use hyperactor::ActorRef;
35use hyperactor::AnyActorHandle;
36use hyperactor::Endpoint as _;
37use hyperactor::PortRef;
38use hyperactor::RemoteEndpoint as _;
39use hyperactor::RemoteHandles;
40use hyperactor::RemoteMessage;
41use hyperactor::actor::ActorStatus;
42use hyperactor::actor::Referable;
43use hyperactor::context;
44use hyperactor::mailbox::PortReceiver;
45use hyperactor::supervision::ActorSupervisionEvent;
46use hyperactor_cast::TilingPolicy;
47use hyperactor_cast::cast_actor::CastActor;
48use hyperactor_cast::cast_actor::CastDestination;
49use hyperactor_cast::cast_actor::CastDomainId;
50use hyperactor_cast::cast_actor::CastDomainRef;
51use hyperactor_cast::cast_actor::CastNode;
52use hyperactor_config::CONFIG;
53use hyperactor_config::ConfigAttr;
54use hyperactor_config::Flattrs;
55use hyperactor_config::attrs::declare_attrs;
56use ndslice::ViewExt as _;
57use ndslice::view;
58use ndslice::view::Ranked;
59use ndslice::view::Region;
60use serde::Deserialize;
61use serde::Deserializer;
62use serde::Serialize;
63use serde::Serializer;
64use tokio::sync::watch;
65
66use crate::Error;
67use crate::ProcMeshRef;
68use crate::ValueMesh;
69use crate::casting;
70use crate::config::MAX_CAST_FANOUT;
71use crate::host_mesh::GET_PROC_STATE_MAX_IDLE;
72use crate::host_mesh::mesh_to_rankedvalues_with_default;
73use crate::mesh_controller::ActorMeshController;
74use crate::mesh_controller::SUPERVISION_POLL_FREQUENCY;
75use crate::mesh_controller::Subscribe;
76use crate::mesh_controller::Unsubscribe;
77use crate::mesh_id::ActorMeshId;
78use crate::mesh_id::ProcMeshId;
79use crate::monitor::MeshMonitor;
80use crate::proc_mesh::GET_ACTOR_STATE_MAX_IDLE;
81use crate::proc_mesh::telemetry_actor_mesh_id;
82use crate::resource;
83use crate::supervision::MeshFailure;
84use crate::supervision::Unhealthy;
85
86declare_attrs! {
87 @meta(CONFIG = ConfigAttr::new(
96 Some("HYPERACTOR_MESH_SUPERVISION_WATCHDOG_TIMEOUT".to_string()),
97 Some("supervision_watchdog_timeout".to_string()),
98 ))
99 pub attr SUPERVISION_WATCHDOG_TIMEOUT: Duration = Duration::from_mins(2);
100}
101
102#[derive(Debug)]
113pub struct ActorMesh<A: Referable> {
114 current_ref: ActorMeshRef<A>,
117 lifecycle: ActorMeshLifecycle,
118}
119
120pub(crate) type ActorMeshStopHandle = AnyActorHandle;
122
123#[derive(Debug, Clone)]
124enum ActorMeshLifecycle {
125 Managed,
126 Data {
127 stop_handles: Vec<ActorMeshStopHandle>,
128 stop_requested: Arc<OnceCell<()>>,
129 },
130}
131
132pub struct DataActorMesh<A: Referable> {
141 id: ActorMeshId,
142 members: ValueMesh<ActorRef<A>>,
143 cast_domain: Arc<ActorMeshCastDomain>,
144 monitor: Arc<ActorLocal<Arc<MeshMonitor>>>,
145}
146
147impl<A: Referable> fmt::Debug for DataActorMesh<A> {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 f.debug_struct("DataActorMesh")
150 .field("id", &self.id)
151 .field("members", &self.members)
152 .field("cast_domain", &self.cast_domain)
153 .finish_non_exhaustive()
154 }
155}
156
157impl<A: Referable> ActorMesh<A> {
159 pub(crate) fn new(
160 proc_mesh: ProcMeshRef,
161 id: ActorMeshId,
162 controller: Option<ActorRef<ActorMeshController<A>>>,
163 members: Arc<ValueMesh<ActorAddr>>,
164 ) -> Self {
165 let region = proc_mesh.region().clone();
166 let current_ref = ActorMeshRef::new_managed(
167 id.clone(),
168 Some(proc_mesh.id().clone()),
169 region,
170 controller.clone(),
171 members,
172 );
173
174 Self {
175 current_ref,
176 lifecycle: ActorMeshLifecycle::Managed,
177 }
178 }
179
180 pub(crate) fn try_new_data(
182 id: ActorMeshId,
183 region: Region,
184 members: Vec<ActorRef<A>>,
185 stop_handles: Vec<ActorMeshStopHandle>,
186 ) -> crate::Result<Self> {
187 assert!(!stop_handles.is_empty(), "data actor mesh must own actors");
188 assert_eq!(
189 stop_handles.len(),
190 members.len(),
191 "data actor mesh must own one lifecycle handle per member"
192 );
193 let current_ref = ActorMeshRef::try_new_data(id, region, members).inspect_err(|_| {
194 stop_handles.iter().for_each(|handle| {
195 let _ = handle.stop("data mesh construction failed");
196 });
197 })?;
198 Ok(Self {
199 current_ref,
200 lifecycle: ActorMeshLifecycle::Data {
201 stop_handles,
202 stop_requested: Arc::new(OnceCell::new()),
203 },
204 })
205 }
206
207 pub(crate) fn set_controller(&mut self, controller: Option<ActorRef<ActorMeshController<A>>>) {
208 self.current_ref
209 .as_managed_mut()
210 .expect("data actor meshes do not use a controller")
211 .set_controller(controller);
212 }
213
214 pub async fn stop(&mut self, cx: &impl context::Actor, reason: String) -> crate::Result<()> {
219 if let ActorMeshLifecycle::Data {
220 stop_handles,
221 stop_requested,
222 } = &mut self.lifecycle
223 {
224 if stop_requested.set(()).is_err() {
225 return Ok(());
226 }
227 let stop_handles = std::mem::take(stop_handles);
228 assert!(
229 !stop_handles.is_empty(),
230 "live data actor mesh must own actors"
231 );
232 let mut stop_error = None;
233 for handle in &stop_handles {
234 if let Err(error) = handle.stop(&reason)
235 && stop_error.is_none()
236 {
237 stop_error = Some(error);
238 }
239 }
240 return match stop_error {
241 Some(error) => Err(Error::Other(anyhow::Error::new(error))),
242 None => Ok(()),
243 };
244 }
245 let (controller, id, num_ranks) = {
251 let managed = self
252 .current_ref
253 .as_managed_mut()
254 .expect("managed actor mesh has a managed ref");
255 (
256 managed.take_controller(),
257 managed.id().resource_id().clone(),
258 managed.region().num_ranks(),
259 )
260 };
261 if let Some(controller) = controller {
262 let result: crate::Result<()> = async {
270 controller.post(
271 cx,
272 resource::Stop {
273 id: id.clone(),
274 reason,
275 },
276 );
277 let (port, mut rx) = cx.mailbox().open_port();
287 controller.post(
288 cx,
289 resource::GetState::<resource::mesh::State<()>> {
290 id: id.clone(),
291 reply: port.bind(),
292 },
293 );
294 let statuses = rx.recv().await?;
295 let Some(state) = &statuses.state else {
296 return Err(Error::Other(anyhow::anyhow!(
297 "non-existent state in GetState reply from controller: {}",
298 controller.actor_addr()
299 )));
300 };
301 let all_terminating = state.statuses.values().all(|s| s.is_terminating());
308 if !all_terminating {
309 let legacy = mesh_to_rankedvalues_with_default(
310 &state.statuses,
311 resource::Status::NotExist,
312 resource::Status::is_not_exist,
313 num_ranks,
314 );
315 return Err(Error::ActorStopError { statuses: legacy });
316 }
317 Ok(())
318 }
319 .await;
320
321 let status = match &result {
327 Ok(()) => ActorStatus::Stopped("mesh stopped".to_string()),
328 Err(e) => ActorStatus::Stopped(format!("mesh stop failed: {e}")),
329 };
330 self.current_ref.record_stopped(cx, status);
331
332 result?;
333 }
334 Ok(())
335 }
336}
337
338declare_attrs! {
339 pub attr ACTOR_MESH_ID: ActorMeshId;
343}
344
345impl<A: Referable> fmt::Display for ActorMesh<A> {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 write!(f, "{}", self.current_ref)
348 }
349}
350
351impl<A: Referable> Deref for ActorMesh<A> {
352 type Target = ActorMeshRef<A>;
353
354 fn deref(&self) -> &Self::Target {
355 &self.current_ref
356 }
357}
358
359impl<A: Referable> Clone for ActorMesh<A> {
362 fn clone(&self) -> Self {
363 Self {
364 current_ref: self.current_ref.clone(),
365 lifecycle: self.lifecycle.clone(),
366 }
367 }
368}
369
370impl<A: Referable> Drop for ActorMesh<A> {
371 fn drop(&mut self) {
372 tracing::info!(
373 name = "ActorMeshStatus",
374 actor_name = %self.id(),
375 status = "Dropped",
376 );
377 }
378}
379
380impl<A: Referable> DataActorMesh<A> {
381 fn new_unchecked(id: ActorMeshId, members: ValueMesh<ActorRef<A>>) -> Self {
382 Self::with_cast_domain_config(
383 id,
384 members,
385 CastDomainId::new(),
386 default_cast_tiling_policy(),
387 )
388 }
389
390 fn with_cast_domain_config(
391 id: ActorMeshId,
392 members: ValueMesh<ActorRef<A>>,
393 cast_domain_id: CastDomainId,
394 tiling_policy: TilingPolicy,
395 ) -> Self {
396 let region = members.region().clone();
397 let member_addrs = members
398 .values()
399 .map(|member| member.actor_addr().clone())
400 .collect::<Vec<_>>();
401
402 Self {
403 id,
404 members,
405 cast_domain: Arc::new(ActorMeshCastDomain::with_config(
406 cast_domain_id,
407 Arc::new(
408 CastDestination::mesh(region, member_addrs)
409 .expect("actor refs from a dense mesh preserve cardinality"),
410 ),
411 tiling_policy,
412 )),
413 monitor: Arc::new(ActorLocal::new()),
414 }
415 }
416
417 pub fn try_new(
423 id: ActorMeshId,
424 region: Region,
425 members: Vec<ActorRef<A>>,
426 ) -> crate::Result<Self> {
427 Ok(Self::new_unchecked(id, ValueMesh::new(region, members)?))
428 }
429
430 pub fn id(&self) -> &ActorMeshId {
432 &self.id
433 }
434
435 pub fn members(&self) -> &ValueMesh<ActorRef<A>> {
437 &self.members
438 }
439
440 pub fn cast<M>(&self, cx: &impl context::Actor, message: M) -> crate::Result<()>
442 where
443 A: RemoteHandles<M>,
444 M: RemoteMessage + Clone,
445 {
446 self.cast_with_headers(cx, &Flattrs::new(), message)
447 }
448
449 pub fn cast_with_headers<M>(
452 &self,
453 cx: &impl context::Actor,
454 caller_headers: &Flattrs,
455 message: M,
456 ) -> crate::Result<()>
457 where
458 A: RemoteHandles<M>,
459 M: RemoteMessage + Clone,
460 {
461 self.cast_with_return_undeliverable(cx, caller_headers, message, true)
462 }
463
464 pub fn cast_with_return_undeliverable<M>(
466 &self,
467 cx: &impl context::Actor,
468 caller_headers: &Flattrs,
469 message: M,
470 return_undeliverable: bool,
471 ) -> crate::Result<()>
472 where
473 A: RemoteHandles<M>,
474 M: RemoteMessage + Clone,
475 {
476 if self.region().num_ranks() == 0 {
477 return Ok(());
478 }
479
480 let mut headers = caller_headers.clone();
481 headers.set(
482 casting::CAST_ORIGINATING_SENDER,
483 cx.instance().self_addr().clone(),
484 );
485 self.cast_domain
488 .ensure_materialized(cx, &headers)
489 .and_then(|domain| {
490 domain.cast_with_return_undeliverable(cx, headers, message, return_undeliverable)
491 })
492 .map_err(Error::Other)
493 }
494}
495
496impl<A: Referable> Clone for DataActorMesh<A> {
497 fn clone(&self) -> Self {
498 Self {
499 id: self.id.clone(),
500 members: self.members.clone(),
501 cast_domain: self.cast_domain.clone(),
502 monitor: Arc::clone(&self.monitor),
506 }
507 }
508}
509
510impl<A: Referable> Serialize for DataActorMesh<A> {
511 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
512 where
513 S: Serializer,
514 {
515 (
519 &self.id,
520 &self.members,
521 &self.cast_domain.id,
522 self.cast_domain.tiling_policy,
523 )
524 .serialize(serializer)
525 }
526}
527
528impl<'de, A: Referable> Deserialize<'de> for DataActorMesh<A> {
529 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
530 where
531 D: Deserializer<'de>,
532 {
533 let (id, members, cast_domain_id, tiling_policy) = Deserialize::deserialize(deserializer)?;
534 Ok(Self::with_cast_domain_config(
535 id,
536 members,
537 cast_domain_id,
538 tiling_policy,
539 ))
540 }
541}
542
543impl<A: Referable> view::Ranked for DataActorMesh<A> {
544 type Item = ActorRef<A>;
545
546 fn region(&self) -> &Region {
547 self.members.region()
548 }
549
550 fn get(&self, rank: usize) -> Option<&Self::Item> {
551 self.members.get(rank)
552 }
553}
554
555impl<A: Referable> view::RankedSliceable for DataActorMesh<A> {
556 fn sliced(&self, region: Region) -> Self {
557 Self::with_cast_domain_config(
558 self.id.clone(),
559 self.members.sliced(region),
560 CastDomainId::new(),
561 self.cast_domain.tiling_policy,
562 )
563 }
564}
565
566#[derive(typeuri::Named)]
573pub enum ActorMeshRef<A: Referable> {
574 Managed(Box<ManagedActorMeshRef<A>>),
577 Data(DataActorMesh<A>),
578}
579
580#[derive(Serialize, Deserialize)]
586#[serde(bound(serialize = "", deserialize = ""))]
587enum ActorMeshRefRepr<A: Referable> {
588 Managed(
589 Box<(
590 ActorMeshId,
591 Option<ProcMeshId>,
592 Option<ActorRef<ActorMeshController<A>>>,
593 ActorMeshCastDomain,
594 )>,
595 ),
596 Data(DataActorMesh<A>),
597}
598
599impl<A: Referable> ActorMeshRef<A> {
600 pub(crate) fn new_managed(
601 id: ActorMeshId,
602 proc_mesh_id: Option<ProcMeshId>,
603 region: Region,
604 controller: Option<ActorRef<ActorMeshController<A>>>,
605 members: Arc<ValueMesh<ActorAddr>>,
606 ) -> Self {
607 Self::Managed(Box::new(ManagedActorMeshRef::new(
608 id,
609 proc_mesh_id,
610 controller,
611 Arc::new(
612 CastDestination::mesh(region, members.values().collect())
613 .expect("actor addresses from a value mesh preserve cardinality"),
614 ),
615 )))
616 }
617
618 pub fn try_new_data(
620 id: ActorMeshId,
621 region: Region,
622 members: Vec<ActorRef<A>>,
623 ) -> crate::Result<Self> {
624 Ok(Self::Data(DataActorMesh::try_new(id, region, members)?))
625 }
626
627 pub fn as_managed(&self) -> Option<&ManagedActorMeshRef<A>> {
629 match self {
630 Self::Managed(managed) => Some(managed),
631 Self::Data(_) => None,
632 }
633 }
634
635 pub fn as_managed_mut(&mut self) -> Option<&mut ManagedActorMeshRef<A>> {
637 match self {
638 Self::Managed(managed) => Some(managed),
639 Self::Data(_) => None,
640 }
641 }
642
643 pub fn id(&self) -> &ActorMeshId {
645 match self {
646 Self::Managed(managed) => managed.id(),
647 Self::Data(data) => data.id(),
648 }
649 }
650
651 pub fn cast<M>(&self, cx: &impl context::Actor, message: M) -> crate::Result<()>
656 where
657 A: RemoteHandles<M>,
658 M: RemoteMessage + Clone,
659 {
660 match self {
661 Self::Managed(m) => m.cast(cx, message),
662 Self::Data(d) => d.cast(cx, message),
663 }
664 }
665
666 pub fn cast_with_headers<M>(
667 &self,
668 cx: &impl context::Actor,
669 caller_headers: &Flattrs,
670 message: M,
671 ) -> crate::Result<()>
672 where
673 A: RemoteHandles<M>,
674 M: RemoteMessage + Clone,
675 {
676 self.cast_with_return_undeliverable(cx, caller_headers, message, true)
677 }
678
679 pub fn cast_with_return_undeliverable<M>(
681 &self,
682 cx: &impl context::Actor,
683 caller_headers: &Flattrs,
684 message: M,
685 return_undeliverable: bool,
686 ) -> crate::Result<()>
687 where
688 A: RemoteHandles<M>,
689 M: RemoteMessage + Clone,
690 {
691 match self {
692 Self::Managed(m) => {
693 m.cast_with_return_undeliverable(cx, caller_headers, message, return_undeliverable)
694 }
695 Self::Data(d) => {
696 d.cast_with_return_undeliverable(cx, caller_headers, message, return_undeliverable)
697 }
698 }
699 }
700
701 pub async fn next_supervision_event(
703 &self,
704 cx: &impl context::Actor,
705 ) -> Result<MeshFailure, anyhow::Error> {
706 match self {
707 Self::Managed(managed) => managed.next_supervision_event(cx).await,
708 Self::Data(data) => {
709 let monitor = data
710 .monitor
711 .entry(cx)
712 .or_insert_with(|| {
713 Arc::new(MeshMonitor::spawn_dormant(cx, self.monitored_actors()))
714 })
715 .get()
716 .clone();
717 let _activation = monitor.activate(cx);
718 Ok((&*monitor).await)
719 }
720 }
721 }
722
723 pub fn clone_with_supervision_receiver(&self) -> Self {
725 match self {
726 Self::Managed(m) => Self::Managed(Box::new(m.clone_with_supervision_receiver())),
727 Self::Data(d) => Self::Data(d.clone()),
728 }
729 }
730
731 pub(crate) fn record_stopped(&self, cx: &impl context::Actor, status: ActorStatus) {
732 self.as_managed()
733 .expect("only managed actor meshes record local stopped state")
734 .record_stopped(cx, status);
735 }
736
737 pub fn monitor(&self, cx: &impl context::Actor) -> MeshMonitor {
739 MeshMonitor::spawn_persistent(cx, self.monitored_actors())
740 }
741
742 fn monitored_actors(&self) -> ValueMesh<ActorAddr> {
743 let region = self.region().clone();
744 let actors = self
745 .values()
746 .map(|actor| actor.actor_addr().clone())
747 .collect();
748 ValueMesh::new(region, actors)
749 .expect("actor addresses collected from a dense mesh preserve cardinality")
750 }
751}
752
753impl<A: Referable> Clone for ActorMeshRef<A> {
754 fn clone(&self) -> Self {
755 match self {
756 Self::Managed(m) => Self::Managed(m.clone()),
757 Self::Data(d) => Self::Data(d.clone()),
758 }
759 }
760}
761
762impl<A: Referable> fmt::Display for ActorMeshRef<A> {
763 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
764 match self {
765 Self::Managed(m) => write!(f, "{}", m),
766 Self::Data(d) => write!(f, "{:?}", d),
767 }
768 }
769}
770
771impl<A: Referable> fmt::Debug for ActorMeshRef<A> {
772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773 match self {
774 Self::Managed(m) => write!(f, "{:?}", m),
775 Self::Data(d) => write!(f, "{:?}", d),
776 }
777 }
778}
779
780impl<A: Referable> PartialEq for ActorMeshRef<A> {
781 fn eq(&self, other: &Self) -> bool {
782 match (self, other) {
783 (Self::Managed(a), Self::Managed(b)) => a == b,
784 (Self::Data(a), Self::Data(b)) => a == b,
785 _ => false,
786 }
787 }
788}
789impl<A: Referable> Eq for ActorMeshRef<A> {}
790
791impl<A: Referable> Hash for ActorMeshRef<A> {
792 fn hash<H: Hasher>(&self, state: &mut H) {
793 match self {
794 Self::Managed(m) => {
795 0u8.hash(state);
796 m.hash(state);
797 }
798 Self::Data(d) => {
799 1u8.hash(state);
800 d.hash(state);
801 }
802 }
803 }
804}
805
806impl<A: Referable> Serialize for ActorMeshRef<A> {
807 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
808 where
809 S: Serializer,
810 {
811 let repr = match self {
812 Self::Managed(m) => ActorMeshRefRepr::<A>::Managed(Box::new((
813 m.id.clone(),
814 m.proc_mesh_id.clone(),
815 m.controller.clone(),
816 m.cast_domain.clone(),
817 ))),
818 Self::Data(d) => ActorMeshRefRepr::<A>::Data(d.clone()),
819 };
820 repr.serialize(serializer)
821 }
822}
823
824impl<'de, A: Referable> Deserialize<'de> for ActorMeshRef<A> {
825 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
826 where
827 D: Deserializer<'de>,
828 {
829 Ok(match ActorMeshRefRepr::<A>::deserialize(deserializer)? {
830 ActorMeshRefRepr::Managed(boxed) => {
831 let (id, proc_mesh_id, controller, cast_domain) = *boxed;
832 Self::Managed(Box::new(ManagedActorMeshRef::with_cast_domain(
833 id,
834 proc_mesh_id,
835 controller,
836 cast_domain,
837 DEFAULT_PAGE,
838 )))
839 }
840 ActorMeshRefRepr::Data(data) => Self::Data(data),
841 })
842 }
843}
844
845impl<A: Referable> view::Ranked for ActorMeshRef<A> {
846 type Item = ActorRef<A>;
847
848 fn region(&self) -> &Region {
849 match self {
850 Self::Managed(managed) => managed.as_ref().region(),
851 Self::Data(data) => data.region(),
852 }
853 }
854
855 fn get(&self, rank: usize) -> Option<&Self::Item> {
856 match self {
857 Self::Managed(managed) => managed.as_ref().get(rank),
858 Self::Data(data) => data.get(rank),
859 }
860 }
861}
862
863impl<A: Referable> view::RankedSliceable for ActorMeshRef<A> {
864 fn sliced(&self, region: Region) -> Self {
865 match self {
866 Self::Managed(managed) => Self::Managed(Box::new(managed.as_ref().sliced(region))),
867 Self::Data(data) => Self::Data(data.sliced(region)),
868 }
869 }
870}
871
872impl<A: Referable> PartialEq for DataActorMesh<A> {
873 fn eq(&self, other: &Self) -> bool {
874 self.id == other.id && self.members == other.members
875 }
876}
877impl<A: Referable> Eq for DataActorMesh<A> {}
878
879impl<A: Referable> Hash for DataActorMesh<A> {
880 fn hash<H: Hasher>(&self, state: &mut H) {
881 self.id.hash(state);
882 self.members.hash(state);
883 }
884}
885
886const DEFAULT_PAGE: usize = 1024;
890
891struct Page<A: Referable> {
893 slots: Box<[OnceCell<ActorRef<A>>]>,
894}
895
896impl<A: Referable> Page<A> {
897 fn new(len: usize) -> Self {
898 let mut v = Vec::with_capacity(len);
899 for _ in 0..len {
900 v.push(OnceCell::new());
901 }
902 Self {
903 slots: v.into_boxed_slice(),
904 }
905 }
906}
907
908#[derive(Default)]
909struct HealthState {
910 unhealthy_event: Option<Unhealthy>,
911 crashed_ranks: HashMap<usize, ActorSupervisionEvent>,
912}
913
914impl HealthState {
915 fn failure_for_region(&self, region: &Region) -> Option<MeshFailure> {
916 let unhealthy = self.unhealthy_event.as_ref()?;
917 let mut failure = match unhealthy {
918 Unhealthy::StreamClosed(failure) | Unhealthy::Crashed(failure) => failure.clone(),
919 };
920 if failure.crashed_ranks.is_empty() {
921 return Some(failure);
922 }
923 let mut crashed_ranks = self
924 .crashed_ranks
925 .keys()
926 .copied()
927 .filter(|rank| region.slice().contains(*rank))
928 .collect::<Vec<_>>();
929 crashed_ranks.sort_unstable();
930 if crashed_ranks.is_empty() {
931 return None;
932 }
933 failure.crashed_ranks = crashed_ranks;
934 Some(failure)
935 }
936}
937
938impl std::fmt::Debug for HealthState {
939 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
940 f.debug_struct("HealthState")
941 .field("unhealthy_event", &self.unhealthy_event)
942 .field("crashed_ranks", &self.crashed_ranks)
943 .finish()
944 }
945}
946
947#[derive(Clone)]
948enum MessageOrFailure<M: Send + Sync + Clone + Default + 'static> {
949 Message(M),
950 Failure(String),
953 Timeout,
954}
955
956impl<M: Send + Sync + Clone + Default + 'static> Default for MessageOrFailure<M> {
957 fn default() -> Self {
958 Self::Message(M::default())
959 }
960}
961
962fn default_cast_tiling_policy() -> TilingPolicy {
963 TilingPolicy::BoundedFanout {
964 fanout: hyperactor_config::global::get(MAX_CAST_FANOUT).into(),
965 }
966}
967
968fn cast_node_mesh(
986 destinations: &ValueMesh<CastDestination>,
987) -> anyhow::Result<ValueMesh<CastNode>> {
988 let nodes = destinations
989 .values()
990 .map(|destination| {
991 CastNode::new(
992 CastActor::ref_for_proc(destination.actor().proc_addr()),
993 destination,
994 )
995 })
996 .collect::<Vec<_>>();
997
998 ValueMesh::new(destinations.region().clone(), nodes).map_err(Into::into)
999}
1000
1001#[derive(Clone)]
1002struct ActorMeshCastDomain {
1003 id: CastDomainId,
1004 destinations: Arc<ValueMesh<CastDestination>>,
1005 tiling_policy: TilingPolicy,
1006 cast_domain: ActorLocal<CastDomainRef>,
1007}
1008
1009impl std::fmt::Debug for ActorMeshCastDomain {
1010 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1011 f.debug_struct("ActorMeshCastDomain")
1012 .field("id", &self.id)
1013 .field("destinations", &self.destinations)
1014 .field("tiling_policy", &self.tiling_policy)
1015 .finish_non_exhaustive()
1016 }
1017}
1018
1019impl ActorMeshCastDomain {
1020 fn new(destinations: Arc<ValueMesh<CastDestination>>) -> Self {
1021 Self::with_config(
1022 CastDomainId::new(),
1023 destinations,
1024 default_cast_tiling_policy(),
1025 )
1026 }
1027
1028 fn with_config(
1029 id: CastDomainId,
1030 destinations: Arc<ValueMesh<CastDestination>>,
1031 tiling_policy: TilingPolicy,
1032 ) -> Self {
1033 Self {
1034 id,
1035 destinations,
1036 tiling_policy,
1037 cast_domain: ActorLocal::new(),
1038 }
1039 }
1040
1041 fn ensure_materialized(
1042 &self,
1043 cx: &impl context::Actor,
1044 headers: &Flattrs,
1045 ) -> anyhow::Result<CastDomainRef> {
1046 if let hyperactor::actor_local::Entry::Occupied(cast_domain) = self.cast_domain.entry(cx) {
1047 return Ok(cast_domain.get().clone());
1048 }
1049
1050 let nodes = Arc::new(cast_node_mesh(&self.destinations)?);
1051 let cast_domain =
1052 self.id
1053 .clone()
1054 .materialize(cx, nodes, self.tiling_policy, headers.clone())?;
1055
1056 self.cast_domain.entry(cx).or_insert(cast_domain.clone());
1057
1058 Ok(cast_domain)
1059 }
1060
1061 fn region(&self) -> &Region {
1062 self.destinations.region()
1063 }
1064}
1065
1066impl Serialize for ActorMeshCastDomain {
1067 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1068 where
1069 S: Serializer,
1070 {
1071 (&self.id, &self.destinations, self.tiling_policy).serialize(serializer)
1072 }
1073}
1074
1075impl<'de> Deserialize<'de> for ActorMeshCastDomain {
1076 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1077 where
1078 D: Deserializer<'de>,
1079 {
1080 let (id, destinations, tiling_policy) =
1081 <(CastDomainId, Arc<ValueMesh<CastDestination>>, TilingPolicy)>::deserialize(
1082 deserializer,
1083 )?;
1084
1085 Ok(Self {
1086 id,
1087 destinations,
1088 tiling_policy,
1089 cast_domain: ActorLocal::new(),
1090 })
1091 }
1092}
1093
1094fn into_watch<M: Send + Sync + Clone + Default + 'static>(
1098 mut rx: PortReceiver<M>,
1099) -> watch::Receiver<MessageOrFailure<M>> {
1100 let (sender, receiver) = watch::channel(MessageOrFailure::<M>::default());
1101 let timeout = hyperactor_config::global::get(SUPERVISION_WATCHDOG_TIMEOUT);
1109 let poll_frequency = hyperactor_config::global::get(SUPERVISION_POLL_FREQUENCY);
1110 let get_actor_state_max_idle = hyperactor_config::global::get(GET_ACTOR_STATE_MAX_IDLE);
1111 let get_proc_state_max_idle = hyperactor_config::global::get(GET_PROC_STATE_MAX_IDLE);
1112 let total_time = poll_frequency + get_actor_state_max_idle + get_proc_state_max_idle;
1113 if timeout < total_time {
1114 tracing::warn!(
1115 "HYPERACTOR_MESH_SUPERVISION_WATCHDOG_TIMEOUT={} is too short. It should be >= {} (SUPERVISION_POLL_FREQUENCY={} + GET_ACTOR_STATE_MAX_IDLE={} + GET_PROC_STATE_MAX_IDLE={})",
1116 humantime::format_duration(timeout),
1117 humantime::format_duration(total_time),
1118 humantime::format_duration(poll_frequency),
1119 humantime::format_duration(get_actor_state_max_idle),
1120 humantime::format_duration(get_proc_state_max_idle),
1121 );
1122 }
1123 tokio::spawn(async move {
1124 loop {
1125 let message = match tokio::time::timeout(timeout, rx.recv()).await {
1126 Ok(Ok(msg)) => MessageOrFailure::Message(msg),
1127 Ok(Err(e)) => MessageOrFailure::Failure(e.to_string()),
1128 Err(_) => MessageOrFailure::Timeout,
1129 };
1130 let is_failure = matches!(
1131 message,
1132 MessageOrFailure::Failure(_) | MessageOrFailure::Timeout
1133 );
1134 if sender.send(message).is_err() {
1135 break;
1137 }
1138 if is_failure {
1139 break;
1141 }
1142 }
1143 });
1144 receiver
1145}
1146
1147pub struct ManagedActorMeshRef<A: Referable> {
1150 id: ActorMeshId,
1151 proc_mesh_id: Option<ProcMeshId>,
1156 controller: Option<ActorRef<ActorMeshController<A>>>,
1163
1164 cast_domain: ActorMeshCastDomain,
1171 health_state: ActorLocal<HealthState>,
1175 receiver: ActorLocal<
1180 Arc<
1181 tokio::sync::Mutex<(
1182 PortRef<Option<MeshFailure>>,
1183 watch::Receiver<MessageOrFailure<Option<MeshFailure>>>,
1184 )>,
1185 >,
1186 >,
1187 pages: OnceCell<Vec<OnceCell<Box<Page<A>>>>>,
1197 page_size: usize,
1199}
1200
1201impl<A: Referable> ManagedActorMeshRef<A> {
1202 fn cached_failure(&self, cx: &impl context::Actor) -> Option<MeshFailure> {
1203 let health_state = self.health_state.entry(cx).or_default();
1204 health_state.get().failure_for_region(self.region())
1205 }
1206
1207 #[allow(clippy::result_large_err)]
1209 pub fn cast<M>(&self, cx: &impl context::Actor, message: M) -> crate::Result<()>
1210 where
1211 A: RemoteHandles<M>,
1212 M: RemoteMessage + Clone,
1213 {
1214 self.cast_with_headers(cx, &Flattrs::new(), message)
1215 }
1216
1217 #[allow(clippy::result_large_err)]
1224 pub fn cast_with_headers<M>(
1225 &self,
1226 cx: &impl context::Actor,
1227 caller_headers: &Flattrs,
1228 message: M,
1229 ) -> crate::Result<()>
1230 where
1231 A: RemoteHandles<M>,
1232 M: RemoteMessage + Clone,
1233 {
1234 self.cast_with_return_undeliverable(cx, caller_headers, message, true)
1235 }
1236
1237 #[expect(
1239 clippy::result_large_err,
1240 reason = "actor mesh errors preserve casting and mesh failure context"
1241 )]
1242 pub fn cast_with_return_undeliverable<M>(
1243 &self,
1244 cx: &impl context::Actor,
1245 caller_headers: &Flattrs,
1246 message: M,
1247 return_undeliverable: bool,
1248 ) -> crate::Result<()>
1249 where
1250 A: RemoteHandles<M>,
1251 M: RemoteMessage + Clone,
1252 {
1253 self.emit_sent_message_telemetry(cx, self.region());
1254
1255 let mut headers = caller_headers.clone();
1256 headers.set(
1257 casting::CAST_ORIGINATING_SENDER,
1258 cx.instance().self_addr().clone(),
1259 );
1260 headers.set(crate::casting::CAST_ACTOR_MESH_ID, self.id.clone());
1261
1262 if self.len() == 0 {
1263 Ok(())
1264 } else {
1265 self.cast_domain
1266 .ensure_materialized(cx, &headers)
1267 .map_err(|e| Error::CastingError(self.id.clone(), e))?
1268 .cast_with_return_undeliverable(cx, headers, message, return_undeliverable)
1269 .map_err(|e| Error::CastingError(self.id.clone(), e))
1270 }
1271 }
1272
1273 #[allow(clippy::result_large_err)]
1276 pub fn cast_choose_with_headers<M>(
1277 &self,
1278 cx: &impl context::Actor,
1279 caller_headers: &Flattrs,
1280 message: M,
1281 ) -> crate::Result<()>
1282 where
1283 A: RemoteHandles<M>,
1284 M: RemoteMessage + Clone,
1285 {
1286 self.emit_sent_message_telemetry(
1287 cx,
1288 &Region::new(
1289 Vec::new(),
1290 ndslice::Slice::new(0, Vec::new(), Vec::new())
1291 .expect("zero-dimensional slice is valid"),
1292 ),
1293 );
1294
1295 let num_ranks = self.cast_domain.region().num_ranks();
1296
1297 if num_ranks == 0 {
1298 return Ok(());
1299 }
1300
1301 let rank_index = rand::random::<u64>() as usize % num_ranks;
1302
1303 let point = self
1304 .cast_domain
1305 .region()
1306 .extent()
1307 .point_of_rank(rank_index)
1308 .map_err(|err| Error::CastingError(self.id.clone(), err.into()))?;
1309
1310 let actor = self.materialize(rank_index).ok_or_else(|| {
1311 Error::CastingError(
1312 self.id.clone(),
1313 anyhow::anyhow!("missing actor for chosen rank {rank_index}"),
1314 )
1315 })?;
1316
1317 self.post_cast_direct(cx, point, actor, message, caller_headers)
1318 }
1319
1320 fn emit_sent_message_telemetry(&self, cx: &impl context::Actor, region: &Region) {
1321 hyperactor_telemetry::notify_sent_message(hyperactor_telemetry::SentMessageEvent {
1322 timestamp: std::time::SystemTime::now(),
1323 sender_actor_id: hyperactor_telemetry::hash_to_u64(cx.mailbox().actor_addr().id()),
1324 actor_mesh_id: match &self.proc_mesh_id {
1325 Some(proc_mesh_id) => telemetry_actor_mesh_id(proc_mesh_id, &self.id),
1326 None => hyperactor_telemetry::hash_to_u64(&self.id),
1329 },
1330 view_json: serde_json::to_string(region).unwrap_or_default(),
1331 shape_json: {
1332 let shape: ndslice::Shape = region.into();
1333 serde_json::to_string(&shape).unwrap_or_default()
1334 },
1335 });
1336 }
1337
1338 #[allow(clippy::result_large_err)]
1339 fn post_cast_direct<M>(
1340 &self,
1341 cx: &impl context::Actor,
1342 point: ndslice::Point,
1343 actor: &ActorRef<A>,
1344 message: M,
1345 caller_headers: &Flattrs,
1346 ) -> crate::Result<()>
1347 where
1348 A: RemoteHandles<M>,
1349 M: RemoteMessage,
1350 {
1351 let create_rank = point.rank();
1352 let mut headers = caller_headers.clone();
1353 casting::set_cast_info_on_headers(&mut headers, point, cx.instance().self_addr().clone());
1354
1355 let mut data = wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&message)
1358 .map_err(|e| Error::CastingError(self.id.clone(), e.into()))?;
1359 data.visit_multipart_parts_mut::<resource::RankRepr, anyhow::Error>(
1360 |resource::RankRepr(rank)| {
1361 *rank = Some(create_rank);
1362 Ok(())
1363 },
1364 )
1365 .map_err(|e| Error::CastingError(self.id.clone(), e))?;
1366 let rebound_message = data
1367 .deserialized_unchecked()
1368 .map_err(|e| Error::CastingError(self.id.clone(), e.into()))?;
1369 actor.post_with_headers(cx, headers, rebound_message);
1370 Ok(())
1371 }
1372
1373 pub(crate) fn new(
1374 id: ActorMeshId,
1375 proc_mesh_id: Option<ProcMeshId>,
1376 controller: Option<ActorRef<ActorMeshController<A>>>,
1377 destinations: Arc<ValueMesh<CastDestination>>,
1378 ) -> Self {
1379 Self::with_page_size(id, proc_mesh_id, DEFAULT_PAGE, controller, destinations)
1380 }
1381
1382 pub fn id(&self) -> &ActorMeshId {
1383 &self.id
1384 }
1385
1386 pub(crate) fn with_page_size(
1387 id: ActorMeshId,
1388 proc_mesh_id: Option<ProcMeshId>,
1389 page_size: usize,
1390 controller: Option<ActorRef<ActorMeshController<A>>>,
1391 destinations: Arc<ValueMesh<CastDestination>>,
1392 ) -> Self {
1393 Self::with_cast_domain(
1394 id,
1395 proc_mesh_id,
1396 controller,
1397 ActorMeshCastDomain::new(destinations),
1398 page_size,
1399 )
1400 }
1401
1402 fn with_cast_domain(
1403 id: ActorMeshId,
1404 proc_mesh_id: Option<ProcMeshId>,
1405 controller: Option<ActorRef<ActorMeshController<A>>>,
1406 cast_domain: ActorMeshCastDomain,
1407 page_size: usize,
1408 ) -> Self {
1409 Self {
1410 id,
1411 proc_mesh_id,
1412 controller,
1413 cast_domain,
1414 health_state: ActorLocal::new(),
1415 receiver: ActorLocal::new(),
1416 pages: OnceCell::new(),
1417 page_size: page_size.max(1),
1418 }
1419 }
1420
1421 #[inline]
1422 fn len(&self) -> usize {
1423 self.cast_domain.region().num_ranks()
1424 }
1425
1426 pub fn controller(&self) -> &Option<ActorRef<ActorMeshController<A>>> {
1427 &self.controller
1428 }
1429
1430 fn take_controller(&mut self) -> Option<ActorRef<ActorMeshController<A>>> {
1431 self.controller.take()
1432 }
1433
1434 fn set_controller(&mut self, controller: Option<ActorRef<ActorMeshController<A>>>) {
1435 self.controller = controller;
1436 }
1437
1438 fn ensure_pages(&self) -> &Vec<OnceCell<Box<Page<A>>>> {
1439 let n = self.len().div_ceil(self.page_size); self.pages
1441 .get_or_init(|| (0..n).map(|_| OnceCell::new()).collect())
1442 }
1443
1444 fn materialize(&self, rank: usize) -> Option<&ActorRef<A>> {
1445 let len = self.len();
1446 if rank >= len {
1447 return None;
1448 }
1449 let cast_domain = &self.cast_domain;
1450 let p = self.page_size;
1451 let page_ix = rank / p;
1452 let local_ix = rank % p;
1453
1454 let pages = self.ensure_pages();
1455 let page = pages[page_ix].get_or_init(|| {
1456 let base = page_ix * p;
1458 let remaining = len - base;
1459 let page_len = remaining.min(p);
1460 Box::new(Page::<A>::new(page_len))
1461 });
1462
1463 Some(page.slots[local_ix].get_or_init(|| {
1464 debug_assert!(rank < self.len(), "rank must be within [0, len)");
1467 ActorRef::attest(
1468 Ranked::get(cast_domain.destinations.as_ref(), rank)
1469 .map(CastDestination::actor)
1470 .expect("rank must be present in cast-domain member map")
1471 .clone(),
1472 )
1473 }))
1474 }
1475
1476 fn init_supervision_receiver(
1477 controller: &ActorRef<ActorMeshController<A>>,
1478 cx: &impl context::Actor,
1479 ) -> (
1480 PortRef<Option<MeshFailure>>,
1481 watch::Receiver<MessageOrFailure<Option<MeshFailure>>>,
1482 ) {
1483 let (tx, rx) = cx.mailbox().open_port();
1484 let tx = tx.bind();
1485 controller.post(cx, Subscribe(tx.clone()));
1486 (tx, into_watch(rx))
1487 }
1488
1489 pub async fn next_supervision_event(
1496 &self,
1497 cx: &impl context::Actor,
1498 ) -> Result<MeshFailure, anyhow::Error> {
1499 if let Some(failure) = self.cached_failure(cx) {
1500 tracing::debug!(
1501 actor_mesh = %self.id,
1502 crashed_ranks = ?failure.crashed_ranks,
1503 "returning cached supervision failure"
1504 );
1505 return Ok(failure);
1506 }
1507 let controller = if let Some(c) = self.controller() {
1508 c
1509 } else {
1510 return Err(anyhow::anyhow!(
1511 "unexpected healthy state while controller is gone"
1512 ));
1513 };
1514 let rx = {
1515 let entry = self.receiver.entry(cx).or_insert_with(|| {
1517 Arc::new(tokio::sync::Mutex::new(Self::init_supervision_receiver(
1518 controller, cx,
1519 )))
1520 });
1521 Arc::clone(entry.get())
1524 };
1525 let message = {
1526 let mut rx = rx.lock().await;
1527 let subscriber_port = rx.0.clone();
1528 let message =
1529 rx.1.wait_for(|message| {
1530 if let MessageOrFailure::Message(message) = message {
1534 if let Some(message) = &message {
1535 let region = self.region().slice();
1536 if message.crashed_ranks.is_empty() {
1537 true
1539 } else {
1540 message.crashed_ranks.iter().any(|r| region.contains(*r))
1543 }
1544 } else {
1545 false
1549 }
1550 } else {
1551 true
1553 }
1554 })
1555 .await?;
1556 let message = message.clone();
1557 let is_failure = matches!(
1558 message,
1559 MessageOrFailure::Failure(_) | MessageOrFailure::Timeout
1560 );
1561 if is_failure {
1562 let mut port = controller.port();
1567 port.return_undeliverable(false);
1569 let _ = port.post(cx, Unsubscribe(subscriber_port));
1570 }
1571 match message {
1575 MessageOrFailure::Message(message) => Ok::<MeshFailure, anyhow::Error>(
1576 message.expect("filter excludes any None messages"),
1577 ),
1578 MessageOrFailure::Failure(failure) => Err(anyhow::anyhow!("{}", failure)),
1579 MessageOrFailure::Timeout => {
1580 Ok(MeshFailure {
1583 actor_mesh_name: Some(self.id().to_string()),
1584 event: ActorSupervisionEvent::new(
1585 controller.actor_addr().clone(),
1586 None,
1587 ActorStatus::generic_failure(format!(
1588 "timed out reaching controller {} for mesh {}. Assuming controller's proc is dead",
1589 controller.actor_addr(),
1590 self.id()
1591 )),
1592 None,
1593 ),
1594 crashed_ranks: vec![],
1595 reporting_controller: None,
1598 })
1599 }
1600 }?
1601 };
1602 let event = &message.event;
1604 let mut entry = self.health_state.entry(cx).or_default();
1606 let health_state = entry.get_mut();
1607 if let ActorStatus::Failed(_) = event.actor_status {
1608 for &rank in &message.crashed_ranks {
1609 health_state.crashed_ranks.insert(rank, event.clone());
1610 }
1611 }
1612 health_state.unhealthy_event = match &event.actor_status {
1613 ActorStatus::Failed(_) => Some(Unhealthy::Crashed(message.clone())),
1614 ActorStatus::Stopped(_) => Some(Unhealthy::StreamClosed(message.clone())),
1615 _ => None,
1616 };
1617 Ok(message)
1618 }
1619
1620 pub(crate) fn record_stopped(&self, cx: &impl context::Actor, status: ActorStatus) {
1623 let mut entry = self.health_state.entry(cx).or_default();
1624 let health_state = entry.get_mut();
1625 health_state.unhealthy_event = Some(Unhealthy::StreamClosed(MeshFailure {
1626 actor_mesh_name: Some(self.id().to_string()),
1627 event: ActorSupervisionEvent::new(
1628 self.get(0)
1630 .expect("mesh must have at least one rank")
1631 .actor_addr()
1632 .clone(),
1633 None,
1634 status,
1635 None,
1636 ),
1637 crashed_ranks: vec![],
1638 reporting_controller: None,
1641 }));
1642 }
1643
1644 pub fn clone_with_supervision_receiver(&self) -> Self {
1648 Self {
1649 id: self.id.clone(),
1650 proc_mesh_id: self.proc_mesh_id.clone(),
1651 controller: self.controller.clone(),
1652 cast_domain: self.cast_domain.clone(),
1653 health_state: self.health_state.clone(),
1654 receiver: self.receiver.clone(),
1655 pages: OnceCell::new(),
1657 page_size: self.page_size,
1658 }
1659 }
1660}
1661
1662impl<A: Referable> Clone for ManagedActorMeshRef<A> {
1663 fn clone(&self) -> Self {
1664 Self {
1665 id: self.id.clone(),
1666 proc_mesh_id: self.proc_mesh_id.clone(),
1667 controller: self.controller.clone(),
1668 cast_domain: self.cast_domain.clone(),
1669 health_state: ActorLocal::new(),
1672 receiver: ActorLocal::new(),
1673 pages: OnceCell::new(), page_size: self.page_size,
1675 }
1676 }
1677}
1678
1679impl<A: Referable> fmt::Display for ManagedActorMeshRef<A> {
1680 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1681 write!(
1682 f,
1683 "{}:{}@{}",
1684 self.id,
1685 A::typename(),
1686 self.cast_domain.region()
1687 )
1688 }
1689}
1690
1691impl<A: Referable> PartialEq for ManagedActorMeshRef<A> {
1692 fn eq(&self, other: &Self) -> bool {
1693 self.id == other.id && self.cast_domain.region() == other.cast_domain.region()
1701 }
1702}
1703impl<A: Referable> Eq for ManagedActorMeshRef<A> {}
1704
1705impl<A: Referable> Hash for ManagedActorMeshRef<A> {
1706 fn hash<H: Hasher>(&self, state: &mut H) {
1707 self.id.hash(state);
1708 self.cast_domain.region().hash(state);
1709 }
1710}
1711
1712impl<A: Referable> fmt::Debug for ManagedActorMeshRef<A> {
1713 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1714 f.debug_struct("ActorMeshRef")
1715 .field("region", self.cast_domain.region())
1716 .field("id", &self.id)
1717 .field("page_size", &self.page_size)
1718 .finish_non_exhaustive() }
1720}
1721
1722impl<A: Referable> Serialize for ManagedActorMeshRef<A> {
1724 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1725 where
1726 S: Serializer,
1727 {
1728 (
1730 &self.id,
1731 &self.proc_mesh_id,
1732 &self.controller,
1733 &self.cast_domain,
1734 )
1735 .serialize(serializer)
1736 }
1737}
1738
1739impl<'de, A: Referable> Deserialize<'de> for ManagedActorMeshRef<A> {
1741 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1742 where
1743 D: Deserializer<'de>,
1744 {
1745 let (id, proc_mesh_id, controller, cast_domain) = <(
1746 ActorMeshId,
1747 Option<ProcMeshId>,
1748 Option<ActorRef<ActorMeshController<A>>>,
1749 ActorMeshCastDomain,
1750 )>::deserialize(deserializer)?;
1751 Ok(Self::with_cast_domain(
1752 id,
1753 proc_mesh_id,
1754 controller,
1755 cast_domain,
1756 DEFAULT_PAGE,
1757 ))
1758 }
1759}
1760
1761impl<A: Referable> view::Ranked for ManagedActorMeshRef<A> {
1762 type Item = ActorRef<A>;
1763
1764 #[inline]
1765 fn region(&self) -> &Region {
1766 self.cast_domain.region()
1767 }
1768
1769 #[inline]
1770 fn get(&self, rank: usize) -> Option<&Self::Item> {
1771 self.materialize(rank)
1772 }
1773}
1774
1775impl<A: Referable> view::RankedSliceable for ManagedActorMeshRef<A> {
1776 fn sliced(&self, region: Region) -> Self {
1783 debug_assert!(region.is_subset(self.region()));
1788 Self {
1789 id: self.id.clone(),
1790 proc_mesh_id: self.proc_mesh_id.clone(),
1791 controller: self.controller.clone(),
1792 cast_domain: ActorMeshCastDomain::new(Arc::new(
1793 CastDestination::subset(self.cast_domain.destinations.as_ref(), region.clone())
1794 .expect("actor mesh slice must preserve its destination nodes"),
1795 )),
1796 health_state: self.health_state.clone(),
1797 receiver: ActorLocal::new(),
1798 pages: OnceCell::new(),
1799 page_size: self.page_size,
1800 }
1801 }
1802}
1803
1804#[cfg(all(test, fbcode_build))]
1805mod tests {
1806
1807 use std::collections::HashMap;
1808 use std::collections::HashSet;
1809 use std::ops::Deref;
1810 use std::sync::Arc;
1811
1812 use hyperactor::ActorRef;
1813 use hyperactor::Endpoint as _;
1814 use hyperactor::Proc;
1815 use hyperactor::ProcAddr;
1816 use hyperactor::actor::ActorErrorKind;
1817 use hyperactor::actor::ActorStatus;
1818 use hyperactor::channel::ChannelAddr;
1819 use hyperactor::context::Mailbox as _;
1820 use hyperactor::id::Label;
1821 use hyperactor::mailbox;
1822 use hyperactor::supervision::ActorSupervisionEvent;
1823 use hyperactor_cast::TilingPolicy;
1824 use hyperactor_cast::cast_actor::CastDestination;
1825 use hyperactor_cast::cast_actor::CastDomainId;
1826 use ndslice::Extent;
1827 use ndslice::Region;
1828 use ndslice::Slice;
1829 use ndslice::ViewExt;
1830 use ndslice::extent;
1831 use ndslice::view::Ranked;
1832 use ndslice::view::RankedSliceable;
1833 use timed_test::assert_no_process_leak;
1834 use timed_test::async_timed_test;
1835 use tokio::time::Duration;
1836
1837 use super::ActorMesh;
1838 use super::DataActorMesh;
1839 use crate::ActorMeshRef;
1840 use crate::ProcMesh;
1841 use crate::ValueMesh;
1842 use crate::host_mesh::GET_PROC_STATE_MAX_IDLE;
1843 use crate::host_mesh::PROC_SPAWN_MAX_IDLE;
1844 use crate::mesh_controller::ActorMeshControlPlane;
1845 use crate::mesh_controller::ActorMeshController;
1846 use crate::mesh_controller::SUPERVISION_POLL_FREQUENCY;
1847 use crate::mesh_id::ActorMeshId;
1848 use crate::proc_agent::ActorState;
1849 use crate::proc_mesh::ACTOR_SPAWN_MAX_IDLE;
1850 use crate::proc_mesh::GET_ACTOR_STATE_MAX_IDLE;
1851 use crate::proc_mesh::USE_DIRECT_SPAWN;
1852 use crate::resource;
1853 use crate::supervision::MeshFailure;
1854 use crate::test_utils::local_host_mesh;
1855 use crate::testactor;
1856 use crate::testing;
1857
1858 #[test]
1859 fn test_actor_mesh_ref_is_send_and_sync() {
1860 fn assert_send_sync<T: Send + Sync>() {}
1861 assert_send_sync::<ActorMeshRef<()>>();
1862 }
1863
1864 #[async_timed_test(timeout_secs = 30)]
1865 async fn test_data_actor_mesh_ref_observes_terminal_status() {
1866 let proc = Proc::isolated();
1867 let client = proc.client("client");
1868 let target = client.spawn_with_label("rank0", testactor::TestActor);
1869 let actor_ref = target.bind::<testactor::TestActor>();
1870 let actor_addr = target.actor_addr().clone();
1871 let mesh = ActorMeshRef::try_new_data(
1872 ActorMeshId::instance(Label::new("actors").unwrap()),
1873 extent!(actors = 1).into(),
1874 vec![actor_ref],
1875 )
1876 .expect("data actor mesh ref should be valid");
1877
1878 let mut wait = Box::pin(mesh.next_supervision_event(&client));
1879 tokio::select! {
1880 failure = &mut wait => panic!("unexpected failure before stop: {failure:?}"),
1881 _ = tokio::task::yield_now() => {}
1882 }
1883 target
1884 .drain_and_stop("rank complete")
1885 .expect("target should accept stop");
1886
1887 let failure = tokio::time::timeout(Duration::from_secs(1), wait)
1888 .await
1889 .expect("timed out waiting for data actor supervision")
1890 .expect("data actor supervision should succeed");
1891 assert_eq!(failure.crashed_ranks, vec![0]);
1892 assert_eq!(failure.event.actor_id, actor_addr);
1893 assert!(matches!(
1894 failure.event.actor_status,
1895 ActorStatus::Stopped(ref reason) if reason == "rank complete"
1896 ));
1897 }
1898
1899 #[test]
1900 fn test_actor_mesh_ref_data_variant_is_detached_data_structure() {
1901 let region: Region = extent!(replicas = 2).into();
1902 let members = vec![
1903 ActorRef::<testactor::TestActor>::attest(
1904 ProcAddr::instance(ChannelAddr::Local(9000), "data").actor_addr("rank0"),
1905 ),
1906 ActorRef::<testactor::TestActor>::attest(
1907 ProcAddr::instance(ChannelAddr::Local(9000), "data").actor_addr("rank1"),
1908 ),
1909 ];
1910
1911 let id = ActorMeshId::instance(Label::new("data").unwrap());
1912 let mesh: ActorMeshRef<testactor::TestActor> =
1913 ActorMeshRef::try_new_data(id.clone(), region.clone(), members.clone()).unwrap();
1914
1915 assert_eq!(mesh.id(), &id);
1916 assert_eq!(mesh.region(), ®ion);
1917 assert_eq!(mesh.get(1).unwrap().actor_addr(), members[1].actor_addr());
1918 assert_eq!(
1919 mesh.values()
1920 .map(|actor_ref| actor_ref.actor_addr().clone())
1921 .collect::<Vec<_>>(),
1922 members
1923 .iter()
1924 .map(|actor_ref| actor_ref.actor_addr().clone())
1925 .collect::<Vec<_>>()
1926 );
1927 let ActorMeshRef::Data(data) = &mesh else {
1928 panic!("data constructor returned a managed ref");
1929 };
1930 assert_eq!(data.members().values().count(), 2);
1931 let cast_domain_id = data.cast_domain.id.clone();
1932 assert!(matches!(
1933 data.cast_domain.tiling_policy,
1934 TilingPolicy::BoundedFanout { .. }
1935 ));
1936
1937 let cloned = mesh.clone();
1938 let ActorMeshRef::Data(cloned_data) = &cloned else {
1939 panic!("cloned data ref changed variant");
1940 };
1941 assert_eq!(cloned_data.cast_domain.id, cast_domain_id);
1942 assert!(Arc::ptr_eq(&data.monitor, &cloned_data.monitor));
1943
1944 let slice_region = region
1945 .range("replicas", 1..2)
1946 .expect("rank 1 slice should exist");
1947 let slice = mesh.sliced(slice_region.clone());
1948 let ActorMeshRef::Data(slice_data) = &slice else {
1949 panic!("data slice changed variant");
1950 };
1951 assert_ne!(slice_data.cast_domain.id, cast_domain_id);
1952 assert!(!Arc::ptr_eq(&data.monitor, &slice_data.monitor));
1953 assert_eq!(slice.id(), &id);
1954 assert_eq!(slice.region(), &slice_region);
1955 assert_eq!(slice.values().count(), 1);
1956 assert_eq!(slice.get(0).unwrap().actor_addr(), members[1].actor_addr());
1957
1958 let encoded = serde_json::to_vec(&mesh).expect("data ref should serialize");
1959 let decoded: ActorMeshRef<testactor::TestActor> =
1960 serde_json::from_slice(&encoded).expect("data ref should deserialize");
1961 assert_eq!(decoded, mesh);
1962 assert_eq!(decoded.id(), &id);
1963 assert_eq!(decoded.region(), ®ion);
1964 assert_eq!(decoded.values().count(), 2);
1965 let ActorMeshRef::Data(decoded_data) = &decoded else {
1966 panic!("decoded data ref changed variant");
1967 };
1968 assert_eq!(decoded_data.cast_domain.id, cast_domain_id);
1969 assert!(!Arc::ptr_eq(&data.monitor, &decoded_data.monitor));
1970
1971 assert!(matches!(
1972 ActorMeshRef::try_new_data(id.clone(), region.clone(), vec![members[0].clone()]),
1973 Err(crate::Error::InvalidRankCardinality {
1974 expected: 2,
1975 actual: 1,
1976 })
1977 ));
1978 }
1979
1980 #[test]
1981 fn test_data_actor_mesh_slice_preserves_tiling_policy() {
1982 let region: Region = extent!(replicas = 2).into();
1983 let members = ValueMesh::new(
1984 region.clone(),
1985 vec![
1986 ActorRef::<testactor::TestActor>::attest(
1987 ProcAddr::instance(ChannelAddr::Local(9000), "data").actor_addr("rank0"),
1988 ),
1989 ActorRef::<testactor::TestActor>::attest(
1990 ProcAddr::instance(ChannelAddr::Local(9000), "data").actor_addr("rank1"),
1991 ),
1992 ],
1993 )
1994 .expect("member mesh should be valid");
1995 let data = DataActorMesh::with_cast_domain_config(
1996 ActorMeshId::instance(Label::new("data").unwrap()),
1997 members,
1998 CastDomainId::new(),
1999 TilingPolicy::BlockPartitioning,
2000 );
2001 let slice_region = region
2002 .range("replicas", 1..2)
2003 .expect("rank 1 slice should exist");
2004
2005 let slice = data.sliced(slice_region);
2006
2007 assert!(matches!(
2008 slice.cast_domain.tiling_policy,
2009 TilingPolicy::BlockPartitioning
2010 ));
2011 assert_ne!(slice.cast_domain.id, data.cast_domain.id);
2012 }
2013
2014 #[tokio::test]
2015 async fn test_actor_mesh_ref_monitor_owns_status_and_guards_operations() {
2016 let proc = Proc::isolated();
2017 let sender = proc.actor_instance::<()>("sender").unwrap();
2018 sender.instance.bind::<()>();
2019 let target = sender
2020 .instance
2021 .spawn_with_label("rank0", testactor::TestActor);
2022 let region: Region = extent!(replicas = 1).into();
2023 let actor_ref: ActorRef<testactor::TestActor> = target.bind();
2024 let mesh = ActorMeshRef::try_new_data(
2025 ActorMeshId::instance(Label::new("data").unwrap()),
2026 region,
2027 vec![actor_ref.clone()],
2028 )
2029 .expect("data ref should be valid");
2030 let monitor = mesh.monitor(&sender.instance);
2031 let independent_monitor = mesh.monitor(&sender.instance);
2032
2033 assert_eq!(monitor.status(0), Some(ActorStatus::Unknown));
2034 assert_eq!(independent_monitor.status(0), Some(ActorStatus::Unknown));
2035 assert_eq!(mesh.get(0).expect("rank 0 should exist"), &actor_ref);
2036
2037 let observed = {
2038 let mut wait = std::pin::pin!(monitor.guard(std::future::pending::<()>()));
2039 tokio::select! {
2040 biased;
2041 failure = &mut wait => panic!("unexpected failure before stop: {failure:?}"),
2042 _ = tokio::task::yield_now() => {}
2043 }
2044
2045 target
2046 .drain_and_stop("rank complete")
2047 .expect("target should accept stop");
2048
2049 tokio::time::timeout(Duration::from_secs(10), wait)
2050 .await
2051 .expect("timed out waiting for monitored guard failure")
2052 .expect_err("pending operation should lose to rank failure")
2053 };
2054 let independently_observed = tokio::time::timeout(
2055 Duration::from_secs(10),
2056 independent_monitor.guard(std::future::pending::<()>()),
2057 )
2058 .await
2059 .expect("timed out waiting for independent monitor failure")
2060 .expect_err("pending operation should lose to rank failure");
2061 assert_eq!(observed.crashed_ranks, vec![0]);
2062 assert_eq!(independently_observed.crashed_ranks, vec![0]);
2063 assert!(matches!(
2064 observed.event.actor_status,
2065 ActorStatus::Stopped(ref reason) if reason == "rank complete"
2066 ));
2067 assert!(matches!(
2068 monitor.status(0).expect("rank 0 should remain monitored"),
2069 ActorStatus::Stopped(ref reason) if reason == "rank complete"
2070 ));
2071
2072 let (port, _rx) = mailbox::open_port(&sender.instance);
2073 mesh.cast(&sender.instance, testactor::GetActorId(port.bind()))
2074 .expect("casting does not implicitly check monitor status");
2075
2076 tokio::time::timeout(Duration::from_secs(5), target)
2077 .await
2078 .expect("timed out waiting for target to stop");
2079
2080 mesh.cast(&sender.instance, ())
2081 .expect("casting to a stopped actor should still initiate delivery");
2082 }
2083
2084 #[tokio::test]
2085 async fn test_actor_mesh_ref_lazy_materialization() {
2086 let config = hyperactor_config::global::lock();
2087 let _mode = config.override_key(USE_DIRECT_SPAWN, false);
2088 let instance = testing::instance();
2090 let mut hm = testing::host_mesh(2).await;
2093 let pm: ProcMesh = hm
2094 .spawn(instance, "test", extent!(gpus = 2), None, None)
2095 .await
2096 .unwrap();
2097 let am: ActorMesh<testactor::TestActor> = pm.spawn(instance, "test", &()).await.unwrap();
2098
2099 let page_size = 2;
2103 let ActorMeshRef::Managed(managed) = am.deref() else {
2104 panic!("spawned actor mesh should be managed");
2105 };
2106 let amr: ActorMeshRef<testactor::TestActor> =
2107 ActorMeshRef::Managed(Box::new(super::ManagedActorMeshRef::with_page_size(
2108 am.id().clone(),
2109 managed.proc_mesh_id.clone(),
2110 page_size,
2111 None,
2112 Arc::clone(&managed.cast_domain.destinations),
2113 )));
2114 assert_eq!(amr.extent(), extent!(hosts = 2, gpus = 2));
2115 assert_eq!(amr.region().num_ranks(), 4);
2116 assert_eq!(amr.values().count(), 4);
2117
2118 let encoded = serde_json::to_vec(&amr).expect("managed ref should serialize");
2119 let decoded: ActorMeshRef<testactor::TestActor> =
2120 serde_json::from_slice(&encoded).expect("managed ref should deserialize");
2121 assert!(matches!(&decoded, ActorMeshRef::Managed(_)));
2122 assert_eq!(decoded, amr);
2123 assert_eq!(decoded.region(), amr.region());
2124 assert_eq!(decoded.get(3), amr.get(3));
2125
2126 let p0_a = amr.get(0).expect("rank 0 exists") as *const _;
2128 let p0_b = amr.get(0).expect("rank 0 exists") as *const _;
2129 assert_eq!(p0_a, p0_b, "same rank should return same cached pointer");
2130
2131 let p1_a = amr.get(1).expect("rank 1 exists") as *const _;
2133 let p1_b = amr.get(1).expect("rank 1 exists") as *const _;
2134 assert_eq!(p1_a, p1_b, "same rank should return same cached pointer");
2135 assert_ne!(p0_a, p1_a, "different ranks have different cache slots");
2138
2139 let p2_a = amr.get(2).expect("rank 2 exists") as *const _;
2141 let p2_b = amr.get(2).expect("rank 2 exists") as *const _;
2142 assert_eq!(p2_a, p2_b, "same rank should return same cached pointer");
2143 assert_ne!(p0_a, p2_a, "different pages have different cache slots");
2144
2145 let amr_clone = amr.clone();
2147 let orig_id_0 = amr.get(0).unwrap().actor_addr().clone();
2148 let clone_id_0 = amr_clone.get(0).unwrap().actor_addr().clone();
2149 assert_eq!(orig_id_0, clone_id_0, "clone preserves identity");
2150 let p0_clone = amr_clone.get(0).unwrap() as *const _;
2151 assert_ne!(
2152 p0_a, p0_clone,
2153 "cloned ActorMeshRef has a fresh cache (different pointer)"
2154 );
2155
2156 let sliced = amr.range("hosts", 0..2).expect("slice should be valid"); assert_eq!(sliced.region().num_ranks(), 4);
2160 assert!(
2161 sliced.get(0).is_some(),
2162 "RankedSliceable::sliced preserves a lazy cast-domain descriptor"
2163 );
2164 let sp0_a = sliced.get(0).unwrap() as *const _;
2166 let sp0_b = sliced.get(0).unwrap() as *const _;
2167 assert_eq!(sp0_a, sp0_b, "sliced view has its own cache slot per rank");
2168 let sp2 = sliced.get(2).unwrap() as *const _;
2171 assert_ne!(sp0_a, sp2, "sliced view crosses its own page boundary");
2172
2173 let mut set = HashSet::new();
2176 set.insert(amr.clone());
2177 set.insert(amr.clone());
2178 assert_eq!(set.len(), 1, "cache state must not affect Hash/Eq");
2179
2180 let (port, mut rx) = mailbox::open_port(instance);
2183 amr.get(0)
2186 .expect("rank 0 exists")
2187 .post(instance, testactor::GetActorId(port.bind()));
2188 amr.get(3)
2189 .expect("rank 3 exists")
2190 .post(instance, testactor::GetActorId(port.bind()));
2191 let id_a = tokio::time::timeout(Duration::from_secs(3), rx.recv())
2192 .await
2193 .expect("timed out waiting for first reply")
2194 .expect("channel closed before first reply");
2195 let id_b = tokio::time::timeout(Duration::from_secs(3), rx.recv())
2196 .await
2197 .expect("timed out waiting for second reply")
2198 .expect("channel closed before second reply");
2199 assert_ne!(id_a, id_b, "two different ranks responded");
2200
2201 let _ = hm.shutdown(instance).await;
2202 }
2203
2204 #[async_timed_test(timeout_secs = 300)]
2205 #[cfg(fbcode_build)]
2206 async fn test_actor_mesh_slice_casts_only_to_slice_members() {
2207 let instance = testing::instance();
2208 let mut hm = testing::host_mesh(2).await;
2209 let pm: ProcMesh = hm
2210 .spawn(instance, "test", extent!(gpus = 2), None, None)
2211 .await
2212 .unwrap();
2213 let actor_mesh: ActorMesh<testactor::TestActor> =
2214 pm.spawn(instance, "test", &()).await.unwrap();
2215
2216 {
2217 let host1_region = actor_mesh
2218 .region()
2219 .range("hosts", 1..2)
2220 .expect("host slice should exist");
2221 let host1 = actor_mesh.sliced(host1_region);
2222 testactor::assert_casting_correctness(&host1, instance, None).await;
2227
2228 {
2229 let host1_gpu1_region = host1
2230 .region()
2231 .range("gpus", 1..2)
2232 .expect("nested slice should exist");
2233 let host1_gpu1 = host1.sliced(host1_gpu1_region);
2234 testactor::assert_casting_correctness(&host1_gpu1, instance, None).await;
2235 }
2236 }
2237
2238 let _ = hm.shutdown(instance).await;
2239 }
2240
2241 async fn assert_slice_cast_points(
2242 actor_mesh: &ActorMeshRef<testactor::TestActor>,
2243 instance: &impl hyperactor::context::Actor,
2244 ) {
2245 let (port, mut rx) = mailbox::open_port(instance);
2246 actor_mesh
2247 .cast(
2248 instance,
2249 testactor::GetCastInfo {
2250 cast_info: port.bind(),
2251 },
2252 )
2253 .unwrap();
2254
2255 let mut expected: HashMap<_, _> = actor_mesh
2256 .values()
2257 .enumerate()
2258 .map(|(rank, actor_ref)| {
2259 (
2260 actor_ref.actor_addr().clone(),
2261 actor_mesh
2262 .extent()
2263 .point_of_rank(rank)
2264 .expect("rank must be in-bounds for slice extent"),
2265 )
2266 })
2267 .collect();
2268
2269 while !expected.is_empty() {
2270 let (point, actor_ref, _sender) =
2271 tokio::time::timeout(Duration::from_secs(3), rx.recv())
2272 .await
2273 .expect("timed out waiting for cast info")
2274 .expect("channel closed before receiving cast info");
2275 let expected_point = expected
2276 .remove(actor_ref.actor_addr())
2277 .expect("received cast info from unexpected actor");
2278 assert_eq!(
2279 point, expected_point,
2280 "cast point should be computed from slice-local rank and shape"
2281 );
2282 }
2283
2284 tokio::time::sleep(Duration::from_secs(1)).await;
2285 let result = rx.try_recv();
2286 assert!(result.as_ref().unwrap().is_none(), "got {result:?}");
2287 }
2288
2289 #[async_timed_test(timeout_secs = 60)]
2290 #[cfg(fbcode_build)]
2291 async fn test_actor_mesh_slice_cast_uses_slice_local_points() {
2292 let instance = testing::instance();
2293 let mut hm = testing::host_mesh(2).await;
2294 let pm: ProcMesh = hm
2295 .spawn(instance, "test", extent!(gpus = 2), None, None)
2296 .await
2297 .unwrap();
2298 let actor_mesh: ActorMesh<testactor::TestActor> =
2299 pm.spawn(instance, "test", &()).await.unwrap();
2300
2301 let host1_region = actor_mesh
2302 .region()
2303 .range("hosts", 1..2)
2304 .expect("host slice should exist");
2305 let host1 = actor_mesh.sliced(host1_region);
2306 assert_slice_cast_points(&host1, instance).await;
2307
2308 let host1_gpu1_region = host1
2309 .region()
2310 .range("gpus", 1..2)
2311 .expect("nested slice should exist");
2312 let host1_gpu1 = host1.sliced(host1_gpu1_region);
2313 assert_slice_cast_points(&host1_gpu1, instance).await;
2314
2315 let _ = hm.shutdown(instance).await;
2316 }
2317
2318 #[async_timed_test(timeout_secs = 300)]
2319 async fn test_actor_states_with_panic() {
2320 hyperactor_telemetry::initialize_logging_for_test();
2321
2322 let instance = testing::instance();
2323 let config = hyperactor_config::global::lock();
2324 let _mode = config.override_key(USE_DIRECT_SPAWN, false);
2325 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
2326 let _actor_spawn = config.override_key(ACTOR_SPAWN_MAX_IDLE, Duration::from_secs(120));
2327 let _host_spawn = config.override_key(
2328 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2329 Duration::from_secs(120),
2330 );
2331
2332 let (supervision_port, mut supervision_receiver) = instance.open_port::<MeshFailure>();
2334 let supervisor = supervision_port.bind();
2335 let num_replicas = 1;
2336 let mut hm = testing::host_mesh(num_replicas).await;
2337 let proc_mesh = hm
2338 .spawn(instance, "test", Extent::unity(), None, None)
2339 .await
2340 .unwrap();
2341 let child_name = ActorMeshId::instance(Label::new("child").unwrap());
2342
2343 let actor_mesh: ActorMesh<testactor::WrapperActor> = proc_mesh
2346 .spawn(
2347 instance,
2348 "wrapper",
2349 &(proc_mesh.deref().clone(), supervisor, child_name.clone()),
2350 )
2351 .await
2352 .unwrap();
2353
2354 actor_mesh
2356 .cast(
2357 instance,
2358 testactor::CauseSupervisionEvent {
2359 kind: testactor::SupervisionEventType::Panic,
2360 send_to_children: true,
2361 },
2362 )
2363 .unwrap();
2364
2365 let (failure_port, mut failure_receiver) = instance.open_port::<Option<MeshFailure>>();
2375 actor_mesh
2376 .cast(
2377 instance,
2378 testactor::NextSupervisionFailure(failure_port.bind()),
2379 )
2380 .unwrap();
2381 let failure = failure_receiver
2382 .recv()
2383 .await
2384 .unwrap()
2385 .expect("no supervision event found on ref from wrapper actor");
2386 let check_failure = move |failure: MeshFailure| {
2387 assert_eq!(failure.actor_mesh_name, Some(child_name.to_string()));
2388 assert!(
2389 failure
2390 .event
2391 .actor_id
2392 .label()
2393 .unwrap()
2394 .as_str()
2395 .starts_with(child_name.label().unwrap().as_str())
2396 );
2397 if let ActorStatus::Failed(ActorErrorKind::Generic(msg)) = &failure.event.actor_status {
2398 assert!(msg.contains("panic"), "{}", msg);
2399 assert!(msg.contains("for testing"), "{}", msg);
2400 } else {
2401 panic!("actor status is not failed: {}", failure.event.actor_status);
2402 }
2403 };
2404 check_failure(failure);
2405
2406 for _ in 0..num_replicas {
2410 let failure =
2411 tokio::time::timeout(Duration::from_secs(20), supervision_receiver.recv())
2412 .await
2413 .expect("timeout")
2414 .unwrap();
2415 check_failure(failure);
2416 }
2417
2418 let _ = hm.shutdown(instance).await;
2419 }
2420
2421 #[assert_no_process_leak]
2422 #[async_timed_test(timeout_secs = 300)]
2423 async fn test_actor_states_with_process_exit() {
2424 hyperactor_telemetry::initialize_logging_for_test();
2425
2426 let config = hyperactor_config::global::lock();
2427 let _mode = config.override_key(USE_DIRECT_SPAWN, false);
2428 let _poll = config.override_key(SUPERVISION_POLL_FREQUENCY, Duration::from_secs(1));
2429 let _guard = config.override_key(GET_ACTOR_STATE_MAX_IDLE, Duration::from_secs(1));
2430 let _proc_guard = config.override_key(GET_PROC_STATE_MAX_IDLE, Duration::from_secs(1));
2431 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
2432 let _host_spawn = config.override_key(
2433 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2434 Duration::from_secs(120),
2435 );
2436
2437 let instance = testing::instance();
2438 let (supervision_port, mut supervision_receiver) = instance.open_port::<MeshFailure>();
2440 let supervisor = supervision_port.bind();
2441 let num_replicas = 1;
2442 let mut hm = testing::host_mesh(num_replicas).await;
2443 let proc_mesh = hm
2444 .spawn(instance, "test", Extent::unity(), None, None)
2445 .await
2446 .unwrap();
2447 let mut second_hm = testing::host_mesh(num_replicas).await;
2448 let second_proc_mesh = second_hm
2449 .spawn(instance, "test2", Extent::unity(), None, None)
2450 .await
2451 .unwrap();
2452 let child_name = ActorMeshId::instance(Label::new("child").unwrap());
2453
2454 let actor_mesh: ActorMesh<testactor::WrapperActor> = proc_mesh
2457 .spawn(
2458 instance,
2459 "wrapper",
2460 &(
2461 second_proc_mesh.deref().clone(),
2464 supervisor,
2465 child_name.clone(),
2466 ),
2467 )
2468 .await
2469 .unwrap();
2470
2471 actor_mesh
2472 .cast(
2473 instance,
2474 testactor::CauseSupervisionEvent {
2475 kind: testactor::SupervisionEventType::ProcessExit(1),
2476 send_to_children: true,
2477 },
2478 )
2479 .unwrap();
2480
2481 let (failure_port, mut failure_receiver) = instance.open_port::<Option<MeshFailure>>();
2483 actor_mesh
2484 .cast(
2485 instance,
2486 testactor::NextSupervisionFailure(failure_port.bind()),
2487 )
2488 .unwrap();
2489 let failure = failure_receiver
2490 .recv()
2491 .await
2492 .unwrap()
2493 .expect("no supervision event found on ref from wrapper actor");
2494
2495 let check_failure = move |failure: MeshFailure| {
2496 assert_eq!(failure.actor_mesh_name, Some(child_name.to_string()));
2497 assert!(
2498 failure
2499 .event
2500 .actor_id
2501 .label()
2502 .unwrap()
2503 .as_str()
2504 .starts_with(child_name.label().unwrap().as_str())
2505 );
2506 if let ActorStatus::Failed(ActorErrorKind::Generic(msg)) = &failure.event.actor_status {
2507 assert!(msg.contains("exited with non-zero code 1"), "{}", msg);
2508 } else {
2509 panic!("actor status is not failed: {}", failure.event.actor_status);
2510 }
2511 };
2512 check_failure(failure);
2513
2514 for _ in 0..num_replicas {
2516 let failure =
2517 tokio::time::timeout(Duration::from_secs(20), supervision_receiver.recv())
2518 .await
2519 .expect("timeout")
2520 .unwrap();
2521 check_failure(failure);
2522 }
2523
2524 let _ = second_hm.shutdown(instance).await;
2525 let _ = hm.shutdown(instance).await;
2526 }
2527
2528 #[async_timed_test(timeout_secs = 300)]
2529 async fn test_actor_states_on_sliced_mesh() {
2530 hyperactor_telemetry::initialize_logging_for_test();
2531
2532 let instance = testing::instance();
2533 let (supervision_port, mut supervision_receiver) = instance.open_port::<MeshFailure>();
2535 let supervisor = supervision_port.bind();
2536 let (mut hm, _actor_mesh, sliced, sliced_replicas, child_name) = {
2537 let config = hyperactor_config::global::lock();
2538 let _mode = config.override_key(USE_DIRECT_SPAWN, false);
2539 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
2540 let _actor_spawn = config.override_key(ACTOR_SPAWN_MAX_IDLE, Duration::from_secs(120));
2541 let _host_spawn = config.override_key(
2542 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2543 Duration::from_secs(120),
2544 );
2545 let num_replicas = 2;
2546 let hm = testing::host_mesh(num_replicas).await;
2547 let proc_mesh = hm
2548 .spawn(instance, "test", Extent::unity(), None, None)
2549 .await
2550 .unwrap();
2551 let child_name = ActorMeshId::instance(Label::new("child").unwrap());
2552
2553 let actor_mesh: ActorMesh<testactor::WrapperActor> = proc_mesh
2556 .spawn(
2557 instance,
2558 "wrapper",
2559 &(proc_mesh.deref().clone(), supervisor, child_name.clone()),
2560 )
2561 .await
2562 .unwrap();
2563 let sliced = actor_mesh
2564 .range("hosts", 1..2)
2565 .expect("slice should be valid");
2566 let sliced_replicas = sliced.region().num_ranks();
2567 (hm, actor_mesh, sliced, sliced_replicas, child_name)
2568 };
2569
2570 sliced
2572 .cast(
2573 instance,
2574 testactor::CauseSupervisionEvent {
2575 kind: testactor::SupervisionEventType::Panic,
2576 send_to_children: true,
2577 },
2578 )
2579 .unwrap();
2580
2581 for _ in 0..sliced_replicas {
2582 let supervision_message =
2583 tokio::time::timeout(Duration::from_secs(20), supervision_receiver.recv())
2584 .await
2585 .expect("timeout")
2586 .unwrap();
2587 let event = supervision_message.event;
2588 assert!(
2589 event
2590 .actor_id
2591 .label()
2592 .unwrap()
2593 .as_str()
2594 .starts_with(child_name.label().unwrap().as_str())
2595 );
2596 if let ActorStatus::Failed(ActorErrorKind::Generic(msg)) = &event.actor_status {
2597 assert!(msg.contains("panic"));
2598 assert!(msg.contains("for testing"));
2599 } else {
2600 panic!("actor status is not failed: {}", event.actor_status);
2601 }
2602 }
2603
2604 let _ = hm.shutdown(instance).await;
2605 }
2606
2607 #[async_timed_test(timeout_secs = 30)]
2618 #[cfg(fbcode_build)]
2619 async fn test_streamed_supervision_uses_current_view_rank() {
2620 let instance = testing::instance();
2621 let (supervision_port, mut supervision_receiver) = instance.open_port::<MeshFailure>();
2622 let supervisor = supervision_port.bind();
2623
2624 let mut hm = local_host_mesh(1).await;
2625 let proc_mesh = hm
2626 .spawn(instance, "test", extent!(gpus = 2), None, None)
2627 .await
2628 .unwrap();
2629 let whole = proc_mesh.deref().clone();
2630 let slice = proc_mesh.range("gpus", 1..2).unwrap();
2632
2633 let child_id = ActorMeshId::singleton(Label::strip("svc_sup"));
2635
2636 let _whole_child: ActorMesh<testactor::TestActor> = whole
2640 .spawn_with_name(instance, child_id.clone(), &(), None, true)
2641 .await
2642 .unwrap();
2643
2644 let child_slice_region = _whole_child
2645 .region()
2646 .range("gpus", 1..2)
2647 .expect("child slice should exist");
2648 let child_slice = _whole_child.sliced(child_slice_region);
2649
2650 let (stream_port, mut stream_rx) =
2651 instance.open_port::<resource::RankedState<ActorState>>();
2652 slice
2653 .agent_mesh()
2654 .cast(
2655 instance,
2656 resource::StreamState::<ActorState> {
2657 id: child_id.resource_id().clone(),
2658 subscriber_rank: resource::Rank::default(),
2659 subscriber: stream_port.bind(),
2660 },
2661 )
2662 .unwrap();
2663 let streamed = tokio::time::timeout(Duration::from_secs(10), stream_rx.recv())
2664 .await
2665 .expect("no initial streamed state before timeout")
2666 .unwrap();
2667 assert_eq!(streamed.rank.unwrap(), 0);
2668 assert_eq!(
2669 streamed
2670 .state
2671 .state
2672 .as_ref()
2673 .expect("running actor should carry ActorState")
2674 .create_rank,
2675 1,
2676 );
2677
2678 let controller: ActorMeshController<testactor::TestActor> = ActorMeshController::new(
2679 ActorMeshControlPlane::new(child_slice.clone(), slice),
2680 Some("svc_sup".to_string()),
2681 Some(supervisor),
2682 crate::StatusMesh::from_single(child_slice.region().clone(), resource::Status::Running),
2683 );
2684 let controller_handle = instance.spawn_with_label("stream_rank_controller", controller);
2685 controller_handle
2686 .status()
2687 .wait_for(|status| matches!(status, ActorStatus::Idle))
2688 .await
2689 .unwrap();
2690 let controller: ActorRef<ActorMeshController<testactor::TestActor>> =
2691 controller_handle.bind();
2692
2693 let actor_id = child_slice
2694 .get(0)
2695 .expect("slice actor should exist")
2696 .actor_addr()
2697 .clone();
2698 let event = ActorSupervisionEvent::new(
2699 actor_id.clone(),
2700 None,
2701 ActorStatus::generic_failure("streamed test failure"),
2702 None,
2703 );
2704 controller.post(
2705 instance,
2706 resource::RankedState {
2707 rank: resource::Rank::new(0),
2708 state: resource::State {
2709 id: child_id.resource_id().clone(),
2710 status: resource::Status::Failed("streamed test failure".to_string()),
2711 state: Some(ActorState {
2712 actor_id,
2713 create_rank: 1,
2714 supervision_events: vec![event],
2715 }),
2716 generation: u64::MAX,
2717 timestamp: std::time::SystemTime::now(),
2718 },
2719 },
2720 );
2721
2722 let failure = tokio::time::timeout(Duration::from_secs(10), supervision_receiver.recv())
2723 .await
2724 .expect("no supervision failure delivered before timeout")
2725 .unwrap();
2726 assert_eq!(
2727 failure.crashed_ranks,
2728 vec![0],
2729 "failure attributed to slice-local rank 0: {failure:?}",
2730 );
2731 assert!(
2732 failure
2733 .actor_mesh_name
2734 .as_deref()
2735 .is_some_and(|name| name.contains("svc_sup")),
2736 "failure names the child mesh: {:?}",
2737 failure.actor_mesh_name,
2738 );
2739
2740 controller_handle.stop("test complete").unwrap();
2741 controller_handle
2742 .status()
2743 .wait_for(ActorStatus::is_terminal)
2744 .await
2745 .unwrap();
2746
2747 let _ = hm.shutdown(instance).await;
2748 }
2749
2750 async fn execute_cast(config: &hyperactor_config::global::ConfigLock) {
2751 let _guard = config.override_key(crate::bootstrap::MESH_BOOTSTRAP_ENABLE_PDEATHSIG, false);
2752 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2753 let _host_spawn = config.override_key(
2754 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2755 Duration::from_secs(60),
2756 );
2757
2758 let instance = testing::instance();
2759 let mut host_mesh = testing::host_mesh(2).await;
2760 let proc_mesh = host_mesh
2761 .spawn(instance, "test", Extent::unity(), None, None)
2762 .await
2763 .unwrap();
2764 let actor_mesh: ActorMesh<testactor::TestActor> =
2765 proc_mesh.spawn(instance, "test", &()).await.unwrap();
2766
2767 let (cast_info, mut cast_info_rx) = instance.mailbox().open_port();
2768 actor_mesh
2769 .cast(
2770 instance,
2771 testactor::GetCastInfo {
2772 cast_info: cast_info.bind(),
2773 },
2774 )
2775 .unwrap();
2776
2777 let mut point_to_actor: HashSet<_> = ndslice::ViewExt::iter(actor_mesh.deref()).collect();
2778 while !point_to_actor.is_empty() {
2779 let (point, origin_actor_ref, sender_actor_id) = cast_info_rx.recv().await.unwrap();
2780 let key = (point, origin_actor_ref);
2781 assert!(
2782 point_to_actor.remove(&key),
2783 "key {:?} not present or removed twice",
2784 key
2785 );
2786 assert_eq!(&sender_actor_id, instance.self_addr());
2787 }
2788
2789 let _ = host_mesh.shutdown(instance).await;
2790 }
2791
2792 #[async_timed_test(timeout_secs = 60)]
2793 async fn test_sliced_actor_mesh_cast_v1_reaches_slice_members() {
2794 let config = hyperactor_config::global::lock();
2795 let _guard = config.override_key(crate::bootstrap::MESH_BOOTSTRAP_ENABLE_PDEATHSIG, false);
2796 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2797 let _host_spawn = config.override_key(
2798 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2799 Duration::from_secs(60),
2800 );
2801
2802 let instance = testing::instance();
2803 let mut host_mesh = testing::host_mesh(2).await;
2804 let proc_mesh = host_mesh
2805 .spawn(instance, "test", Extent::unity(), None, None)
2806 .await
2807 .unwrap();
2808 let root_actor_mesh: ActorMesh<testactor::TestActor> =
2809 proc_mesh.spawn(instance, "test", &()).await.unwrap();
2810
2811 let actor_mesh = root_actor_mesh.sliced(Region::new(
2814 vec!["rank".to_string()],
2815 Slice::new(0, vec![1], vec![1]).unwrap(),
2816 ));
2817 let (cast_info, mut cast_info_rx) = instance.mailbox().open_port();
2818 actor_mesh
2819 .cast(
2820 instance,
2821 testactor::GetCastInfo {
2822 cast_info: cast_info.bind(),
2823 },
2824 )
2825 .unwrap();
2826
2827 let (point, _actor_ref, _sender) = cast_info_rx.recv().await.unwrap();
2828 let received_ranks = HashSet::from([point.rank()]);
2829 assert_eq!(received_ranks, HashSet::from([0]));
2830
2831 let (cast_info2, mut cast_info_rx2) = instance.mailbox().open_port();
2833 root_actor_mesh
2834 .cast(
2835 instance,
2836 testactor::GetCastInfo {
2837 cast_info: cast_info2.bind(),
2838 },
2839 )
2840 .unwrap();
2841
2842 let mut all_ranks: HashSet<usize> = HashSet::new();
2843 for _ in 0..2 {
2844 let (point, _actor_ref, _sender) = cast_info_rx2.recv().await.unwrap();
2845 all_ranks.insert(point.rank());
2846 }
2847 assert_eq!(all_ranks, HashSet::from([0, 1]));
2848
2849 let _ = host_mesh.shutdown(instance).await;
2850 }
2851
2852 #[async_timed_test(timeout_secs = 60)]
2853 async fn test_cast_domain_stamps_resource_rank_binding() {
2854 let client_proc = hyperactor::proc::Proc::direct(
2855 hyperactor::channel::ChannelTransport::Unix.any(),
2856 "client_proc".into(),
2857 )
2858 .unwrap();
2859
2860 let client = client_proc.client("client");
2861
2862 let mut procs = Vec::new();
2863
2864 let members = (0..2)
2865 .map(|rank| {
2866 let proc = hyperactor::proc::Proc::direct(
2867 hyperactor::channel::ChannelTransport::Unix.any(),
2868 format!("proc_{rank}"),
2869 )
2870 .unwrap();
2871
2872 let cast_handle = proc
2873 .spawn_with_uid(
2874 hyperactor::Uid::singleton(Label::strip("cast")),
2875 hyperactor_cast::cast_actor::CastActor::default(),
2876 )
2877 .unwrap();
2878
2879 let _: hyperactor::ActorRef<hyperactor_cast::cast_actor::CastActor> =
2880 cast_handle.bind();
2881
2882 let receiver_handle = proc
2883 .spawn_with_uid(
2884 hyperactor::Uid::singleton(Label::strip("receiver")),
2885 testactor::TestActor,
2886 )
2887 .unwrap();
2888
2889 let _: hyperactor::ActorRef<testactor::TestActor> = receiver_handle.bind();
2890
2891 let actor_addr =
2892 hyperactor::ActorAddr::root(proc.proc_addr().clone(), Label::strip("receiver"));
2893
2894 procs.push(proc);
2895
2896 (rank, actor_addr)
2897 })
2898 .collect::<HashMap<_, _>>();
2899
2900 let region = Region::from(ndslice::shape!(rank = 2));
2901 let destinations = CastDestination::mesh(
2902 region.clone(),
2903 (0..2).map(|rank| members[&rank].clone()).collect(),
2904 )
2905 .unwrap();
2906 let nodes = super::cast_node_mesh(&destinations).unwrap();
2907 let cast_domain = hyperactor_cast::cast_actor::CastDomainId::new()
2908 .materialize(
2909 &client,
2910 Arc::new(nodes),
2911 hyperactor_cast::cast_actor::TilingPolicy::BlockPartitioning,
2912 hyperactor_config::Flattrs::new(),
2913 )
2914 .unwrap();
2915
2916 let (rank_port, mut rank_rx) = client.mailbox().open_port();
2917
2918 cast_domain
2919 .cast(
2920 &client,
2921 hyperactor_config::Flattrs::new(),
2922 testactor::GetResourceRank {
2923 rank: crate::resource::Rank::default(),
2924 reply: rank_port.bind(),
2925 },
2926 )
2927 .unwrap();
2928
2929 let mut received_ranks = HashSet::new();
2930
2931 for _ in 0..2 {
2932 let (_point, rank) = rank_rx.recv().await.unwrap();
2933
2934 received_ranks.insert(rank);
2935 }
2936
2937 assert_eq!(received_ranks, HashSet::from([Some(0), Some(1)]));
2938 }
2939
2940 #[async_timed_test(timeout_secs = 30)]
2941 async fn test_cast() {
2942 let config = hyperactor_config::global::lock();
2943 execute_cast(&config).await;
2944 }
2945
2946 #[assert_no_process_leak]
2952 #[async_timed_test(timeout_secs = 60)]
2953 async fn test_undeliverable_message_return() {
2954 use hyperactor::mailbox::MessageEnvelope;
2955 use hyperactor::mailbox::Undeliverable;
2956 use hyperactor::testing::pingpong::PingPongActor;
2957 use hyperactor::testing::pingpong::PingPongMessage;
2958
2959 hyperactor_telemetry::initialize_logging_for_test();
2960
2961 let instance = testing::instance();
2962
2963 let (mut hm, proc_mesh) = {
2965 let config = hyperactor_config::global::lock();
2966 let _proc_spawn_guard =
2967 config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2968 let _host_spawn_guard = config.override_key(
2969 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2970 Duration::from_secs(60),
2971 );
2972 let hm = testing::host_mesh(2).await;
2973 let proc_mesh = hm
2974 .spawn(instance, "test", Extent::unity(), None, None)
2975 .await
2976 .unwrap();
2977 (hm, proc_mesh)
2978 };
2979
2980 let (undeliverable_port, mut undeliverable_rx) =
2982 instance.open_port::<Undeliverable<MessageEnvelope>>();
2983
2984 let ping_proc_mesh = proc_mesh.range("hosts", 0..1).unwrap();
2987 let pong_proc_mesh = proc_mesh.range("hosts", 1..2).unwrap();
2988
2989 let ping_mesh: ActorMesh<PingPongActor> = ping_proc_mesh
2990 .spawn(
2991 instance,
2992 "ping",
2993 &(Some(undeliverable_port.bind()), None, None),
2994 )
2995 .await
2996 .unwrap();
2997
2998 let mut pong_mesh: ActorMesh<PingPongActor> = pong_proc_mesh
2999 .spawn(instance, "pong", &(None, None, None))
3000 .await
3001 .unwrap();
3002
3003 let ping_handle = ping_mesh.values().next().unwrap();
3005 let pong_handle = pong_mesh.values().next().unwrap();
3006
3007 let (done_tx, done_rx) = instance.open_once_port();
3009 ping_handle.post(
3010 instance,
3011 PingPongMessage(2, pong_handle.clone(), done_tx.bind()),
3012 );
3013 assert!(
3014 done_rx.recv().await.unwrap(),
3015 "Initial ping-pong should work"
3016 );
3017
3018 pong_mesh
3020 .stop(instance, "test stop".to_string())
3021 .await
3022 .unwrap();
3023
3024 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
3026
3027 let config = hyperactor_config::global::lock();
3029 let _guard = config.override_key(
3030 hyperactor::config::MESSAGE_DELIVERY_TIMEOUT,
3031 std::time::Duration::from_secs(5),
3032 );
3033
3034 let n = 100usize;
3036 for i in 1..=n {
3037 let ttl = 66 + i as u64; let (once_tx, _once_rx) = instance.open_once_port();
3039 ping_handle.post(
3040 instance,
3041 PingPongMessage(ttl, pong_handle.clone(), once_tx.bind()),
3042 );
3043 }
3044
3045 let mut count = 0;
3049 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
3050 while count < n && tokio::time::Instant::now() < deadline {
3051 match tokio::time::timeout(std::time::Duration::from_secs(1), undeliverable_rx.recv())
3052 .await
3053 {
3054 Ok(Ok(Undeliverable::Returned(envelope))) => {
3055 let _: PingPongMessage = envelope.deserialized().unwrap();
3056 count += 1;
3057 }
3058 Ok(Ok(Undeliverable::Report(_))) => break,
3059 Ok(Err(_)) => break, Err(_) => break, }
3062 }
3063
3064 assert_eq!(
3065 count, n,
3066 "Expected {} undeliverable messages, got {}",
3067 n, count
3068 );
3069
3070 let _ = hm.shutdown(instance).await;
3071 }
3072
3073 #[async_timed_test(timeout_secs = 30)]
3083 async fn test_actor_mesh_stop_timeout() {
3084 hyperactor_telemetry::initialize_logging_for_test();
3085
3086 let config = hyperactor_config::global::lock();
3090 let _mode = config.override_key(USE_DIRECT_SPAWN, false);
3091 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
3092 let _host_spawn = config.override_key(
3093 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
3094 Duration::from_secs(60),
3095 );
3096
3097 let instance = testing::instance();
3098
3099 let mut hm = testing::host_mesh(2).await;
3101 let proc_mesh = hm
3102 .spawn(instance, "test", Extent::unity(), None, None)
3103 .await
3104 .unwrap();
3105
3106 let mut sleep_mesh: ActorMesh<testactor::SleepActor> =
3109 proc_mesh.spawn(instance, "sleepers", &()).await.unwrap();
3110 let _guard = config.override_key(ACTOR_SPAWN_MAX_IDLE, std::time::Duration::from_secs(1));
3111
3112 for actor_ref in sleep_mesh.values() {
3117 actor_ref.post(instance, std::time::Duration::from_secs(5));
3118 }
3119
3120 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
3122
3123 let expected_actors = sleep_mesh.values().count();
3125
3126 let stop_start = tokio::time::Instant::now();
3131 let result = sleep_mesh.stop(instance, "test stop".to_string()).await;
3132 let stop_duration = tokio::time::Instant::now().duration_since(stop_start);
3133
3134 match result {
3138 Ok(_) => {
3139 tracing::info!(
3140 "stop returned Ok for {} actors; their tokio tasks \
3141 may still be running until their handler yields",
3142 expected_actors
3143 );
3144 }
3145 Err(ref e) => {
3146 let err_str = format!("{:?}", e);
3147 assert!(
3148 err_str.contains("Timeout"),
3149 "Expected Timeout error, got: {:?}",
3150 e
3151 );
3152 }
3153 }
3154
3155 assert!(
3160 stop_duration < std::time::Duration::from_millis(4500),
3161 "Stop took {:?}, expected < 4.5s (controller should have given up waiting for Stopped)",
3162 stop_duration
3163 );
3164 assert!(
3165 stop_duration >= std::time::Duration::from_millis(900),
3166 "Stop took {:?}, expected >= 900ms (should have waited for the 1s idle timeout)",
3167 stop_duration
3168 );
3169
3170 let _ = hm.shutdown(instance).await;
3171 }
3172
3173 #[async_timed_test(timeout_secs = 60)]
3179 async fn test_actor_mesh_stop_graceful() {
3180 hyperactor_telemetry::initialize_logging_for_test();
3181
3182 let config = hyperactor_config::global::lock();
3183 let _mode = config.override_key(USE_DIRECT_SPAWN, false);
3184 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
3185 let _host_spawn = config.override_key(
3186 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
3187 Duration::from_secs(60),
3188 );
3189
3190 let instance = testing::instance();
3191
3192 let mut hm = testing::host_mesh(2).await;
3194 let proc_mesh = hm
3195 .spawn(instance, "test", Extent::unity(), None, None)
3196 .await
3197 .unwrap();
3198
3199 let mut actor_mesh: ActorMesh<testactor::TestActor> =
3202 proc_mesh.spawn(instance, "test_actors", &()).await.unwrap();
3203
3204 let mesh_ref = actor_mesh.deref().clone();
3207
3208 let expected_actors = actor_mesh.values().count();
3209 assert!(expected_actors > 0, "Should have spawned some actors");
3210
3211 let stop_start = tokio::time::Instant::now();
3213 let result = actor_mesh.stop(instance, "test stop".to_string()).await;
3214 let stop_duration = tokio::time::Instant::now().duration_since(stop_start);
3215
3216 assert!(
3218 result.is_ok(),
3219 "Stop should succeed for responsive actors, got: {:?}",
3220 result.err()
3221 );
3222
3223 assert!(
3227 stop_duration < std::time::Duration::from_secs(5),
3228 "Graceful stop took {:?}, expected < 5s (actors should stop quickly)",
3229 stop_duration
3230 );
3231
3232 tracing::info!(
3233 "Successfully stopped {} actors in {:?}",
3234 expected_actors,
3235 stop_duration
3236 );
3237
3238 let next_event = actor_mesh.next_supervision_event(instance).await.unwrap();
3244 let mesh_id = mesh_ref
3245 .as_managed()
3246 .expect("owned actor mesh ref should be managed")
3247 .id();
3248 assert_eq!(next_event.actor_mesh_name, Some(mesh_id.to_string()));
3249 assert!(matches!(
3250 next_event.event.actor_status,
3251 ActorStatus::Stopped(_)
3252 ));
3253 let next_event = mesh_ref.next_supervision_event(instance).await.unwrap();
3256 assert_eq!(next_event.actor_mesh_name, Some(mesh_id.to_string()));
3257 assert!(matches!(
3258 next_event.event.actor_status,
3259 ActorStatus::Stopped(_)
3260 ));
3261
3262 let _ = hm.shutdown(instance).await;
3263 }
3264}