1use std::future::Future;
10use std::ops::Deref;
11use std::pin::Pin;
12use std::sync::Arc;
13use std::thread;
14use std::time::Duration;
15
16use async_trait::async_trait;
17use futures::future;
18use futures::future::FutureExt;
19use futures::future::Shared;
20use hyperactor::Instance;
21use hyperactor::supervision::ActorSupervisionEvent;
22use hyperactor_mesh::actor_mesh::ActorMesh;
23use hyperactor_mesh::actor_mesh::ActorMeshRef;
24use monarch_types::py_global;
25use monarch_types::py_module_add_function;
26use ndslice::view::Ranked;
27use ndslice::view::RankedSliceable;
28use pyo3::IntoPyObjectExt;
29use pyo3::exceptions::PyNotImplementedError;
30use pyo3::exceptions::PyRuntimeError;
31use pyo3::exceptions::PyValueError;
32use pyo3::prelude::*;
33use pyo3::types::PyBytes;
34use pyo3::types::PyTuple;
35use tokio::sync::mpsc::UnboundedSender;
36use tokio::sync::mpsc::unbounded_channel;
37use tracing::Instrument;
38
39use crate::actor::PythonActor;
40use crate::actor::PythonMessage;
41use crate::actor::PythonMessageKind;
42use crate::context::PyInstance;
43use crate::pickle::PendingMessage;
44use crate::proc::PyActorAddr;
45use crate::pytokio::PyPythonTask;
46use crate::runtime::GilSite;
47use crate::runtime::get_tokio_runtime;
48use crate::runtime::monarch_with_gil;
49use crate::runtime::monarch_with_gil_blocking;
50use crate::shape::PyRegion;
51use crate::supervision::Supervisable;
52use crate::supervision::SupervisionError;
53
54py_global!(_pickle, "monarch._src.actor.actor_mesh", "_pickle");
55
56py_global!(
57 shared_class,
58 "monarch._rust_bindings.monarch_hyperactor.pytokio",
59 "Shared"
60);
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub(crate) enum AllOrChoose {
69 All,
70 Choose,
71}
72
73impl AllOrChoose {
74 pub(crate) fn as_str(self) -> &'static str {
75 match self {
76 Self::All => "all",
77 Self::Choose => "choose",
78 }
79 }
80}
81
82pub(crate) trait ActorMeshProtocol: Send + Sync {
85 fn cast(
87 &self,
88 message: PythonMessage,
89 selection: AllOrChoose,
90 instance: &Instance<PythonActor>,
91 ) -> PyResult<()>;
92
93 fn cast_with_headers(
100 &self,
101 message: PythonMessage,
102 selection: AllOrChoose,
103 instance: &Instance<PythonActor>,
104 _caller_headers: hyperactor_config::Flattrs,
105 ) -> PyResult<()> {
106 self.cast(message, selection, instance)
107 }
108
109 fn cast_unresolved(
114 &self,
115 message: PendingMessage,
116 selection: AllOrChoose,
117 instance: &Instance<PythonActor>,
118 ) -> PyResult<()> {
119 let message = get_tokio_runtime().block_on(message.resolve())?;
120 self.cast(message, selection, instance)
121 }
122
123 fn cast_unresolved_with_headers(
128 &self,
129 message: PendingMessage,
130 selection: AllOrChoose,
131 instance: &Instance<PythonActor>,
132 caller_headers: hyperactor_config::Flattrs,
133 ) -> PyResult<()> {
134 let message = get_tokio_runtime().block_on(message.resolve())?;
135 self.cast_with_headers(message, selection, instance, caller_headers)
136 }
137
138 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>;
139
140 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>>;
144
145 fn stop(&self, _instance: &PyInstance, _reason: String) -> PyResult<PyPythonTask> {
148 Err(PyNotImplementedError::new_err(format!(
149 "stop() is not supported for {}",
150 std::any::type_name::<Self>()
151 )))
152 }
153
154 fn initialized(&self) -> PyResult<PyPythonTask> {
157 PyPythonTask::new(async { Ok(None::<()>) })
158 }
159
160 fn name(&self) -> PyResult<PyPythonTask>;
162}
163
164pub(crate) trait SupervisableActorMesh: ActorMeshProtocol + Supervisable {
165 fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>>;
166}
167
168#[pyclass(
170 name = "PythonActorMesh",
171 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
172)]
173#[derive(Clone)]
174pub(crate) struct PythonActorMesh {
175 inner: Arc<dyn SupervisableActorMesh>,
176}
177
178impl PythonActorMesh {
179 pub(crate) fn new<F>(f: F, supervised: bool) -> Self
180 where
181 F: Future<Output = PyResult<Box<dyn SupervisableActorMesh>>> + Send + 'static,
182 {
183 let f = async move { Ok(Arc::from(f.await?)) }.boxed().shared();
184 PythonActorMesh {
185 inner: Arc::new(AsyncActorMesh::new_queue(f, supervised)),
186 }
187 }
188
189 pub(crate) fn from_impl(inner: Arc<dyn SupervisableActorMesh>) -> Self {
190 PythonActorMesh { inner }
191 }
192
193 pub(crate) fn get_inner(&self) -> Arc<dyn SupervisableActorMesh> {
194 self.inner.clone()
195 }
196}
197
198pub(crate) fn to_all_or_choose(selection: &str) -> PyResult<AllOrChoose> {
199 match selection {
200 "choose" => Ok(AllOrChoose::Choose),
201 "all" => Ok(AllOrChoose::All),
202 _ => Err(PyErr::new::<PyValueError, _>(format!(
203 "Invalid selection: {}",
204 selection
205 ))),
206 }
207}
208
209#[pymethods]
210impl PythonActorMesh {
211 #[tracing::instrument(level = "debug", skip_all)]
212 #[pyo3(name = "cast")]
213 fn py_cast(
214 &self,
215 message: &PythonMessage,
216 selection: &str,
217 instance: &PyInstance,
218 ) -> PyResult<()> {
219 let sel = to_all_or_choose(selection)?;
220 self.inner.cast(message.clone(), sel, instance.deref())
221 }
222
223 #[hyperactor::instrument]
224 pub(crate) fn cast_unresolved(
225 &self,
226 message: &mut PendingMessage,
227 selection: &str,
228 instance: &PyInstance,
229 ) -> PyResult<()> {
230 let sel = to_all_or_choose(selection)?;
231 let message = message.take()?;
232 self.inner.cast_unresolved(message, sel, instance)
233 }
234
235 fn new_with_region(&self, region: &PyRegion) -> PyResult<PythonActorMesh> {
236 let inner = self.inner.new_with_region(region)?;
237 Ok(PythonActorMesh {
238 inner: Arc::from(inner),
239 })
240 }
241
242 fn stop(&self, instance: &PyInstance, reason: String) -> PyResult<PyPythonTask> {
243 self.inner.stop(instance, reason)
244 }
245
246 fn initialized(&self) -> PyResult<PyPythonTask> {
247 self.inner.initialized()
248 }
249
250 fn name(&self) -> PyResult<PyPythonTask> {
251 self.inner.name()
252 }
253
254 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
255 self.inner.__reduce__(py)
256 }
257}
258
259#[derive(Debug)]
260pub(crate) struct ClonePyErr {
261 inner: PyErr,
262}
263
264impl From<ClonePyErr> for PyErr {
265 fn from(value: ClonePyErr) -> PyErr {
266 value.inner
267 }
268}
269impl From<PyErr> for ClonePyErr {
270 fn from(inner: PyErr) -> ClonePyErr {
271 ClonePyErr { inner }
272 }
273}
274
275impl Clone for ClonePyErr {
276 fn clone(&self) -> Self {
277 monarch_with_gil_blocking(GilSite::Convert, |py| self.inner.clone_ref(py).into())
278 }
279}
280
281type ActorMeshResult = Result<Arc<dyn SupervisableActorMesh>, ClonePyErr>;
282type ActorMeshFut = Shared<Pin<Box<dyn Future<Output = ActorMeshResult> + Send + 'static>>>;
283
284pub(crate) struct AsyncActorMesh {
285 mesh: ActorMeshFut,
286 queue: UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
287 supervised: bool,
288}
289
290impl AsyncActorMesh {
291 pub(crate) fn new_queue(f: ActorMeshFut, supervised: bool) -> AsyncActorMesh {
292 let (queue, mut recv) = unbounded_channel();
293
294 get_tokio_runtime().spawn(async move {
295 loop {
296 let r = recv.recv().await;
297 if let Some(r) = r {
298 r.await;
299 } else {
300 return;
301 }
302 }
303 });
304
305 let mesh = AsyncActorMesh::new(queue, supervised, f);
306 let f = mesh.mesh.clone();
314 mesh.push(async move {
315 let _ = f.await;
316 });
317 mesh
318 }
319
320 fn new(
321 queue: UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
322 supervised: bool,
323 f: ActorMeshFut,
324 ) -> AsyncActorMesh {
325 AsyncActorMesh {
326 mesh: f,
327 queue,
328 supervised,
329 }
330 }
331
332 fn push<F>(&self, f: F)
333 where
334 F: Future<Output = ()> + Send + 'static,
335 {
336 self.queue.send(f.boxed()).unwrap();
337 }
338
339 pub(crate) fn from_impl(mesh: Arc<dyn SupervisableActorMesh>) -> Self {
340 let fut = future::ready(Ok::<Arc<dyn SupervisableActorMesh>, ClonePyErr>(mesh))
341 .boxed()
342 .shared();
343 let _ = futures::executor::block_on(fut.clone());
345 Self::new_queue(fut, true)
346 }
347}
348
349impl ActorMeshProtocol for AsyncActorMesh {
350 fn cast(
351 &self,
352 _message: PythonMessage,
353 _selection: AllOrChoose,
354 _instance: &Instance<PythonActor>,
355 ) -> PyResult<()> {
356 panic!("not implemented")
357 }
358
359 fn cast_unresolved(
360 &self,
361 message: PendingMessage,
362 selection: AllOrChoose,
363 instance: &Instance<PythonActor>,
364 ) -> PyResult<()> {
365 self.cast_unresolved_with_headers(
366 message,
367 selection,
368 instance,
369 hyperactor_config::Flattrs::new(),
370 )
371 }
372
373 fn cast_unresolved_with_headers(
374 &self,
375 message: PendingMessage,
376 selection: AllOrChoose,
377 instance: &Instance<PythonActor>,
378 caller_headers: hyperactor_config::Flattrs,
379 ) -> PyResult<()> {
380 let mesh = self.mesh.clone();
381 let instance = instance.clone_for_py();
382 let port = match &message.kind {
383 PythonMessageKind::CallMethod { response_port, .. } => response_port.clone(),
384 _ => None,
385 };
386 self.push(
387 async move {
388 let result = async {
389 let resolved = message.resolve().await?;
390 mesh.await?
391 .cast_with_headers(resolved, selection, &instance, caller_headers)
392 }
393 .await;
394 if let (Some(mut port_ref), Err(pyerr)) = (port, result) {
395 let _ = monarch_with_gil(GilSite::Traceback, |py: Python<'_>| {
396 let exception_str = crate::logging::format_traceback(py, &pyerr);
397 tracing::error!(
398 actor_id = instance.self_addr().to_string(),
399 "error occurred during cast unresolved: {}",
400 exception_str
401 );
402
403 port_ref.set_return_undeliverable(false);
423
424 let mut state = crate::pickle::pickle(
425 py,
426 pyerr.into_value(py).into_any(),
427 false,
428 false,
429 )?;
430 let _ = port_ref.post(
431 &instance,
432 PythonMessage::new_from_buf(
433 PythonMessageKind::Exception { rank: Some(0) },
434 state.take_inner()?.take_buffer(),
435 ),
436 );
437
438 Ok::<_, PyErr>(())
439 })
440 .await;
441 }
442 }
443 .instrument(tracing::debug_span!("AsyncActorMesh::cast")),
444 );
445 Ok(())
446 }
447
448 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
449 let fut = self.mesh.clone();
450 match fut.peek().cloned() {
451 Some(mesh) => mesh?.__reduce__(py),
452 None => {
453 let shared =
454 PyPythonTask::new(async move { Ok(PythonActorMesh::from_impl(fut.await?)) })?
455 .spawn_abortable()?;
456 let shared = Py::new(py, shared)?;
457 if crate::pickle::reserve_mesh_reference_if_active(shared.clone_ref(py)) {
458 let pop_fn = py
459 .import("monarch._rust_bindings.monarch_hyperactor.pickle")?
460 .getattr("pop_mesh_reference")?;
461 return Ok((pop_fn, PyTuple::empty(py).into_any()));
462 }
463 let block_on = shared_class(py).getattr("block_on")?;
465 let args = PyTuple::new(py, [shared])?;
466 Ok((block_on, args.into_any()))
467 }
468 }
469 }
470
471 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
472 Err(pyo3::exceptions::PyRuntimeError::new_err(
476 "pending actor mesh has no serializable ref; it is carried via the reserve/fill slot",
477 ))
478 }
479
480 fn stop(&self, instance: &PyInstance, reason: String) -> PyResult<PyPythonTask> {
481 let mesh = self.mesh.clone();
482 let instance = monarch_with_gil_blocking(GilSite::Stop, |_py| instance.clone());
483 let (tx, rx) = tokio::sync::oneshot::channel();
484 self.push(async move {
485 let result =
486 async move { mesh.await?.stop(&instance, reason)?.take_task()?.await }.await;
487 if tx.send(result).is_err() {
488 panic!("oneshot failed");
489 }
490 });
491 PyPythonTask::new(async move { rx.await.map_err(anyhow::Error::from)? })
492 }
493
494 fn initialized<'py>(&self) -> PyResult<PyPythonTask> {
495 let mesh = self.mesh.clone();
496 PyPythonTask::new(async {
497 mesh.await?;
498 Ok(None::<()>)
499 })
500 }
501
502 fn name(&self) -> PyResult<PyPythonTask> {
503 let mesh = self.mesh.clone();
504 let (tx, rx) = tokio::sync::oneshot::channel();
505 self.push(async move {
506 let result = async move { mesh.await?.name()?.take_task()?.await }.await;
507 if tx.send(result).is_err() {
508 panic!("oneshot failed");
509 }
510 });
511 PyPythonTask::new(async move { rx.await.map_err(anyhow::Error::from)? })
512 }
513}
514
515#[async_trait]
516impl Supervisable for AsyncActorMesh {
517 async fn supervision_event(&self, instance: &Instance<PythonActor>) -> Option<PyErr> {
518 if !self.supervised {
519 return None;
520 }
521 let mesh = self.mesh.clone();
522 match mesh.await {
523 Ok(mesh) => mesh.supervision_event(instance).await,
524 Err(e) => Some(e.into()),
525 }
526 }
527}
528
529impl SupervisableActorMesh for AsyncActorMesh {
530 fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
531 let mesh = self.mesh.clone();
532 let region = region.clone();
533 Ok(Box::new(AsyncActorMesh::new(
534 self.queue.clone(),
535 self.supervised,
536 async move { Ok(Arc::from(mesh.await?.new_with_region(®ion)?)) }
537 .boxed()
538 .shared(),
539 )))
540 }
541}
542
543#[derive(Debug, Clone)]
544#[pyclass(
545 name = "PyActorMesh",
546 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
547)]
548pub(crate) struct PyActorMesh {
549 mesh: ActorMesh<PythonActor>,
550}
551
552#[derive(Debug, Clone)]
553#[pyclass(
554 name = "PyActorMeshRef",
555 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
556)]
557pub(crate) struct PyActorMeshRef {
558 mesh: ActorMeshRef<PythonActor>,
559}
560
561#[derive(Debug, Clone)]
562#[pyclass(
563 name = "PythonActorMeshImpl",
564 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
565)]
566#[expect(
567 clippy::large_enum_variant,
568 reason = "PyO3 #[pyclass] enum; Box wrapping interacts with PyO3 codegen and Python interop — separate diff"
569)]
570pub(crate) enum PythonActorMeshImpl {
571 Owned(PyActorMesh),
572 Ref(PyActorMeshRef),
573}
574
575impl PythonActorMeshImpl {
576 pub(crate) fn new_owned(inner: ActorMesh<PythonActor>) -> Self {
578 PythonActorMeshImpl::Owned(PyActorMesh { mesh: inner })
579 }
580
581 pub(crate) fn new_ref(inner: ActorMeshRef<PythonActor>) -> Self {
583 PythonActorMeshImpl::Ref(PyActorMeshRef { mesh: inner })
584 }
585
586 fn mesh_ref(&self) -> &ActorMeshRef<PythonActor> {
587 match self {
588 PythonActorMeshImpl::Owned(inner) => &inner.mesh,
589 PythonActorMeshImpl::Ref(inner) => &inner.mesh,
590 }
591 }
592}
593
594#[async_trait]
595impl Supervisable for PythonActorMeshImpl {
596 async fn supervision_event(&self, instance: &Instance<PythonActor>) -> Option<PyErr> {
597 let mesh = self.mesh_ref();
598 match mesh.next_supervision_event(instance).await {
599 Ok(supervision_failure) => Some(SupervisionError::new_err_from(supervision_failure)),
600 Err(e) => Some(PyValueError::new_err(e.to_string())),
601 }
602 }
603}
604
605impl ActorMeshProtocol for PythonActorMeshImpl {
606 fn cast(
607 &self,
608 message: PythonMessage,
609 selection: AllOrChoose,
610 instance: &Instance<PythonActor>,
611 ) -> PyResult<()> {
612 <ActorMeshRef<PythonActor> as ActorMeshProtocol>::cast(
613 self.mesh_ref(),
614 message,
615 selection,
616 instance,
617 )
618 }
619
620 fn cast_with_headers(
621 &self,
622 message: PythonMessage,
623 selection: AllOrChoose,
624 instance: &Instance<PythonActor>,
625 caller_headers: hyperactor_config::Flattrs,
626 ) -> PyResult<()> {
627 <ActorMeshRef<PythonActor> as ActorMeshProtocol>::cast_with_headers(
628 self.mesh_ref(),
629 message,
630 selection,
631 instance,
632 caller_headers,
633 )
634 }
635
636 fn stop(&self, instance: &PyInstance, reason: String) -> PyResult<PyPythonTask> {
637 let (slf, instance) =
638 monarch_with_gil_blocking(GilSite::Stop, |_py| (self.clone(), instance.clone()));
639 match slf {
640 PythonActorMeshImpl::Owned(mut mesh) => PyPythonTask::new(async move {
641 mesh.mesh
642 .stop(instance.deref(), reason)
643 .await
644 .map_err(|err| PyValueError::new_err(err.to_string()))
645 }),
646 PythonActorMeshImpl::Ref(_) => Err(PyNotImplementedError::new_err(
647 "Cannot call stop on an ActorMeshRef, requires an owned ActorMesh",
648 )),
649 }
650 }
651
652 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
653 self.mesh_ref().__reduce__(py)
654 }
655
656 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
657 Ok(PythonActorMeshImpl::mesh_ref(self).clone())
659 }
660
661 fn name(&self) -> PyResult<PyPythonTask> {
662 let name = self.mesh_ref().id().to_string();
663 PyPythonTask::new(async move { Ok(name) })
664 }
665}
666
667impl SupervisableActorMesh for PythonActorMeshImpl {
668 fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
669 assert!(region.as_inner().is_subset(self.mesh_ref().region()));
670 Ok(Box::new(PythonActorMeshImpl::new_ref(
671 self.mesh_ref().sliced(region.as_inner().clone()),
672 )))
673 }
674}
675
676fn cast_error_to_py_error(err: hyperactor_mesh::Error) -> PyErr {
679 if let hyperactor_mesh::Error::Supervision(failure) = err {
680 SupervisionError::new_err_from(*failure)
681 } else {
682 PyRuntimeError::new_err(err.to_string())
683 }
684}
685
686impl ActorMeshProtocol for ActorMeshRef<PythonActor> {
687 fn cast(
688 &self,
689 message: PythonMessage,
690 selection: AllOrChoose,
691 instance: &Instance<PythonActor>,
692 ) -> PyResult<()> {
693 <Self as ActorMeshProtocol>::cast_with_headers(
694 self,
695 message,
696 selection,
697 instance,
698 hyperactor_config::Flattrs::new(),
699 )
700 }
701
702 fn cast_with_headers(
703 &self,
704 message: PythonMessage,
705 selection: AllOrChoose,
706 instance: &Instance<PythonActor>,
707 caller_headers: hyperactor_config::Flattrs,
708 ) -> PyResult<()> {
709 match selection {
710 AllOrChoose::All => ActorMeshRef::<PythonActor>::cast_with_headers(
711 self,
712 instance,
713 &caller_headers,
714 message,
715 )
716 .map_err(cast_error_to_py_error),
717 AllOrChoose::Choose => ActorMeshRef::<PythonActor>::cast_choose_with_headers(
718 self,
719 instance,
720 &caller_headers,
721 message,
722 )
723 .map_err(cast_error_to_py_error),
724 }
725 }
726
727 fn stop(&self, _instance: &PyInstance, _reason: String) -> PyResult<PyPythonTask> {
729 Err(PyNotImplementedError::new_err(
730 "This cannot be used on ActorMeshRef, only on owned ActorMesh",
731 ))
732 }
733
734 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
735 if crate::pickle::push_mesh_reference_if_active(crate::actor::MeshRef::Actor(Box::new(
736 self.clone(),
737 ))) {
738 let pop_fn = py
739 .import("monarch._rust_bindings.monarch_hyperactor.pickle")?
740 .getattr("pop_mesh_reference")?;
741 return Ok((pop_fn, pyo3::types::PyTuple::empty(py).into_any()));
742 }
743 let bytes = bincode::serde::encode_to_vec(self, bincode::config::legacy())
744 .map_err(|e| PyValueError::new_err(e.to_string()))?;
745 let py_bytes = (PyBytes::new(py, &bytes),).into_bound_py_any(py).unwrap();
746 let module = py
747 .import("monarch._rust_bindings.monarch_hyperactor.actor_mesh")
748 .unwrap();
749 let from_bytes = module.getattr("py_actor_mesh_from_bytes").unwrap();
750 Ok((from_bytes, py_bytes))
751 }
752
753 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
754 Ok(self.clone())
755 }
756
757 fn name(&self) -> PyResult<PyPythonTask> {
758 let name = self.id().to_string();
759 PyPythonTask::new(async move { Ok(name) })
760 }
761}
762
763#[pymethods]
764impl PythonActorMeshImpl {
765 fn get(&self, rank: usize) -> PyResult<Option<PyActorAddr>> {
766 Ok(self
767 .mesh_ref()
768 .get(rank)
769 .map(|r| hyperactor::ActorRef::into_actor_addr(r.clone()))
770 .map(PyActorAddr::from))
771 }
772
773 fn __repr__(&self) -> String {
774 format!("PythonActorMeshImpl({:?})", self.mesh_ref())
775 }
776}
777
778#[pyfunction]
779fn py_actor_mesh_from_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult<PythonActorMesh> {
780 let r: PyResult<ActorMeshRef<PythonActor>> =
781 bincode::serde::decode_from_slice(bytes.as_bytes(), bincode::config::legacy())
782 .map(|(v, _)| v)
783 .map_err(|e| PyValueError::new_err(e.to_string()));
784 r.map(|r| AsyncActorMesh::from_impl(Arc::new(PythonActorMeshImpl::new_ref(r))))
785 .map(|r| PythonActorMesh::from_impl(Arc::from(r)))
786}
787
788#[pyclass(
789 name = "ActorSupervisionEvent",
790 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
791)]
792#[derive(Debug)]
793pub struct PyActorSupervisionEvent {
794 inner: ActorSupervisionEvent,
795}
796
797#[pymethods]
798impl PyActorSupervisionEvent {
799 pub(crate) fn __repr__(&self) -> PyResult<String> {
800 Ok(format!("<PyActorSupervisionEvent: {}>", self.inner))
801 }
802
803 #[getter]
804 pub(crate) fn actor_id(&self) -> PyResult<PyActorAddr> {
805 Ok(PyActorAddr::from(self.inner.actor_id.clone()))
806 }
807
808 #[getter]
809 pub(crate) fn actor_status(&self) -> PyResult<String> {
810 Ok(self.inner.actor_status.to_string())
811 }
812}
813
814impl From<ActorSupervisionEvent> for PyActorSupervisionEvent {
815 fn from(event: ActorSupervisionEvent) -> Self {
816 PyActorSupervisionEvent { inner: event }
817 }
818}
819
820#[pyfunction]
821fn py_identity(obj: Py<PyAny>) -> PyResult<Py<PyAny>> {
822 Ok(obj)
823}
824
825#[pyfunction]
841#[pyo3(name = "hold_gil_for_test", signature = (delay_secs, hold_secs))]
842pub fn hold_gil_for_test(delay_secs: f64, hold_secs: f64) {
843 thread::spawn(move || {
844 thread::sleep(Duration::from_secs_f64(delay_secs));
846 monarch_with_gil_blocking(GilSite::Test, |_py| {
848 tracing::info!("start holding the gil...");
849 thread::sleep(Duration::from_secs_f64(hold_secs));
850 tracing::info!("end holding the gil...");
851 });
852 });
853}
854
855pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
856 py_module_add_function!(
857 hyperactor_mod,
858 "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
859 py_identity
860 );
861 py_module_add_function!(
862 hyperactor_mod,
863 "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
864 py_actor_mesh_from_bytes
865 );
866 py_module_add_function!(
867 hyperactor_mod,
868 "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
869 hold_gil_for_test
870 );
871 hyperactor_mod.add_class::<PythonActorMesh>()?;
872 hyperactor_mod.add_class::<PythonActorMeshImpl>()?;
873 hyperactor_mod.add_class::<PyActorSupervisionEvent>()?;
874 Ok(())
875}
876
877#[cfg(test)]
878mod tests {
879 use std::sync::OnceLock;
880 use std::time::Duration;
881
882 use async_trait::async_trait;
883 use hyperactor::Actor;
884 use hyperactor::Context;
885 use hyperactor::Endpoint as _;
886 use hyperactor::Handler;
887 use hyperactor::Instance;
888 use hyperactor::Proc;
889 use hyperactor::actor::Signal;
890 use hyperactor::channel::ChannelTransport;
891 use hyperactor::mailbox;
892 use hyperactor::proc::ActorWorkReceiver;
893 use hyperactor::supervision::ActorSupervisionEvent;
894 use hyperactor_mesh::host_mesh::HostMesh;
895 use hyperactor_mesh::mesh_controller::GetSubscriberCount;
896 use hyperactor_mesh::supervision::MeshFailure;
897 use monarch_types::PickledPyObject;
898 use ndslice::extent;
899 use tokio::sync::mpsc;
900
901 use super::*;
902 use crate::actor::PythonActor;
903 use crate::actor::PythonActorParams;
904 use crate::config::ACTOR_QUEUE_DISPATCH;
905
906 #[derive(Debug)]
909 struct TestClient {
910 signal_rx: mpsc::UnboundedReceiver<Signal>,
911 supervision_rx: mpsc::UnboundedReceiver<ActorSupervisionEvent>,
912 work_rx: ActorWorkReceiver<Self>,
913 }
914
915 impl Actor for TestClient {}
916
917 #[async_trait]
918 impl Handler<MeshFailure> for TestClient {
919 async fn handle(
920 &mut self,
921 _cx: &Context<Self>,
922 msg: MeshFailure,
923 ) -> Result<(), anyhow::Error> {
924 panic!("unexpected supervision failure in test: {}", msg);
925 }
926 }
927
928 impl TestClient {
929 fn run(mut self, instance: &'static Instance<Self>) {
930 tokio::spawn(async move {
931 loop {
932 tokio::select! {
933 work = self.work_rx.recv() => {
934 match work {
935 Some(work) => {
936 let _ = work.handle(&mut self, instance).await;
937 }
938 None => break,
939 }
940 }
941 Some(_) = self.signal_rx.recv() => {}
942 Some(event) = self.supervision_rx.recv() => {
943 let _ = instance
944 .handle_supervision_event(&mut self, event)
945 .await;
946 }
947 }
948 }
949 });
950 }
951 }
952
953 fn init_test_instance() -> &'static Instance<TestClient> {
954 static INSTANCE: OnceLock<Instance<TestClient>> = OnceLock::new();
955 let proc = Proc::direct(ChannelTransport::Unix.any(), "test_proc".to_string()).unwrap();
956 let ai = proc.actor_instance("test_client").unwrap();
957
958 INSTANCE
959 .set(ai.instance)
960 .map_err(|_| "already initialized")
961 .unwrap();
962 let instance = INSTANCE.get().unwrap();
963
964 TestClient {
965 signal_rx: ai.signal,
966 supervision_rx: ai.supervision,
967 work_rx: ai.work,
968 }
969 .run(instance);
970
971 instance
972 }
973
974 fn test_instance() -> &'static Instance<TestClient> {
975 static INSTANCE: OnceLock<&'static Instance<TestClient>> = OnceLock::new();
976 INSTANCE.get_or_init(init_test_instance)
977 }
978
979 #[tokio::test]
984 async fn test_subscriber_count_stable_across_supervision_calls() {
985 crate::pytokio::ensure_python();
986
987 let instance = test_instance();
988
989 let mut host_mesh = HostMesh::local_in_process().await.unwrap();
990 let proc_mesh = host_mesh
991 .spawn(instance, "test", extent!(replicas = 2), None, None)
992 .await
993 .unwrap();
994
995 let pickled_type = monarch_with_gil_blocking(GilSite::Test, |py| {
999 py.run(c"class MinimalActor: pass", None, None).unwrap();
1000
1001 PickledPyObject::pickle(
1002 &py.import("__main__")
1003 .unwrap()
1004 .getattr("MinimalActor")
1005 .unwrap(),
1006 )
1007 .unwrap()
1008 });
1009
1010 let actor_params = PythonActorParams::new(pickled_type, None, None);
1011 let config_lock = hyperactor_config::global::lock();
1012 let _queue_dispatch_guard = config_lock.override_key(ACTOR_QUEUE_DISPATCH, false);
1013 let actor_mesh = proc_mesh
1014 .spawn::<PythonActor, _>(instance, "test_actors", &actor_params)
1015 .await
1016 .unwrap();
1017 drop(_queue_dispatch_guard);
1018 drop(config_lock);
1019
1020 let controller = actor_mesh.controller().as_ref().unwrap().clone();
1021
1022 let mesh_impl =
1024 async move { Ok::<_, PyErr>(Box::new(PythonActorMeshImpl::new_owned(actor_mesh))) };
1025 let python_actor_mesh = PythonActorMesh::new(
1026 async move {
1027 let mesh_impl: Box<dyn SupervisableActorMesh> = mesh_impl.await?;
1028 Ok(mesh_impl)
1029 },
1030 true,
1031 );
1032
1033 let py_ai = Proc::direct(ChannelTransport::Unix.any(), "py_proc".to_string())
1037 .unwrap()
1038 .actor_instance::<PythonActor>("py_client")
1039 .unwrap();
1040 let py_instance = py_ai.instance;
1041
1042 let (port, mut rx) = mailbox::open_port::<usize>(instance);
1044 controller.post(instance, GetSubscriberCount(port.bind()));
1045 let initial_count = tokio::time::timeout(Duration::from_secs(5), rx.recv())
1046 .await
1047 .expect("timed out waiting for subscriber count")
1048 .expect("channel closed");
1049 assert_eq!(initial_count, 0, "should have 0 subscribers initially");
1050
1051 for _ in 0..5 {
1056 tokio::select! {
1057 _ = python_actor_mesh.inner.supervision_event(&py_instance) => {
1058 panic!("unexpected supervision event on healthy mesh");
1059 }
1060 _ = tokio::time::sleep(Duration::from_millis(200)) => {}
1061 }
1062 }
1063
1064 let (port, mut rx) = mailbox::open_port::<usize>(instance);
1067 controller.post(instance, GetSubscriberCount(port.bind()));
1068 let after_count = tokio::time::timeout(Duration::from_secs(5), rx.recv())
1069 .await
1070 .expect("timed out waiting for subscriber count")
1071 .expect("channel closed");
1072 assert_eq!(
1073 after_count, 1,
1074 "subscriber count should be exactly 1, not growing with each call"
1075 );
1076
1077 for _ in 0..5 {
1079 tokio::select! {
1080 _ = python_actor_mesh.inner.supervision_event(&py_instance) => {
1081 panic!("unexpected supervision event on healthy mesh");
1082 }
1083 _ = tokio::time::sleep(Duration::from_millis(200)) => {}
1084 }
1085 }
1086
1087 let (port, mut rx) = mailbox::open_port::<usize>(instance);
1088 controller.post(instance, GetSubscriberCount(port.bind()));
1089 let final_count = tokio::time::timeout(Duration::from_secs(5), rx.recv())
1090 .await
1091 .expect("timed out waiting for subscriber count")
1092 .expect("channel closed");
1093 assert_eq!(
1094 final_count, 1,
1095 "subscriber count should still be 1 after repeated calls"
1096 );
1097
1098 let _ = host_mesh.shutdown(instance).await;
1099 }
1100}