1use std::any::type_name;
10use std::collections::HashMap;
11use std::collections::HashSet;
12use std::fmt;
13use std::hash::Hash;
14use std::ops::Deref;
15use std::sync::Arc;
16use std::time::Duration;
17
18use hyperactor::ActorAddr;
19use hyperactor::ActorId;
20use hyperactor::ActorRef;
21use hyperactor::Endpoint as _;
22use hyperactor::Handler;
23use hyperactor::ProcAddr;
24use hyperactor::RemoteMessage;
25use hyperactor::RemoteSpawn;
26use hyperactor::accum::IdleFlushReducerOpts;
27use hyperactor::actor::ActorStatus;
28use hyperactor::actor::remote::Remote;
29use hyperactor::context;
30use hyperactor::id::Label;
31use hyperactor::supervision::ActorSupervisionEvent;
32use hyperactor_config::CONFIG;
33use hyperactor_config::ConfigAttr;
34use hyperactor_config::Flattrs;
35use hyperactor_config::NonZeroUsize;
36use hyperactor_config::attrs::declare_attrs;
37use hyperactor_remote::ActorSpawner;
38use hyperactor_remote::ActorSpawnerEndpoint;
39use hyperactor_remote::KeepaliveLink;
40use hyperactor_remote::proc_spawner::actor_spawner_uid;
41use hyperactor_telemetry::hash_to_u64;
42use ndslice::Extent;
43use ndslice::ViewExt as _;
44use ndslice::view;
45use ndslice::view::CollectMeshExt;
46use ndslice::view::Ranked;
47use ndslice::view::Region;
48use serde::Deserialize;
49use serde::Serialize;
50use typeuri::Named;
51
52use crate::ActorMesh;
53use crate::ActorMeshRef;
54use crate::Error;
55use crate::HostMeshRef;
56use crate::ValueMesh;
57use crate::actor_mesh::ACTOR_MESH_ID;
58use crate::actor_mesh::ActorMeshStopHandle;
59use crate::casting::CAST_POINT;
60use crate::host_mesh::GET_PROC_STATE_MAX_IDLE;
61use crate::host_mesh::host_agent::GetHostProcStates;
62use crate::host_mesh::host_agent::ProcState;
63use crate::host_mesh::mesh_to_rankedvalues_with_default;
64use crate::mesh_controller::ActorMeshControlPlane;
65use crate::mesh_controller::ActorMeshController;
66use crate::mesh_id::ActorMeshId;
67use crate::mesh_id::ProcMeshId;
68use crate::mesh_id::ResourceId;
69use crate::proc_agent;
70use crate::proc_agent::ActorState;
71use crate::proc_agent::ProcAgent;
72use crate::resource;
73use crate::resource::GetRankStatus;
74use crate::resource::Status;
75use crate::supervision::MeshFailure;
76
77declare_attrs! {
78 @meta(CONFIG = ConfigAttr::new(
80 Some("HYPERACTOR_MESH_USE_DIRECT_SPAWN".to_string()),
81 Some("use_direct_spawn".to_string()),
82 ))
83 pub attr USE_DIRECT_SPAWN: bool = false;
84
85 @meta(CONFIG = ConfigAttr::new(
88 Some("HYPERACTOR_MESH_ACTOR_SPAWN_MAX_IDLE".to_string()),
89 Some("actor_spawn_max_idle".to_string()),
90 ))
91 pub attr ACTOR_SPAWN_MAX_IDLE: Duration = Duration::from_secs(30);
92
93 @meta(CONFIG = ConfigAttr::new(
96 Some("HYPERACTOR_MESH_GET_ACTOR_STATE_MAX_IDLE".to_string()),
97 Some("get_actor_state_max_idle".to_string()),
98 ))
99 pub attr GET_ACTOR_STATE_MAX_IDLE: Duration = Duration::from_secs(30);
100}
101
102pub fn telemetry_actor_mesh_id(proc_mesh_id: &ProcMeshId, actor_mesh_id: &ActorMeshId) -> u64 {
104 hash_to_u64(&(proc_mesh_id, actor_mesh_id))
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
109pub struct ProcRef {
110 proc_id: ProcAddr,
111 create_rank: usize,
113 agent: ActorRef<ProcAgent>,
115}
116
117impl ProcRef {
118 pub fn new(proc_id: ProcAddr, create_rank: usize, agent: ActorRef<ProcAgent>) -> Self {
120 Self {
121 proc_id,
122 create_rank,
123 agent,
124 }
125 }
126
127 pub fn proc_addr(&self) -> &ProcAddr {
128 &self.proc_id
129 }
130
131 pub(crate) fn actor_addr(&self, id: &ActorMeshId) -> ActorAddr {
132 self.proc_id.actor_addr_uid(id.uid().clone())
133 }
134
135 pub(crate) fn spawner(&self) -> ActorRef<ActorSpawner> {
137 ActorRef::attest(self.proc_id.actor_addr_uid(actor_spawner_uid()))
138 }
139}
140
141#[derive(Debug)]
143pub struct ProcMesh {
144 #[allow(dead_code)]
145 id: ProcMeshId,
146 current_ref: ProcMeshRef,
147 controller: Option<ActorRef<crate::mesh_controller::ProcMeshController>>,
148}
149
150enum SpawnedActorMesh<A: RemoteSpawn> {
151 Managed {
152 mesh: ActorMesh<A>,
153 statuses: crate::StatusMesh,
154 },
155 Data(ActorMesh<A>),
156}
157
158impl ProcMesh {
159 pub(crate) fn create(
160 id: ProcMeshId,
161 extent: Extent,
162 hosts: HostMeshRef,
163 ranks: Vec<ProcRef>,
164 ) -> crate::Result<Self> {
165 let region = extent.into();
166 let ranks = Arc::new(ranks);
167
168 if let Some(first) = ranks.first() {
172 crate::global_context::set_global_supervision_sink(
173 first.agent.port::<ActorSupervisionEvent>(),
174 );
175 }
176
177 let current_ref = ProcMeshRef::new(id.clone(), region, ranks, Some(hosts)).unwrap();
178
179 {
181 let name_str = id.to_string();
182 let mesh_id_hash = hash_to_u64(&id);
183
184 let hm = current_ref
185 .host_mesh
186 .as_ref()
187 .expect("ProcMesh always has a host mesh");
188 let parent_mesh_id = hash_to_u64(hm.id());
189 let parent_view_json = serde_json::to_string(hm.region())
190 .unwrap_or_else(|e| format!("encountered error when serializing region: {}", e));
191
192 hyperactor_telemetry::notify_mesh_created(hyperactor_telemetry::MeshEvent {
193 id: mesh_id_hash,
194 timestamp: std::time::SystemTime::now(),
195 class: "Proc".to_string(),
196 given_name: id
197 .display_label()
198 .map(|l| l.as_str())
199 .unwrap_or("unnamed")
200 .to_string(),
201 full_name: name_str,
202 shape_json: serde_json::to_string(¤t_ref.region.extent()).unwrap_or_default(),
203 parent_mesh_id: Some(parent_mesh_id),
204 parent_view_json: Some(parent_view_json),
205 });
206
207 let now = std::time::SystemTime::now();
210 for rank in current_ref.ranks.iter() {
211 let actor_addr = rank.agent.actor_addr();
212
213 hyperactor_telemetry::notify_actor_created(hyperactor_telemetry::ActorEvent {
214 id: hyperactor_telemetry::hash_to_u64(actor_addr.id()),
215 timestamp: now,
216 mesh_id: mesh_id_hash,
217 rank: rank.create_rank as u64,
218 full_name: actor_addr.to_string(),
219 display_name: None,
220 });
221 }
222 }
223
224 Ok(Self {
225 id,
226 current_ref,
227 controller: None,
228 })
229 }
230
231 pub(crate) fn set_controller(
233 &mut self,
234 controller: Option<ActorRef<crate::mesh_controller::ProcMeshController>>,
235 ) {
236 self.controller = controller;
237 }
238
239 pub async fn stop(&mut self, cx: &impl context::Actor, reason: String) -> anyhow::Result<()> {
250 if let Some(controller) = self.controller.take() {
251 let id = self.id.resource_id().clone();
252 controller.post(
253 cx,
254 resource::Stop {
255 id: id.clone(),
256 reason,
257 },
258 );
259
260 let (port, mut rx) = cx.mailbox().open_port();
265 controller.post(
266 cx,
267 resource::GetState::<resource::mesh::State<()>> {
268 id: id.clone(),
269 reply: port.bind(),
270 },
271 );
272
273 let statuses = rx.recv().await?;
274 let Some(state) = &statuses.state else {
275 anyhow::bail!(
276 "non-existent state in GetState reply from controller: {}",
277 controller.actor_addr()
278 );
279 };
280 let all_stopped = state.statuses.values().all(|s| s.is_terminating());
286 if !all_stopped {
287 anyhow::bail!(
288 "proc mesh {} not all procs reached terminating state after stop: {:?}",
289 id,
290 state.statuses,
291 );
292 }
293 return Ok(());
294 }
295
296 let region = self.region.clone();
297 let procs = self.current_ref.proc_ids().collect::<Vec<ProcAddr>>();
298 self.current_ref
301 .host_mesh
302 .as_ref()
303 .expect("ProcMesh always has a host mesh")
304 .stop_proc_mesh(cx, &self.id, procs, region, reason)
305 .await
306 .map(|_| ())
307 .map_err(anyhow::Error::from)
308 }
309}
310
311impl fmt::Display for ProcMesh {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 write!(f, "{}", self.current_ref)
314 }
315}
316
317impl Deref for ProcMesh {
318 type Target = ProcMeshRef;
319
320 fn deref(&self) -> &Self::Target {
321 &self.current_ref
322 }
323}
324
325impl Drop for ProcMesh {
326 fn drop(&mut self) {
327 tracing::info!(
328 name = "ProcMeshStatus",
329 proc_mesh = %self.id,
330 status = "Dropped",
331 );
332 }
333}
334
335#[derive(Debug, Clone, Named, Serialize, Deserialize)]
346pub struct ProcMeshRef {
347 id: ProcMeshId,
348 region: Region,
349 ranks: Arc<Vec<ProcRef>>,
350 proc_agent_mesh: ActorMeshRef<ProcAgent>,
360 host_mesh: Option<HostMeshRef>,
362}
363wirevalue::register_type!(ProcMeshRef);
364
365impl PartialEq for ProcMeshRef {
368 fn eq(&self, other: &Self) -> bool {
369 self.id == other.id
370 && self.region == other.region
371 && self.ranks == other.ranks
372 && self.host_mesh == other.host_mesh
373 }
374}
375
376impl Eq for ProcMeshRef {}
377
378impl std::hash::Hash for ProcMeshRef {
379 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
380 self.id.hash(state);
381 self.region.hash(state);
382 self.ranks.hash(state);
383 self.host_mesh.hash(state);
384 }
385}
386
387impl ProcMeshRef {
388 #[allow(clippy::result_large_err)]
390 fn new(
391 id: ProcMeshId,
392 region: Region,
393 ranks: Arc<Vec<ProcRef>>,
394 host_mesh: Option<HostMeshRef>,
395 ) -> crate::Result<Self> {
396 if ranks.is_empty() {
397 return Err(crate::Error::ConfigurationError(anyhow::anyhow!(
398 "empty proc meshes are not supported"
399 )));
400 }
401 if region.num_ranks() != ranks.len() {
402 return Err(crate::Error::InvalidRankCardinality {
403 expected: region.num_ranks(),
404 actual: ranks.len(),
405 });
406 }
407 let proc_agent_mesh = Self::proc_agent_mesh_ref(&id, ®ion, &ranks)?;
408 Ok(Self {
409 id,
410 region,
411 ranks,
412 proc_agent_mesh,
413 host_mesh,
414 })
415 }
416
417 pub fn new_singleton(id: ProcMeshId, proc_ref: ProcRef) -> crate::Result<Self> {
421 let region: Region = Extent::unity().into();
422 let ranks = Arc::new(vec![proc_ref]);
423 let proc_agent_mesh = Self::proc_agent_mesh_ref(&id, ®ion, &ranks)?;
424 Ok(Self {
425 id,
426 region,
427 ranks,
428 proc_agent_mesh,
429 host_mesh: None,
430 })
431 }
432
433 pub fn id(&self) -> &ProcMeshId {
434 &self.id
435 }
436
437 pub fn host_mesh_id(&self) -> Option<&crate::mesh_id::HostMeshId> {
438 self.host_mesh.as_ref().map(|h| h.id())
439 }
440
441 pub fn hosts(&self) -> Option<&HostMeshRef> {
443 self.host_mesh.as_ref()
444 }
445
446 pub(crate) fn agent_mesh(&self) -> &ActorMeshRef<ProcAgent> {
447 &self.proc_agent_mesh
448 }
449
450 fn proc_agent_mesh_ref(
451 proc_mesh_id: &ProcMeshId,
452 region: &Region,
453 ranks: &[ProcRef],
454 ) -> crate::Result<ActorMeshRef<ProcAgent>> {
455 let agent_label = ranks
456 .first()
457 .unwrap()
458 .agent
459 .actor_addr()
460 .label()
461 .cloned()
462 .unwrap_or_else(|| Label::new(proc_agent::PROC_AGENT_ACTOR_NAME).unwrap());
463 let id = ActorMeshId::singleton(agent_label);
464
465 let members = Arc::new(
466 ranks
467 .iter()
468 .map(|rank| rank.agent.actor_addr().clone())
469 .collect_mesh::<ValueMesh<_>>(region.clone())
470 .map_err(|error| crate::Error::ConfigurationError(error.into()))?,
471 );
472
473 Ok(ActorMeshRef::new_managed(
474 id,
475 Some(proc_mesh_id.clone()),
476 region.clone(),
477 None,
478 members,
479 ))
480 }
481
482 pub async fn actor_states(
484 &self,
485 cx: &impl context::Actor,
486 id: ActorMeshId,
487 ) -> crate::Result<ValueMesh<resource::State<ActorState>>> {
488 self.actor_states_with_keepalive(cx, id, None).await
489 }
490
491 pub(crate) async fn actor_states_with_keepalive(
497 &self,
498 cx: &impl context::Actor,
499 id: ActorMeshId,
500 keepalive: Option<std::time::SystemTime>,
501 ) -> crate::Result<ValueMesh<resource::State<ActorState>>> {
502 let (port, mut rx) = cx.mailbox().open_port::<resource::State<ActorState>>();
503 let mut port = port.bind();
504 port.return_undeliverable(false);
507 let get_state = resource::GetState::<ActorState> {
510 id: id.resource_id().clone(),
511 reply: port,
512 };
513 if let Some(expires_after) = keepalive {
514 self.proc_agent_mesh.cast(
515 cx,
516 resource::KeepaliveGetState {
517 expires_after,
518 get_state,
519 },
520 )?;
521 } else {
522 self.proc_agent_mesh.cast(cx, get_state)?;
523 }
524 let expected = self.ranks.len();
525 let rank_of_actor: HashMap<ActorId, usize> = self
529 .ranks
530 .iter()
531 .enumerate()
532 .map(|(rank, proc_ref)| (proc_ref.actor_addr(&id).id().clone(), rank))
533 .collect();
534 let mut states = Vec::with_capacity(expected);
535 let timeout = hyperactor_config::global::get(GET_ACTOR_STATE_MAX_IDLE);
536 for _ in 0..expected {
537 let state = tokio::time::timeout(timeout, rx.recv()).await;
543 if let Ok(state) = state {
544 let state = state?;
546 match state.state {
547 Some(ref inner) => {
548 let rank = *rank_of_actor.get(inner.actor_id.id()).ok_or_else(|| {
551 anyhow::anyhow!(
552 "state reply from {} is not a member of the queried view",
553 inner.actor_id,
554 )
555 })?;
556 states.push((rank, state));
557 }
558 None => {
559 return Err(Error::NotExist(state.id));
563 }
564 }
565 } else {
566 tracing::error!(
567 "timeout waiting for a message after {:?} from proc mesh agent in mesh {}",
568 timeout,
569 self.proc_agent_mesh
570 );
571 let all_ranks = (0..self.ranks.len()).collect::<HashSet<_>>();
574 let completed_ranks = states.iter().map(|(rank, _)| *rank).collect::<HashSet<_>>();
575 let mut leftover_ranks = all_ranks.difference(&completed_ranks).collect::<Vec<_>>();
576 assert_eq!(leftover_ranks.len(), expected - states.len());
577 while states.len() < expected {
578 let rank = *leftover_ranks
579 .pop()
580 .expect("leftover ranks should not be empty");
581 let agent = self.proc_agent_mesh.get(rank).expect("agent should exist");
582 let agent_id = agent.actor_addr().clone();
583 states.push((
584 rank,
586 resource::State {
587 id: id.resource_id().clone(),
588 status: resource::Status::Timeout(timeout),
589 generation: u64::MAX,
594 timestamp: std::time::SystemTime::now(),
595 state: Some(ActorState {
596 actor_id: agent_id.clone(),
597 create_rank: rank,
598 supervision_events: vec![ActorSupervisionEvent::new(
599 agent_id,
600 None,
601 ActorStatus::generic_failure(format!(
602 "timeout waiting for message from proc mesh agent while querying for \"{}\". The process likely crashed",
603 id,
604 )),
605 None,
606 )],
607 }),
608 },
609 ));
610 }
611 break;
612 }
613 }
614 states.sort_by_key(|(rank, _)| *rank);
618 let vm = states
619 .into_iter()
620 .map(|(_, state)| state)
621 .collect_mesh::<ValueMesh<_>>(self.region.clone())?;
622 Ok(vm)
623 }
624
625 #[allow(clippy::result_large_err)]
638 pub async fn states(
639 &self,
640 cx: &impl context::Actor,
641 keepalive: Option<std::time::SystemTime>,
642 ) -> crate::Result<Option<ValueMesh<resource::State<ProcState>>>> {
643 let Some(host_mesh) = self.host_mesh.as_ref() else {
645 return Ok(None);
646 };
647 let region = self.region.clone();
648 let timeout = hyperactor_config::global::get(GET_PROC_STATE_MAX_IDLE);
649
650 let template: ValueMesh<resource::State<ProcState>> = self
654 .ranks
655 .iter()
656 .map(|proc_ref| resource::State {
657 id: ResourceId::new(
658 proc_ref.proc_id.uid().clone(),
659 proc_ref.proc_id.label().cloned(),
660 ),
661 status: resource::Status::Timeout(timeout),
662 state: None,
663 generation: 0,
664 timestamp: std::time::SystemTime::now(),
665 })
666 .collect_mesh::<ValueMesh<_>>(region.clone())?;
667 let fallback = template.clone();
669
670 let (port, rx) = cx.mailbox().open_idle_flush_accum_port(
675 template,
676 IdleFlushReducerOpts {
677 idle_timeout: Duration::from_millis(50),
678 abandon_timeout: Duration::from_secs(30),
679 expected_updates_per_destination: NonZeroUsize::MIN,
680 },
681 );
682
683 let reply = port.bind();
684
685 host_mesh.agent_mesh().cast(
686 cx,
687 GetHostProcStates {
688 proc_mesh_id: self.id.clone(),
689 region: region.clone(),
690 keepalive,
691 reply,
692 },
693 )?;
694
695 let mesh =
700 match resource::wait_mesh(rx, timeout, fallback, |s: &resource::State<ProcState>| {
701 !matches!(s.status, resource::Status::Timeout(_))
702 })
703 .await
704 {
705 Ok(mesh) | Err(mesh) => mesh,
706 };
707
708 Ok(Some(mesh))
709 }
710
711 pub(crate) fn proc_ids(&self) -> impl Iterator<Item = ProcAddr> {
713 self.ranks.iter().map(|proc_ref| proc_ref.proc_id.clone())
714 }
715
716 pub async fn spawn<A: RemoteSpawn, C: context::Actor>(
726 &self,
727 cx: &C,
728 name: &str,
729 params: &A::Params,
730 ) -> crate::Result<ActorMesh<A>>
731 where
732 A::Params: RemoteMessage,
733 C::A: Handler<MeshFailure>,
734 {
735 let id = ActorMeshId::instance(Label::strip(name));
737 self.spawn_with_name(cx, id, params, None, false).await
738 }
739
740 pub async fn spawn_service<A: RemoteSpawn, C: context::Actor>(
748 &self,
749 cx: &C,
750 name: &str,
751 params: &A::Params,
752 ) -> crate::Result<ActorMesh<A>>
753 where
754 A::Params: RemoteMessage,
755 C::A: Handler<MeshFailure>,
756 {
757 let id = ActorMeshId::singleton(Label::strip(name));
758 self.spawn_with_name(cx, id, params, None, false).await
759 }
760
761 #[hyperactor::instrument(fields(
773 host_mesh=self.host_mesh_id().map(|id| id.to_string()),
774 proc_mesh=self.id.to_string(),
775 actor_name=name.to_string(),
776 ))]
777 pub async fn spawn_controllerless_service<A: RemoteSpawn, C: context::Actor>(
778 &self,
779 cx: &C,
780 name: &str,
781 params: &A::Params,
782 ) -> crate::Result<ActorMesh<A>>
783 where
784 A::Params: RemoteMessage,
785 {
786 tracing::info!(
787 name = "ProcMeshStatus",
788 status = "ActorMesh::Spawn::Attempt",
789 );
790 tracing::info!(name = "ActorMeshStatus", status = "Spawn::Attempt");
791 let id = ActorMeshId::singleton(Label::strip(name));
792 let result = self
793 .spawn_managed_mesh_inner(cx, id, params, None)
794 .await
795 .map(|(mesh, _statuses)| mesh);
796 match &result {
797 Ok(_) => {
798 tracing::info!(
799 name = "ProcMeshStatus",
800 status = "ActorMesh::Spawn::Success",
801 );
802 tracing::info!(name = "ActorMeshStatus", status = "Spawn::Success");
803 }
804 Err(error) => {
805 tracing::error!(name = "ProcMeshStatus", status = "ActorMesh::Spawn::Failed", %error);
806 tracing::error!(name = "ActorMeshStatus", status = "Spawn::Failed", %error);
807 }
808 }
809 result
810 }
811
812 pub async fn spawn_data<A: RemoteSpawn, C: context::Actor>(
828 &self,
829 cx: &C,
830 actor_mesh_id: ActorMeshId,
831 params: &A::Params,
832 ) -> crate::Result<ActorMesh<A>>
833 where
834 A::Params: RemoteMessage,
835 {
836 if !actor_mesh_id.uid().is_instance() {
837 return Err(Error::ConfigurationError(anyhow::anyhow!(
838 "data-backed actor meshes require instance ids"
839 )));
840 }
841 self.spawn_data_mesh_inner(cx, params, actor_mesh_id)
842 }
843
844 fn spawn_data_mesh_inner<A: RemoteSpawn, C: context::Actor>(
845 &self,
846 cx: &C,
847 params: &A::Params,
848 actor_mesh_id: ActorMeshId,
849 ) -> crate::Result<ActorMesh<A>>
850 where
851 A::Params: RemoteMessage,
852 {
853 let region = self.region().clone();
854 let mut members = Vec::with_capacity(self.ranks.len());
855 let mut stop_handles = Vec::with_capacity(self.ranks.len());
856 let serialized_params = bincode::serde::encode_to_vec(params, bincode::config::legacy())?;
857
858 let stop_spawned = |handles: &[ActorMeshStopHandle], reason: &str| {
859 for handle in handles {
860 let _ = handle.stop(reason);
861 }
862 };
863
864 for (rank, proc_ref) in self.ranks.iter().enumerate() {
867 let params = match bincode::serde::decode_from_slice(
868 &serialized_params,
869 bincode::config::legacy(),
870 ) {
871 Ok((params, _)) => params,
872 Err(error) => {
873 stop_spawned(&stop_handles, "data mesh spawn failed");
874 return Err(error.into());
875 }
876 };
877 let point = region
878 .extent()
879 .point_of_rank(rank)
880 .expect("proc mesh rank must have a point");
881 let mut environment = cx.instance().actor_environment().clone();
882 if let Err(error) = environment.set(CAST_POINT, point.clone()) {
883 stop_spawned(&stop_handles, "data mesh spawn failed");
884 return Err(Error::Other(error.into()));
885 }
886 let mut labels = Flattrs::new();
890 labels.set(CAST_POINT, point);
891 labels.set(ACTOR_MESH_ID, actor_mesh_id.clone());
892 let actor_spawner = proc_ref.spawner();
893 let (actor_ref, stop_handle) = match actor_spawner.spawn_uid_with_link_and_ready(
894 cx,
895 actor_mesh_id.uid().clone(),
896 params,
897 KeepaliveLink::default(),
898 None,
899 environment,
900 labels,
901 ) {
902 Ok(spawned) => spawned,
903 Err(error) => {
904 stop_spawned(&stop_handles, "data mesh spawn failed");
905 return Err(Error::Other(error));
906 }
907 };
908 members.push(actor_ref);
909 stop_handles.push(stop_handle.into_any());
910 }
911
912 ActorMesh::try_new_data(actor_mesh_id, region, members, stop_handles)
913 }
914
915 #[hyperactor::instrument(fields(
933 host_mesh=self.host_mesh_id().map(|id| id.to_string()),
934 proc_mesh=self.id.to_string(),
935 actor_name=name.to_string(),
936 ))]
937 pub async fn spawn_with_name<A: RemoteSpawn, C: context::Actor>(
938 &self,
939 cx: &C,
940 name: ActorMeshId,
941 params: &A::Params,
942 supervision_display_name: Option<String>,
943 is_system_actor: bool,
944 ) -> crate::Result<ActorMesh<A>>
945 where
946 A::Params: RemoteMessage,
947 C::A: Handler<MeshFailure>,
948 {
949 tracing::info!(
950 name = "ProcMeshStatus",
951 status = "ActorMesh::Spawn::Attempt",
952 );
953 tracing::info!(name = "ActorMeshStatus", status = "Spawn::Attempt");
954 let result = self
955 .spawn_with_name_inner(cx, name, params, supervision_display_name, is_system_actor)
956 .await;
957 match &result {
958 Ok(_) => {
959 tracing::info!(
960 name = "ProcMeshStatus",
961 status = "ActorMesh::Spawn::Success",
962 );
963 tracing::info!(name = "ActorMeshStatus", status = "Spawn::Success");
964 }
965 Err(error) => {
966 tracing::error!(name = "ProcMeshStatus", status = "ActorMesh::Spawn::Failed", %error);
967 tracing::error!(name = "ActorMeshStatus", status = "Spawn::Failed", %error);
968 }
969 }
970 result
971 }
972
973 async fn spawn_with_name_inner<A: RemoteSpawn, C: context::Actor>(
974 &self,
975 cx: &C,
976 actor_mesh_id: ActorMeshId,
977 params: &A::Params,
978 supervision_display_name: Option<String>,
979 is_system_actor: bool,
980 ) -> crate::Result<ActorMesh<A>>
981 where
982 C::A: Handler<MeshFailure>,
983 {
984 let spawned = self
985 .spawn_mesh_inner(
986 cx,
987 actor_mesh_id,
988 params,
989 supervision_display_name.clone(),
990 is_system_actor,
991 )
992 .await?;
993 match spawned {
994 SpawnedActorMesh::Data(mesh) => Ok(mesh),
995 SpawnedActorMesh::Managed { mut mesh, statuses } => {
996 if !is_system_actor {
999 let controller: ActorMeshController<A> = ActorMeshController::new(
1002 ActorMeshControlPlane::new(mesh.deref().clone(), self.clone()),
1003 supervision_display_name,
1004 Some(cx.instance().port().bind()),
1005 statuses,
1006 );
1007 let controller_name = format!(
1012 "{}_{}",
1013 crate::mesh_controller::ACTOR_MESH_CONTROLLER_NAME,
1014 mesh.id()
1015 );
1016 let controller = cx.spawn_with_label(&controller_name, controller);
1017 mesh.set_controller(Some(controller.bind()));
1020 }
1021 Ok(mesh)
1022 }
1023 }
1024 }
1025
1026 async fn spawn_mesh_inner<A: RemoteSpawn, C: context::Actor>(
1027 &self,
1028 cx: &C,
1029 actor_mesh_id: ActorMeshId,
1030 params: &A::Params,
1031 supervision_display_name: Option<String>,
1032 is_system_actor: bool,
1033 ) -> crate::Result<SpawnedActorMesh<A>> {
1034 if hyperactor_config::global::get(USE_DIRECT_SPAWN)
1035 && !is_system_actor
1036 && actor_mesh_id.uid().is_instance()
1037 {
1038 return self
1039 .spawn_data_mesh_inner(cx, params, actor_mesh_id)
1040 .map(SpawnedActorMesh::Data);
1041 }
1042
1043 let (mesh, statuses) = self
1044 .spawn_managed_mesh_inner(cx, actor_mesh_id, params, supervision_display_name)
1045 .await?;
1046 Ok(SpawnedActorMesh::Managed { mesh, statuses })
1047 }
1048
1049 async fn spawn_managed_mesh_inner<A: RemoteSpawn, C: context::Actor>(
1060 &self,
1061 cx: &C,
1062 actor_mesh_id: ActorMeshId,
1063 params: &A::Params,
1064 supervision_display_name: Option<String>,
1065 ) -> crate::Result<(ActorMesh<A>, crate::StatusMesh)> {
1066 let remote = Remote::collect();
1067 let actor_type = remote
1071 .name_of::<A>()
1072 .ok_or(Error::ActorTypeNotRegistered(type_name::<A>().to_string()))?
1073 .to_string();
1074
1075 let serialized_params = bincode::serde::encode_to_vec(params, bincode::config::legacy())?;
1076 self.proc_agent_mesh.cast(
1077 cx,
1078 resource::CreateOrUpdate::<proc_agent::ActorSpec> {
1079 id: actor_mesh_id.resource_id().clone(),
1080 rank: Default::default(),
1081 spec: proc_agent::ActorSpec {
1082 actor_type: actor_type.clone(),
1083 params_data: serialized_params.clone(),
1084 actor_environment: cx.instance().actor_environment().clone(),
1085 },
1086 },
1087 )?;
1088
1089 let region = self.region().clone();
1090 let (port, rx) = cx.mailbox().open_idle_flush_accum_port(
1100 crate::StatusMesh::from_single(region.clone(), Status::NotExist),
1103 IdleFlushReducerOpts {
1104 idle_timeout: Duration::from_millis(50),
1105 abandon_timeout: Duration::from_secs(30),
1106 expected_updates_per_destination: NonZeroUsize::MIN,
1107 },
1108 );
1109
1110 let mut reply = port.bind();
1111 reply.return_undeliverable(false);
1114 self.proc_agent_mesh.cast(
1117 cx,
1118 resource::GetRankStatus {
1119 id: actor_mesh_id.resource_id().clone(),
1120 rank: Default::default(),
1122 reply,
1123 },
1124 )?;
1125
1126 let start_time = tokio::time::Instant::now();
1127
1128 let statuses = match GetRankStatus::wait(
1137 rx,
1138 self.ranks.len(),
1139 hyperactor_config::global::get(ACTOR_SPAWN_MAX_IDLE),
1140 region.clone(), )
1142 .await
1143 {
1144 Ok(statuses) => {
1145 let has_terminating = statuses.values().any(|s| s.is_terminating());
1149 if !has_terminating {
1150 Ok(statuses)
1151 } else {
1152 let legacy = mesh_to_rankedvalues_with_default(
1153 &statuses,
1154 Status::NotExist,
1155 Status::is_not_exist,
1156 self.ranks.len(),
1157 );
1158 Err(Error::ActorSpawnError { statuses: legacy })
1159 }
1160 }
1161 Err(complete) => {
1162 let elapsed = start_time.elapsed();
1165 let legacy = mesh_to_rankedvalues_with_default(
1166 &complete,
1167 Status::Timeout(elapsed),
1168 Status::is_not_exist,
1169 self.ranks.len(),
1170 );
1171 Err(Error::ActorSpawnError { statuses: legacy })
1172 }
1173 }?;
1174
1175 let actor_mesh_members = Arc::new(
1176 self.ranks
1177 .iter()
1178 .map(|rank| rank.actor_addr(&actor_mesh_id))
1179 .collect_mesh::<ValueMesh<_>>(self.region().clone())
1180 .map_err(|error| crate::Error::ConfigurationError(error.into()))?,
1181 );
1182
1183 let mesh = ActorMesh::new(
1184 self.clone(),
1185 actor_mesh_id.clone(),
1186 None,
1187 actor_mesh_members,
1188 );
1189 {
1191 let id_str = actor_mesh_id.to_string();
1192
1193 let parent_mesh_id_hash = hash_to_u64(self.id());
1195 let mesh_id_hash = telemetry_actor_mesh_id(self.id(), &actor_mesh_id);
1196
1197 hyperactor_telemetry::notify_mesh_created(hyperactor_telemetry::MeshEvent {
1198 id: mesh_id_hash,
1199 timestamp: std::time::SystemTime::now(),
1200 class: supervision_display_name
1201 .as_deref()
1202 .and_then(python_class_from_supervision_name)
1203 .unwrap_or(actor_type),
1204 given_name: actor_mesh_id
1205 .display_label()
1206 .map(|l| l.as_str())
1207 .unwrap_or("unnamed")
1208 .to_string(),
1209 full_name: id_str,
1210 shape_json: serde_json::to_string(&self.region().extent()).unwrap_or_default(),
1211 parent_mesh_id: Some(parent_mesh_id_hash),
1212 parent_view_json: serde_json::to_string(self.region()).ok(),
1213 });
1214
1215 let now = std::time::SystemTime::now();
1219 for (rank, proc_ref) in self.ranks.iter().enumerate() {
1220 let display_name = supervision_display_name.as_ref().map(|sdn| {
1221 let point = self.region().extent().point_of_rank(rank).unwrap();
1222 crate::actor_display_name(sdn, &point)
1223 });
1224 let actor_addr = proc_ref.actor_addr(&actor_mesh_id);
1225 hyperactor_telemetry::notify_actor_created(hyperactor_telemetry::ActorEvent {
1226 id: hyperactor_telemetry::hash_to_u64(actor_addr.id()),
1227 timestamp: now,
1228 mesh_id: mesh_id_hash,
1229 rank: rank as u64,
1230 full_name: actor_addr.to_string(),
1231 display_name,
1232 });
1233 }
1234 }
1235
1236 Ok((mesh, statuses))
1237 }
1238
1239 #[hyperactor::instrument(fields(
1241 host_mesh = self.host_mesh_id().map(|id| id.to_string()),
1242 proc_mesh = self.id.to_string(),
1243 actor_mesh = actor_mesh_id.to_string(),
1244 ))]
1245 pub(crate) async fn stop_actor_by_id(
1246 &self,
1247 cx: &impl context::Actor,
1248 actor_mesh_id: ActorMeshId,
1249 reason: String,
1250 ) -> crate::Result<ValueMesh<Status>> {
1251 tracing::info!(name = "ProcMeshStatus", status = "ActorMesh::Stop::Attempt");
1252 tracing::info!(name = "ActorMeshStatus", status = "Stop::Attempt");
1253 let result = self.stop_actor_by_id_inner(cx, actor_mesh_id, reason).await;
1254 match &result {
1255 Ok(_) => {
1256 tracing::info!(name = "ProcMeshStatus", status = "ActorMesh::Stop::Success");
1257 tracing::info!(name = "ActorMeshStatus", status = "Stop::Success");
1258 }
1259 Err(error) => {
1260 tracing::error!(name = "ProcMeshStatus", status = "ActorMesh::Stop::Failed", %error);
1261 tracing::error!(name = "ActorMeshStatus", status = "Stop::Failed", %error);
1262 }
1263 }
1264 result
1265 }
1266
1267 async fn stop_actor_by_id_inner(
1268 &self,
1269 cx: &impl context::Actor,
1270 actor_mesh_id: ActorMeshId,
1271 reason: String,
1272 ) -> crate::Result<ValueMesh<Status>> {
1273 let region = self.region().clone();
1274 self.proc_agent_mesh.cast(
1275 cx,
1276 resource::Stop {
1277 id: actor_mesh_id.resource_id().clone(),
1278 reason,
1279 },
1280 )?;
1281
1282 let (port, rx) = cx.mailbox().open_idle_flush_accum_port(
1292 crate::StatusMesh::from_single(region.clone(), Status::NotExist),
1295 IdleFlushReducerOpts {
1296 idle_timeout: Duration::from_millis(50),
1297 abandon_timeout: Duration::from_secs(30),
1298 expected_updates_per_destination: NonZeroUsize::MIN,
1299 },
1300 );
1301 self.proc_agent_mesh.cast(
1305 cx,
1306 resource::WaitRankStatus {
1307 id: actor_mesh_id.resource_id().clone(),
1308 rank: Default::default(),
1310 min_status: Status::Stopped,
1311 reply: port.bind(),
1312 },
1313 )?;
1314 let start_time = tokio::time::Instant::now();
1315
1316 let max_idle_time = hyperactor_config::global::get(ACTOR_SPAWN_MAX_IDLE);
1318 match GetRankStatus::wait(
1319 rx,
1320 self.ranks.len(),
1321 max_idle_time,
1322 region.clone(), )
1324 .await
1325 {
1326 Ok(statuses) => {
1327 let all_stopped = statuses.values().all(|s| s.is_terminating());
1331 if all_stopped {
1332 Ok(statuses)
1333 } else {
1334 let legacy = mesh_to_rankedvalues_with_default(
1335 &statuses,
1336 Status::NotExist,
1337 Status::is_not_exist,
1338 self.ranks.len(),
1339 );
1340 Err(Error::ActorStopError { statuses: legacy })
1341 }
1342 }
1343 Err(complete) => {
1344 let legacy = mesh_to_rankedvalues_with_default(
1347 &complete,
1348 Status::Timeout(start_time.elapsed()),
1349 Status::is_not_exist,
1350 self.ranks.len(),
1351 );
1352 Err(Error::ActorStopError { statuses: legacy })
1353 }
1354 }
1355 }
1356}
1357
1358impl fmt::Display for ProcMeshRef {
1359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1360 write!(f, "{}{{{}}}", self.id, self.region)
1361 }
1362}
1363
1364impl view::Ranked for ProcMeshRef {
1365 type Item = ProcRef;
1366
1367 fn region(&self) -> &Region {
1368 &self.region
1369 }
1370
1371 fn get(&self, rank: usize) -> Option<&Self::Item> {
1372 self.ranks.get(rank)
1373 }
1374}
1375
1376impl view::RankedSliceable for ProcMeshRef {
1377 fn sliced(&self, region: Region) -> Self {
1382 debug_assert!(region.is_subset(view::Ranked::region(self)));
1383 let ranks = self
1384 .region()
1385 .remap(®ion)
1386 .unwrap()
1387 .map(|index| self.get(index).unwrap().clone())
1388 .collect::<Vec<_>>();
1389
1390 Self {
1391 id: self.id.clone(),
1392 proc_agent_mesh: self.proc_agent_mesh.sliced(region.clone()),
1393 region,
1394 ranks: Arc::new(ranks),
1395 host_mesh: self.host_mesh.clone(),
1396 }
1397 }
1398}
1399
1400fn python_class_from_supervision_name(sdn: &str) -> Option<String> {
1416 let inner = sdn.rsplit_once('<')?.1.strip_suffix('>')?;
1417 let qualified = inner.split_whitespace().next()?;
1418 let class_name = qualified.rsplit_once('.')?.1;
1419 Some(format!("Python<{class_name}>"))
1420}
1421
1422#[cfg(test)]
1423mod tests {
1424 #[cfg(fbcode_build)]
1425 use std::collections::HashMap;
1426 #[cfg(fbcode_build)]
1427 use std::collections::HashSet;
1428 #[cfg(fbcode_build)]
1429 use std::net::IpAddr;
1430 #[cfg(fbcode_build)]
1431 use std::net::Ipv4Addr;
1432 #[cfg(fbcode_build)]
1433 use std::net::SocketAddr;
1434 #[cfg(fbcode_build)]
1435 use std::ops::Deref;
1436 #[cfg(fbcode_build)]
1437 use std::time::Duration;
1438
1439 #[cfg(fbcode_build)]
1440 use hyperactor::ActorEnvironment;
1441 #[cfg(fbcode_build)]
1442 use hyperactor::ActorRef;
1443 #[cfg(fbcode_build)]
1444 use hyperactor::Endpoint as _;
1445 #[cfg(fbcode_build)]
1446 use hyperactor::Instance;
1447 #[cfg(fbcode_build)]
1448 use hyperactor::ProcAddr;
1449 #[cfg(fbcode_build)]
1450 use hyperactor::accum::IdleFlushReducerOpts;
1451 #[cfg(fbcode_build)]
1452 use hyperactor::channel::BindSpec;
1453 #[cfg(fbcode_build)]
1454 use hyperactor::channel::ChannelAddr;
1455 #[cfg(fbcode_build)]
1456 use hyperactor::context::Mailbox;
1457 #[cfg(fbcode_build)]
1458 use hyperactor::id::Label;
1459 #[cfg(fbcode_build)]
1460 use hyperactor_config::NonZeroUsize;
1461 #[cfg(fbcode_build)]
1462 use ndslice::ViewExt as _;
1463 #[cfg(fbcode_build)]
1464 use ndslice::extent;
1465 #[cfg(fbcode_build)]
1466 use ndslice::view::Ranked as _;
1467 #[cfg(fbcode_build)]
1468 use timed_test::assert_no_process_leak;
1469 #[cfg(fbcode_build)]
1470 use timed_test::async_timed_test;
1471 #[cfg(fbcode_build)]
1472 use uuid::Uuid;
1473
1474 #[cfg(fbcode_build)]
1475 use super::ACTOR_SPAWN_MAX_IDLE;
1476 #[cfg(fbcode_build)]
1477 use super::ProcMeshRef;
1478 #[cfg(fbcode_build)]
1479 use super::ProcRef;
1480 #[cfg(fbcode_build)]
1481 use super::USE_DIRECT_SPAWN;
1482 #[cfg(fbcode_build)]
1483 use crate::ActorMesh;
1484 #[cfg(fbcode_build)]
1485 use crate::casting::CAST_POINT;
1486 #[cfg(fbcode_build)]
1487 use crate::host_mesh::HostMesh;
1488 #[cfg(fbcode_build)]
1489 use crate::host_mesh::PROC_SPAWN_MAX_IDLE;
1490 #[cfg(fbcode_build)]
1491 use crate::mesh_id::ActorMeshId;
1492 #[cfg(fbcode_build)]
1493 use crate::mesh_id::ProcMeshId;
1494 #[cfg(fbcode_build)]
1495 use crate::proc_agent::PROC_AGENT_ACTOR_NAME;
1496 #[cfg(fbcode_build)]
1497 use crate::proc_agent::ProcAgent;
1498 #[cfg(fbcode_build)]
1499 use crate::resource::RankedValues;
1500 #[cfg(fbcode_build)]
1501 use crate::resource::Status;
1502 #[cfg(fbcode_build)]
1503 use crate::testactor;
1504 #[cfg(fbcode_build)]
1505 use crate::testing;
1506 #[cfg(fbcode_build)]
1507 use crate::transport::DEFAULT_TRANSPORT;
1508
1509 #[cfg(fbcode_build)]
1510 async fn execute_spawn_actor() {
1511 hyperactor_telemetry::initialize_logging(hyperactor_telemetry::DefaultTelemetryClock {});
1512
1513 let instance = testing::instance();
1514
1515 let mut hm = testing::host_mesh(2).await;
1516 let proc_mesh = hm
1517 .spawn(&instance, "test", extent!(gpus = 1), None, None)
1518 .await
1519 .unwrap();
1520 let actor_mesh = proc_mesh.spawn(instance, "test", &()).await.unwrap();
1521 testactor::assert_mesh_shape(actor_mesh).await;
1522
1523 let _ = hm.shutdown(instance).await;
1524 }
1525
1526 #[async_timed_test(timeout_secs = 300)]
1527 #[cfg(fbcode_build)]
1528 async fn spawn_data_uses_default_actor_spawner() {
1529 let instance = testing::instance();
1530 let mut hm = testing::host_mesh(1).await;
1531 let proc_mesh = hm
1532 .spawn(instance, "spawn_data", extent!(gpus = 2), None, None)
1533 .await
1534 .expect("spawn proc mesh");
1535
1536 let mut actor_mesh: ActorMesh<testactor::TestActor> = proc_mesh
1537 .spawn_data(
1538 instance,
1539 ActorMeshId::instance(Label::strip("default_spawner")),
1540 &(),
1541 )
1542 .await
1543 .expect("spawn data actor mesh through default spawner");
1544 assert_eq!(actor_mesh.region().num_ranks(), 2);
1545 let actor_uids: HashSet<_> = actor_mesh
1546 .values()
1547 .map(|actor| actor.actor_addr().id().uid().clone())
1548 .collect();
1549 assert_eq!(
1550 actor_uids.len(),
1551 1,
1552 "all data mesh actors should share one uid"
1553 );
1554
1555 let (port, mut rx) = instance.mailbox().open_port();
1557 for actor in actor_mesh.values() {
1558 actor.post(instance, testactor::GetActorId(port.bind()));
1559 }
1560 let mut replied = HashSet::new();
1561 for _ in 0..actor_mesh.region().num_ranks() {
1562 let (actor_addr, _seq) = tokio::time::timeout(Duration::from_secs(60), rx.recv())
1563 .await
1564 .expect("timed out waiting for a rank to reply")
1565 .expect("reply port closed");
1566 replied.insert(actor_addr);
1567 }
1568 assert_eq!(
1569 replied,
1570 actor_mesh
1571 .values()
1572 .map(|actor| actor.actor_addr().clone())
1573 .collect::<HashSet<_>>(),
1574 "every rank should receive the message sent before it started"
1575 );
1576
1577 let mut actor_mesh_clone = actor_mesh.clone();
1578 actor_mesh
1579 .stop(instance, "test complete".to_string())
1580 .await
1581 .expect("stop data actor mesh");
1582 actor_mesh_clone
1583 .stop(instance, "test complete".to_string())
1584 .await
1585 .expect("stopping a data actor mesh clone should be idempotent");
1586
1587 hm.shutdown(instance)
1588 .await
1589 .expect("host mesh shutdown should complete");
1590 }
1591
1592 #[async_timed_test(timeout_secs = 300)]
1593 #[cfg(fbcode_build)]
1594 async fn spawn_uses_direct_path_when_configured_but_services_remain_managed() {
1595 let config = hyperactor_config::global::lock();
1596 let _mode = config.override_key(USE_DIRECT_SPAWN, true);
1597 let instance = testing::instance();
1598 let mut hm = testing::host_mesh(1).await;
1599 let proc_mesh = hm
1600 .spawn(
1601 instance,
1602 "configured_data_spawn",
1603 extent!(gpus = 2),
1604 None,
1605 None,
1606 )
1607 .await
1608 .expect("spawn proc mesh");
1609
1610 let mut actor_mesh: ActorMesh<testactor::TestActor> = proc_mesh
1611 .spawn(instance, "configured_data_actor", &())
1612 .await
1613 .expect("initiate configured data actor mesh spawn");
1614 assert!(
1615 actor_mesh.as_managed().is_none(),
1616 "configured instance spawn should return a data mesh"
1617 );
1618 assert_eq!(
1619 actor_mesh.region().num_ranks(),
1620 2,
1621 "direct spawn should cover all proc mesh ranks"
1622 );
1623 assert!(
1624 actor_mesh.values().all(|actor| {
1625 actor.actor_addr().label().map(|label| label.as_str())
1626 == Some("configured_data_actor")
1627 }),
1628 "direct spawn should preserve the requested actor label"
1629 );
1630
1631 let service: ActorMesh<testactor::TestActor> = proc_mesh
1632 .spawn_service(instance, "configured_managed_service", &())
1633 .await
1634 .expect("spawn managed service");
1635 assert!(
1636 service.as_managed().is_some(),
1637 "service spawn should ignore the data mesh flag"
1638 );
1639
1640 let system_actor: ActorMesh<testactor::TestActor> = proc_mesh
1641 .spawn_with_name(
1642 instance,
1643 ActorMeshId::instance(Label::strip("configured_managed_system_actor")),
1644 &(),
1645 None,
1646 true,
1647 )
1648 .await
1649 .expect("spawn managed system actor");
1650 assert!(
1651 system_actor.as_managed().is_some(),
1652 "system actor spawn should ignore the direct spawn flag"
1653 );
1654
1655 actor_mesh
1656 .stop(instance, "test complete".to_string())
1657 .await
1658 .expect("stop configured data actor mesh");
1659 hm.shutdown(instance)
1660 .await
1661 .expect("host mesh shutdown should complete");
1662 }
1663
1664 #[async_timed_test(timeout_secs = 300)]
1665 #[cfg(fbcode_build)]
1666 async fn actor_environment_survives_two_remote_hops_and_shapes_cast_point() {
1667 const SENTINEL: u64 = 0xA0FE;
1668 let operation_timeout = Duration::from_secs(120);
1669
1670 let config = hyperactor_config::global::lock();
1671 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, operation_timeout);
1672 let _guard2 = config.override_key(
1673 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
1674 operation_timeout,
1675 );
1676
1677 let base = testing::instance();
1678 let persistent_point = extent!(persistent = 3).point_of_rank(2).unwrap();
1679 let mut environment = ActorEnvironment::default();
1680 environment
1681 .set(testactor::ACTOR_ENVIRONMENT_TEST_TAG, SENTINEL)
1682 .expect("seed persistent tag");
1683 environment
1684 .set(CAST_POINT, persistent_point.clone())
1685 .expect("seed persistent cast-point collision");
1686 let root = base
1687 .proc()
1688 .actor_instance_in_environment::<testing::TestRootClient>(
1689 &format!("actor_environment_root_{}", Uuid::now_v7()),
1690 environment,
1691 )
1692 .expect("create seeded root instance");
1693
1694 let mut hm = testing::host_mesh(1).await;
1695 let proc_mesh = hm
1696 .spawn(
1697 &root.instance,
1698 "actor_environment",
1699 extent!(gpus = 2),
1700 None,
1701 None,
1702 )
1703 .await
1704 .expect("spawn proc mesh");
1705 let first_hop = proc_mesh
1706 .range("gpus", 0..1)
1707 .expect("first proc-mesh slice");
1708 let second_hop = proc_mesh
1709 .range("gpus", 1..2)
1710 .expect("second proc-mesh slice");
1711 let first_point = first_hop
1712 .region()
1713 .extent()
1714 .point_of_rank(0)
1715 .expect("first slice has rank zero");
1716 let second_point = second_hop
1717 .region()
1718 .extent()
1719 .point_of_rank(0)
1720 .expect("second slice has rank zero");
1721 let first_proc_addr = first_hop
1722 .get(0)
1723 .expect("first proc")
1724 .proc_addr()
1725 .to_string();
1726 let second_proc_addr = second_hop
1727 .get(0)
1728 .expect("second proc")
1729 .proc_addr()
1730 .to_string();
1731 assert_ne!(
1732 first_proc_addr, second_proc_addr,
1733 "the two hops must target distinct proc addresses",
1734 );
1735 assert!(
1736 persistent_point != first_point && persistent_point != second_point,
1737 "seeded and assigned cast points must be distinct",
1738 );
1739
1740 let (reply, mut replies) = root
1741 .instance
1742 .open_port::<testactor::ActorEnvironmentObservation>();
1743 first_hop
1744 .spawn_controllerless_service::<testactor::ActorEnvironmentProbe, _>(
1745 &root.instance,
1746 "actor_environment_hop_1",
1747 &testactor::ActorEnvironmentProbeParams {
1748 label: "hop_1".to_string(),
1749 reply: reply.bind(),
1750 nested: Some((
1751 second_hop,
1752 "actor_environment_hop_2".to_string(),
1753 "hop_2".to_string(),
1754 )),
1755 },
1756 )
1757 .await
1758 .expect("spawn first environment probe");
1759
1760 let expected = HashMap::from([
1761 ("hop_1".to_string(), (first_point, first_proc_addr)),
1762 ("hop_2".to_string(), (second_point, second_proc_addr)),
1763 ]);
1764 let mut seen = HashSet::new();
1765 let reply_deadline = tokio::time::Instant::now() + operation_timeout;
1766 for _ in 0..2 {
1767 let observed = tokio::time::timeout_at(reply_deadline, replies.recv())
1768 .await
1769 .expect("environment probe reply timed out")
1770 .expect("environment probe reply port closed");
1771 let (expected_point, expected_proc_addr) = expected
1772 .get(&observed.label)
1773 .unwrap_or_else(|| panic!("unexpected probe label {}", observed.label));
1774 assert!(
1775 seen.insert(observed.label.clone()),
1776 "received duplicate observation from {}",
1777 observed.label,
1778 );
1779 assert_eq!(
1780 observed.persistent_tag,
1781 Some(SENTINEL),
1782 "{} must inherit the persistent tag",
1783 observed.label,
1784 );
1785 assert_eq!(
1786 observed.constructor_tag,
1787 Some(SENTINEL),
1788 "{} constructor must see the persistent tag",
1789 observed.label,
1790 );
1791 assert_eq!(
1792 observed.stored_point,
1793 Some(expected_point.clone()),
1794 "{} must store its caller-shaped construction point",
1795 observed.label,
1796 );
1797 assert_eq!(
1798 observed.constructor_point,
1799 Some(expected_point.clone()),
1800 "{} constructor must see its real rank-local cast point",
1801 observed.label,
1802 );
1803 assert_eq!(
1804 &observed.proc_addr, expected_proc_addr,
1805 "{} must run on its requested proc",
1806 observed.label,
1807 );
1808 }
1809 assert_eq!(seen.len(), expected.len(), "both hops must report");
1810
1811 hm.shutdown(&root.instance)
1812 .await
1813 .expect("host mesh shutdown should complete");
1814 }
1815
1816 #[async_timed_test(timeout_secs = 300)]
1817 #[cfg(fbcode_build)]
1818 async fn direct_spawn_shapes_cast_point() {
1819 const SENTINEL: u64 = 0xD1EC;
1820 let config = hyperactor_config::global::lock();
1821 let _mode = config.override_key(USE_DIRECT_SPAWN, true);
1822
1823 let base = testing::instance();
1824 let persistent_point = extent!(persistent = 3).point_of_rank(2).unwrap();
1825 let mut environment = ActorEnvironment::default();
1826 environment
1827 .set(testactor::ACTOR_ENVIRONMENT_TEST_TAG, SENTINEL)
1828 .expect("seed persistent tag");
1829 environment
1830 .set(CAST_POINT, persistent_point.clone())
1831 .expect("seed persistent cast-point collision");
1832 let root = base
1833 .proc()
1834 .actor_instance_in_environment::<testing::TestRootClient>(
1835 &format!("direct_spawn_root_{}", Uuid::now_v7()),
1836 environment,
1837 )
1838 .expect("create seeded root instance");
1839
1840 let mut hm = testing::host_mesh(1).await;
1841 let proc_mesh = hm
1842 .spawn(
1843 &root.instance,
1844 "direct_spawn_env",
1845 extent!(gpus = 2),
1846 None,
1847 None,
1848 )
1849 .await
1850 .expect("spawn proc mesh");
1851 let expected: HashMap<_, _> = (0..proc_mesh.region().num_ranks())
1852 .map(|rank| {
1853 (
1854 proc_mesh.get(rank).expect("proc").proc_addr().to_string(),
1855 proc_mesh.region().extent().point_of_rank(rank).unwrap(),
1856 )
1857 })
1858 .collect();
1859 assert!(
1860 !expected.values().any(|point| *point == persistent_point),
1861 "seeded and assigned cast points must be distinct",
1862 );
1863
1864 let (reply, mut replies) = root
1865 .instance
1866 .open_port::<testactor::ActorEnvironmentObservation>();
1867 let mut actor_mesh: ActorMesh<testactor::ActorEnvironmentProbe> = proc_mesh
1868 .spawn_with_name(
1869 &root.instance,
1870 ActorMeshId::instance(Label::strip("direct_spawn_probe")),
1871 &testactor::ActorEnvironmentProbeParams {
1872 label: "direct".to_string(),
1873 reply: reply.bind(),
1874 nested: None,
1875 },
1876 None,
1877 false,
1878 )
1879 .await
1880 .expect("spawn direct environment probes");
1881 assert!(
1882 actor_mesh.as_managed().is_none(),
1883 "configured instance spawn should return a data mesh"
1884 );
1885
1886 let mut seen = HashSet::new();
1887 for _ in 0..expected.len() {
1888 let observed = tokio::time::timeout(Duration::from_secs(120), replies.recv())
1889 .await
1890 .expect("environment probe reply timed out")
1891 .expect("environment probe reply port closed");
1892 let expected_point = expected
1893 .get(&observed.proc_addr)
1894 .unwrap_or_else(|| panic!("unexpected probe proc {}", observed.proc_addr));
1895 assert!(
1896 seen.insert(observed.proc_addr.clone()),
1897 "received duplicate observation from {}",
1898 observed.proc_addr,
1899 );
1900 assert_eq!(
1901 observed.constructor_tag,
1902 Some(SENTINEL),
1903 "constructor must see the persistent tag"
1904 );
1905 assert_eq!(
1906 observed.constructor_point.as_ref(),
1907 Some(expected_point),
1908 "constructor must see its rank-local point, not the caller's"
1909 );
1910 assert_eq!(
1911 observed.stored_point.as_ref(),
1912 Some(expected_point),
1913 "instance must store its rank-local point"
1914 );
1915 }
1916
1917 actor_mesh
1918 .stop(&root.instance, "test complete".to_string())
1919 .await
1920 .expect("stop direct environment probes");
1921 hm.shutdown(&root.instance)
1922 .await
1923 .expect("host mesh shutdown should complete");
1924 }
1925
1926 #[async_timed_test(timeout_secs = 120)]
1945 #[cfg(fbcode_build)]
1946 async fn test_singleton_service_reuse_over_overlapping_views() {
1947 let instance = testing::instance();
1948 let mut hm = testing::host_mesh(1).await;
1949 let proc_mesh = hm
1950 .spawn(&instance, "test", extent!(gpus = 2), None, None)
1951 .await
1952 .unwrap();
1953 let whole = proc_mesh.deref().clone();
1954 let slice = proc_mesh.range("gpus", 1..2).unwrap();
1955
1956 fn assert_shared(
1958 whole_mesh: &ActorMesh<testactor::TestActor>,
1959 slice_mesh: &ActorMesh<testactor::TestActor>,
1960 ) {
1961 let whole_ids: HashSet<_> = whole_mesh
1962 .values()
1963 .map(|actor_ref| actor_ref.actor_addr().id().clone())
1964 .collect();
1965 let slice_ids: HashSet<_> = slice_mesh
1966 .values()
1967 .map(|actor_ref| actor_ref.actor_addr().id().clone())
1968 .collect();
1969 assert_eq!(whole_ids.len(), 2, "whole view spans two procs");
1970 assert_eq!(slice_ids.len(), 1, "slice spans one proc");
1971 assert!(
1972 slice_ids.is_subset(&whole_ids),
1973 "slice reuses the shared proc's actor: {slice_ids:?} vs {whole_ids:?}",
1974 );
1975 assert_eq!(
1976 whole_ids.union(&slice_ids).count(),
1977 2,
1978 "no new actors created for the overlapping slice",
1979 );
1980 }
1981
1982 let w1 = whole
1984 .spawn_service::<testactor::TestActor, _>(instance, "svc_whole_first", &())
1985 .await
1986 .unwrap();
1987 let s1 = slice
1988 .spawn_service::<testactor::TestActor, _>(instance, "svc_whole_first", &())
1989 .await
1990 .unwrap();
1991 assert_shared(&w1, &s1);
1992 let s1_states = slice.actor_states(instance, s1.id().clone()).await.unwrap();
1993 for rank in 0..slice.region().num_ranks() {
1994 assert_eq!(
1995 s1_states
1996 .get(rank)
1997 .and_then(|state| state.state.as_ref())
1998 .map(|state| &state.actor_id),
1999 s1.get(rank).map(|actor| actor.actor_addr()),
2000 "whole-first state is positioned at slice rank {rank}",
2001 );
2002 }
2003
2004 let s2 = slice
2006 .spawn_service::<testactor::TestActor, _>(instance, "svc_slice_first", &())
2007 .await
2008 .unwrap();
2009 let w2 = whole
2010 .spawn_service::<testactor::TestActor, _>(instance, "svc_slice_first", &())
2011 .await
2012 .unwrap();
2013 assert_shared(&w2, &s2);
2014 let w2_states = whole.actor_states(instance, w2.id().clone()).await.unwrap();
2015 for rank in 0..whole.region().num_ranks() {
2016 assert_eq!(
2017 w2_states
2018 .get(rank)
2019 .and_then(|state| state.state.as_ref())
2020 .map(|state| &state.actor_id),
2021 w2.get(rank).map(|actor| actor.actor_addr()),
2022 "slice-first state is positioned at whole-view rank {rank}",
2023 );
2024 }
2025
2026 whole
2032 .spawn_service::<testactor::TestActor, _>(instance, "svc_wait", &())
2033 .await
2034 .unwrap();
2035 let sw = slice
2036 .spawn_service::<testactor::TestActor, _>(instance, "svc_wait", &())
2037 .await
2038 .unwrap();
2039 let id = sw.id().resource_id().clone();
2040 let region = slice.region().clone();
2041 let num_ranks = region.num_ranks();
2042 let (port, rx) = instance.mailbox().open_idle_flush_accum_port(
2043 crate::StatusMesh::from_single(region.clone(), Status::NotExist),
2044 IdleFlushReducerOpts {
2045 idle_timeout: Duration::from_millis(50),
2046 abandon_timeout: Duration::from_secs(30),
2047 expected_updates_per_destination: NonZeroUsize::MIN,
2048 },
2049 );
2050 slice
2051 .agent_mesh()
2052 .cast(
2053 instance,
2054 crate::resource::WaitRankStatus {
2055 id: id.clone(),
2056 rank: Default::default(),
2058 min_status: Status::Stopped,
2059 reply: port.bind(),
2060 },
2061 )
2062 .unwrap();
2063 slice
2064 .agent_mesh()
2065 .cast(
2066 instance,
2067 crate::resource::Stop {
2068 id: id.clone(),
2069 reason: "test".to_string(),
2070 },
2071 )
2072 .unwrap();
2073 let statuses = crate::resource::GetRankStatus::wait(
2074 rx,
2075 num_ranks,
2076 hyperactor_config::global::get(ACTOR_SPAWN_MAX_IDLE),
2077 region,
2078 )
2079 .await
2080 .unwrap();
2081 assert!(
2082 statuses.values().all(|s| s.is_terminating()),
2083 "deferred waiters flushed at slice-local ranks: {statuses:?}",
2084 );
2085
2086 hm.shutdown(instance)
2087 .await
2088 .expect("host mesh shutdown should complete");
2089 }
2090
2091 #[async_timed_test(timeout_secs = 120)]
2094 #[cfg(fbcode_build)]
2095 async fn test_actor_states_matches_actor_identity_across_ipv4_and_ipv6() {
2096 let instance = testing::instance();
2097 let config = hyperactor_config::global::lock();
2098 let _guard = config.override_key(
2099 DEFAULT_TRANSPORT,
2100 BindSpec::Addr(ChannelAddr::Tcp("[::1]:0".parse().unwrap())),
2101 );
2102 let mut host_mesh = HostMesh::local().await.unwrap();
2103 let mut worker_mesh = host_mesh
2104 .spawn(
2105 instance,
2106 "location_mismatch_worker",
2107 extent!(gpus = 1),
2108 None,
2109 None,
2110 )
2111 .await
2112 .unwrap();
2113
2114 let worker_proc_addr = worker_mesh.get(0).unwrap().proc_addr().clone();
2115 let ChannelAddr::Tcp(worker_socket_addr) = worker_proc_addr.addr() else {
2116 panic!("worker proc must use TCP")
2117 };
2118 assert!(worker_socket_addr.is_ipv6(), "worker must use IPv6");
2119 let proc_agent: ActorRef<ProcAgent> =
2120 ActorRef::attest(worker_proc_addr.actor_addr(PROC_AGENT_ACTOR_NAME).clone());
2121 let controller_proc_addr = ProcAddr::new(
2122 worker_proc_addr.id().clone(),
2123 ChannelAddr::Tcp(SocketAddr::new(
2124 IpAddr::V4(Ipv4Addr::LOCALHOST),
2125 worker_socket_addr.port(),
2126 ))
2127 .into(),
2128 );
2129 let controller_view = ProcMeshRef::new_singleton(
2130 ProcMeshId::singleton(Label::strip("location_mismatch_view")),
2131 ProcRef::new(controller_proc_addr, 0, proc_agent),
2132 )
2133 .unwrap();
2134
2135 let actor_mesh = controller_view
2136 .spawn_controllerless_service::<testactor::TestActor, _>(
2137 instance,
2138 "location_mismatch_actor",
2139 &(),
2140 )
2141 .await
2142 .unwrap();
2143 let expected_actor = actor_mesh.get(0).unwrap().actor_addr();
2144 let states = controller_view
2145 .actor_states(instance, actor_mesh.id().clone())
2146 .await
2147 .unwrap();
2148 let reported_actor = &states
2149 .get(0)
2150 .and_then(|state| state.state.as_ref())
2151 .unwrap()
2152 .actor_id;
2153
2154 assert_ne!(
2155 reported_actor, expected_actor,
2156 "IPv6 worker and IPv4 controller addresses must differ",
2157 );
2158 assert_eq!(
2159 reported_actor.id(),
2160 expected_actor.id(),
2161 "worker and controller addresses must identify the same actor",
2162 );
2163 assert!(
2164 matches!(reported_actor.addr(), ChannelAddr::Tcp(addr) if addr.is_ipv6()),
2165 "worker must report an IPv6 location",
2166 );
2167 assert!(
2168 matches!(expected_actor.addr(), ChannelAddr::Tcp(addr) if addr.is_ipv4()),
2169 "controller must retain an IPv4 location",
2170 );
2171
2172 worker_mesh
2173 .stop(instance, "test complete".to_string())
2174 .await
2175 .unwrap();
2176 host_mesh.shutdown(instance).await.unwrap();
2177 }
2178
2179 #[async_timed_test(timeout_secs = 120)]
2180 #[cfg(fbcode_build)]
2181 async fn test_spawn_actor_casting() {
2182 let config = hyperactor_config::global::lock();
2183 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
2184 let _guard2 = config.override_key(
2185 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2186 Duration::from_secs(120),
2187 );
2188 execute_spawn_actor().await;
2189 }
2190
2191 #[cfg(fbcode_build)]
2195 async fn spawn_for_seq_test(
2196 cx: &Instance<testing::TestRootClient>,
2197 proc_mesh: &super::ProcMeshRef,
2198 ) -> ActorMesh<testactor::TestActor> {
2199 let actor_mesh: ActorMesh<testactor::TestActor> =
2200 proc_mesh.spawn(cx, "test", &()).await.unwrap();
2201
2202 let instance = cx
2203 .proc()
2204 .client(&format!("random_casts_{}", Uuid::now_v7()));
2205 let n = 1;
2206 for _ in 0..n {
2207 actor_mesh.cast(&instance, ()).unwrap();
2208 }
2209 println!(
2210 "did {} casts with sequencer session id {}",
2211 n,
2212 instance.sequencer().session_id()
2213 );
2214 actor_mesh
2215 }
2216
2217 #[async_timed_test(timeout_secs = 60)]
2218 #[cfg(fbcode_build)]
2219 async fn test_seq_from_same_sender_to_different_meshes() {
2220 let config = hyperactor_config::global::lock();
2221 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2222 let _guard2 = config.override_key(
2223 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2224 Duration::from_secs(60),
2225 );
2226
2227 hyperactor_telemetry::initialize_logging_for_test();
2228 let instance = testing::instance();
2229 let session_id = instance.sequencer().session_id();
2230
2231 let mut hm = testing::host_mesh(2).await;
2232 let proc_mesh = hm
2233 .spawn(&instance, "test", extent!(gpus = 1), None, None)
2234 .await
2235 .unwrap();
2236 let proc_mesh_ref = proc_mesh.deref();
2237
2238 let handles = (0..3)
2242 .map(|_| {
2243 let proc_mesh_ref_clone = proc_mesh_ref.clone();
2244 tokio::spawn(async move {
2245 let actor_mesh = spawn_for_seq_test(instance, &proc_mesh_ref_clone).await;
2246 let expected_seqs = vec![1; 2];
2247 testactor::assert_casting_correctness(
2248 &actor_mesh,
2249 instance,
2250 Some((session_id, expected_seqs)),
2251 )
2252 .await;
2253 })
2254 })
2255 .collect::<Vec<_>>();
2256 futures::future::join_all(handles).await;
2257
2258 let _ = hm.shutdown(instance).await;
2259 }
2260
2261 #[async_timed_test(timeout_secs = 60)]
2264 #[cfg(fbcode_build)]
2265 async fn test_seq_from_same_sender_to_different_views() {
2266 let config = hyperactor_config::global::lock();
2267 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2268 let _guard2 = config.override_key(
2269 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2270 Duration::from_secs(60),
2271 );
2272
2273 hyperactor_telemetry::initialize_logging_for_test();
2274
2275 let instance = testing::instance();
2276 let session_id = instance.sequencer().session_id();
2277
2278 let mut hm = testing::host_mesh(3).await;
2279 let proc_mesh = hm
2280 .spawn(&instance, "test", extent!(gpus = 1), None, None)
2281 .await
2282 .unwrap();
2283
2284 let actor_mesh = spawn_for_seq_test(instance, &proc_mesh).await;
2285
2286 let expected_seqs = vec![1; 3];
2288 testactor::assert_casting_correctness(
2289 &actor_mesh,
2290 instance,
2291 Some((session_id, expected_seqs)),
2292 )
2293 .await;
2294
2295 let sliced_actor_mesh = actor_mesh.range("hosts", 1..3).unwrap();
2297 let expected_seqs = vec![2; 2];
2299 testactor::assert_casting_correctness(
2300 &sliced_actor_mesh,
2301 instance,
2302 Some((session_id, expected_seqs)),
2303 )
2304 .await;
2305
2306 let sliced_actor_mesh = actor_mesh.range("hosts", 0..2).unwrap();
2308 let expected_seqs = vec![2, 3];
2312 testactor::assert_casting_correctness(
2313 &sliced_actor_mesh,
2314 instance,
2315 Some((session_id, expected_seqs)),
2316 )
2317 .await;
2318
2319 let _ = hm.shutdown(instance).await;
2320 }
2321
2322 #[async_timed_test(timeout_secs = 60)]
2323 #[cfg(fbcode_build)]
2324 async fn test_seq_from_different_senders() {
2325 let config = hyperactor_config::global::lock();
2326 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2327 let _guard2 = config.override_key(
2328 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2329 Duration::from_secs(60),
2330 );
2331
2332 hyperactor_telemetry::initialize_logging_for_test();
2333
2334 use hyperactor::Proc;
2335 use hyperactor::channel::ChannelTransport;
2336
2337 let proc = Proc::direct(ChannelTransport::Unix.any(), "test_0".to_string()).unwrap();
2338 let instance = proc
2339 .actor_instance::<testing::TestRootClient>("test_client")
2340 .unwrap()
2341 .instance;
2342 let first_instance = proc
2343 .actor_instance::<testing::TestRootClient>("first_client")
2344 .unwrap()
2345 .instance;
2346 let second_instance = proc
2347 .actor_instance::<testing::TestRootClient>("second_client")
2348 .unwrap()
2349 .instance;
2350 let third_instance = proc
2351 .actor_instance::<testing::TestRootClient>("third_client")
2352 .unwrap()
2353 .instance;
2354
2355 let mut hm = testing::host_mesh(2).await;
2356 let proc_mesh = hm
2357 .spawn(&instance, "test", extent!(gpus = 1), None, None)
2358 .await
2359 .unwrap();
2360
2361 let actor_mesh = spawn_for_seq_test(&instance, &proc_mesh).await;
2362
2363 for inst in [&first_instance, &second_instance, &third_instance] {
2366 let expected_seqs = vec![1; 2];
2367 let session_id = inst.sequencer().session_id();
2368 testactor::assert_casting_correctness(
2369 &actor_mesh,
2370 inst,
2371 Some((session_id, expected_seqs)),
2372 )
2373 .await;
2374 }
2375
2376 let _ = hm.shutdown(&instance).await;
2377 }
2378
2379 #[cfg(fbcode_build)]
2380 #[assert_no_process_leak]
2381 #[tokio::test]
2382 async fn test_failing_spawn_actor() {
2383 hyperactor_telemetry::initialize_logging(hyperactor_telemetry::DefaultTelemetryClock {});
2384
2385 let config = hyperactor_config::global::lock();
2386 let _mode = config.override_key(USE_DIRECT_SPAWN, false);
2387 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2388 let _guard2 = config.override_key(
2389 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2390 Duration::from_secs(60),
2391 );
2392
2393 let instance = testing::instance();
2394
2395 let mut hm = testing::host_mesh(1).await;
2396 let proc_mesh = hm
2397 .spawn(&instance, "test", extent!(gpus = 1), None, None)
2398 .await
2399 .unwrap();
2400 let err = proc_mesh
2401 .spawn::<testactor::FailingCreateTestActor, Instance<testing::TestRootClient>>(
2402 instance,
2403 "testfail",
2404 &(),
2405 )
2406 .await
2407 .unwrap_err();
2408 let statuses = err.into_actor_spawn_error().unwrap();
2409 assert_eq!(
2410 statuses,
2411 RankedValues::from((0..1, Status::Failed("test failure".to_string()))),
2412 );
2413
2414 let _ = hm.shutdown(instance).await;
2415 }
2416
2417 #[cfg(fbcode_build)]
2418 #[assert_no_process_leak]
2419 #[tokio::test]
2420 async fn test_failing_spawn_controllerless() {
2421 hyperactor_telemetry::initialize_logging(hyperactor_telemetry::DefaultTelemetryClock {});
2422
2423 let config = hyperactor_config::global::lock();
2424 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(60));
2425 let _guard2 = config.override_key(
2426 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2427 Duration::from_secs(60),
2428 );
2429
2430 let instance = testing::instance();
2431
2432 let mut hm = testing::host_mesh(1).await;
2433 let proc_mesh = hm
2434 .spawn(&instance, "test", extent!(gpus = 1), None, None)
2435 .await
2436 .unwrap();
2437 let err = proc_mesh
2441 .spawn_controllerless_service::<testactor::FailingCreateTestActor, _>(
2442 instance,
2443 "testfail_controllerless",
2444 &(),
2445 )
2446 .await
2447 .unwrap_err();
2448 let statuses = err.into_actor_spawn_error().unwrap();
2449 assert_eq!(
2450 statuses,
2451 RankedValues::from((0..1, Status::Failed("test failure".to_string()))),
2452 );
2453
2454 let _ = hm.shutdown(instance).await;
2455 }
2456
2457 #[async_timed_test(timeout_secs = 60)]
2458 #[cfg(fbcode_build)]
2459 async fn test_spawn_actor_on_proc_mesh_slice_only_spawns_slice_members() {
2460 let instance = testing::instance();
2461
2462 let mut hm = testing::host_mesh(2).await;
2463 let proc_mesh = hm
2464 .spawn(&instance, "test", extent!(gpus = 2), None, None)
2465 .await
2466 .unwrap();
2467 let host1 = proc_mesh.range("hosts", 1..2).unwrap();
2468 let actor_name = crate::mesh_id::ActorMeshId::instance(
2469 hyperactor::id::Label::new("slice_only").unwrap(),
2470 );
2471
2472 let actor_mesh = host1
2473 .spawn_with_name::<testactor::TestActor, _>(
2474 instance,
2475 actor_name.clone(),
2476 &(),
2477 None,
2478 false,
2479 )
2480 .await
2481 .unwrap();
2482 testactor::assert_casting_correctness(&actor_mesh, instance, None).await;
2483
2484 let slice_states = host1
2485 .actor_states(instance, actor_name.clone())
2486 .await
2487 .unwrap();
2488 assert_eq!(slice_states.extent(), host1.extent());
2489
2490 let err = proc_mesh
2491 .actor_states(instance, actor_name.clone())
2492 .await
2493 .unwrap_err();
2494 let expected_name = actor_name.into();
2495 match err {
2496 crate::Error::NotExist(name) if name == expected_name => {}
2497 other => panic!("expected NotExist for {expected_name}, got {other:?}"),
2498 }
2499
2500 let _ = hm.shutdown(instance).await;
2501 }
2502
2503 #[cfg(fbcode_build)]
2508 async fn assert_proc_states_match_slice<C: hyperactor::context::Actor>(
2509 mesh: &super::ProcMeshRef,
2510 cx: &C,
2511 ) {
2512 let mut expected: Vec<usize> = mesh.ranks.iter().map(|r| r.create_rank).collect();
2514 expected.sort();
2515
2516 let states = mesh
2517 .states(cx, None)
2518 .await
2519 .unwrap()
2520 .expect("host-backed mesh yields Some");
2521
2522 assert_eq!(states.extent(), mesh.extent());
2523
2524 let mut got: Vec<usize> = states
2525 .values()
2526 .map(|s| {
2527 assert!(
2528 !matches!(
2529 s.status,
2530 Status::NotExist | Status::Failed(_) | Status::Timeout(_)
2531 ),
2532 "rank should resolve to a live proc, got status {:?}",
2533 s.status
2534 );
2535 s.state.expect("live proc carries ProcState").create_rank
2536 })
2537 .collect();
2538
2539 got.sort();
2540
2541 assert_eq!(
2542 got, expected,
2543 "proc_states must return exactly this (sub)mesh's procs"
2544 );
2545 }
2546
2547 #[async_timed_test(timeout_secs = 60)]
2548 #[cfg(fbcode_build)]
2549 async fn test_proc_states_on_sliced_mesh() {
2550 let instance = testing::instance();
2551 let mut hm = testing::host_mesh(2).await;
2552 let proc_mesh = hm
2554 .spawn(&instance, "test", extent!(gpus = 4), None, None)
2555 .await
2556 .unwrap();
2557
2558 assert_proc_states_match_slice(&proc_mesh, instance).await;
2560 assert_proc_states_match_slice(&proc_mesh.range("gpus", 2..4).unwrap(), instance).await;
2562 assert_proc_states_match_slice(&proc_mesh.range("hosts", 1..2).unwrap(), instance).await;
2564 assert_proc_states_match_slice(
2566 &proc_mesh
2567 .range("gpus", 2..4)
2568 .unwrap()
2569 .range("hosts", 1..2)
2570 .unwrap(),
2571 instance,
2572 )
2573 .await;
2574
2575 let _ = hm.shutdown(instance).await;
2576 }
2577
2578 #[test]
2579 fn test_python_class_from_supervision_name() {
2580 use super::python_class_from_supervision_name;
2581
2582 assert_eq!(
2583 python_class_from_supervision_name("instance0.<my_module.MyWorker test_mesh>"),
2584 Some("Python<MyWorker>".to_string()),
2585 );
2586 assert_eq!(
2587 python_class_from_supervision_name(
2588 "instance0.<package.submodule.TrainingActor mesh_0>"
2589 ),
2590 Some("Python<TrainingActor>".to_string()),
2591 );
2592 assert_eq!(python_class_from_supervision_name("plain_name"), None,);
2594 assert_eq!(
2596 python_class_from_supervision_name("instance0.<NoModule mesh>"),
2597 None,
2598 );
2599 }
2600
2601 #[allow(dead_code)]
2608 async fn _spawn_controllerless_needs_no_mesh_failure_handler<A, C>(
2609 pm: &super::ProcMeshRef,
2610 cx: &C,
2611 name: &str,
2612 params: &A::Params,
2613 ) -> crate::Result<crate::ActorMesh<A>>
2614 where
2615 A: hyperactor::RemoteSpawn,
2616 A::Params: hyperactor::RemoteMessage,
2617 C: hyperactor::context::Actor,
2618 {
2619 pm.spawn_controllerless_service::<A, C>(cx, name, params)
2620 .await
2621 }
2622
2623 #[cfg(fbcode_build)]
2624 async fn execute_spawn_controllerless() {
2625 hyperactor_telemetry::initialize_logging(hyperactor_telemetry::DefaultTelemetryClock {});
2626
2627 let instance = testing::instance();
2628
2629 let mut hm = testing::host_mesh(2).await;
2630 let proc_mesh = hm
2631 .spawn(&instance, "test", extent!(gpus = 1), None, None)
2632 .await
2633 .unwrap();
2634
2635 let controllerless = proc_mesh
2637 .spawn_controllerless_service::<testactor::TestActor, _>(
2638 instance,
2639 "svc_controllerless",
2640 &(),
2641 )
2642 .await
2643 .unwrap();
2644 assert!(
2645 controllerless
2646 .as_managed()
2647 .expect("controllerless service mesh should be managed")
2648 .controller()
2649 .is_none(),
2650 "spawn_controllerless_service must not create a controller",
2651 );
2652
2653 let with_controller = proc_mesh
2655 .spawn_service::<testactor::TestActor, _>(instance, "svc_with_controller", &())
2656 .await
2657 .unwrap();
2658 assert!(
2659 with_controller
2660 .as_managed()
2661 .expect("service mesh should be managed")
2662 .controller()
2663 .is_some(),
2664 "spawn_service must create a controller",
2665 );
2666
2667 let _ = hm.shutdown(instance).await;
2668 }
2669
2670 #[async_timed_test(timeout_secs = 120)]
2671 #[cfg(fbcode_build)]
2672 async fn test_spawn_controllerless_service() {
2673 let config = hyperactor_config::global::lock();
2674 let _guard = config.override_key(PROC_SPAWN_MAX_IDLE, Duration::from_secs(120));
2675 let _guard2 = config.override_key(
2676 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2677 Duration::from_secs(120),
2678 );
2679 execute_spawn_controllerless().await;
2680 }
2681}