1use std::collections::HashMap;
62use std::collections::hash_map::Entry;
63use std::fmt;
64use std::marker::PhantomData;
65use std::panic::AssertUnwindSafe;
66use std::str::FromStr;
67use std::sync::Arc;
68use std::sync::Mutex as StdMutex;
69use std::time::Duration;
70
71use async_trait::async_trait;
72use futures::Future;
73use futures::FutureExt;
74use futures::StreamExt;
75use futures::stream;
76use hyperactor::Actor;
77use hyperactor::ActorAddr;
78use hyperactor::ActorHandle;
79use hyperactor::ActorRef;
80use hyperactor::Gateway;
81use hyperactor::Location;
82use hyperactor::Proc;
83use hyperactor::ProcAddr;
84use hyperactor::ProcId;
85use hyperactor::actor::Binds;
86use hyperactor::actor::Referable;
87use hyperactor::channel;
88use hyperactor::channel::ChannelAddr;
89use hyperactor::channel::ChannelError;
90use hyperactor::channel::ChannelTransport;
91use hyperactor::channel::Rx;
92use hyperactor::channel::ServerError;
93use hyperactor::channel::Tx;
94use hyperactor::context;
95use hyperactor::gateway::GatewayServeHandle;
96use hyperactor::gateway::PeerAttachError;
97use hyperactor::gateway::PeerAttachGuard;
98use hyperactor::id::Label;
99use hyperactor::mailbox::IntoBoxedMailboxSender as _;
100use hyperactor::mailbox::MailboxClient;
101use hyperactor::mailbox::MailboxServer;
102use tokio::process::Child;
103use tokio::process::Command;
104use tokio::sync::Mutex;
105
106use crate::mesh_id::ResourceId;
107
108pub const SERVICE_PROC_NAME: &str = hyperactor::proc::LEGACY_SERVICE_PROC_NAME;
112
113pub(crate) fn legacy_service_proc_id() -> ProcId {
114 ProcId::singleton(Label::strip(SERVICE_PROC_NAME))
115}
116
117pub const LOCAL_PROC_NAME: &str = "local";
126
127#[derive(Debug, thiserror::Error)]
129pub enum HostError {
130 #[error(transparent)]
132 ChannelError(#[from] ChannelError),
133
134 #[error(transparent)]
136 ServerError(#[from] ServerError),
137
138 #[error("proc '{0}' already exists")]
140 ProcExists(String),
141
142 #[error("host proc-name registry is poisoned")]
144 ProcRegistryPoisoned,
145
146 #[error("failed to attach proc '{0}' to the host gateway: {1}")]
148 PeerAttachFailure(ProcAddr, #[source] PeerAttachError),
149
150 #[error("proc '{0}' spawn panicked: {1}")]
152 SpawnPanicked(String, String),
153
154 #[error("proc '{0}' (command: {1}) failed to spawn process: {2}")]
156 ProcessSpawnFailure(ProcAddr, String, #[source] std::io::Error),
157
158 #[error("proc '{0}' failed to configure process: {1}")]
160 ProcessConfigurationFailure(ProcAddr, #[source] anyhow::Error),
161
162 #[error("failed to spawn agent on proc '{0}': {1}")]
164 AgentSpawnFailure(ProcAddr, #[source] anyhow::Error),
165
166 #[error("parameter '{0}' missing: {1}")]
168 MissingParameter(String, std::env::VarError),
169
170 #[error("parameter '{0}' invalid: {1}")]
172 InvalidParameter(String, anyhow::Error),
173
174 #[error("failed to attach gateway via session: {0}")]
176 ViaAttachFailure(#[source] anyhow::Error),
177
178 #[error("failed to construct built-in proc: {0}")]
180 ProcConstructionFailure(#[source] anyhow::Error),
181
182 #[error("host service proc address must have a direct frontend location, got {0}")]
184 InvalidFrontendLocation(Location),
185}
186
187#[derive(Debug)]
192pub(crate) struct HostShutdownHandles {
193 frontend: Option<GatewayServeHandle>,
194 via: Option<GatewayServeHandle>,
195}
196
197impl HostShutdownHandles {
198 pub(crate) fn new(frontend: GatewayServeHandle, via: Option<GatewayServeHandle>) -> Self {
199 Self {
200 frontend: Some(frontend),
201 via,
202 }
203 }
204
205 pub(crate) async fn stop_and_join(mut self, reason: &str) {
206 if let Some(mut frontend) = self.frontend.take() {
207 frontend.stop(reason);
208 if let Err(error) = frontend.join().await {
209 tracing::warn!(%error, "failed to join host shutdown frontend");
210 }
211 }
212 if let Some(mut via) = self.via.take() {
213 via.stop(reason);
214 if let Err(error) = via.join().await {
215 tracing::warn!(%error, "failed to join host shutdown via transport");
216 }
217 }
218 }
219}
220
221impl Drop for HostShutdownHandles {
222 fn drop(&mut self) {
223 if let Some(frontend) = &mut self.frontend {
224 frontend.stop("host shutdown handles dropped");
225 }
226 if let Some(via) = &mut self.via {
227 via.stop("host shutdown handles dropped");
228 }
229 }
230}
231
232struct ProcHandleKillGuard<H: ProcHandle> {
233 handle: Option<H>,
234}
235
236impl<H: ProcHandle> ProcHandleKillGuard<H> {
237 fn handle(&self) -> &H {
238 self.handle
239 .as_ref()
240 .expect("active spawn cleanup owns the proc handle")
241 }
242
243 fn disarm(mut self) {
244 self.handle = None;
245 }
246
247 fn maybe_kill(&mut self) -> impl Future<Output = ()> + Send + 'static {
248 let handle = self.handle.take();
249
250 async move {
251 let Some(handle) = handle else {
252 return;
253 };
254 match handle.kill().await {
255 Ok(_) | Err(TerminateError::AlreadyTerminated(_)) => {}
256 Err(error) => {
257 tracing::warn!(proc = %handle.proc_addr(), %error, "failed to clean up proc spawn");
258 }
259 }
260 }
261 }
262}
263
264impl<H: ProcHandle> Drop for ProcHandleKillGuard<H> {
265 fn drop(&mut self) {
266 if self.handle.is_some() {
267 tokio::spawn(self.maybe_kill());
268 }
269 }
270}
271
272pub struct Host<M> {
278 procs: Arc<StdMutex<HashMap<String, PeerAttachGuard>>>,
283 frontend_addr: ChannelAddr,
284 backend_addr: ChannelAddr,
285 gateway: Gateway,
291 frontend_handle: Option<GatewayServeHandle>,
292 backend_handle: Option<GatewayServeHandle>,
293 via_handle: Option<GatewayServeHandle>,
297 manager: Arc<M>,
298 service_proc: Proc,
299 local_proc: Proc,
300}
301
302impl<M: ProcManager> Host<M> {
303 pub async fn new(manager: M, addr: ChannelAddr) -> Result<Self, HostError> {
305 Self::new_with_default(manager, addr, None).await
306 }
307
308 pub async fn new_with_default(
313 manager: M,
314 addr: ChannelAddr,
315 listener: Option<std::net::TcpListener>,
316 ) -> Result<Self, HostError> {
317 Self::new_with_gateway(
322 manager,
323 ProcAddr::new(legacy_service_proc_id(), addr.into()),
324 listener,
325 Gateway::global().clone(),
326 None,
327 )
328 .await
329 }
330
331 #[hyperactor::instrument(fields(addr=addr.to_string()))]
349 pub async fn new_with_gateway(
350 manager: M,
351 addr: ProcAddr,
352 listener: Option<std::net::TcpListener>,
353 gateway: Gateway,
354 via: Option<ChannelAddr>,
355 ) -> Result<Self, HostError> {
356 let service_proc_id = addr.id().clone();
357 let addr = match addr.location() {
358 Location::Addr(addr) => addr.clone(),
359 Location::Via(..) => {
360 return Err(HostError::InvalidFrontendLocation(addr.location().clone()));
361 }
362 };
363 let mut backend_handle = Gateway::serve(&gateway, ChannelAddr::any(manager.transport()))?;
364 let backend_addr = gateway.default_location().addr().clone();
365
366 let frontend_result: Result<GatewayServeHandle, ChannelError> = if addr.transport().is_net()
376 {
377 gateway.serve_mux_with_listener(addr, listener)
378 } else {
379 gateway.serve_with_listener(addr, listener)
380 };
381 let mut frontend_handle = match frontend_result {
382 Ok(handle) => handle,
383 Err(error) => {
384 backend_handle.stop("host setup failed");
385 if let Err(join_error) = backend_handle.join().await {
386 tracing::warn!(
387 error = %join_error,
388 "failed to join backend server after host setup error"
389 );
390 }
391 return Err(error.into());
392 }
393 };
394 let frontend_addr = gateway.default_location().addr().clone();
395 assert!(
396 !matches!(frontend_addr, ChannelAddr::Alias { .. }),
397 "gateway frontend serve must canonicalize aliases"
398 );
399
400 let via_handle = match via {
407 Some(via_addr) => match gateway.serve_via(via_addr).await {
408 Ok(handle) => Some(handle),
409 Err(error) => {
410 frontend_handle.stop("host setup failed");
411 if let Err(join_error) = frontend_handle.join().await {
412 tracing::warn!(
413 error = %join_error,
414 "failed to join frontend server after via attach error"
415 );
416 }
417 backend_handle.stop("host setup failed");
418 if let Err(join_error) = backend_handle.join().await {
419 tracing::warn!(
420 error = %join_error,
421 "failed to join backend server after via attach error"
422 );
423 }
424 return Err(HostError::ViaAttachFailure(error));
425 }
426 },
427 None => None,
428 };
429
430 let expected_service_proc_addr =
435 ProcAddr::new(service_proc_id.clone(), gateway.default_location());
436 let service_proc = Proc::builder()
437 .proc_id(service_proc_id)
438 .shared_gateway(gateway.clone())
439 .build()
440 .map_err(HostError::ProcConstructionFailure)?;
441 assert_eq!(
442 service_proc.proc_addr(),
443 expected_service_proc_addr,
444 "service proc must use the gateway's active advertised location"
445 );
446 let local_proc = Proc::builder()
447 .proc_id(ProcId::instance(Label::strip(LOCAL_PROC_NAME)))
448 .shared_gateway(gateway.clone())
449 .build()
450 .map_err(HostError::ProcConstructionFailure)?;
451 let service_proc_id = service_proc.proc_addr().clone();
452 let local_proc_id = local_proc.proc_addr().clone();
453
454 tracing::info!(
455 frontend_addr = frontend_addr.to_string(),
456 backend_addr = backend_addr.to_string(),
457 service_proc_id = service_proc_id.to_string(),
458 local_proc_id = local_proc_id.to_string(),
459 "serving host"
460 );
461
462 Ok(Host {
463 procs: Arc::new(StdMutex::new(HashMap::new())),
464 frontend_addr,
465 backend_addr,
466 gateway,
467 frontend_handle: Some(frontend_handle),
468 backend_handle: Some(backend_handle),
469 via_handle,
470 manager: Arc::new(manager),
471 service_proc,
472 local_proc,
473 })
474 }
475
476 pub fn manager(&self) -> &M {
478 self.manager.as_ref()
479 }
480
481 pub fn addr(&self) -> &ChannelAddr {
483 &self.frontend_addr
484 }
485
486 pub fn system_proc(&self) -> &Proc {
489 &self.service_proc
490 }
491
492 pub fn local_proc(&self) -> &Proc {
497 &self.local_proc
498 }
499
500 pub(crate) fn spawn(
509 &self,
510 name: String,
511 config: M::Config,
512 ) -> impl Future<Output = Result<(ProcAddr, ActorRef<ManagerAgent<M>>), HostError>> + Send + 'static
513 where
514 M: Send + Sync + 'static,
515 M::Config: Send + 'static,
516 {
517 let procs = Arc::clone(&self.procs);
518 let backend_addr = self.backend_addr.clone();
519 let gateway = self.gateway.clone();
520 let manager = Arc::clone(&self.manager);
521
522 async move {
523 if procs
524 .lock()
525 .map_err(|_| HostError::ProcRegistryPoisoned)?
526 .contains_key(&name)
527 {
528 return Err(HostError::ProcExists(name));
529 }
530
531 let resource_id = ResourceId::from_name(&name);
534 let proc_uid = resource_id.uid().clone();
535 let proc_id =
536 resource_id.proc_addr(gateway.default_location().append_via(proc_uid.clone()));
537 let handle = manager.spawn(proc_id.clone(), backend_addr, config).await?;
538 let mut proc_handle_guard = ProcHandleKillGuard {
539 handle: Some(handle),
540 };
541
542 let created = async {
543 let timeout: Duration =
544 hyperactor_config::global::get(hyperactor::config::HOST_SPAWN_READY_TIMEOUT);
545
546 let ready = if timeout == Duration::from_secs(0) {
547 ReadyProc::ensure(proc_handle_guard.handle()).await
548 } else {
549 match tokio::time::timeout(
550 timeout,
551 ReadyProc::ensure(proc_handle_guard.handle()),
552 )
553 .await
554 {
555 Ok(result) => result,
556 Err(_elapsed) => Err(ReadyProcError::Timeout),
557 }
558 }
559 .map_err(|e| {
560 HostError::ProcessConfigurationFailure(
561 proc_id.clone(),
562 anyhow::anyhow!("{e:?}"),
563 )
564 })?;
565
566 let child_sender = MailboxClient::dial(ready.addr().clone()).map_err(|e| {
567 HostError::ProcessConfigurationFailure(
568 proc_id.clone(),
569 anyhow::anyhow!("failed to dial spawned proc at {}: {}", ready.addr(), e),
570 )
571 })?;
572 let agent_ref = ready.agent_ref().clone();
573 drop(ready);
574
575 let guard = gateway
576 .attach_peer(proc_uid.clone(), child_sender.into_boxed())
577 .map_err(|error| HostError::PeerAttachFailure(proc_id.clone(), error))?;
578 match procs
579 .lock()
580 .map_err(|_| HostError::ProcRegistryPoisoned)?
581 .entry(name.clone())
582 {
583 Entry::Vacant(entry) => {
584 entry.insert(guard);
585 }
586 Entry::Occupied(_) => return Err(HostError::ProcExists(name.clone())),
587 }
588
589 Ok((proc_id.clone(), agent_ref))
590 }
591 .await;
592
593 match created {
594 Ok(created) => {
595 proc_handle_guard.disarm();
596 Ok(created)
597 }
598 Err(error) => {
599 proc_handle_guard.maybe_kill().await;
600 Err(error)
601 }
602 }
603 }
604 }
605
606 pub fn gateway(&self) -> &Gateway {
612 &self.gateway
613 }
614
615 pub(crate) fn remove_proc(&self, name: &str) {
617 let guard = self
618 .procs
619 .lock()
620 .expect("procs mutex poisoned")
621 .remove(name);
622 drop(guard);
623 }
624
625 pub(crate) fn take_frontend_handle(&mut self) -> Option<GatewayServeHandle> {
631 self.frontend_handle.take()
632 }
633
634 pub(crate) fn take_shutdown_handles(&mut self) -> HostShutdownHandles {
636 let frontend = self
637 .take_frontend_handle()
638 .expect("host shutdown must own its frontend handle");
639 HostShutdownHandles::new(frontend, self.via_handle.take())
640 }
641
642 pub(crate) async fn shutdown_servers(&mut self) {
644 if let Some(mut handle) = self.frontend_handle.take() {
645 handle.stop("host shutdown");
646 if let Err(error) = handle.join().await {
647 tracing::warn!(%error, "failed to join host frontend server");
648 }
649 }
650 if let Some(mut handle) = self.backend_handle.take() {
651 handle.stop("host shutdown");
652 if let Err(error) = handle.join().await {
653 tracing::warn!(%error, "failed to join host backend server");
654 }
655 }
656 if let Some(mut handle) = self.via_handle.take() {
657 handle.stop("host shutdown");
658 if let Err(error) = handle.join().await {
659 tracing::warn!(%error, "failed to join host via server");
660 }
661 }
662 }
663}
664
665impl<M> Drop for Host<M> {
666 fn drop(&mut self) {
667 if let Some(mut handle) = self.frontend_handle.take() {
668 handle.stop("host dropped");
669 }
670 if let Some(mut handle) = self.backend_handle.take() {
671 handle.stop("host dropped");
672 }
673 if let Some(mut handle) = self.via_handle.take() {
674 handle.stop("host dropped");
675 }
676 }
677}
678
679#[derive(Debug, Clone)]
681pub enum ReadyError<TerminalStatus> {
682 Terminal(TerminalStatus),
684 ChannelClosed,
686}
687
688#[derive(Debug, Clone)]
690pub enum ReadyProcError<TerminalStatus> {
691 Timeout,
693 Ready(ReadyError<TerminalStatus>),
695 MissingAddr,
697 MissingAgentRef,
700}
701
702impl<T> From<ReadyError<T>> for ReadyProcError<T> {
703 fn from(e: ReadyError<T>) -> Self {
704 ReadyProcError::Ready(e)
705 }
706}
707
708#[derive(Debug, Clone)]
710pub enum WaitError {
711 ChannelClosed,
713}
714
715#[derive(Debug)]
728pub enum TerminateError<TerminalStatus> {
729 Unsupported,
731 AlreadyTerminated(TerminalStatus),
734 ChannelClosed,
736 Io(anyhow::Error),
739}
740
741impl<T: fmt::Debug> fmt::Display for TerminateError<T> {
742 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
743 match self {
744 TerminateError::Unsupported => write!(f, "terminate/kill unsupported by manager"),
745 TerminateError::AlreadyTerminated(st) => {
746 write!(f, "proc already terminated (status: {st:?})")
747 }
748 TerminateError::ChannelClosed => {
749 write!(f, "lifecycle channel closed; cannot observe state")
750 }
751 TerminateError::Io(err) => write!(f, "I/O error during terminate/kill: {err}"),
752 }
753 }
754}
755
756impl<T: fmt::Debug> std::error::Error for TerminateError<T> {
757 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
758 match self {
759 TerminateError::Io(err) => Some(err.root_cause()),
760 _ => None,
761 }
762 }
763}
764
765#[derive(Debug)]
774pub struct TerminateSummary {
775 pub attempted: usize,
778 pub ok: usize,
784 pub failed: usize,
791}
792
793impl fmt::Display for TerminateSummary {
794 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795 write!(
796 f,
797 "attempted={} ok={} failed={}",
798 self.attempted, self.ok, self.failed
799 )
800 }
801}
802
803#[async_trait::async_trait]
804pub trait SingleTerminate: Send + Sync {
806 async fn terminate_proc(
825 &self,
826 cx: &impl context::Actor,
827 proc: &ProcAddr,
828 timeout: std::time::Duration,
829 reason: &str,
830 ) -> Result<(Vec<ActorAddr>, Vec<ActorAddr>), anyhow::Error>;
831}
832
833#[async_trait::async_trait]
843pub trait BulkTerminate: Send + Sync {
844 async fn terminate_all(
867 &self,
868 cx: &impl context::Actor,
869 timeout: std::time::Duration,
870 max_in_flight: usize,
871 reason: &str,
872 ) -> TerminateSummary;
873}
874
875impl<M: ProcManager + BulkTerminate> Host<M> {
878 pub async fn terminate_children(
892 &mut self,
893 cx: &impl context::Actor,
894 timeout: Duration,
895 max_in_flight: usize,
896 reason: &str,
897 ) -> TerminateSummary {
898 let summary = self
899 .manager
900 .terminate_all(cx, timeout, max_in_flight, reason)
901 .await;
902 {
903 let _guards = {
904 let mut entries = self.procs.lock().expect("procs mutex poisoned");
905 std::mem::take(&mut *entries)
906 };
907 }
908 summary
909 }
910}
911
912#[async_trait::async_trait]
913impl<M: ProcManager + SingleTerminate> SingleTerminate for Host<M> {
914 async fn terminate_proc(
915 &self,
916 cx: &impl context::Actor,
917 proc: &ProcAddr,
918 timeout: Duration,
919 reason: &str,
920 ) -> Result<(Vec<ActorAddr>, Vec<ActorAddr>), anyhow::Error> {
921 self.manager.terminate_proc(cx, proc, timeout, reason).await
922 }
923}
924
925pub struct ReadyProc<'a, H: ProcHandle> {
933 handle: &'a H,
934 addr: ChannelAddr,
935 agent_ref: ActorRef<H::Agent>,
936}
937
938impl<'a, H: ProcHandle> ReadyProc<'a, H> {
939 pub async fn ensure(
947 handle: &'a H,
948 ) -> Result<ReadyProc<'a, H>, ReadyProcError<H::TerminalStatus>> {
949 handle.ready().await?;
950 let addr = handle.addr().ok_or(ReadyProcError::MissingAddr)?;
951 let agent_ref = handle.agent_ref().ok_or(ReadyProcError::MissingAgentRef)?;
952 Ok(ReadyProc {
953 handle,
954 addr,
955 agent_ref,
956 })
957 }
958
959 pub fn proc_addr(&self) -> &ProcAddr {
961 self.handle.proc_addr()
962 }
963
964 pub fn addr(&self) -> &ChannelAddr {
966 &self.addr
967 }
968
969 pub fn agent_ref(&self) -> &ActorRef<H::Agent> {
971 &self.agent_ref
972 }
973}
974
975#[async_trait]
1010pub trait ProcHandle: Clone + Send + Sync + 'static {
1011 type Agent: Actor + Referable;
1017
1018 type TerminalStatus: std::fmt::Debug + Clone + Send + Sync + 'static;
1025
1026 fn proc_addr(&self) -> &ProcAddr;
1028
1029 fn addr(&self) -> Option<ChannelAddr>;
1036
1037 fn agent_ref(&self) -> Option<ActorRef<Self::Agent>>;
1044
1045 async fn ready(&self) -> Result<(), ReadyError<Self::TerminalStatus>>;
1048
1049 async fn wait(&self) -> Result<Self::TerminalStatus, WaitError>;
1052
1053 async fn terminate(
1069 &self,
1070 cx: &impl context::Actor,
1071 timeout: Duration,
1072 reason: &str,
1073 ) -> Result<Self::TerminalStatus, TerminateError<Self::TerminalStatus>>;
1074
1075 async fn kill(&self) -> Result<Self::TerminalStatus, TerminateError<Self::TerminalStatus>>;
1080}
1081
1082#[async_trait]
1086pub trait ProcManager {
1087 type Handle: ProcHandle;
1089
1090 type Config = ();
1092
1093 fn transport(&self) -> ChannelTransport;
1098
1099 async fn spawn(
1106 &self,
1107 proc_id: ProcAddr,
1108 forwarder_addr: ChannelAddr,
1109 config: Self::Config,
1110 ) -> Result<Self::Handle, HostError>;
1111}
1112
1113pub type ManagerAgent<M> = <<M as ProcManager>::Handle as ProcHandle>::Agent; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1132pub enum LocalProcStatus {
1133 Stopping,
1135 Stopped,
1137}
1138
1139pub struct LocalProcManager<S> {
1152 procs: Arc<Mutex<HashMap<ProcAddr, Proc>>>,
1153 stopping: Arc<Mutex<HashMap<ProcAddr, tokio::sync::watch::Sender<LocalProcStatus>>>>,
1154 spawn: S,
1155}
1156
1157impl<S> LocalProcManager<S> {
1158 pub fn new(spawn: S) -> Self {
1161 Self {
1162 procs: Arc::new(Mutex::new(HashMap::new())),
1163 stopping: Arc::new(Mutex::new(HashMap::new())),
1164 spawn,
1165 }
1166 }
1167
1168 async fn discard_proc(&self, proc_id: &ProcAddr, reason: &str) {
1169 let proc = self.procs.lock().await.remove(proc_id);
1170 if let Some(mut proc) = proc
1171 && let Err(error) = proc.destroy_and_wait(Duration::ZERO, reason).await
1172 {
1173 tracing::warn!(%proc_id, %error, "failed to discard local proc");
1174 }
1175 }
1176
1177 pub async fn request_stop(&self, proc: &ProcAddr, timeout: Duration, reason: &str) {
1184 {
1185 let guard = self.stopping.lock().await;
1186 if guard.contains_key(proc) {
1187 return;
1188 }
1189 }
1190
1191 let mut proc_handle = {
1192 let mut guard = self.procs.lock().await;
1193 match guard.remove(proc) {
1194 Some(p) => p,
1195 None => return,
1196 }
1197 };
1198
1199 let proc_ref: ProcAddr = proc_handle.proc_addr().clone();
1200 let (tx, _) = tokio::sync::watch::channel(LocalProcStatus::Stopping);
1201 self.stopping.lock().await.insert(proc_ref.clone(), tx);
1202
1203 let stopping = Arc::clone(&self.stopping);
1204 let reason = reason.to_string();
1205 tokio::spawn(async move {
1206 if let Err(e) = proc_handle.destroy_and_wait(timeout, &reason).await {
1207 tracing::warn!(error = %e, "request_stop(local): destroy_and_wait failed");
1208 }
1209 if let Some(tx) = stopping.lock().await.get(&proc_ref) {
1210 let _ = tx.send(LocalProcStatus::Stopped);
1211 }
1212 });
1213 }
1214
1215 pub async fn local_proc_status(&self, proc: &ProcAddr) -> Option<LocalProcStatus> {
1220 self.stopping.lock().await.get(proc).map(|tx| *tx.borrow())
1221 }
1222
1223 pub async fn watch(
1228 &self,
1229 proc: &ProcAddr,
1230 ) -> Option<tokio::sync::watch::Receiver<LocalProcStatus>> {
1231 self.stopping
1232 .lock()
1233 .await
1234 .get(proc)
1235 .map(|tx| tx.subscribe())
1236 }
1237}
1238
1239#[async_trait]
1240impl<S> BulkTerminate for LocalProcManager<S>
1241where
1242 S: Send + Sync,
1243{
1244 async fn terminate_all(
1245 &self,
1246 _cx: &impl context::Actor,
1247 timeout: std::time::Duration,
1248 max_in_flight: usize,
1249 reason: &str,
1250 ) -> TerminateSummary {
1251 let procs: Vec<Proc> = {
1254 let mut guard = self.procs.lock().await;
1255 guard.drain().map(|(_, v)| v).collect()
1256 };
1257
1258 let attempted = procs.len();
1259
1260 let results = stream::iter(procs.into_iter().map(|mut p| async move {
1261 match p.destroy_and_wait(timeout, reason).await {
1263 Ok(_) => true,
1264 Err(e) => {
1265 tracing::warn!(error=%e, "terminate_all(local): destroy_and_wait failed");
1266 false
1267 }
1268 }
1269 }))
1270 .buffer_unordered(max_in_flight.max(1))
1271 .collect::<Vec<bool>>()
1272 .await;
1273
1274 let ok = results.into_iter().filter(|b| *b).count();
1275
1276 TerminateSummary {
1277 attempted,
1278 ok,
1279 failed: attempted.saturating_sub(ok),
1280 }
1281 }
1282}
1283
1284#[async_trait::async_trait]
1285impl<S> SingleTerminate for LocalProcManager<S>
1286where
1287 S: Send + Sync,
1288{
1289 async fn terminate_proc(
1290 &self,
1291 _cx: &impl context::Actor,
1292 proc: &ProcAddr,
1293 timeout: std::time::Duration,
1294 reason: &str,
1295 ) -> Result<(Vec<ActorAddr>, Vec<ActorAddr>), anyhow::Error> {
1296 let procs: Option<Proc> = {
1298 let mut guard = self.procs.lock().await;
1299 guard.remove(proc)
1300 };
1301 if let Some(mut p) = procs {
1302 p.destroy_and_wait(timeout, reason).await
1303 } else {
1304 Err(anyhow::anyhow!("proc {} doesn't exist", proc))
1305 }
1306 }
1307}
1308
1309pub struct LocalHandle<A: Actor + Referable> {
1327 proc_id: ProcAddr,
1328 addr: ChannelAddr,
1329 agent_ref: ActorRef<A>,
1330 procs: Arc<Mutex<HashMap<ProcAddr, Proc>>>,
1331}
1332
1333impl<A: Actor + Referable> Clone for LocalHandle<A> {
1335 fn clone(&self) -> Self {
1336 Self {
1337 proc_id: self.proc_id.clone(),
1338 addr: self.addr.clone(),
1339 agent_ref: self.agent_ref.clone(),
1340 procs: Arc::clone(&self.procs),
1341 }
1342 }
1343}
1344
1345#[async_trait]
1346impl<A: Actor + Referable> ProcHandle for LocalHandle<A> {
1347 type Agent = A;
1350 type TerminalStatus = ();
1351
1352 fn proc_addr(&self) -> &ProcAddr {
1353 &self.proc_id
1354 }
1355
1356 fn addr(&self) -> Option<ChannelAddr> {
1357 Some(self.addr.clone())
1358 }
1359
1360 fn agent_ref(&self) -> Option<ActorRef<Self::Agent>> {
1361 Some(self.agent_ref.clone())
1362 }
1363
1364 async fn ready(&self) -> Result<(), ReadyError<Self::TerminalStatus>> {
1367 Ok(())
1368 }
1369 async fn wait(&self) -> Result<Self::TerminalStatus, WaitError> {
1373 Ok(())
1374 }
1375
1376 async fn terminate(
1377 &self,
1378 _cx: &impl context::Actor,
1379 timeout: Duration,
1380 reason: &str,
1381 ) -> Result<(), TerminateError<Self::TerminalStatus>> {
1382 let mut proc = {
1383 let guard = self.procs.lock().await;
1384 match guard.get(self.proc_addr()) {
1385 Some(p) => p.clone(),
1386 None => {
1387 return Err(TerminateError::AlreadyTerminated(()));
1390 }
1391 }
1392 };
1393
1394 let _ = proc
1397 .destroy_and_wait(timeout, reason)
1398 .await
1399 .map_err(TerminateError::Io)?;
1400
1401 Ok(())
1402 }
1403
1404 async fn kill(&self) -> Result<(), TerminateError<Self::TerminalStatus>> {
1405 let mut proc = {
1408 let guard = self.procs.lock().await;
1409 match guard.get(self.proc_addr()) {
1410 Some(p) => p.clone(),
1411 None => return Err(TerminateError::AlreadyTerminated(())),
1412 }
1413 };
1414
1415 let _ = proc
1416 .destroy_and_wait(Duration::from_millis(0), "kill")
1417 .await
1418 .map_err(TerminateError::Io)?;
1419
1420 Ok(())
1421 }
1422}
1423
1424#[async_trait]
1439impl<A, S, F> ProcManager for LocalProcManager<S>
1440where
1441 A: Actor + Referable + Binds<A>,
1442 F: Future<Output = anyhow::Result<ActorHandle<A>>> + Send,
1443 S: Fn(Proc) -> F + Sync,
1444{
1445 type Handle = LocalHandle<A>;
1446
1447 fn transport(&self) -> ChannelTransport {
1448 ChannelTransport::Local
1449 }
1450
1451 #[hyperactor::instrument(fields(proc_id=proc_id.to_string(), addr=forwarder_addr.to_string()))]
1452 async fn spawn(
1453 &self,
1454 proc_id: ProcAddr,
1455 forwarder_addr: ChannelAddr,
1456 _config: (),
1457 ) -> Result<Self::Handle, HostError> {
1458 let transport = forwarder_addr.transport();
1459 let proc = Proc::configured(
1460 proc_id.clone(),
1461 MailboxClient::dial(forwarder_addr)?.into_boxed(),
1462 );
1463 let (proc_addr, rx) = channel::serve(ChannelAddr::any(transport))?;
1464 self.procs
1465 .lock()
1466 .await
1467 .insert(proc_id.clone(), proc.clone());
1468 let _handle = proc.clone().serve(rx);
1469 let agent_handle = match AssertUnwindSafe((self.spawn)(proc)).catch_unwind().await {
1470 Ok(Ok(agent_handle)) => agent_handle,
1471 Ok(Err(error)) => {
1472 self.discard_proc(&proc_id, "proc agent failed to spawn")
1473 .await;
1474 return Err(HostError::AgentSpawnFailure(proc_id, error));
1475 }
1476 Err(panic) => {
1477 self.discard_proc(&proc_id, "proc agent spawn panicked")
1478 .await;
1479 std::panic::resume_unwind(panic);
1480 }
1481 };
1482
1483 Ok(LocalHandle {
1484 proc_id,
1485 addr: proc_addr,
1486 agent_ref: agent_handle.bind(),
1487 procs: Arc::clone(&self.procs),
1488 })
1489 }
1490}
1491
1492pub struct ProcessProcManager<A> {
1515 program: std::path::PathBuf,
1516 children: Arc<Mutex<HashMap<ProcAddr, Child>>>,
1517 _phantom: PhantomData<A>,
1518}
1519
1520struct ProcessChildCleanup {
1521 children: Arc<Mutex<HashMap<ProcAddr, Child>>>,
1522 proc_id: Option<ProcAddr>,
1523}
1524
1525impl ProcessChildCleanup {
1526 fn disarm(mut self) {
1527 self.proc_id = None;
1528 }
1529}
1530
1531impl Drop for ProcessChildCleanup {
1532 fn drop(&mut self) {
1533 let Some(proc_id) = self.proc_id.take() else {
1534 return;
1535 };
1536 let children = Arc::clone(&self.children);
1537
1538 tokio::spawn(async move {
1539 if let Some(mut child) = children.lock().await.remove(&proc_id) {
1540 if let Err(error) = child.kill().await {
1541 tracing::warn!(%proc_id, %error, "failed to kill interrupted process spawn");
1542 }
1543
1544 if let Err(error) = child.wait().await {
1545 tracing::warn!(%proc_id, %error, "failed to reap interrupted process spawn");
1546 }
1547 }
1548 });
1549 }
1550}
1551
1552impl<A> ProcessProcManager<A> {
1553 pub fn new(program: std::path::PathBuf) -> Self {
1556 Self {
1557 program,
1558 children: Arc::new(Mutex::new(HashMap::new())),
1559 _phantom: PhantomData,
1560 }
1561 }
1562}
1563
1564impl<A> Drop for ProcessProcManager<A> {
1565 fn drop(&mut self) {
1566 }
1570}
1571
1572#[derive(Debug)]
1594pub struct ProcessHandle<A: Actor + Referable> {
1595 proc_id: ProcAddr,
1596 addr: ChannelAddr,
1597 agent_ref: ActorRef<A>,
1598 children: Arc<Mutex<HashMap<ProcAddr, Child>>>,
1599}
1600
1601impl<A: Actor + Referable> Clone for ProcessHandle<A> {
1603 fn clone(&self) -> Self {
1604 Self {
1605 proc_id: self.proc_id.clone(),
1606 addr: self.addr.clone(),
1607 agent_ref: self.agent_ref.clone(),
1608 children: Arc::clone(&self.children),
1609 }
1610 }
1611}
1612
1613#[async_trait]
1614impl<A: Actor + Referable> ProcHandle for ProcessHandle<A> {
1615 type Agent = A;
1618 type TerminalStatus = ();
1619
1620 fn proc_addr(&self) -> &ProcAddr {
1621 &self.proc_id
1622 }
1623
1624 fn addr(&self) -> Option<ChannelAddr> {
1625 Some(self.addr.clone())
1626 }
1627
1628 fn agent_ref(&self) -> Option<ActorRef<Self::Agent>> {
1629 Some(self.agent_ref.clone())
1630 }
1631
1632 async fn ready(&self) -> Result<(), ReadyError<Self::TerminalStatus>> {
1636 Ok(())
1637 }
1638 async fn wait(&self) -> Result<Self::TerminalStatus, WaitError> {
1641 Ok(())
1642 }
1643
1644 async fn terminate(
1645 &self,
1646 _cx: &impl context::Actor,
1647 _deadline: Duration,
1648 _reason: &str,
1649 ) -> Result<(), TerminateError<Self::TerminalStatus>> {
1650 self.kill().await
1651 }
1652
1653 async fn kill(&self) -> Result<(), TerminateError<Self::TerminalStatus>> {
1654 let Some(mut child) = self.children.lock().await.remove(&self.proc_id) else {
1655 return Err(TerminateError::AlreadyTerminated(()));
1656 };
1657 child
1658 .kill()
1659 .await
1660 .map_err(|error| TerminateError::Io(error.into()))?;
1661 child
1662 .wait()
1663 .await
1664 .map_err(|error| TerminateError::Io(error.into()))?;
1665 Ok(())
1666 }
1667}
1668
1669#[async_trait]
1670impl<A> ProcManager for ProcessProcManager<A>
1671where
1672 A: Actor + Referable + Sync,
1675{
1676 type Handle = ProcessHandle<A>;
1677
1678 fn transport(&self) -> ChannelTransport {
1679 ChannelTransport::Unix
1680 }
1681
1682 #[hyperactor::instrument(fields(proc_id=proc_id.to_string(), addr=forwarder_addr.to_string()))]
1683 async fn spawn(
1684 &self,
1685 proc_id: ProcAddr,
1686 forwarder_addr: ChannelAddr,
1687 _config: (),
1688 ) -> Result<Self::Handle, HostError> {
1689 let (callback_addr, mut callback_rx) =
1690 channel::serve(ChannelAddr::any(ChannelTransport::Unix))?;
1691
1692 let mut cmd = Command::new(&self.program);
1693 cmd.env("HYPERACTOR_HOST_PROC_ID", proc_id.to_string());
1694 cmd.env("HYPERACTOR_HOST_BACKEND_ADDR", forwarder_addr.to_string());
1695 cmd.env("HYPERACTOR_HOST_CALLBACK_ADDR", callback_addr.to_string());
1696
1697 cmd.kill_on_drop(true);
1708
1709 let child = cmd.spawn().map_err(|e| {
1710 HostError::ProcessSpawnFailure(proc_id.clone(), self.program.display().to_string(), e)
1711 })?;
1712
1713 {
1716 let mut children = self.children.lock().await;
1717 children.insert(proc_id.clone(), child);
1718 }
1719 let cleanup = ProcessChildCleanup {
1720 children: Arc::clone(&self.children),
1721 proc_id: Some(proc_id.clone()),
1722 };
1723
1724 let (proc_addr, agent_ref) = callback_rx.recv().await?;
1726
1727 let handle = ProcessHandle {
1736 proc_id,
1737 addr: proc_addr,
1738 agent_ref,
1739 children: Arc::clone(&self.children),
1740 };
1741 cleanup.disarm();
1742 Ok(handle)
1743 }
1744}
1745
1746impl<A> ProcessProcManager<A>
1747where
1748 A: Actor + Referable + Binds<A>,
1751{
1752 pub async fn boot_proc<S, F>(spawn: S) -> Result<Proc, HostError>
1757 where
1758 S: FnOnce(Proc) -> F,
1759 F: Future<Output = Result<ActorHandle<A>, anyhow::Error>>,
1760 {
1761 let proc_id: ProcAddr = Self::parse_env("HYPERACTOR_HOST_PROC_ID")?;
1762 let backend_addr: ChannelAddr = Self::parse_env("HYPERACTOR_HOST_BACKEND_ADDR")?;
1763 let callback_addr: ChannelAddr = Self::parse_env("HYPERACTOR_HOST_CALLBACK_ADDR")?;
1764 spawn_proc(proc_id, backend_addr, callback_addr, spawn).await
1765 }
1766
1767 fn parse_env<T, E>(key: &str) -> Result<T, HostError>
1768 where
1769 T: FromStr<Err = E>,
1770 E: Into<anyhow::Error>,
1771 {
1772 std::env::var(key)
1773 .map_err(|e| HostError::MissingParameter(key.to_string(), e))?
1774 .parse()
1775 .map_err(|e: E| HostError::InvalidParameter(key.to_string(), e.into()))
1776 }
1777}
1778
1779#[hyperactor::instrument(fields(proc_id=proc_id.to_string(), addr=backend_addr.to_string(), callback_addr=callback_addr.to_string()))]
1784pub async fn spawn_proc<A, S, F>(
1785 proc_id: ProcAddr,
1786 backend_addr: ChannelAddr,
1787 callback_addr: ChannelAddr,
1788 spawn: S,
1789) -> Result<Proc, HostError>
1790where
1791 A: Actor + Referable + Binds<A>,
1794 S: FnOnce(Proc) -> F,
1795 F: Future<Output = Result<ActorHandle<A>, anyhow::Error>>,
1796{
1797 let backend_transport = backend_addr.transport();
1798 let proc = Proc::configured(
1799 proc_id.clone(),
1800 MailboxClient::dial(backend_addr)?.into_boxed(),
1801 );
1802
1803 let agent_handle = spawn(proc.clone())
1804 .await
1805 .map_err(|e| HostError::AgentSpawnFailure(proc_id.clone(), e))?;
1806
1807 let (proc_addr, proc_rx) = channel::serve(ChannelAddr::any(backend_transport))?;
1810 proc.clone().serve(proc_rx);
1811 let agent_ref: ActorRef<A> = agent_handle.bind::<A>();
1812 channel::dial::<(ChannelAddr, ActorRef<A>)>(callback_addr)?
1813 .send((proc_addr, agent_ref))
1814 .await
1815 .map_err(ChannelError::from)?;
1816
1817 Ok(proc)
1818}
1819
1820pub mod testing {
1823 use async_trait::async_trait;
1824 use hyperactor::Actor;
1825 use hyperactor::ActorAddr;
1826 use hyperactor::Context;
1827 use hyperactor::Endpoint as _;
1828 use hyperactor::Handler;
1829 use hyperactor::OncePortRef;
1830 #[derive(Debug, Default)]
1833 #[hyperactor::export(handlers = [OncePortRef<ActorAddr>])]
1834 pub struct EchoActor;
1835
1836 impl Actor for EchoActor {}
1837
1838 #[async_trait]
1839 impl Handler<OncePortRef<ActorAddr>> for EchoActor {
1840 async fn handle(
1841 &mut self,
1842 cx: &Context<Self>,
1843 reply: OncePortRef<ActorAddr>,
1844 ) -> Result<(), anyhow::Error> {
1845 reply.post(cx, cx.self_addr().clone());
1846 Ok(())
1847 }
1848 }
1849}
1850
1851#[cfg(test)]
1852mod tests {
1853 use std::os::unix::fs::PermissionsExt;
1854 use std::sync::Arc;
1855 use std::time::Duration;
1856
1857 use hyperactor::Addr;
1858 use hyperactor::Endpoint as _;
1859 use hyperactor::Label;
1860 use hyperactor::Location;
1861 use hyperactor::OncePortRef;
1862 use hyperactor::Uid;
1863 use hyperactor::channel::ChannelTransport;
1864 use hyperactor::channel::Tx;
1865 use hyperactor::channel::TxStatus;
1866 use hyperactor::context::Mailbox;
1867 use hyperactor::mailbox::DialMailboxRouter;
1868 use hyperactor::mailbox::MessageEnvelope;
1869 use hyperactor::port::Port;
1870 #[cfg(fbcode_build)]
1871 use hyperactor_config::attrs::Attrs;
1872 use timed_test::async_timed_test;
1873
1874 use super::testing::EchoActor;
1875 use super::*;
1876 #[cfg(fbcode_build)]
1877 use crate::bootstrap::BootstrapCommand;
1878 #[cfg(fbcode_build)]
1879 use crate::bootstrap::BootstrapProcConfig;
1880 #[cfg(fbcode_build)]
1881 use crate::bootstrap::BootstrapProcManager;
1882 #[cfg(fbcode_build)]
1883 use crate::config_dump::ConfigDump;
1884 #[cfg(fbcode_build)]
1885 use crate::config_dump::ConfigDumpResult;
1886
1887 #[tokio::test]
1888 async fn test_basic() {
1889 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
1890 Ok(proc.spawn_with_label::<()>("host_agent", ()))
1891 });
1892 let procs = Arc::clone(&proc_manager.procs);
1893 let host = Host::new(proc_manager, ChannelAddr::any(ChannelTransport::Unix))
1894 .await
1895 .unwrap();
1896
1897 let (proc_id1, _ref) = host.spawn("proc1".to_string(), ()).await.unwrap();
1898 assert_eq!(proc_id1.id(), &ResourceId::from_name("proc1").proc_id());
1902 assert_eq!(proc_id1.location().addr(), host.addr());
1903 let (via_uid, _) = proc_id1
1904 .location()
1905 .as_via()
1906 .expect("spawned proc_addr must carry a via prefix");
1907 assert_eq!(via_uid, proc_id1.id().uid());
1908 assert!(procs.lock().await.contains_key(&proc_id1));
1909
1910 let (proc_id2, _ref) = host.spawn("proc2".to_string(), ()).await.unwrap();
1911 assert!(procs.lock().await.contains_key(&proc_id2));
1912
1913 let proc1 = procs.lock().await.get(&proc_id1).unwrap().clone();
1914 let proc2 = procs.lock().await.get(&proc_id2).unwrap().clone();
1915
1916 let instance1 = proc1.client("client");
1918 let instance2 = proc2.client("client");
1919
1920 let (port, mut rx) = instance1.mailbox().open_port();
1921
1922 port.bind().post(&instance2, "hello".to_string());
1923 assert_eq!(rx.recv().await.unwrap(), "hello".to_string());
1924
1925 let system_actor = host.system_proc().client("test");
1927
1928 port.bind()
1930 .post(&system_actor, "hello from the system proc".to_string());
1931 assert_eq!(
1932 rx.recv().await.unwrap(),
1933 "hello from the system proc".to_string()
1934 );
1935
1936 let (port, mut rx) = system_actor.mailbox().open_port();
1938 port.bind()
1939 .post(&system_actor, "hello from the system".to_string());
1940 assert_eq!(
1941 rx.recv().await.unwrap(),
1942 "hello from the system".to_string()
1943 );
1944
1945 port.bind()
1947 .post(&instance1, "hello from the instance1".to_string());
1948 assert_eq!(
1949 rx.recv().await.unwrap(),
1950 "hello from the instance1".to_string()
1951 );
1952 }
1953
1954 #[cfg(fbcode_build)]
1955 async fn assert_bootstrap_child_round_trip(service_proc_id: ProcId) {
1956 let manager = BootstrapProcManager::new(BootstrapCommand::test()).unwrap();
1957 let mut host = Host::new_with_gateway(
1958 manager,
1959 ProcAddr::new(
1960 service_proc_id,
1961 ChannelAddr::any(ChannelTransport::Unix).into(),
1962 ),
1963 None,
1964 Gateway::new(),
1965 None,
1966 )
1967 .await
1968 .unwrap();
1969
1970 let child_name = ResourceId::instance(Label::strip("route-child")).to_string();
1971 let (_child_proc, child_agent) = host
1972 .spawn(
1973 child_name,
1974 BootstrapProcConfig {
1975 create_rank: 0,
1976 client_config_override: Attrs::new(),
1977 proc_bind: None,
1978 bootstrap_command: None,
1979 },
1980 )
1981 .await
1982 .unwrap();
1983
1984 let service_client = host.system_proc().client("route-test");
1985 let (reply_handle, reply_rx) = service_client
1986 .mailbox()
1987 .open_once_port::<ConfigDumpResult>();
1988 child_agent.post(
1989 &service_client,
1990 ConfigDump {
1991 result: reply_handle.bind(),
1992 },
1993 );
1994 tokio::time::timeout(Duration::from_secs(5), reply_rx.recv())
1995 .await
1996 .expect("service-to-child request and child-to-service reply timed out")
1997 .expect("config dump reply channel closed");
1998
1999 let summary = host
2000 .terminate_children(
2001 &service_client,
2002 Duration::from_secs(2),
2003 1,
2004 "routing test complete",
2005 )
2006 .await;
2007 assert_eq!(summary.failed, 0);
2008 host.shutdown_servers().await;
2009 }
2010
2011 #[tokio::test]
2012 #[cfg(fbcode_build)]
2013 async fn test_bootstrap_child_round_trip_with_legacy_service_proc() {
2014 assert_bootstrap_child_round_trip(legacy_service_proc_id()).await;
2015 }
2016
2017 #[tokio::test]
2018 #[cfg(fbcode_build)]
2019 async fn test_bootstrap_child_round_trip_with_instance_service_proc() {
2020 let service_proc_id = ProcId::new(
2021 Uid::Instance(0x5e12_71ce, Some(Label::strip(SERVICE_PROC_NAME))),
2022 None,
2023 );
2024 assert_bootstrap_child_round_trip(service_proc_id).await;
2025 }
2026
2027 #[tokio::test]
2028 #[cfg_attr(not(fbcode_build), ignore)]
2030 async fn test_process_proc_manager() {
2031 hyperactor_telemetry::initialize_logging(hyperactor_telemetry::DefaultTelemetryClock {});
2032
2033 let process_manager = ProcessProcManager::<EchoActor>::new(
2035 buck_resources::get("monarch/hyperactor_mesh/host_bootstrap").unwrap(),
2036 );
2037 let host = Host::new(process_manager, ChannelAddr::any(ChannelTransport::Unix))
2038 .await
2039 .unwrap();
2040
2041 assert!(matches!(host.addr().transport(), ChannelTransport::Unix));
2043 let (proc1, echo1) = host.spawn("proc1".to_string(), ()).await.unwrap();
2044 let (proc2, echo2) = host.spawn("proc2".to_string(), ()).await.unwrap();
2045 assert_eq!(echo1.actor_addr().proc_addr(), proc1);
2046 assert_eq!(echo2.actor_addr().proc_addr(), proc2);
2047
2048 let dup = host.spawn("proc1".to_string(), ()).await;
2050 assert!(matches!(dup, Err(HostError::ProcExists(_))));
2051
2052 let client = Proc::direct(
2059 ChannelAddr::any(host.addr().transport()),
2060 "test".to_string(),
2061 )
2062 .unwrap();
2063 let client_inst = client.client("test");
2064 let (port, rx) = client_inst.mailbox().open_once_port();
2065 echo1.post(&client_inst, port.bind());
2066 let id = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2067 .await
2068 .unwrap()
2069 .unwrap();
2070 assert_eq!(id, *echo1.actor_addr());
2071
2072 let (port2, rx2) = client_inst.mailbox().open_once_port();
2080 echo2.post(&client_inst, port2.bind());
2081 let id2 = tokio::time::timeout(Duration::from_secs(5), rx2.recv())
2082 .await
2083 .unwrap()
2084 .unwrap();
2085 assert_eq!(id2, *echo2.actor_addr());
2086
2087 let sys_inst = host.system_proc().client("sys-client");
2096 let (port3, rx3) = client_inst.mailbox().open_once_port();
2097 echo1.post(&sys_inst, port3.bind());
2100 let id3 = tokio::time::timeout(Duration::from_secs(5), rx3.recv())
2101 .await
2102 .unwrap()
2103 .unwrap();
2104 assert_eq!(id3, *echo1.actor_addr());
2105 }
2106
2107 #[async_timed_test(timeout_secs = 30)]
2108 async fn cancelled_process_startup_kills_reaps_and_unregisters_child() {
2109 fn process_exists(pid: u32) -> bool {
2110 (
2111 unsafe { libc::kill(pid as libc::pid_t, 0) }
2113 ) == 0
2114 }
2115
2116 let program_dir = tempfile::tempdir().expect("test directory should be created");
2117 let program = {
2118 let program = program_dir.path().join("pending_process.sh");
2119
2120 tokio::fs::write(&program, "#!/bin/sh\nexec /bin/sleep 600\n")
2121 .await
2122 .expect("test program should be written");
2123
2124 let mut permissions = tokio::fs::metadata(&program)
2125 .await
2126 .expect("test program metadata should be available")
2127 .permissions();
2128
2129 permissions.set_mode(0o700);
2130
2131 tokio::fs::set_permissions(&program, permissions)
2132 .await
2133 .expect("test program should be executable");
2134
2135 program
2136 };
2137
2138 let manager = Arc::new(ProcessProcManager::<EchoActor>::new(program));
2139 let proc_id = ResourceId::proc_addr_from_name(
2140 ChannelAddr::any(ChannelTransport::Unix),
2141 "cancelled-process",
2142 );
2143 let startup = tokio::spawn({
2144 let manager = Arc::clone(&manager);
2145 let proc_id = proc_id.clone();
2146
2147 async move {
2148 manager
2149 .spawn(proc_id, ChannelAddr::any(ChannelTransport::Unix), ())
2150 .await
2151 }
2152 });
2153
2154 let pid = loop {
2155 {
2156 let children = manager.children.lock().await;
2157
2158 if let Some(child) = children.get(&proc_id) {
2159 break child.id().expect("test child should have a process ID");
2160 }
2161 }
2162 tokio::task::yield_now().await;
2163 };
2164 assert!(process_exists(pid));
2165
2166 startup.abort();
2167
2168 assert!(
2169 startup
2170 .await
2171 .expect_err("startup task should be cancelled")
2172 .is_cancelled(),
2173 );
2174
2175 loop {
2176 if !process_exists(pid) {
2177 assert_eq!(
2178 std::io::Error::last_os_error().raw_os_error(),
2179 Some(libc::ESRCH),
2180 "cancelled child should be killed and reaped",
2181 );
2182 break;
2183 }
2184 tokio::task::yield_now().await;
2185 }
2186 assert!(
2187 !manager.children.lock().await.contains_key(&proc_id),
2188 "cancelled child should be removed from the child registry",
2189 );
2190 }
2191
2192 #[tokio::test]
2193 async fn local_ready_and_wait_are_immediate() {
2194 let addr = ChannelAddr::any(ChannelTransport::Local);
2196 let proc_ref = ResourceId::proc_addr_from_name(addr.clone(), "p");
2197 let actor_ref = proc_ref.actor_addr("host_agent");
2198 let agent_ref = ActorRef::<()>::attest(actor_ref);
2199 let h = LocalHandle::<()> {
2200 proc_id: proc_ref,
2201 addr,
2202 agent_ref,
2203 procs: Arc::new(Mutex::new(HashMap::new())),
2204 };
2205
2206 assert!(h.ready().await.is_ok());
2208
2209 assert!(h.wait().await.is_ok());
2211
2212 let (r1, r2) = tokio::join!(h.ready(), h.ready());
2214 assert!(r1.is_ok() && r2.is_ok());
2215 }
2216
2217 #[derive(Debug, Clone, Copy)]
2221 enum ReadyMode {
2222 OkAfter(Duration),
2223 Pending,
2224 ErrTerminal,
2225 ErrChannelClosed,
2226 }
2227
2228 #[derive(Debug, Clone)]
2229 struct TestHandle {
2230 id: ProcAddr,
2231 addr: ChannelAddr,
2232 agent: ActorRef<()>,
2233 mode: ReadyMode,
2234 omit_addr: bool,
2235 omit_agent: bool,
2236 kill_notify: Option<Arc<tokio::sync::Notify>>,
2237 kill_release: Option<Arc<tokio::sync::Notify>>,
2238 }
2239
2240 #[async_trait::async_trait]
2241 impl ProcHandle for TestHandle {
2242 type Agent = ();
2243 type TerminalStatus = ();
2244
2245 fn proc_addr(&self) -> &ProcAddr {
2246 &self.id
2247 }
2248
2249 fn addr(&self) -> Option<ChannelAddr> {
2250 if self.omit_addr {
2251 None
2252 } else {
2253 Some(self.addr.clone())
2254 }
2255 }
2256
2257 fn agent_ref(&self) -> Option<ActorRef<Self::Agent>> {
2258 if self.omit_agent {
2259 None
2260 } else {
2261 Some(self.agent.clone())
2262 }
2263 }
2264
2265 async fn ready(&self) -> Result<(), ReadyError<Self::TerminalStatus>> {
2266 match self.mode {
2267 ReadyMode::OkAfter(d) => {
2268 if !d.is_zero() {
2269 tokio::time::sleep(d).await;
2270 }
2271 Ok(())
2272 }
2273 ReadyMode::Pending => std::future::pending().await,
2274 ReadyMode::ErrTerminal => Err(ReadyError::Terminal(())),
2275 ReadyMode::ErrChannelClosed => Err(ReadyError::ChannelClosed),
2276 }
2277 }
2278 async fn wait(&self) -> Result<Self::TerminalStatus, WaitError> {
2279 Ok(())
2280 }
2281 async fn terminate(
2282 &self,
2283 _cx: &impl context::Actor,
2284 _timeout: Duration,
2285 _reason: &str,
2286 ) -> Result<Self::TerminalStatus, TerminateError<Self::TerminalStatus>> {
2287 Err(TerminateError::Unsupported)
2288 }
2289 async fn kill(&self) -> Result<Self::TerminalStatus, TerminateError<Self::TerminalStatus>> {
2290 if let Some(notify) = &self.kill_notify {
2291 notify.notify_one();
2292 }
2293 if let Some(release) = &self.kill_release {
2294 release.notified().await;
2295 }
2296 Ok(())
2297 }
2298 }
2299
2300 #[derive(Debug, Clone)]
2301 struct TestManager {
2302 mode: ReadyMode,
2303 omit_addr: bool,
2304 omit_agent: bool,
2305 transport: ChannelTransport,
2306 kill_notify: Option<Arc<tokio::sync::Notify>>,
2307 kill_release: Option<Arc<tokio::sync::Notify>>,
2308 }
2309
2310 impl TestManager {
2311 fn local(mode: ReadyMode) -> Self {
2312 Self {
2313 mode,
2314 omit_addr: false,
2315 omit_agent: false,
2316 transport: ChannelTransport::Local,
2317 kill_notify: None,
2318 kill_release: None,
2319 }
2320 }
2321 fn with_omissions(mut self, addr: bool, agent: bool) -> Self {
2322 self.omit_addr = addr;
2323 self.omit_agent = agent;
2324 self
2325 }
2326 fn with_kill_notify(mut self, notify: Arc<tokio::sync::Notify>) -> Self {
2327 self.kill_notify = Some(notify);
2328 self
2329 }
2330 fn with_kill_release(mut self, release: Arc<tokio::sync::Notify>) -> Self {
2331 self.kill_release = Some(release);
2332 self
2333 }
2334 }
2335
2336 #[async_trait::async_trait]
2337 impl ProcManager for TestManager {
2338 type Handle = TestHandle;
2339
2340 fn transport(&self) -> ChannelTransport {
2341 self.transport.clone()
2342 }
2343
2344 async fn spawn(
2345 &self,
2346 proc_id: ProcAddr,
2347 forwarder_addr: ChannelAddr,
2348 _config: (),
2349 ) -> Result<Self::Handle, HostError> {
2350 let agent = ActorRef::<()>::attest(proc_id.actor_addr("host_agent"));
2351 Ok(TestHandle {
2352 id: proc_id,
2353 addr: forwarder_addr,
2354 agent,
2355 mode: self.mode,
2356 omit_addr: self.omit_addr,
2357 omit_agent: self.omit_agent,
2358 kill_notify: self.kill_notify.clone(),
2359 kill_release: self.kill_release.clone(),
2360 })
2361 }
2362 }
2363
2364 #[tokio::test]
2365 async fn host_spawn_cleans_up_handle_after_readiness_failure() {
2366 let cleanup_started = Arc::new(tokio::sync::Notify::new());
2367 let cleanup_release = Arc::new(tokio::sync::Notify::new());
2368 let host = Host::new(
2369 TestManager::local(ReadyMode::ErrTerminal)
2370 .with_kill_notify(Arc::clone(&cleanup_started))
2371 .with_kill_release(Arc::clone(&cleanup_release)),
2372 ChannelAddr::any(ChannelTransport::Local),
2373 )
2374 .await
2375 .expect("host should start");
2376
2377 let spawn = tokio::spawn(host.spawn("failed-proc".to_string(), ()));
2378 cleanup_started.notified().await;
2379 assert!(
2380 !spawn.is_finished(),
2381 "spawn returned before failed-proc cleanup completed",
2382 );
2383 cleanup_release.notify_one();
2384
2385 let error = spawn
2386 .await
2387 .expect("spawn task should complete")
2388 .expect_err("readiness failure should fail the spawn");
2389 assert!(matches!(
2390 error,
2391 HostError::ProcessConfigurationFailure(_, _)
2392 ));
2393 assert!(
2394 !host
2395 .procs
2396 .lock()
2397 .expect("procs mutex poisoned")
2398 .contains_key("failed-proc"),
2399 "failed spawn should not leave a peer registration",
2400 );
2401 }
2402
2403 #[tokio::test]
2404 async fn local_manager_cleans_up_proc_when_agent_spawn_panics() {
2405 async fn panicking_spawn(_proc: Proc) -> anyhow::Result<ActorHandle<()>> {
2406 panic!("test agent spawn panic");
2407 }
2408
2409 let manager = LocalProcManager::new(panicking_spawn);
2410 let managed_procs = Arc::clone(&manager.procs);
2411 let host = Host::new(manager, ChannelAddr::any(ChannelTransport::Local))
2412 .await
2413 .expect("host should start");
2414
2415 let result = AssertUnwindSafe(host.spawn("panicking-proc".to_string(), ()))
2416 .catch_unwind()
2417 .await;
2418 assert!(result.is_err(), "agent spawn panic should be preserved");
2419 assert!(
2420 managed_procs.lock().await.is_empty(),
2421 "panicking agent spawn should not leave a managed proc",
2422 );
2423 assert!(
2424 !host
2425 .procs
2426 .lock()
2427 .expect("procs mutex poisoned")
2428 .contains_key("panicking-proc"),
2429 "panicking agent spawn should release its name reservation",
2430 );
2431 }
2432
2433 #[tokio::test]
2434 async fn host_spawn_times_out_when_configured() {
2435 let cfg = hyperactor_config::global::lock();
2436 let _g = cfg.override_key(
2437 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2438 Duration::from_millis(10),
2439 );
2440
2441 let host = Host::new(
2442 TestManager::local(ReadyMode::Pending),
2443 ChannelAddr::any(ChannelTransport::Local),
2444 )
2445 .await
2446 .unwrap();
2447
2448 let err = host.spawn("t".into(), ()).await.expect_err("must time out");
2449 assert!(matches!(err, HostError::ProcessConfigurationFailure(_, _)));
2450 }
2451
2452 #[tokio::test]
2453 async fn host_spawn_timeout_zero_disables_and_succeeds() {
2454 let cfg = hyperactor_config::global::lock();
2455 let _g = cfg.override_key(
2456 hyperactor::config::HOST_SPAWN_READY_TIMEOUT,
2457 Duration::from_secs(0),
2458 );
2459
2460 let host = Host::new(
2461 TestManager::local(ReadyMode::OkAfter(Duration::from_millis(20))),
2462 ChannelAddr::any(ChannelTransport::Local),
2463 )
2464 .await
2465 .unwrap();
2466
2467 let (pid, agent) = host.spawn("ok".into(), ()).await.expect("must succeed");
2468 assert_eq!(agent.actor_addr().proc_addr(), pid);
2469 assert!(
2470 host.procs
2471 .lock()
2472 .expect("procs mutex poisoned")
2473 .contains_key("ok")
2474 );
2475 }
2476
2477 #[tokio::test]
2478 async fn host_spawn_maps_channel_closed_ready_error_to_config_failure() {
2479 let host = Host::new(
2480 TestManager::local(ReadyMode::ErrChannelClosed),
2481 ChannelAddr::any(ChannelTransport::Local),
2482 )
2483 .await
2484 .unwrap();
2485
2486 let err = host.spawn("p".into(), ()).await.expect_err("must fail");
2487 assert!(matches!(err, HostError::ProcessConfigurationFailure(_, _)));
2488 }
2489
2490 #[tokio::test]
2491 async fn host_spawn_maps_terminal_ready_error_to_config_failure() {
2492 let host = Host::new(
2493 TestManager::local(ReadyMode::ErrTerminal),
2494 ChannelAddr::any(ChannelTransport::Local),
2495 )
2496 .await
2497 .unwrap();
2498
2499 let err = host.spawn("p".into(), ()).await.expect_err("must fail");
2500 assert!(matches!(err, HostError::ProcessConfigurationFailure(_, _)));
2501 }
2502
2503 #[tokio::test]
2504 async fn host_spawn_fails_if_ready_but_missing_addr() {
2505 let host = Host::new(
2506 TestManager::local(ReadyMode::OkAfter(Duration::ZERO)).with_omissions(true, false),
2507 ChannelAddr::any(ChannelTransport::Local),
2508 )
2509 .await
2510 .unwrap();
2511
2512 let err = host
2513 .spawn("no-addr".into(), ())
2514 .await
2515 .expect_err("must fail");
2516 assert!(matches!(err, HostError::ProcessConfigurationFailure(_, _)));
2517 }
2518
2519 #[tokio::test]
2520 async fn host_spawn_fails_if_ready_but_missing_agent() {
2521 let host = Host::new(
2522 TestManager::local(ReadyMode::OkAfter(Duration::ZERO)).with_omissions(false, true),
2523 ChannelAddr::any(ChannelTransport::Local),
2524 )
2525 .await
2526 .unwrap();
2527
2528 let err = host
2529 .spawn("no-agent".into(), ())
2530 .await
2531 .expect_err("must fail");
2532 assert!(matches!(err, HostError::ProcessConfigurationFailure(_, _)));
2533 }
2534
2535 #[tokio::test]
2555 async fn test_simplex_peer_sees_clean_close_on_host_shutdown() {
2556 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2557 Ok(proc.spawn_with_label::<EchoActor>("host_agent", EchoActor))
2558 });
2559 let mut host =
2560 Host::new_with_default(proc_manager, ChannelAddr::any(ChannelTransport::Unix), None)
2561 .await
2562 .unwrap();
2563 let mut serve_handle = host.take_frontend_handle().unwrap();
2564
2565 let echo_handle = host.system_proc().spawn(EchoActor);
2567 let echo_ref = echo_handle.bind::<EchoActor>();
2568
2569 let dial_router = DialMailboxRouter::new();
2570 dial_router.bind(
2571 Addr::from(host.system_proc().proc_addr().clone()),
2572 host.addr().clone(),
2573 );
2574 let client_addr = ChannelAddr::any(ChannelTransport::Unix);
2575 let (client_listen_addr, client_rx) = channel::serve(client_addr).unwrap();
2576 let client_proc_id = ResourceId::proc_addr_from_name(client_listen_addr, "client");
2577 let client_proc = Proc::configured(client_proc_id, dial_router.into_boxed());
2578 let _client_handle = client_proc.clone().serve(client_rx);
2579
2580 let client_inst = client_proc.client("requester");
2581 let (reply_port, reply_handle) = client_inst.mailbox().open_once_port::<ActorAddr>();
2582 let reply_port = reply_port.bind();
2583 echo_ref
2584 .port::<OncePortRef<ActorAddr>>()
2585 .post(&client_inst, reply_port);
2586 let _ = tokio::time::timeout(Duration::from_secs(5), reply_handle.recv())
2587 .await
2588 .expect("baseline round-trip timed out")
2589 .expect("baseline recv failed");
2590
2591 let host_tx = channel::dial::<MessageEnvelope>(host.addr().clone()).unwrap();
2593 let dummy_dest = host
2595 .system_proc()
2596 .proc_addr()
2597 .actor_addr("noop")
2598 .port_addr(Port::from(0u64));
2599 let mut envelope = MessageEnvelope::serialize(
2600 client_inst.self_addr().clone(),
2601 dummy_dest,
2602 &"warmup".to_string(),
2603 Default::default(),
2604 )
2605 .unwrap();
2606 envelope.set_return_undeliverable(false);
2607 host_tx.post(envelope);
2608 tokio::time::sleep(Duration::from_millis(200)).await;
2610 assert!(matches!(*host_tx.status().borrow(), TxStatus::Active));
2611
2612 serve_handle.stop("test shutdown");
2616 let _ = tokio::time::timeout(Duration::from_secs(5), serve_handle.join())
2617 .await
2618 .expect("serve handle did not resolve");
2619
2620 let mut status = host_tx.status().clone();
2624 tokio::time::timeout(Duration::from_secs(10), async {
2625 loop {
2626 if let TxStatus::Closed(_) = *status.borrow() {
2627 return;
2628 }
2629 if status.changed().await.is_err() {
2630 return;
2631 }
2632 }
2633 })
2634 .await
2635 .expect("simplex peer did not see Closed within 10s of host shutdown");
2636
2637 match &*host_tx.status().borrow() {
2638 TxStatus::Closed(_) => {}
2639 other => panic!("expected TxStatus::Closed, got {:?}", other),
2640 }
2641 }
2642
2643 #[tokio::test]
2648 async fn test_simplex_clients_during_host_shutdown() {
2649 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2650 Ok(proc.spawn_with_label::<EchoActor>("host_agent", EchoActor))
2651 });
2652 let mut host =
2653 Host::new_with_default(proc_manager, ChannelAddr::any(ChannelTransport::Unix), None)
2654 .await
2655 .unwrap();
2656 let mut serve_handle = host.take_frontend_handle().unwrap();
2657
2658 let echo_handle = host.system_proc().spawn(EchoActor);
2659 let echo_ref = echo_handle.bind::<EchoActor>();
2660 let host_addr = host.addr().clone();
2661 let echo_actor_id = echo_ref.actor_addr().clone();
2662 let system_proc_id = host.system_proc().proc_addr().clone();
2663
2664 const N_CLIENTS: usize = 4;
2666 const M_REQUESTS: usize = 5;
2667
2668 let mut client_tasks = Vec::new();
2669 for ci in 0..N_CLIENTS {
2670 let host_addr = host_addr.clone();
2671 let echo_actor_id = echo_actor_id.clone();
2672 let system_proc_id = system_proc_id.clone();
2673 client_tasks.push(tokio::spawn(async move {
2674 let dial_router = DialMailboxRouter::new();
2675 dial_router.bind(Addr::from(system_proc_id.clone()), host_addr);
2676 let client_addr = ChannelAddr::any(ChannelTransport::Unix);
2677 let (client_listen_addr, client_rx) = channel::serve(client_addr).unwrap();
2678 let client_proc_id =
2679 ResourceId::proc_addr_from_name(client_listen_addr, format!("client-{}", ci));
2680 let client_proc = Proc::configured(client_proc_id, dial_router.into_boxed());
2681 let _client_handle = client_proc.clone().serve(client_rx);
2682
2683 let echo_ref = ActorRef::<EchoActor>::attest(echo_actor_id);
2684
2685 for ri in 0..M_REQUESTS {
2686 let client_inst = client_proc.client(&format!("req-{}", ri));
2687 let (reply_port, reply_handle) =
2688 client_inst.mailbox().open_once_port::<ActorAddr>();
2689 let reply_port = reply_port.bind();
2690 echo_ref
2691 .port::<OncePortRef<ActorAddr>>()
2692 .post(&client_inst, reply_port);
2693 let received =
2694 tokio::time::timeout(Duration::from_secs(10), reply_handle.recv())
2695 .await
2696 .expect("timeout waiting for reply")
2697 .expect("recv failed");
2698 assert_eq!(received, *echo_ref.actor_addr());
2699 }
2700 }));
2701 }
2702
2703 for task in client_tasks {
2704 task.await.unwrap();
2705 }
2706
2707 serve_handle.stop("test cleanup");
2709 tokio::time::timeout(Duration::from_secs(10), serve_handle.join())
2710 .await
2711 .expect("serve handle did not resolve")
2712 .expect("serve task error");
2713 }
2714
2715 #[tokio::test]
2719 async fn test_simplex_client_to_duplex_host() {
2720 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2721 Ok(proc.spawn_with_label::<EchoActor>("host_agent", EchoActor))
2722 });
2723 let host =
2724 Host::new_with_default(proc_manager, ChannelAddr::any(ChannelTransport::Unix), None)
2725 .await
2726 .unwrap();
2727
2728 let echo_handle = host.system_proc().spawn(EchoActor);
2730 let echo_ref = echo_handle.bind::<EchoActor>();
2731
2732 let client_addr = ChannelAddr::any(ChannelTransport::Unix);
2738 let dial_router = DialMailboxRouter::new();
2739 dial_router.bind(
2740 Addr::from(host.system_proc().proc_addr().clone()),
2741 host.addr().clone(),
2742 );
2743 let (client_listen_addr, client_rx) = channel::serve(client_addr).unwrap();
2744 let client_proc_id = ResourceId::proc_addr_from_name(client_listen_addr, "external-client");
2745 let client_proc = Proc::configured(client_proc_id, dial_router.into_boxed());
2746 let _client_handle = client_proc.clone().serve(client_rx);
2747
2748 let client_inst = client_proc.client("requester");
2749
2750 let (reply_port, reply_handle) = client_inst.mailbox().open_once_port::<ActorAddr>();
2754 let reply_port = reply_port.bind();
2755 echo_ref
2756 .port::<OncePortRef<ActorAddr>>()
2757 .post(&client_inst, reply_port);
2758
2759 let received = tokio::time::timeout(Duration::from_secs(10), reply_handle.recv())
2760 .await
2761 .expect("timed out waiting for reply")
2762 .expect("recv failed");
2763 assert_eq!(received, *echo_ref.actor_addr());
2764 }
2765
2766 #[tokio::test]
2767 async fn test_explicit_service_and_local_proc_ids_are_instances() {
2768 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2769 Ok(proc.spawn_with_label::<()>("host_agent", ()))
2770 });
2771 let service_proc_id = ProcId::instance(hyperactor::id::Label::strip("service"));
2772
2773 let host = Host::new_with_gateway(
2774 proc_manager,
2775 ProcAddr::new(
2776 service_proc_id.clone(),
2777 ChannelAddr::any(ChannelTransport::Unix).into(),
2778 ),
2779 None,
2780 Gateway::new(),
2781 None,
2782 )
2783 .await
2784 .unwrap();
2785
2786 assert_eq!(host.system_proc().proc_id(), &service_proc_id);
2787 assert!(matches!(
2788 host.local_proc().proc_id().uid(),
2789 hyperactor::id::Uid::Instance(..)
2790 ));
2791 assert_eq!(
2792 host.local_proc()
2793 .proc_id()
2794 .label()
2795 .map(|label| label.as_str()),
2796 Some(LOCAL_PROC_NAME)
2797 );
2798 }
2799
2800 #[tokio::test]
2801 async fn test_new_with_gateway_rejects_via_frontend_location() {
2802 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2803 Ok(proc.spawn_with_label::<()>("host_agent", ()))
2804 });
2805 let location = Location::from(ChannelAddr::any(ChannelTransport::Unix))
2806 .with_via(Uid::instance(Label::strip("remote")));
2807 let service_proc_addr = ProcAddr::new(legacy_service_proc_id(), location.clone());
2808
2809 let result =
2810 Host::new_with_gateway(proc_manager, service_proc_addr, None, Gateway::new(), None)
2811 .await;
2812
2813 assert!(matches!(
2814 result,
2815 Err(HostError::InvalidFrontendLocation(error_location))
2816 if error_location == location
2817 ));
2818 }
2819
2820 #[tokio::test]
2821 async fn test_new_with_gateway_remints_service_proc_at_bound_address() {
2822 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2823 Ok(proc.spawn_with_label::<()>("host_agent", ()))
2824 });
2825 let service_proc_id = ProcId::instance(Label::strip("service"));
2826 let startup_addr = ChannelAddr::from_zmq_url("tcp://127.0.0.1:0").unwrap();
2827 let mut host = Host::new_with_gateway(
2828 proc_manager,
2829 ProcAddr::new(service_proc_id.clone(), startup_addr.into()),
2830 None,
2831 Gateway::new(),
2832 None,
2833 )
2834 .await
2835 .unwrap();
2836
2837 let ChannelAddr::Tcp(bound_addr) = host.addr() else {
2838 panic!("expected TCP frontend address")
2839 };
2840 assert_ne!(bound_addr.port(), 0);
2841 assert_eq!(
2842 host.system_proc().proc_addr(),
2843 ProcAddr::new(service_proc_id, Location::from(host.addr().clone()))
2844 );
2845
2846 host.shutdown_servers().await;
2847 }
2848
2849 #[tokio::test]
2850 async fn test_spawn_uses_latest_serve_location_after_prior_default_override() {
2851 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2852 Ok(proc.spawn_with_label::<()>("host_agent", ()))
2853 });
2854 let gateway = Gateway::new();
2855 let attached_host_addr: ChannelAddr = "tcp:10.0.159.108:26600".parse().unwrap();
2856 let attached_uid = Uid::instance(Label::strip("attached"));
2857 let attached_location = Location::from(attached_host_addr).with_via(attached_uid);
2858 gateway.set_default_location(attached_location.clone());
2859
2860 let host = Host::new_with_gateway(
2861 proc_manager,
2862 ProcAddr::new(
2863 legacy_service_proc_id(),
2864 ChannelAddr::any(ChannelTransport::Unix).into(),
2865 ),
2866 None,
2867 gateway,
2868 None,
2869 )
2870 .await
2871 .unwrap();
2872
2873 let (proc_id, _agent) = host.spawn("proc1".to_string(), ()).await.unwrap();
2874 let (proc_uid, host_location) = proc_id
2875 .location()
2876 .as_via()
2877 .expect("spawned proc must carry child via");
2878
2879 assert_eq!(proc_uid, proc_id.id().uid());
2880 assert_eq!(host_location.as_ref(), &Location::from(host.addr().clone()));
2881 assert_ne!(host_location.as_ref(), &attached_location);
2882 }
2883
2884 #[tokio::test]
2885 async fn test_spawn_uses_latest_direct_gateway_serve_location() {
2886 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2887 Ok(proc.spawn_with_label::<()>("host_agent", ()))
2888 });
2889
2890 let host = Host::new_with_gateway(
2891 proc_manager,
2892 ProcAddr::new(
2893 legacy_service_proc_id(),
2894 ChannelAddr::any(ChannelTransport::Unix).into(),
2895 ),
2896 None,
2897 Gateway::new(),
2898 None,
2899 )
2900 .await
2901 .unwrap();
2902 let mut later_frontend = host
2903 .gateway()
2904 .serve_with_listener(ChannelAddr::any(ChannelTransport::Unix), None)
2905 .unwrap();
2906 let later_location = host.gateway().default_location();
2907 assert_ne!(later_location.addr(), host.addr());
2908
2909 let (proc_id, _agent) = host.spawn("proc1".to_string(), ()).await.unwrap();
2910 let (proc_uid, host_location) = proc_id
2911 .location()
2912 .as_via()
2913 .expect("spawned proc must carry child via");
2914
2915 assert_eq!(proc_uid, proc_id.id().uid());
2916 assert_eq!(host_location.as_ref(), &later_location);
2917
2918 later_frontend.stop("test cleanup");
2919 later_frontend.join().await.unwrap();
2920 }
2921
2922 #[tokio::test]
2929 async fn test_new_with_gateway_via_advertises_via_location() {
2930 let remote_gw = Gateway::new();
2933 let mut remote_accept = remote_gw
2934 .serve_duplex(ChannelAddr::any(ChannelTransport::Unix))
2935 .unwrap();
2936 let remote_addr = remote_gw.default_location().addr().clone();
2937
2938 let proc_manager = LocalProcManager::new(|proc: Proc| async move {
2939 Ok(proc.spawn_with_label::<()>("host_agent", ()))
2940 });
2941
2942 let gateway = Gateway::new();
2943 let fallback_location = gateway.default_location();
2944 let mut host = Host::new_with_gateway(
2945 proc_manager,
2946 ProcAddr::new(
2947 legacy_service_proc_id(),
2948 ChannelAddr::any(ChannelTransport::Unix).into(),
2949 ),
2950 None,
2951 gateway.clone(),
2952 Some(remote_addr.clone()),
2953 )
2954 .await
2955 .unwrap();
2956
2957 let default_location = host.gateway().default_location();
2960 let (via_uid, inner) = default_location
2961 .as_via()
2962 .expect("default location must carry the via prefix");
2963 assert_eq!(via_uid, host.gateway().uid());
2964 assert_eq!(inner.addr(), &remote_addr);
2965
2966 let svc_proc_addr = host.system_proc().proc_addr();
2970 let (_, svc_inner) = svc_proc_addr
2971 .location()
2972 .as_via()
2973 .expect("service proc must carry the via prefix");
2974 assert_eq!(svc_inner.addr(), &remote_addr);
2975
2976 let shutdown_handles = host.take_shutdown_handles();
2977 drop(host);
2978 assert_eq!(
2979 gateway.default_location(),
2980 default_location,
2981 "shutdown handles must retain the via location after the host drops"
2982 );
2983 shutdown_handles.stop_and_join("test cleanup").await;
2984 assert_eq!(gateway.default_location(), fallback_location);
2985
2986 remote_accept.stop("test cleanup");
2987 remote_accept.join().await.unwrap();
2988 }
2989}