1use std::collections::HashMap;
10use std::collections::VecDeque;
11use std::error::Error;
12use std::fmt::Debug;
13use std::future::pending;
14use std::ops::Deref;
15use std::sync::Arc;
16use std::sync::Mutex;
17use std::sync::Once;
18use std::sync::OnceLock;
19use std::sync::atomic::AtomicU64;
20use std::sync::atomic::Ordering as AtomicOrdering;
21use std::time::SystemTime;
22
23use async_trait::async_trait;
24use hyperactor::Actor;
25use hyperactor::ActorHandle;
26use hyperactor::Context;
27use hyperactor::Endpoint as _;
28use hyperactor::Handler;
29use hyperactor::Instance;
30use hyperactor::OncePortHandle;
31use hyperactor::Proc;
32use hyperactor::RemoteSpawn;
33use hyperactor::actor::ActorError;
34use hyperactor::actor::ActorErrorKind;
35use hyperactor::actor::ActorStatus;
36use hyperactor::actor::Signal;
37use hyperactor::context::Actor as ContextActor;
38use hyperactor::mailbox::MessageEnvelope;
39use hyperactor::mailbox::Undeliverable;
40use hyperactor::mailbox::UndeliverableMessageError;
41use hyperactor::mailbox::UndeliverableReason;
42use hyperactor::supervision::ActorSupervisionEvent;
43use hyperactor_config::Flattrs;
44use hyperactor_mesh::ProcMeshRef;
45use hyperactor_mesh::actor_mesh::ActorMeshRef;
46use hyperactor_mesh::casting::update_undeliverable_envelope_for_casting;
47use hyperactor_mesh::comm::multicast::CAST_POINT;
48use hyperactor_mesh::comm::multicast::CastInfo;
49use hyperactor_mesh::host_mesh::HostMeshRef;
50use hyperactor_mesh::introspect::ActiveHandler;
51use hyperactor_mesh::introspect::EXECUTION;
52use hyperactor_mesh::introspect::Execution;
53use hyperactor_mesh::supervision::MeshFailure;
54use hyperactor_mesh::transport::default_bind_spec;
55use hyperactor_mesh::value_mesh::ValueOverlay;
56use monarch_types::PickledPyObject;
57use monarch_types::SerializablePyErr;
58use monarch_types::py_global;
59use ndslice::Point;
60use ndslice::extent;
61use pyo3::IntoPyObjectExt;
62use pyo3::exceptions::PyBaseException;
63use pyo3::exceptions::PyRuntimeError;
64use pyo3::exceptions::PyValueError;
65use pyo3::prelude::*;
66use pyo3::types::PyDict;
67use pyo3::types::PyList;
68use pyo3::types::PyType;
69use serde::Deserialize;
70use serde::Serialize;
71use serde_multipart::Part;
72use tokio::sync::mpsc;
73use tokio::sync::oneshot;
74use typeuri::Named;
75
76use crate::buffers::FrozenBuffer;
77use crate::config::ACTOR_QUEUE_DISPATCH;
78use crate::context::PyInstance;
79use crate::local_state_broker::BrokerId;
80use crate::local_state_broker::LocalStateBrokerMessage;
81use crate::mailbox::EitherPortRef;
82use crate::mailbox::PyMailbox;
83use crate::mailbox::PythonUndeliverableMessageEnvelope;
84use crate::metrics::ENDPOINT_ACTOR_COUNT;
85use crate::metrics::ENDPOINT_ACTOR_ERROR;
86use crate::metrics::ENDPOINT_ACTOR_LATENCY_US_HISTOGRAM;
87use crate::metrics::ENDPOINT_ACTOR_PANIC;
88use crate::pickle::PicklingState;
89use crate::pickle::pickle_to_part;
90use crate::proc::PyActorAddr;
91use crate::pympsc;
92use crate::pytokio::PyPythonTask;
93use crate::pytokio::PythonTask;
94use crate::runtime::GilSite;
95use crate::runtime::get_tokio_runtime;
96use crate::runtime::monarch_with_gil;
97use crate::runtime::monarch_with_gil_blocking;
98use crate::supervision::PyMeshFailure;
99
100py_global!(
101 unhandled_fault_hook_exception,
102 "monarch._src.actor.supervision",
103 "UnhandledFaultHookException"
104);
105
106#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
107#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
108pub enum UnflattenArg {
109 Mailbox,
110 PyObject,
111}
112
113#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
114#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
115pub enum MethodSpecifier {
116 ReturnsResponse { name: String },
118 ExplicitPort { name: String },
120 Init {},
122}
123
124impl std::fmt::Display for MethodSpecifier {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}", self.name())
127 }
128}
129
130#[pymethods]
131impl MethodSpecifier {
132 #[getter(name)]
133 fn py_name(&self) -> &str {
134 self.name()
135 }
136}
137
138impl MethodSpecifier {
139 pub(crate) fn name(&self) -> &str {
140 match self {
141 MethodSpecifier::ReturnsResponse { name } => name,
142 MethodSpecifier::ExplicitPort { name } => name,
143 MethodSpecifier::Init {} => "__init__",
144 }
145 }
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
155pub enum PythonResponseMessage {
156 Result {
157 part: serde_multipart::Part,
158 refs: Vec<MeshRef>,
159 },
160 Exception {
161 part: serde_multipart::Part,
162 refs: Vec<MeshRef>,
163 },
164}
165
166wirevalue::register_type!(PythonResponseMessage);
167wirevalue::register_type!(ValueOverlay<PythonResponseMessage>);
168
169impl PythonResponseMessage {
170 pub(crate) fn decode(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
174 let (part, refs) = match self {
175 PythonResponseMessage::Result { part, refs }
176 | PythonResponseMessage::Exception { part, refs } => (part, refs),
177 };
178 let mesh_references = refs.iter().cloned().map(Some).collect();
179 let mut state = PicklingState::from_parts(part.clone(), VecDeque::new(), mesh_references);
180 state.unpickle(py)
181 }
182}
183
184#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
189#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
190pub struct AccumulatedResponses(ValueOverlay<PythonResponseMessage>);
191
192#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
193#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq)]
194pub enum PythonMessageKind {
195 CallMethod {
196 name: MethodSpecifier,
197 response_port: Option<EitherPortRef>,
198 },
199 Result {
200 rank: Option<usize>,
201 },
202 Exception {
203 rank: Option<usize>,
204 },
205 Uninit {},
206 CallMethodIndirect {
207 name: MethodSpecifier,
208 local_state_broker: (String, usize),
209 id: usize,
210 unflatten_args: Vec<UnflattenArg>,
213 },
214 AccumulatedResponses(AccumulatedResponses),
215}
216wirevalue::register_type!(PythonMessageKind);
217
218impl Default for PythonMessageKind {
219 fn default() -> Self {
220 PythonMessageKind::Uninit {}
221 }
222}
223
224fn mailbox<'py, T: Actor>(py: Python<'py>, cx: &Context<'_, T>) -> Bound<'py, PyAny> {
225 let mailbox: PyMailbox = cx.mailbox_for_py().clone().into();
226 mailbox.into_bound_py_any(py).unwrap()
227}
228
229#[derive(Clone, Debug, Named, PartialEq, Eq)]
235pub enum MeshRef {
236 Actor(Box<ActorMeshRef<PythonActor>>),
237 Proc(Box<ProcMeshRef>),
238 Host(Box<HostMeshRef>),
239}
240
241#[doc(hidden)]
243#[derive(Clone, Debug, Serialize, Deserialize, Named)]
244pub enum MeshRefRepr {
245 Actor(Box<ActorMeshRef<PythonActor>>),
246 Proc(Box<ProcMeshRef>),
247 Host(Box<HostMeshRef>),
248}
249
250impl TryFrom<&MeshRef> for MeshRefRepr {
251 type Error = serde_multipart::Error;
252 fn try_from(m: &MeshRef) -> serde_multipart::Result<Self> {
253 Ok(match m {
254 MeshRef::Actor(r) => MeshRefRepr::Actor(r.clone()),
255 MeshRef::Proc(r) => MeshRefRepr::Proc(r.clone()),
256 MeshRef::Host(r) => MeshRefRepr::Host(r.clone()),
257 })
258 }
259}
260
261impl TryFrom<MeshRefRepr> for MeshRef {
262 type Error = serde_multipart::Error;
263 fn try_from(r: MeshRefRepr) -> serde_multipart::Result<Self> {
264 Ok(match r {
265 MeshRefRepr::Actor(r) => MeshRef::Actor(r),
266 MeshRefRepr::Proc(r) => MeshRef::Proc(r),
267 MeshRefRepr::Host(r) => MeshRef::Host(r),
268 })
269 }
270}
271
272serde_multipart::part_codec! {
273 impl MeshRef
274 {
275 type Repr = MeshRefRepr;
276 }
277}
278
279impl MeshRef {
280 pub(crate) fn reconstruct(self, py: Python<'_>) -> PyResult<Py<PyAny>> {
285 match self {
286 MeshRef::Proc(r) => {
287 Ok(Py::new(py, crate::proc_mesh::PyProcMesh::new_ref(*r))?.into_any())
288 }
289 MeshRef::Host(r) => {
290 Ok(Py::new(py, crate::host_mesh::PyHostMesh::new_ref(*r))?.into_any())
291 }
292 MeshRef::Actor(r) => {
293 let inner = crate::actor_mesh::PythonActorMeshImpl::new_ref(*r);
294 let async_mesh = crate::actor_mesh::AsyncActorMesh::from_impl(Arc::new(inner));
295 let mesh = crate::actor_mesh::PythonActorMesh::from_impl(Arc::from(async_mesh));
296 Ok(Py::new(py, mesh)?.into_any())
297 }
298 }
299 }
300}
301
302#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
305pub struct PyMeshRef {
306 pub(crate) inner: MeshRef,
307}
308
309impl<'py> IntoPyObject<'py> for MeshRef {
310 type Target = PyMeshRef;
311 type Output = Bound<'py, PyMeshRef>;
312 type Error = PyErr;
313
314 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
315 Bound::new(py, PyMeshRef { inner: self })
316 }
317}
318
319pub(crate) fn mesh_ref_from_pyobject(value: &Bound<'_, PyAny>) -> PyResult<MeshRef> {
322 if let Ok(m) = value.downcast::<crate::proc_mesh::PyProcMesh>() {
323 return Ok(MeshRef::Proc(Box::new(m.borrow().mesh_ref()?)));
324 }
325 if let Ok(m) = value.downcast::<crate::host_mesh::PyHostMesh>() {
326 return Ok(MeshRef::Host(Box::new(m.borrow().mesh_ref().map_err(
327 |e| pyo3::exceptions::PyValueError::new_err(e.to_string()),
328 )?)));
329 }
330 if let Ok(m) = value.downcast::<crate::actor_mesh::PythonActorMesh>() {
331 return Ok(MeshRef::Actor(Box::new(m.borrow().get_inner().mesh_ref()?)));
332 }
333 Err(pyo3::exceptions::PyRuntimeError::new_err(
334 "pending pickle did not resolve to a mesh reference",
335 ))
336}
337
338#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
339#[derive(Clone, Serialize, Deserialize, Named, Default, PartialEq)]
340pub struct PythonMessage {
341 pub kind: PythonMessageKind,
342 pub message: Part,
343 pub refs: Vec<MeshRef>,
345}
346
347fn python_message_endpoint_name(msg: &PythonMessage) -> Option<String> {
349 match &msg.kind {
350 PythonMessageKind::CallMethod { name, .. }
351 | PythonMessageKind::CallMethodIndirect { name, .. } => Some(name.name().to_string()),
352 _ => None,
353 }
354}
355
356wirevalue::submit! {
361 wirevalue::TypeInfo {
362 typename: <PythonMessage as wirevalue::Named>::typename,
363 typehash: <PythonMessage as wirevalue::Named>::typehash,
364 typeid: <PythonMessage as wirevalue::Named>::typeid,
365 port: <PythonMessage as wirevalue::Named>::port,
366 dump: Some(<PythonMessage as wirevalue::NamedDumpable>::dump),
367 arm_unchecked: <PythonMessage as wirevalue::Named>::arm_unchecked,
368 endpoint_name: |ptr| {
369 let msg = unsafe { &*(ptr as *const PythonMessage) };
371 python_message_endpoint_name(msg)
372 },
373 }
374}
375
376impl From<ValueOverlay<PythonResponseMessage>> for PythonMessage {
377 fn from(overlay: ValueOverlay<PythonResponseMessage>) -> Self {
378 PythonMessage {
379 kind: PythonMessageKind::AccumulatedResponses(AccumulatedResponses(overlay)),
380 message: Default::default(),
381 refs: Vec::new(),
382 }
383 }
384}
385
386impl PythonMessage {
387 pub fn into_overlay(self) -> anyhow::Result<ValueOverlay<PythonResponseMessage>> {
392 match self.kind {
393 PythonMessageKind::AccumulatedResponses(overlay) => Ok(overlay.0),
394 PythonMessageKind::Result { rank, .. } => {
395 let rank = rank.expect("accumulated response should have a rank");
396 let mut overlay = ValueOverlay::new();
397 overlay.push_run(
398 rank..rank + 1,
399 PythonResponseMessage::Result {
400 part: self.message,
401 refs: self.refs,
402 },
403 )?;
404 Ok(overlay)
405 }
406 PythonMessageKind::Exception { rank, .. } => {
407 let rank = rank.expect("accumulated exception should have a rank");
408 let mut overlay = ValueOverlay::new();
409 overlay.push_run(
410 rank..rank + 1,
411 PythonResponseMessage::Exception {
412 part: self.message,
413 refs: self.refs,
414 },
415 )?;
416 Ok(overlay)
417 }
418 other => {
419 anyhow::bail!(
420 "unexpected message kind {:?} in collected responses reducer",
421 other
422 );
423 }
424 }
425 }
426}
427
428struct ResolvedCallMethod {
429 method: MethodSpecifier,
430 bytes: FrozenBuffer,
431 local_state: Option<Py<PyAny>>,
432 mesh_references: Vec<MeshRef>,
433 response_port: ResponsePort,
436}
437
438enum ResponsePort {
439 Dropping,
440 Port(Port),
441 Local(LocalPort),
442}
443
444impl ResponsePort {
445 fn into_py_any(self, py: Python<'_>) -> PyResult<Py<PyAny>> {
446 match self {
447 ResponsePort::Dropping => DroppingPort.into_py_any(py),
448 ResponsePort::Port(port) => port.into_py_any(py),
449 ResponsePort::Local(port) => port.into_py_any(py),
450 }
451 }
452}
453
454#[pyclass(frozen, module = "monarch._rust_bindings.monarch_hyperactor.actor")]
457pub struct QueuedMessage {
458 #[pyo3(get)]
459 pub context: Py<crate::context::PyContext>,
460 #[pyo3(get)]
461 pub method: MethodSpecifier,
462 #[pyo3(get)]
463 pub bytes: FrozenBuffer,
464 #[pyo3(get)]
465 pub local_state: Py<PyAny>,
466 #[pyo3(get)]
467 pub refs: Py<PyAny>,
468 #[pyo3(get)]
469 pub response_port: Py<PyAny>,
470}
471
472impl PythonMessage {
473 pub fn new_from_buf(kind: PythonMessageKind, message: impl Into<Part>) -> Self {
474 Self::new_from_buf_with_refs(kind, message, Vec::new())
475 }
476
477 pub fn new_from_buf_with_refs(
478 kind: PythonMessageKind,
479 message: impl Into<Part>,
480 refs: Vec<MeshRef>,
481 ) -> Self {
482 Self {
483 kind,
484 message: message.into(),
485 refs,
486 }
487 }
488
489 pub fn into_rank(self, rank: usize) -> Self {
490 let rank = Some(rank);
491 match self.kind {
492 PythonMessageKind::Result { .. } => PythonMessage {
493 kind: PythonMessageKind::Result { rank },
494 message: self.message,
495 refs: self.refs,
496 },
497 PythonMessageKind::Exception { .. } => PythonMessage {
498 kind: PythonMessageKind::Exception { rank },
499 message: self.message,
500 refs: self.refs,
501 },
502 _ => panic!("PythonMessage is not a response but {:?}", self),
503 }
504 }
505 async fn resolve_indirect_call(
506 self,
507 cx: &Context<'_, PythonActor>,
508 ) -> anyhow::Result<ResolvedCallMethod> {
509 match self.kind {
510 PythonMessageKind::CallMethodIndirect {
511 name,
512 local_state_broker,
513 id,
514 unflatten_args,
515 } => {
516 let broker = BrokerId::new(local_state_broker).resolve(cx).await;
517 let (send, recv) = cx.open_once_port();
518 broker.post(cx, LocalStateBrokerMessage::Get(id, send));
519 let state = recv.recv().await?;
520 let mut state_it = state.state.into_iter();
521 monarch_with_gil(GilSite::EndpointDispatch, |py| {
522 let mailbox = mailbox(py, cx);
523 let local_state = Some(
524 PyList::new(
525 py,
526 unflatten_args.into_iter().map(|x| -> Bound<'_, PyAny> {
527 match x {
528 UnflattenArg::Mailbox => mailbox.clone(),
529 UnflattenArg::PyObject => {
530 state_it.next().unwrap().into_bound(py)
531 }
532 }
533 }),
534 )
535 .unwrap()
536 .into(),
537 );
538 let response_port = ResponsePort::Local(LocalPort {
539 instance: cx.into(),
540 inner: Some(state.response_port),
541 });
542 Ok(ResolvedCallMethod {
543 method: name,
544 bytes: FrozenBuffer {
545 inner: self.message.into_bytes(),
546 },
547 local_state,
548 mesh_references: self.refs,
549 response_port,
550 })
551 })
552 .await
553 }
554 PythonMessageKind::CallMethod {
555 name,
556 response_port,
557 } => {
558 let method_name = name.name().to_string();
559 let response_port = response_port.map_or(ResponsePort::Dropping, |port_ref| {
560 let point = cx.cast_point();
561 let mut reply_headers = hyperactor_config::Flattrs::new();
566 hyperactor_config::attrs::copy_marked_flattrs(
567 &mut reply_headers,
568 cx.headers(),
569 hyperactor_config::attrs::OPERATION_CONTEXT_HEADER,
570 );
571 if reply_headers
572 .get(hyperactor::mailbox::headers::OPERATION_ENDPOINT)
573 .is_none()
574 {
575 reply_headers.set(
576 hyperactor::mailbox::headers::OPERATION_ENDPOINT,
577 format!("{}()", method_name),
578 );
579 }
580 ResponsePort::Port(Port::with_reply_headers(
581 port_ref,
582 cx.instance().clone_for_py(),
583 Some(point.rank()),
584 reply_headers,
585 ))
586 });
587 Ok(ResolvedCallMethod {
588 method: name,
589 bytes: FrozenBuffer {
590 inner: self.message.into_bytes(),
591 },
592 local_state: None,
593 mesh_references: self.refs,
594 response_port,
595 })
596 }
597 _ => {
598 panic!("unexpected message kind {:?}", self.kind)
599 }
600 }
601 }
602}
603
604impl std::fmt::Debug for PythonMessage {
605 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606 f.debug_struct("PythonMessage")
607 .field("kind", &self.kind)
608 .field(
609 "message",
610 &wirevalue::HexFmt(&(*self.message.to_bytes())[..]).to_string(),
611 )
612 .field("refs", &self.refs.len())
613 .finish()
614 }
615}
616
617#[pymethods]
618impl PythonMessage {
619 #[new]
620 #[pyo3(signature = (kind, message, refs))]
621 pub fn new(
622 kind: PythonMessageKind,
623 message: PyRef<'_, FrozenBuffer>,
624 refs: &Bound<'_, PyList>,
625 ) -> PyResult<Self> {
626 let mesh_refs: Vec<MeshRef> = refs
627 .iter()
628 .map(|item| Ok(item.downcast::<PyMeshRef>()?.borrow().inner.clone()))
629 .collect::<PyResult<_>>()?;
630 Ok(PythonMessage::new_from_buf_with_refs(
631 kind,
632 message.inner.clone(),
633 mesh_refs,
634 ))
635 }
636
637 #[getter]
638 fn kind(&self) -> PythonMessageKind {
639 self.kind.clone()
640 }
641
642 #[pyo3(signature = (local_state=None))]
647 fn decode(
648 &self,
649 py: Python<'_>,
650 local_state: Option<&Bound<'_, PyList>>,
651 ) -> PyResult<Py<PyAny>> {
652 let tensor_engine_references: VecDeque<Py<PyAny>> = local_state
653 .map(|list| list.iter().map(|item| item.unbind()).collect())
654 .unwrap_or_default();
655 let mesh_references: VecDeque<Option<MeshRef>> =
656 self.refs.iter().cloned().map(Some).collect();
657 let mut state = PicklingState::from_parts(
658 self.message.clone(),
659 tensor_engine_references,
660 mesh_references,
661 );
662 state.unpickle(py)
663 }
664
665 #[getter]
666 fn refs(&self) -> Vec<MeshRef> {
667 self.refs.clone()
668 }
669}
670
671#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
672pub(super) struct PythonActorHandle {
673 pub(super) inner: ActorHandle<PythonActor>,
674}
675
676#[pymethods]
677impl PythonActorHandle {
678 fn send(&self, instance: &PyInstance, message: &PythonMessage) -> PyResult<()> {
680 self.inner.post(instance.deref(), message.clone());
681 Ok(())
682 }
683
684 fn bind(&self) -> PyActorAddr {
685 self.inner.bind::<PythonActor>().into_actor_addr().into()
686 }
687}
688
689#[derive(Debug)]
691pub enum PythonActorDispatchMode {
692 Direct,
694 Queue {
696 sender: pympsc::Sender,
698 receiver: Option<pympsc::PyReceiver>,
700 },
701}
702
703const MAX_ACTIVE_HANDLERS: usize = 64;
721
722#[derive(Debug)]
724struct ActiveEntry {
725 name: String,
726 started_at: SystemTime,
727}
728
729#[derive(Debug)]
734pub(crate) struct ExecutionTracker {
735 active_count: AtomicU64,
737 next_token: AtomicU64,
740 handlers: Mutex<HashMap<u64, ActiveEntry>>,
742}
743
744fn aggregate_active(
749 handlers: &HashMap<u64, ActiveEntry>,
750 max: usize,
751) -> (Vec<ActiveHandler>, bool) {
752 let mut by_name: HashMap<&str, (u64, SystemTime)> = HashMap::new();
753 for entry in handlers.values() {
754 let slot = by_name
755 .entry(entry.name.as_str())
756 .or_insert((0, entry.started_at));
757 slot.0 += 1;
758 if entry.started_at < slot.1 {
759 slot.1 = entry.started_at;
760 }
761 }
762 let mut out: Vec<ActiveHandler> = by_name
763 .into_iter()
764 .map(|(name, (active_count, oldest_since))| ActiveHandler {
765 name: name.to_string(),
766 active_count,
767 oldest_since,
768 })
769 .collect();
770 out.sort_by(|a, b| {
772 a.oldest_since
773 .cmp(&b.oldest_since)
774 .then_with(|| a.name.cmp(&b.name))
775 });
776 let truncated = out.len() > max;
777 if truncated {
778 out.truncate(max);
779 }
780 (out, truncated)
781}
782
783impl ExecutionTracker {
784 pub(crate) fn new() -> Self {
785 Self {
786 active_count: AtomicU64::new(0),
787 next_token: AtomicU64::new(1),
788 handlers: Mutex::new(HashMap::new()),
789 }
790 }
791
792 pub(crate) fn start(&self, name: String) -> u64 {
794 let token = self.next_token.fetch_add(1, AtomicOrdering::Relaxed);
795 self.handlers
796 .lock()
797 .unwrap_or_else(|e| e.into_inner())
798 .insert(
799 token,
800 ActiveEntry {
801 name,
802 started_at: SystemTime::now(),
803 },
804 );
805 self.active_count.fetch_add(1, AtomicOrdering::Relaxed);
806 token
807 }
808
809 pub(crate) fn finish(&self, token: u64) {
812 if token == 0 {
813 return;
814 }
815 let removed = self
816 .handlers
817 .lock()
818 .unwrap_or_else(|e| e.into_inner())
819 .remove(&token)
820 .is_some();
821 if removed {
822 self.active_count.fetch_sub(1, AtomicOrdering::Relaxed);
823 }
824 }
825
826 pub(crate) fn snapshot(&self) -> Execution {
831 let active_count = self.active_count.load(AtomicOrdering::Relaxed);
832 match self.handlers.try_lock() {
833 Ok(guard) => {
834 let (active_handlers, truncated) = aggregate_active(&guard, MAX_ACTIVE_HANDLERS);
835 Execution {
836 active_count,
837 active_handlers,
838 complete: true,
839 truncated,
840 }
841 }
842 Err(_) => Execution {
843 active_count,
844 active_handlers: Vec::new(),
845 complete: false,
846 truncated: false,
847 },
848 }
849 }
850}
851
852#[cfg(test)]
853mod execution_tracker_tests {
854 use std::time::Duration;
855 use std::time::UNIX_EPOCH;
856
857 use super::*;
858
859 fn at(secs: u64) -> SystemTime {
860 UNIX_EPOCH + Duration::from_secs(secs)
861 }
862
863 #[test]
864 fn aggregates_by_name_oldest_first() {
865 let mut h = HashMap::new();
866 h.insert(
867 1,
868 ActiveEntry {
869 name: "b".to_string(),
870 started_at: at(10),
871 },
872 );
873 h.insert(
874 2,
875 ActiveEntry {
876 name: "a".to_string(),
877 started_at: at(20),
878 },
879 );
880 h.insert(
881 3,
882 ActiveEntry {
883 name: "a".to_string(),
884 started_at: at(30),
885 },
886 );
887 let (out, truncated) = aggregate_active(&h, MAX_ACTIVE_HANDLERS);
888 assert!(!truncated);
889 assert_eq!(out.len(), 2);
890 assert_eq!(out[0].name, "b");
892 assert_eq!(out[0].active_count, 1);
893 assert_eq!(out[0].oldest_since, at(10));
894 assert_eq!(out[1].name, "a");
896 assert_eq!(out[1].active_count, 2);
897 assert_eq!(out[1].oldest_since, at(20));
898 }
899
900 #[test]
901 fn tie_break_on_name_when_same_oldest() {
902 let mut h = HashMap::new();
903 h.insert(
904 1,
905 ActiveEntry {
906 name: "zebra".to_string(),
907 started_at: at(5),
908 },
909 );
910 h.insert(
911 2,
912 ActiveEntry {
913 name: "alpha".to_string(),
914 started_at: at(5),
915 },
916 );
917 let (out, _) = aggregate_active(&h, MAX_ACTIVE_HANDLERS);
918 assert_eq!(out[0].name, "alpha");
919 assert_eq!(out[1].name, "zebra");
920 }
921
922 #[test]
923 fn truncates_to_n_oldest() {
924 let mut h = HashMap::new();
925 for i in 0..(MAX_ACTIVE_HANDLERS as u64 + 6) {
926 h.insert(
927 i,
928 ActiveEntry {
929 name: format!("h{:03}", i),
930 started_at: at(i),
931 },
932 );
933 }
934 let (out, truncated) = aggregate_active(&h, MAX_ACTIVE_HANDLERS);
935 assert!(truncated);
936 assert_eq!(out.len(), MAX_ACTIVE_HANDLERS);
937 assert_eq!(out[0].name, "h000");
939 assert_eq!(
940 out[MAX_ACTIVE_HANDLERS - 1].name,
941 format!("h{:03}", MAX_ACTIVE_HANDLERS - 1)
942 );
943 }
944
945 #[test]
946 fn start_assigns_nonzero_distinct_tokens() {
947 let t = ExecutionTracker::new();
948 let a = t.start("a".to_string());
949 let b = t.start("b".to_string());
950 assert!(a >= 1);
951 assert!(b >= 1);
952 assert_ne!(a, b);
953 let snap = t.snapshot();
954 assert_eq!(snap.active_count, 2);
955 assert!(snap.complete);
956 assert_eq!(snap.active_handlers.len(), 2);
957 }
958
959 #[test]
960 fn finish_is_idempotent_and_zero_is_noop() {
961 let t = ExecutionTracker::new();
962 let tok = t.start("a".to_string());
963 t.finish(tok);
964 assert_eq!(t.snapshot().active_count, 0);
965 t.finish(tok);
967 assert_eq!(t.snapshot().active_count, 0);
968 t.finish(0);
970 assert_eq!(t.snapshot().active_count, 0);
971 }
972}
973
974#[derive(Debug)]
976#[hyperactor::export(
977 handlers = [
978 PythonMessage,
979 MeshFailure,
980 ],
981)]
982#[hyperactor::spawnable]
983pub struct PythonActor {
984 actor: Py<PyAny>,
986 task_locals: pyo3_async_runtimes::TaskLocals,
988 instance: Option<Py<crate::context::PyInstance>>,
991 dispatch_mode: PythonActorDispatchMode,
993 spawn_point: OnceLock<Option<Point>>,
995 init_message: Option<PythonMessage>,
997 mesh_base_name: Option<String>,
1006
1007 execution_tracker: Arc<ExecutionTracker>,
1012}
1013
1014impl PythonActor {
1015 pub(crate) fn new(
1016 actor_type: PickledPyObject,
1017 init_message: Option<PythonMessage>,
1018 spawn_point: Option<Point>,
1019 mesh_base_name: Option<String>,
1020 ) -> Result<Self, anyhow::Error> {
1021 let use_queue_dispatch = hyperactor_config::global::get(ACTOR_QUEUE_DISPATCH);
1022 if !use_queue_dispatch {
1023 static WARNED: Once = Once::new();
1024 WARNED.call_once(|| {
1025 tracing::warn!(
1026 "actor_queue_dispatch=false is deprecated and direct dispatch will be removed in a future release"
1027 );
1028 });
1029 }
1030
1031 Ok(monarch_with_gil_blocking(
1032 GilSite::ActorConstruct,
1033 |py| -> Result<Self, SerializablePyErr> {
1034 let unpickled = actor_type.unpickle(py)?;
1035 let class_type: &Bound<'_, PyType> = unpickled.downcast()?;
1036 let actor: Py<PyAny> = class_type.call0()?.into_py_any(py)?;
1037
1038 let task_locals = Python::detach(py, create_task_locals);
1039
1040 let dispatch_mode = if use_queue_dispatch {
1041 let (sender, receiver) = pympsc::channel().map_err(|e| {
1042 let py_err = PyRuntimeError::new_err(e.to_string());
1043 SerializablePyErr::from(py, &py_err)
1044 })?;
1045 PythonActorDispatchMode::Queue {
1046 sender,
1047 receiver: Some(receiver),
1048 }
1049 } else {
1050 PythonActorDispatchMode::Direct
1051 };
1052
1053 Ok(Self {
1054 actor,
1055 task_locals,
1056 instance: None,
1057 dispatch_mode,
1058 spawn_point: OnceLock::from(spawn_point),
1059 init_message,
1060 mesh_base_name,
1061 execution_tracker: Arc::new(ExecutionTracker::new()),
1062 })
1063 },
1064 )?)
1065 }
1066
1067 fn cancel_tasks_and_stop_python_loop(
1068 py: Python<'_>,
1069 task_locals: &pyo3_async_runtimes::TaskLocals,
1070 ) -> PyResult<()> {
1071 let asyncio = py.import("asyncio")?;
1072 let event_loop = task_locals.event_loop(py);
1073 let tasks = asyncio.call_method1("all_tasks", (&event_loop,))?;
1074 let mut has_tasks = false;
1075 for task in tasks.try_iter()? {
1076 let task = task?;
1077 let cancel = task.getattr("cancel")?;
1078 event_loop.call_method1("call_soon_threadsafe", (cancel,))?;
1079 has_tasks = true;
1080 }
1081 if has_tasks {
1082 asyncio
1083 .call_method1(
1084 "run_coroutine_threadsafe",
1085 (asyncio.call_method1("sleep", (0,))?, &event_loop),
1086 )?
1087 .call_method0("result")?;
1088 }
1089 let stop = event_loop.getattr("stop")?;
1090 event_loop.call_method1("call_soon_threadsafe", (stop,))?;
1091 Ok(())
1092 }
1093
1094 fn cancel_pending_python_tasks_and_stop_loop(&self) -> anyhow::Result<()> {
1095 let task_locals = &self.task_locals;
1096 monarch_with_gil_blocking(GilSite::Stop, |py| -> anyhow::Result<()> {
1097 Self::cancel_tasks_and_stop_python_loop(py, task_locals)
1098 .map_err(|err| anyhow::Error::from(SerializablePyErr::from(py, &err)))?;
1099 Ok(())
1100 })
1101 }
1102
1103 fn ensure_py_instance(
1109 &mut self,
1110 py: Python<'_>,
1111 src: impl Into<crate::context::PyInstance>,
1112 ) -> Py<crate::context::PyInstance> {
1113 let tracker = self.execution_tracker.clone();
1114 self.instance
1115 .get_or_insert_with(|| {
1116 let mut inst: crate::context::PyInstance = src.into();
1117 inst.set_execution_tracker(tracker);
1118 inst.into_pyobject(py).unwrap().into()
1119 })
1120 .clone_ref(py)
1121 }
1122
1123 pub(crate) fn bootstrap_client(py: Python<'_>) -> (&'static Instance<Self>, ActorHandle<Self>) {
1126 static ROOT_CLIENT_INSTANCE: OnceLock<Instance<PythonActor>> = OnceLock::new();
1127
1128 let client_proc = Proc::direct(
1129 default_bind_spec().binding_addr(),
1130 "mesh_root_client_proc".into(),
1131 )
1132 .unwrap();
1133
1134 Self::bootstrap_client_inner(py, client_proc, &ROOT_CLIENT_INSTANCE)
1135 }
1136
1137 pub(crate) fn bootstrap_client_inner(
1141 py: Python<'_>,
1142 client_proc: Proc,
1143 root_client_instance: &'static OnceLock<Instance<PythonActor>>,
1144 ) -> (&'static Instance<Self>, ActorHandle<Self>) {
1145 let actor_mesh_mod = py
1146 .import("monarch._src.actor.actor_mesh")
1147 .expect("import actor_mesh");
1148 let root_client_class = actor_mesh_mod
1149 .getattr("RootClientActor")
1150 .expect("get RootClientActor");
1151
1152 let actor_type =
1153 PickledPyObject::pickle(&actor_mesh_mod.getattr("_Actor").expect("get _Actor"))
1154 .expect("pickle _Actor");
1155
1156 let init_frozen_buffer: FrozenBuffer = root_client_class
1157 .call_method0("_pickled_init_args")
1158 .expect("call RootClientActor._pickled_init_args")
1159 .extract()
1160 .expect("extract FrozenBuffer from _pickled_init_args");
1161 let init_message = PythonMessage::new_from_buf(
1162 PythonMessageKind::CallMethod {
1163 name: MethodSpecifier::Init {},
1164 response_port: None,
1165 },
1166 init_frozen_buffer,
1167 );
1168
1169 let mut actor = PythonActor::new(
1170 actor_type,
1171 Some(init_message),
1172 Some(extent!().point_of_rank(0).unwrap()),
1173 None, )
1175 .expect("create client PythonActor");
1176
1177 let ai = client_proc
1178 .actor_instance(
1179 root_client_class
1180 .getattr("name")
1181 .expect("get RootClientActor.name")
1182 .extract()
1183 .expect("extract RootClientActor.name"),
1184 )
1185 .expect("root instance create");
1186
1187 let handle = ai.handle;
1188 let signal_rx = ai.signal;
1189 let supervision_rx = ai.supervision;
1190 let work_rx = ai.work;
1191
1192 root_client_instance
1193 .set(ai.instance)
1194 .map_err(|_| "already initialized root client instance")
1195 .unwrap();
1196 let instance = root_client_instance.get().unwrap();
1197
1198 instance.set_system();
1202
1203 let _client_ref = handle.bind::<PythonActor>();
1205
1206 get_tokio_runtime().spawn(async move {
1207 actor.init(instance).await.unwrap();
1209
1210 let mut signal_rx = signal_rx;
1211 let mut supervision_rx = supervision_rx;
1212 let mut work_rx = work_rx;
1213 let mut need_drain = false;
1214 let mut err = 'messages: loop {
1215 tokio::select! {
1216 work = work_rx.recv() => {
1217 let work = work.expect("inconsistent work queue state");
1218 if let Err(err) = work.handle(&mut actor, instance).await {
1219 let is_hook_exception = monarch_with_gil(GilSite::Supervise, |py| {
1226 err.downcast_ref::<pyo3::PyErr>()
1227 .is_some_and(|pyerr| {
1228 pyerr.is_instance(
1229 py,
1230 &unhandled_fault_hook_exception(py),
1231 )
1232 })
1233 }).await;
1234
1235 let kind = ActorErrorKind::processing(err);
1236 let err = ActorError {
1237 actor_id: Box::new(instance.self_addr().clone()),
1238 kind: Box::new(kind),
1239 };
1240
1241 if is_hook_exception {
1242 break Some(err);
1243 }
1244
1245 let supervision_event = actor_error_to_event(instance, &actor, err);
1251 if let Err(err) = instance.handle_supervision_event(&mut actor, supervision_event).await {
1255 while let Ok(supervision_event) = supervision_rx.try_recv() {
1256 if let Err(err) = instance.handle_supervision_event(&mut actor, supervision_event).await {
1257 break 'messages Some(err);
1258 }
1259 }
1260 break Some(err);
1261 }
1262 }
1263 }
1264 signal = signal_rx.recv() => {
1265 tracing::info!(actor_id = %instance.self_addr(), "client received signal {signal:?}");
1266 match signal {
1267 Some(signal@(Signal::Stop(_) | Signal::DrainAndStop(_))) => {
1268 need_drain = matches!(signal, Signal::DrainAndStop(_));
1269 break None;
1270 },
1271 Some(Signal::ExitRequested(_)) => break None,
1272 Some(Signal::ChildStopped(_)) => {},
1273 Some(Signal::Kill(reason)) => {
1274 break Some(ActorError { actor_id: Box::new(instance.self_addr().clone()), kind: Box::new(ActorErrorKind::Aborted(reason)) })
1275 },
1276 None => {
1277 break Some(ActorError {
1278 actor_id: Box::new(instance.self_addr().clone()),
1279 kind: Box::new(ActorErrorKind::SignalChannelClosed),
1280 })
1281 },
1282 }
1283 }
1284 Some(supervision_event) = supervision_rx.recv() => {
1285 if let Err(err) = instance.handle_supervision_event(&mut actor, supervision_event).await {
1286 break Some(err);
1287 }
1288 }
1289 };
1290 };
1291 if need_drain {
1292 let mut n = 0;
1293 while let Ok(work) = work_rx.try_recv() {
1294 if let Err(e) = work.handle(&mut actor, instance).await {
1295 err = Some(ActorError {
1296 actor_id: Box::new(instance.self_addr().clone()),
1297 kind: Box::new(ActorErrorKind::processing(e)),
1298 });
1299 break;
1300 }
1301 n += 1;
1302 }
1303 tracing::debug!(actor_id = %instance.self_addr(), "client drained {} messages before stopping", n);
1304 }
1305 if let Some(err) = err {
1306 let event = actor_error_to_event(instance, &actor, err);
1307 tracing::error!(
1311 actor_id = %instance.self_addr(),
1312 "could not propagate supervision event {} because it reached the global client: signaling KeyboardInterrupt to main thread",
1313 event,
1314 );
1315
1316 monarch_with_gil_blocking(GilSite::Stop, |py| {
1325 let thread_mod = py.import("_thread").expect("import _thread");
1328 let interrupt_main = thread_mod
1329 .getattr("interrupt_main")
1330 .expect("get interrupt_main");
1331
1332 if let Err(e) = interrupt_main.call0() {
1334 tracing::error!("unable to interrupt main, exiting the process instead: {:?}", e);
1335 eprintln!("unable to interrupt main, exiting the process with code 1 instead: {:?}", e);
1336 std::process::exit(1);
1337 }
1338 });
1339 } else {
1340 tracing::info!(actor_id = %instance.self_addr(), "client stopped");
1341 instance.change_status(hyperactor::actor::ActorStatus::Stopped("client stopped".into()));
1342 }
1343 });
1344
1345 (root_client_instance.get().unwrap(), handle)
1346 }
1347}
1348
1349fn actor_error_to_event(
1350 instance: &Instance<PythonActor>,
1351 actor: &PythonActor,
1352 err: ActorError,
1353) -> ActorSupervisionEvent {
1354 match *err.kind {
1355 ActorErrorKind::UnhandledSupervisionEvent(event) => *event,
1356 _ => {
1357 let status = ActorStatus::generic_failure(err.kind.to_string());
1358 ActorSupervisionEvent::new(
1359 instance.self_addr().clone(),
1360 actor.display_name(),
1361 status,
1362 None,
1363 )
1364 }
1365 }
1366}
1367
1368pub(crate) fn root_client_actor(py: Python<'_>) -> &'static Instance<PythonActor> {
1369 static ROOT_CLIENT_ACTOR: OnceLock<&'static Instance<PythonActor>> = OnceLock::new();
1370
1371 py.detach(|| {
1376 ROOT_CLIENT_ACTOR.get_or_init(|| {
1377 monarch_with_gil_blocking(GilSite::Bootstrap, |py| {
1378 let (client, _handle) = PythonActor::bootstrap_client(py);
1379 client
1380 })
1381 })
1382 })
1383}
1384
1385#[async_trait]
1386impl Actor for PythonActor {
1387 async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
1388 let tracker = self.execution_tracker.clone();
1393 this.set_attrs_snapshot(move || {
1394 let mut attrs = hyperactor_config::Attrs::new();
1395 attrs.set(EXECUTION, tracker.snapshot());
1396 attrs
1397 });
1398
1399 if let PythonActorDispatchMode::Queue { receiver, .. } = &mut self.dispatch_mode {
1400 let receiver = receiver.take().unwrap();
1401
1402 monarch_with_gil(GilSite::DispatchInit, |py| {
1403 let self_instance = self.ensure_py_instance(py, this);
1404 let actor_mesh_mod = py.import("monarch._src.actor.actor_mesh")?;
1405
1406 let tl = &self.task_locals;
1407 let awaitable = actor_mesh_mod.call_method(
1408 "_dispatch_loop",
1409 (self.actor.clone_ref(py), receiver, self_instance),
1410 None,
1411 )?;
1412 let future = pyo3_async_runtimes::into_future_with_locals(tl, awaitable)?;
1413 tokio::spawn(async move {
1414 if let Err(e) = future.await {
1415 tracing::error!("message loop error: {}", e);
1416 }
1417 });
1418 Ok::<_, anyhow::Error>(())
1419 })
1420 .await?;
1421 }
1422
1423 if let Some(init_message) = self.init_message.take() {
1424 let spawn_point = self.spawn_point.get().unwrap().as_ref().expect("PythonActor should never be spawned with init_message unless spawn_point also specified").clone();
1425 let mut headers = Flattrs::new();
1426 headers.set(CAST_POINT, spawn_point);
1427 let cx = Context::new(this, headers);
1428 <Self as Handler<PythonMessage>>::handle(self, &cx, init_message).await?;
1429 }
1430
1431 Ok(())
1432 }
1433
1434 async fn cleanup(
1435 &mut self,
1436 this: &Instance<Self>,
1437 err: Option<&ActorError>,
1438 ) -> anyhow::Result<()> {
1439 let cx = Context::new(this, Flattrs::new());
1443 let err_as_str = err.map(|e| e.to_string());
1447 let future = monarch_with_gil(GilSite::EndpointCleanup, |py| {
1448 let py_cx = match &self.instance {
1449 Some(instance) => crate::context::PyContext::new(&cx, instance.clone_ref(py)),
1450 None => {
1451 let py_instance: crate::context::PyInstance = this.into();
1452 crate::context::PyContext::new(
1453 &cx,
1454 py_instance
1455 .into_py_any(py)?
1456 .downcast_bound(py)
1457 .map_err(PyErr::from)?
1458 .clone()
1459 .unbind(),
1460 )
1461 }
1462 }
1463 .into_bound_py_any(py)?;
1464 let actor = self.actor.bind(py);
1465 match actor.hasattr("__cleanup__") {
1468 Ok(false) | Err(_) => {
1469 return Ok(None);
1471 }
1472 _ => {}
1473 }
1474 let awaitable = actor
1475 .call_method("__cleanup__", (&py_cx, err_as_str), None)
1476 .map_err(|err| anyhow::Error::from(SerializablePyErr::from(py, &err)))?;
1477 if awaitable.is_none() {
1478 Ok(None)
1479 } else {
1480 pyo3_async_runtimes::into_future_with_locals(&self.task_locals, awaitable)
1481 .map(Some)
1482 .map_err(anyhow::Error::from)
1483 }
1484 })
1485 .await;
1486 let cleanup_result = match future {
1487 Ok(Some(future)) => future.await.map(|_| ()).map_err(anyhow::Error::from),
1488 Ok(None) => Ok(()),
1489 Err(err) => Err(err),
1490 };
1491 let loop_shutdown_result = self.cancel_pending_python_tasks_and_stop_loop();
1492 cleanup_result?;
1493 loop_shutdown_result?;
1494 Ok(())
1495 }
1496
1497 fn display_name(&self) -> Option<String> {
1498 self.instance.as_ref().and_then(|instance| {
1499 monarch_with_gil_blocking(GilSite::DisplayName, |py| {
1500 instance.bind(py).str().ok().map(|s| s.to_string())
1501 })
1502 })
1503 }
1504
1505 async fn handle_undeliverable_message(
1506 &mut self,
1507 ins: &Instance<Self>,
1508 reason: UndeliverableReason,
1509 mut envelope: Undeliverable<MessageEnvelope>,
1510 ) -> Result<(), anyhow::Error> {
1511 if envelope
1512 .as_message()
1513 .is_some_and(|envelope| envelope.sender() != ins.self_addr())
1514 {
1515 envelope = update_undeliverable_envelope_for_casting(envelope);
1517 }
1518 let envelope = match envelope {
1519 Undeliverable::Returned(envelope) => envelope,
1520 Undeliverable::Report(report) => {
1521 return Err(UndeliverableMessageError::Report { report }.into());
1522 }
1523 };
1524 assert_eq!(
1525 envelope.sender(),
1526 ins.self_addr(),
1527 "undeliverable message was returned to the wrong actor. \
1528 Return address = {}, src actor = {}, dest handler port = {}, message type = {}, envelope headers = {}",
1529 envelope.sender(),
1530 ins.self_addr(),
1531 envelope.dest(),
1532 envelope.data().typename().unwrap_or("unknown"),
1533 envelope.headers()
1534 );
1535
1536 let cx = Context::new(ins, envelope.headers().clone());
1537
1538 let (envelope, handled) = monarch_with_gil(GilSite::EndpointDispatch, |py| {
1539 let py_cx = match &self.instance {
1540 Some(instance) => crate::context::PyContext::new(&cx, instance.clone_ref(py)),
1541 None => {
1542 let py_instance: crate::context::PyInstance = ins.into();
1543 crate::context::PyContext::new(
1544 &cx,
1545 py_instance
1546 .into_py_any(py)?
1547 .downcast_bound(py)
1548 .map_err(PyErr::from)?
1549 .clone()
1550 .unbind(),
1551 )
1552 }
1553 }
1554 .into_bound_py_any(py)?;
1555 let py_envelope = PythonUndeliverableMessageEnvelope {
1556 inner: Some(Undeliverable::Returned(envelope)),
1557 }
1558 .into_bound_py_any(py)?;
1559 let handled = self
1560 .actor
1561 .call_method(
1562 py,
1563 "_handle_undeliverable_message",
1564 (&py_cx, &py_envelope),
1565 None,
1566 )
1567 .map_err(|err| anyhow::Error::from(SerializablePyErr::from(py, &err)))?
1568 .extract::<bool>(py)?;
1569 Ok::<_, anyhow::Error>((
1570 py_envelope
1571 .downcast::<PythonUndeliverableMessageEnvelope>()
1572 .map_err(PyErr::from)?
1573 .try_borrow_mut()
1574 .map_err(PyErr::from)?
1575 .take()?,
1576 handled,
1577 ))
1578 })
1579 .await?;
1580
1581 if !handled {
1582 hyperactor::actor::handle_undeliverable_message(ins, reason, envelope)
1583 } else {
1584 Ok(())
1585 }
1586 }
1587
1588 async fn handle_supervision_event(
1589 &mut self,
1590 this: &Instance<Self>,
1591 event: &ActorSupervisionEvent,
1592 ) -> Result<bool, anyhow::Error> {
1593 let cx = Context::new(this, Flattrs::new());
1594 self.handle(
1595 &cx,
1596 MeshFailure {
1597 actor_mesh_name: self.mesh_base_name.clone(),
1601 event: event.clone(),
1602 crashed_ranks: vec![],
1603 },
1604 )
1605 .await
1606 .map(|_| true)
1607 }
1608}
1609
1610#[derive(Debug, Clone, Serialize, Deserialize, Named)]
1611pub struct PythonActorParams {
1612 actor_type: PickledPyObject,
1614 init_message: Option<PythonMessage>,
1616 mesh_base_name: Option<String>,
1627}
1628
1629impl PythonActorParams {
1630 pub(crate) fn new(
1631 actor_type: PickledPyObject,
1632 init_message: Option<PythonMessage>,
1633 mesh_base_name: Option<String>,
1634 ) -> Self {
1635 Self {
1636 actor_type,
1637 init_message,
1638 mesh_base_name,
1639 }
1640 }
1641}
1642
1643#[async_trait]
1644impl RemoteSpawn for PythonActor {
1645 type Params = PythonActorParams;
1646
1647 async fn new(
1648 PythonActorParams {
1649 actor_type,
1650 init_message,
1651 mesh_base_name,
1652 }: PythonActorParams,
1653 environment: Flattrs,
1654 ) -> Result<Self, anyhow::Error> {
1655 let spawn_point = environment.get(CAST_POINT);
1656 Self::new(actor_type, init_message, spawn_point, mesh_base_name)
1657 }
1658}
1659
1660fn create_task_locals() -> pyo3_async_runtimes::TaskLocals {
1662 monarch_with_gil_blocking(GilSite::TaskLocals, |py| {
1663 let asyncio = Python::import(py, "asyncio").unwrap();
1664 let event_loop = asyncio.call_method0("new_event_loop").unwrap();
1665 let task_locals = pyo3_async_runtimes::TaskLocals::new(event_loop.clone())
1666 .copy_context(py)
1667 .unwrap();
1668
1669 let kwargs = PyDict::new(py);
1670 let target = event_loop.getattr("run_forever").unwrap();
1671 kwargs.set_item("target", target).unwrap();
1672 kwargs.set_item("daemon", true).unwrap();
1674 let thread = py
1675 .import("threading")
1676 .unwrap()
1677 .call_method("Thread", (), Some(&kwargs))
1678 .unwrap();
1679 thread.call_method0("start").unwrap();
1680 task_locals
1681 })
1682}
1683
1684#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
1720struct PanicFlag {
1721 sender: Option<tokio::sync::oneshot::Sender<Py<PyAny>>>,
1722}
1723
1724#[pymethods]
1725impl PanicFlag {
1726 fn signal_panic(&mut self, ex: Py<PyAny>) {
1727 self.sender.take().unwrap().send(ex).unwrap();
1728 }
1729}
1730
1731#[async_trait]
1732impl Handler<PythonMessage> for PythonActor {
1733 #[tracing::instrument(level = "debug", skip_all)]
1734 async fn handle(
1735 &mut self,
1736 cx: &Context<PythonActor>,
1737 message: PythonMessage,
1738 ) -> anyhow::Result<()> {
1739 match &self.dispatch_mode {
1740 PythonActorDispatchMode::Direct => self.handle_direct(cx, message).await,
1741 PythonActorDispatchMode::Queue { sender, .. } => {
1742 let sender = sender.clone();
1743 self.handle_queue(cx, sender, message).await
1744 }
1745 }
1746 }
1747}
1748
1749impl PythonActor {
1750 async fn handle_direct(
1752 &mut self,
1753 cx: &Context<'_, PythonActor>,
1754 message: PythonMessage,
1755 ) -> anyhow::Result<()> {
1756 let resolved = message.resolve_indirect_call(cx).await?;
1757 let endpoint = resolved.method.to_string();
1758
1759 let (sender, receiver) = oneshot::channel();
1762
1763 let future = monarch_with_gil(
1764 GilSite::EndpointDispatch,
1765 |py| -> Result<_, SerializablePyErr> {
1766 let inst = self.ensure_py_instance(py, cx);
1767
1768 let awaitable = self.actor.call_method(
1769 py,
1770 "handle",
1771 (
1772 crate::context::PyContext::new(cx, inst.clone_ref(py)),
1773 resolved.method,
1774 resolved.bytes,
1775 PanicFlag {
1776 sender: Some(sender),
1777 },
1778 resolved
1779 .local_state
1780 .unwrap_or_else(|| PyList::empty(py).unbind().into()),
1781 resolved.mesh_references.into_py_any(py)?,
1782 resolved.response_port.into_py_any(py)?,
1783 ),
1784 None,
1785 )?;
1786
1787 pyo3_async_runtimes::into_future_with_locals(
1788 &self.task_locals,
1789 awaitable.into_bound(py),
1790 )
1791 .map_err(|err| err.into())
1792 },
1793 )
1794 .await?;
1795
1796 tokio::spawn(handle_async_endpoint_panic(
1798 cx.signal_sender(),
1799 PythonTask::new(future)?,
1800 receiver,
1801 cx.self_addr().to_string(),
1802 endpoint,
1803 ));
1804 Ok(())
1805 }
1806
1807 async fn handle_queue(
1810 &mut self,
1811 cx: &Context<'_, PythonActor>,
1812 sender: pympsc::Sender,
1813 message: PythonMessage,
1814 ) -> anyhow::Result<()> {
1815 let resolved = message.resolve_indirect_call(cx).await?;
1816
1817 let queued_msg = monarch_with_gil(
1818 GilSite::QueueDispatch,
1819 |py| -> anyhow::Result<QueuedMessage> {
1820 let inst = self.ensure_py_instance(py, cx);
1821
1822 let py_context = crate::context::PyContext::new(cx, inst.clone_ref(py));
1823 let py_context_obj = Py::new(py, py_context)?;
1824
1825 Ok(QueuedMessage {
1826 context: py_context_obj,
1827 method: resolved.method,
1828 bytes: resolved.bytes,
1829 local_state: resolved
1830 .local_state
1831 .unwrap_or_else(|| PyList::empty(py).unbind().into()),
1832 refs: resolved.mesh_references.into_py_any(py)?,
1833 response_port: resolved.response_port.into_py_any(py)?,
1834 })
1835 },
1836 )
1837 .await?;
1838
1839 sender
1840 .send(queued_msg)
1841 .map_err(|_| anyhow::anyhow!("failed to send message to queue"))?;
1842
1843 Ok(())
1844 }
1845}
1846
1847#[async_trait]
1848impl Handler<MeshFailure> for PythonActor {
1849 async fn handle(&mut self, cx: &Context<Self>, message: MeshFailure) -> anyhow::Result<()> {
1850 if !message.event.actor_status.is_failed() {
1854 tracing::info!(
1855 "ignoring non-failure supervision event from child: {}",
1856 message
1857 );
1858 return Ok(());
1859 }
1860 let (display_name, fut) = monarch_with_gil(GilSite::Supervise, |py| {
1869 let inst = self.ensure_py_instance(py, cx);
1870 let display_name: Option<String> = inst.bind(py).str().ok().map(|s| s.to_string());
1872 let actor_bound = self.actor.bind(py);
1873 if !actor_bound.hasattr("__supervise__")? {
1876 return Err(anyhow::anyhow!(
1877 "no __supervise__ method on {:?}",
1878 actor_bound
1879 ));
1880 }
1881 let awaitable = actor_bound.call_method(
1882 "__supervise__",
1883 (
1884 crate::context::PyContext::new(cx, inst.clone_ref(py)),
1885 PyMeshFailure::from(message.clone()),
1886 ),
1887 None,
1888 )?;
1889 let fut = pyo3_async_runtimes::into_future_with_locals(&self.task_locals, awaitable)?;
1890 anyhow::Ok((display_name, fut))
1891 })
1892 .await?;
1893
1894 let awaited = fut.await;
1895
1896 monarch_with_gil(GilSite::Supervise, |py| match awaited {
1897 Ok(s) => {
1898 if s.bind(py).is_truthy()? {
1899 tracing::info!(
1904 name = "ActorMeshStatus",
1905 status = "SupervisionError::Handled",
1906 actor_name = message.actor_mesh_name,
1908 event = %message.event,
1909 "__supervise__ on {} handled a supervision event, not reporting any further",
1910 cx.self_addr(),
1911 );
1912 Ok(())
1913 } else {
1914 for (actor_name, status) in [
1923 (
1924 message
1925 .actor_mesh_name
1926 .as_deref()
1927 .unwrap_or_else(|| message.event.actor_id.log_name()),
1928 "SupervisionError::Unhandled",
1929 ),
1930 (cx.self_addr().log_name(), "UnhandledSupervisionEvent"),
1931 ] {
1932 tracing::info!(
1933 name = "ActorMeshStatus",
1934 status,
1935 actor_name,
1936 event = %message.event,
1937 "__supervise__ on {} did not handle a supervision event, reporting to the next next owner",
1938 cx.self_addr(),
1939 );
1940 }
1941 let err = ActorErrorKind::UnhandledSupervisionEvent(Box::new(
1942 ActorSupervisionEvent::new(
1943 cx.self_addr().clone(),
1944 display_name.clone(),
1945 ActorStatus::Failed(ActorErrorKind::UnhandledSupervisionEvent(
1946 Box::new(message.event.clone()),
1947 )),
1948 None,
1949 ),
1950 ));
1951 Err(anyhow::Error::new(err))
1952 }
1953 }
1954 Err(err) => {
1955 if err.is_instance(py, &unhandled_fault_hook_exception(py)) {
1960 return Err(err.into());
1961 }
1962
1963 for (actor_name, status) in [
1969 (
1970 message
1971 .actor_mesh_name
1972 .as_deref()
1973 .unwrap_or_else(|| message.event.actor_id.log_name()),
1974 "SupervisionError::__supervise__::exception",
1975 ),
1976 (cx.self_addr().log_name(), "UnhandledSupervisionEvent"),
1977 ] {
1978 tracing::info!(
1979 name = "ActorMeshStatus",
1980 status,
1981 actor_name,
1982 event = %message.event,
1983 "__supervise__ on {} threw an exception",
1984 cx.self_addr(),
1985 );
1986 }
1987 let err = ActorErrorKind::UnhandledSupervisionEvent(Box::new(
1988 ActorSupervisionEvent::new(
1989 cx.self_addr().clone(),
1990 display_name,
1991 ActorStatus::Failed(ActorErrorKind::ErrorDuringHandlingSupervision(
1992 err.to_string(),
1993 Box::new(message.event.clone()),
1994 )),
1995 None,
1996 ),
1997 ));
1998 Err(anyhow::Error::new(err))
1999 }
2000 })
2001 .await
2002 }
2003}
2004
2005async fn handle_async_endpoint_panic(
2006 panic_sender: mpsc::UnboundedSender<Signal>,
2007 task: PythonTask,
2008 side_channel: oneshot::Receiver<Py<PyAny>>,
2009 actor_id: String,
2010 endpoint: String,
2011) {
2012 let attributes =
2014 hyperactor_telemetry::kv_pairs!("actor_id" => actor_id, "endpoint" => endpoint);
2015
2016 let start_time = std::time::Instant::now();
2018
2019 ENDPOINT_ACTOR_COUNT.add(1, attributes);
2021
2022 let err_or_never = async {
2023 match side_channel.await {
2026 Ok(value) => {
2027 monarch_with_gil(GilSite::AwaitDrive, |py| -> Option<SerializablePyErr> {
2028 let err: PyErr = value
2029 .downcast_bound::<PyBaseException>(py)
2030 .unwrap()
2031 .clone()
2032 .into();
2033 ENDPOINT_ACTOR_PANIC.add(1, attributes);
2034 Some(err.into())
2035 })
2036 .await
2037 }
2038 Err(_) => pending().await,
2043 }
2044 };
2045 let future = task.take();
2046 if let Some(panic) = tokio::select! {
2047 result = future => {
2048 match result {
2049 Ok(_) => None,
2050 Err(e) => Some(e.into()),
2051 }
2052 },
2053 result = err_or_never => {
2054 result
2055 }
2056 } {
2057 ENDPOINT_ACTOR_ERROR.add(1, attributes);
2059 if panic_sender.send(Signal::Kill(panic.to_string())).is_err() {
2060 tracing::warn!("dropped panic signal: actor already stopped: {panic}");
2061 }
2062 }
2063
2064 let elapsed_micros = start_time.elapsed().as_micros() as f64;
2066 ENDPOINT_ACTOR_LATENCY_US_HISTOGRAM.record(elapsed_micros, attributes);
2067}
2068
2069#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
2070struct LocalPort {
2071 instance: PyInstance,
2072 inner: Option<OncePortHandle<Result<Py<PyAny>, Py<PyAny>>>>,
2073}
2074
2075impl Debug for LocalPort {
2076 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2077 f.debug_struct("LocalPort")
2078 .field("inner", &self.inner)
2079 .finish()
2080 }
2081}
2082
2083pub(crate) fn to_py_error<T>(e: T) -> PyErr
2084where
2085 T: Error,
2086{
2087 PyErr::new::<PyValueError, _>(e.to_string())
2088}
2089
2090#[pymethods]
2091impl LocalPort {
2092 fn send(&mut self, obj: Py<PyAny>) -> PyResult<()> {
2093 let port = self.inner.take().expect("use local port once");
2094 port.post(self.instance.deref(), Ok(obj));
2095 Ok(())
2096 }
2097 fn resolve_and_send(&mut self, obj: Py<PyAny>) -> PyResult<PyPythonTask> {
2098 self.send(obj)?;
2099 PyPythonTask::new(async { Ok(()) })
2100 }
2101 fn exception(&mut self, e: Py<PyAny>) -> PyResult<()> {
2102 let port = self.inner.take().expect("use local port once");
2103 port.post(self.instance.deref(), Err(e));
2104 Ok(())
2105 }
2106}
2107
2108#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.actor")]
2112#[derive(Debug)]
2113pub struct DroppingPort;
2114
2115#[pymethods]
2116impl DroppingPort {
2117 #[new]
2118 fn new() -> Self {
2119 DroppingPort
2120 }
2121
2122 fn send(&self, _obj: Py<PyAny>) -> PyResult<()> {
2123 Ok(())
2124 }
2125
2126 fn resolve_and_send(&self, obj: Py<PyAny>) -> PyResult<PyPythonTask> {
2127 self.send(obj)?;
2128 PyPythonTask::new(async { Ok(()) })
2129 }
2130
2131 fn send_message(&self, _message: PythonMessage) -> PyResult<()> {
2132 Ok(())
2133 }
2134
2135 fn exception(&self, e: Bound<'_, PyAny>) -> PyResult<()> {
2136 let exc = if let Ok(inner) = e.getattr("exception") {
2138 inner
2139 } else {
2140 e
2141 };
2142 Err(PyErr::from_value(exc))
2143 }
2144
2145 #[getter]
2146 fn get_return_undeliverable(&self) -> bool {
2147 true
2148 }
2149
2150 #[setter]
2151 fn set_return_undeliverable(&self, _value: bool) {}
2152}
2153
2154#[pyclass(module = "monarch._src.actor.actor_mesh")]
2157pub struct Port {
2158 port_ref: EitherPortRef,
2159 instance: Instance<PythonActor>,
2160 rank: Option<usize>,
2161 reply_headers: hyperactor_config::Flattrs,
2165}
2166
2167#[pymethods]
2168impl Port {
2169 #[new]
2170 fn new(
2171 port_ref: EitherPortRef,
2172 instance: &crate::context::PyInstance,
2173 rank: Option<usize>,
2174 ) -> Self {
2175 Self {
2176 port_ref,
2177 instance: instance.clone().into_instance(),
2178 rank,
2179 reply_headers: hyperactor_config::Flattrs::new(),
2180 }
2181 }
2182
2183 #[getter("_port_ref")]
2184 fn port_ref_py(&self) -> EitherPortRef {
2185 self.port_ref.clone()
2186 }
2187
2188 #[getter("_rank")]
2189 fn rank_py(&self) -> Option<usize> {
2190 self.rank
2191 }
2192
2193 #[getter]
2194 fn get_return_undeliverable(&self) -> bool {
2195 self.port_ref.get_return_undeliverable()
2196 }
2197
2198 #[setter]
2199 fn set_return_undeliverable(&mut self, value: bool) {
2200 self.port_ref.set_return_undeliverable(value);
2201 }
2202
2203 #[tracing::instrument(level = "debug", skip_all)]
2204 fn send(&mut self, py: Python<'_>, obj: Py<PyAny>) -> PyResult<()> {
2205 let message = PythonMessage::new_from_buf(
2206 PythonMessageKind::Result { rank: self.rank },
2207 pickle_to_part(py, &obj)?,
2208 );
2209
2210 self.port_ref
2211 .post_with_headers(&self.instance, self.reply_headers.clone(), message)
2212 .map_err(|e| PyRuntimeError::new_err(e.to_string()))
2213 }
2214
2215 #[tracing::instrument(level = "debug", skip_all)]
2216 fn send_message(&mut self, message: PythonMessage) -> PyResult<()> {
2217 self.port_ref
2218 .post_with_headers(&self.instance, self.reply_headers.clone(), message)
2219 .map_err(|e| PyRuntimeError::new_err(e.to_string()))
2220 }
2221
2222 fn exception(&mut self, py: Python<'_>, e: Py<PyAny>) -> PyResult<()> {
2223 let message = PythonMessage::new_from_buf(
2224 PythonMessageKind::Exception { rank: self.rank },
2225 pickle_to_part(py, &e)?,
2226 );
2227
2228 self.port_ref
2229 .post_with_headers(&self.instance, self.reply_headers.clone(), message)
2230 .map_err(|e| PyRuntimeError::new_err(e.to_string()))
2231 }
2232}
2233
2234impl Port {
2235 pub(crate) fn with_reply_headers(
2239 port_ref: EitherPortRef,
2240 instance: Instance<PythonActor>,
2241 rank: Option<usize>,
2242 reply_headers: hyperactor_config::Flattrs,
2243 ) -> Self {
2244 Self {
2245 port_ref,
2246 instance,
2247 rank,
2248 reply_headers,
2249 }
2250 }
2251}
2252
2253pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
2254 hyperactor_mod.add_class::<PythonActorHandle>()?;
2255 hyperactor_mod.add_class::<PythonMessage>()?;
2256 hyperactor_mod.add_class::<PyMeshRef>()?;
2257 hyperactor_mod.add_class::<PythonMessageKind>()?;
2258 hyperactor_mod.add_class::<MethodSpecifier>()?;
2259 hyperactor_mod.add_class::<UnflattenArg>()?;
2260 hyperactor_mod.add_class::<PanicFlag>()?;
2261 hyperactor_mod.add_class::<QueuedMessage>()?;
2262 hyperactor_mod.add_class::<DroppingPort>()?;
2263 hyperactor_mod.add_class::<Port>()?;
2264 Ok(())
2265}
2266
2267#[cfg(test)]
2268mod tests {
2269 use hyperactor as reference;
2270 use hyperactor::accum::ReducerSpec;
2271 use hyperactor::accum::StreamingReducerOpts;
2272 use hyperactor::id::Label;
2273 use hyperactor::testing::ids::test_port_id;
2274 use hyperactor_mesh::Error as MeshError;
2275 use hyperactor_mesh::host_mesh::host_agent::ProcState;
2276 use hyperactor_mesh::mesh_id::ResourceId;
2277 use hyperactor_mesh::resource::Status;
2278 use hyperactor_mesh::resource::{self};
2279 use pyo3::PyTypeInfo;
2280
2281 use super::*;
2282 use crate::actor::to_py_error;
2283
2284 #[test]
2285 fn test_python_message_part_codec() {
2286 let reducer_spec = ReducerSpec {
2287 typehash: 123,
2288 builder_params: Some(wirevalue::Any::serialize(&"abcdefg12345".to_string()).unwrap()),
2289 };
2290 let port_ref = hyperactor::PortRef::<PythonMessage>::attest_reducible(
2291 test_port_id("world_0", "client", 123),
2292 Some(reducer_spec),
2293 StreamingReducerOpts::default(),
2294 );
2295 let message = PythonMessage {
2296 kind: PythonMessageKind::CallMethod {
2297 name: MethodSpecifier::ReturnsResponse {
2298 name: "test".to_string(),
2299 },
2300 response_port: Some(EitherPortRef::Unbounded(port_ref.clone().into())),
2301 },
2302 message: Part::from(vec![1, 2, 3]),
2303 refs: Vec::new(),
2304 };
2305 {
2306 let mut multipart_message =
2307 wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&message).unwrap();
2308 let mut ports = vec![];
2309 multipart_message
2310 .visit_multipart_parts_mut::<reference::PortRefRepr, anyhow::Error>(|b| {
2311 ports.push(b.clone());
2312 Ok(())
2313 })
2314 .unwrap();
2315 assert_eq!(ports.len(), 1);
2316 assert_eq!(ports[0].port_addr(), port_ref.port_addr());
2317 assert_eq!(ports[0].reducer_spec(), port_ref.reducer_spec());
2318 assert_eq!(
2319 ports[0].get_return_undeliverable(),
2320 port_ref.get_return_undeliverable()
2321 );
2322 assert!(!ports[0].unsplit());
2323 assert_eq!(
2324 message,
2325 multipart_message
2326 .deserialized_unchecked::<PythonMessage>()
2327 .unwrap()
2328 );
2329 }
2330
2331 let no_port_message = PythonMessage {
2332 kind: PythonMessageKind::CallMethod {
2333 name: MethodSpecifier::ReturnsResponse {
2334 name: "test".to_string(),
2335 },
2336 response_port: None,
2337 },
2338 ..message
2339 };
2340 {
2341 let mut multipart_message =
2342 wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&no_port_message)
2343 .unwrap();
2344 let mut ports = vec![];
2345 multipart_message
2346 .visit_multipart_parts_mut::<reference::PortRefRepr, anyhow::Error>(|b| {
2347 ports.push(b.clone());
2348 Ok(())
2349 })
2350 .unwrap();
2351 assert_eq!(ports.len(), 0);
2352 assert_eq!(
2353 no_port_message,
2354 multipart_message
2355 .deserialized_unchecked::<PythonMessage>()
2356 .unwrap()
2357 );
2358 }
2359 }
2360
2361 #[test]
2362 fn test_python_message_refs_travel_as_parts() {
2363 fn proc_mesh_ref(seed: u64, label: &str) -> MeshRef {
2365 let proc_id = hyperactor::ProcId::new(
2366 hyperactor::id::Uid::Instance(seed, None),
2367 Some(Label::new("local").unwrap()),
2368 );
2369 let proc_addr = hyperactor::ProcAddr::new(
2370 proc_id,
2371 hyperactor::channel::ChannelAddr::Local(seed).into(),
2372 );
2373 let agent: hyperactor::ActorRef<hyperactor_mesh::proc_agent::ProcAgent> =
2374 hyperactor::ActorRef::attest(
2375 proc_addr.actor_addr(hyperactor_mesh::proc_agent::PROC_AGENT_ACTOR_NAME),
2376 );
2377 let proc_ref = hyperactor_mesh::proc_mesh::ProcRef::new(proc_addr, 0, agent);
2378 MeshRef::Proc(Box::new(
2379 hyperactor_mesh::proc_mesh::ProcMeshRef::new_singleton(
2380 hyperactor_mesh::mesh_id::ProcMeshId::singleton(Label::new(label).unwrap()),
2381 proc_ref,
2382 )
2383 .unwrap(),
2384 ))
2385 }
2386
2387 let message = PythonMessage {
2388 kind: PythonMessageKind::CallMethod {
2389 name: MethodSpecifier::ReturnsResponse {
2390 name: "test".to_string(),
2391 },
2392 response_port: None,
2393 },
2394 message: Part::from(vec![1, 2, 3]),
2395 refs: vec![proc_mesh_ref(1, "a"), proc_mesh_ref(2, "b")],
2396 };
2397
2398 let mut multipart_message =
2399 wirevalue::Any::<wirevalue::encoding::Multipart>::serialize(&message).unwrap();
2400 let mut parts = vec![];
2401 multipart_message
2402 .visit_multipart_parts_mut::<MeshRefRepr, anyhow::Error>(|b| {
2403 parts.push(b.clone());
2404 Ok(())
2405 })
2406 .unwrap();
2407 assert_eq!(parts.len(), 2);
2409 assert_eq!(
2411 message,
2412 multipart_message
2413 .deserialized_unchecked::<PythonMessage>()
2414 .unwrap()
2415 );
2416 }
2417
2418 #[test]
2419 fn to_py_error_preserves_proc_creation_message() {
2420 let state: resource::State<ProcState> = resource::State {
2422 id: ResourceId::instance(Label::new("my-proc").unwrap()),
2423 status: Status::Failed("boom".into()),
2424 state: None,
2425 generation: 0,
2426 timestamp: std::time::SystemTime::now(),
2427 };
2428
2429 let mesh_agent: hyperactor::ActorRef<hyperactor_mesh::host_mesh::HostAgent> =
2431 hyperactor::ActorRef::attest(test_port_id("hello_0", "actor", 0).actor_addr());
2432 let expected_prefix = format!(
2433 "error creating proc (host rank 0) on host mesh agent {}",
2434 mesh_agent
2435 );
2436 let err = MeshError::ProcCreationError {
2437 host_rank: 0,
2438 mesh_agent,
2439 state: Box::new(state),
2440 };
2441
2442 let rust_msg = err.to_string();
2443 let pyerr = to_py_error(err);
2444
2445 pyo3::Python::initialize();
2446 monarch_with_gil_blocking(GilSite::Test, |py| {
2447 assert!(pyerr.get_type(py).is(PyValueError::type_object(py)));
2448 let py_msg = pyerr.value(py).to_string();
2449
2450 assert_eq!(py_msg, rust_msg);
2452 assert!(py_msg.contains(", state: "));
2454 assert!(py_msg.contains("\"status\":{\"Failed\":\"boom\"}"));
2455 assert!(py_msg.starts_with(&expected_prefix));
2457 });
2458 }
2459}