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::BoxFuture;
19use futures::future::FutureExt;
20use futures::future::Shared;
21use hyperactor::Instance;
22use hyperactor::RemoteEndpoint as _;
23use hyperactor::supervision::ActorSupervisionEvent;
24use hyperactor_mesh::actor_mesh::ActorMesh;
25use hyperactor_mesh::actor_mesh::ActorMeshRef;
26use monarch_types::py_global;
27use monarch_types::py_module_add_function;
28use ndslice::view::Ranked;
29use ndslice::view::RankedSliceable;
30use pyo3::IntoPyObjectExt;
31use pyo3::exceptions::PyNotImplementedError;
32use pyo3::exceptions::PyRuntimeError;
33use pyo3::exceptions::PyValueError;
34use pyo3::prelude::*;
35use pyo3::types::PyBytes;
36use pyo3::types::PyTuple;
37use rand::RngExt as _;
38use tokio::sync::mpsc::UnboundedSender;
39use tokio::sync::mpsc::unbounded_channel;
40use tracing::Instrument;
41
42use crate::actor::PythonActor;
43use crate::actor::PythonMessage;
44use crate::actor::PythonMessageKind;
45use crate::context::PyInstance;
46use crate::handle::PyHandle;
47use crate::pickle::PendingMessage;
48use crate::proc::PyActorAddr;
49use crate::pytokio::PyPythonTask;
50use crate::runtime::GilSite;
51use crate::runtime::get_tokio_runtime;
52use crate::runtime::monarch_with_gil;
53use crate::runtime::monarch_with_gil_blocking;
54use crate::shape::PyRegion;
55use crate::supervision::Supervisable;
56use crate::supervision::SupervisionError;
57
58py_global!(_pickle, "monarch._src.actor.actor_mesh", "_pickle");
59
60py_global!(
61 shared_class,
62 "monarch._rust_bindings.monarch_hyperactor.pytokio",
63 "Shared"
64);
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub(crate) enum AllOrChoose {
73 All,
74 Choose,
75}
76
77impl AllOrChoose {
78 pub(crate) fn as_str(self) -> &'static str {
79 match self {
80 Self::All => "all",
81 Self::Choose => "choose",
82 }
83 }
84}
85
86pub(crate) trait ActorMeshProtocol: Send + Sync {
89 fn cast(
91 &self,
92 message: PythonMessage,
93 selection: AllOrChoose,
94 instance: &Instance<PythonActor>,
95 ) -> PyResult<()>;
96
97 fn cast_with_headers(
104 &self,
105 message: PythonMessage,
106 selection: AllOrChoose,
107 instance: &Instance<PythonActor>,
108 _caller_headers: hyperactor_config::Flattrs,
109 ) -> PyResult<()> {
110 self.cast(message, selection, instance)
111 }
112
113 fn cast_unresolved(
118 &self,
119 message: PendingMessage,
120 selection: AllOrChoose,
121 instance: &Instance<PythonActor>,
122 ) -> PyResult<()> {
123 let message = get_tokio_runtime().block_on(message.resolve())?;
124 self.cast(message, selection, instance)
125 }
126
127 fn cast_unresolved_with_headers(
135 &self,
136 message: PendingMessage,
137 selection: AllOrChoose,
138 instance: &Instance<PythonActor>,
139 caller_headers: hyperactor_config::Flattrs,
140 ) -> PyResult<()> {
141 let message = get_tokio_runtime().block_on(message.resolve())?;
142 self.cast_with_headers(message, selection, instance, caller_headers)
143 }
144
145 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>;
146
147 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>>;
151
152 fn stop(
157 &self,
158 _instance: &PyInstance,
159 _reason: String,
160 ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
161 Err(PyNotImplementedError::new_err(format!(
162 "stop() is not supported for {}",
163 std::any::type_name::<Self>()
164 )))
165 }
166
167 fn initialized(&self) -> BoxFuture<'static, PyResult<Option<()>>> {
173 future::ready(Ok(None)).boxed()
174 }
175
176 fn name(&self) -> BoxFuture<'static, PyResult<String>>;
178}
179
180pub(crate) trait SupervisableActorMesh: ActorMeshProtocol + Supervisable {
181 fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>>;
182}
183
184#[pyclass(
186 name = "PythonActorMesh",
187 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
188)]
189#[derive(Clone)]
190pub(crate) struct PythonActorMesh {
191 inner: Arc<dyn SupervisableActorMesh>,
192}
193
194impl PythonActorMesh {
195 pub(crate) fn new<F>(f: F, supervised: bool) -> Self
196 where
197 F: Future<Output = PyResult<Box<dyn SupervisableActorMesh>>> + Send + 'static,
198 {
199 let f = async move { Ok(Arc::from(f.await?)) }.boxed().shared();
200 PythonActorMesh {
201 inner: Arc::new(AsyncActorMesh::new_queue(f, supervised)),
202 }
203 }
204
205 pub(crate) fn from_impl(inner: Arc<dyn SupervisableActorMesh>) -> Self {
206 PythonActorMesh { inner }
207 }
208
209 pub(crate) fn get_inner(&self) -> Arc<dyn SupervisableActorMesh> {
210 self.inner.clone()
211 }
212}
213
214pub(crate) fn to_all_or_choose(selection: &str) -> PyResult<AllOrChoose> {
215 match selection {
216 "choose" => Ok(AllOrChoose::Choose),
217 "all" => Ok(AllOrChoose::All),
218 _ => Err(PyErr::new::<PyValueError, _>(format!(
219 "Invalid selection: {}",
220 selection
221 ))),
222 }
223}
224
225#[pymethods]
226impl PythonActorMesh {
227 #[tracing::instrument(level = "debug", skip_all)]
228 #[pyo3(name = "cast")]
229 fn py_cast(
230 &self,
231 message: &PythonMessage,
232 selection: &str,
233 instance: &PyInstance,
234 ) -> PyResult<()> {
235 let sel = to_all_or_choose(selection)?;
236 self.inner.cast(message.clone(), sel, instance.deref())
237 }
238
239 #[hyperactor::instrument]
240 pub(crate) fn cast_unresolved(
241 &self,
242 message: &mut PendingMessage,
243 selection: &str,
244 instance: &PyInstance,
245 ) -> PyResult<()> {
246 let sel = to_all_or_choose(selection)?;
247 let message = message.take()?;
248 self.inner.cast_unresolved(message, sel, instance)
249 }
250
251 fn new_with_region(&self, region: &PyRegion) -> PyResult<PythonActorMesh> {
252 let inner = self.inner.new_with_region(region)?;
253 Ok(PythonActorMesh {
254 inner: Arc::from(inner),
255 })
256 }
257
258 fn stop(&self, instance: &PyInstance, reason: String) -> PyResult<PyHandle> {
262 self.inner.stop(instance, reason).map(PyHandle::spawn)
263 }
264
265 fn initialized(&self) -> PyHandle {
268 PyHandle::spawn(self.inner.initialized())
269 }
270
271 fn name(&self) -> PyHandle {
274 PyHandle::spawn(self.inner.name())
275 }
276
277 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
278 self.inner.__reduce__(py)
279 }
280}
281
282#[derive(Debug)]
283pub(crate) struct ClonePyErr {
284 inner: PyErr,
285}
286
287impl From<ClonePyErr> for PyErr {
288 fn from(value: ClonePyErr) -> PyErr {
289 value.inner
290 }
291}
292impl From<PyErr> for ClonePyErr {
293 fn from(inner: PyErr) -> ClonePyErr {
294 ClonePyErr { inner }
295 }
296}
297
298impl Clone for ClonePyErr {
299 fn clone(&self) -> Self {
300 monarch_with_gil_blocking(GilSite::Convert, |py| self.inner.clone_ref(py).into())
301 }
302}
303
304type ActorMeshResult = Result<Arc<dyn SupervisableActorMesh>, ClonePyErr>;
305type ActorMeshFut = Shared<Pin<Box<dyn Future<Output = ActorMeshResult> + Send + 'static>>>;
306
307pub(crate) struct AsyncActorMesh {
308 mesh: ActorMeshFut,
309 queue: UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
310 supervised: bool,
311}
312
313impl AsyncActorMesh {
314 pub(crate) fn new_queue(f: ActorMeshFut, supervised: bool) -> AsyncActorMesh {
315 let (queue, mut recv) = unbounded_channel();
316
317 get_tokio_runtime().spawn(async move {
318 loop {
319 let r = recv.recv().await;
320 if let Some(r) = r {
321 r.await;
322 } else {
323 return;
324 }
325 }
326 });
327
328 let mesh = AsyncActorMesh::new(queue, supervised, f);
329 let f = mesh.mesh.clone();
337 mesh.push(async move {
338 let _ = f.await;
339 });
340 mesh
341 }
342
343 fn new(
344 queue: UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
345 supervised: bool,
346 f: ActorMeshFut,
347 ) -> AsyncActorMesh {
348 AsyncActorMesh {
349 mesh: f,
350 queue,
351 supervised,
352 }
353 }
354
355 fn push<F>(&self, f: F)
356 where
357 F: Future<Output = ()> + Send + 'static,
358 {
359 self.queue.send(f.boxed()).unwrap();
360 }
361
362 pub(crate) fn from_impl(mesh: Arc<dyn SupervisableActorMesh>) -> Self {
363 let fut = future::ready(Ok::<Arc<dyn SupervisableActorMesh>, ClonePyErr>(mesh))
364 .boxed()
365 .shared();
366 let _ = futures::executor::block_on(fut.clone());
368 Self::new_queue(fut, true)
369 }
370}
371
372impl ActorMeshProtocol for AsyncActorMesh {
373 fn cast(
374 &self,
375 _message: PythonMessage,
376 _selection: AllOrChoose,
377 _instance: &Instance<PythonActor>,
378 ) -> PyResult<()> {
379 panic!("not implemented")
380 }
381
382 fn cast_unresolved(
383 &self,
384 message: PendingMessage,
385 selection: AllOrChoose,
386 instance: &Instance<PythonActor>,
387 ) -> PyResult<()> {
388 self.cast_unresolved_with_headers(
389 message,
390 selection,
391 instance,
392 hyperactor_config::Flattrs::new(),
393 )
394 }
395
396 fn cast_unresolved_with_headers(
400 &self,
401 message: PendingMessage,
402 selection: AllOrChoose,
403 instance: &Instance<PythonActor>,
404 caller_headers: hyperactor_config::Flattrs,
405 ) -> PyResult<()> {
406 let mesh = self.mesh.clone();
407 let instance = instance.clone_for_py();
408 let port = match &message.kind {
409 PythonMessageKind::CallMethod { response_port, .. } => response_port.clone(),
410 _ => None,
411 };
412 self.push(
413 async move {
414 let result = async {
415 let resolved = match message.try_resolve_now()? {
416 Ok(resolved) => resolved,
417 Err(message) => message.resolve().await?,
418 };
419 mesh.await?
420 .cast_with_headers(resolved, selection, &instance, caller_headers)
421 }
422 .await;
423 if let (Some(mut port_ref), Err(pyerr)) = (port, result) {
424 let _ = monarch_with_gil(GilSite::Traceback, |py: Python<'_>| {
425 let exception_str = crate::logging::format_traceback(py, &pyerr);
426 tracing::error!(
427 actor_id = instance.self_addr().to_string(),
428 "error occurred during cast unresolved: {}",
429 exception_str
430 );
431
432 port_ref.set_return_undeliverable(false);
452
453 let mut state = crate::pickle::pickle(
454 py,
455 pyerr.into_value(py).into_any(),
456 false,
457 false,
458 )?;
459 let _ = port_ref.post(
460 &instance,
461 PythonMessage::new_from_buf(
462 PythonMessageKind::Exception { rank: Some(0) },
463 state.take_inner()?.take_buffer(),
464 ),
465 );
466
467 Ok::<_, PyErr>(())
468 })
469 .await;
470 }
471 }
472 .instrument(tracing::debug_span!("AsyncActorMesh::cast")),
473 );
474 Ok(())
475 }
476
477 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
478 let fut = self.mesh.clone();
479 match fut.peek().cloned() {
480 Some(mesh) => mesh?.__reduce__(py),
481 None => {
482 let observer = async move { Ok(PythonActorMesh::from_impl(fut.await?)) };
483 #[cfg(test)]
487 let observer = tests::probe_reduction_observer(observer);
488 let shared = PyPythonTask::new(observer)?.spawn_abortable()?;
489 let shared = Py::new(py, shared)?;
490 if crate::pickle::reserve_mesh_reference_if_active(shared.clone_ref(py)) {
491 let pop_fn = py
492 .import("monarch._rust_bindings.monarch_hyperactor.pickle")?
493 .getattr("pop_mesh_reference")?;
494 return Ok((pop_fn, PyTuple::empty(py).into_any()));
495 }
496 let block_on = shared_class(py).getattr("block_on")?;
498 let args = PyTuple::new(py, [shared])?;
499 Ok((block_on, args.into_any()))
500 }
501 }
502 }
503
504 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
505 Err(pyo3::exceptions::PyRuntimeError::new_err(
509 "pending actor mesh has no serializable ref; it is carried via the reserve/fill slot",
510 ))
511 }
512
513 fn stop(
519 &self,
520 instance: &PyInstance,
521 reason: String,
522 ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
523 let mesh = self.mesh.clone();
525 let instance = monarch_with_gil_blocking(GilSite::Stop, |_py| instance.clone());
526 let (tx, rx) = tokio::sync::oneshot::channel();
529 self.push(async move {
530 let result = async move {
534 let mesh = mesh.await?;
536 let completion = mesh.stop(&instance, reason)?;
539 completion.await
541 }
542 .await;
543 let _ = tx.send(result);
547 });
548 Ok(async move { rx.await.map_err(anyhow::Error::from)? }.boxed())
549 }
550
551 fn initialized(&self) -> BoxFuture<'static, PyResult<Option<()>>> {
554 let mesh = self.mesh.clone();
555 async move {
556 mesh.await?;
559 Ok(None::<()>)
560 }
561 .boxed()
562 }
563
564 fn name(&self) -> BoxFuture<'static, PyResult<String>> {
567 let mesh = self.mesh.clone();
568 async move { mesh.await?.name().await }.boxed()
569 }
570}
571
572#[async_trait]
573impl Supervisable for AsyncActorMesh {
574 async fn supervision_event(&self, instance: &Instance<PythonActor>) -> Option<PyErr> {
575 if !self.supervised {
576 return None;
577 }
578 let mesh = self.mesh.clone();
579 match mesh.await {
580 Ok(mesh) => mesh.supervision_event(instance).await,
581 Err(e) => Some(e.into()),
582 }
583 }
584}
585
586impl SupervisableActorMesh for AsyncActorMesh {
587 fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
588 let mesh = self.mesh.clone();
589 let region = region.clone();
590 Ok(Box::new(AsyncActorMesh::new(
591 self.queue.clone(),
592 self.supervised,
593 async move { Ok(Arc::from(mesh.await?.new_with_region(®ion)?)) }
594 .boxed()
595 .shared(),
596 )))
597 }
598}
599
600#[derive(Debug, Clone)]
601#[pyclass(
602 name = "PyActorMesh",
603 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
604)]
605pub(crate) struct PyActorMesh {
606 mesh: ActorMesh<PythonActor>,
607}
608
609#[derive(Debug, Clone)]
610#[pyclass(
611 name = "PyActorMeshRef",
612 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
613)]
614pub(crate) struct PyActorMeshRef {
615 mesh: ActorMeshRef<PythonActor>,
616}
617
618#[derive(Debug, Clone)]
619#[pyclass(
620 name = "PythonActorMeshImpl",
621 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
622)]
623pub(crate) enum PythonActorMeshImpl {
624 Owned(PyActorMesh),
625 Ref(PyActorMeshRef),
626}
627
628impl PythonActorMeshImpl {
629 pub(crate) fn new_owned(inner: ActorMesh<PythonActor>) -> Self {
631 PythonActorMeshImpl::Owned(PyActorMesh { mesh: inner })
632 }
633
634 pub(crate) fn new_ref(inner: ActorMeshRef<PythonActor>) -> Self {
636 PythonActorMeshImpl::Ref(PyActorMeshRef { mesh: inner })
637 }
638
639 fn mesh_ref(&self) -> &ActorMeshRef<PythonActor> {
640 match self {
641 PythonActorMeshImpl::Owned(inner) => &inner.mesh,
642 PythonActorMeshImpl::Ref(inner) => &inner.mesh,
643 }
644 }
645}
646
647#[async_trait]
648impl Supervisable for PythonActorMeshImpl {
649 async fn supervision_event(&self, instance: &Instance<PythonActor>) -> Option<PyErr> {
650 let mesh = self.mesh_ref();
651 match mesh.next_supervision_event(instance).await {
652 Ok(supervision_failure) => Some(SupervisionError::new_err_from(supervision_failure)),
653 Err(e) => Some(PyValueError::new_err(e.to_string())),
654 }
655 }
656}
657
658impl ActorMeshProtocol for PythonActorMeshImpl {
659 fn cast(
660 &self,
661 message: PythonMessage,
662 selection: AllOrChoose,
663 instance: &Instance<PythonActor>,
664 ) -> PyResult<()> {
665 <ActorMeshRef<PythonActor> as ActorMeshProtocol>::cast(
666 self.mesh_ref(),
667 message,
668 selection,
669 instance,
670 )
671 }
672
673 fn cast_with_headers(
674 &self,
675 message: PythonMessage,
676 selection: AllOrChoose,
677 instance: &Instance<PythonActor>,
678 caller_headers: hyperactor_config::Flattrs,
679 ) -> PyResult<()> {
680 <ActorMeshRef<PythonActor> as ActorMeshProtocol>::cast_with_headers(
681 self.mesh_ref(),
682 message,
683 selection,
684 instance,
685 caller_headers,
686 )
687 }
688
689 fn stop(
694 &self,
695 instance: &PyInstance,
696 reason: String,
697 ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
698 let (slf, instance) =
699 monarch_with_gil_blocking(GilSite::Stop, |_py| (self.clone(), instance.clone()));
700 match slf {
701 PythonActorMeshImpl::Owned(mut mesh) => Ok(async move {
702 mesh.mesh
703 .stop(instance.deref(), reason)
704 .await
705 .map_err(|err| PyValueError::new_err(err.to_string()))
706 }
707 .boxed()),
708 PythonActorMeshImpl::Ref(_) => Err(PyNotImplementedError::new_err(
709 "Cannot call stop on an ActorMeshRef, requires an owned ActorMesh",
710 )),
711 }
712 }
713
714 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
715 self.mesh_ref().__reduce__(py)
716 }
717
718 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
719 Ok(PythonActorMeshImpl::mesh_ref(self).clone())
721 }
722
723 fn name(&self) -> BoxFuture<'static, PyResult<String>> {
727 <ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(self.mesh_ref())
728 }
729}
730
731impl SupervisableActorMesh for PythonActorMeshImpl {
732 fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
733 assert!(region.as_inner().is_subset(self.mesh_ref().region()));
734 Ok(Box::new(PythonActorMeshImpl::new_ref(
735 self.mesh_ref().sliced(region.as_inner().clone()),
736 )))
737 }
738}
739
740fn cast_error_to_py_error(err: hyperactor_mesh::Error) -> PyErr {
741 PyRuntimeError::new_err(err.to_string())
742}
743
744impl ActorMeshProtocol for ActorMeshRef<PythonActor> {
745 fn cast(
746 &self,
747 message: PythonMessage,
748 selection: AllOrChoose,
749 instance: &Instance<PythonActor>,
750 ) -> PyResult<()> {
751 <Self as ActorMeshProtocol>::cast_with_headers(
752 self,
753 message,
754 selection,
755 instance,
756 hyperactor_config::Flattrs::new(),
757 )
758 }
759
760 fn cast_with_headers(
761 &self,
762 message: PythonMessage,
763 selection: AllOrChoose,
764 instance: &Instance<PythonActor>,
765 caller_headers: hyperactor_config::Flattrs,
766 ) -> PyResult<()> {
767 match selection {
768 AllOrChoose::All => ActorMeshRef::<PythonActor>::cast_with_headers(
769 self,
770 instance,
771 &caller_headers,
772 message,
773 )
774 .map_err(cast_error_to_py_error),
775 AllOrChoose::Choose => match self {
776 ActorMeshRef::Managed(mesh) => mesh
777 .cast_choose_with_headers(instance, &caller_headers, message)
778 .map_err(cast_error_to_py_error),
779 ActorMeshRef::Data(mesh) => {
780 let num_ranks = mesh.region().num_ranks();
781 if num_ranks > 0 {
782 let actor = mesh
783 .get(rand::rng().random_range(0..num_ranks))
784 .expect("selected rank should exist in dense data mesh");
785 actor.post_with_headers(instance, caller_headers, message);
786 }
787 Ok(())
788 }
789 },
790 }
791 }
792
793 fn stop(
796 &self,
797 _instance: &PyInstance,
798 _reason: String,
799 ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
800 Err(PyNotImplementedError::new_err(
801 "This cannot be used on ActorMeshRef, only on owned ActorMesh",
802 ))
803 }
804
805 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
806 if crate::pickle::push_mesh_reference_if_active(crate::actor::MeshRef::Actor(Box::new(
807 self.clone(),
808 ))) {
809 let pop_fn = py
810 .import("monarch._rust_bindings.monarch_hyperactor.pickle")?
811 .getattr("pop_mesh_reference")?;
812 return Ok((pop_fn, pyo3::types::PyTuple::empty(py).into_any()));
813 }
814 let bytes = bincode::serde::encode_to_vec(self, bincode::config::legacy())
815 .map_err(|e| PyValueError::new_err(e.to_string()))?;
816 let py_bytes = (PyBytes::new(py, &bytes),).into_bound_py_any(py).unwrap();
817 let module = py
818 .import("monarch._rust_bindings.monarch_hyperactor.actor_mesh")
819 .unwrap();
820 let from_bytes = module.getattr("py_actor_mesh_from_bytes").unwrap();
821 Ok((from_bytes, py_bytes))
822 }
823
824 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
825 Ok(self.clone())
826 }
827
828 fn name(&self) -> BoxFuture<'static, PyResult<String>> {
831 let name = self.id().to_string();
832 future::ready(Ok(name)).boxed()
833 }
834}
835
836#[pymethods]
837impl PythonActorMeshImpl {
838 fn get(&self, rank: usize) -> PyResult<Option<PyActorAddr>> {
839 Ok(self
840 .mesh_ref()
841 .get(rank)
842 .map(|r| hyperactor::ActorRef::into_actor_addr(r.clone()))
843 .map(PyActorAddr::from))
844 }
845
846 fn __repr__(&self) -> String {
847 format!("PythonActorMeshImpl({:?})", self.mesh_ref())
848 }
849}
850
851#[pyfunction]
852fn py_actor_mesh_from_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult<PythonActorMesh> {
853 let r: PyResult<ActorMeshRef<PythonActor>> =
854 bincode::serde::decode_from_slice(bytes.as_bytes(), bincode::config::legacy())
855 .map(|(v, _)| v)
856 .map_err(|e| PyValueError::new_err(e.to_string()));
857 r.map(|r| AsyncActorMesh::from_impl(Arc::new(PythonActorMeshImpl::new_ref(r))))
858 .map(|r| PythonActorMesh::from_impl(Arc::from(r)))
859}
860
861#[pyclass(
862 name = "ActorSupervisionEvent",
863 module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
864)]
865#[derive(Debug)]
866pub struct PyActorSupervisionEvent {
867 inner: ActorSupervisionEvent,
868}
869
870#[pymethods]
871impl PyActorSupervisionEvent {
872 pub(crate) fn __repr__(&self) -> PyResult<String> {
873 Ok(format!("<PyActorSupervisionEvent: {}>", self.inner))
874 }
875
876 #[getter]
877 pub(crate) fn actor_id(&self) -> PyResult<PyActorAddr> {
878 Ok(PyActorAddr::from(self.inner.actor_id.clone()))
879 }
880
881 #[getter]
882 pub(crate) fn actor_status(&self) -> PyResult<String> {
883 Ok(self.inner.actor_status.to_string())
884 }
885}
886
887impl From<ActorSupervisionEvent> for PyActorSupervisionEvent {
888 fn from(event: ActorSupervisionEvent) -> Self {
889 PyActorSupervisionEvent { inner: event }
890 }
891}
892
893#[pyfunction]
894fn py_identity(obj: Py<PyAny>) -> PyResult<Py<PyAny>> {
895 Ok(obj)
896}
897
898#[pyfunction]
914#[pyo3(name = "hold_gil_for_test", signature = (delay_secs, hold_secs))]
915pub fn hold_gil_for_test(delay_secs: f64, hold_secs: f64) {
916 thread::spawn(move || {
917 thread::sleep(Duration::from_secs_f64(delay_secs));
919 monarch_with_gil_blocking(GilSite::Test, |_py| {
921 tracing::info!("start holding the gil...");
922 thread::sleep(Duration::from_secs_f64(hold_secs));
923 tracing::info!("end holding the gil...");
924 });
925 });
926}
927
928pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
929 py_module_add_function!(
930 hyperactor_mod,
931 "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
932 py_identity
933 );
934 py_module_add_function!(
935 hyperactor_mod,
936 "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
937 py_actor_mesh_from_bytes
938 );
939 py_module_add_function!(
940 hyperactor_mod,
941 "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
942 hold_gil_for_test
943 );
944 hyperactor_mod.add_class::<PythonActorMesh>()?;
945 hyperactor_mod.add_class::<PythonActorMeshImpl>()?;
946 hyperactor_mod.add_class::<PyActorSupervisionEvent>()?;
947 Ok(())
948}
949
950#[cfg(test)]
951mod tests {
952 use std::cell::RefCell;
953 use std::sync::atomic::AtomicU8;
954 use std::sync::atomic::AtomicUsize;
955 use std::sync::atomic::Ordering;
956 use std::sync::mpsc::Receiver;
957 use std::sync::mpsc::SyncSender;
958 use std::task::Context;
959 use std::task::Poll;
960 use std::time::Duration;
961
962 use hyperactor::ActorRef;
963 use hyperactor::Proc;
964 use hyperactor::ProcAddr;
965 use hyperactor::ProcId;
966 use hyperactor::channel::ChannelAddr;
967 use hyperactor::id::Label;
968 use hyperactor::id::Uid;
969 use hyperactor_mesh::host_mesh::HostMeshRef;
970 use hyperactor_mesh::mesh_id::ActorMeshId;
971 use hyperactor_mesh::mesh_id::HostMeshId;
972 use ndslice::extent;
973 use pyo3::PyTypeInfo;
974 use tokio::sync::watch;
975
976 use super::*;
977
978 fn fixture_mesh_id() -> ActorMeshId {
981 ActorMeshId::instance(Label::new("mesh_name_fixture").expect("test label should be valid"))
982 }
983
984 fn managed_mesh_ref(id: &ActorMeshId) -> ActorMeshRef<PythonActor> {
1008 let host_mesh = HostMeshRef::from_hosts(
1009 HostMeshId::singleton(Label::strip("managed_name_fixture")),
1010 vec![ChannelAddr::Local(1)],
1011 );
1012 let mut wire = serde_json::to_value(&host_mesh).expect("HostMeshRef should serialize");
1013 let payload = wire
1014 .get_mut("host_agent_mesh")
1015 .and_then(|mesh| mesh.get_mut("Managed"))
1016 .and_then(serde_json::Value::as_array_mut)
1017 .expect("the host agent mesh should encode as the managed variant");
1018 assert_eq!(
1019 payload.len(),
1020 4,
1021 "the managed payload should be the (id, proc mesh, controller, cast domain) tuple"
1022 );
1023 payload[0] = serde_json::to_value(id).expect("ActorMeshId should serialize");
1024
1025 let mesh_ref: ActorMeshRef<PythonActor> =
1026 serde_json::from_value(wire["host_agent_mesh"].clone())
1027 .expect("the host agent mesh should decode as an ActorMeshRef");
1028 assert_eq!(
1029 mesh_ref
1030 .as_managed()
1031 .expect("the fixture must decode as the managed variant")
1032 .id(),
1033 id,
1034 "the fixture must carry the caller-selected id, not the constructor default"
1035 );
1036 mesh_ref
1037 }
1038
1039 fn data_mesh_ref(id: &ActorMeshId) -> ActorMeshRef<PythonActor> {
1042 let proc_addr = ProcAddr::new(
1043 ProcId::new(
1044 Uid::Instance(1, None),
1045 Some(Label::new("local").expect("test label should be valid")),
1046 ),
1047 ChannelAddr::Local(1).into(),
1048 );
1049 let member: ActorRef<PythonActor> = ActorRef::attest(proc_addr.actor_addr("member"));
1050 ActorMeshRef::try_new_data(id.clone(), extent!(members = 1).into(), vec![member])
1051 .expect("a one-rank data mesh should be valid")
1052 }
1053
1054 fn drive_name(future: BoxFuture<'static, PyResult<String>>) -> String {
1056 get_tokio_runtime()
1057 .block_on(future)
1058 .expect("the name future should resolve")
1059 }
1060
1061 #[test]
1072 fn discarded_managed_name_wrappers_leave_exact_name_repeatable() {
1073 pyo3::Python::initialize();
1074 let id = fixture_mesh_id();
1075 let mesh_ref = managed_mesh_ref(&id);
1076 let mesh_impl = PythonActorMeshImpl::new_ref(mesh_ref.clone());
1077 let expected = id.to_string();
1078
1079 drop(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(
1080 &mesh_ref,
1081 ));
1082 drop(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl));
1083
1084 assert_eq!(
1085 drive_name(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(
1086 &mesh_ref
1087 )),
1088 expected,
1089 "discarding a ref-wrapper task must not disturb the next one"
1090 );
1091 assert_eq!(
1092 drive_name(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl)),
1093 expected,
1094 "discarding an impl-wrapper task must not disturb the next one"
1095 );
1096 }
1097
1098 #[test]
1100 fn data_name_wrappers_report_mesh_id() {
1101 pyo3::Python::initialize();
1102 let id = fixture_mesh_id();
1103 let mesh_ref = data_mesh_ref(&id);
1104 let mesh_impl = PythonActorMeshImpl::new_ref(mesh_ref.clone());
1105 let expected = id.to_string();
1106
1107 assert_eq!(
1108 drive_name(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(
1109 &mesh_ref
1110 )),
1111 expected,
1112 "the ref wrapper must report the data mesh id"
1113 );
1114 assert_eq!(
1115 drive_name(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl)),
1116 expected,
1117 "the impl wrapper must report the data mesh id"
1118 );
1119 }
1120
1121 #[test]
1151 fn default_initialized_wrappers_return_none_and_leave_mesh_usable() {
1152 pyo3::Python::initialize();
1153 let id = fixture_mesh_id();
1154 let mesh_ref = managed_mesh_ref(&id);
1155 let mesh_impl = PythonActorMeshImpl::new_ref(mesh_ref.clone());
1156
1157 drop(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::initialized(&mesh_ref));
1158 drop(<PythonActorMeshImpl as ActorMeshProtocol>::initialized(
1159 &mesh_impl,
1160 ));
1161
1162 for (future, which) in [
1163 (
1164 <ActorMeshRef<PythonActor> as ActorMeshProtocol>::initialized(&mesh_ref),
1165 "ref",
1166 ),
1167 (
1168 <PythonActorMeshImpl as ActorMeshProtocol>::initialized(&mesh_impl),
1169 "impl",
1170 ),
1171 ] {
1172 let handle = PyHandle::spawn(future);
1173 let value = get_tokio_runtime()
1174 .block_on(handle.wait_future())
1175 .expect("the default initialized Handle should resolve");
1176 assert!(
1177 monarch_with_gil_blocking(GilSite::Test, |py| value.is_none(py)),
1178 "the default {which} initialized wrapper must resolve to None"
1179 );
1180 }
1181
1182 assert_eq!(
1183 drive_name(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl)),
1184 id.to_string(),
1185 "observing the default initialized wrapper must leave the mesh usable"
1186 );
1187 }
1188
1189 const CONTROLLED_MARKER: &str = "controlled-actor-mesh-reconstructed";
1196
1197 struct ReductionProbe {
1200 entered: SyncSender<()>,
1201 dropped: SyncSender<()>,
1202 }
1203
1204 thread_local! {
1205 static REDUCTION_PROBE: RefCell<Option<ReductionProbe>> = const { RefCell::new(None) };
1209 }
1210
1211 struct ArmedProbe {
1216 previous: Option<ReductionProbe>,
1217 }
1218
1219 impl Drop for ArmedProbe {
1220 fn drop(&mut self) {
1221 let previous = self.previous.take();
1222 REDUCTION_PROBE.with(|slot| *slot.borrow_mut() = previous);
1223 }
1224 }
1225
1226 #[must_use]
1227 fn arm_reduction_probe(probe: ReductionProbe) -> ArmedProbe {
1228 let previous = REDUCTION_PROBE.with(|slot| slot.borrow_mut().replace(probe));
1229 ArmedProbe { previous }
1230 }
1231
1232 pub(super) struct ProbedObserver {
1235 inner: Option<Pin<Box<dyn Future<Output = PyResult<PythonActorMesh>> + Send>>>,
1236 probe: Option<ReductionProbe>,
1237 entered: bool,
1238 completed: bool,
1239 }
1240
1241 pub(super) fn probe_reduction_observer<F>(inner: F) -> ProbedObserver
1242 where
1243 F: Future<Output = PyResult<PythonActorMesh>> + Send + 'static,
1244 {
1245 ProbedObserver {
1246 inner: Some(Box::pin(inner)),
1247 probe: REDUCTION_PROBE.with(|slot| slot.borrow_mut().take()),
1248 entered: false,
1249 completed: false,
1250 }
1251 }
1252
1253 impl Future for ProbedObserver {
1254 type Output = PyResult<PythonActorMesh>;
1255
1256 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1257 let this = self.get_mut();
1258 let polled = match this.inner.as_mut() {
1259 Some(inner) => inner.as_mut().poll(cx),
1260 None => Poll::Pending,
1261 };
1262 if polled.is_ready() {
1263 this.completed = true;
1264 }
1265 if !this.entered {
1269 this.entered = true;
1270 if let Some(probe) = &this.probe {
1271 let _ = probe.entered.try_send(());
1272 }
1273 }
1274 polled
1275 }
1276 }
1277
1278 impl Drop for ProbedObserver {
1279 fn drop(&mut self) {
1280 drop(self.inner.take());
1285 if let Some(probe) = self.probe.take()
1289 && !self.completed
1290 {
1291 let _ = probe.dropped.try_send(());
1292 }
1293 }
1294 }
1295
1296 struct Gate {
1298 release: watch::Sender<bool>,
1299 entered: SyncSender<()>,
1300 }
1301
1302 impl Gate {
1303 fn new(entered: SyncSender<()>) -> (Arc<Self>, watch::Receiver<bool>) {
1304 let (release, rx) = watch::channel(false);
1305 (Arc::new(Self { release, entered }), rx)
1306 }
1307
1308 fn open(&self) {
1309 let _ = self.release.send(true);
1310 }
1311 }
1312
1313 #[derive(Default)]
1315 enum OpOutcome {
1316 #[default]
1317 Succeed,
1318 Fail(&'static str),
1320 }
1321
1322 #[derive(Default)]
1331 struct OpControl {
1332 release: Option<watch::Receiver<bool>>,
1334 entered: Option<SyncSender<()>>,
1336 completed: Arc<AtomicUsize>,
1339 outcome: OpOutcome,
1340 }
1341
1342 impl OpControl {
1343 fn gated(gate: &Arc<Gate>, release: watch::Receiver<bool>, outcome: OpOutcome) -> Self {
1345 Self {
1346 release: Some(release),
1347 entered: Some(gate.entered.clone()),
1348 completed: Arc::new(AtomicUsize::new(0)),
1349 outcome,
1350 }
1351 }
1352
1353 fn immediate(outcome: OpOutcome) -> Self {
1356 Self {
1357 outcome,
1358 ..Self::default()
1359 }
1360 }
1361
1362 fn completions(&self) -> usize {
1363 self.completed.load(Ordering::SeqCst)
1364 }
1365
1366 async fn run(&self) -> PyResult<()> {
1374 if let Some(entered) = &self.entered {
1375 let _ = entered.try_send(());
1376 }
1377 if let Some(release) = &self.release {
1378 let mut release = release.clone();
1379 while !*release.borrow() {
1380 if release.changed().await.is_err() {
1381 break;
1382 }
1383 }
1384 }
1385 self.completed.fetch_add(1, Ordering::SeqCst);
1386 match self.outcome {
1387 OpOutcome::Succeed => Ok(()),
1388 OpOutcome::Fail(message) => Err(PyValueError::new_err(message)),
1389 }
1390 }
1391 }
1392
1393 #[derive(Default, Clone)]
1399 struct ControlledOps {
1400 name: Arc<OpControl>,
1401 stop: Arc<OpControl>,
1402 }
1403
1404 struct ControlledActorMesh {
1407 name: String,
1408 slices: Arc<AtomicUsize>,
1409 ops: ControlledOps,
1410 }
1411
1412 impl ActorMeshProtocol for ControlledActorMesh {
1413 fn cast(
1414 &self,
1415 _message: PythonMessage,
1416 _selection: AllOrChoose,
1417 _instance: &Instance<PythonActor>,
1418 ) -> PyResult<()> {
1419 unreachable!("the controlled mesh has no cast path")
1420 }
1421
1422 fn __reduce__<'py>(
1423 &self,
1424 py: Python<'py>,
1425 ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
1426 let ctor = py.get_type::<pyo3::types::PyString>().into_any();
1432 let args = PyTuple::new(py, [CONTROLLED_MARKER])?;
1433 Ok((ctor, args.into_any()))
1434 }
1435
1436 fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
1437 Err(PyRuntimeError::new_err(
1440 "the controlled mesh has no serializable reference",
1441 ))
1442 }
1443
1444 fn name(&self) -> BoxFuture<'static, PyResult<String>> {
1445 let name = self.name.clone();
1446 let ops = self.ops.name.clone();
1447 async move {
1448 ops.run().await?;
1449 Ok(name)
1450 }
1451 .boxed()
1452 }
1453
1454 fn stop(
1461 &self,
1462 _instance: &PyInstance,
1463 _reason: String,
1464 ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
1465 let ops = self.ops.stop.clone();
1466 Ok(async move {
1467 ops.run().await?;
1468 Ok(())
1469 }
1470 .boxed())
1471 }
1472 }
1473
1474 #[async_trait]
1475 impl Supervisable for ControlledActorMesh {
1476 async fn supervision_event(&self, _instance: &Instance<PythonActor>) -> Option<PyErr> {
1477 std::future::pending().await
1478 }
1479 }
1480
1481 impl SupervisableActorMesh for ControlledActorMesh {
1482 fn new_with_region(&self, _region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
1483 self.slices.fetch_add(1, Ordering::SeqCst);
1484 Ok(Box::new(ControlledActorMesh {
1485 name: format!("{}-slice", self.name),
1486 slices: self.slices.clone(),
1487 ops: self.ops.clone(),
1491 }))
1492 }
1493 }
1494
1495 fn controlled_root(
1498 gate: &Arc<Gate>,
1499 mut release: watch::Receiver<bool>,
1500 slices: Option<Arc<AtomicUsize>>,
1501 ops: ControlledOps,
1502 ) -> ActorMeshFut {
1503 let entered = gate.entered.clone();
1504 let slices = slices.unwrap_or_default();
1505 async move {
1506 let _ = entered.try_send(());
1507 while !*release.borrow() {
1508 if release.changed().await.is_err() {
1509 break;
1510 }
1511 }
1512 Ok::<Arc<dyn SupervisableActorMesh>, ClonePyErr>(Arc::new(ControlledActorMesh {
1513 name: "controlled".to_string(),
1514 slices,
1515 ops,
1516 }))
1517 }
1518 .boxed()
1519 .shared()
1520 }
1521
1522 fn test_queue() -> (
1528 UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
1529 tokio::task::JoinHandle<()>,
1530 ) {
1531 let (queue, mut recv) = unbounded_channel();
1532 let driver = get_tokio_runtime().spawn(async move {
1533 loop {
1534 let r = recv.recv().await;
1535 if let Some(r) = r {
1536 r.await;
1537 } else {
1538 return;
1539 }
1540 }
1541 });
1542 (queue, driver)
1543 }
1544
1545 const PHASE_RUNNING: u8 = 0;
1547 const PHASE_RELEASE_PERMITTED: u8 = 1;
1549 const PHASE_DUMPS_RETURNED: u8 = 2;
1551
1552 const WAIT: Duration = Duration::from_secs(30);
1555
1556 fn signal() -> (SyncSender<()>, Receiver<()>) {
1557 std::sync::mpsc::sync_channel(1)
1558 }
1559
1560 fn awaited(rx: &Receiver<()>) -> bool {
1563 rx.recv_timeout(WAIT).is_ok()
1564 }
1565
1566 fn drive_bounded<T>(future: BoxFuture<'static, PyResult<T>>) -> Option<T> {
1570 get_tokio_runtime()
1571 .block_on(async { tokio::time::timeout(WAIT, future).await })
1572 .ok()
1573 .map(|resolved| resolved.expect("the future should resolve"))
1574 }
1575
1576 fn bounded_name(future: BoxFuture<'static, PyResult<String>>) -> Option<String> {
1577 drive_bounded(future)
1578 }
1579
1580 struct Teardown {
1594 gate: Arc<Gate>,
1595 op_gates: Vec<Arc<Gate>>,
1596 driver: Option<tokio::task::JoinHandle<()>>,
1597 }
1598
1599 impl Teardown {
1600 fn new(gate: Arc<Gate>, driver: tokio::task::JoinHandle<()>) -> Self {
1601 Self {
1602 gate,
1603 op_gates: Vec::new(),
1604 driver: Some(driver),
1605 }
1606 }
1607
1608 #[must_use]
1610 fn with_op_gate(mut self, gate: Arc<Gate>) -> Self {
1611 self.op_gates.push(gate);
1612 self
1613 }
1614
1615 fn open_all(&self) {
1617 self.gate.open();
1618 for gate in &self.op_gates {
1619 gate.open();
1620 }
1621 }
1622
1623 fn join(&mut self) -> bool {
1627 let Some(driver) = self.driver.as_mut() else {
1628 return false;
1629 };
1630 let outcome =
1631 get_tokio_runtime().block_on(async { tokio::time::timeout(WAIT, driver).await });
1632 match outcome {
1633 Ok(result) => {
1637 self.driver.take();
1638 result.is_ok()
1639 }
1640 Err(_) => {
1645 if let Some(driver) = self.driver.take() {
1646 driver.abort();
1647 let _ = get_tokio_runtime()
1648 .block_on(async { tokio::time::timeout(WAIT, driver).await });
1649 }
1650 false
1651 }
1652 }
1653 }
1654
1655 async fn join_async(&mut self) -> bool {
1657 let Some(driver) = self.driver.as_mut() else {
1658 return false;
1659 };
1660 match tokio::time::timeout(WAIT, driver).await {
1661 Ok(result) => {
1662 self.driver.take();
1663 result.is_ok()
1664 }
1665 Err(_) => {
1666 if let Some(driver) = self.driver.take() {
1667 driver.abort();
1668 let _ = tokio::time::timeout(WAIT, driver).await;
1669 }
1670 false
1671 }
1672 }
1673 }
1674 }
1675
1676 impl Drop for Teardown {
1677 fn drop(&mut self) {
1678 self.open_all();
1679 if let Some(driver) = self.driver.take() {
1680 driver.abort();
1681 }
1682 }
1683 }
1684
1685 #[test]
1700 fn pending_bare_actor_mesh_pickle_waits_for_release() {
1701 pyo3::Python::initialize();
1702 monarch_with_gil_blocking(GilSite::Test, |py| {
1703 PyPythonTask::install_test_module(py).expect("the pytokio module should install")
1704 });
1705
1706 let (entered_tx, entered_rx) = signal();
1707 let (gate, release) = Gate::new(entered_tx);
1708 let (queue, driver) = test_queue();
1709 let mesh = AsyncActorMesh::new(
1711 queue,
1712 true,
1713 controlled_root(&gate, release, None, ControlledOps::default()),
1714 );
1715 let mut teardown = Teardown::new(gate.clone(), driver);
1716 let mesh = monarch_with_gil_blocking(GilSite::Test, |py| {
1717 Py::new(py, PythonActorMesh::from_impl(Arc::new(mesh)))
1718 .expect("the mesh wrapper should construct")
1719 });
1720
1721 let phase = Arc::new(AtomicU8::new(PHASE_RUNNING));
1729 let (ack_tx, ack_rx) = signal();
1730 let helper_gate = gate.clone();
1731 let helper_phase = phase.clone();
1732 let helper = std::thread::spawn(move || {
1733 let acknowledged = ack_rx.recv_timeout(WAIT).is_ok();
1734 let release_won = helper_phase
1735 .compare_exchange(
1736 PHASE_RUNNING,
1737 PHASE_RELEASE_PERMITTED,
1738 Ordering::SeqCst,
1739 Ordering::SeqCst,
1740 )
1741 .is_ok();
1742 helper_gate.open();
1745 (acknowledged, release_won)
1746 });
1747
1748 let blob = monarch_with_gil_blocking(GilSite::Test, |py| {
1749 let _routing = PyPythonTask::route_pending_block_on(ack_tx);
1750 let pickle = py.import("pickle").expect("pickle should import");
1751 let blob = pickle
1752 .call_method1("dumps", (mesh.clone_ref(py),))
1753 .expect("pickling a pending mesh should succeed once released")
1754 .unbind();
1755 let _ = phase.compare_exchange(
1756 PHASE_RUNNING,
1757 PHASE_DUMPS_RETURNED,
1758 Ordering::SeqCst,
1759 Ordering::SeqCst,
1760 );
1761 blob
1762 });
1763
1764 let (acknowledged, release_won) =
1765 helper.join().expect("the helper thread should not panic");
1766
1767 let restored = monarch_with_gil_blocking(GilSite::Test, |py| {
1768 let pickle = py.import("pickle").expect("pickle should import");
1769 pickle
1770 .call_method1("loads", (blob.bind(py),))
1771 .expect("the pickled mesh should load")
1772 .extract::<String>()
1773 .expect("the fixture reconstructs to its marker")
1774 });
1775
1776 monarch_with_gil_blocking(GilSite::Test, move |_py| {
1780 drop(mesh);
1781 drop(blob);
1782 });
1783 let joined = teardown.join();
1784
1785 assert!(
1786 awaited(&entered_rx),
1787 "the controlled root must have been polled by the reduction observer"
1788 );
1789 assert!(
1790 acknowledged,
1791 "pickling must reach the pending branch of PyShared::block_on"
1792 );
1793 assert!(
1794 release_won,
1795 "pickle.dumps must not return before release permission is granted"
1796 );
1797 assert_eq!(
1798 restored, CONTROLLED_MARKER,
1799 "the recursive controlled reducer must have run after the blocking branch"
1800 );
1801 assert!(
1802 joined,
1803 "the queue driver must return once its senders close"
1804 );
1805 }
1806
1807 fn pending_reduce(py: Python<'_>, mesh: &dyn ActorMeshProtocol) -> Py<PyAny> {
1810 let (callable, args) = mesh.__reduce__(py).expect("__reduce__ should succeed");
1811 let expected = shared_class(py)
1812 .getattr("block_on")
1813 .expect("Shared.block_on should resolve");
1814 assert!(
1815 callable.is(&expected),
1816 "a pending mesh must reduce through the real Shared.block_on descriptor"
1817 );
1818 let args = args
1819 .cast_into::<PyTuple>()
1820 .expect("the reducer arguments should be a tuple");
1821 assert_eq!(args.len(), 1, "exactly one observer argument is expected");
1822 let observer = args.get_item(0).expect("the tuple has one item");
1823 assert!(
1824 observer
1825 .is_instance(&shared_class(py))
1826 .expect("instance check should succeed"),
1827 "the reducer argument must be a real Shared"
1828 );
1829 args.into_any().unbind()
1830 }
1831
1832 #[test]
1841 fn dropped_root_observers_do_not_cancel_initialization() {
1842 pyo3::Python::initialize();
1843 monarch_with_gil_blocking(GilSite::Test, |py| {
1844 PyPythonTask::install_test_module(py).expect("the pytokio module should install")
1845 });
1846
1847 let (entered_tx, _entered_rx) = signal();
1848 let (gate, release) = Gate::new(entered_tx);
1849 let (queue, driver) = test_queue();
1850 let mesh = AsyncActorMesh::new(
1851 queue,
1852 true,
1853 controlled_root(&gate, release, None, ControlledOps::default()),
1854 );
1855 let mut teardown = Teardown::new(gate.clone(), driver);
1856
1857 let (completed_tx, completed_rx) = signal();
1867 let root = mesh.mesh.clone();
1868 mesh.push(async move {
1869 let _ = root.await;
1870 let _ = completed_tx.try_send(());
1871 });
1872
1873 let (probe_entered_tx, probe_entered_rx) = signal();
1874 let (probe_dropped_tx, probe_dropped_rx) = signal();
1875 let _probe = arm_reduction_probe(ReductionProbe {
1876 entered: probe_entered_tx,
1877 dropped: probe_dropped_tx,
1878 });
1879 let tuple = monarch_with_gil_blocking(GilSite::Test, |py| pending_reduce(py, &mesh));
1880
1881 let observed = awaited(&probe_entered_rx);
1882 monarch_with_gil_blocking(GilSite::Test, move |_py| drop(tuple));
1887 let acknowledged = awaited(&probe_dropped_rx);
1888
1889 gate.open();
1890 let completed = awaited(&completed_rx);
1891
1892 let readiness = completed.then(|| drive_bounded(mesh.initialized()));
1897 let name = completed.then(|| bounded_name(mesh.name()));
1898
1899 drop(mesh);
1900 let joined = teardown.join();
1901
1902 assert!(
1903 observed,
1904 "the reduction observer must be polled before it is dropped"
1905 );
1906 assert!(
1907 acknowledged,
1908 "the reduction observer must be destroyed while still pending"
1909 );
1910 assert!(
1911 completed,
1912 "the queue-held root initializer must complete on its own after release"
1913 );
1914 let readiness = readiness.flatten().expect("readiness must resolve in time");
1915 assert!(
1916 readiness.is_none(),
1917 "a replacement readiness observer resolves to None"
1918 );
1919 assert_eq!(
1920 name.flatten().as_deref(),
1921 Some("controlled"),
1922 "the resolved implementation stays usable"
1923 );
1924 assert!(
1925 joined,
1926 "the queue driver must return once its senders close"
1927 );
1928 }
1929
1930 #[test]
1940 fn dropped_slice_observers_leave_the_slice_reobservable() {
1941 pyo3::Python::initialize();
1942 monarch_with_gil_blocking(GilSite::Test, |py| {
1943 PyPythonTask::install_test_module(py).expect("the pytokio module should install")
1944 });
1945
1946 let slices = Arc::new(AtomicUsize::new(0));
1947 let (entered_tx, _entered_rx) = signal();
1948 let (gate, release) = Gate::new(entered_tx);
1949 let (queue, driver) = test_queue();
1950 let mesh = AsyncActorMesh::new(
1951 queue,
1952 true,
1953 controlled_root(
1954 &gate,
1955 release,
1956 Some(slices.clone()),
1957 ControlledOps::default(),
1958 ),
1959 );
1960 let mut teardown = Teardown::new(gate.clone(), driver);
1961
1962 let (completed_tx, completed_rx) = signal();
1972 let root = mesh.mesh.clone();
1973 mesh.push(async move {
1974 let _ = root.await;
1975 let _ = completed_tx.try_send(());
1976 });
1977
1978 let region: PyRegion = ndslice::Region::from(extent!(slices = 1)).into();
1979 let slice = mesh
1980 .new_with_region(®ion)
1981 .expect("slicing a pending mesh should succeed");
1982
1983 let (probe_entered_tx, probe_entered_rx) = signal();
1984 let (probe_dropped_tx, probe_dropped_rx) = signal();
1985 let _probe = arm_reduction_probe(ReductionProbe {
1986 entered: probe_entered_tx,
1987 dropped: probe_dropped_tx,
1988 });
1989 let tuple =
1990 monarch_with_gil_blocking(GilSite::Test, |py| pending_reduce(py, slice.as_ref()));
1991
1992 let observed = awaited(&probe_entered_rx);
1993 monarch_with_gil_blocking(GilSite::Test, move |_py| drop(tuple));
1994 let acknowledged = awaited(&probe_dropped_rx);
1995
1996 gate.open();
1997 let completed = awaited(&completed_rx);
1998 let dormant = slices.load(Ordering::SeqCst);
1999
2000 let readiness = completed.then(|| drive_bounded(slice.initialized()));
2001 let driven = slices.load(Ordering::SeqCst);
2002 let name = completed.then(|| bounded_name(slice.name()));
2003
2004 drop(slice);
2005 drop(mesh);
2006 let joined = teardown.join();
2007
2008 assert!(
2009 observed,
2010 "the slice reduction observer must be polled before it is dropped"
2011 );
2012 assert!(
2013 acknowledged,
2014 "the slice reduction observer must be destroyed while still pending"
2015 );
2016 assert!(completed, "the root must complete on its own after release");
2017 assert_eq!(
2018 dormant, 0,
2019 "the slice has no queue item, so discarding its observers leaves it undriven"
2020 );
2021 let readiness = readiness.flatten().expect("readiness must resolve in time");
2022 assert!(
2023 readiness.is_none(),
2024 "a replacement slice observer resolves to None"
2025 );
2026 assert_eq!(
2027 driven, 1,
2028 "the replacement observer drives the slice exactly once"
2029 );
2030 assert_eq!(
2031 name.flatten().as_deref(),
2032 Some("controlled-slice"),
2033 "the retained slice stays usable"
2034 );
2035 assert!(
2036 joined,
2037 "the queue driver must return once its senders close"
2038 );
2039 }
2040
2041 const CONTROLLED_FAILURE: &str = "controlled operation failed";
2046
2047 const REF_STOP_REJECTION: &str =
2050 "Cannot call stop on an ActorMeshRef, requires an owned ActorMesh";
2051
2052 fn isolated_py_instance(name: &str) -> PyInstance {
2059 let instance = Proc::isolated()
2060 .actor_instance::<PythonActor>(name)
2061 .expect("an isolated proc should host a client instance")
2062 .instance;
2063 PyInstance::from(instance)
2064 }
2065
2066 fn controlled_fixture(
2072 ops: ControlledOps,
2073 op_gates: &[Arc<Gate>],
2074 ) -> (AsyncActorMesh, Arc<Gate>, Receiver<()>, Teardown) {
2075 let (root_entered_tx, root_entered_rx) = signal();
2076 let (root_gate, release) = Gate::new(root_entered_tx);
2077 let (queue, driver) = test_queue();
2078 let mesh =
2079 AsyncActorMesh::new(queue, true, controlled_root(&root_gate, release, None, ops));
2080 let mut teardown = Teardown::new(root_gate.clone(), driver);
2081 for gate in op_gates {
2082 teardown = teardown.with_op_gate(gate.clone());
2083 }
2084 (mesh, root_gate, root_entered_rx, teardown)
2085 }
2086
2087 fn reach_inner_operation(
2104 root_entered: &Receiver<()>,
2105 root_gate: &Arc<Gate>,
2106 op_entered: &Receiver<()>,
2107 ) -> (bool, bool) {
2108 let reached_root = awaited(root_entered);
2109 root_gate.open();
2110 (reached_root, awaited(op_entered))
2111 }
2112
2113 fn discard_observer<T>(observer: PyResult<T>) -> PyResult<()> {
2116 observer.map(drop)
2117 }
2118
2119 enum DriveOutcome {
2123 NotConstructed(PyErr),
2125 TimedOut,
2127 Resolved(PyResult<Py<PyAny>>),
2128 }
2129
2130 impl DriveOutcome {
2131 fn resolved(self, context: &str) -> PyResult<Py<PyAny>> {
2133 match self {
2134 DriveOutcome::Resolved(result) => result,
2135 DriveOutcome::NotConstructed(err) => {
2136 panic!("{context}: the wrapper returned no observer: {err:?}")
2137 }
2138 DriveOutcome::TimedOut => panic!("{context}: the observer did not resolve"),
2139 }
2140 }
2141 }
2142
2143 fn drive_evidence(handle: PyResult<PyHandle>) -> DriveOutcome {
2145 get_tokio_runtime().block_on(drive_evidence_async(handle))
2146 }
2147
2148 async fn drive_evidence_async(handle: PyResult<PyHandle>) -> DriveOutcome {
2150 let handle = match handle {
2151 Ok(handle) => handle,
2152 Err(err) => return DriveOutcome::NotConstructed(err),
2153 };
2154 match tokio::time::timeout(WAIT, handle.wait_future()).await {
2155 Ok(resolved) => DriveOutcome::Resolved(resolved),
2156 Err(_) => DriveOutcome::TimedOut,
2157 }
2158 }
2159
2160 fn queue_barrier(mesh: &AsyncActorMesh) -> bool {
2170 let (passed_tx, passed_rx) = signal();
2171 let queued = mesh
2172 .queue
2173 .send(
2174 async move {
2175 let _ = passed_tx.try_send(());
2176 }
2177 .boxed(),
2178 )
2179 .is_ok();
2180 queued && awaited(&passed_rx)
2181 }
2182
2183 fn assert_exact_error<E: PyTypeInfo>(err: &PyErr, message: &str, context: &str) {
2186 monarch_with_gil_blocking(GilSite::Test, |py| {
2187 assert!(
2188 err.get_type(py).is(py.get_type::<E>()),
2189 "{context}: expected exactly {}, got {}",
2190 std::any::type_name::<E>(),
2191 err.get_type(py),
2192 );
2193 assert_eq!(
2194 err.value(py).to_string(),
2195 message,
2196 "{context}: unexpected error message"
2197 );
2198 });
2199 }
2200
2201 #[test]
2212 fn async_name_survives_a_dropped_observer_and_leaves_queue_usable() {
2213 pyo3::Python::initialize();
2214
2215 {
2217 let name_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2218 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2219 ControlledOps {
2220 name: name_ops.clone(),
2221 stop: Arc::default(),
2222 },
2223 &[],
2224 );
2225
2226 let handle = PyHandle::spawn(<AsyncActorMesh as ActorMeshProtocol>::name(&mesh));
2227 let reached_root = awaited(&root_entered);
2228 root_gate.open();
2229 let outcome = drive_evidence(Ok(handle));
2230
2231 teardown.open_all();
2232 drop(mesh);
2233 let joined = teardown.join();
2234
2235 assert!(
2236 reached_root,
2237 "the name producer should reach root initialization"
2238 );
2239 let value = outcome
2240 .resolved("retained name success")
2241 .expect("the controlled name should succeed");
2242 let observed = monarch_with_gil_blocking(GilSite::Test, move |py| {
2243 value
2244 .extract::<String>(py)
2245 .expect("name() should resolve to a string")
2246 });
2247 assert_eq!(
2248 observed, "controlled",
2249 "a retained observer must receive the inner name"
2250 );
2251 assert_eq!(
2252 name_ops.completions(),
2253 1,
2254 "the inner operation must run exactly once"
2255 );
2256 assert!(joined, "the driver must return once its senders close");
2257 }
2258
2259 {
2261 let name_ops = Arc::new(OpControl::immediate(OpOutcome::Fail(CONTROLLED_FAILURE)));
2262 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2263 ControlledOps {
2264 name: name_ops.clone(),
2265 stop: Arc::default(),
2266 },
2267 &[],
2268 );
2269
2270 let handle = PyHandle::spawn(<AsyncActorMesh as ActorMeshProtocol>::name(&mesh));
2271 let reached_root = awaited(&root_entered);
2272 root_gate.open();
2273 let outcome = drive_evidence(Ok(handle));
2274
2275 teardown.open_all();
2276 drop(mesh);
2277 let joined = teardown.join();
2278
2279 assert!(
2280 reached_root,
2281 "the name producer should reach root initialization"
2282 );
2283 let err = outcome
2284 .resolved("retained name error")
2285 .expect_err("the fixed-error control must surface as an error");
2286 assert_exact_error::<PyValueError>(&err, CONTROLLED_FAILURE, "retained name error");
2287 assert_eq!(
2288 name_ops.completions(),
2289 1,
2290 "the failing inner operation must still run exactly once"
2291 );
2292 assert!(joined, "the driver must return once its senders close");
2293 }
2294
2295 {
2297 let name_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2298 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2299 ControlledOps {
2300 name: name_ops.clone(),
2301 stop: Arc::default(),
2302 },
2303 &[],
2304 );
2305 let (completed_tx, completed_rx) = signal();
2306 let future = mesh.name();
2307 let handle = PyHandle::spawn(async move {
2308 let result = future.await;
2309 let _ = completed_tx.try_send(());
2310 result
2311 });
2312 let reached_root = awaited(&root_entered);
2313 root_gate.open();
2314 let completed = awaited(&completed_rx);
2315 drop(handle);
2316 let usable = queue_barrier(&mesh);
2317 teardown.open_all();
2318 drop(mesh);
2319 let joined = teardown.join();
2320
2321 assert!(reached_root, "the name producer must reach initialization");
2322 assert!(completed, "name must complete before its Handle is dropped");
2323 assert_eq!(name_ops.completions(), 1);
2324 assert!(usable, "later queue work must still run");
2325 assert!(joined, "the queue driver must return normally");
2326 }
2327
2328 {
2331 let (op_entered_tx, op_entered) = signal();
2332 let (op_gate, op_release) = Gate::new(op_entered_tx);
2333 let name_ops = Arc::new(OpControl::gated(&op_gate, op_release, OpOutcome::Succeed));
2334 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2335 ControlledOps {
2336 name: name_ops.clone(),
2337 stop: Arc::default(),
2338 },
2339 std::slice::from_ref(&op_gate),
2340 );
2341 let (completed_tx, completed_rx) = signal();
2342 let future = mesh.name();
2343 let handle = PyHandle::spawn(async move {
2344 let result = future.await;
2345 let _ = completed_tx.try_send(());
2346 result
2347 });
2348 let (reached_root, entered_op) =
2349 reach_inner_operation(&root_entered, &root_gate, &op_entered);
2350 drop(handle);
2351 let usable = queue_barrier(&mesh);
2352 teardown.open_all();
2353 let completed = awaited(&completed_rx);
2354 drop(mesh);
2355 let joined = teardown.join();
2356
2357 assert!(reached_root, "the name producer must reach initialization");
2358 assert!(
2359 entered_op,
2360 "name must be pending when its Handle is dropped"
2361 );
2362 assert!(usable, "name must not hold up the cast/stop queue");
2363 assert!(completed, "unobserved name must complete after release");
2364 assert_eq!(name_ops.completions(), 1);
2365 assert!(joined, "the queue driver must return normally");
2366 }
2367 }
2368
2369 #[tokio::test]
2384 async fn async_stop_survives_a_dropped_observer_and_leaves_queue_usable() {
2385 pyo3::Python::initialize();
2386 let instance = isolated_py_instance("stop_characterization_client");
2387
2388 {
2390 let stop_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2391 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2392 ControlledOps {
2393 name: Arc::default(),
2394 stop: stop_ops.clone(),
2395 },
2396 &[],
2397 );
2398
2399 let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2400 &mesh,
2401 &instance,
2402 "characterization".to_string(),
2403 );
2404 let reached_root = awaited(&root_entered);
2405 root_gate.open();
2406 let outcome = drive_evidence_async(task.map(PyHandle::spawn)).await;
2407
2408 teardown.open_all();
2409 drop(mesh);
2410 let joined = teardown.join_async().await;
2411
2412 assert!(
2413 reached_root,
2414 "the queued closure should reach root initialization"
2415 );
2416 let value = outcome
2417 .resolved("retained stop success")
2418 .expect("the controlled stop should succeed");
2419 monarch_with_gil_blocking(GilSite::Test, move |py| {
2420 let bound = value.bind(py);
2424 let tuple = bound
2425 .cast::<PyTuple>()
2426 .expect("the unit result should convert to a tuple");
2427 assert!(
2428 tuple.is_empty(),
2429 "a successful owned stop resolves to the unit conversion, the empty tuple"
2430 );
2431 });
2432 assert_eq!(
2433 stop_ops.completions(),
2434 1,
2435 "the inner operation must run exactly once"
2436 );
2437 assert!(joined, "the driver must return once its senders close");
2438 }
2439
2440 {
2442 let stop_ops = Arc::new(OpControl::immediate(OpOutcome::Fail(CONTROLLED_FAILURE)));
2443 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2444 ControlledOps {
2445 name: Arc::default(),
2446 stop: stop_ops.clone(),
2447 },
2448 &[],
2449 );
2450
2451 let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2452 &mesh,
2453 &instance,
2454 "characterization".to_string(),
2455 );
2456 let reached_root = awaited(&root_entered);
2457 root_gate.open();
2458 let outcome = drive_evidence_async(task.map(PyHandle::spawn)).await;
2459
2460 teardown.open_all();
2461 drop(mesh);
2462 let joined = teardown.join_async().await;
2463
2464 assert!(
2465 reached_root,
2466 "the queued closure should reach root initialization"
2467 );
2468 let err = outcome
2469 .resolved("retained stop error")
2470 .expect_err("the fixed-error control must surface as an error");
2471 assert_exact_error::<PyValueError>(&err, CONTROLLED_FAILURE, "retained stop error");
2472 assert_eq!(
2473 stop_ops.completions(),
2474 1,
2475 "the failing inner operation must still run exactly once"
2476 );
2477 assert!(joined, "the driver must return once its senders close");
2478 }
2479
2480 {
2483 let stop_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2484 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2485 ControlledOps {
2486 name: Arc::default(),
2487 stop: stop_ops.clone(),
2488 },
2489 &[],
2490 );
2491
2492 let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2493 &mesh,
2494 &instance,
2495 "characterization".to_string(),
2496 );
2497 let reached_root = awaited(&root_entered);
2498 root_gate.open();
2499 let sent = queue_barrier(&mesh);
2500 let construction = discard_observer(task);
2501
2502 teardown.open_all();
2503 drop(mesh);
2504 let joined = teardown.join_async().await;
2505
2506 assert!(
2507 reached_root,
2508 "the queued closure should reach root initialization"
2509 );
2510 assert!(sent, "the barrier should prove the stop closure completed");
2511 assert!(
2512 construction.is_ok(),
2513 "the outer observer must have been constructed: {:?}",
2514 construction.err()
2515 );
2516 assert_eq!(
2517 stop_ops.completions(),
2518 1,
2519 "the inner operation must run exactly once"
2520 );
2521 assert!(
2522 joined,
2523 "destroying an unpolled observer after a successful send must not panic the driver"
2524 );
2525 }
2526
2527 {
2529 let (op_entered_tx, op_entered) = signal();
2530 let (op_gate, op_release) = Gate::new(op_entered_tx);
2531 let stop_ops = Arc::new(OpControl::gated(&op_gate, op_release, OpOutcome::Succeed));
2532 let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2533 ControlledOps {
2534 name: Arc::default(),
2535 stop: stop_ops.clone(),
2536 },
2537 std::slice::from_ref(&op_gate),
2538 );
2539
2540 let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2541 &mesh,
2542 &instance,
2543 "characterization".to_string(),
2544 );
2545 let (reached_root, entered_op) =
2546 reach_inner_operation(&root_entered, &root_gate, &op_entered);
2547 let construction = discard_observer(task);
2548
2549 teardown.open_all();
2550 let sent = queue_barrier(&mesh);
2551 drop(mesh);
2552 let joined = teardown.join_async().await;
2553
2554 assert!(
2555 reached_root,
2556 "the queued closure should reach root initialization"
2557 );
2558 assert!(
2559 entered_op,
2560 "the queued closure should enter the inner operation"
2561 );
2562 assert!(
2563 construction.is_ok(),
2564 "the outer observer must have been constructed: {:?}",
2565 construction.err()
2566 );
2567 assert!(
2568 sent,
2569 "later queue work must run after the closed-receiver send"
2570 );
2571 assert!(joined, "the queue driver must return normally");
2572 assert_eq!(
2573 stop_ops.completions(),
2574 1,
2575 "the inner stop must finish exactly once despite losing its observer"
2576 );
2577 }
2578 }
2579
2580 #[tokio::test]
2599 async fn actor_mesh_impl_ref_stop_rejects_before_returning_a_future() {
2600 pyo3::Python::initialize();
2601 let mesh_impl = PythonActorMeshImpl::new_ref(data_mesh_ref(&fixture_mesh_id()));
2604 let instance = isolated_py_instance("ref_stop_characterization_client");
2605
2606 let rejected = <PythonActorMeshImpl as ActorMeshProtocol>::stop(
2607 &mesh_impl,
2608 &instance,
2609 "characterization".to_string(),
2610 );
2611
2612 let Err(err) = rejected else {
2613 panic!("the Ref arm must reject rather than return a future");
2614 };
2615 assert_exact_error::<PyNotImplementedError>(&err, REF_STOP_REJECTION, "Ref stop rejection");
2616 }
2617}