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::Endpoint as _;
36use hyperactor::OncePortRefRepr;
37use hyperactor::PortRef;
38use hyperactor::PortRefRepr;
39use hyperactor::RemoteEndpoint as _;
40use hyperactor::RemoteHandles;
41use hyperactor::RemoteMessage;
42use hyperactor::accum::ReducerMode;
43use hyperactor::actor::ActorStatus;
44use hyperactor::actor::Referable;
45use hyperactor::context;
46use hyperactor::mailbox::PortReceiver;
47use hyperactor::port::Port;
48use hyperactor::supervision::ActorSupervisionEvent;
49use hyperactor_cast::TilingPolicy;
50use hyperactor_cast::cast_actor::CastDomainId;
51use hyperactor_cast::cast_actor::CastDomainRef;
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::Region;
59use ndslice::view::View;
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::comm::multicast;
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::proc_mesh::GET_ACTOR_STATE_MAX_IDLE;
80use crate::proc_mesh::telemetry_actor_mesh_id;
81use crate::resource;
82use crate::supervision::MeshFailure;
83use crate::supervision::Unhealthy;
84
85declare_attrs! {
86 @meta(CONFIG = ConfigAttr::new(
95 Some("HYPERACTOR_MESH_SUPERVISION_WATCHDOG_TIMEOUT".to_string()),
96 Some("supervision_watchdog_timeout".to_string()),
97 ))
98 pub attr SUPERVISION_WATCHDOG_TIMEOUT: Duration = Duration::from_mins(2);
99}
100
101#[derive(Debug)]
106pub struct ActorMesh<A: Referable> {
107 proc_mesh: ProcMeshRef,
108 id: ActorMeshId,
109 current_ref: ActorMeshRef<A>,
110 controller: Option<ActorRef<ActorMeshController<A>>>,
116}
117
118impl<A: Referable> ActorMesh<A> {
120 pub(crate) fn new(
121 proc_mesh: ProcMeshRef,
122 id: ActorMeshId,
123 controller: Option<ActorRef<ActorMeshController<A>>>,
124 members: Arc<ValueMesh<ActorAddr>>,
125 ) -> Self {
126 let current_ref = ActorMeshRef::new(
127 id.clone(),
128 Some(proc_mesh.id().clone()),
129 proc_mesh.region().clone(),
130 controller.clone(),
131 members,
132 );
133
134 Self {
135 proc_mesh,
136 id,
137 current_ref,
138 controller,
139 }
140 }
141
142 pub fn id(&self) -> &ActorMeshId {
143 &self.id
144 }
145
146 pub(crate) fn set_controller(&mut self, controller: Option<ActorRef<ActorMeshController<A>>>) {
147 self.controller = controller.clone();
148 self.current_ref.set_controller(controller);
149 }
150
151 pub async fn stop(&mut self, cx: &impl context::Actor, reason: String) -> crate::Result<()> {
153 if let Some(controller) = self.controller.take() {
159 let id = self.id.resource_id().clone();
167 let num_ranks = self.current_ref.region().num_ranks();
168 let result: crate::Result<()> = async {
169 controller.post(
170 cx,
171 resource::Stop {
172 id: id.clone(),
173 reason,
174 },
175 );
176 let (port, mut rx) = cx.mailbox().open_port();
186 controller.post(
187 cx,
188 resource::GetState::<resource::mesh::State<()>> {
189 id: id.clone(),
190 reply: port.bind(),
191 },
192 );
193 let statuses = rx.recv().await?;
194 let Some(state) = &statuses.state else {
195 return Err(Error::Other(anyhow::anyhow!(
196 "non-existent state in GetState reply from controller: {}",
197 controller.actor_addr()
198 )));
199 };
200 let all_terminating = state.statuses.values().all(|s| s.is_terminating());
207 if !all_terminating {
208 let legacy = mesh_to_rankedvalues_with_default(
209 &state.statuses,
210 resource::Status::NotExist,
211 resource::Status::is_not_exist,
212 num_ranks,
213 );
214 return Err(Error::ActorStopError { statuses: legacy });
215 }
216 Ok(())
217 }
218 .await;
219
220 let status = match &result {
226 Ok(()) => ActorStatus::Stopped("mesh stopped".to_string()),
227 Err(e) => ActorStatus::Stopped(format!("mesh stop failed: {e}")),
228 };
229 let mut entry = self.health_state.entry(cx).or_default();
230 let health_state = entry.get_mut();
231 health_state.unhealthy_event = Some(Unhealthy::StreamClosed(MeshFailure {
232 actor_mesh_name: Some(self.id().to_string()),
233 event: ActorSupervisionEvent::new(
234 ndslice::view::Ranked::get(&self.current_ref, 0)
236 .unwrap()
237 .actor_addr()
238 .clone(),
239 None,
240 status,
241 None,
242 ),
243 crashed_ranks: vec![],
244 }));
245
246 result?;
247 }
248 self.current_ref.controller.take();
251 Ok(())
252 }
253}
254
255impl<A: Referable> fmt::Display for ActorMesh<A> {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 write!(f, "{}", self.current_ref)
258 }
259}
260
261impl<A: Referable> Deref for ActorMesh<A> {
262 type Target = ActorMeshRef<A>;
263
264 fn deref(&self) -> &Self::Target {
265 &self.current_ref
266 }
267}
268
269impl<A: Referable> Clone for ActorMesh<A> {
272 fn clone(&self) -> Self {
273 Self {
274 proc_mesh: self.proc_mesh.clone(),
275 id: self.id.clone(),
276 current_ref: self.current_ref.clone(),
277 controller: self.controller.clone(),
278 }
279 }
280}
281
282impl<A: Referable> Drop for ActorMesh<A> {
283 fn drop(&mut self) {
284 tracing::info!(
285 name = "ActorMeshStatus",
286 actor_name = %self.id,
287 status = "Dropped",
288 );
289 }
290}
291
292const DEFAULT_PAGE: usize = 1024;
296
297struct Page<A: Referable> {
299 slots: Box<[OnceCell<ActorRef<A>>]>,
300}
301
302impl<A: Referable> Page<A> {
303 fn new(len: usize) -> Self {
304 let mut v = Vec::with_capacity(len);
305 for _ in 0..len {
306 v.push(OnceCell::new());
307 }
308 Self {
309 slots: v.into_boxed_slice(),
310 }
311 }
312}
313
314#[derive(Default)]
315struct HealthState {
316 unhealthy_event: Option<Unhealthy>,
317 crashed_ranks: HashMap<usize, ActorSupervisionEvent>,
318}
319
320impl HealthState {
321 fn failure_for_region(&self, region: &Region) -> Option<MeshFailure> {
322 let unhealthy = self.unhealthy_event.as_ref()?;
323 let mut failure = match unhealthy {
324 Unhealthy::StreamClosed(failure) | Unhealthy::Crashed(failure) => failure.clone(),
325 };
326 if failure.crashed_ranks.is_empty() {
327 return Some(failure);
328 }
329 let mut crashed_ranks = self
330 .crashed_ranks
331 .keys()
332 .copied()
333 .filter(|rank| region.slice().contains(*rank))
334 .collect::<Vec<_>>();
335 crashed_ranks.sort_unstable();
336 if crashed_ranks.is_empty() {
337 return None;
338 }
339 failure.crashed_ranks = crashed_ranks;
340 Some(failure)
341 }
342}
343
344impl std::fmt::Debug for HealthState {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.debug_struct("HealthState")
347 .field("unhealthy_event", &self.unhealthy_event)
348 .field("crashed_ranks", &self.crashed_ranks)
349 .finish()
350 }
351}
352
353#[derive(Clone)]
354enum MessageOrFailure<M: Send + Sync + Clone + Default + 'static> {
355 Message(M),
356 Failure(String),
359 Timeout,
360}
361
362impl<M: Send + Sync + Clone + Default + 'static> Default for MessageOrFailure<M> {
363 fn default() -> Self {
364 Self::Message(M::default())
365 }
366}
367
368fn default_cast_tiling_policy() -> TilingPolicy {
369 TilingPolicy::BoundedFanout {
370 fanout: hyperactor_config::global::get(MAX_CAST_FANOUT).into(),
371 }
372}
373
374#[derive(Clone)]
375struct ActorMeshCastDomain {
376 id: CastDomainId,
377 members: Arc<ValueMesh<ActorAddr>>,
378 region: Region,
379 tiling_policy: TilingPolicy,
380 cast_domain: ActorLocal<CastDomainRef>,
381}
382
383impl std::fmt::Debug for ActorMeshCastDomain {
384 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385 f.debug_struct("ActorMeshCastDomain")
386 .field("id", &self.id)
387 .field("members", &self.members)
388 .field("region", &self.region)
389 .field("tiling_policy", &self.tiling_policy)
390 .finish_non_exhaustive()
391 }
392}
393
394impl ActorMeshCastDomain {
395 fn new(members: Arc<ValueMesh<ActorAddr>>, region: Region) -> Self {
396 Self {
397 id: CastDomainId::new(),
398 members,
399 region,
400 tiling_policy: default_cast_tiling_policy(),
401 cast_domain: ActorLocal::new(),
402 }
403 }
404
405 fn ensure_materialized(
406 &self,
407 cx: &impl context::Actor,
408 headers: &Flattrs,
409 ) -> anyhow::Result<CastDomainRef> {
410 if let hyperactor::actor_local::Entry::Occupied(cast_domain) = self.cast_domain.entry(cx) {
411 return Ok(cast_domain.get().clone());
412 }
413
414 let members =
415 self.region
416 .slice()
417 .iter()
418 .map(|rank| {
419 let member = self.members.get_by_base_rank(rank).ok_or_else(|| {
420 anyhow::anyhow!("missing cast-domain member for rank {rank}")
421 })?;
422 Ok((rank, member.clone()))
423 })
424 .collect::<anyhow::Result<HashMap<_, _>>>()?;
425
426 let cast_domain = self.id.clone().materialize(
427 cx,
428 members,
429 self.region.clone(),
430 self.tiling_policy,
431 headers.clone(),
432 )?;
433
434 self.cast_domain.entry(cx).or_insert(cast_domain.clone());
435
436 Ok(cast_domain)
437 }
438
439 fn members(&self) -> &ValueMesh<ActorAddr> {
440 &self.members
441 }
442
443 fn region(&self) -> &Region {
444 &self.region
445 }
446}
447
448impl Serialize for ActorMeshCastDomain {
449 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
450 where
451 S: Serializer,
452 {
453 (&self.id, &self.members, &self.region, self.tiling_policy).serialize(serializer)
454 }
455}
456
457impl<'de> Deserialize<'de> for ActorMeshCastDomain {
458 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
459 where
460 D: Deserializer<'de>,
461 {
462 let (id, members, region, tiling_policy) = <(
463 CastDomainId,
464 Arc<ValueMesh<ActorAddr>>,
465 Region,
466 TilingPolicy,
467 )>::deserialize(deserializer)?;
468 Ok(Self {
469 id,
470 members,
471 region,
472 tiling_policy,
473 cast_domain: ActorLocal::new(),
474 })
475 }
476}
477
478fn into_watch<M: Send + Sync + Clone + Default + 'static>(
482 mut rx: PortReceiver<M>,
483) -> watch::Receiver<MessageOrFailure<M>> {
484 let (sender, receiver) = watch::channel(MessageOrFailure::<M>::default());
485 let timeout = hyperactor_config::global::get(SUPERVISION_WATCHDOG_TIMEOUT);
493 let poll_frequency = hyperactor_config::global::get(SUPERVISION_POLL_FREQUENCY);
494 let get_actor_state_max_idle = hyperactor_config::global::get(GET_ACTOR_STATE_MAX_IDLE);
495 let get_proc_state_max_idle = hyperactor_config::global::get(GET_PROC_STATE_MAX_IDLE);
496 let total_time = poll_frequency + get_actor_state_max_idle + get_proc_state_max_idle;
497 if timeout < total_time {
498 tracing::warn!(
499 "HYPERACTOR_MESH_SUPERVISION_WATCHDOG_TIMEOUT={} is too short. It should be >= {} (SUPERVISION_POLL_FREQUENCY={} + GET_ACTOR_STATE_MAX_IDLE={} + GET_PROC_STATE_MAX_IDLE={})",
500 humantime::format_duration(timeout),
501 humantime::format_duration(total_time),
502 humantime::format_duration(poll_frequency),
503 humantime::format_duration(get_actor_state_max_idle),
504 humantime::format_duration(get_proc_state_max_idle),
505 );
506 }
507 tokio::spawn(async move {
508 loop {
509 let message = match tokio::time::timeout(timeout, rx.recv()).await {
510 Ok(Ok(msg)) => MessageOrFailure::Message(msg),
511 Ok(Err(e)) => MessageOrFailure::Failure(e.to_string()),
512 Err(_) => MessageOrFailure::Timeout,
513 };
514 let is_failure = matches!(
515 message,
516 MessageOrFailure::Failure(_) | MessageOrFailure::Timeout
517 );
518 if sender.send(message).is_err() {
519 break;
521 }
522 if is_failure {
523 break;
525 }
526 }
527 });
528 receiver
529}
530
531#[derive(typeuri::Named)]
533pub struct ActorMeshRef<A: Referable> {
534 id: ActorMeshId,
535 proc_mesh_id: Option<ProcMeshId>,
540 controller: Option<ActorRef<ActorMeshController<A>>>,
547
548 cast_domain: ActorMeshCastDomain,
555 health_state: ActorLocal<HealthState>,
559 receiver: ActorLocal<
564 Arc<
565 tokio::sync::Mutex<(
566 PortRef<Option<MeshFailure>>,
567 watch::Receiver<MessageOrFailure<Option<MeshFailure>>>,
568 )>,
569 >,
570 >,
571 pages: OnceCell<Vec<OnceCell<Box<Page<A>>>>>,
581 page_size: usize,
583}
584
585impl<A: Referable> ActorMeshRef<A> {
586 fn cached_failure(&self, cx: &impl context::Actor) -> Option<MeshFailure> {
587 let health_state = self.health_state.entry(cx).or_default();
588 health_state
589 .get()
590 .failure_for_region(ndslice::view::Ranked::region(self))
591 }
592
593 #[allow(clippy::result_large_err)]
595 pub fn cast<M>(&self, cx: &impl context::Actor, message: M) -> crate::Result<()>
596 where
597 A: RemoteHandles<M>,
598 M: RemoteMessage + Clone, {
600 self.cast_with_headers(cx, &Flattrs::new(), message)
601 }
602
603 #[allow(clippy::result_large_err)]
610 pub fn cast_with_headers<M>(
611 &self,
612 cx: &impl context::Actor,
613 caller_headers: &Flattrs,
614 message: M,
615 ) -> crate::Result<()>
616 where
617 A: RemoteHandles<M>,
618 M: RemoteMessage + Clone,
619 {
620 self.check_cached_failure(cx)?;
621 self.emit_sent_message_telemetry(cx, view::Ranked::region(self));
622
623 let mut headers = caller_headers.clone();
624 headers.set(
625 multicast::CAST_ORIGINATING_SENDER,
626 cx.instance().self_addr().clone(),
627 );
628 headers.set(crate::casting::CAST_ACTOR_MESH_ID, self.id.clone());
629
630 let threshold =
631 hyperactor_config::global::get(crate::config::V1_CAST_POINT_TO_POINT_THRESHOLD);
632
633 let num_ranks = self.len();
634
635 match num_ranks {
636 0 => Ok(()),
637 1 if threshold >= 1 => {
638 let point = self
641 .cast_domain
642 .region()
643 .extent()
644 .point_of_rank(0)
645 .map_err(|err| Error::CastingError(self.id.clone(), err.into()))?;
646
647 let actor = self.materialize(0).ok_or_else(|| {
648 Error::CastingError(
649 self.id.clone(),
650 anyhow::anyhow!("missing actor for rank 0"),
651 )
652 })?;
653
654 self.post_cast_direct(cx, point, actor, message, &headers)
655 }
656 n if threshold > 0 && n < threshold => {
657 let sender = cx.instance().self_addr().clone();
661 let dest_port = M::port();
662 let mut data =
663 wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&message)
664 .expect("cast message serialization should not fail");
665
666 data.visit_multipart_parts_mut::<PortRefRepr, anyhow::Error>(|port| {
669 if port.unsplit() {
670 return Ok(());
671 }
672 let split = port.port_addr().split(
673 cx,
674 port.reducer_spec().clone(),
675 ReducerMode::Streaming(port.streaming_opts().clone()),
676 port.get_return_undeliverable(),
677 )?;
678 port.update_port_addr(split);
679 Ok(())
680 })
681 .map_err(|e| Error::CastingError(self.id.clone(), e))?;
682
683 data.visit_multipart_parts_mut::<OncePortRefRepr, anyhow::Error>(|port| {
684 if port.unsplit() || port.reducer_spec().is_none() {
685 return Ok(());
688 }
689 let split = port.port_addr().split(
690 cx,
691 port.reducer_spec().clone(),
692 ReducerMode::Once(n),
693 port.get_return_undeliverable(),
694 )?;
695 port.update_port_addr(split);
696 Ok(())
697 })
698 .map_err(|e| Error::CastingError(self.id.clone(), e))?;
699
700 for rank in 0..n {
701 let point = self
702 .cast_domain
703 .region()
704 .extent()
705 .point_of_rank(rank)
706 .map_err(|err| Error::CastingError(self.id.clone(), err.into()))?;
707
708 let actor = self.materialize(rank).ok_or_else(|| {
709 Error::CastingError(
710 self.id.clone(),
711 anyhow::anyhow!("missing actor for rank {rank}"),
712 )
713 })?;
714
715 let mut rank_data = data.clone();
716
717 rank_data
718 .visit_multipart_parts_mut::<resource::RankRepr, anyhow::Error>(
719 |resource::RankRepr(rank)| {
720 *rank = Some(point.rank());
721 Ok(())
722 },
723 )
724 .map_err(|e| Error::CastingError(self.id.clone(), e))?;
725
726 let mut rank_headers = headers.clone();
727
728 multicast::set_cast_info_on_headers(&mut rank_headers, point, sender.clone());
729
730 cx.instance().post(
731 actor
732 .actor_addr()
733 .port_addr(Port::handler_id(dest_port, None)),
734 rank_headers,
735 rank_data.erase_encoding(),
736 );
737 }
738
739 Ok(())
740 }
741 _ => self
742 .cast_domain
743 .ensure_materialized(cx, &headers)
744 .map_err(|e| Error::CastingError(self.id.clone(), e))?
745 .cast(cx, headers, message)
746 .map_err(|e| Error::CastingError(self.id.clone(), e)),
747 }
748 }
749
750 #[allow(clippy::result_large_err)]
753 pub fn cast_choose_with_headers<M>(
754 &self,
755 cx: &impl context::Actor,
756 caller_headers: &Flattrs,
757 message: M,
758 ) -> crate::Result<()>
759 where
760 A: RemoteHandles<M>,
761 M: RemoteMessage + Clone,
762 {
763 self.check_cached_failure(cx)?;
764 self.emit_sent_message_telemetry(
765 cx,
766 &Region::new(
767 Vec::new(),
768 ndslice::Slice::new(0, Vec::new(), Vec::new())
769 .expect("zero-dimensional slice is valid"),
770 ),
771 );
772
773 let num_ranks = self.cast_domain.region().num_ranks();
774
775 if num_ranks == 0 {
776 return Ok(());
777 }
778
779 let rank_index = rand::random::<u64>() as usize % num_ranks;
780
781 let point = self
782 .cast_domain
783 .region()
784 .extent()
785 .point_of_rank(rank_index)
786 .map_err(|err| Error::CastingError(self.id.clone(), err.into()))?;
787
788 let actor = self.materialize(rank_index).ok_or_else(|| {
789 Error::CastingError(
790 self.id.clone(),
791 anyhow::anyhow!("missing actor for chosen rank {rank_index}"),
792 )
793 })?;
794
795 self.post_cast_direct(cx, point, actor, message, caller_headers)
796 }
797
798 #[allow(clippy::result_large_err)]
799 fn check_cached_failure(&self, cx: &impl context::Actor) -> crate::Result<()> {
800 if let Some(failure) = self.cached_failure(cx) {
803 tracing::debug!(
804 actor_mesh = %self.id,
805 crashed_ranks = ?failure.crashed_ranks,
806 "rejecting cast due to cached supervision failure"
807 );
808 return Err(crate::Error::Supervision(Box::new(failure)));
809 }
810
811 Ok(())
812 }
813
814 fn emit_sent_message_telemetry(&self, cx: &impl context::Actor, region: &Region) {
815 hyperactor_telemetry::notify_sent_message(hyperactor_telemetry::SentMessageEvent {
816 timestamp: std::time::SystemTime::now(),
817 sender_actor_id: hyperactor_telemetry::hash_to_u64(cx.mailbox().actor_addr().id()),
818 actor_mesh_id: match &self.proc_mesh_id {
819 Some(proc_mesh_id) => telemetry_actor_mesh_id(proc_mesh_id, &self.id),
820 None => hyperactor_telemetry::hash_to_u64(&self.id),
823 },
824 view_json: serde_json::to_string(region).unwrap_or_default(),
825 shape_json: {
826 let shape: ndslice::Shape = region.into();
827 serde_json::to_string(&shape).unwrap_or_default()
828 },
829 });
830 }
831
832 #[allow(clippy::result_large_err)]
833 fn post_cast_direct<M>(
834 &self,
835 cx: &impl context::Actor,
836 point: ndslice::Point,
837 actor: &ActorRef<A>,
838 message: M,
839 caller_headers: &Flattrs,
840 ) -> crate::Result<()>
841 where
842 A: RemoteHandles<M>,
843 M: RemoteMessage,
844 {
845 let create_rank = point.rank();
846 let mut headers = caller_headers.clone();
847 multicast::set_cast_info_on_headers(&mut headers, point, cx.instance().self_addr().clone());
848
849 let mut data = wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&message)
852 .map_err(|e| Error::CastingError(self.id.clone(), e.into()))?;
853 data.visit_multipart_parts_mut::<resource::RankRepr, anyhow::Error>(
854 |resource::RankRepr(rank)| {
855 *rank = Some(create_rank);
856 Ok(())
857 },
858 )
859 .map_err(|e| Error::CastingError(self.id.clone(), e))?;
860 let rebound_message = data
861 .deserialized_unchecked()
862 .map_err(|e| Error::CastingError(self.id.clone(), e.into()))?;
863 actor.post_with_headers(cx, headers, rebound_message);
864 Ok(())
865 }
866
867 pub(crate) fn new(
868 id: ActorMeshId,
869 proc_mesh_id: Option<ProcMeshId>,
870 region: Region,
871 controller: Option<ActorRef<ActorMeshController<A>>>,
872 members: Arc<ValueMesh<ActorAddr>>,
873 ) -> Self {
874 Self::with_page_size(id, proc_mesh_id, region, DEFAULT_PAGE, controller, members)
875 }
876
877 pub fn id(&self) -> &ActorMeshId {
878 &self.id
879 }
880
881 pub(crate) fn with_page_size(
882 id: ActorMeshId,
883 proc_mesh_id: Option<ProcMeshId>,
884 region: Region,
885 page_size: usize,
886 controller: Option<ActorRef<ActorMeshController<A>>>,
887 members: Arc<ValueMesh<ActorAddr>>,
888 ) -> Self {
889 Self::with_cast_domain(
890 id,
891 proc_mesh_id,
892 controller,
893 ActorMeshCastDomain::new(members, region),
894 page_size,
895 )
896 }
897
898 fn with_cast_domain(
899 id: ActorMeshId,
900 proc_mesh_id: Option<ProcMeshId>,
901 controller: Option<ActorRef<ActorMeshController<A>>>,
902 cast_domain: ActorMeshCastDomain,
903 page_size: usize,
904 ) -> Self {
905 Self {
906 id,
907 proc_mesh_id,
908 controller,
909 cast_domain,
910 health_state: ActorLocal::new(),
911 receiver: ActorLocal::new(),
912 pages: OnceCell::new(),
913 page_size: page_size.max(1),
914 }
915 }
916
917 #[inline]
918 fn len(&self) -> usize {
919 self.cast_domain.region().num_ranks()
920 }
921
922 pub fn controller(&self) -> &Option<ActorRef<ActorMeshController<A>>> {
923 &self.controller
924 }
925
926 fn set_controller(&mut self, controller: Option<ActorRef<ActorMeshController<A>>>) {
927 self.controller = controller;
928 }
929
930 fn ensure_pages(&self) -> &Vec<OnceCell<Box<Page<A>>>> {
931 let n = self.len().div_ceil(self.page_size); self.pages
933 .get_or_init(|| (0..n).map(|_| OnceCell::new()).collect())
934 }
935
936 fn materialize(&self, rank: usize) -> Option<&ActorRef<A>> {
937 let len = self.len();
938 if rank >= len {
939 return None;
940 }
941 let cast_domain = &self.cast_domain;
942 let p = self.page_size;
943 let page_ix = rank / p;
944 let local_ix = rank % p;
945
946 let pages = self.ensure_pages();
947 let page = pages[page_ix].get_or_init(|| {
948 let base = page_ix * p;
950 let remaining = len - base;
951 let page_len = remaining.min(p);
952 Box::new(Page::<A>::new(page_len))
953 });
954
955 Some(page.slots[local_ix].get_or_init(|| {
956 debug_assert!(rank < self.len(), "rank must be within [0, len)");
959 ActorRef::attest(
960 view::Ranked::get(cast_domain.members(), rank)
961 .expect("rank must be present in cast-domain member map")
962 .clone(),
963 )
964 }))
965 }
966
967 fn init_supervision_receiver(
968 controller: &ActorRef<ActorMeshController<A>>,
969 cx: &impl context::Actor,
970 ) -> (
971 PortRef<Option<MeshFailure>>,
972 watch::Receiver<MessageOrFailure<Option<MeshFailure>>>,
973 ) {
974 let (tx, rx) = cx.mailbox().open_port();
975 let tx = tx.bind();
976 controller.post(cx, Subscribe(tx.clone()));
977 (tx, into_watch(rx))
978 }
979
980 pub async fn next_supervision_event(
987 &self,
988 cx: &impl context::Actor,
989 ) -> Result<MeshFailure, anyhow::Error> {
990 if let Some(failure) = self.cached_failure(cx) {
991 tracing::debug!(
992 actor_mesh = %self.id,
993 crashed_ranks = ?failure.crashed_ranks,
994 "returning cached supervision failure"
995 );
996 return Ok(failure);
997 }
998 let controller = if let Some(c) = self.controller() {
999 c
1000 } else {
1001 return Err(anyhow::anyhow!(
1002 "unexpected healthy state while controller is gone"
1003 ));
1004 };
1005 let rx = {
1006 let entry = self.receiver.entry(cx).or_insert_with(|| {
1008 Arc::new(tokio::sync::Mutex::new(Self::init_supervision_receiver(
1009 controller, cx,
1010 )))
1011 });
1012 Arc::clone(entry.get())
1015 };
1016 let message = {
1017 let mut rx = rx.lock().await;
1018 let subscriber_port = rx.0.clone();
1019 let message =
1020 rx.1.wait_for(|message| {
1021 if let MessageOrFailure::Message(message) = message {
1025 if let Some(message) = &message {
1026 let region = ndslice::view::Ranked::region(self).slice();
1027 if message.crashed_ranks.is_empty() {
1028 true
1030 } else {
1031 message.crashed_ranks.iter().any(|r| region.contains(*r))
1034 }
1035 } else {
1036 false
1040 }
1041 } else {
1042 true
1044 }
1045 })
1046 .await?;
1047 let message = message.clone();
1048 let is_failure = matches!(
1049 message,
1050 MessageOrFailure::Failure(_) | MessageOrFailure::Timeout
1051 );
1052 if is_failure {
1053 let mut port = controller.port();
1058 port.return_undeliverable(false);
1060 let _ = port.post(cx, Unsubscribe(subscriber_port));
1061 }
1062 match message {
1066 MessageOrFailure::Message(message) => Ok::<MeshFailure, anyhow::Error>(
1067 message.expect("filter excludes any None messages"),
1068 ),
1069 MessageOrFailure::Failure(failure) => Err(anyhow::anyhow!("{}", failure)),
1070 MessageOrFailure::Timeout => {
1071 Ok(MeshFailure {
1074 actor_mesh_name: Some(self.id().to_string()),
1075 event: ActorSupervisionEvent::new(
1076 controller.actor_addr().clone(),
1077 None,
1078 ActorStatus::generic_failure(format!(
1079 "timed out reaching controller {} for mesh {}. Assuming controller's proc is dead",
1080 controller.actor_addr(),
1081 self.id()
1082 )),
1083 None,
1084 ),
1085 crashed_ranks: vec![],
1086 })
1087 }
1088 }?
1089 };
1090 let event = &message.event;
1092 let mut entry = self.health_state.entry(cx).or_default();
1094 let health_state = entry.get_mut();
1095 if let ActorStatus::Failed(_) = event.actor_status {
1096 for &rank in &message.crashed_ranks {
1097 health_state.crashed_ranks.insert(rank, event.clone());
1098 }
1099 }
1100 health_state.unhealthy_event = match &event.actor_status {
1101 ActorStatus::Failed(_) => Some(Unhealthy::Crashed(message.clone())),
1102 ActorStatus::Stopped(_) => Some(Unhealthy::StreamClosed(message.clone())),
1103 _ => None,
1104 };
1105 Ok(message)
1106 }
1107
1108 pub fn clone_with_supervision_receiver(&self) -> Self {
1112 Self {
1113 id: self.id.clone(),
1114 proc_mesh_id: self.proc_mesh_id.clone(),
1115 controller: self.controller.clone(),
1116 cast_domain: self.cast_domain.clone(),
1117 health_state: self.health_state.clone(),
1118 receiver: self.receiver.clone(),
1119 pages: OnceCell::new(),
1121 page_size: self.page_size,
1122 }
1123 }
1124}
1125
1126impl<A: Referable> Clone for ActorMeshRef<A> {
1127 fn clone(&self) -> Self {
1128 Self {
1129 id: self.id.clone(),
1130 proc_mesh_id: self.proc_mesh_id.clone(),
1131 controller: self.controller.clone(),
1132 cast_domain: self.cast_domain.clone(),
1133 health_state: ActorLocal::new(),
1136 receiver: ActorLocal::new(),
1137 pages: OnceCell::new(), page_size: self.page_size,
1139 }
1140 }
1141}
1142
1143impl<A: Referable> fmt::Display for ActorMeshRef<A> {
1144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1145 write!(
1146 f,
1147 "{}:{}@{}",
1148 self.id,
1149 A::typename(),
1150 self.cast_domain.region()
1151 )
1152 }
1153}
1154
1155impl<A: Referable> PartialEq for ActorMeshRef<A> {
1156 fn eq(&self, other: &Self) -> bool {
1157 self.id == other.id && self.cast_domain.region() == other.cast_domain.region()
1165 }
1166}
1167impl<A: Referable> Eq for ActorMeshRef<A> {}
1168
1169impl<A: Referable> Hash for ActorMeshRef<A> {
1170 fn hash<H: Hasher>(&self, state: &mut H) {
1171 self.id.hash(state);
1172 self.cast_domain.region().hash(state);
1173 }
1174}
1175
1176impl<A: Referable> fmt::Debug for ActorMeshRef<A> {
1177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1178 f.debug_struct("ActorMeshRef")
1179 .field("region", self.cast_domain.region())
1180 .field("id", &self.id)
1181 .field("page_size", &self.page_size)
1182 .finish_non_exhaustive() }
1184}
1185
1186impl<A: Referable> Serialize for ActorMeshRef<A> {
1188 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1189 where
1190 S: Serializer,
1191 {
1192 (
1194 &self.id,
1195 &self.proc_mesh_id,
1196 &self.controller,
1197 &self.cast_domain,
1198 )
1199 .serialize(serializer)
1200 }
1201}
1202
1203impl<'de, A: Referable> Deserialize<'de> for ActorMeshRef<A> {
1205 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1206 where
1207 D: Deserializer<'de>,
1208 {
1209 let (id, proc_mesh_id, controller, cast_domain) = <(
1210 ActorMeshId,
1211 Option<ProcMeshId>,
1212 Option<ActorRef<ActorMeshController<A>>>,
1213 ActorMeshCastDomain,
1214 )>::deserialize(deserializer)?;
1215 Ok(Self::with_cast_domain(
1216 id,
1217 proc_mesh_id,
1218 controller,
1219 cast_domain,
1220 DEFAULT_PAGE,
1221 ))
1222 }
1223}
1224
1225impl<A: Referable> view::Ranked for ActorMeshRef<A> {
1226 type Item = ActorRef<A>;
1227
1228 #[inline]
1229 fn region(&self) -> &Region {
1230 self.cast_domain.region()
1231 }
1232
1233 #[inline]
1234 fn get(&self, rank: usize) -> Option<&Self::Item> {
1235 self.materialize(rank)
1236 }
1237}
1238
1239impl<A: Referable> view::RankedSliceable for ActorMeshRef<A> {
1240 fn sliced(&self, region: Region) -> Self {
1247 debug_assert!(region.is_subset(view::Ranked::region(self)));
1252 Self {
1253 id: self.id.clone(),
1254 proc_mesh_id: self.proc_mesh_id.clone(),
1255 controller: self.controller.clone(),
1256 cast_domain: ActorMeshCastDomain::new(
1257 Arc::new(self.cast_domain.members().sliced(region.clone())),
1258 region.clone(),
1259 ),
1260 health_state: self.health_state.clone(),
1261 receiver: ActorLocal::new(),
1262 pages: OnceCell::new(),
1263 page_size: self.page_size,
1264 }
1265 }
1266}
1267
1268#[cfg(all(test, fbcode_build))]
1269mod tests {
1270
1271 use std::collections::HashMap;
1272 use std::collections::HashSet;
1273 use std::ops::Deref;
1274 use std::sync::Arc;
1275
1276 use hyperactor::Endpoint as _;
1277 use hyperactor::actor::ActorErrorKind;
1278 use hyperactor::actor::ActorStatus;
1279 use hyperactor::context::Mailbox as _;
1280 use hyperactor::id::Label;
1281 use hyperactor::mailbox;
1282 use ndslice::Extent;
1283 use ndslice::Region;
1284 use ndslice::Slice;
1285 use ndslice::ViewExt;
1286 use ndslice::extent;
1287 use ndslice::view::Ranked;
1288 use ndslice::view::RankedSliceable;
1289 use timed_test::assert_no_process_leak;
1290 use timed_test::async_timed_test;
1291 use tokio::time::Duration;
1292
1293 use super::ActorMesh;
1294 use crate::ActorMeshRef;
1295 use crate::ProcMesh;
1296 use crate::host_mesh::GET_PROC_STATE_MAX_IDLE;
1297 use crate::host_mesh::PROC_SPAWN_MAX_IDLE;
1298 use crate::mesh_controller::SUPERVISION_POLL_FREQUENCY;
1299 use crate::mesh_id::ActorMeshId;
1300 use crate::proc_mesh::ACTOR_SPAWN_MAX_IDLE;
1301 use crate::proc_mesh::GET_ACTOR_STATE_MAX_IDLE;
1302 use crate::supervision::MeshFailure;
1303 use crate::testactor;
1304 use crate::testing;
1305
1306 #[test]
1307 fn test_actor_mesh_ref_is_send_and_sync() {
1308 fn assert_send_sync<T: Send + Sync>() {}
1309 assert_send_sync::<ActorMeshRef<()>>();
1310 }
1311
1312 #[tokio::test]
1313 async fn test_actor_mesh_ref_lazy_materialization() {
1314 let instance = testing::instance();
1316 let mut hm = testing::host_mesh(2).await;
1319 let pm: ProcMesh = hm
1320 .spawn(instance, "test", extent!(gpus = 2), None, None)
1321 .await
1322 .unwrap();
1323 let am: ActorMesh<testactor::TestActor> = pm.spawn(instance, "test", &()).await.unwrap();
1324
1325 let page_size = 2;
1329 let amr: ActorMeshRef<testactor::TestActor> = ActorMeshRef::with_page_size(
1330 am.id.clone(),
1331 am.deref().proc_mesh_id.clone(),
1332 am.region().clone(),
1333 page_size,
1334 None,
1335 Arc::clone(&am.deref().cast_domain.members),
1336 );
1337 assert_eq!(amr.extent(), extent!(hosts = 2, gpus = 2));
1338 assert_eq!(amr.region().num_ranks(), 4);
1339
1340 let p0_a = amr.get(0).expect("rank 0 exists") as *const _;
1342 let p0_b = amr.get(0).expect("rank 0 exists") as *const _;
1343 assert_eq!(p0_a, p0_b, "same rank should return same cached pointer");
1344
1345 let p1_a = amr.get(1).expect("rank 1 exists") as *const _;
1347 let p1_b = amr.get(1).expect("rank 1 exists") as *const _;
1348 assert_eq!(p1_a, p1_b, "same rank should return same cached pointer");
1349 assert_ne!(p0_a, p1_a, "different ranks have different cache slots");
1352
1353 let p2_a = amr.get(2).expect("rank 2 exists") as *const _;
1355 let p2_b = amr.get(2).expect("rank 2 exists") as *const _;
1356 assert_eq!(p2_a, p2_b, "same rank should return same cached pointer");
1357 assert_ne!(p0_a, p2_a, "different pages have different cache slots");
1358
1359 let amr_clone = amr.clone();
1361 let orig_id_0 = amr.get(0).unwrap().actor_addr().clone();
1362 let clone_id_0 = amr_clone.get(0).unwrap().actor_addr().clone();
1363 assert_eq!(orig_id_0, clone_id_0, "clone preserves identity");
1364 let p0_clone = amr_clone.get(0).unwrap() as *const _;
1365 assert_ne!(
1366 p0_a, p0_clone,
1367 "cloned ActorMeshRef has a fresh cache (different pointer)"
1368 );
1369
1370 let sliced = amr.range("hosts", 0..2).expect("slice should be valid"); assert_eq!(sliced.region().num_ranks(), 4);
1374 assert!(
1375 sliced.get(0).is_some(),
1376 "RankedSliceable::sliced preserves a lazy cast-domain descriptor"
1377 );
1378 let sp0_a = sliced.get(0).unwrap() as *const _;
1380 let sp0_b = sliced.get(0).unwrap() as *const _;
1381 assert_eq!(sp0_a, sp0_b, "sliced view has its own cache slot per rank");
1382 let sp2 = sliced.get(2).unwrap() as *const _;
1385 assert_ne!(sp0_a, sp2, "sliced view crosses its own page boundary");
1386
1387 let mut set = HashSet::new();
1390 set.insert(amr.clone());
1391 set.insert(amr.clone());
1392 assert_eq!(set.len(), 1, "cache state must not affect Hash/Eq");
1393
1394 let (port, mut rx) = mailbox::open_port(instance);
1397 amr.get(0)
1400 .expect("rank 0 exists")
1401 .post(instance, testactor::GetActorId(port.bind()));
1402 amr.get(3)
1403 .expect("rank 3 exists")
1404 .post(instance, testactor::GetActorId(port.bind()));
1405 let id_a = tokio::time::timeout(Duration::from_secs(3), rx.recv())
1406 .await
1407 .expect("timed out waiting for first reply")
1408 .expect("channel closed before first reply");
1409 let id_b = tokio::time::timeout(Duration::from_secs(3), rx.recv())
1410 .await
1411 .expect("timed out waiting for second reply")
1412 .expect("channel closed before second reply");
1413 assert_ne!(id_a, id_b, "two different ranks responded");
1414
1415 let _ = hm.shutdown(instance).await;
1416 }
1417
1418 #[async_timed_test(timeout_secs = 300)]
1419 #[cfg(fbcode_build)]
1420 async fn test_actor_mesh_slice_casts_only_to_slice_members() {
1421 let instance = testing::instance();
1422 let mut hm = testing::host_mesh(2).await;
1423 let pm: ProcMesh = hm
1424 .spawn(instance, "test", extent!(gpus = 2), None, None)
1425 .await
1426 .unwrap();
1427 let actor_mesh: ActorMesh<testactor::TestActor> =
1428 pm.spawn(instance, "test", &()).await.unwrap();
1429
1430 {
1431 let host1_region = actor_mesh
1432 .region()
1433 .range("hosts", 1..2)
1434 .expect("host slice should exist");
1435 let host1 = actor_mesh.sliced(host1_region);
1436 testactor::assert_casting_correctness(&host1, instance, None).await;
1441
1442 {
1443 let host1_gpu1_region = host1
1444 .region()
1445 .range("gpus", 1..2)
1446 .expect("nested slice should exist");
1447 let host1_gpu1 = host1.sliced(host1_gpu1_region);
1448 testactor::assert_casting_correctness(&host1_gpu1, instance, None).await;
1449 }
1450 }
1451
1452 let _ = hm.shutdown(instance).await;
1453 }
1454
1455 async fn assert_slice_cast_points(
1456 actor_mesh: &ActorMeshRef<testactor::TestActor>,
1457 instance: &impl hyperactor::context::Actor,
1458 ) {
1459 let (port, mut rx) = mailbox::open_port(instance);
1460 actor_mesh
1461 .cast(
1462 instance,
1463 testactor::GetCastInfo {
1464 cast_info: port.bind(),
1465 },
1466 )
1467 .unwrap();
1468
1469 let mut expected: HashMap<_, _> = actor_mesh
1470 .values()
1471 .enumerate()
1472 .map(|(rank, actor_ref)| {
1473 (
1474 actor_ref.actor_addr().clone(),
1475 actor_mesh
1476 .extent()
1477 .point_of_rank(rank)
1478 .expect("rank must be in-bounds for slice extent"),
1479 )
1480 })
1481 .collect();
1482
1483 while !expected.is_empty() {
1484 let (point, actor_ref, _sender) =
1485 tokio::time::timeout(Duration::from_secs(3), rx.recv())
1486 .await
1487 .expect("timed out waiting for cast info")
1488 .expect("channel closed before receiving cast info");
1489 let expected_point = expected
1490 .remove(actor_ref.actor_addr())
1491 .expect("received cast info from unexpected actor");
1492 assert_eq!(
1493 point, expected_point,
1494 "cast point should be computed from slice-local rank and shape"
1495 );
1496 }
1497
1498 tokio::time::sleep(Duration::from_secs(1)).await;
1499 let result = rx.try_recv();
1500 assert!(result.as_ref().unwrap().is_none(), "got {result:?}");
1501 }
1502
1503 #[async_timed_test(timeout_secs = 60)]
1504 #[cfg(fbcode_build)]
1505 async fn test_actor_mesh_slice_cast_uses_slice_local_points() {
1506 let instance = testing::instance();
1507 let mut hm = testing::host_mesh(2).await;
1508 let pm: ProcMesh = hm
1509 .spawn(instance, "test", extent!(gpus = 2), None, None)
1510 .await
1511 .unwrap();
1512 let actor_mesh: ActorMesh<testactor::TestActor> =
1513 pm.spawn(instance, "test", &()).await.unwrap();
1514
1515 let host1_region = actor_mesh
1516 .region()
1517 .range("hosts", 1..2)
1518 .expect("host slice should exist");
1519 let host1 = actor_mesh.sliced(host1_region);
1520 assert_slice_cast_points(&host1, instance).await;
1521
1522 let host1_gpu1_region = host1
1523 .region()
1524 .range("gpus", 1..2)
1525 .expect("nested slice should exist");
1526 let host1_gpu1 = host1.sliced(host1_gpu1_region);
1527 assert_slice_cast_points(&host1_gpu1, instance).await;
1528
1529 let _ = hm.shutdown(instance).await;
1530 }
1531
1532 #[async_timed_test(timeout_secs = 300)]
1533 async fn test_actor_states_with_panic() {
1534 hyperactor_telemetry::initialize_logging_for_test();
1535
1536 let instance = testing::instance();
1537 let config = hyperactor_config::global::lock();
1538 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
1539 let _actor_spawn = config.override_key(ACTOR_SPAWN_MAX_IDLE, Duration::from_secs(120));
1540 let _host_spawn = config.override_key(
1541 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1542 Duration::from_secs(120),
1543 );
1544
1545 let (supervision_port, mut supervision_receiver) = instance.open_port::<MeshFailure>();
1547 let supervisor = supervision_port.bind();
1548 let num_replicas = 1;
1549 let mut hm = testing::host_mesh(num_replicas).await;
1550 let proc_mesh = hm
1551 .spawn(instance, "test", Extent::unity(), None, None)
1552 .await
1553 .unwrap();
1554 let child_name = ActorMeshId::instance(Label::new("child").unwrap());
1555
1556 let actor_mesh: ActorMesh<testactor::WrapperActor> = proc_mesh
1559 .spawn(
1560 instance,
1561 "wrapper",
1562 &(proc_mesh.deref().clone(), supervisor, child_name.clone()),
1563 )
1564 .await
1565 .unwrap();
1566
1567 actor_mesh
1569 .cast(
1570 instance,
1571 testactor::CauseSupervisionEvent {
1572 kind: testactor::SupervisionEventType::Panic,
1573 send_to_children: true,
1574 },
1575 )
1576 .unwrap();
1577
1578 let (failure_port, mut failure_receiver) = instance.open_port::<Option<MeshFailure>>();
1588 actor_mesh
1589 .cast(
1590 instance,
1591 testactor::NextSupervisionFailure(failure_port.bind()),
1592 )
1593 .unwrap();
1594 let failure = failure_receiver
1595 .recv()
1596 .await
1597 .unwrap()
1598 .expect("no supervision event found on ref from wrapper actor");
1599 let check_failure = move |failure: MeshFailure| {
1600 assert_eq!(failure.actor_mesh_name, Some(child_name.to_string()));
1601 assert!(
1602 failure
1603 .event
1604 .actor_id
1605 .label()
1606 .unwrap()
1607 .as_str()
1608 .starts_with(child_name.label().unwrap().as_str())
1609 );
1610 if let ActorStatus::Failed(ActorErrorKind::Generic(msg)) = &failure.event.actor_status {
1611 assert!(msg.contains("panic"), "{}", msg);
1612 assert!(msg.contains("for testing"), "{}", msg);
1613 } else {
1614 panic!("actor status is not failed: {}", failure.event.actor_status);
1615 }
1616 };
1617 check_failure(failure);
1618
1619 for _ in 0..num_replicas {
1623 let failure =
1624 tokio::time::timeout(Duration::from_secs(20), supervision_receiver.recv())
1625 .await
1626 .expect("timeout")
1627 .unwrap();
1628 check_failure(failure);
1629 }
1630
1631 let _ = hm.shutdown(instance).await;
1632 }
1633
1634 #[assert_no_process_leak]
1635 #[async_timed_test(timeout_secs = 300)]
1636 async fn test_actor_states_with_process_exit() {
1637 hyperactor_telemetry::initialize_logging_for_test();
1638
1639 let config = hyperactor_config::global::lock();
1640 let _poll = config.override_key(SUPERVISION_POLL_FREQUENCY, Duration::from_secs(1));
1641 let _guard = config.override_key(GET_ACTOR_STATE_MAX_IDLE, Duration::from_secs(1));
1642 let _proc_guard = config.override_key(GET_PROC_STATE_MAX_IDLE, Duration::from_secs(1));
1643 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
1644 let _host_spawn = config.override_key(
1645 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1646 Duration::from_secs(120),
1647 );
1648
1649 let instance = testing::instance();
1650 let (supervision_port, mut supervision_receiver) = instance.open_port::<MeshFailure>();
1652 let supervisor = supervision_port.bind();
1653 let num_replicas = 1;
1654 let mut hm = testing::host_mesh(num_replicas).await;
1655 let proc_mesh = hm
1656 .spawn(instance, "test", Extent::unity(), None, None)
1657 .await
1658 .unwrap();
1659 let mut second_hm = testing::host_mesh(num_replicas).await;
1660 let second_proc_mesh = second_hm
1661 .spawn(instance, "test2", Extent::unity(), None, None)
1662 .await
1663 .unwrap();
1664 let child_name = ActorMeshId::instance(Label::new("child").unwrap());
1665
1666 let actor_mesh: ActorMesh<testactor::WrapperActor> = proc_mesh
1669 .spawn(
1670 instance,
1671 "wrapper",
1672 &(
1673 second_proc_mesh.deref().clone(),
1676 supervisor,
1677 child_name.clone(),
1678 ),
1679 )
1680 .await
1681 .unwrap();
1682
1683 actor_mesh
1684 .cast(
1685 instance,
1686 testactor::CauseSupervisionEvent {
1687 kind: testactor::SupervisionEventType::ProcessExit(1),
1688 send_to_children: true,
1689 },
1690 )
1691 .unwrap();
1692
1693 let (failure_port, mut failure_receiver) = instance.open_port::<Option<MeshFailure>>();
1695 actor_mesh
1696 .cast(
1697 instance,
1698 testactor::NextSupervisionFailure(failure_port.bind()),
1699 )
1700 .unwrap();
1701 let failure = failure_receiver
1702 .recv()
1703 .await
1704 .unwrap()
1705 .expect("no supervision event found on ref from wrapper actor");
1706
1707 let check_failure = move |failure: MeshFailure| {
1708 assert_eq!(failure.actor_mesh_name, Some(child_name.to_string()));
1709 assert!(
1710 failure
1711 .event
1712 .actor_id
1713 .label()
1714 .unwrap()
1715 .as_str()
1716 .starts_with(child_name.label().unwrap().as_str())
1717 );
1718 if let ActorStatus::Failed(ActorErrorKind::Generic(msg)) = &failure.event.actor_status {
1719 assert!(msg.contains("exited with non-zero code 1"), "{}", msg);
1720 } else {
1721 panic!("actor status is not failed: {}", failure.event.actor_status);
1722 }
1723 };
1724 check_failure(failure);
1725
1726 for _ in 0..num_replicas {
1728 let failure =
1729 tokio::time::timeout(Duration::from_secs(20), supervision_receiver.recv())
1730 .await
1731 .expect("timeout")
1732 .unwrap();
1733 check_failure(failure);
1734 }
1735
1736 let _ = second_hm.shutdown(instance).await;
1737 let _ = hm.shutdown(instance).await;
1738 }
1739
1740 #[async_timed_test(timeout_secs = 300)]
1741 async fn test_actor_states_on_sliced_mesh() {
1742 hyperactor_telemetry::initialize_logging_for_test();
1743
1744 let instance = testing::instance();
1745 let (supervision_port, mut supervision_receiver) = instance.open_port::<MeshFailure>();
1747 let supervisor = supervision_port.bind();
1748 let (mut hm, _actor_mesh, sliced, sliced_replicas, child_name) = {
1749 let config = hyperactor_config::global::lock();
1750 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
1751 let _actor_spawn = config.override_key(ACTOR_SPAWN_MAX_IDLE, Duration::from_secs(120));
1752 let _host_spawn = config.override_key(
1753 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1754 Duration::from_secs(120),
1755 );
1756 let num_replicas = 2;
1757 let hm = testing::host_mesh(num_replicas).await;
1758 let proc_mesh = hm
1759 .spawn(instance, "test", Extent::unity(), None, None)
1760 .await
1761 .unwrap();
1762 let child_name = ActorMeshId::instance(Label::new("child").unwrap());
1763
1764 let actor_mesh: ActorMesh<testactor::WrapperActor> = proc_mesh
1767 .spawn(
1768 instance,
1769 "wrapper",
1770 &(proc_mesh.deref().clone(), supervisor, child_name.clone()),
1771 )
1772 .await
1773 .unwrap();
1774 let sliced = actor_mesh
1775 .range("hosts", 1..2)
1776 .expect("slice should be valid");
1777 let sliced_replicas = sliced.len();
1778 (hm, actor_mesh, sliced, sliced_replicas, child_name)
1779 };
1780
1781 sliced
1783 .cast(
1784 instance,
1785 testactor::CauseSupervisionEvent {
1786 kind: testactor::SupervisionEventType::Panic,
1787 send_to_children: true,
1788 },
1789 )
1790 .unwrap();
1791
1792 for _ in 0..sliced_replicas {
1793 let supervision_message =
1794 tokio::time::timeout(Duration::from_secs(20), supervision_receiver.recv())
1795 .await
1796 .expect("timeout")
1797 .unwrap();
1798 let event = supervision_message.event;
1799 assert!(
1800 event
1801 .actor_id
1802 .label()
1803 .unwrap()
1804 .as_str()
1805 .starts_with(child_name.label().unwrap().as_str())
1806 );
1807 if let ActorStatus::Failed(ActorErrorKind::Generic(msg)) = &event.actor_status {
1808 assert!(msg.contains("panic"));
1809 assert!(msg.contains("for testing"));
1810 } else {
1811 panic!("actor status is not failed: {}", event.actor_status);
1812 }
1813 }
1814
1815 let _ = hm.shutdown(instance).await;
1816 }
1817
1818 async fn execute_cast(config: &hyperactor_config::global::ConfigLock) {
1819 let _guard = config.override_key(crate::bootstrap::MESH_BOOTSTRAP_ENABLE_PDEATHSIG, false);
1820 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
1821 let _host_spawn = config.override_key(
1822 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1823 Duration::from_secs(60),
1824 );
1825
1826 let instance = testing::instance();
1827 let mut host_mesh = testing::host_mesh(2).await;
1828 let proc_mesh = host_mesh
1829 .spawn(instance, "test", Extent::unity(), None, None)
1830 .await
1831 .unwrap();
1832 let actor_mesh: ActorMesh<testactor::TestActor> =
1833 proc_mesh.spawn(instance, "test", &()).await.unwrap();
1834
1835 let (cast_info, mut cast_info_rx) = instance.mailbox().open_port();
1836 actor_mesh
1837 .cast(
1838 instance,
1839 testactor::GetCastInfo {
1840 cast_info: cast_info.bind(),
1841 },
1842 )
1843 .unwrap();
1844
1845 let mut point_to_actor: HashSet<_> = actor_mesh.iter().collect();
1846 while !point_to_actor.is_empty() {
1847 let (point, origin_actor_ref, sender_actor_id) = cast_info_rx.recv().await.unwrap();
1848 let key = (point, origin_actor_ref);
1849 assert!(
1850 point_to_actor.remove(&key),
1851 "key {:?} not present or removed twice",
1852 key
1853 );
1854 assert_eq!(&sender_actor_id, instance.self_addr());
1855 }
1856
1857 let _ = host_mesh.shutdown(instance).await;
1858 }
1859
1860 #[async_timed_test(timeout_secs = 60)]
1861 async fn test_sliced_actor_mesh_cast_v1_reaches_slice_members() {
1862 use hyperactor::config::ENABLE_DEST_ACTOR_REORDERING_BUFFER;
1863
1864 let config = hyperactor_config::global::lock();
1865 let _guard = config.override_key(crate::bootstrap::MESH_BOOTSTRAP_ENABLE_PDEATHSIG, false);
1866 let _v1 = config.override_key(crate::comm::ENABLE_NATIVE_V1_CASTING, true);
1867 let _reorder = config.override_key(ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1868 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
1869 let _host_spawn = config.override_key(
1870 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1871 Duration::from_secs(60),
1872 );
1873
1874 let instance = testing::instance();
1875 let mut host_mesh = testing::host_mesh(2).await;
1876 let proc_mesh = host_mesh
1877 .spawn(instance, "test", Extent::unity(), None, None)
1878 .await
1879 .unwrap();
1880 let root_actor_mesh: ActorMesh<testactor::TestActor> =
1881 proc_mesh.spawn(instance, "test", &()).await.unwrap();
1882
1883 let actor_mesh = root_actor_mesh.sliced(Region::new(
1886 vec!["rank".to_string()],
1887 Slice::new(0, vec![1], vec![1]).unwrap(),
1888 ));
1889 let (cast_info, mut cast_info_rx) = instance.mailbox().open_port();
1890 actor_mesh
1891 .cast(
1892 instance,
1893 testactor::GetCastInfo {
1894 cast_info: cast_info.bind(),
1895 },
1896 )
1897 .unwrap();
1898
1899 let (point, _actor_ref, _sender) = cast_info_rx.recv().await.unwrap();
1900 let received_ranks = HashSet::from([point.rank()]);
1901 assert_eq!(received_ranks, HashSet::from([0]));
1902
1903 let (cast_info2, mut cast_info_rx2) = instance.mailbox().open_port();
1905 root_actor_mesh
1906 .cast(
1907 instance,
1908 testactor::GetCastInfo {
1909 cast_info: cast_info2.bind(),
1910 },
1911 )
1912 .unwrap();
1913
1914 let mut all_ranks: HashSet<usize> = HashSet::new();
1915 for _ in 0..2 {
1916 let (point, _actor_ref, _sender) = cast_info_rx2.recv().await.unwrap();
1917 all_ranks.insert(point.rank());
1918 }
1919 assert_eq!(all_ranks, HashSet::from([0, 1]));
1920
1921 let _ = host_mesh.shutdown(instance).await;
1922 }
1923
1924 #[async_timed_test(timeout_secs = 60)]
1925 async fn test_cast_domain_stamps_resource_rank_binding() {
1926 let client_proc = hyperactor::proc::Proc::direct(
1927 hyperactor::channel::ChannelTransport::Unix.any(),
1928 "client_proc".into(),
1929 )
1930 .unwrap();
1931
1932 let client = client_proc.client("client");
1933
1934 let mut procs = Vec::new();
1935
1936 let members = (0..2)
1937 .map(|rank| {
1938 let proc = hyperactor::proc::Proc::direct(
1939 hyperactor::channel::ChannelTransport::Unix.any(),
1940 format!("proc_{rank}"),
1941 )
1942 .unwrap();
1943
1944 let cast_handle = proc
1945 .spawn_with_uid(
1946 hyperactor::Uid::singleton(Label::strip("cast")),
1947 hyperactor_cast::cast_actor::CastActor::default(),
1948 )
1949 .unwrap();
1950
1951 let _: hyperactor::ActorRef<hyperactor_cast::cast_actor::CastActor> =
1952 cast_handle.bind();
1953
1954 let receiver_handle = proc
1955 .spawn_with_uid(
1956 hyperactor::Uid::singleton(Label::strip("receiver")),
1957 testactor::TestActor,
1958 )
1959 .unwrap();
1960
1961 let _: hyperactor::ActorRef<testactor::TestActor> = receiver_handle.bind();
1962
1963 let actor_addr =
1964 hyperactor::ActorAddr::root(proc.proc_addr().clone(), Label::strip("receiver"));
1965
1966 procs.push(proc);
1967
1968 (rank, actor_addr)
1969 })
1970 .collect::<HashMap<_, _>>();
1971
1972 let cast_domain = hyperactor_cast::cast_actor::CastDomainId::new()
1973 .materialize(
1974 &client,
1975 members,
1976 Region::from(ndslice::shape!(rank = 2)),
1977 hyperactor_cast::cast_actor::TilingPolicy::BlockPartitioning,
1978 hyperactor_config::Flattrs::new(),
1979 )
1980 .unwrap();
1981
1982 let (rank_port, mut rank_rx) = client.mailbox().open_port();
1983
1984 cast_domain
1985 .cast(
1986 &client,
1987 hyperactor_config::Flattrs::new(),
1988 testactor::GetResourceRank {
1989 rank: crate::resource::Rank::default(),
1990 reply: rank_port.bind(),
1991 },
1992 )
1993 .unwrap();
1994
1995 let mut received_ranks = HashSet::new();
1996
1997 for _ in 0..2 {
1998 let (_point, rank) = rank_rx.recv().await.unwrap();
1999
2000 received_ranks.insert(rank);
2001 }
2002
2003 assert_eq!(received_ranks, HashSet::from([Some(0), Some(1)]));
2004 }
2005
2006 #[async_timed_test(timeout_secs = 30)]
2007 async fn test_cast() {
2008 let config = hyperactor_config::global::lock();
2009 execute_cast(&config).await;
2010 }
2011
2012 #[async_timed_test(timeout_secs = 30)]
2013 async fn test_cast_p2p() {
2014 let config = hyperactor_config::global::lock();
2015 let _guard = config.override_key(crate::comm::ENABLE_NATIVE_V1_CASTING, true);
2016 let _guard2 = config.override_key(
2017 hyperactor::config::ENABLE_DEST_ACTOR_REORDERING_BUFFER,
2018 true,
2019 );
2020 let _guard3 = config.override_key(crate::config::V1_CAST_POINT_TO_POINT_THRESHOLD, 1024);
2021 execute_cast(&config).await;
2022 }
2023 #[assert_no_process_leak]
2029 #[async_timed_test(timeout_secs = 60)]
2030 async fn test_undeliverable_message_return() {
2031 use hyperactor::mailbox::MessageEnvelope;
2032 use hyperactor::mailbox::Undeliverable;
2033 use hyperactor::testing::pingpong::PingPongActor;
2034 use hyperactor::testing::pingpong::PingPongMessage;
2035
2036 hyperactor_telemetry::initialize_logging_for_test();
2037
2038 let instance = testing::instance();
2039
2040 let (mut hm, proc_mesh) = {
2042 let config = hyperactor_config::global::lock();
2043 let _proc_spawn_guard =
2044 config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2045 let _host_spawn_guard = config.override_key(
2046 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2047 Duration::from_secs(60),
2048 );
2049 let hm = testing::host_mesh(2).await;
2050 let proc_mesh = hm
2051 .spawn(instance, "test", Extent::unity(), None, None)
2052 .await
2053 .unwrap();
2054 (hm, proc_mesh)
2055 };
2056
2057 let (undeliverable_port, mut undeliverable_rx) =
2059 instance.open_port::<Undeliverable<MessageEnvelope>>();
2060
2061 let ping_proc_mesh = proc_mesh.range("hosts", 0..1).unwrap();
2064 let pong_proc_mesh = proc_mesh.range("hosts", 1..2).unwrap();
2065
2066 let ping_mesh: ActorMesh<PingPongActor> = ping_proc_mesh
2067 .spawn(
2068 instance,
2069 "ping",
2070 &(Some(undeliverable_port.bind()), None, None),
2071 )
2072 .await
2073 .unwrap();
2074
2075 let mut pong_mesh: ActorMesh<PingPongActor> = pong_proc_mesh
2076 .spawn(instance, "pong", &(None, None, None))
2077 .await
2078 .unwrap();
2079
2080 let ping_handle = ping_mesh.values().next().unwrap();
2082 let pong_handle = pong_mesh.values().next().unwrap();
2083
2084 let (done_tx, done_rx) = instance.open_once_port();
2086 ping_handle.post(
2087 instance,
2088 PingPongMessage(2, pong_handle.clone(), done_tx.bind()),
2089 );
2090 assert!(
2091 done_rx.recv().await.unwrap(),
2092 "Initial ping-pong should work"
2093 );
2094
2095 pong_mesh
2097 .stop(instance, "test stop".to_string())
2098 .await
2099 .unwrap();
2100
2101 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2103
2104 let config = hyperactor_config::global::lock();
2106 let _guard = config.override_key(
2107 hyperactor::config::MESSAGE_DELIVERY_TIMEOUT,
2108 std::time::Duration::from_secs(5),
2109 );
2110
2111 let n = 100usize;
2113 for i in 1..=n {
2114 let ttl = 66 + i as u64; let (once_tx, _once_rx) = instance.open_once_port();
2116 ping_handle.post(
2117 instance,
2118 PingPongMessage(ttl, pong_handle.clone(), once_tx.bind()),
2119 );
2120 }
2121
2122 let mut count = 0;
2126 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
2127 while count < n && tokio::time::Instant::now() < deadline {
2128 match tokio::time::timeout(std::time::Duration::from_secs(1), undeliverable_rx.recv())
2129 .await
2130 {
2131 Ok(Ok(Undeliverable::Returned(envelope))) => {
2132 let _: PingPongMessage = envelope.deserialized().unwrap();
2133 count += 1;
2134 }
2135 Ok(Ok(Undeliverable::Report(_))) => break,
2136 Ok(Err(_)) => break, Err(_) => break, }
2139 }
2140
2141 assert_eq!(
2142 count, n,
2143 "Expected {} undeliverable messages, got {}",
2144 n, count
2145 );
2146
2147 let _ = hm.shutdown(instance).await;
2148 }
2149
2150 #[async_timed_test(timeout_secs = 30)]
2160 async fn test_actor_mesh_stop_timeout() {
2161 hyperactor_telemetry::initialize_logging_for_test();
2162
2163 let config = hyperactor_config::global::lock();
2167 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2168 let _host_spawn = config.override_key(
2169 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2170 Duration::from_secs(60),
2171 );
2172
2173 let instance = testing::instance();
2174
2175 let mut hm = testing::host_mesh(2).await;
2177 let proc_mesh = hm
2178 .spawn(instance, "test", Extent::unity(), None, None)
2179 .await
2180 .unwrap();
2181
2182 let mut sleep_mesh: ActorMesh<testactor::SleepActor> =
2185 proc_mesh.spawn(instance, "sleepers", &()).await.unwrap();
2186 let _guard = config.override_key(ACTOR_SPAWN_MAX_IDLE, std::time::Duration::from_secs(1));
2187
2188 for actor_ref in sleep_mesh.values() {
2193 actor_ref.post(instance, std::time::Duration::from_secs(5));
2194 }
2195
2196 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2198
2199 let expected_actors = sleep_mesh.values().count();
2201
2202 let stop_start = tokio::time::Instant::now();
2207 let result = sleep_mesh.stop(instance, "test stop".to_string()).await;
2208 let stop_duration = tokio::time::Instant::now().duration_since(stop_start);
2209
2210 match result {
2214 Ok(_) => {
2215 tracing::info!(
2216 "stop returned Ok for {} actors; their tokio tasks \
2217 may still be running until their handler yields",
2218 expected_actors
2219 );
2220 }
2221 Err(ref e) => {
2222 let err_str = format!("{:?}", e);
2223 assert!(
2224 err_str.contains("Timeout"),
2225 "Expected Timeout error, got: {:?}",
2226 e
2227 );
2228 }
2229 }
2230
2231 assert!(
2236 stop_duration < std::time::Duration::from_millis(4500),
2237 "Stop took {:?}, expected < 4.5s (controller should have given up waiting for Stopped)",
2238 stop_duration
2239 );
2240 assert!(
2241 stop_duration >= std::time::Duration::from_millis(900),
2242 "Stop took {:?}, expected >= 900ms (should have waited for the 1s idle timeout)",
2243 stop_duration
2244 );
2245
2246 let _ = hm.shutdown(instance).await;
2247 }
2248
2249 #[async_timed_test(timeout_secs = 60)]
2255 async fn test_actor_mesh_stop_graceful() {
2256 hyperactor_telemetry::initialize_logging_for_test();
2257
2258 let config = hyperactor_config::global::lock();
2259 let _proc_spawn = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2260 let _host_spawn = config.override_key(
2261 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2262 Duration::from_secs(60),
2263 );
2264
2265 let instance = testing::instance();
2266
2267 let mut hm = testing::host_mesh(2).await;
2269 let proc_mesh = hm
2270 .spawn(instance, "test", Extent::unity(), None, None)
2271 .await
2272 .unwrap();
2273
2274 let mut actor_mesh: ActorMesh<testactor::TestActor> =
2277 proc_mesh.spawn(instance, "test_actors", &()).await.unwrap();
2278
2279 let mesh_ref = actor_mesh.deref().clone();
2282
2283 let expected_actors = actor_mesh.values().count();
2284 assert!(expected_actors > 0, "Should have spawned some actors");
2285
2286 let stop_start = tokio::time::Instant::now();
2288 let result = actor_mesh.stop(instance, "test stop".to_string()).await;
2289 let stop_duration = tokio::time::Instant::now().duration_since(stop_start);
2290
2291 assert!(
2293 result.is_ok(),
2294 "Stop should succeed for responsive actors, got: {:?}",
2295 result.err()
2296 );
2297
2298 assert!(
2302 stop_duration < std::time::Duration::from_secs(5),
2303 "Graceful stop took {:?}, expected < 5s (actors should stop quickly)",
2304 stop_duration
2305 );
2306
2307 tracing::info!(
2308 "Successfully stopped {} actors in {:?}",
2309 expected_actors,
2310 stop_duration
2311 );
2312
2313 let next_event = actor_mesh.next_supervision_event(instance).await.unwrap();
2319 assert_eq!(next_event.actor_mesh_name, Some(mesh_ref.id().to_string()));
2320 assert!(matches!(
2321 next_event.event.actor_status,
2322 ActorStatus::Stopped(_)
2323 ));
2324 let next_event = mesh_ref.next_supervision_event(instance).await.unwrap();
2327 assert_eq!(next_event.actor_mesh_name, Some(mesh_ref.id().to_string()));
2328 assert!(matches!(
2329 next_event.event.actor_status,
2330 ActorStatus::Stopped(_)
2331 ));
2332
2333 let _ = hm.shutdown(instance).await;
2334 }
2335}