1mod async_actor;
10mod sync_actor;
11
12use std::collections::HashMap;
13use std::collections::VecDeque;
14use std::fmt::Debug;
15use std::ops::Deref;
16use std::sync::Arc;
17use std::sync::Mutex;
18use std::sync::OnceLock;
19use std::sync::atomic::AtomicBool;
20use std::sync::atomic::AtomicU64;
21use std::sync::atomic::AtomicUsize;
22use std::sync::atomic::Ordering as AtomicOrdering;
23use std::time::SystemTime;
24
25use async_trait::async_trait;
26use hyperactor::Actor;
27use hyperactor::ActorEnvironment;
28use hyperactor::ActorHandle;
29use hyperactor::Context;
30use hyperactor::Endpoint as _;
31use hyperactor::Handler;
32use hyperactor::Instance;
33use hyperactor::MessageStatusReporting;
34use hyperactor::OncePortHandle;
35use hyperactor::PortHandle;
36use hyperactor::Proc;
37use hyperactor::RemoteSpawn;
38use hyperactor::actor::ActorError;
39use hyperactor::actor::ActorErrorKind;
40use hyperactor::actor::ActorStatus;
41use hyperactor::actor::Signal;
42use hyperactor::context::Actor as ContextActor;
43use hyperactor::mailbox::MessageEnvelope;
44use hyperactor::mailbox::Undeliverable;
45use hyperactor::mailbox::UndeliverableMessageError;
46use hyperactor::mailbox::UndeliverableReason;
47use hyperactor::supervision::ActorSupervisionEvent;
48use hyperactor_config::Flattrs;
49use hyperactor_mesh::ProcMeshRef;
50use hyperactor_mesh::actor_mesh::ACTOR_MESH_ID;
51use hyperactor_mesh::actor_mesh::ActorMeshRef;
52use hyperactor_mesh::casting::CAST_POINT;
53use hyperactor_mesh::casting::CastInfo;
54use hyperactor_mesh::casting::update_undeliverable_envelope_for_casting;
55use hyperactor_mesh::host_mesh::HostMeshRef;
56use hyperactor_mesh::introspect::ActiveHandler;
57use hyperactor_mesh::introspect::EXECUTION;
58use hyperactor_mesh::introspect::Execution;
59use hyperactor_mesh::supervision::MeshFailure;
60use hyperactor_mesh::transport::default_bind_spec;
61use hyperactor_mesh::value_mesh::ValueOverlay;
62use monarch_types::PickledPyObject;
63use monarch_types::SerializablePyErr;
64use ndslice::Point;
65use ndslice::extent;
66use pyo3::IntoPyObjectExt;
67use pyo3::exceptions::PyRuntimeError;
68use pyo3::prelude::*;
69use pyo3::types::PyDict;
70use pyo3::types::PyList;
71use pyo3::types::PyType;
72use serde::Deserialize;
73use serde::Serialize;
74use serde_multipart::Part;
75use typeuri::Named;
76
77use crate::buffers::FrozenBuffer;
78use crate::context::PyContext;
79use crate::context::PyInstance;
80use crate::handle::PyHandleCompleter;
81use crate::handle::handle_pair;
82pub(crate) use crate::handle::to_py_error;
84use crate::local_state_broker::BrokerId;
85use crate::local_state_broker::LocalStateBrokerMessage;
86use crate::mailbox::EitherPortRef;
87use crate::mailbox::PyMailbox;
88use crate::mailbox::PythonUndeliverableMessageEnvelope;
89use crate::pickle::PicklingState;
90use crate::pickle::pickle_to_part;
91use crate::proc::PyActorAddr;
92use crate::pympsc::Sender;
93use crate::runtime::GilSite;
94use crate::runtime::get_tokio_runtime;
95use crate::runtime::mark_actor_driver_thread;
96use crate::runtime::mark_actor_event_loop_thread;
97use crate::runtime::monarch_with_gil;
98use crate::runtime::monarch_with_gil_blocking;
99use crate::supervision::PyMeshFailure;
100
101#[pyclass(
106 eq,
107 eq_int,
108 frozen,
109 module = "monarch._rust_bindings.monarch_hyperactor.actor"
110)]
111#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
112pub enum ActorKind {
113 #[pyo3(name = "SYNC")]
115 Sync,
116 #[pyo3(name = "ASYNC")]
118 Async,
119}
120
121#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
122#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
123pub enum UnflattenArg {
124 Mailbox,
125 PyObject,
126}
127
128#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
129#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
130pub enum MethodSpecifier {
131 ReturnsResponse { name: String },
133 ExplicitPort { name: String },
135 Init {},
137}
138
139impl std::fmt::Display for MethodSpecifier {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 write!(f, "{}", self.name())
142 }
143}
144
145#[pymethods]
146impl MethodSpecifier {
147 #[getter(name)]
148 fn py_name(&self) -> &str {
149 self.name()
150 }
151}
152
153impl MethodSpecifier {
154 pub(crate) fn name(&self) -> &str {
155 match self {
156 MethodSpecifier::ReturnsResponse { name } => name,
157 MethodSpecifier::ExplicitPort { name } => name,
158 MethodSpecifier::Init {} => "__init__",
159 }
160 }
161}
162
163#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
170pub enum PythonResponseMessage {
171 Result {
172 part: serde_multipart::Part,
173 refs: Vec<MeshRef>,
174 },
175 Exception {
176 part: serde_multipart::Part,
177 refs: Vec<MeshRef>,
178 },
179}
180
181wirevalue::register_type!(PythonResponseMessage);
182wirevalue::register_type!(ValueOverlay<PythonResponseMessage>);
183
184impl PythonResponseMessage {
185 pub(crate) fn decode(
193 &self,
194 py: Python<'_>,
195 instance: &Instance<PythonActor>,
196 ) -> PyResult<Py<PyAny>> {
197 let (part, refs) = match self {
198 PythonResponseMessage::Result { part, refs }
199 | PythonResponseMessage::Exception { part, refs } => (part, refs),
200 };
201 let mesh_references = refs.iter().cloned().map(Some).collect();
202 let mut state = PicklingState::from_parts(part.clone(), VecDeque::new(), mesh_references);
203 state.unpickle_with_receiver(py, instance)
204 }
205}
206
207#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
212#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
213pub struct AccumulatedResponses(ValueOverlay<PythonResponseMessage>);
214
215#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
216#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq)]
217pub enum PythonMessageKind {
218 #[pyo3(constructor = (name, response_port, correlation_id=None))]
219 CallMethod {
220 name: MethodSpecifier,
221 response_port: Option<EitherPortRef>,
222 correlation_id: Option<u64>,
223 },
224 Result {
225 rank: Option<usize>,
226 },
227 Exception {
228 rank: Option<usize>,
229 },
230 Uninit {},
231 #[pyo3(constructor = (name, local_state_broker, id, unflatten_args, correlation_id=None))]
232 CallMethodIndirect {
233 name: MethodSpecifier,
234 local_state_broker: (String, usize),
235 id: usize,
236 unflatten_args: Vec<UnflattenArg>,
239 correlation_id: Option<u64>,
240 },
241 AccumulatedResponses(AccumulatedResponses),
242}
243wirevalue::register_type!(PythonMessageKind);
244
245impl Default for PythonMessageKind {
246 fn default() -> Self {
247 PythonMessageKind::Uninit {}
248 }
249}
250
251#[derive(Clone, Debug, Named, PartialEq, Eq)]
257pub enum MeshRef {
258 Actor(Box<ActorMeshRef<PythonActor>>),
259 Proc(Box<ProcMeshRef>),
260 Host(Box<HostMeshRef>),
261}
262
263#[doc(hidden)]
265#[derive(Clone, Debug, Serialize, Deserialize, Named)]
266pub enum MeshRefRepr {
267 Actor(Box<ActorMeshRef<PythonActor>>),
268 Proc(Box<ProcMeshRef>),
269 Host(Box<HostMeshRef>),
270}
271
272impl TryFrom<&MeshRef> for MeshRefRepr {
273 type Error = serde_multipart::Error;
274 fn try_from(m: &MeshRef) -> serde_multipart::Result<Self> {
275 Ok(match m {
276 MeshRef::Actor(r) => MeshRefRepr::Actor(r.clone()),
277 MeshRef::Proc(r) => MeshRefRepr::Proc(r.clone()),
278 MeshRef::Host(r) => MeshRefRepr::Host(r.clone()),
279 })
280 }
281}
282
283impl TryFrom<MeshRefRepr> for MeshRef {
284 type Error = serde_multipart::Error;
285 fn try_from(r: MeshRefRepr) -> serde_multipart::Result<Self> {
286 Ok(match r {
287 MeshRefRepr::Actor(r) => MeshRef::Actor(r),
288 MeshRefRepr::Proc(r) => MeshRef::Proc(r),
289 MeshRefRepr::Host(r) => MeshRef::Host(r),
290 })
291 }
292}
293
294serde_multipart::part_codec! {
295 impl MeshRef
296 {
297 type Repr = MeshRefRepr;
298 }
299}
300
301impl MeshRef {
302 pub(crate) fn reconstruct(self, py: Python<'_>) -> PyResult<Py<PyAny>> {
307 match self {
308 MeshRef::Proc(r) => {
309 Ok(Py::new(py, crate::proc_mesh::PyProcMesh::new_ref(*r))?.into_any())
310 }
311 MeshRef::Host(r) => {
312 Ok(Py::new(py, crate::host_mesh::PyHostMesh::new_ref(*r))?.into_any())
313 }
314 MeshRef::Actor(r) => {
315 let inner = crate::actor_mesh::PythonActorMeshImpl::new_ref(*r);
316 let async_mesh = crate::actor_mesh::AsyncActorMesh::from_impl(Arc::new(inner));
317 let mesh = crate::actor_mesh::PythonActorMesh::from_impl(Arc::from(async_mesh));
318 Ok(Py::new(py, mesh)?.into_any())
319 }
320 }
321 }
322}
323
324#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
327pub struct PyMeshRef {
328 pub(crate) inner: MeshRef,
329}
330
331impl<'py> IntoPyObject<'py> for MeshRef {
332 type Target = PyMeshRef;
333 type Output = Bound<'py, PyMeshRef>;
334 type Error = PyErr;
335
336 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
337 Bound::new(py, PyMeshRef { inner: self })
338 }
339}
340
341pub(crate) fn mesh_ref_from_pyobject(value: &Bound<'_, PyAny>) -> PyResult<MeshRef> {
344 if let Ok(m) = value.cast::<crate::proc_mesh::PyProcMesh>() {
345 return Ok(MeshRef::Proc(Box::new(m.borrow().mesh_ref()?)));
346 }
347 if let Ok(m) = value.cast::<crate::host_mesh::PyHostMesh>() {
348 return Ok(MeshRef::Host(Box::new(m.borrow().mesh_ref().map_err(
349 |e| pyo3::exceptions::PyValueError::new_err(e.to_string()),
350 )?)));
351 }
352 if let Ok(m) = value.cast::<crate::actor_mesh::PythonActorMesh>() {
353 return Ok(MeshRef::Actor(Box::new(m.borrow().get_inner().mesh_ref()?)));
354 }
355 Err(pyo3::exceptions::PyRuntimeError::new_err(
356 "pending pickle did not resolve to a mesh reference",
357 ))
358}
359
360#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
361#[derive(Clone, Serialize, Deserialize, Named, Default, PartialEq)]
362pub struct PythonMessage {
363 pub kind: PythonMessageKind,
364 pub message: Part,
365 pub refs: Vec<MeshRef>,
367}
368
369fn python_message_endpoint_name(msg: &PythonMessage) -> Option<String> {
371 match &msg.kind {
372 PythonMessageKind::CallMethod { name, .. }
373 | PythonMessageKind::CallMethodIndirect { name, .. } => Some(name.name().to_string()),
374 _ => None,
375 }
376}
377
378wirevalue::submit! {
383 wirevalue::TypeInfo {
384 typename: <PythonMessage as wirevalue::Named>::typename,
385 typehash: <PythonMessage as wirevalue::Named>::typehash,
386 typeid: <PythonMessage as wirevalue::Named>::typeid,
387 port: <PythonMessage as wirevalue::Named>::port,
388 dump: Some(<PythonMessage as wirevalue::NamedDumpable>::dump),
389 arm_unchecked: <PythonMessage as wirevalue::Named>::arm_unchecked,
390 endpoint_name: |ptr| {
391 let msg = unsafe { &*(ptr as *const PythonMessage) };
393 python_message_endpoint_name(msg)
394 },
395 }
396}
397
398impl From<ValueOverlay<PythonResponseMessage>> for PythonMessage {
399 fn from(overlay: ValueOverlay<PythonResponseMessage>) -> Self {
400 PythonMessage {
401 kind: PythonMessageKind::AccumulatedResponses(AccumulatedResponses(overlay)),
402 message: Default::default(),
403 refs: Vec::new(),
404 }
405 }
406}
407
408impl PythonMessage {
409 pub fn into_overlay(self) -> anyhow::Result<ValueOverlay<PythonResponseMessage>> {
414 match self.kind {
415 PythonMessageKind::AccumulatedResponses(overlay) => Ok(overlay.0),
416 PythonMessageKind::Result { rank, .. } => {
417 let rank = rank.expect("accumulated response should have a rank");
418 let mut overlay = ValueOverlay::new();
419 overlay.push_run(
420 rank..rank + 1,
421 PythonResponseMessage::Result {
422 part: self.message,
423 refs: self.refs,
424 },
425 )?;
426 Ok(overlay)
427 }
428 PythonMessageKind::Exception { rank, .. } => {
429 let rank = rank.expect("accumulated exception should have a rank");
430 let mut overlay = ValueOverlay::new();
431 overlay.push_run(
432 rank..rank + 1,
433 PythonResponseMessage::Exception {
434 part: self.message,
435 refs: self.refs,
436 },
437 )?;
438 Ok(overlay)
439 }
440 other => {
441 anyhow::bail!(
442 "unexpected message kind {:?} in collected responses reducer",
443 other
444 );
445 }
446 }
447 }
448}
449
450struct ResolvedCallMethod {
451 method: MethodSpecifier,
452 bytes: FrozenBuffer,
453 local_state: PendingLocalState,
454 mesh_references: Vec<MeshRef>,
455 response_port: ResponsePort,
458 correlation_id: Option<u64>,
459}
460
461enum ResponsePort {
462 Dropping,
463 Port(Port),
464 Local(LocalPort),
465}
466
467impl ResponsePort {
468 fn into_py_any(self, py: Python<'_>) -> PyResult<Py<PyAny>> {
469 match self {
470 ResponsePort::Dropping => DroppingPort.into_py_any(py),
471 ResponsePort::Port(port) => port.into_py_any(py),
472 ResponsePort::Local(port) => port.into_py_any(py),
473 }
474 }
475}
476
477enum PendingLocalState {
478 Empty,
479 Indirect {
480 mailbox: PyMailbox,
481 unflatten_args: Vec<UnflattenArg>,
482 state: Vec<Py<PyAny>>,
483 },
484}
485
486impl PendingLocalState {
487 fn into_py_any(self, py: Python<'_>) -> PyResult<Py<PyAny>> {
488 match self {
489 PendingLocalState::Empty => Ok(PyList::empty(py).into_any().unbind()),
490 PendingLocalState::Indirect {
491 mailbox,
492 unflatten_args,
493 state,
494 } => {
495 let mailbox = mailbox.into_bound_py_any(py)?;
496 let mut state = state.into_iter();
497 let items = unflatten_args.into_iter().map(|arg| match arg {
498 UnflattenArg::Mailbox => mailbox.clone(),
499 UnflattenArg::PyObject => state
500 .next()
501 .expect("local state broker should return an object per PyObject arg")
502 .into_bound(py),
503 });
504 Ok(PyList::new(py, items)?.into_any().unbind())
505 }
506 }
507 }
508}
509
510struct PendingMessage {
516 instance: Arc<Py<PyInstance>>,
517 rank: Point,
518 recording_span: tracing::Span,
519 resolved: ResolvedCallMethod,
520 telemetry_message_id: Option<u64>,
521}
522
523impl<'py> IntoPyObject<'py> for PendingMessage {
524 type Target = QueuedMessage;
525 type Output = Bound<'py, QueuedMessage>;
526 type Error = PyErr;
527
528 fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
531 let telemetry_message_id = self.telemetry_message_id;
532 self.into_queued(py)
533 .inspect_err(|_| report_message_status(telemetry_message_id, "failed"))
534 }
535}
536
537impl PendingMessage {
538 fn into_queued(self, py: Python<'_>) -> PyResult<Bound<'_, QueuedMessage>> {
539 let context = crate::context::PyContext::from_parts(
540 self.instance.clone_ref(py),
541 self.rank,
542 Some(self.recording_span),
543 );
544 let resolved = self.resolved;
545 Bound::new(
546 py,
547 QueuedMessage {
548 context: Py::new(py, context)?,
549 method: resolved.method,
550 bytes: resolved.bytes,
551 local_state: resolved.local_state.into_py_any(py)?,
552 refs: resolved.mesh_references.into_py_any(py)?,
553 response_port: resolved.response_port.into_py_any(py)?,
554 telemetry_message_id: self.telemetry_message_id,
555 correlation_id: resolved.correlation_id,
556 },
557 )
558 }
559}
560
561#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
564pub struct QueuedMessage {
565 #[pyo3(get)]
566 pub context: Py<crate::context::PyContext>,
567 #[pyo3(get)]
568 pub method: MethodSpecifier,
569 #[pyo3(get)]
570 pub bytes: FrozenBuffer,
571 #[pyo3(get)]
572 pub local_state: Py<PyAny>,
573 #[pyo3(get)]
574 pub refs: Py<PyAny>,
575 #[pyo3(get)]
576 pub response_port: Py<PyAny>,
577 telemetry_message_id: Option<u64>,
578 #[pyo3(get)]
579 pub correlation_id: Option<u64>,
580}
581
582fn report_message_status(message_id: Option<u64>, status: &str) {
583 if let Some(message_id) = message_id {
584 hyperactor_telemetry::notify_message_status(hyperactor_telemetry::MessageStatusEvent {
585 timestamp: std::time::SystemTime::now(),
586 id: hyperactor_telemetry::generate_status_event_id(message_id),
587 message_id,
588 status: status.to_string(),
589 });
590 }
591}
592
593#[pymethods]
594impl QueuedMessage {
595 fn _report_complete(&self) {
596 report_message_status(self.telemetry_message_id, "complete");
597 }
598
599 fn _report_failed(&self) {
600 report_message_status(self.telemetry_message_id, "failed");
601 }
602}
603
604impl PythonMessage {
605 pub fn new_from_buf(kind: PythonMessageKind, message: impl Into<Part>) -> Self {
606 Self::new_from_buf_with_refs(kind, message, Vec::new())
607 }
608
609 pub fn new_from_buf_with_refs(
610 kind: PythonMessageKind,
611 message: impl Into<Part>,
612 refs: Vec<MeshRef>,
613 ) -> Self {
614 Self {
615 kind,
616 message: message.into(),
617 refs,
618 }
619 }
620
621 pub fn into_rank(self, rank: usize) -> Self {
622 let rank = Some(rank);
623 match self.kind {
624 PythonMessageKind::Result { .. } => PythonMessage {
625 kind: PythonMessageKind::Result { rank },
626 message: self.message,
627 refs: self.refs,
628 },
629 PythonMessageKind::Exception { .. } => PythonMessage {
630 kind: PythonMessageKind::Exception { rank },
631 message: self.message,
632 refs: self.refs,
633 },
634 _ => panic!("PythonMessage is not a response but {:?}", self),
635 }
636 }
637 async fn resolve_indirect_call(
638 self,
639 cx: &Context<'_, PythonActor>,
640 ) -> anyhow::Result<ResolvedCallMethod> {
641 match self.kind {
642 PythonMessageKind::CallMethodIndirect {
643 name,
644 local_state_broker,
645 id,
646 unflatten_args,
647 correlation_id,
648 } => {
649 let broker = BrokerId::new(local_state_broker).resolve(cx).await;
650 let (send, recv) = cx.open_once_port();
651 broker.post(cx, LocalStateBrokerMessage::Get(id, send));
652 let state = recv.recv().await?;
653 Ok(ResolvedCallMethod {
654 method: name,
655 bytes: FrozenBuffer {
656 inner: self.message.into_bytes(),
657 },
658 local_state: PendingLocalState::Indirect {
659 mailbox: cx.mailbox_for_py().clone().into(),
660 unflatten_args,
661 state: state.state,
662 },
663 mesh_references: self.refs,
664 response_port: ResponsePort::Local(LocalPort {
665 instance: cx.into(),
666 inner: Some(state.response_port),
667 }),
668 correlation_id,
669 })
670 }
671 PythonMessageKind::CallMethod {
672 name,
673 response_port,
674 correlation_id,
675 } => {
676 let method_name = name.name().to_string();
677 let response_port = response_port.map_or(ResponsePort::Dropping, |port_ref| {
678 let point = cx.cast_point();
679 let mut reply_headers = hyperactor_config::Flattrs::new();
684 hyperactor_config::attrs::copy_marked_flattrs(
685 &mut reply_headers,
686 cx.headers(),
687 hyperactor_config::attrs::OPERATION_CONTEXT_HEADER,
688 );
689 if reply_headers
690 .get(hyperactor::mailbox::headers::OPERATION_ENDPOINT)
691 .is_none()
692 {
693 reply_headers.set(
694 hyperactor::mailbox::headers::OPERATION_ENDPOINT,
695 format!("{}()", method_name),
696 );
697 }
698 ResponsePort::Port(Port::with_reply_headers(
699 port_ref,
700 cx.instance().clone_for_py(),
701 Some(point.rank()),
702 reply_headers,
703 ))
704 });
705 Ok(ResolvedCallMethod {
706 method: name,
707 bytes: FrozenBuffer {
708 inner: self.message.into_bytes(),
709 },
710 local_state: PendingLocalState::Empty,
711 mesh_references: self.refs,
712 response_port,
713 correlation_id,
714 })
715 }
716 _ => {
717 panic!("unexpected message kind {:?}", self.kind)
718 }
719 }
720 }
721}
722
723impl std::fmt::Debug for PythonMessage {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 f.debug_struct("PythonMessage")
726 .field("kind", &self.kind)
727 .field(
728 "message",
729 &wirevalue::HexFmt(&(*self.message.to_bytes())[..]).to_string(),
730 )
731 .field("refs", &self.refs.len())
732 .finish()
733 }
734}
735
736#[pymethods]
737impl PythonMessage {
738 #[new]
739 #[pyo3(signature = (kind, message, refs))]
740 pub fn new(
741 kind: PythonMessageKind,
742 message: PyRef<'_, FrozenBuffer>,
743 refs: &Bound<'_, PyList>,
744 ) -> PyResult<Self> {
745 let mesh_refs: Vec<MeshRef> = refs
746 .iter()
747 .map(|item| Ok(item.cast::<PyMeshRef>()?.borrow().inner.clone()))
748 .collect::<PyResult<_>>()?;
749 Ok(PythonMessage::new_from_buf_with_refs(
750 kind,
751 message.inner.clone(),
752 mesh_refs,
753 ))
754 }
755
756 #[getter]
757 fn kind(&self) -> PythonMessageKind {
758 self.kind.clone()
759 }
760
761 #[pyo3(signature = (local_state=None))]
766 fn decode(
767 &self,
768 py: Python<'_>,
769 local_state: Option<&Bound<'_, PyList>>,
770 ) -> PyResult<Py<PyAny>> {
771 let tensor_engine_references: VecDeque<Py<PyAny>> = local_state
772 .map(|list| list.iter().map(|item| item.unbind()).collect())
773 .unwrap_or_default();
774 let mesh_references: VecDeque<Option<MeshRef>> =
775 self.refs.iter().cloned().map(Some).collect();
776 let mut state = PicklingState::from_parts(
777 self.message.clone(),
778 tensor_engine_references,
779 mesh_references,
780 );
781 state.unpickle(py)
782 }
783
784 #[getter]
785 fn refs(&self) -> Vec<MeshRef> {
786 self.refs.clone()
787 }
788}
789
790#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
791pub(super) struct PythonActorHandle {
792 pub(super) inner: ActorHandle<PythonActor>,
793}
794
795#[pymethods]
796impl PythonActorHandle {
797 fn send(&self, instance: &PyInstance, message: &PythonMessage) -> PyResult<()> {
799 self.inner.post(instance.deref(), message.clone());
800 Ok(())
801 }
802
803 fn bind(&self) -> PyActorAddr {
804 self.inner.bind::<PythonActor>().into_actor_addr().into()
805 }
806}
807
808const MAX_ACTIVE_HANDLERS: usize = 64;
826
827#[derive(Debug)]
829struct ActiveEntry {
830 name: String,
831 started_at: SystemTime,
832}
833
834#[derive(Debug)]
839pub(crate) struct ExecutionTracker {
840 active_count: AtomicU64,
842 next_token: AtomicU64,
845 handlers: Mutex<HashMap<u64, ActiveEntry>>,
847}
848
849fn aggregate_active(
854 handlers: &HashMap<u64, ActiveEntry>,
855 max: usize,
856) -> (Vec<ActiveHandler>, bool) {
857 let mut by_name: HashMap<&str, (u64, SystemTime)> = HashMap::new();
858 for entry in handlers.values() {
859 let slot = by_name
860 .entry(entry.name.as_str())
861 .or_insert((0, entry.started_at));
862 slot.0 += 1;
863 if entry.started_at < slot.1 {
864 slot.1 = entry.started_at;
865 }
866 }
867 let mut out: Vec<ActiveHandler> = by_name
868 .into_iter()
869 .map(|(name, (active_count, oldest_since))| ActiveHandler {
870 name: name.to_string(),
871 active_count,
872 oldest_since,
873 })
874 .collect();
875 out.sort_by(|a, b| {
877 a.oldest_since
878 .cmp(&b.oldest_since)
879 .then_with(|| a.name.cmp(&b.name))
880 });
881 let truncated = out.len() > max;
882 if truncated {
883 out.truncate(max);
884 }
885 (out, truncated)
886}
887
888impl ExecutionTracker {
889 pub(crate) fn new() -> Self {
890 Self {
891 active_count: AtomicU64::new(0),
892 next_token: AtomicU64::new(1),
893 handlers: Mutex::new(HashMap::new()),
894 }
895 }
896
897 pub(crate) fn start(&self, name: String) -> u64 {
899 let token = self.next_token.fetch_add(1, AtomicOrdering::Relaxed);
900 self.handlers
901 .lock()
902 .unwrap_or_else(|e| e.into_inner())
903 .insert(
904 token,
905 ActiveEntry {
906 name,
907 started_at: SystemTime::now(),
908 },
909 );
910 self.active_count.fetch_add(1, AtomicOrdering::Relaxed);
911 token
912 }
913
914 pub(crate) fn finish(&self, token: u64) {
917 if token == 0 {
918 return;
919 }
920 let removed = self
921 .handlers
922 .lock()
923 .unwrap_or_else(|e| e.into_inner())
924 .remove(&token)
925 .is_some();
926 if removed {
927 self.active_count.fetch_sub(1, AtomicOrdering::Relaxed);
928 }
929 }
930
931 pub(crate) fn snapshot(&self) -> Execution {
936 let active_count = self.active_count.load(AtomicOrdering::Relaxed);
937 match self.handlers.try_lock() {
938 Ok(guard) => {
939 let (active_handlers, truncated) = aggregate_active(&guard, MAX_ACTIVE_HANDLERS);
940 Execution {
941 active_count,
942 active_handlers,
943 complete: true,
944 truncated,
945 }
946 }
947 Err(_) => Execution {
948 active_count,
949 active_handlers: Vec::new(),
950 complete: false,
951 truncated: false,
952 },
953 }
954 }
955}
956
957#[cfg(test)]
958mod execution_tracker_tests {
959 use std::time::Duration;
960 use std::time::UNIX_EPOCH;
961
962 use super::*;
963
964 fn at(secs: u64) -> SystemTime {
965 UNIX_EPOCH + Duration::from_secs(secs)
966 }
967
968 #[test]
969 fn aggregates_by_name_oldest_first() {
970 let mut h = HashMap::new();
971 h.insert(
972 1,
973 ActiveEntry {
974 name: "b".to_string(),
975 started_at: at(10),
976 },
977 );
978 h.insert(
979 2,
980 ActiveEntry {
981 name: "a".to_string(),
982 started_at: at(20),
983 },
984 );
985 h.insert(
986 3,
987 ActiveEntry {
988 name: "a".to_string(),
989 started_at: at(30),
990 },
991 );
992 let (out, truncated) = aggregate_active(&h, MAX_ACTIVE_HANDLERS);
993 assert!(!truncated);
994 assert_eq!(out.len(), 2);
995 assert_eq!(out[0].name, "b");
997 assert_eq!(out[0].active_count, 1);
998 assert_eq!(out[0].oldest_since, at(10));
999 assert_eq!(out[1].name, "a");
1001 assert_eq!(out[1].active_count, 2);
1002 assert_eq!(out[1].oldest_since, at(20));
1003 }
1004
1005 #[test]
1006 fn tie_break_on_name_when_same_oldest() {
1007 let mut h = HashMap::new();
1008 h.insert(
1009 1,
1010 ActiveEntry {
1011 name: "zebra".to_string(),
1012 started_at: at(5),
1013 },
1014 );
1015 h.insert(
1016 2,
1017 ActiveEntry {
1018 name: "alpha".to_string(),
1019 started_at: at(5),
1020 },
1021 );
1022 let (out, _) = aggregate_active(&h, MAX_ACTIVE_HANDLERS);
1023 assert_eq!(out[0].name, "alpha");
1024 assert_eq!(out[1].name, "zebra");
1025 }
1026
1027 #[test]
1028 fn truncates_to_n_oldest() {
1029 let mut h = HashMap::new();
1030 for i in 0..(MAX_ACTIVE_HANDLERS as u64 + 6) {
1031 h.insert(
1032 i,
1033 ActiveEntry {
1034 name: format!("h{:03}", i),
1035 started_at: at(i),
1036 },
1037 );
1038 }
1039 let (out, truncated) = aggregate_active(&h, MAX_ACTIVE_HANDLERS);
1040 assert!(truncated);
1041 assert_eq!(out.len(), MAX_ACTIVE_HANDLERS);
1042 assert_eq!(out[0].name, "h000");
1044 assert_eq!(
1045 out[MAX_ACTIVE_HANDLERS - 1].name,
1046 format!("h{:03}", MAX_ACTIVE_HANDLERS - 1)
1047 );
1048 }
1049
1050 #[test]
1051 fn start_assigns_nonzero_distinct_tokens() {
1052 let t = ExecutionTracker::new();
1053 let a = t.start("a".to_string());
1054 let b = t.start("b".to_string());
1055 assert!(a >= 1);
1056 assert!(b >= 1);
1057 assert_ne!(a, b);
1058 let snap = t.snapshot();
1059 assert_eq!(snap.active_count, 2);
1060 assert!(snap.complete);
1061 assert_eq!(snap.active_handlers.len(), 2);
1062 }
1063
1064 #[test]
1065 fn finish_is_idempotent_and_zero_is_noop() {
1066 let t = ExecutionTracker::new();
1067 let tok = t.start("a".to_string());
1068 t.finish(tok);
1069 assert_eq!(t.snapshot().active_count, 0);
1070 t.finish(tok);
1072 assert_eq!(t.snapshot().active_count, 0);
1073 t.finish(0);
1075 assert_eq!(t.snapshot().active_count, 0);
1076 }
1077}
1078
1079#[derive(Debug)]
1081#[hyperactor::export(
1082 handlers = [
1083 PythonMessage,
1084 MeshFailure,
1085 ],
1086)]
1087#[hyperactor::spawnable]
1088pub struct PythonActor {
1089 actor: Py<PyAny>,
1091 dispatch: Dispatch,
1093 instance: Option<Arc<Py<crate::context::PyInstance>>>,
1097 message_sender: Sender,
1099 control_sender: Sender,
1102 callbacks_pending: Arc<AtomicUsize>,
1105 construction_point: OnceLock<Option<Point>>,
1107 init_message: Option<PythonMessage>,
1109
1110 execution_tracker: Arc<ExecutionTracker>,
1115}
1116
1117#[derive(Debug)]
1119enum Dispatch {
1120 Async(async_actor::dispatch::State),
1122 Sync(sync_actor::dispatch::State),
1124}
1125
1126struct DispatchParts {
1134 message_sender: Sender,
1136 control_sender: Sender,
1138 dispatch: Dispatch,
1140}
1141
1142fn serialize_channel_error(py: Python<'_>, error: nix::Error) -> SerializablePyErr {
1145 let error = PyRuntimeError::new_err(error.to_string());
1146 SerializablePyErr::from(py, &error)
1147}
1148
1149impl DispatchParts {
1150 fn try_new(py: Python<'_>, kind: ActorKind) -> Result<Self, SerializablePyErr> {
1152 match kind {
1153 ActorKind::Async => {
1154 let async_actor::inbox::Inbox {
1155 message_sender,
1156 control_sender,
1157 receiver,
1158 } = async_actor::inbox::new()
1159 .map_err(|error| serialize_channel_error(py, error))?;
1160 Ok(Self {
1161 message_sender,
1162 control_sender,
1163 dispatch: Dispatch::Async(async_actor::dispatch::State::new(py, receiver)),
1164 })
1165 }
1166 ActorKind::Sync => {
1167 let sync_actor::inbox::Inbox {
1168 message_sender,
1169 control_sender,
1170 receiver,
1171 } = sync_actor::inbox::new().map_err(|error| serialize_channel_error(py, error))?;
1172 Ok(Self {
1173 message_sender,
1174 control_sender,
1175 dispatch: Dispatch::Sync(sync_actor::dispatch::State::new(receiver)),
1176 })
1177 }
1178 }
1179 }
1180}
1181
1182impl PythonActor {
1183 pub(crate) fn new(
1184 actor_type: PickledPyObject,
1185 init_message: Option<PythonMessage>,
1186 construction_point: Option<Point>,
1187 kind: ActorKind,
1188 ) -> Result<Self, anyhow::Error> {
1189 Ok(monarch_with_gil_blocking(
1190 GilSite::ActorConstruct,
1191 |py| -> Result<Self, SerializablePyErr> {
1192 let unpickled = actor_type.unpickle(py)?;
1193 let class_type: &Bound<'_, PyType> = unpickled.cast()?;
1194 let actor: Py<PyAny> = class_type.call0()?.into_py_any(py)?;
1195
1196 let DispatchParts {
1197 message_sender,
1198 control_sender,
1199 dispatch,
1200 } = DispatchParts::try_new(py, kind)?;
1201
1202 Ok(Self {
1203 actor,
1204 dispatch,
1205 instance: None,
1206 message_sender,
1207 control_sender,
1208 callbacks_pending: Arc::new(AtomicUsize::new(0)),
1209 construction_point: OnceLock::from(construction_point),
1210 init_message,
1211 execution_tracker: Arc::new(ExecutionTracker::new()),
1212 })
1213 },
1214 )?)
1215 }
1216
1217 fn ensure_py_instance(
1222 &mut self,
1223 py: Python<'_>,
1224 src: impl Into<crate::context::PyInstance>,
1225 ) -> Py<crate::context::PyInstance> {
1226 let tracker = self.execution_tracker.clone();
1227 self.instance
1228 .get_or_insert_with(|| {
1229 let mut inst: crate::context::PyInstance = src.into();
1230 inst.set_execution_tracker(tracker);
1231 Arc::new(inst.into_pyobject(py).unwrap().into())
1232 })
1233 .clone_ref(py)
1234 }
1235
1236 pub(crate) fn bootstrap_client(py: Python<'_>) -> (&'static Instance<Self>, ActorHandle<Self>) {
1239 static ROOT_CLIENT_INSTANCE: OnceLock<Instance<PythonActor>> = OnceLock::new();
1240
1241 let client_proc = Proc::direct(
1242 default_bind_spec().binding_addr(),
1243 "mesh_root_client_proc".into(),
1244 )
1245 .unwrap();
1246
1247 Self::bootstrap_client_inner(
1249 py,
1250 client_proc,
1251 ActorEnvironment::default(),
1252 &ROOT_CLIENT_INSTANCE,
1253 )
1254 }
1255
1256 pub(crate) fn bootstrap_client_inner(
1265 py: Python<'_>,
1266 client_proc: Proc,
1267 environment: ActorEnvironment,
1268 root_client_instance: &'static OnceLock<Instance<PythonActor>>,
1269 ) -> (&'static Instance<Self>, ActorHandle<Self>) {
1270 let actor_mesh_mod = py
1271 .import("monarch._src.actor.actor_mesh")
1272 .expect("import actor_mesh");
1273 let root_client_class = actor_mesh_mod
1274 .getattr("RootClientActor")
1275 .expect("get RootClientActor");
1276
1277 let actor_type =
1278 PickledPyObject::pickle(&actor_mesh_mod.getattr("_Actor").expect("get _Actor"))
1279 .expect("pickle _Actor");
1280
1281 let init_frozen_buffer: FrozenBuffer = root_client_class
1282 .call_method0("_pickled_init_args")
1283 .expect("call RootClientActor._pickled_init_args")
1284 .extract()
1285 .expect("extract FrozenBuffer from _pickled_init_args");
1286 let init_message = PythonMessage::new_from_buf(
1287 PythonMessageKind::CallMethod {
1288 name: MethodSpecifier::Init {},
1289 response_port: None,
1290 correlation_id: None,
1291 },
1292 init_frozen_buffer,
1293 );
1294
1295 let mut actor = PythonActor::new(
1296 actor_type,
1297 Some(init_message),
1298 Some(extent!().point_of_rank(0).unwrap()),
1299 ActorKind::Async,
1300 )
1301 .expect("create client PythonActor");
1302
1303 let ai = client_proc
1304 .actor_instance_in_environment(
1305 root_client_class
1306 .getattr("name")
1307 .expect("get RootClientActor.name")
1308 .extract()
1309 .expect("extract RootClientActor.name"),
1310 environment,
1311 )
1312 .expect("root instance create");
1313
1314 let handle = ai.handle;
1315 let signal_rx = ai.signal;
1316 let supervision_rx = ai.supervision;
1317 let work_rx = ai.work;
1318
1319 root_client_instance
1320 .set(ai.instance)
1321 .map_err(|_| "already initialized root client instance")
1322 .unwrap();
1323 let instance = root_client_instance.get().unwrap();
1324
1325 instance.set_system();
1329
1330 let _client_ref = handle.bind::<PythonActor>();
1332
1333 get_tokio_runtime().spawn(async move {
1334 actor.init(instance).await.unwrap();
1336
1337 let mut signal_rx = signal_rx;
1338 let mut supervision_rx = supervision_rx;
1339 let mut work_rx = work_rx;
1340 let mut need_drain = false;
1341 let mut err = loop {
1342 tokio::select! {
1343 work = work_rx.recv() => {
1344 let work = work.expect("inconsistent work queue state");
1345 if let Err(err) = work.handle(&mut actor, instance).await {
1346 let kind = ActorErrorKind::processing(err);
1347 let err = ActorError {
1348 actor_id: Box::new(instance.self_addr().clone()),
1349 kind: Box::new(kind),
1350 };
1351
1352 if matches!(*err.kind, ActorErrorKind::UnhandledSupervisionEvent(_)) {
1358 break Some(err);
1359 }
1360
1361 let supervision_event = actor_error_to_event(instance, &actor, err);
1369 if let Err(err) = instance.handle_supervision_event(&mut actor, supervision_event).await {
1370 break Some(err);
1371 }
1372 }
1373 }
1374 signal = signal_rx.recv() => {
1375 tracing::info!(actor_id = %instance.self_addr(), "client received signal {signal:?}");
1376 match signal {
1377 Some(signal@(Signal::Stop(_) | Signal::DrainAndStop(_))) => {
1378 need_drain = matches!(signal, Signal::DrainAndStop(_));
1379 break None;
1380 },
1381 Some(Signal::ExitRequested(_)) => break None,
1382 Some(Signal::ChildStopped(_)) => {},
1383 Some(Signal::Kill(reason)) => {
1384 break Some(ActorError { actor_id: Box::new(instance.self_addr().clone()), kind: Box::new(ActorErrorKind::Aborted(reason)) })
1385 },
1386 None => {
1387 break Some(ActorError {
1388 actor_id: Box::new(instance.self_addr().clone()),
1389 kind: Box::new(ActorErrorKind::SignalChannelClosed),
1390 })
1391 },
1392 }
1393 }
1394 Some(supervision_event) = supervision_rx.recv() => {
1395 if let Err(err) = instance.handle_supervision_event(&mut actor, supervision_event).await {
1396 break Some(err);
1397 }
1398 }
1399 };
1400 };
1401 if need_drain {
1402 let mut n = 0;
1403 while let Ok(work) = work_rx.try_recv() {
1404 if let Err(e) = work.handle(&mut actor, instance).await {
1405 err = Some(ActorError {
1406 actor_id: Box::new(instance.self_addr().clone()),
1407 kind: Box::new(ActorErrorKind::processing(e)),
1408 });
1409 break;
1410 }
1411 n += 1;
1412 }
1413 tracing::debug!(actor_id = %instance.self_addr(), "client drained {} messages before stopping", n);
1414 }
1415 if let Some(err) = err {
1416 let event = actor_error_to_event(instance, &actor, err);
1417 tracing::error!(
1421 actor_id = %instance.self_addr(),
1422 "could not propagate supervision event {} because it reached the global client: signaling KeyboardInterrupt to main thread",
1423 event,
1424 );
1425
1426 monarch_with_gil_blocking(GilSite::Stop, |py| {
1435 let thread_mod = py.import("_thread").expect("import _thread");
1438 let interrupt_main = thread_mod
1439 .getattr("interrupt_main")
1440 .expect("get interrupt_main");
1441
1442 if let Err(e) = interrupt_main.call0() {
1444 tracing::error!("unable to interrupt main, exiting the process instead: {:?}", e);
1445 eprintln!("unable to interrupt main, exiting the process with code 1 instead: {:?}", e);
1446 std::process::exit(1);
1447 }
1448 });
1449 } else {
1450 tracing::info!(actor_id = %instance.self_addr(), "client stopped");
1451 instance.change_status(hyperactor::actor::ActorStatus::Stopped("client stopped".into()));
1452 }
1453 });
1454
1455 (root_client_instance.get().unwrap(), handle)
1456 }
1457}
1458
1459fn actor_error_to_event(
1460 instance: &Instance<PythonActor>,
1461 actor: &PythonActor,
1462 err: ActorError,
1463) -> ActorSupervisionEvent {
1464 match *err.kind {
1465 ActorErrorKind::UnhandledSupervisionEvent(event) => *event,
1466 _ => {
1467 let status = ActorStatus::generic_failure(err.kind.to_string());
1468 ActorSupervisionEvent::new(
1469 instance.self_addr().clone(),
1470 actor.display_name(),
1471 status,
1472 None,
1473 )
1474 }
1475 }
1476}
1477
1478pub(crate) fn root_client_actor(py: Python<'_>) -> &'static Instance<PythonActor> {
1479 static ROOT_CLIENT_ACTOR: OnceLock<&'static Instance<PythonActor>> = OnceLock::new();
1480
1481 py.detach(|| {
1486 ROOT_CLIENT_ACTOR.get_or_init(|| {
1487 monarch_with_gil_blocking(GilSite::Bootstrap, |py| {
1488 let (client, _handle) = PythonActor::bootstrap_client(py);
1489 client
1490 })
1491 })
1492 })
1493}
1494
1495#[async_trait]
1496impl Actor for PythonActor {
1497 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
1498 let tracker = self.execution_tracker.clone();
1503 this.set_attrs_snapshot(move || {
1504 let mut attrs = hyperactor_config::Attrs::new();
1505 attrs.set(EXECUTION, tracker.snapshot());
1506 attrs
1507 });
1508
1509 monarch_with_gil(GilSite::DispatchInit, |py| {
1510 let self_instance = self.ensure_py_instance(py, this);
1511 let actor_mesh_mod = py.import("monarch._src.actor.actor_mesh")?;
1512
1513 match &mut self.dispatch {
1514 Dispatch::Async(state) => state.init(
1515 py,
1516 &actor_mesh_mod,
1517 &self.actor,
1518 &self_instance,
1519 self.callbacks_pending.clone(),
1520 )?,
1521 Dispatch::Sync(state) => {
1522 state.init(py, &actor_mesh_mod, &self.actor, &self_instance)?
1523 }
1524 }
1525 Ok::<_, anyhow::Error>(())
1526 })
1527 .await?;
1528
1529 if let Some(init_message) = self.init_message.take() {
1530 let construction_point = self.construction_point.get().unwrap().as_ref().expect("PythonActor should never be spawned with init_message unless construction_point is also specified").clone();
1531 let mut headers = Flattrs::new();
1532 headers.set(CAST_POINT, construction_point);
1533 let cx = Context::new(this, headers);
1534 <Self as Handler<PythonMessage>>::handle(self, &cx, init_message).await?;
1535 }
1536
1537 Ok(())
1538 }
1539
1540 async fn cleanup(
1541 &mut self,
1542 this: &Instance<Self>,
1543 err: Option<&ActorError>,
1544 ) -> anyhow::Result<()> {
1545 let cx = Context::new(this, Flattrs::new());
1549 let error = err.map(|e| e.to_string());
1553 let actor = &self.actor;
1554 let instance = self.instance.clone();
1555 let control_sender = &self.control_sender;
1556 match &mut self.dispatch {
1557 Dispatch::Async(state) => {
1558 state
1559 .cleanup(actor, instance.as_deref(), &cx, this, error)
1560 .await
1561 }
1562 Dispatch::Sync(state) => state.cleanup(control_sender, instance, &cx, error).await,
1563 }
1564 }
1565
1566 fn display_name(&self) -> Option<String> {
1567 self.instance.as_ref().and_then(|instance| {
1568 monarch_with_gil_blocking(GilSite::DisplayName, |py| {
1569 instance.bind(py).str().ok().map(|s| s.to_string())
1570 })
1571 })
1572 }
1573
1574 async fn handle_undeliverable_message(
1580 &mut self,
1581 ins: &Instance<Self>,
1582 reason: UndeliverableReason,
1583 mut envelope: Undeliverable<MessageEnvelope>,
1584 ) -> Result<(), anyhow::Error> {
1585 if envelope
1586 .as_message()
1587 .is_some_and(|envelope| envelope.sender() != ins.self_addr())
1588 {
1589 envelope = update_undeliverable_envelope_for_casting(envelope);
1591 }
1592 let envelope = match envelope {
1593 Undeliverable::Returned(envelope) => envelope,
1594 Undeliverable::Report(report) => {
1595 return Err(UndeliverableMessageError::Report { report }.into());
1596 }
1597 };
1598 assert_eq!(
1599 envelope.sender(),
1600 ins.self_addr(),
1601 "undeliverable message was returned to the wrong actor. \
1602 Return address = {}, src actor = {}, dest handler port = {}, message type = {}, envelope headers = {}",
1603 envelope.sender(),
1604 ins.self_addr(),
1605 envelope.dest(),
1606 envelope.data().typename().unwrap_or("unknown"),
1607 envelope.headers()
1608 );
1609
1610 let cx = Context::new(ins, envelope.headers().clone());
1611 let pending = PendingUndeliverable {
1612 instance: self
1613 .instance
1614 .clone()
1615 .expect("PythonActor::init should have created the instance"),
1616 rank: cx.cast_point(),
1617 recording_span: cx.recording_span(),
1618 envelope: Undeliverable::Returned(envelope),
1619 reply: UndeliverableReply {
1620 port: ins.port(),
1621 instance: ins.into(),
1622 reason,
1623 _in_flight: InFlight::new(&self.callbacks_pending),
1624 },
1625 };
1626
1627 self.control_sender
1628 .send(pending)
1629 .map_err(|_| anyhow::anyhow!("failed to send undeliverable message to queue"))?;
1630 Ok(())
1631 }
1632
1633 async fn handle_supervision_event(
1658 &mut self,
1659 this: &Instance<Self>,
1660 event: &ActorSupervisionEvent,
1661 ) -> Result<bool, anyhow::Error> {
1662 let cx = Context::new(this, Flattrs::new());
1663 let crashed_ranks = event
1666 .labels
1667 .get(CAST_POINT)
1668 .map(|point| point.rank())
1669 .into_iter()
1670 .collect();
1671 self.handle(
1672 &cx,
1673 MeshFailure {
1674 actor_mesh_name: event
1678 .labels
1679 .get(ACTOR_MESH_ID)
1680 .map(|mesh_id| mesh_id.to_string()),
1681 event: event.clone(),
1682 crashed_ranks,
1683 reporting_controller: None,
1686 },
1687 )
1688 .await
1689 .map(|_| true)
1690 }
1691}
1692
1693#[derive(Debug, Clone, Serialize, Deserialize, Named)]
1694pub struct PythonActorParams {
1695 actor_type: PickledPyObject,
1697 init_message: Option<PythonMessage>,
1699 kind: ActorKind,
1700}
1701
1702impl PythonActorParams {
1703 pub(crate) fn new(
1704 actor_type: PickledPyObject,
1705 init_message: Option<PythonMessage>,
1706 kind: ActorKind,
1707 ) -> Self {
1708 Self {
1709 actor_type,
1710 init_message,
1711 kind,
1712 }
1713 }
1714}
1715
1716#[async_trait]
1717impl RemoteSpawn for PythonActor {
1718 type Params = PythonActorParams;
1719
1720 async fn new(
1721 PythonActorParams {
1722 actor_type,
1723 init_message,
1724 kind,
1725 }: PythonActorParams,
1726 environment: &ActorEnvironment,
1727 ) -> Result<Self, anyhow::Error> {
1728 let construction_point = environment.get(CAST_POINT);
1729 Self::new(actor_type, init_message, construction_point, kind)
1730 }
1731}
1732
1733#[async_trait]
1734impl Handler<PythonMessage> for PythonActor {
1735 fn message_status_reporting() -> MessageStatusReporting {
1736 MessageStatusReporting::Deferred
1737 }
1738
1739 #[tracing::instrument(level = "debug", skip_all)]
1743 async fn handle(
1744 &mut self,
1745 cx: &Context<PythonActor>,
1746 message: PythonMessage,
1747 ) -> anyhow::Result<()> {
1748 self.handle_queue(cx, message).await
1749 }
1750}
1751
1752impl PythonActor {
1753 async fn handle_queue(
1759 &self,
1760 cx: &Context<'_, PythonActor>,
1761 message: PythonMessage,
1762 ) -> anyhow::Result<()> {
1763 let resolved = message.resolve_indirect_call(cx).await?;
1764
1765 let pending = PendingMessage {
1766 instance: self
1767 .instance
1768 .clone()
1769 .expect("PythonActor::init should have created the instance"),
1770 rank: cx.cast_point(),
1771 recording_span: cx.recording_span(),
1772 resolved,
1773 telemetry_message_id: cx
1774 .headers()
1775 .get(hyperactor::mailbox::headers::TELEMETRY_MESSAGE_ID),
1776 };
1777
1778 self.message_sender
1779 .send(pending)
1780 .map_err(|_| anyhow::anyhow!("failed to send message to queue"))?;
1781
1782 Ok(())
1783 }
1784}
1785
1786#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
1793pub struct QueuedSupervision {
1794 #[pyo3(get)]
1795 context: Py<crate::context::PyContext>,
1796 #[pyo3(get)]
1797 failure: Py<PyMeshFailure>,
1798 reply: Option<SupervisionReply>,
1799}
1800
1801struct SupervisionReply {
1802 port: PortHandle<SupervisionOutcome>,
1803 instance: PyInstance,
1804 failure: MeshFailure,
1805 display_name: Option<String>,
1806 _in_flight: InFlight,
1807}
1808
1809#[pymethods]
1810impl QueuedSupervision {
1811 fn _handled(&mut self, handled: bool) {
1812 self.report(if handled {
1813 SupervisionVerdict::Handled
1814 } else {
1815 SupervisionVerdict::Unhandled
1816 });
1817 }
1818
1819 fn _raised(&mut self, exc: Bound<'_, PyAny>) {
1820 self.report(SupervisionVerdict::Raised(
1821 PyErr::from_value(exc).to_string(),
1822 ));
1823 }
1824}
1825
1826impl QueuedSupervision {
1827 fn report(&mut self, verdict: SupervisionVerdict) {
1828 let SupervisionReply {
1829 port,
1830 instance,
1831 failure,
1832 display_name,
1833 _in_flight,
1834 } = self
1835 .reply
1836 .take()
1837 .expect("supervision verdict reported twice");
1838 let outcome = SupervisionOutcome {
1839 failure,
1840 display_name,
1841 verdict,
1842 };
1843 if let Err(err) = port.try_post(instance.deref(), outcome) {
1844 tracing::debug!(
1845 actor_id = %instance.self_addr(),
1846 "dropping supervision verdict, actor is gone: {}",
1847 err
1848 );
1849 }
1850 }
1851}
1852
1853struct PendingSupervision {
1856 instance: Arc<Py<PyInstance>>,
1857 rank: Point,
1858 recording_span: tracing::Span,
1859 reply: SupervisionReply,
1860}
1861
1862impl<'py> IntoPyObject<'py> for PendingSupervision {
1863 type Target = QueuedSupervision;
1864 type Output = Bound<'py, QueuedSupervision>;
1865 type Error = PyErr;
1866
1867 fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
1868 let mut reply = self.reply;
1869 reply.display_name = self.instance.bind(py).str().ok().map(|s| s.to_string());
1870 let context = crate::context::PyContext::from_parts(
1871 self.instance.clone_ref(py),
1872 self.rank,
1873 Some(self.recording_span),
1874 );
1875 let failure = PyMeshFailure::from(reply.failure.clone());
1876 Bound::new(
1877 py,
1878 QueuedSupervision {
1879 context: Py::new(py, context)?,
1880 failure: Py::new(py, failure)?,
1881 reply: Some(reply),
1882 },
1883 )
1884 }
1885}
1886
1887#[derive(Debug)]
1889struct InFlight(Arc<AtomicUsize>);
1890
1891impl InFlight {
1892 fn new(count: &Arc<AtomicUsize>) -> Self {
1893 count.fetch_add(1, AtomicOrdering::Relaxed);
1894 Self(count.clone())
1895 }
1896}
1897
1898impl Drop for InFlight {
1899 fn drop(&mut self) {
1900 self.0.fetch_sub(1, AtomicOrdering::Relaxed);
1901 }
1902}
1903
1904#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
1909pub struct CallbacksPending(Arc<AtomicUsize>);
1910
1911#[pymethods]
1912impl CallbacksPending {
1913 #[new]
1915 fn new() -> Self {
1916 Self(Arc::new(AtomicUsize::new(0)))
1917 }
1918
1919 fn __bool__(&self) -> bool {
1920 self.0.load(AtomicOrdering::Relaxed) > 0
1921 }
1922}
1923
1924#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
1930pub struct QueuedUndeliverable {
1931 #[pyo3(get)]
1932 context: Py<crate::context::PyContext>,
1933 #[pyo3(get)]
1934 envelope: Py<PythonUndeliverableMessageEnvelope>,
1935 reply: Option<UndeliverableReply>,
1936}
1937
1938struct UndeliverableReply {
1939 port: PortHandle<UndeliverableOutcome>,
1940 instance: PyInstance,
1941 reason: UndeliverableReason,
1942 _in_flight: InFlight,
1943}
1944
1945#[pymethods]
1946impl QueuedUndeliverable {
1947 fn _handled(&mut self, py: Python<'_>, handled: bool) {
1948 let verdict = if handled {
1949 UndeliverableVerdict::Handled
1950 } else {
1951 match self.envelope.bind(py).try_borrow_mut() {
1952 Ok(mut envelope) => match envelope.take() {
1953 Ok(envelope) => UndeliverableVerdict::Unhandled(Box::new(envelope)),
1954 Err(err) => UndeliverableVerdict::Raised(err),
1955 },
1956 Err(err) => UndeliverableVerdict::Raised(PyErr::from(err).into()),
1957 }
1958 };
1959 self.report(verdict);
1960 }
1961
1962 fn _raised(&mut self, exc: Bound<'_, PyAny>) {
1963 let err = SerializablePyErr::from(exc.py(), &PyErr::from_value(exc));
1964 self.report(UndeliverableVerdict::Raised(err.into()));
1965 }
1966}
1967
1968impl QueuedUndeliverable {
1969 fn report(&mut self, verdict: UndeliverableVerdict) {
1970 let UndeliverableReply {
1971 port,
1972 instance,
1973 reason,
1974 _in_flight,
1975 } = self
1976 .reply
1977 .take()
1978 .expect("undeliverable verdict reported twice");
1979 if let Err(err) = port.try_post(instance.deref(), UndeliverableOutcome { reason, verdict })
1980 {
1981 tracing::debug!(
1982 actor_id = %instance.self_addr(),
1983 "dropping undeliverable verdict, actor is gone: {}",
1984 err
1985 );
1986 }
1987 }
1988}
1989
1990struct PendingUndeliverable {
1993 instance: Arc<Py<PyInstance>>,
1994 rank: Point,
1995 recording_span: tracing::Span,
1996 envelope: Undeliverable<MessageEnvelope>,
1997 reply: UndeliverableReply,
1998}
1999
2000impl<'py> IntoPyObject<'py> for PendingUndeliverable {
2001 type Target = QueuedUndeliverable;
2002 type Output = Bound<'py, QueuedUndeliverable>;
2003 type Error = PyErr;
2004
2005 fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
2006 let context = crate::context::PyContext::from_parts(
2007 self.instance.clone_ref(py),
2008 self.rank,
2009 Some(self.recording_span),
2010 );
2011 let envelope = PythonUndeliverableMessageEnvelope {
2012 inner: Some(self.envelope),
2013 };
2014 Bound::new(
2015 py,
2016 QueuedUndeliverable {
2017 context: Py::new(py, context)?,
2018 envelope: Py::new(py, envelope)?,
2019 reply: Some(self.reply),
2020 },
2021 )
2022 }
2023}
2024
2025#[derive(Debug)]
2028pub struct UndeliverableOutcome {
2029 reason: UndeliverableReason,
2030 verdict: UndeliverableVerdict,
2031}
2032
2033#[derive(Debug)]
2034enum UndeliverableVerdict {
2035 Handled,
2036 Unhandled(Box<Undeliverable<MessageEnvelope>>),
2037 Raised(anyhow::Error),
2038}
2039
2040#[async_trait]
2041impl Handler<UndeliverableOutcome> for PythonActor {
2042 async fn handle(
2043 &mut self,
2044 cx: &Context<Self>,
2045 outcome: UndeliverableOutcome,
2046 ) -> anyhow::Result<()> {
2047 match outcome.verdict {
2048 UndeliverableVerdict::Handled => Ok(()),
2049 UndeliverableVerdict::Unhandled(envelope) => {
2050 hyperactor::actor::handle_undeliverable_message(cx, outcome.reason, *envelope)
2051 }
2052 UndeliverableVerdict::Raised(err) => Err(err),
2053 }
2054 }
2055}
2056
2057#[derive(Debug)]
2060pub struct SupervisionOutcome {
2061 failure: MeshFailure,
2062 display_name: Option<String>,
2063 verdict: SupervisionVerdict,
2064}
2065
2066#[derive(Debug)]
2067enum SupervisionVerdict {
2068 Handled,
2069 Unhandled,
2070 Raised(String),
2072}
2073
2074#[async_trait]
2075impl Handler<MeshFailure> for PythonActor {
2076 async fn handle(&mut self, cx: &Context<Self>, message: MeshFailure) -> anyhow::Result<()> {
2077 if !message.event.actor_status.is_failed() {
2081 tracing::info!(
2082 "ignoring non-failure supervision event from child: {}",
2083 message
2084 );
2085 return Ok(());
2086 }
2087
2088 let pending = PendingSupervision {
2092 instance: self
2093 .instance
2094 .clone()
2095 .expect("PythonActor::init should have created the instance"),
2096 rank: cx.cast_point(),
2097 recording_span: cx.recording_span(),
2098 reply: SupervisionReply {
2099 port: cx.port(),
2100 instance: cx.into(),
2101 failure: message,
2102 display_name: None,
2103 _in_flight: InFlight::new(&self.callbacks_pending),
2104 },
2105 };
2106
2107 self.control_sender
2108 .send(pending)
2109 .map_err(|_| anyhow::anyhow!("failed to send supervision event to queue"))?;
2110 Ok(())
2111 }
2112}
2113
2114#[async_trait]
2115impl Handler<SupervisionOutcome> for PythonActor {
2116 async fn handle(
2117 &mut self,
2118 cx: &Context<Self>,
2119 outcome: SupervisionOutcome,
2120 ) -> anyhow::Result<()> {
2121 let SupervisionOutcome {
2122 failure: message,
2123 display_name,
2124 verdict,
2125 } = outcome;
2126 let (status, description, cause) = match verdict {
2127 SupervisionVerdict::Handled => {
2128 tracing::info!(
2131 name = "ActorMeshStatus",
2132 status = "SupervisionError::Handled",
2133 actor_name = message.actor_mesh_name,
2135 event = %message.event,
2136 "__supervise__ on {} handled a supervision event, not reporting any further",
2137 cx.self_addr(),
2138 );
2139 return Ok(());
2140 }
2141 SupervisionVerdict::Unhandled => (
2146 "SupervisionError::Unhandled",
2147 "did not handle a supervision event, reporting to the next owner",
2148 ActorErrorKind::UnhandledSupervisionEvent(Box::new(message.event.clone())),
2149 ),
2150 SupervisionVerdict::Raised(err) => (
2151 "SupervisionError::__supervise__::exception",
2152 "threw an exception",
2153 ActorErrorKind::ErrorDuringHandlingSupervision(
2154 err,
2155 Box::new(message.event.clone()),
2156 ),
2157 ),
2158 };
2159 for (actor_name, status) in [
2160 (
2161 message
2162 .actor_mesh_name
2163 .as_deref()
2164 .unwrap_or_else(|| message.event.actor_id.log_name()),
2165 status,
2166 ),
2167 (cx.self_addr().log_name(), "UnhandledSupervisionEvent"),
2168 ] {
2169 tracing::info!(
2170 name = "ActorMeshStatus",
2171 status,
2172 actor_name,
2173 event = %message.event,
2174 "__supervise__ on {} {}",
2175 cx.self_addr(),
2176 description,
2177 );
2178 }
2179 Err(anyhow::Error::new(
2180 ActorErrorKind::UnhandledSupervisionEvent(Box::new(ActorSupervisionEvent::new(
2181 cx.self_addr().clone(),
2182 display_name,
2183 ActorStatus::Failed(cause),
2184 None,
2185 ))),
2186 ))
2187 }
2188}
2189
2190#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
2191struct LocalPort {
2192 instance: PyInstance,
2193 inner: Option<OncePortHandle<Result<Py<PyAny>, Py<PyAny>>>>,
2194}
2195
2196impl Debug for LocalPort {
2197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2198 f.debug_struct("LocalPort")
2199 .field("inner", &self.inner)
2200 .finish()
2201 }
2202}
2203
2204#[pymethods]
2205impl LocalPort {
2206 fn send(&mut self, obj: Py<PyAny>) -> PyResult<()> {
2207 let port = self.inner.take().expect("use local port once");
2208 port.post(self.instance.deref(), Ok(obj));
2209 Ok(())
2210 }
2211 fn _resolve_and_send(&mut self, obj: Py<PyAny>) -> PyResult<()> {
2214 self.send(obj)
2215 }
2216 fn _resolve_and_send_blocking(&mut self, obj: Py<PyAny>) -> PyResult<()> {
2219 self.send(obj)
2220 }
2221 fn exception(&mut self, e: Py<PyAny>) -> PyResult<()> {
2222 let port = self.inner.take().expect("use local port once");
2223 port.post(self.instance.deref(), Err(e));
2224 Ok(())
2225 }
2226}
2227
2228#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
2232#[derive(Debug)]
2233pub struct DroppingPort;
2234
2235#[pymethods]
2236impl DroppingPort {
2237 #[new]
2238 fn new() -> Self {
2239 DroppingPort
2240 }
2241
2242 fn send(&self, _obj: Py<PyAny>) -> PyResult<()> {
2243 Ok(())
2244 }
2245
2246 fn _resolve_and_send(&self, obj: Py<PyAny>) -> PyResult<()> {
2249 self.send(obj)
2250 }
2251
2252 fn _resolve_and_send_blocking(&self, obj: Py<PyAny>) -> PyResult<()> {
2255 self.send(obj)
2256 }
2257
2258 fn exception(&self, e: Bound<'_, PyAny>) -> PyResult<()> {
2259 let exc = if let Ok(inner) = e.getattr("exception") {
2261 inner
2262 } else {
2263 e
2264 };
2265 Err(PyErr::from_value(exc))
2266 }
2267
2268 #[getter]
2269 fn get_return_undeliverable(&self) -> bool {
2270 true
2271 }
2272
2273 #[setter]
2274 fn set_return_undeliverable(&self, _value: bool) {}
2275}
2276
2277#[pyclass(module = "monarch._src.actor.actor_mesh")]
2280pub struct Port {
2281 port_ref: EitherPortRef,
2282 instance: Instance<PythonActor>,
2283 rank: Option<usize>,
2284 reply_headers: hyperactor_config::Flattrs,
2288}
2289
2290#[pymethods]
2291impl Port {
2292 #[new]
2293 fn new(
2294 port_ref: EitherPortRef,
2295 instance: &crate::context::PyInstance,
2296 rank: Option<usize>,
2297 ) -> Self {
2298 Self {
2299 port_ref,
2300 instance: instance.clone().into_instance(),
2301 rank,
2302 reply_headers: hyperactor_config::Flattrs::new(),
2303 }
2304 }
2305
2306 #[getter("_port_ref")]
2307 fn port_ref_py(&self) -> EitherPortRef {
2308 self.port_ref.clone()
2309 }
2310
2311 #[getter("_rank")]
2312 fn rank_py(&self) -> Option<usize> {
2313 self.rank
2314 }
2315
2316 #[getter]
2317 fn get_return_undeliverable(&self) -> bool {
2318 self.port_ref.get_return_undeliverable()
2319 }
2320
2321 #[setter]
2322 fn set_return_undeliverable(&mut self, value: bool) {
2323 self.port_ref.set_return_undeliverable(value);
2324 }
2325
2326 #[tracing::instrument(level = "debug", skip_all)]
2330 fn send(&mut self, py: Python<'_>, obj: Py<PyAny>) -> PyResult<()> {
2331 let message = PythonMessage::new_from_buf(
2332 PythonMessageKind::Result { rank: self.rank },
2333 pickle_to_part(py, &obj)?,
2334 );
2335
2336 self.port_ref
2337 .post_with_headers(&self.instance, self.reply_headers.clone(), message)
2338 .map_err(|e| PyRuntimeError::new_err(e.to_string()))
2339 }
2340
2341 #[tracing::instrument(level = "debug", skip_all)]
2342 fn _send_message(&mut self, message: PythonMessage) -> PyResult<()> {
2343 self.port_ref
2344 .post_with_headers(&self.instance, self.reply_headers.clone(), message)
2345 .map_err(|e| PyRuntimeError::new_err(e.to_string()))
2346 }
2347
2348 fn exception(&mut self, py: Python<'_>, e: Py<PyAny>) -> PyResult<()> {
2349 let message = PythonMessage::new_from_buf(
2350 PythonMessageKind::Exception { rank: self.rank },
2351 pickle_to_part(py, &e)?,
2352 );
2353
2354 self.port_ref
2355 .post_with_headers(&self.instance, self.reply_headers.clone(), message)
2356 .map_err(|e| PyRuntimeError::new_err(e.to_string()))
2357 }
2358}
2359
2360impl Port {
2361 pub(crate) fn with_reply_headers(
2365 port_ref: EitherPortRef,
2366 instance: Instance<PythonActor>,
2367 rank: Option<usize>,
2368 reply_headers: hyperactor_config::Flattrs,
2369 ) -> Self {
2370 Self {
2371 port_ref,
2372 instance,
2373 rank,
2374 reply_headers,
2375 }
2376 }
2377}
2378
2379pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
2380 hyperactor_mod.add_class::<PythonActorHandle>()?;
2381 hyperactor_mod.add_class::<PythonMessage>()?;
2382 hyperactor_mod.add_class::<PyMeshRef>()?;
2383 hyperactor_mod.add_class::<PythonMessageKind>()?;
2384 hyperactor_mod.add_class::<MethodSpecifier>()?;
2385 hyperactor_mod.add_class::<UnflattenArg>()?;
2386 hyperactor_mod.add_class::<QueuedMessage>()?;
2387 hyperactor_mod.add_class::<QueuedSupervision>()?;
2388 hyperactor_mod.add_class::<CallbacksPending>()?;
2389 hyperactor_mod.add_class::<QueuedUndeliverable>()?;
2390 hyperactor_mod.add_class::<DroppingPort>()?;
2391 hyperactor_mod.add_class::<Port>()?;
2392 hyperactor_mod.add_class::<ActorKind>()?;
2393 sync_actor::dispatch::register_python_bindings(hyperactor_mod)?;
2394 sync_actor::inbox::register_python_bindings(hyperactor_mod)?;
2395 Ok(())
2396}
2397
2398pub(crate) fn register_testing_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
2400 sync_actor::inbox::register_testing_python_bindings(module)
2401}
2402
2403#[cfg(test)]
2404mod tests {
2405 use futures::future::FutureExt;
2406 use hyperactor as reference;
2407 use hyperactor::accum::ReducerSpec;
2408 use hyperactor::accum::StreamingReducerOpts;
2409 use hyperactor::id::Label;
2410 use hyperactor::testing::ids::test_port_id;
2411 use hyperactor_mesh::Error as MeshError;
2412 use hyperactor_mesh::host_mesh::host_agent::ProcState;
2413 use hyperactor_mesh::mesh_id::ResourceId;
2414 use hyperactor_mesh::resource::Status;
2415 use hyperactor_mesh::resource::{self};
2416 use pyo3::PyTypeInfo;
2417 use pyo3::exceptions::PyValueError;
2418 use pyo3::ffi::c_str;
2419 use pyo3::panic::PanicException;
2420
2421 use super::*;
2422 use crate::actor::to_py_error;
2423
2424 #[test]
2425 fn test_call_method_indirect_correlation_id_round_trip() {
2426 let kind = PythonMessageKind::CallMethodIndirect {
2427 name: MethodSpecifier::ReturnsResponse {
2428 name: "forward".to_string(),
2429 },
2430 local_state_broker: ("broker".to_string(), 3),
2431 id: 7,
2432 unflatten_args: vec![UnflattenArg::Mailbox, UnflattenArg::PyObject],
2433 correlation_id: Some(42),
2434 };
2435
2436 let serialized = wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&kind)
2437 .expect("indirect call message should serialize");
2438 let decoded = serialized
2439 .deserialized_unchecked::<PythonMessageKind>()
2440 .expect("serialized indirect call message should deserialize");
2441
2442 assert_eq!(decoded, kind);
2443 }
2444
2445 #[test]
2446 fn test_python_message_part_codec() {
2447 let reducer_spec = ReducerSpec {
2448 typehash: 123,
2449 builder_params: Some(wirevalue::Any::serialize(&"abcdefg12345".to_string()).unwrap()),
2450 };
2451 let port_ref = hyperactor::PortRef::<PythonMessage>::attest_reducible(
2452 test_port_id("world_0", "client", 123),
2453 Some(reducer_spec),
2454 StreamingReducerOpts::default(),
2455 );
2456 let message = PythonMessage {
2457 kind: PythonMessageKind::CallMethod {
2458 name: MethodSpecifier::ReturnsResponse {
2459 name: "test".to_string(),
2460 },
2461 response_port: Some(EitherPortRef::Unbounded(port_ref.clone().into())),
2462 correlation_id: None,
2463 },
2464 message: Part::from(vec![1, 2, 3]),
2465 refs: Vec::new(),
2466 };
2467 {
2468 let mut multipart_message =
2469 wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&message).unwrap();
2470 let mut ports = vec![];
2471 multipart_message
2472 .visit_multipart_parts_mut::<reference::PortRefRepr, anyhow::Error>(|b| {
2473 ports.push(b.clone());
2474 Ok(())
2475 })
2476 .unwrap();
2477 assert_eq!(ports.len(), 1);
2478 assert_eq!(ports[0].port_addr(), port_ref.port_addr());
2479 assert_eq!(ports[0].reducer_spec(), port_ref.reducer_spec());
2480 assert_eq!(
2481 ports[0].get_return_undeliverable(),
2482 port_ref.get_return_undeliverable()
2483 );
2484 assert!(!ports[0].unsplit());
2485 assert_eq!(
2486 message,
2487 multipart_message
2488 .deserialized_unchecked::<PythonMessage>()
2489 .unwrap()
2490 );
2491 }
2492
2493 let no_port_message = PythonMessage {
2494 kind: PythonMessageKind::CallMethod {
2495 name: MethodSpecifier::ReturnsResponse {
2496 name: "test".to_string(),
2497 },
2498 response_port: None,
2499 correlation_id: None,
2500 },
2501 ..message
2502 };
2503 {
2504 let mut multipart_message =
2505 wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&no_port_message)
2506 .unwrap();
2507 let mut ports = vec![];
2508 multipart_message
2509 .visit_multipart_parts_mut::<reference::PortRefRepr, anyhow::Error>(|b| {
2510 ports.push(b.clone());
2511 Ok(())
2512 })
2513 .unwrap();
2514 assert_eq!(ports.len(), 0);
2515 assert_eq!(
2516 no_port_message,
2517 multipart_message
2518 .deserialized_unchecked::<PythonMessage>()
2519 .unwrap()
2520 );
2521 }
2522 }
2523
2524 #[test]
2525 fn test_python_message_refs_travel_as_parts() {
2526 fn proc_mesh_ref(seed: u64, label: &str) -> MeshRef {
2528 let proc_id = hyperactor::ProcId::new(
2529 hyperactor::id::Uid::Instance(seed, None),
2530 Some(Label::new("local").unwrap()),
2531 );
2532 let proc_addr = hyperactor::ProcAddr::new(
2533 proc_id,
2534 hyperactor::channel::ChannelAddr::Local(seed).into(),
2535 );
2536 let agent: hyperactor::ActorRef<hyperactor_mesh::proc_agent::ProcAgent> =
2537 hyperactor::ActorRef::attest(
2538 proc_addr.actor_addr(hyperactor_mesh::proc_agent::PROC_AGENT_ACTOR_NAME),
2539 );
2540 let proc_ref = hyperactor_mesh::proc_mesh::ProcRef::new(proc_addr, 0, agent);
2541 MeshRef::Proc(Box::new(
2542 hyperactor_mesh::proc_mesh::ProcMeshRef::new_singleton(
2543 hyperactor_mesh::mesh_id::ProcMeshId::singleton(Label::new(label).unwrap()),
2544 proc_ref,
2545 )
2546 .unwrap(),
2547 ))
2548 }
2549
2550 let message = PythonMessage {
2551 kind: PythonMessageKind::CallMethod {
2552 name: MethodSpecifier::ReturnsResponse {
2553 name: "test".to_string(),
2554 },
2555 response_port: None,
2556 correlation_id: None,
2557 },
2558 message: Part::from(vec![1, 2, 3]),
2559 refs: vec![proc_mesh_ref(1, "a"), proc_mesh_ref(2, "b")],
2560 };
2561
2562 let mut multipart_message =
2563 wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&message).unwrap();
2564 let mut parts = vec![];
2565 multipart_message
2566 .visit_multipart_parts_mut::<MeshRefRepr, anyhow::Error>(|b| {
2567 parts.push(b.clone());
2568 Ok(())
2569 })
2570 .unwrap();
2571 assert_eq!(parts.len(), 2);
2573 assert_eq!(
2575 message,
2576 multipart_message
2577 .deserialized_unchecked::<PythonMessage>()
2578 .unwrap()
2579 );
2580 }
2581
2582 #[test]
2583 fn to_py_error_preserves_proc_creation_message() {
2584 let state: resource::State<ProcState> = resource::State {
2586 id: ResourceId::instance(Label::new("my-proc").unwrap()),
2587 status: Status::Failed("boom".into()),
2588 state: None,
2589 generation: 0,
2590 timestamp: std::time::SystemTime::now(),
2591 };
2592
2593 let mesh_agent: hyperactor::ActorRef<hyperactor_mesh::host_mesh::HostAgent> =
2595 hyperactor::ActorRef::attest(test_port_id("hello_0", "actor", 0).actor_addr());
2596 let expected_prefix = format!(
2597 "error creating proc (host rank 0) on host mesh agent {}",
2598 mesh_agent
2599 );
2600 let err = MeshError::ProcCreationError {
2601 host_rank: 0,
2602 mesh_agent,
2603 state: Box::new(state),
2604 };
2605
2606 let rust_msg = err.to_string();
2607 let pyerr = to_py_error(err);
2608
2609 pyo3::Python::initialize();
2610 monarch_with_gil_blocking(GilSite::Test, |py| {
2611 assert!(pyerr.get_type(py).is(PyValueError::type_object(py)));
2612 let py_msg = pyerr.value(py).to_string();
2613
2614 assert_eq!(py_msg, rust_msg);
2616 assert!(py_msg.contains(", state: "));
2618 assert!(py_msg.contains("\"status\":{\"Failed\":\"boom\"}"));
2619 assert!(py_msg.starts_with(&expected_prefix));
2621 });
2622 }
2623
2624 fn local_port_fixture() -> (
2643 LocalPort,
2644 hyperactor::mailbox::OncePortReceiver<Result<Py<PyAny>, Py<PyAny>>>,
2645 ) {
2646 let proc = Proc::isolated();
2647 let instance = proc
2648 .actor_instance::<PythonActor>("resolve_and_send_client")
2649 .unwrap()
2650 .instance;
2651 let (handle, receiver) = instance.open_once_port::<Result<Py<PyAny>, Py<PyAny>>>();
2652 let port = LocalPort {
2653 instance: PyInstance::from(instance),
2654 inner: Some(handle),
2655 };
2656 (port, receiver)
2657 }
2658
2659 #[tokio::test]
2664 async fn local_port_resolve_and_send_posts_before_return() {
2665 pyo3::Python::initialize();
2666 let (mut port, receiver) = local_port_fixture();
2667
2668 monarch_with_gil_blocking(GilSite::Test, |py| {
2669 let value = 41i64.into_py_any(py).unwrap();
2670 port._resolve_and_send(value).unwrap()
2671 });
2672
2673 let received = receiver
2674 .recv()
2675 .now_or_never()
2676 .expect("the value must be posted before _resolve_and_send returns")
2677 .unwrap();
2678 monarch_with_gil_blocking(GilSite::Test, |py| {
2679 assert_eq!(
2680 received.unwrap().extract::<i64>(py).unwrap(),
2681 41,
2682 "the synchronously posted value must be preserved"
2683 );
2684 });
2685 }
2686
2687 #[tokio::test]
2690 async fn local_port_resolve_and_send_returns_none() {
2691 pyo3::Python::initialize();
2692 let (port, receiver) = local_port_fixture();
2693
2694 monarch_with_gil_blocking(GilSite::Test, |py| {
2695 let port = Py::new(py, port).unwrap();
2696 let returned = port
2697 .bind(py)
2698 .call_method1("_resolve_and_send", (42,))
2699 .unwrap();
2700 assert!(returned.is_none(), "direct completion must return None");
2701 });
2702
2703 let received = receiver
2704 .recv()
2705 .now_or_never()
2706 .expect("the value must be posted before _resolve_and_send returns")
2707 .unwrap();
2708 monarch_with_gil_blocking(GilSite::Test, |py| {
2709 assert_eq!(
2710 received.unwrap().extract::<i64>(py).unwrap(),
2711 42,
2712 "returning None must not change the posted value"
2713 );
2714 });
2715 }
2716
2717 #[tokio::test]
2732 async fn local_port_second_resolve_and_send_raises_panic_exception() {
2733 pyo3::Python::initialize();
2734 let (port, _receiver) = local_port_fixture();
2735
2736 monarch_with_gil_blocking(GilSite::Test, |py| {
2737 let locals = PyDict::new(py);
2738 locals.set_item("port", Py::new(py, port).unwrap()).unwrap();
2739 py.run(
2740 c_str!(
2741 r#"
2742assert port._resolve_and_send(1) is None
2743try:
2744 port._resolve_and_send(2)
2745except BaseException as err:
2746 raised = type(err)
2747 message = str(err)
2748else:
2749 raise AssertionError("a second _resolve_and_send must fail")
2750"#
2751 ),
2752 None,
2753 Some(&locals),
2754 )
2755 .unwrap();
2756
2757 let raised = locals.get_item("raised").unwrap().unwrap();
2758 let message: String = locals
2759 .get_item("message")
2760 .unwrap()
2761 .unwrap()
2762 .extract()
2763 .unwrap();
2764 assert!(
2765 raised.is(PanicException::type_object(py)),
2766 "a second send must surface as PanicException, got {raised}"
2767 );
2768 assert!(
2769 message.contains("use local port once"),
2770 "expected the one-shot panic message, got {message}"
2771 );
2772 });
2773 }
2774
2775 #[test]
2777 fn dropping_port_resolve_and_send_returns_none_and_is_idempotent() {
2778 pyo3::Python::initialize();
2779 monarch_with_gil_blocking(GilSite::Test, |py| {
2780 let port = Py::new(py, DroppingPort).unwrap();
2781 for value in [7, 8, 9] {
2782 let returned = port
2783 .bind(py)
2784 .call_method1("_resolve_and_send", (value,))
2785 .unwrap();
2786 assert!(
2787 returned.is_none(),
2788 "every DroppingPort completion must return None"
2789 );
2790 }
2791 });
2792 }
2793}