1use std::collections::HashMap;
10use std::ops::Deref;
11use std::path::PathBuf;
12use std::sync::OnceLock;
13use std::time::Duration;
14
15use hyperactor::ActorHandle;
16use hyperactor::Endpoint as _;
17use hyperactor::Gateway;
18use hyperactor::Instance;
19use hyperactor::Proc;
20use hyperactor::ProcAddr;
21use hyperactor::ProcId;
22use hyperactor::channel::ChannelAddr;
23use hyperactor::id::Label;
24use hyperactor_mesh::ProcMeshRef;
25use hyperactor_mesh::bootstrap::BootstrapCommand;
26use hyperactor_mesh::bootstrap::ProcBind;
27use hyperactor_mesh::bootstrap::host;
28use hyperactor_mesh::host_mesh;
29use hyperactor_mesh::host_mesh::HostMesh;
30use hyperactor_mesh::host_mesh::HostMeshRef;
31use hyperactor_mesh::host_mesh::PerRankBootstrapFn;
32use hyperactor_mesh::host_mesh::host_agent::GetLocalProcClient;
33use hyperactor_mesh::host_mesh::host_agent::HostAgent;
34use hyperactor_mesh::host_mesh::host_agent::ShutdownHost;
35use hyperactor_mesh::mesh_admin::MeshAdminMessageClient;
36use hyperactor_mesh::mesh_id::ActorMeshId;
37use hyperactor_mesh::mesh_id::HostMeshId;
38use hyperactor_mesh::mesh_id::ProcMeshId;
39use hyperactor_mesh::proc_agent::GetProcClient;
40use hyperactor_mesh::proc_mesh::ProcRef;
41use hyperactor_mesh::proc_mesh::telemetry_actor_mesh_id;
42use hyperactor_mesh::shared_cell::SharedCell;
43use hyperactor_mesh::transport::default_bind_spec;
44use hyperactor_telemetry::hash_to_u64;
45use ndslice::View;
46use ndslice::view::RankedSliceable;
47use pyo3::IntoPyObjectExt;
48use pyo3::exceptions::PyException;
49use pyo3::exceptions::PyRuntimeError;
50use pyo3::exceptions::PyValueError;
51use pyo3::prelude::*;
52use pyo3::types::PyBytes;
53use pyo3::types::PyMapping;
54
55use crate::actor::PythonActor;
56use crate::actor::to_py_error;
57use crate::context::PyInstance;
58use crate::context::stop_instance_and_wait;
59use crate::handle::PyHandle;
60use crate::proc_mesh::PyProcMesh;
61use crate::pytokio::PyPythonTask;
62use crate::runtime::GilSite;
63use crate::runtime::monarch_with_gil;
64use crate::runtime::monarch_with_gil_blocking;
65use crate::shape::PyExtent;
66use crate::shape::PyPoint;
67use crate::shape::PyRegion;
68
69#[pyclass(
89 name = "BootstrapCommand",
90 module = "monarch._rust_bindings.monarch_hyperactor.host_mesh"
91)]
92#[derive(Clone)]
93pub struct PyBootstrapCommand {
94 #[pyo3(get, set)]
95 pub program: String,
96 #[pyo3(get, set)]
97 pub arg0: Option<String>,
98 #[pyo3(get, set)]
99 pub args: Vec<String>,
100 pub env: HashMap<String, Option<String>>,
103 #[pyo3(get, set)]
104 pub inherit_env: bool,
105}
106
107fn extract_env(env: &Bound<'_, PyAny>) -> PyResult<HashMap<String, Option<String>>> {
110 env.cast::<PyMapping>()?
111 .items()?
112 .iter()
113 .map(|item| item.extract())
114 .collect()
115}
116
117#[pymethods]
118impl PyBootstrapCommand {
119 #[new]
120 #[pyo3(signature = (program, arg0, args, env, inherit_env = true))]
121 fn new(
122 program: String,
123 arg0: Option<String>,
124 args: Vec<String>,
125 env: &Bound<'_, PyAny>,
126 inherit_env: bool,
127 ) -> PyResult<Self> {
128 Ok(Self {
129 program,
130 arg0,
131 args,
132 env: extract_env(env)?,
133 inherit_env,
134 })
135 }
136
137 #[getter]
138 fn env(&self) -> HashMap<String, Option<String>> {
139 self.env.clone()
140 }
141
142 #[setter]
143 fn set_env(&mut self, env: &Bound<'_, PyAny>) -> PyResult<()> {
144 self.env = extract_env(env)?;
145 Ok(())
146 }
147
148 fn __repr__(&self) -> String {
149 format!(
150 "BootstrapCommand(program='{}', args={:?}, env={:?}, inherit_env={})",
151 self.program,
152 self.args,
153 self.env,
154 if self.inherit_env { "True" } else { "False" }
155 )
156 }
157
158 fn with_env(&self, env: &Bound<'_, PyAny>) -> PyResult<Self> {
162 let mut new_env = self.env.clone();
163 new_env.extend(extract_env(env)?);
164 Ok(Self {
165 env: new_env,
166 ..self.clone()
167 })
168 }
169}
170
171impl PyBootstrapCommand {
172 pub fn to_rust(&self) -> BootstrapCommand {
173 BootstrapCommand {
174 program: PathBuf::from(&self.program),
175 arg0: self.arg0.clone(),
176 args: self.args.clone(),
177 env: self
178 .env
179 .iter()
180 .filter_map(|(k, v)| Some((k.clone(), v.clone()?)))
181 .collect(),
182 env_remove: self
183 .env
184 .iter()
185 .filter(|(_, v)| v.is_none())
186 .map(|(k, _)| k.clone())
187 .collect(),
188 inherit_env: self.inherit_env,
189 }
190 }
191}
192
193#[pyclass(
194 name = "HostMesh",
195 module = "monarch._rust_bindings.monarch_hyperactor.host_mesh"
196)]
197#[expect(
198 clippy::large_enum_variant,
199 reason = "PyO3 #[pyclass] enum; Box wrapping interacts with PyO3 codegen and Python interop — separate diff"
200)]
201pub(crate) enum PyHostMesh {
202 Owned(PyHostMeshImpl),
203 Ref(PyHostMeshRefImpl),
204}
205
206impl PyHostMesh {
207 pub(crate) fn new_owned(inner: HostMesh) -> Self {
208 Self::Owned(PyHostMeshImpl(SharedCell::from(inner)))
209 }
210
211 pub(crate) fn new_ref(inner: HostMeshRef) -> Self {
212 Self::Ref(PyHostMeshRefImpl(inner))
213 }
214
215 pub(crate) fn mesh_ref(&self) -> Result<HostMeshRef, anyhow::Error> {
216 match self {
217 PyHostMesh::Owned(inner) => Ok(inner.0.borrow()?.clone()),
218 PyHostMesh::Ref(inner) => Ok(inner.0.clone()),
219 }
220 }
221}
222
223#[pymethods]
224impl PyHostMesh {
225 #[pyo3(signature = (instance, name, per_host, proc_bind = None, per_rank_bootstrap = None))]
226 fn spawn_nonblocking(
227 &self,
228 _py: Python<'_>,
229 instance: &PyInstance,
230 name: String,
231 per_host: &PyExtent,
232 proc_bind: Option<Vec<HashMap<String, String>>>,
233 per_rank_bootstrap: Option<Py<PyAny>>,
234 ) -> PyResult<PyPythonTask> {
235 let host_mesh = self.mesh_ref()?.clone();
236 let per_rank_bootstrap: Option<Box<PerRankBootstrapFn>> = per_rank_bootstrap
237 .map(|callable| -> PyResult<Box<PerRankBootstrapFn>> {
238 Ok(Box::new(move |point| {
239 monarch_with_gil_blocking(GilSite::Bootstrap, |py| {
240 let result =
241 callable
242 .bind(py)
243 .call1((PyPoint::from(point),))
244 .map_err(|e| {
245 anyhow::anyhow!("per-rank bootstrap callable raised: {}", e)
246 })?;
247 let cmd: PyBootstrapCommand = result.extract().map_err(|e| {
248 anyhow::anyhow!(
249 "per-rank bootstrap callable did not return BootstrapCommand: {}",
250 e
251 )
252 })?;
253 Ok(cmd.to_rust())
254 })
255 }))
256 })
257 .transpose()?;
258 let instance = instance.clone();
259 let per_host = per_host.clone().into();
260 let proc_bind = proc_bind.map(|v| v.into_iter().map(ProcBind::from).collect());
261 let mesh_impl = async move {
262 let proc_mesh = host_mesh
263 .spawn(
264 instance.deref(),
265 &name,
266 per_host,
267 proc_bind,
268 per_rank_bootstrap,
269 )
270 .await
271 .map_err(to_py_error)?;
272 Ok(PyProcMesh::new_owned(proc_mesh))
273 };
274 PyPythonTask::new(mesh_impl)
275 }
276
277 fn with_bootstrap(&self, bootstrap_command: &PyBootstrapCommand) -> PyResult<Self> {
278 match self {
279 PyHostMesh::Owned(inner) => {
280 let cmd = bootstrap_command.to_rust();
281 inner
282 .0
283 .try_with_mut(|mesh| mesh.set_bootstrap(cmd))
284 .map_err(|e| PyException::new_err(e.to_string()))?;
285 Ok(Self::Owned(inner.clone()))
286 }
287 PyHostMesh::Ref(_) => Ok(Self::new_ref(
288 self.mesh_ref()?.with_bootstrap(bootstrap_command.to_rust()),
289 )),
290 }
291 }
292
293 fn sliced(&self, region: &PyRegion) -> PyResult<Self> {
294 Ok(Self::new_ref(
295 self.mesh_ref()?.sliced(region.as_inner().clone()),
296 ))
297 }
298
299 #[getter]
300 fn region(&self) -> PyResult<PyRegion> {
301 Ok(PyRegion::from(self.mesh_ref()?.region()))
302 }
303
304 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
305 let mesh_ref = self.mesh_ref()?;
306 if crate::pickle::push_mesh_reference_if_active(crate::actor::MeshRef::Host(Box::new(
307 mesh_ref.clone(),
308 ))) {
309 let pop_fn = PyModule::import(py, "monarch._rust_bindings.monarch_hyperactor.pickle")?
310 .getattr("pop_mesh_reference")?;
311 return Ok((pop_fn, pyo3::types::PyTuple::empty(py).into_any()));
312 }
313 let bytes = bincode::serde::encode_to_vec(&mesh_ref, bincode::config::legacy())
314 .map_err(|e| PyErr::new::<PyValueError, _>(e.to_string()))?;
315 let py_bytes = (PyBytes::new(py, &bytes),).into_bound_py_any(py).unwrap();
316 let from_bytes =
317 PyModule::import(py, "monarch._rust_bindings.monarch_hyperactor.host_mesh")?
318 .getattr("py_host_mesh_from_bytes")?;
319 Ok((from_bytes, py_bytes))
320 }
321
322 fn __eq__(&self, other: &PyHostMesh) -> PyResult<bool> {
323 Ok(self.mesh_ref()? == other.mesh_ref()?)
324 }
325
326 fn shutdown(&self, instance: &PyInstance) -> PyResult<PyHandle> {
327 match self {
328 PyHostMesh::Owned(inner) => {
329 let instance = instance.clone();
330 let mesh_borrow = inner.0.clone();
331 let fut = async move {
332 match mesh_borrow.take().await {
333 Ok(mut mesh) => {
334 mesh.shutdown(instance.deref()).await?;
335 Ok(())
336 }
337 Err(_) => {
338 tracing::info!("shutdown was already called on host mesh");
341 Ok(())
342 }
343 }
344 };
345 Ok(PyHandle::spawn(fut))
346 }
347 PyHostMesh::Ref(_) => Err(PyRuntimeError::new_err(
348 "cannot shut down `HostMesh` that is a reference instead of owned",
349 )),
350 }
351 }
352
353 fn stop(&self, instance: &PyInstance) -> PyResult<PyHandle> {
354 match self {
355 PyHostMesh::Owned(inner) => {
356 let instance = instance.clone();
357 let mesh_borrow = inner.0.clone();
358 let fut = async move {
359 match mesh_borrow.take().await {
360 Ok(mut mesh) => {
361 mesh.stop(instance.deref()).await?;
362 Ok(())
363 }
364 Err(_) => {
365 tracing::info!("stop was already called on host mesh");
366 Ok(())
367 }
368 }
369 };
370 Ok(PyHandle::spawn(fut))
371 }
372 PyHostMesh::Ref(_) => Err(PyRuntimeError::new_err(
373 "cannot stop `HostMesh` that is a reference instead of owned",
374 )),
375 }
376 }
377}
378
379#[derive(Clone)]
380#[pyclass(
381 name = "HostMeshImpl",
382 module = "monarch._rust_bindings.monarch_hyperactor.host_mesh"
383)]
384pub(crate) struct PyHostMeshImpl(SharedCell<HostMesh>);
385
386#[derive(Debug, Clone)]
387#[pyclass(
388 name = "HostMeshRefImpl",
389 module = "monarch._rust_bindings.monarch_hyperactor.host_mesh"
390)]
391pub(crate) struct PyHostMeshRefImpl(HostMeshRef);
392
393impl PyHostMeshRefImpl {
394 fn __repr__(&self) -> PyResult<String> {
395 Ok(format!("<HostMeshRefImpl {:?}>", self.0))
396 }
397}
398
399static ROOT_CLIENT_INSTANCE_FOR_HOST: OnceLock<Instance<PythonActor>> = OnceLock::new();
401
402static HOST_MESH_AGENT_FOR_HOST: OnceLock<ActorHandle<HostAgent>> = OnceLock::new();
404
405static HOST_SHUTDOWN_HANDLE: OnceLock<
409 tokio::sync::Mutex<Option<hyperactor_mesh::bootstrap::HostShutdownHandle>>,
410> = OnceLock::new();
411
412#[pyfunction]
448#[pyo3(signature = (bootstrap_cmd, via=None, bind_addr=None))]
449fn bootstrap_host(
450 bootstrap_cmd: Option<PyBootstrapCommand>,
451 via: Option<&str>,
452 bind_addr: Option<&str>,
453) -> PyResult<PyPythonTask> {
454 let bootstrap_cmd = match bootstrap_cmd {
455 Some(cmd) => cmd.to_rust(),
456 None => BootstrapCommand::current().map_err(|e| PyException::new_err(e.to_string()))?,
457 };
458 let via_addr = via
459 .map(|s| {
460 ChannelAddr::from_zmq_url(s)
461 .map_err(|e| PyValueError::new_err(format!("via address: {}", e)))
462 })
463 .transpose()?;
464 let bind_addr = bind_addr
465 .map(|s| {
466 ChannelAddr::from_zmq_url(s)
467 .map_err(|e| PyValueError::new_err(format!("bind address: {}", e)))
468 })
469 .transpose()?
470 .unwrap_or_else(|| default_bind_spec().binding_addr());
471
472 PyPythonTask::new(async move {
473 let gateway = Gateway::global().clone();
481 let service_proc_addr = ProcAddr::new(
482 ProcId::singleton(Label::strip(hyperactor::proc::LEGACY_SERVICE_PROC_NAME)),
483 bind_addr.into(),
484 );
485
486 let (host_mesh_agent, shutdown_handle) = host(
487 service_proc_addr,
488 Some(bootstrap_cmd),
489 None,
490 false,
491 None,
492 gateway,
493 via_addr,
494 )
495 .await
496 .map_err(|e| PyException::new_err(e.to_string()))?;
497
498 HOST_MESH_AGENT_FOR_HOST.set(host_mesh_agent.clone()).ok();
500 HOST_SHUTDOWN_HANDLE.get_or_init(|| tokio::sync::Mutex::new(Some(shutdown_handle)));
501
502 let host_mesh_id = HostMeshId::singleton(Label::new("local").unwrap());
503 let host_mesh = HostMeshRef::from_host_agent(host_mesh_id, host_mesh_agent.bind())
504 .map_err(|e| PyException::new_err(e.to_string()))?;
505
506 hyperactor_mesh::global_context::register_client_host(host_mesh.clone());
509
510 let temp_proc = Proc::isolated();
512 let temp_instance = temp_proc.client("temp");
513
514 let local_proc_agent: hyperactor::ActorHandle<hyperactor_mesh::proc_agent::ProcAgent> =
515 host_mesh_agent
516 .get_local_proc(&temp_instance)
517 .await
518 .map_err(|e| PyException::new_err(e.to_string()))?;
519
520 let proc_mesh = ProcMeshRef::new_singleton(
521 ProcMeshId::singleton(Label::new("local").unwrap()),
522 ProcRef::new(
523 local_proc_agent.actor_addr().proc_addr(),
524 0,
525 local_proc_agent.bind(),
526 ),
527 )
528 .map_err(|e| PyException::new_err(e.to_string()))?;
529
530 let local_proc = local_proc_agent
531 .get_proc(&temp_instance)
532 .await
533 .map_err(|e| PyException::new_err(e.to_string()))?;
534
535 let client_root = hyperactor_mesh::client_root::ClientRootRef::bind(&local_proc_agent);
540 let mut root_env = hyperactor::ActorEnvironment::default();
541 root_env
542 .set(hyperactor_mesh::client_root::CLIENT_ROOT, client_root)
543 .map_err(|e| PyException::new_err(e.to_string()))?;
544
545 let (instance, _handle) = monarch_with_gil(GilSite::Bootstrap, |py| {
546 PythonActor::bootstrap_client_inner(
547 py,
548 local_proc,
549 root_env,
550 &ROOT_CLIENT_INSTANCE_FOR_HOST,
551 )
552 })
553 .await;
554
555 hyperactor_mesh::global_context::register_client_proc(proc_mesh.clone());
561
562 {
564 let now = std::time::SystemTime::now();
565
566 let host_name_str = host_mesh.id().to_string();
567 let host_mesh_id = hash_to_u64(host_mesh.id());
568 hyperactor_telemetry::notify_mesh_created(hyperactor_telemetry::MeshEvent {
569 id: host_mesh_id,
570 timestamp: now,
571 class: "Host".to_string(),
572 given_name: host_mesh
573 .id()
574 .display_label()
575 .map(|l| l.as_str())
576 .unwrap_or("unnamed")
577 .to_string(),
578 full_name: host_name_str,
579 shape_json: serde_json::to_string(&host_mesh.region().extent()).unwrap_or_default(),
580 parent_mesh_id: None,
581 parent_view_json: None,
582 });
583
584 let host_agent_addr = host_mesh_agent.actor_addr();
585 hyperactor_telemetry::notify_actor_created(hyperactor_telemetry::ActorEvent {
586 id: hyperactor_telemetry::hash_to_u64(host_agent_addr.id()),
587 timestamp: now,
588 mesh_id: host_mesh_id,
589 rank: 0,
590 full_name: host_agent_addr.to_string(),
591 display_name: None,
592 });
593
594 let proc_id_str = proc_mesh.id().to_string();
595 let proc_mesh_id = hash_to_u64(proc_mesh.id());
596 hyperactor_telemetry::notify_mesh_created(hyperactor_telemetry::MeshEvent {
597 id: proc_mesh_id,
598 timestamp: now,
599 class: "Proc".to_string(),
600 given_name: proc_mesh
601 .id()
602 .display_label()
603 .map(|l| l.as_str())
604 .unwrap_or("unnamed")
605 .to_string(),
606 full_name: proc_id_str,
607 shape_json: serde_json::to_string(&proc_mesh.region().extent()).unwrap_or_default(),
608 parent_mesh_id: Some(host_mesh_id),
609 parent_view_json: None,
610 });
611
612 let proc_agent_addr = local_proc_agent.actor_addr();
613 hyperactor_telemetry::notify_actor_created(hyperactor_telemetry::ActorEvent {
614 id: hyperactor_telemetry::hash_to_u64(proc_agent_addr.id()),
615 timestamp: now,
616 mesh_id: proc_mesh_id,
617 rank: 0,
618 full_name: proc_agent_addr.to_string(),
619 display_name: None,
620 });
621
622 let client_mesh_actor_id = ActorMeshId::singleton(Label::new("client").unwrap());
623 let client_mesh_name = format!("{}/client", proc_mesh.id());
624 let client_mesh_id = telemetry_actor_mesh_id(proc_mesh.id(), &client_mesh_actor_id);
625 hyperactor_telemetry::notify_mesh_created(hyperactor_telemetry::MeshEvent {
626 id: client_mesh_id,
627 timestamp: now,
628 class: <PythonActor as typeuri::Named>::typename().to_string(),
629 given_name: "client".to_string(),
630 full_name: client_mesh_name,
631 shape_json: serde_json::to_string(&proc_mesh.region().extent()).unwrap_or_default(),
632 parent_mesh_id: Some(proc_mesh_id),
633 parent_view_json: None,
634 });
635
636 hyperactor_telemetry::notify_actor_created(hyperactor_telemetry::ActorEvent {
637 id: hyperactor_telemetry::hash_to_u64(instance.self_addr().id()),
638 timestamp: now,
639 mesh_id: client_mesh_id,
640 rank: 0,
641 full_name: instance.self_addr().to_string(),
642 display_name: Some("<root>".to_string()),
643 });
644 }
645
646 Ok((
647 PyHostMesh::new_ref(host_mesh),
648 PyProcMesh::new_ref(proc_mesh),
649 PyInstance::from(instance),
650 ))
651 })
652}
653
654pub struct PythonClientRoot {
658 cx: &'static Instance<PythonActor>,
659 proc_mesh: ProcMeshRef,
660}
661
662impl PythonClientRoot {
663 pub async fn spawn_controllerless_owner<A: hyperactor::RemoteSpawn>(
677 &self,
678 name: &str,
679 params: &A::Params,
680 ) -> hyperactor_mesh::Result<hyperactor::ActorRef<A>>
681 where
682 A::Params: hyperactor::RemoteMessage,
683 {
684 let mesh = self
685 .proc_mesh
686 .spawn_controllerless_service(self.cx, name, params)
687 .await?;
688 let actual = ndslice::view::Ranked::region(&*mesh).num_ranks();
691 if actual != 1 {
692 return Err(hyperactor_mesh::Error::InvalidRankCardinality {
693 expected: 1,
694 actual,
695 });
696 }
697 Ok(ndslice::view::Ranked::get(&*mesh, 0)
698 .expect("singleton owner mesh must contain rank 0")
699 .clone())
700 }
701}
702
703pub fn python_client_root() -> Result<Option<PythonClientRoot>, anyhow::Error> {
714 match (
715 ROOT_CLIENT_INSTANCE_FOR_HOST.get(),
716 hyperactor_mesh::global_context::try_registered_client_proc(),
717 ) {
718 (Some(cx), Some(proc_mesh)) => Ok(Some(PythonClientRoot {
719 cx,
720 proc_mesh: proc_mesh.clone(),
721 })),
722 (None, None) => Ok(None),
723 (Some(_), None) => Err(anyhow::anyhow!(
724 "partial Python client root registration: root client instance is registered but client proc mesh is missing"
725 )),
726 (None, Some(_)) => Err(anyhow::anyhow!(
727 "partial Python client root registration: client proc mesh is registered but root client instance is missing"
728 )),
729 }
730}
731
732#[pyfunction]
733fn py_host_mesh_from_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult<PyHostMesh> {
734 let r: PyResult<HostMeshRef> =
735 bincode::serde::decode_from_slice(bytes.as_bytes(), bincode::config::legacy())
736 .map(|(v, _)| v)
737 .map_err(|e| PyErr::new::<PyValueError, _>(e.to_string()));
738 r.map(PyHostMesh::new_ref)
739}
740
741#[pyfunction]
742#[pyo3(signature = (timeout_secs=None))]
743fn shutdown_local_host_mesh(timeout_secs: Option<f64>) -> PyResult<PyHandle> {
744 let timeout = timeout_secs
745 .map(Duration::try_from_secs_f64)
746 .transpose()
747 .map_err(|e| PyValueError::new_err(format!("timeout_secs: {}", e)))?
748 .unwrap_or(Duration::from_secs(10));
749 let agent = HOST_MESH_AGENT_FOR_HOST
750 .get()
751 .ok_or_else(|| PyRuntimeError::new_err("No local host mesh to shutdown"))?
752 .clone();
753
754 Ok(PyHandle::spawn(async move {
755 let temp_proc = hyperactor::Proc::isolated();
757 let instance = temp_proc.client("shutdown_requester");
758
759 tracing::info!(
760 "sending shutdown_host request to agent {}",
761 agent.actor_addr()
762 );
763 let (port, _) = instance.open_port::<usize>();
768 let mut port = port.bind();
769 port.return_undeliverable(false);
772 agent.post(
773 &instance,
774 ShutdownHost {
775 timeout,
776 max_in_flight: 16,
777 rank: hyperactor_mesh::resource::Rank::new(0),
778 ack: port,
779 },
780 );
781
782 let flush_timeout =
785 hyperactor_config::global::get(hyperactor::config::FORWARDER_FLUSH_TIMEOUT)
786 .min(timeout);
787 let shutdown = if let Some(lock) = HOST_SHUTDOWN_HANDLE.get() {
788 lock.lock().await.take()
789 } else {
790 None
791 };
792 if let Some(shutdown) = shutdown {
793 if let Some(root) = ROOT_CLIENT_INSTANCE_FOR_HOST.get() {
794 shutdown
795 .stop_and_join_after_drain(stop_instance_and_wait(
796 root,
797 "shutdown".to_string(),
798 flush_timeout,
799 ))
800 .await;
801 } else {
802 tracing::warn!("shutting down partially bootstrapped host without root client");
803 shutdown.stop_and_join().await;
804 }
805 } else if let Some(root) = ROOT_CLIENT_INSTANCE_FOR_HOST.get() {
806 stop_instance_and_wait(root, "shutdown".to_string(), flush_timeout).await;
807 }
808
809 Ok(None::<()>)
811 }))
812}
813
814#[pyclass(
819 name = "PyMeshAdminRef",
820 module = "monarch._rust_bindings.monarch_hyperactor.host_mesh"
821)]
822#[derive(Clone)]
823pub struct PyMeshAdminRef(hyperactor::ActorRef<hyperactor_mesh::mesh_admin::MeshAdminAgent>);
824
825impl PyMeshAdminRef {
826 pub fn actor_ref(&self) -> hyperactor::ActorRef<hyperactor_mesh::mesh_admin::MeshAdminAgent> {
827 self.0.clone()
828 }
829}
830
831#[pyfunction]
836fn _spawn_admin(
837 host_meshes: Vec<PyRef<'_, PyHostMesh>>,
838 instance: &PyInstance,
839 admin_addr: Option<String>,
840 telemetry_url: Option<String>,
841) -> PyResult<PyPythonTask> {
842 if host_meshes.is_empty() {
843 return Err(PyException::new_err("at least one mesh is required"));
844 }
845
846 let admin_addr = admin_addr
847 .map(|s| {
848 s.parse::<std::net::SocketAddr>()
849 .map_err(|e| PyException::new_err(format!("invalid admin_addr '{}': {}", s, e)))
850 })
851 .transpose()?;
852
853 let mesh_refs = host_meshes
854 .iter()
855 .map(|m| -> PyResult<HostMeshRef> { Ok(m.mesh_ref()?.clone()) })
856 .collect::<PyResult<Vec<HostMeshRef>>>()?;
857
858 let instance = instance.clone();
859 PyPythonTask::new(async move {
860 let admin_ref =
861 host_mesh::spawn_admin(&mesh_refs, instance.deref(), admin_addr, telemetry_url)
862 .await
863 .map_err(|e| PyException::new_err(e.to_string()))?;
864 let admin_url = admin_ref
865 .get_admin_addr(instance.deref())
866 .await
867 .map_err(|e| PyException::new_err(e.to_string()))?
868 .addr
869 .ok_or_else(|| PyException::new_err("mesh admin agent did not report an address"))?;
870 Ok((admin_url, PyMeshAdminRef(admin_ref)))
871 })
872}
873
874pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
875 let f = wrap_pyfunction!(py_host_mesh_from_bytes, hyperactor_mod)?;
876 f.setattr(
877 "__module__",
878 "monarch._rust_bindings.monarch_hyperactor.host_mesh",
879 )?;
880 hyperactor_mod.add_function(f)?;
881
882 let f2 = wrap_pyfunction!(bootstrap_host, hyperactor_mod)?;
883 f2.setattr(
884 "__module__",
885 "monarch._rust_bindings.monarch_hyperactor.host_mesh",
886 )?;
887 hyperactor_mod.add_function(f2)?;
888
889 let f3 = wrap_pyfunction!(shutdown_local_host_mesh, hyperactor_mod)?;
890 f3.setattr(
891 "__module__",
892 "monarch._rust_bindings.monarch_hyperactor.host_mesh",
893 )?;
894 hyperactor_mod.add_function(f3)?;
895
896 let f4 = wrap_pyfunction!(_spawn_admin, hyperactor_mod)?;
897 f4.setattr(
898 "__module__",
899 "monarch._rust_bindings.monarch_hyperactor.host_mesh",
900 )?;
901 hyperactor_mod.add_function(f4)?;
902
903 hyperactor_mod.add_class::<PyHostMesh>()?;
904 hyperactor_mod.add_class::<PyBootstrapCommand>()?;
905 hyperactor_mod.add_class::<PyMeshAdminRef>()?;
906 Ok(())
907}