1#![allow(unused_assignments)]
16
17use std::collections::HashMap;
18use std::collections::HashSet;
19use std::collections::hash_map::DefaultHasher;
20use std::fmt;
21use std::hash::Hash;
22use std::hash::Hasher;
23use std::pin::Pin;
24use std::sync::OnceLock;
25
26use async_trait::async_trait;
27use enum_as_inner::EnumAsInner;
28use hyperactor::Actor;
29use hyperactor::ActorHandle;
30use hyperactor::ActorRef;
31use hyperactor::Addr;
32use hyperactor::Context;
33use hyperactor::Endpoint as _;
34use hyperactor::HandleClient;
35use hyperactor::Handler;
36use hyperactor::Instance;
37use hyperactor::PortHandle;
38use hyperactor::PortRef;
39use hyperactor::Proc;
40use hyperactor::ProcAddr;
41use hyperactor::RefClient;
42use hyperactor::RemoteEndpoint as _;
43use hyperactor::Uid;
44use hyperactor::actor::ActorStatus;
45use hyperactor::actor::ActorStoppingReason;
46use hyperactor::context;
47use hyperactor::gateway::GatewayServeHandle;
48use hyperactor::id::Label;
49use hyperactor::value_mesh::ValueOverlay;
50use hyperactor_config::Flattrs;
51use hyperactor_config::attrs::Attrs;
52use ndslice::view::Region;
53use serde::Deserialize;
54use serde::Serialize;
55use tokio::time::Duration;
56use typeuri::Named;
57
58use crate::StatusOverlay;
59use crate::bootstrap;
60use crate::bootstrap::BootstrapCommand;
61use crate::bootstrap::BootstrapProcConfig;
62use crate::bootstrap::BootstrapProcManager;
63use crate::bootstrap::ProcBind;
64use crate::config_dump::ConfigDump;
65use crate::config_dump::ConfigDumpResult;
66use crate::host::Host;
67use crate::host::HostError;
68use crate::host::LOCAL_PROC_NAME;
69use crate::host::LocalProcManager;
70use crate::host::SERVICE_PROC_NAME;
71use crate::host::SingleTerminate;
72use crate::mesh_id::HostMeshId;
73use crate::mesh_id::ProcMeshId;
74use crate::mesh_id::ResourceId;
75use crate::proc_agent::ProcAgent;
76use crate::pyspy::PySpyDump;
77use crate::pyspy::PySpyProfile;
78use crate::pyspy::PySpyProfileWorker;
79use crate::pyspy::PySpyWorker;
80use crate::resource;
81use crate::resource::ProcSpec;
82use crate::resource::Status;
83
84pub(crate) type ProcManagerSpawnFuture =
85 Pin<Box<dyn Future<Output = anyhow::Result<ActorHandle<ProcAgent>>> + Send>>;
86pub(crate) type ProcManagerSpawnFn = Box<dyn Fn(Proc) -> ProcManagerSpawnFuture + Send + Sync>;
87
88#[derive(EnumAsInner)]
99pub enum HostAgentMode {
100 Process {
101 host: Host<BootstrapProcManager>,
102 shutdown_tx: Option<tokio::sync::oneshot::Sender<GatewayServeHandle>>,
106 },
107 Local(Host<LocalProcManager<ProcManagerSpawnFn>>),
108}
109
110impl HostAgentMode {
111 pub(crate) fn addr(&self) -> &hyperactor::channel::ChannelAddr {
112 #[allow(clippy::match_same_arms)]
113 match self {
114 HostAgentMode::Process { host, .. } => host.addr(),
115 HostAgentMode::Local(host) => host.addr(),
116 }
117 }
118
119 pub(crate) fn system_proc(&self) -> &Proc {
120 #[allow(clippy::match_same_arms)]
121 match self {
122 HostAgentMode::Process { host, .. } => host.system_proc(),
123 HostAgentMode::Local(host) => host.system_proc(),
124 }
125 }
126
127 pub(crate) fn local_proc(&self) -> &Proc {
128 #[allow(clippy::match_same_arms)]
129 match self {
130 HostAgentMode::Process { host, .. } => host.local_proc(),
131 HostAgentMode::Local(host) => host.local_proc(),
132 }
133 }
134
135 async fn request_stop(
139 &self,
140 cx: &impl context::Actor,
141 proc: &ProcAddr,
142 timeout: Duration,
143 reason: &str,
144 ) {
145 match self {
146 HostAgentMode::Process { host, .. } => {
147 host.manager().request_stop(cx, proc, timeout, reason).await;
148 }
149 HostAgentMode::Local(host) => {
150 host.manager().request_stop(proc, timeout, reason).await;
151 }
152 }
153 }
154
155 async fn proc_status(
160 &self,
161 proc_id: &ProcAddr,
162 ) -> (resource::Status, Option<bootstrap::ProcStatus>) {
163 match self {
164 HostAgentMode::Process { host, .. } => match host.manager().status(proc_id).await {
165 Some(proc_status) => (proc_status.clone().into(), Some(proc_status)),
166 None => (resource::Status::Unknown, None),
167 },
168 HostAgentMode::Local(host) => {
169 let status = match host.manager().local_proc_status(proc_id).await {
170 Some(crate::host::LocalProcStatus::Stopping) => resource::Status::Stopping,
171 Some(crate::host::LocalProcStatus::Stopped) => resource::Status::Stopped,
172 None => resource::Status::Running,
173 };
174 (status, None)
175 }
176 }
177 }
178
179 fn bootstrap_command(&self) -> Option<BootstrapCommand> {
181 match self {
182 HostAgentMode::Process { host, .. } => Some(host.manager().command().clone()),
183 HostAgentMode::Local(_) => None,
184 }
185 }
186}
187
188pub(crate) fn proc_name(proc_mesh_id: &ProcMeshId, rank: usize) -> ResourceId {
200 let label = Label::strip(&format!(
201 "{}-{}",
202 proc_mesh_id
203 .display_label()
204 .map(|label| label.as_str())
205 .unwrap_or("unnamed"),
206 rank
207 ));
208
209 match proc_mesh_id.uid() {
210 Uid::Singleton(_) => ResourceId::singleton(label),
211 Uid::Instance(_, _) => {
212 let mut hasher = DefaultHasher::new();
213 proc_mesh_id.hash(&mut hasher);
214 rank.hash(&mut hasher);
215 ResourceId::new(
216 Uid::Instance(hasher.finish(), Some(label.clone())),
217 Some(label),
218 )
219 }
220 }
221}
222
223#[derive(Debug)]
224pub(crate) struct ProcCreationState {
225 pub(crate) rank: usize,
226 pub(crate) host_mesh_id: Option<HostMeshId>,
227 pub(crate) proc_mesh_id: Option<ProcMeshId>,
235 pub(crate) created: Result<(ProcAddr, ActorRef<ProcAgent>), HostError>,
236 pub(crate) expiry_time: Option<std::time::SystemTime>,
240}
241
242pub const HOST_MESH_AGENT_ACTOR_NAME: &str = "host_agent";
244
245enum HostAgentState {
247 Detached(HostAgentMode),
250 Attached(HostAgentMode),
252 Draining,
256 Shutdown,
258}
259
260#[derive(Debug, Serialize, Deserialize, Named)]
265struct ProcStatusChanged {
266 id: ResourceId,
267}
268
269struct DrainComplete {
272 host: HostAgentMode,
273 rank: usize,
275 reply: PortRef<crate::StatusOverlay>,
278}
279
280#[hyperactor::export(handlers = [])]
285struct DrainWorker {
286 host: Option<HostAgentMode>,
287 timeout: Duration,
288 max_in_flight: usize,
289 rank: usize,
290 reply: Option<PortRef<crate::StatusOverlay>>,
291 done_notify: PortHandle<DrainComplete>,
292}
293
294#[async_trait]
295impl Actor for DrainWorker {
296 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
297 if let Some(host) = self.host.as_mut() {
298 match host {
299 HostAgentMode::Process { host, .. } => {
300 host.terminate_children(
301 this,
302 self.timeout,
303 self.max_in_flight.clamp(1, 256),
304 "drain host",
305 )
306 .await;
307 }
308 HostAgentMode::Local(host) => {
309 host.terminate_children(this, self.timeout, self.max_in_flight, "drain host")
310 .await;
311 }
312 }
313 }
314
315 if let (Some(host), Some(reply)) = (self.host.take(), self.reply.take()) {
319 let _ = self.done_notify.post(
320 this,
321 DrainComplete {
322 host,
323 rank: self.rank,
324 reply,
325 },
326 );
327 }
328
329 Ok(())
330 }
331}
332
333impl fmt::Debug for DrainWorker {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 f.debug_struct("DrainWorker")
336 .field("timeout", &self.timeout)
337 .field("max_in_flight", &self.max_in_flight)
338 .finish()
339 }
340}
341
342#[hyperactor::export(
343 handlers=[
344 resource::CreateOrUpdate<ProcSpec>,
345 SpawnProcs,
346 resource::Stop,
347 resource::GetState<ProcState>,
348 resource::KeepaliveGetState<ProcState>,
349 GetHostProcStates,
350 resource::StreamState<ProcState>,
351 resource::GetRankStatus,
352 resource::WaitRankStatus,
353 resource::List,
354 ShutdownHost,
355 DrainHost,
356 SetClientConfig,
357 ProcStatusChanged,
358 PySpyDump,
359 PySpyProfile,
360 ConfigDump,
361 crate::proc_agent::SelfCheck,
362 ]
363)]
364pub struct HostAgent {
365 state: HostAgentState,
366 pub(crate) created: HashMap<ResourceId, ProcCreationState>,
367 pending_proc_waiters:
371 HashMap<ResourceId, Vec<(resource::Status, usize, PortRef<crate::StatusOverlay>)>>,
372 watching: HashSet<ResourceId>,
374 proc_status_port: Option<PortHandle<ProcStatusChanged>>,
376 local_mesh_agent: OnceLock<anyhow::Result<ActorHandle<ProcAgent>>>,
380}
381
382impl HostAgent {
383 pub fn new_process(
385 host: Host<BootstrapProcManager>,
386 shutdown_tx: Option<tokio::sync::oneshot::Sender<GatewayServeHandle>>,
387 ) -> Self {
388 Self::new(HostAgentMode::Process { host, shutdown_tx })
389 }
390
391 pub fn new_local(host: Host<LocalProcManager<ProcManagerSpawnFn>>) -> Self {
393 Self::new(HostAgentMode::Local(host))
394 }
395
396 fn new(host: HostAgentMode) -> Self {
397 Self {
398 state: HostAgentState::Detached(host),
399 created: HashMap::new(),
400 pending_proc_waiters: HashMap::new(),
401 watching: HashSet::new(),
402 proc_status_port: None,
403 local_mesh_agent: OnceLock::new(),
404 }
405 }
406
407 pub async fn wait_initialized(handle: &ActorHandle<Self>) -> anyhow::Result<()> {
410 let mut status = handle.status();
411 loop {
412 let current = status.borrow_and_update().clone();
413 match current {
414 ActorStatus::Idle | ActorStatus::Processing(_, _) => return Ok(()),
415 ActorStatus::Failed(err) => anyhow::bail!("host agent init failed: {err}"),
416 ActorStatus::Stopped(reason) => anyhow::bail!("host agent stopped: {reason}"),
417 ActorStatus::Stopping(ActorStoppingReason::Zombie(reason)) => {
418 anyhow::bail!("host agent zombie: {reason}")
419 }
420 ActorStatus::Unknown
421 | ActorStatus::Created
422 | ActorStatus::Initializing
423 | ActorStatus::Client
424 | ActorStatus::Stopping(_) => {}
425 }
426 if status.changed().await.is_err() {
427 anyhow::bail!("host agent status channel closed before init completed");
428 }
429 }
430 }
431
432 fn min_proc_status(&self) -> resource::Status {
435 match &self.state {
436 HostAgentState::Detached(_) | HostAgentState::Attached(_) => resource::Status::Running,
437 HostAgentState::Draining => resource::Status::Stopping,
438 HostAgentState::Shutdown => resource::Status::Stopped,
439 }
440 }
441
442 fn host(&self) -> Option<&HostAgentMode> {
443 match &self.state {
444 HostAgentState::Detached(h) | HostAgentState::Attached(h) => Some(h),
445 _ => None,
446 }
447 }
448
449 fn host_mut(&mut self) -> Option<&mut HostAgentMode> {
450 match &mut self.state {
451 HostAgentState::Detached(h) | HostAgentState::Attached(h) => Some(h),
452 _ => None,
453 }
454 }
455
456 async fn drain(
462 &mut self,
463 cx: &Context<'_, Self>,
464 timeout: std::time::Duration,
465 max_in_flight: usize,
466 ) {
467 if let Some(host_mode) = self.host_mut() {
468 match host_mode {
469 HostAgentMode::Process { host, .. } => {
470 let summary = host
471 .terminate_children(cx, timeout, max_in_flight.clamp(1, 256), "stop host")
472 .await;
473 tracing::info!(?summary, "terminated children on host");
474 }
475 HostAgentMode::Local(host) => {
476 let summary = host
477 .terminate_children(cx, timeout, max_in_flight, "stop host")
478 .await;
479 tracing::info!(?summary, "terminated children on local host");
480 }
481 }
482 }
483 self.created.clear();
484 }
485
486 async fn drain_by_mesh_name(
490 &mut self,
491 cx: &Context<'_, Self>,
492 timeout: std::time::Duration,
493 filter: Option<&HostMeshId>,
494 ) {
495 let matching_ids: Vec<ResourceId> = self
496 .created
497 .iter()
498 .filter(|(_, state)| state.host_mesh_id.as_ref() == filter)
499 .map(|(id, _)| id.clone())
500 .collect();
501
502 if let Some(host_mode) = self.host() {
503 for id in &matching_ids {
504 if let Some(ProcCreationState {
505 created: Ok((proc_id, _)),
506 ..
507 }) = self.created.get(id)
508 {
509 match host_mode {
510 HostAgentMode::Process { host, .. } => {
511 let _ = host
512 .terminate_proc(cx, proc_id, timeout, "selective drain")
513 .await;
514 }
515 HostAgentMode::Local(host) => {
516 let _ = host
517 .terminate_proc(cx, proc_id, timeout, "selective drain")
518 .await;
519 }
520 }
521 }
522 }
523 }
524
525 for id in &matching_ids {
528 self.created.remove(id);
529 self.watching.remove(id);
530 self.pending_proc_waiters.remove(id);
531 }
532
533 tracing::info!(
534 count = matching_ids.len(),
535 filter = ?filter,
536 "selectively drained procs",
537 );
538 }
539
540 fn publish_introspect_properties(&self, cx: &Instance<Self>) {
544 let host = match self.host() {
545 Some(h) => h,
546 None => return, };
548
549 let addr = host.addr().to_string();
550 let mut children: Vec<hyperactor::introspect::IntrospectRef> = Vec::new();
551 let system_children: Vec<crate::introspect::NodeRef> = Vec::new(); children.push(hyperactor::introspect::IntrospectRef::Proc(
557 host.system_proc().proc_addr().clone(),
558 ));
559 children.push(hyperactor::introspect::IntrospectRef::Proc(
560 host.local_proc().proc_addr().clone(),
561 ));
562
563 for state in self.created.values() {
565 if let Ok((proc_id, _agent_ref)) = &state.created {
566 children.push(hyperactor::introspect::IntrospectRef::Proc(proc_id.clone()));
567 }
568 }
569
570 let num_procs = children.len();
571
572 let mut attrs = hyperactor_config::Attrs::new();
573 attrs.set(crate::introspect::NODE_TYPE, "host".to_string());
574 attrs.set(crate::introspect::ADDR, addr);
575 attrs.set(crate::introspect::NUM_PROCS, num_procs);
576 attrs.set(hyperactor::introspect::CHILDREN, children);
577 attrs.set(crate::introspect::SYSTEM_CHILDREN, system_children);
578 let memory = crate::introspect::ProcessMemoryStats::read_from_procfs();
583 memory.to_attrs(&mut attrs);
584 cx.publish_attrs(attrs);
585 }
586}
587
588#[async_trait]
589impl Actor for HostAgent {
590 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
591 this.bind::<Self>();
592 if matches!(self.host().unwrap(), HostAgentMode::Process { .. }) {
593 let (directory, file) = hyperactor_telemetry::log_file_path(
594 hyperactor_telemetry::env::Env::current(),
595 None,
596 )
597 .unwrap();
598 eprintln!(
599 "Monarch internal logs are being written to {}/{}.log; execution id {}",
600 directory,
601 file,
602 hyperactor_telemetry::env::execution_id(),
603 );
604 }
605 this.set_system();
606 self.publish_introspect_properties(this);
607
608 let host = self.host().expect("host present");
611 let system_proc = host.system_proc().clone();
612 let local_proc = host.local_proc().clone();
613 let self_id = this.self_addr().clone();
614 this.set_query_child_handler(move |child_ref| {
615 use hyperactor::introspect::IntrospectResult;
616
617 let proc = match child_ref {
618 Addr::Proc(proc_ref) => {
619 if *proc_ref == system_proc.proc_addr() {
620 Some((&system_proc, SERVICE_PROC_NAME))
621 } else if *proc_ref == local_proc.proc_addr() {
622 Some((&local_proc, LOCAL_PROC_NAME))
623 } else {
624 None
625 }
626 }
627 _ => None,
628 };
629
630 match proc {
631 Some((proc, label)) => {
632 let all_keys = proc.all_instance_keys();
646 let mut actors: Vec<hyperactor::introspect::IntrospectRef> =
647 Vec::with_capacity(all_keys.len());
648 let mut system_actors: Vec<crate::introspect::NodeRef> = Vec::new();
649 for id in all_keys {
650 if let Some(cell) = proc.get_instance_by_id(&id) {
651 let actor_addr = cell.actor_addr().clone();
652 if cell.is_system() {
653 system_actors
654 .push(crate::introspect::NodeRef::Actor(actor_addr.clone()));
655 }
656 actors.push(hyperactor::introspect::IntrospectRef::Actor(actor_addr));
657 }
658 }
659 let mut attrs = hyperactor_config::Attrs::new();
660 attrs.set(crate::introspect::NODE_TYPE, "proc".to_string());
661 attrs.set(crate::introspect::PROC_NAME, label.to_string());
662 attrs.set(crate::introspect::NUM_ACTORS, actors.len());
663 attrs.set(crate::introspect::SYSTEM_CHILDREN, system_actors.clone());
664 let memory = crate::introspect::ProcessMemoryStats::read_from_procfs();
668 memory.to_attrs(&mut attrs);
669 attrs.set(
670 crate::introspect::ACTOR_WORK_QUEUE_DEPTH_TOTAL,
671 proc.queue_depth_total(),
672 );
673 let mut queue_max: u64 = 0;
675 for aid in proc.all_instance_keys() {
676 if let Some(cell) = proc.get_instance_by_id(&aid) {
677 queue_max = queue_max.max(cell.queue_depth());
678 }
679 }
680 attrs.set(crate::introspect::ACTOR_WORK_QUEUE_DEPTH_MAX, queue_max);
681 attrs.set(
682 crate::introspect::ACTOR_WORK_QUEUE_DEPTH_HIGH_WATER_MARK,
683 proc.queue_depth_high_water_mark(),
684 );
685 attrs.set(
686 crate::introspect::LAST_NONZERO_QUEUE_DEPTH_AGE_MS,
687 proc.last_nonzero_queue_depth_age_ms(),
688 );
689 let attrs_json =
690 serde_json::to_string(&attrs).unwrap_or_else(|_| "{}".to_string());
691
692 IntrospectResult {
693 identity: hyperactor::introspect::IntrospectRef::Proc(
694 proc.proc_addr().clone(),
695 ),
696 attrs: attrs_json,
697 children: actors,
698 parent: Some(hyperactor::introspect::IntrospectRef::Actor(
699 self_id.clone(),
700 )),
701 as_of: std::time::SystemTime::now(),
702 }
703 }
704 None => {
705 let mut error_attrs = hyperactor_config::Attrs::new();
706 error_attrs.set(hyperactor::introspect::ERROR_CODE, "not_found".to_string());
707 error_attrs.set(
708 hyperactor::introspect::ERROR_MESSAGE,
709 format!("child {} not found", child_ref),
710 );
711 let identity = match child_ref {
712 Addr::Proc(p) => hyperactor::introspect::IntrospectRef::Proc(p.clone()),
713 Addr::Actor(a) => hyperactor::introspect::IntrospectRef::Actor(a.clone()),
714 Addr::Port(p) => {
715 hyperactor::introspect::IntrospectRef::Actor(p.actor_addr())
716 }
717 };
718 IntrospectResult {
719 identity,
720 attrs: serde_json::to_string(&error_attrs)
721 .unwrap_or_else(|_| "{}".to_string()),
722 children: Vec::new(),
723 parent: None,
724 as_of: std::time::SystemTime::now(),
725 }
726 }
727 }
728 });
729
730 self.proc_status_port = Some(this.port::<ProcStatusChanged>());
731
732 if let Some(delay) = hyperactor_config::global::get(crate::proc_agent::MESH_ORPHAN_TIMEOUT)
736 {
737 this.post_after(this, crate::proc_agent::SelfCheck::default(), delay);
738 }
739
740 Ok(())
741 }
742}
743
744impl fmt::Debug for HostAgent {
745 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
746 f.debug_struct("HostAgent")
747 .field("host", &"..")
748 .field("created", &self.created)
749 .finish()
750 }
751}
752
753#[derive(
758 Serialize,
759 Deserialize,
760 Clone,
761 Debug,
762 Named,
763 Handler,
764 RefClient,
765 HandleClient
766)]
767pub struct SpawnProcs {
768 pub rank: resource::Rank,
770 pub proc_mesh_id: ProcMeshId,
772 pub num_per_host: usize,
774 pub client_config_override: Attrs,
777 pub host_mesh_id: Option<HostMeshId>,
780 pub default_bootstrap_command: Option<BootstrapCommand>,
782 pub proc_bind: Option<Vec<ProcBind>>,
787 pub bootstrap_commands: Option<Vec<Option<BootstrapCommand>>>,
790 #[serde(default)]
794 pub status_reply: Option<PortRef<crate::StatusOverlay>>,
795}
796wirevalue::register_type!(SpawnProcs);
797
798#[async_trait]
799impl Handler<SpawnProcs> for HostAgent {
800 #[tracing::instrument("HostAgent::SpawnProcs", level = "info", skip_all, fields(host_rank, num = spawn.num_per_host))]
801 async fn handle(&mut self, cx: &Context<Self>, spawn: SpawnProcs) -> anyhow::Result<()> {
802 let host_rank = spawn
803 .rank
804 .0
805 .expect("cast layer stamps the rank before delivery");
806
807 tracing::Span::current().record("host_rank", host_rank);
808
809 let mut spawn_result = Ok(());
810
811 for per_host_rank in 0..spawn.num_per_host {
812 let rank = spawn.num_per_host * host_rank + per_host_rank;
813
814 let id = proc_name(&spawn.proc_mesh_id, rank);
815
816 let bootstrap_command = spawn
817 .bootstrap_commands
818 .as_ref()
819 .and_then(|commands| commands.get(rank).cloned().flatten())
820 .or_else(|| spawn.default_bootstrap_command.clone());
821
822 let proc_bind = spawn
823 .proc_bind
824 .as_ref()
825 .and_then(|binds| binds.get(per_host_rank).cloned());
826
827 if let Err(e) = <Self as Handler<resource::CreateOrUpdate<ProcSpec>>>::handle(
828 self,
829 cx,
830 resource::CreateOrUpdate {
831 id,
832 rank: resource::Rank::new(rank),
833 spec: ProcSpec {
834 client_config_override: spawn.client_config_override.clone(),
835 proc_bind,
836 bootstrap_command,
837 host_mesh_id: spawn.host_mesh_id.clone(),
838 proc_mesh_id: Some(spawn.proc_mesh_id.clone()),
839 },
840 },
841 )
842 .await
843 {
844 spawn_result = Err(e);
846 break;
847 }
848 }
849
850 if let Some(reply) = &spawn.status_reply {
856 let mut runs = Vec::with_capacity(spawn.num_per_host);
857
858 for per_host_rank in 0..spawn.num_per_host {
859 let rank = spawn.num_per_host * host_rank + per_host_rank;
860
861 let id = proc_name(&spawn.proc_mesh_id, rank);
862
863 let status = match self.proc_rank_status(&id).await {
864 (resolved, status) if resolved != usize::MAX => status,
865 _ => match &spawn_result {
869 Err(e) => Status::Failed(e.to_string()),
870 Ok(()) => continue,
871 },
872 };
873
874 runs.push((rank..(rank + 1), status));
875 }
876
877 reply.post(cx, crate::StatusOverlay::try_from_runs(runs)?);
878 }
879
880 spawn_result
881 }
882}
883
884#[async_trait]
888impl Handler<resource::CreateOrUpdate<ProcSpec>> for HostAgent {
889 #[tracing::instrument("HostAgent::CreateOrUpdate", level = "info", skip_all, fields(id=%create_or_update.id))]
890 async fn handle(
891 &mut self,
892 cx: &Context<Self>,
893 create_or_update: resource::CreateOrUpdate<ProcSpec>,
894 ) -> anyhow::Result<()> {
895 if self.created.contains_key(&create_or_update.id) {
896 return Ok(());
898 }
899
900 let host = match self.host_mut() {
901 Some(h) => h,
902 None => {
903 tracing::warn!(
904 id = %create_or_update.id,
905 "ignoring CreateOrUpdate: HostAgent has already shut down"
906 );
907 return Ok(());
908 }
909 };
910 let created = match host {
911 HostAgentMode::Process { host, .. } => {
912 host.spawn(
913 create_or_update.id.to_string(),
914 BootstrapProcConfig {
915 create_rank: create_or_update.rank.unwrap(),
916 client_config_override: create_or_update
917 .spec
918 .client_config_override
919 .clone(),
920 proc_bind: create_or_update.spec.proc_bind.clone(),
921 bootstrap_command: create_or_update.spec.bootstrap_command.clone(),
922 },
923 )
924 .await
925 }
926 HostAgentMode::Local(host) => host.spawn(create_or_update.id.to_string(), ()).await,
927 };
928
929 let rank = create_or_update.rank.unwrap();
930
931 if let Err(e) = &created {
932 tracing::error!("failed to spawn proc {}: {}", create_or_update.id, e);
933 }
934 let was_empty = self.created.is_empty();
935 self.created.insert(
936 create_or_update.id.clone(),
937 ProcCreationState {
938 rank,
939 host_mesh_id: create_or_update.spec.host_mesh_id.clone(),
940 proc_mesh_id: create_or_update.spec.proc_mesh_id.clone(),
941 created,
942 expiry_time: None,
943 },
944 );
945
946 if was_empty && let HostAgentState::Detached(_) = &self.state {
948 let host = match std::mem::replace(&mut self.state, HostAgentState::Shutdown) {
949 HostAgentState::Detached(h) => h,
950 _ => unreachable!(),
951 };
952 self.state = HostAgentState::Attached(host);
953 }
954
955 let proc_id = self
961 .created
962 .get(&create_or_update.id)
963 .and_then(|s| s.created.as_ref().ok())
964 .map(|(pid, _)| pid.clone());
965
966 if let Some(waiters) = self.pending_proc_waiters.get_mut(&create_or_update.id) {
967 for (_, waiter_rank, _) in waiters.iter_mut() {
968 if *waiter_rank == usize::MAX {
969 *waiter_rank = rank;
970 }
971 }
972 }
973
974 if self.pending_proc_waiters.contains_key(&create_or_update.id) {
976 if let Some(proc_id) = &proc_id {
977 self.start_watch_bridge(&create_or_update.id, proc_id).await;
978 }
979 self.flush_proc_waiters(cx, &create_or_update.id).await;
980 }
981
982 self.publish_introspect_properties(cx);
983 Ok(())
984 }
985}
986
987#[async_trait]
988impl Handler<resource::Stop> for HostAgent {
989 async fn handle(&mut self, cx: &Context<Self>, message: resource::Stop) -> anyhow::Result<()> {
990 tracing::info!(
991 name = "HostMeshAgentStatus",
992 proc_id = %message.id,
993 reason = %message.reason,
994 "stopping proc"
995 );
996 let host = match self.host() {
997 Some(h) => h,
998 None => {
999 tracing::debug!(
1001 proc_id = %message.id,
1002 "ignoring Stop: HostAgent has already shut down"
1003 );
1004 return Ok(());
1005 }
1006 };
1007 let timeout = hyperactor_config::global::get(hyperactor::config::PROCESS_EXIT_TIMEOUT);
1008
1009 if let Some(ProcCreationState {
1010 created: Ok((proc_id, _)),
1011 ..
1012 }) = self.created.get(&message.id)
1013 {
1014 host.request_stop(cx, proc_id, timeout, &message.reason)
1015 .await;
1016 }
1017
1018 self.flush_proc_waiters(cx, &message.id).await;
1020
1021 self.publish_introspect_properties(cx);
1022 Ok(())
1023 }
1024}
1025
1026impl HostAgent {
1027 async fn proc_rank_status(&self, id: &ResourceId) -> (usize, Status) {
1030 match self.created.get(id) {
1031 Some(ProcCreationState {
1032 rank,
1033 created: Ok((proc_id, _mesh_agent)),
1034 ..
1035 }) => {
1036 let raw_status = match self.host() {
1037 Some(host) => host.proc_status(proc_id).await.0,
1038 None => resource::Status::Unknown,
1039 };
1040 (*rank, raw_status.clamp_min(self.min_proc_status()))
1041 }
1042 Some(ProcCreationState {
1043 rank,
1044 created: Err(e),
1045 ..
1046 }) => (*rank, Status::Failed(e.to_string())),
1047 None => (usize::MAX, Status::NotExist),
1048 }
1049 }
1050}
1051
1052#[async_trait]
1053impl Handler<resource::GetRankStatus> for HostAgent {
1054 async fn handle(
1055 &mut self,
1056 cx: &Context<Self>,
1057 get_rank_status: resource::GetRankStatus,
1058 ) -> anyhow::Result<()> {
1059 let (rank, status) = self.proc_rank_status(&get_rank_status.id).await;
1060
1061 let overlay = if rank == usize::MAX {
1062 StatusOverlay::new()
1063 } else {
1064 StatusOverlay::try_from_runs(vec![(rank..(rank + 1), status)])
1065 .expect("valid single-run overlay")
1066 };
1067 get_rank_status.reply.post(cx, overlay);
1068 Ok(())
1069 }
1070}
1071
1072#[async_trait]
1073impl Handler<resource::WaitRankStatus> for HostAgent {
1074 async fn handle(
1075 &mut self,
1076 cx: &Context<Self>,
1077 msg: resource::WaitRankStatus,
1078 ) -> anyhow::Result<()> {
1079 use crate::StatusOverlay;
1080 use crate::resource::Status;
1081
1082 match self.created.get(&msg.id) {
1083 Some(ProcCreationState {
1084 rank,
1085 created: Ok((proc_id, _)),
1086 ..
1087 }) => {
1088 let rank = *rank;
1089 let status = match self.host() {
1090 Some(host) => host.proc_status(proc_id).await.0,
1091 None => Status::Stopped,
1092 };
1093
1094 if status >= msg.min_status {
1096 let overlay = StatusOverlay::try_from_runs(vec![(rank..(rank + 1), status)])
1097 .expect("valid single-run overlay");
1098 let _ = msg.reply.post(cx, overlay);
1099 return Ok(());
1100 }
1101
1102 self.pending_proc_waiters
1104 .entry(msg.id.clone())
1105 .or_default()
1106 .push((msg.min_status, rank, msg.reply));
1107
1108 let proc_id = proc_id.clone();
1109 self.start_watch_bridge(&msg.id, &proc_id).await;
1110 }
1111 Some(ProcCreationState {
1112 rank,
1113 created: Err(e),
1114 ..
1115 }) => {
1116 let overlay = StatusOverlay::try_from_runs(vec![(
1118 *rank..(*rank + 1),
1119 Status::Failed(e.to_string()),
1120 )])
1121 .expect("valid single-run overlay");
1122 let _ = msg.reply.post(cx, overlay);
1123 }
1124 None => {
1125 self.pending_proc_waiters
1129 .entry(msg.id.clone())
1130 .or_default()
1131 .push((msg.min_status, usize::MAX, msg.reply));
1132 }
1133 }
1134
1135 Ok(())
1136 }
1137}
1138
1139#[async_trait]
1140impl Handler<ProcStatusChanged> for HostAgent {
1141 async fn handle(&mut self, cx: &Context<Self>, msg: ProcStatusChanged) -> anyhow::Result<()> {
1142 self.flush_proc_waiters(cx, &msg.id).await;
1143 Ok(())
1144 }
1145}
1146
1147impl HostAgent {
1148 async fn flush_proc_waiters(&mut self, cx: &Context<'_, Self>, id: &ResourceId) {
1150 use crate::StatusOverlay;
1151 use crate::resource::Status;
1152
1153 let status = match self.created.get(id) {
1154 Some(ProcCreationState {
1155 created: Ok((proc_id, _)),
1156 ..
1157 }) => match self.host() {
1158 Some(host) => host.proc_status(proc_id).await.0,
1159 None => Status::Stopped,
1160 },
1161 Some(ProcCreationState {
1162 created: Err(_), ..
1163 }) => {
1164 return;
1166 }
1167 None => {
1168 return;
1170 }
1171 };
1172
1173 let Some(waiters) = self.pending_proc_waiters.get_mut(id) else {
1174 return;
1175 };
1176
1177 let remaining = std::mem::take(waiters);
1178 for (min_status, rank, reply) in remaining {
1179 if status >= min_status {
1180 let overlay =
1181 StatusOverlay::try_from_runs(vec![(rank..(rank + 1), status.clone())])
1182 .expect("valid single-run overlay");
1183 let _ = reply.post(cx, overlay);
1184 } else {
1185 waiters.push((min_status, rank, reply));
1186 }
1187 }
1188
1189 if waiters.is_empty() {
1190 self.pending_proc_waiters.remove(id);
1191 }
1192 }
1193
1194 async fn start_watch_bridge(&mut self, id: &ResourceId, proc_id: &ProcAddr) {
1197 if self.watching.contains(id) {
1198 return;
1199 }
1200 self.watching.insert(id.clone());
1201
1202 let port = match &self.proc_status_port {
1203 Some(p) => p.clone(),
1204 None => return,
1205 };
1206
1207 match self.host() {
1208 Some(HostAgentMode::Process { host, .. }) => {
1209 if let Some(rx) = host.manager().watch(proc_id).await {
1210 start_proc_watch(port, rx, id.clone(), |s| s.clone().into());
1211 }
1212 }
1213 Some(HostAgentMode::Local(host)) => {
1214 if let Some(rx) = host.manager().watch(proc_id).await {
1215 start_proc_watch(port, rx, id.clone(), |s| (*s).into());
1216 }
1217 }
1218 None => {}
1219 }
1220 }
1221}
1222
1223fn start_proc_watch<S>(
1226 port: PortHandle<ProcStatusChanged>,
1227 mut rx: tokio::sync::watch::Receiver<S>,
1228 id: ResourceId,
1229 to_status: impl Fn(&S) -> resource::Status + Send + 'static,
1230) where
1231 S: Send + Sync + 'static,
1232{
1233 let client = Instance::<()>::self_client();
1236 tokio::spawn(async move {
1237 loop {
1238 match rx.changed().await {
1239 Ok(()) => {
1240 let status = to_status(&*rx.borrow());
1241 let terminated = status.is_terminated();
1242 let _ = port.post(client, ProcStatusChanged { id: id.clone() });
1243 if terminated {
1244 return;
1245 }
1246 }
1247 Err(_) => {
1248 let _ = port.post(client, ProcStatusChanged { id: id.clone() });
1249 return;
1250 }
1251 }
1252 }
1253 });
1254}
1255
1256#[derive(
1257 Serialize,
1258 Deserialize,
1259 Clone,
1260 Debug,
1261 Named,
1262 Handler,
1263 RefClient,
1264 HandleClient
1265)]
1266pub struct ShutdownHost {
1267 pub timeout: std::time::Duration,
1270 pub max_in_flight: usize,
1272 pub rank: resource::Rank,
1276 pub ack: PortRef<usize>,
1285}
1286wirevalue::register_type!(ShutdownHost);
1287
1288#[derive(
1296 Serialize,
1297 Deserialize,
1298 Clone,
1299 Debug,
1300 Named,
1301 Handler,
1302 RefClient,
1303 HandleClient
1304)]
1305pub struct DrainHost {
1306 pub timeout: std::time::Duration,
1307 pub max_in_flight: usize,
1308 pub host_mesh_id: Option<HostMeshId>,
1309 pub rank: resource::Rank,
1312 pub reply: PortRef<crate::StatusOverlay>,
1316}
1317wirevalue::register_type!(DrainHost);
1318
1319#[async_trait]
1320impl Handler<DrainHost> for HostAgent {
1321 async fn handle(&mut self, cx: &Context<Self>, msg: DrainHost) -> anyhow::Result<()> {
1322 let rank = msg.rank.unwrap();
1323 let drained_overlay = || {
1326 crate::StatusOverlay::try_from_runs(vec![(rank..(rank + 1), resource::Status::Stopped)])
1327 .expect("valid single-run overlay")
1328 };
1329
1330 if msg.host_mesh_id.is_some() {
1331 self.drain_by_mesh_name(cx, msg.timeout, msg.host_mesh_id.as_ref())
1333 .await;
1334 msg.reply.post(cx, drained_overlay());
1335 return Ok(());
1336 }
1337
1338 let host = match std::mem::replace(&mut self.state, HostAgentState::Draining) {
1340 HostAgentState::Attached(h) => h,
1341 other @ (HostAgentState::Detached(_) | HostAgentState::Draining) => {
1342 self.state = other;
1344 msg.reply.post(cx, drained_overlay());
1345 return Ok(());
1346 }
1347 HostAgentState::Shutdown => {
1348 self.state = HostAgentState::Shutdown;
1349 msg.reply.post(cx, drained_overlay());
1350 return Ok(());
1351 }
1352 };
1353
1354 let done_port = cx.port::<DrainComplete>();
1363
1364 cx.spawn_with_label(
1365 "drain_worker",
1366 DrainWorker {
1367 host: Some(host),
1368 timeout: msg.timeout,
1369 max_in_flight: msg.max_in_flight,
1370 rank,
1371 reply: Some(msg.reply),
1372 done_notify: done_port,
1373 },
1374 );
1375
1376 Ok(())
1377 }
1378}
1379
1380#[async_trait]
1381impl Handler<DrainComplete> for HostAgent {
1382 async fn handle(&mut self, cx: &Context<Self>, msg: DrainComplete) -> anyhow::Result<()> {
1383 self.state = HostAgentState::Detached(msg.host);
1384 self.created.clear();
1385 let overlay = crate::StatusOverlay::try_from_runs(vec![(
1386 msg.rank..(msg.rank + 1),
1387 resource::Status::Stopped,
1388 )])
1389 .expect("valid single-run overlay");
1390 msg.reply.post(cx, overlay);
1391 Ok(())
1392 }
1393}
1394
1395#[async_trait]
1396impl Handler<ShutdownHost> for HostAgent {
1397 async fn handle(&mut self, cx: &Context<Self>, msg: ShutdownHost) -> anyhow::Result<()> {
1398 let rank = msg.rank.unwrap();
1399 if !self.created.is_empty() {
1406 self.drain(cx, msg.timeout, msg.max_in_flight).await;
1407 }
1408
1409 msg.ack.post(cx, rank);
1412
1413 match std::mem::replace(&mut self.state, HostAgentState::Shutdown) {
1416 HostAgentState::Detached(HostAgentMode::Process {
1417 mut host,
1418 shutdown_tx: Some(tx),
1419 })
1420 | HostAgentState::Attached(HostAgentMode::Process {
1421 mut host,
1422 shutdown_tx: Some(tx),
1423 }) => {
1424 tracing::info!(
1425 proc_id = %cx.self_addr().proc_addr(),
1426 actor_id = %cx.self_addr(),
1427 "host is shut down, sending mailbox handle to bootstrap for draining"
1428 );
1429 if let Some(handle) = host.take_frontend_handle()
1430 && let Err(mut handle) = tx.send(handle)
1431 {
1432 handle.stop("bootstrap shutdown receiver dropped");
1433 }
1434 }
1435 _ => {}
1436 }
1437
1438 Ok(())
1439 }
1440}
1441
1442#[derive(Debug, Clone, PartialEq, Eq, Named, Serialize, Deserialize)]
1443pub struct ProcState {
1444 pub proc_id: ProcAddr,
1445 pub create_rank: usize,
1446 pub mesh_agent: ActorRef<ProcAgent>,
1447 pub bootstrap_command: Option<BootstrapCommand>,
1448 pub proc_status: Option<bootstrap::ProcStatus>,
1449}
1450wirevalue::register_type!(ProcState);
1451
1452impl HostAgent {
1453 async fn proc_state(&self, id: &ResourceId) -> resource::State<ProcState> {
1456 match self.created.get(id) {
1457 Some(state) => self.proc_state_from(id, state).await,
1458 None => resource::State {
1459 id: id.clone(),
1460 status: resource::Status::NotExist,
1461 state: None,
1462 generation: 0,
1463 timestamp: std::time::SystemTime::now(),
1464 },
1465 }
1466 }
1467
1468 async fn proc_state_from(
1472 &self,
1473 id: &ResourceId,
1474 state: &ProcCreationState,
1475 ) -> resource::State<ProcState> {
1476 match state {
1477 ProcCreationState {
1478 rank,
1479 created: Ok((proc_id, mesh_agent)),
1480 ..
1481 } => {
1482 let (raw_status, proc_status, bootstrap_command) = match self.host() {
1483 Some(host) => {
1484 let (status, proc_status) = host.proc_status(proc_id).await;
1485 (status, proc_status, host.bootstrap_command())
1486 }
1487 None => (resource::Status::Unknown, None, None),
1488 };
1489 let status = raw_status.clamp_min(self.min_proc_status());
1490 resource::State {
1491 id: id.clone(),
1492 status,
1493 state: Some(ProcState {
1494 proc_id: proc_id.clone(),
1495 create_rank: *rank,
1496 mesh_agent: mesh_agent.clone(),
1497 bootstrap_command,
1498 proc_status,
1499 }),
1500 generation: 0,
1501 timestamp: std::time::SystemTime::now(),
1502 }
1503 }
1504 ProcCreationState {
1505 created: Err(e), ..
1506 } => resource::State {
1507 id: id.clone(),
1508 status: resource::Status::Failed(e.to_string()),
1509 state: None,
1510 generation: 0,
1511 timestamp: std::time::SystemTime::now(),
1512 },
1513 }
1514 }
1515}
1516
1517#[async_trait]
1518impl Handler<resource::GetState<ProcState>> for HostAgent {
1519 async fn handle(
1520 &mut self,
1521 cx: &Context<Self>,
1522 get_state: resource::GetState<ProcState>,
1523 ) -> anyhow::Result<()> {
1524 let state = self.proc_state(&get_state.id).await;
1525 get_state.reply.post(cx, state);
1526 Ok(())
1527 }
1528}
1529
1530#[derive(Debug, Clone, Serialize, Deserialize, Named)]
1548pub struct GetHostProcStates {
1549 pub proc_mesh_id: ProcMeshId,
1550 pub region: Region,
1555 pub keepalive: Option<std::time::SystemTime>,
1556 pub reply: hyperactor::PortRef<ValueOverlay<resource::State<ProcState>>>,
1561}
1562wirevalue::register_type!(GetHostProcStates);
1563
1564#[async_trait]
1565impl Handler<GetHostProcStates> for HostAgent {
1566 async fn handle(
1567 &mut self,
1568 cx: &Context<Self>,
1569 message: GetHostProcStates,
1570 ) -> anyhow::Result<()> {
1571 let selects = |state: &ProcCreationState| {
1572 state.proc_mesh_id.as_ref() == Some(&message.proc_mesh_id)
1573 && message.region.slice().contains(state.rank)
1574 };
1575
1576 if let Some(expires_after) = message.keepalive {
1579 for state in self.created.values_mut() {
1580 if selects(state) {
1581 state.expiry_time = Some(expires_after);
1582 }
1583 }
1584 }
1585
1586 let mut runs = Vec::new();
1594 for (id, state) in self.created.iter() {
1595 if selects(state) {
1596 let base = message.region.slice().index(state.rank)?;
1597 runs.push((base..(base + 1), self.proc_state_from(id, state).await));
1598 }
1599 }
1600
1601 if !runs.is_empty() {
1602 runs.sort_by_key(|(range, _)| range.start);
1605 message.reply.post(cx, ValueOverlay::try_from_runs(runs)?);
1606 }
1607
1608 Ok(())
1609 }
1610}
1611
1612#[async_trait]
1613impl Handler<crate::proc_agent::SelfCheck> for HostAgent {
1614 async fn handle(
1615 &mut self,
1616 cx: &Context<Self>,
1617 _: crate::proc_agent::SelfCheck,
1618 ) -> anyhow::Result<()> {
1619 let Some(duration) = hyperactor_config::global::get(crate::proc_agent::MESH_ORPHAN_TIMEOUT)
1624 else {
1625 return Ok(());
1626 };
1627 let now = std::time::SystemTime::now();
1628 let timeout = hyperactor_config::global::get(hyperactor::config::PROCESS_EXIT_TIMEOUT);
1629
1630 let expired: Vec<ResourceId> = self
1631 .created
1632 .iter()
1633 .filter_map(|(id, state)| {
1634 let expiry = state.expiry_time?;
1635 if now > expiry { Some(id.clone()) } else { None }
1636 })
1637 .collect();
1638
1639 if !expired.is_empty() {
1640 tracing::info!(
1641 "stopping {} orphaned procs past their keepalive expiry",
1642 expired.len(),
1643 );
1644 }
1645
1646 for id in expired {
1647 if let Some(ProcCreationState {
1648 created: Ok((proc_id, _)),
1649 ..
1650 }) = self.created.get(&id)
1651 {
1652 let proc_id = proc_id.clone();
1653 if let Some(host) = self.host() {
1654 host.request_stop(cx, &proc_id, timeout, "orphaned").await;
1655 }
1656 if let Some(state) = self.created.get_mut(&id) {
1658 state.expiry_time = None;
1659 }
1660 }
1661 }
1662
1663 cx.post_after(cx, crate::proc_agent::SelfCheck::default(), duration);
1664 Ok(())
1665 }
1666}
1667
1668#[async_trait]
1669impl Handler<resource::List> for HostAgent {
1670 async fn handle(&mut self, cx: &Context<Self>, list: resource::List) -> anyhow::Result<()> {
1671 list.reply.post(cx, self.created.keys().cloned().collect());
1672 Ok(())
1673 }
1674}
1675
1676#[async_trait]
1677impl Handler<resource::KeepaliveGetState<ProcState>> for HostAgent {
1678 async fn handle(
1679 &mut self,
1680 cx: &Context<Self>,
1681 message: resource::KeepaliveGetState<ProcState>,
1682 ) -> anyhow::Result<()> {
1683 if let Some(state) = self.created.get_mut(&message.get_state.id) {
1688 state.expiry_time = Some(message.expires_after);
1689 }
1690 <Self as Handler<resource::GetState<ProcState>>>::handle(self, cx, message.get_state).await
1691 }
1692}
1693
1694#[async_trait]
1695impl Handler<resource::StreamState<ProcState>> for HostAgent {
1696 async fn handle(
1697 &mut self,
1698 cx: &Context<Self>,
1699 stream_state: resource::StreamState<ProcState>,
1700 ) -> anyhow::Result<()> {
1701 let mut headers = Flattrs::new();
1705 headers.set(crate::proc_agent::STREAM_STATE_SUBSCRIBER, true);
1706
1707 for (id, proc) in self.created.iter() {
1708 if proc
1710 .proc_mesh_id
1711 .as_ref()
1712 .is_none_or(|mesh| mesh.resource_id() != &stream_state.id)
1713 {
1714 continue;
1715 }
1716
1717 let state = match &proc.created {
1718 Ok((proc_id, mesh_agent)) => {
1719 let (raw_status, proc_status, bootstrap_command) = match self.host() {
1720 Some(host) => {
1721 let (status, proc_status) = host.proc_status(proc_id).await;
1722 (status, proc_status, host.bootstrap_command())
1723 }
1724 None => (resource::Status::Unknown, None, None),
1725 };
1726 let status = raw_status.clamp_min(self.min_proc_status());
1727 resource::State {
1728 id: id.clone(),
1729 status,
1730 state: Some(ProcState {
1731 proc_id: proc_id.clone(),
1732 create_rank: proc.rank,
1733 mesh_agent: mesh_agent.clone(),
1734 bootstrap_command,
1735 proc_status,
1736 }),
1737 generation: 0,
1738 timestamp: std::time::SystemTime::now(),
1739 }
1740 }
1741 Err(e) => resource::State {
1742 id: id.clone(),
1743 status: resource::Status::Failed(e.to_string()),
1744 state: None,
1745 generation: 0,
1746 timestamp: std::time::SystemTime::now(),
1747 },
1748 };
1749
1750 stream_state
1751 .subscriber
1752 .post_with_headers(cx, headers.clone(), state);
1753 }
1754 Ok(())
1755 }
1756}
1757
1758#[derive(
1770 Debug,
1771 Clone,
1772 Named,
1773 Handler,
1774 RefClient,
1775 HandleClient,
1776 Serialize,
1777 Deserialize
1778)]
1779pub struct SetClientConfig {
1780 pub attrs: Attrs,
1781 pub rank: resource::Rank,
1784 pub reply: PortRef<crate::StatusOverlay>,
1791}
1792wirevalue::register_type!(SetClientConfig);
1793
1794#[async_trait]
1795impl Handler<SetClientConfig> for HostAgent {
1796 async fn handle(&mut self, cx: &Context<Self>, msg: SetClientConfig) -> anyhow::Result<()> {
1797 let rank = msg.rank.0.expect("rank should be stamped before delivery");
1798 hyperactor_config::global::set(
1802 hyperactor_config::global::Source::ClientOverride,
1803 msg.attrs,
1804 );
1805 tracing::debug!("installed client config override on host agent");
1806 let installed_overlay = crate::StatusOverlay::try_from_runs(vec![(
1814 rank..(rank + 1),
1815 resource::Status::Running,
1816 )])
1817 .expect("valid single-run overlay");
1818
1819 msg.reply.post(cx, installed_overlay);
1820
1821 Ok(())
1822 }
1823}
1824
1825#[derive(Debug, hyperactor::Handler, hyperactor::HandleClient)]
1834pub struct GetLocalProc {
1835 #[reply]
1836 pub proc_mesh_agent: PortHandle<ActorHandle<ProcAgent>>,
1837}
1838
1839#[async_trait]
1840impl Handler<GetLocalProc> for HostAgent {
1841 async fn handle(
1842 &mut self,
1843 cx: &Context<Self>,
1844 GetLocalProc { proc_mesh_agent }: GetLocalProc,
1845 ) -> anyhow::Result<()> {
1846 let host = self
1847 .host()
1848 .ok_or_else(|| anyhow::anyhow!("HostAgent has already shut down"))?;
1849 let agent = self
1850 .local_mesh_agent
1851 .get_or_init(|| ProcAgent::boot_v1(host.local_proc().clone(), None));
1852
1853 match agent {
1854 Err(e) => anyhow::bail!("error booting local proc: {}", e),
1855 Ok(agent) => proc_mesh_agent.post(cx, agent.clone()),
1856 };
1857
1858 Ok(())
1859 }
1860}
1861
1862#[async_trait]
1863impl Handler<PySpyDump> for HostAgent {
1864 async fn handle(
1865 &mut self,
1866 cx: &Context<Self>,
1867 message: PySpyDump,
1868 ) -> Result<(), anyhow::Error> {
1869 PySpyWorker::spawn_and_forward(cx, message.opts, message.result)
1870 }
1871}
1872
1873#[async_trait]
1874impl Handler<PySpyProfile> for HostAgent {
1875 async fn handle(
1876 &mut self,
1877 cx: &Context<Self>,
1878 message: PySpyProfile,
1879 ) -> Result<(), anyhow::Error> {
1880 PySpyProfileWorker::spawn_and_forward(cx, message.request, message.result)
1881 }
1882}
1883
1884#[async_trait]
1885impl Handler<ConfigDump> for HostAgent {
1886 async fn handle(
1887 &mut self,
1888 cx: &Context<Self>,
1889 message: ConfigDump,
1890 ) -> Result<(), anyhow::Error> {
1891 let entries = hyperactor_config::global::config_entries();
1892 message.result.post(cx, ConfigDumpResult { entries });
1893 Ok(())
1894 }
1895}
1896
1897#[cfg(all(test, fbcode_build))]
1898mod tests {
1899 use std::assert_matches;
1900
1901 use hyperactor::ActorAddr;
1902 use hyperactor::Proc;
1903 use hyperactor::channel::ChannelTransport;
1904 use hyperactor::id::Label;
1905 use hyperactor::id::Uid;
1906
1907 use super::*;
1908 use crate::bootstrap::ProcStatus;
1909 use crate::mesh_id::ResourceId;
1910 use crate::resource::CreateOrUpdateClient;
1911 use crate::resource::GetStateClient;
1912 use crate::resource::WaitRankStatusClient;
1913
1914 #[tokio::test]
1915 async fn test_basic() {
1916 let host = Host::new(
1917 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
1918 ChannelTransport::Unix.any(),
1919 )
1920 .await
1921 .unwrap();
1922
1923 let host_addr = host.addr().clone();
1924 let system_proc = host.system_proc().clone();
1925 let host_agent = system_proc
1926 .spawn_with_uid(
1927 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
1928 HostAgent::new_process(host, None),
1929 )
1930 .unwrap();
1931 HostAgent::wait_initialized(&host_agent).await.unwrap();
1932
1933 let client_proc = Proc::direct(ChannelTransport::Unix.any(), "client".to_string()).unwrap();
1934 let client = client_proc.client("client");
1935
1936 let id = ResourceId::instance(Label::new("proc1").unwrap());
1937
1938 host_agent
1941 .create_or_update(
1942 &client,
1943 id.clone(),
1944 resource::Rank::new(0),
1945 ProcSpec::default(),
1946 )
1947 .await
1948 .unwrap();
1949 let expected_location =
1954 hyperactor::Location::from(host_addr.clone()).with_via(id.uid().clone());
1955 let expected_proc_addr = ProcAddr::new(id.proc_id(), expected_location);
1956 assert_matches!(
1957 host_agent.get_state(&client, id.clone()).await.unwrap(),
1958 resource::State {
1959 id: resource_id,
1960 status: resource::Status::Running,
1961 state: Some(ProcState {
1962 proc_id,
1964 mesh_agent,
1967 bootstrap_command,
1968 proc_status: Some(ProcStatus::Ready { started_at: _, addr: _, agent: proc_status_mesh_agent}),
1969 ..
1970 }),
1971 ..
1972 } if id == resource_id
1973 && proc_id == expected_proc_addr
1974 && mesh_agent == ActorRef::attest(expected_proc_addr.actor_addr(crate::proc_agent::PROC_AGENT_ACTOR_NAME))
1975 && bootstrap_command == Some(BootstrapCommand::test())
1976 && mesh_agent == proc_status_mesh_agent
1977 );
1978 }
1979
1980 #[tokio::test]
1982 async fn test_wait_rank_status_already_running() {
1983 let host = Host::new(
1984 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
1985 ChannelTransport::Unix.any(),
1986 )
1987 .await
1988 .unwrap();
1989
1990 let system_proc = host.system_proc().clone();
1991 let host_agent = system_proc
1992 .spawn_with_uid(
1993 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
1994 HostAgent::new_process(host, None),
1995 )
1996 .unwrap();
1997 HostAgent::wait_initialized(&host_agent).await.unwrap();
1998
1999 let client_proc = Proc::direct(ChannelTransport::Unix.any(), "client".to_string()).unwrap();
2000 let client = client_proc.client("client");
2001
2002 let id = ResourceId::instance(Label::new("proc1").unwrap());
2003 host_agent
2004 .create_or_update(
2005 &client,
2006 id.clone(),
2007 resource::Rank::new(0),
2008 ProcSpec::default(),
2009 )
2010 .await
2011 .unwrap();
2012
2013 let (port, mut rx) = client.open_port::<crate::StatusOverlay>();
2015 host_agent
2016 .wait_rank_status(&client, id, resource::Status::Running, port.bind())
2017 .await
2018 .unwrap();
2019
2020 let overlay = tokio::time::timeout(Duration::from_secs(30), rx.recv())
2021 .await
2022 .expect("reply timed out")
2023 .expect("reply channel closed");
2024 assert!(!overlay.is_empty(), "expected non-empty overlay");
2025 }
2026
2027 #[tokio::test]
2030 async fn test_wait_rank_status_stop() {
2031 let host = Host::new(
2032 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
2033 ChannelTransport::Unix.any(),
2034 )
2035 .await
2036 .unwrap();
2037
2038 let system_proc = host.system_proc().clone();
2039 let host_agent = system_proc
2040 .spawn_with_uid(
2041 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
2042 HostAgent::new_process(host, None),
2043 )
2044 .unwrap();
2045 HostAgent::wait_initialized(&host_agent).await.unwrap();
2046
2047 let client_proc = Proc::direct(ChannelTransport::Unix.any(), "client".to_string()).unwrap();
2048 let client = client_proc.client("client");
2049
2050 let id = ResourceId::instance(Label::new("proc1").unwrap());
2051 host_agent
2052 .create_or_update(
2053 &client,
2054 id.clone(),
2055 resource::Rank::new(0),
2056 ProcSpec::default(),
2057 )
2058 .await
2059 .unwrap();
2060
2061 let (port, mut rx) = client.open_port::<crate::StatusOverlay>();
2063 host_agent
2064 .wait_rank_status(&client, id.clone(), resource::Status::Stopped, port.bind())
2065 .await
2066 .unwrap();
2067
2068 crate::resource::StopClient::stop(&host_agent, &client, id, "test".to_string())
2070 .await
2071 .unwrap();
2072
2073 let overlay = tokio::time::timeout(Duration::from_secs(30), rx.recv())
2075 .await
2076 .expect("reply timed out — proc did not reach Stopped")
2077 .expect("reply channel closed");
2078 assert!(!overlay.is_empty(), "expected non-empty overlay");
2079 }
2080
2081 #[tokio::test]
2084 async fn test_wait_rank_status_before_proc_exists() {
2085 let host = Host::new(
2086 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
2087 ChannelTransport::Unix.any(),
2088 )
2089 .await
2090 .unwrap();
2091
2092 let system_proc = host.system_proc().clone();
2093 let host_agent = system_proc
2094 .spawn_with_uid(
2095 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
2096 HostAgent::new_process(host, None),
2097 )
2098 .unwrap();
2099 HostAgent::wait_initialized(&host_agent).await.unwrap();
2100
2101 let client_proc = Proc::direct(ChannelTransport::Unix.any(), "client".to_string()).unwrap();
2102 let client = client_proc.client("client");
2103
2104 let id = ResourceId::instance(Label::new("proc1").unwrap());
2105
2106 let (port, mut rx) = client.open_port::<crate::StatusOverlay>();
2108 host_agent
2109 .wait_rank_status(&client, id.clone(), resource::Status::Running, port.bind())
2110 .await
2111 .unwrap();
2112
2113 host_agent
2116 .create_or_update(&client, id, resource::Rank::new(0), ProcSpec::default())
2117 .await
2118 .unwrap();
2119
2120 let overlay = tokio::time::timeout(Duration::from_secs(30), rx.recv())
2121 .await
2122 .expect("reply timed out — waiter was not flushed after CreateOrUpdate")
2123 .expect("reply channel closed");
2124 assert!(!overlay.is_empty(), "expected non-empty overlay");
2125 }
2126
2127 #[tokio::test]
2130 async fn test_drain_scoped_to_host_mesh_id() {
2131 let host = Host::new(
2132 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
2133 ChannelTransport::Unix.any(),
2134 )
2135 .await
2136 .unwrap();
2137
2138 let system_proc = host.system_proc().clone();
2139 let host_agent = system_proc
2140 .spawn_with_uid(
2141 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
2142 HostAgent::new_process(host, None),
2143 )
2144 .unwrap();
2145 HostAgent::wait_initialized(&host_agent).await.unwrap();
2146
2147 let client_proc = Proc::direct(ChannelTransport::Unix.any(), "client".to_string()).unwrap();
2148 let client = client_proc.client("client");
2149
2150 let mesh_a = HostMeshId::instance(Label::new("mesh-a").unwrap());
2151 let mesh_b = HostMeshId::instance(Label::new("mesh-b").unwrap());
2152 let proc_a_id = ResourceId::instance(Label::new("proc-a").unwrap());
2153 let proc_b_id = ResourceId::instance(Label::new("proc-b").unwrap());
2154
2155 let spec_a = ProcSpec {
2157 host_mesh_id: Some(mesh_a.clone()),
2158 ..Default::default()
2159 };
2160 host_agent
2161 .create_or_update(&client, proc_a_id.clone(), resource::Rank::new(0), spec_a)
2162 .await
2163 .unwrap();
2164
2165 let spec_b = ProcSpec {
2167 host_mesh_id: Some(mesh_b.clone()),
2168 ..Default::default()
2169 };
2170 host_agent
2171 .create_or_update(&client, proc_b_id.clone(), resource::Rank::new(1), spec_b)
2172 .await
2173 .unwrap();
2174
2175 assert_matches!(
2177 host_agent
2178 .get_state(&client, proc_a_id.clone())
2179 .await
2180 .unwrap(),
2181 resource::State {
2182 status: resource::Status::Running,
2183 ..
2184 }
2185 );
2186 assert_matches!(
2187 host_agent
2188 .get_state(&client, proc_b_id.clone())
2189 .await
2190 .unwrap(),
2191 resource::State {
2192 status: resource::Status::Running,
2193 ..
2194 }
2195 );
2196
2197 let (drain_reply, mut drain_rx) = client.open_port::<crate::StatusOverlay>();
2199 host_agent
2200 .drain_host(
2201 &client,
2202 Duration::from_secs(5),
2203 16,
2204 Some(mesh_a.clone()),
2205 resource::Rank::new(0),
2206 drain_reply.bind(),
2207 )
2208 .await
2209 .unwrap();
2210 drain_rx.recv().await.unwrap();
2212
2213 assert_matches!(
2215 host_agent
2216 .get_state(&client, proc_a_id.clone())
2217 .await
2218 .unwrap(),
2219 resource::State {
2220 status: resource::Status::NotExist,
2221 ..
2222 }
2223 );
2224
2225 assert_matches!(
2227 host_agent
2228 .get_state(&client, proc_b_id.clone())
2229 .await
2230 .unwrap(),
2231 resource::State {
2232 status: resource::Status::Running,
2233 ..
2234 }
2235 );
2236 }
2237
2238 #[tokio::test]
2241 async fn test_drain_none_drains_all() {
2242 let host = Host::new(
2243 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
2244 ChannelTransport::Unix.any(),
2245 )
2246 .await
2247 .unwrap();
2248
2249 let system_proc = host.system_proc().clone();
2250 let host_agent = system_proc
2251 .spawn_with_uid(
2252 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
2253 HostAgent::new_process(host, None),
2254 )
2255 .unwrap();
2256 HostAgent::wait_initialized(&host_agent).await.unwrap();
2257
2258 let client_proc = Proc::direct(ChannelTransport::Unix.any(), "client".to_string()).unwrap();
2259 let client = client_proc.client("client");
2260
2261 let mesh_a = HostMeshId::instance(Label::new("mesh-a").unwrap());
2262 let mesh_b = HostMeshId::instance(Label::new("mesh-b").unwrap());
2263 let proc_a_id = ResourceId::instance(Label::new("proc-a").unwrap());
2264 let proc_b_id = ResourceId::instance(Label::new("proc-b").unwrap());
2265
2266 let spec_a = ProcSpec {
2267 host_mesh_id: Some(mesh_a),
2268 ..Default::default()
2269 };
2270 host_agent
2271 .create_or_update(&client, proc_a_id.clone(), resource::Rank::new(0), spec_a)
2272 .await
2273 .unwrap();
2274
2275 let spec_b = ProcSpec {
2276 host_mesh_id: Some(mesh_b),
2277 ..Default::default()
2278 };
2279 host_agent
2280 .create_or_update(&client, proc_b_id.clone(), resource::Rank::new(1), spec_b)
2281 .await
2282 .unwrap();
2283
2284 let (drain_reply, mut drain_rx) = client.open_port::<crate::StatusOverlay>();
2286 host_agent
2287 .drain_host(
2288 &client,
2289 Duration::from_secs(5),
2290 16,
2291 None,
2292 resource::Rank::new(0),
2293 drain_reply.bind(),
2294 )
2295 .await
2296 .unwrap();
2297 drain_rx.recv().await.unwrap();
2299
2300 assert_matches!(
2302 host_agent.get_state(&client, proc_a_id).await.unwrap(),
2303 resource::State {
2304 status: resource::Status::NotExist,
2305 ..
2306 }
2307 );
2308 assert_matches!(
2309 host_agent.get_state(&client, proc_b_id).await.unwrap(),
2310 resource::State {
2311 status: resource::Status::NotExist,
2312 ..
2313 }
2314 );
2315 }
2316
2317 #[tokio::test]
2323 async fn test_service_proc_query_child_has_queue_stats() {
2324 use hyperactor::introspect::IntrospectMessage;
2325 use hyperactor::introspect::IntrospectResult;
2326
2327 let host = Host::new(
2328 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
2329 ChannelTransport::Unix.any(),
2330 )
2331 .await
2332 .unwrap();
2333
2334 let system_proc = host.system_proc().clone();
2335 let host_agent = system_proc
2336 .spawn_with_uid(
2337 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
2338 HostAgent::new_process(host, None),
2339 )
2340 .unwrap();
2341 HostAgent::wait_initialized(&host_agent).await.unwrap();
2342
2343 let client_proc =
2344 Proc::direct(ChannelTransport::Unix.any(), "qd_client".to_string()).unwrap();
2345 let client = client_proc.client("client");
2346
2347 let name = ResourceId::instance(Label::new("qd_test_proc").unwrap());
2350 host_agent
2351 .create_or_update(
2352 &client,
2353 name.clone(),
2354 resource::Rank::new(0),
2355 ProcSpec::default(),
2356 )
2357 .await
2358 .unwrap();
2359
2360 let agent_ref = system_proc
2363 .proc_addr()
2364 .actor_addr(HOST_MESH_AGENT_ACTOR_NAME);
2365 let agent_id: ActorAddr = agent_ref;
2366 let port = agent_id.introspect_port();
2367
2368 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
2371 loop {
2372 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2373 port.post(
2374 &client,
2375 IntrospectMessage::QueryChild {
2376 child_ref: Addr::Proc(system_proc.proc_addr().clone()),
2377 reply: reply_port.bind(),
2378 },
2379 );
2380 let payload = tokio::time::timeout(std::time::Duration::from_secs(5), reply_rx.recv())
2381 .await
2382 .expect("QueryChild timed out")
2383 .expect("reply channel closed");
2384
2385 let attrs: hyperactor_config::Attrs =
2386 serde_json::from_str(&payload.attrs).expect("valid attrs JSON");
2387
2388 let hwm = attrs
2389 .get(crate::introspect::ACTOR_WORK_QUEUE_DEPTH_HIGH_WATER_MARK)
2390 .copied()
2391 .unwrap_or(0);
2392 let last_nonzero: Option<u64> = attrs
2393 .get(crate::introspect::LAST_NONZERO_QUEUE_DEPTH_AGE_MS)
2394 .copied()
2395 .flatten();
2396
2397 if hwm > 0 {
2398 assert!(
2401 last_nonzero.is_some(),
2402 "last-nonzero should be Some when watermark is {hwm}",
2403 );
2404 break;
2405 }
2406
2407 assert!(
2408 tokio::time::Instant::now() < deadline,
2409 "timed out waiting for service proc watermark > 0",
2410 );
2411 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2412 }
2413 }
2414
2415 #[tokio::test]
2419 async fn test_spawn_procs_many_per_host() {
2420 let host = Host::new(
2421 BootstrapProcManager::new(BootstrapCommand::test()).unwrap(),
2422 ChannelTransport::Unix.any(),
2423 )
2424 .await
2425 .unwrap();
2426
2427 let system_proc = host.system_proc().clone();
2428 let host_agent = system_proc
2429 .spawn_with_uid(
2430 Uid::singleton(Label::new(HOST_MESH_AGENT_ACTOR_NAME).unwrap()),
2431 HostAgent::new(HostAgentMode::Process {
2432 host,
2433 shutdown_tx: None,
2434 }),
2435 )
2436 .unwrap();
2437
2438 let client_proc = Proc::direct(ChannelTransport::Unix.any(), "client".to_string()).unwrap();
2439 let client = client_proc.client("client");
2440
2441 let proc_mesh_id = ProcMeshId::singleton(Label::new("spawn-many").unwrap());
2442 let num_per_host = 4;
2443
2444 let agent_ref: ActorRef<HostAgent> = host_agent.bind();
2447 agent_ref.post(
2448 &client,
2449 SpawnProcs {
2450 rank: resource::Rank::new(0),
2451 proc_mesh_id: proc_mesh_id.clone(),
2452 num_per_host,
2453 client_config_override: Attrs::new(),
2454 host_mesh_id: None,
2455 default_bootstrap_command: None,
2456 proc_bind: None,
2457 bootstrap_commands: None,
2458 status_reply: None,
2459 },
2460 );
2461
2462 for rank in 0..num_per_host {
2464 let id = proc_name(&proc_mesh_id, rank);
2465 let (port, mut rx) = client.open_port::<crate::StatusOverlay>();
2466 host_agent
2467 .wait_rank_status(&client, id.clone(), resource::Status::Running, port.bind())
2468 .await
2469 .unwrap();
2470 let overlay = tokio::time::timeout(Duration::from_secs(30), rx.recv())
2471 .await
2472 .unwrap_or_else(|_| panic!("proc {rank} did not reach Running"))
2473 .expect("reply channel closed");
2474 assert!(
2475 !overlay.is_empty(),
2476 "expected non-empty Running overlay for proc {rank}",
2477 );
2478
2479 assert_matches!(
2480 host_agent.get_state(&client, id).await.unwrap(),
2481 resource::State {
2482 status: resource::Status::Running,
2483 ..
2484 }
2485 );
2486 }
2487 }
2488}