1use std::cell::Cell;
10use std::collections::VecDeque;
11use std::sync::Arc;
12use std::sync::atomic::AtomicUsize;
13use std::sync::atomic::Ordering;
14
15use hyperactor::ActorAddr;
16use hyperactor::Instance;
17use hyperactor::accum::Accumulator;
18use hyperactor::accum::CommReducer;
19use hyperactor::accum::ReducerFactory;
20use hyperactor::accum::ReducerSpec;
21use hyperactor::id::Label;
22use hyperactor::mailbox::OncePortReceiver;
23use hyperactor::mailbox::PortReceiver;
24use hyperactor_mesh::value_mesh::ValueOverlay;
25use hyperactor_mesh::value_mesh::rle;
26use monarch_types::py_global;
27use ndslice::Extent;
28use ndslice::Shape;
29use pyo3::prelude::*;
30use pyo3::types::PyDict;
31use pyo3::types::PyTuple;
32use serde_multipart::Part;
33use typeuri::Named;
34
35use crate::actor::MeshRef;
36use crate::actor::MethodSpecifier;
37use crate::actor::PythonActor;
38use crate::actor::PythonMessage;
39use crate::actor::PythonMessageKind;
40use crate::actor::PythonResponseMessage;
41use crate::actor_mesh::AllOrChoose;
42use crate::actor_mesh::PythonActorMesh;
43use crate::actor_mesh::SupervisableActorMesh;
44use crate::actor_mesh::to_all_or_choose;
45use crate::context::PyInstance;
46use crate::mailbox::EitherPortRef;
47use crate::mailbox::PythonOncePortRef;
48use crate::mailbox::PythonPortRef;
49use crate::metrics::ENDPOINT_BROADCAST_ERROR;
50use crate::metrics::ENDPOINT_BROADCAST_THROUGHPUT;
51use crate::metrics::ENDPOINT_CALL_ERROR;
52use crate::metrics::ENDPOINT_CALL_LATENCY_US_HISTOGRAM;
53use crate::metrics::ENDPOINT_CALL_ONE_ERROR;
54use crate::metrics::ENDPOINT_CALL_ONE_LATENCY_US_HISTOGRAM;
55use crate::metrics::ENDPOINT_CALL_ONE_THROUGHPUT;
56use crate::metrics::ENDPOINT_CALL_THROUGHPUT;
57use crate::metrics::ENDPOINT_CHOOSE_ERROR;
58use crate::metrics::ENDPOINT_CHOOSE_LATENCY_US_HISTOGRAM;
59use crate::metrics::ENDPOINT_CHOOSE_THROUGHPUT;
60use crate::metrics::ENDPOINT_MESSAGE_SIZE_HISTOGRAM;
61use crate::metrics::ENDPOINT_STREAM_ERROR;
62use crate::metrics::ENDPOINT_STREAM_LATENCY_US_HISTOGRAM;
63use crate::metrics::ENDPOINT_STREAM_THROUGHPUT;
64use crate::metrics::EndpointAttrs;
65use crate::metrics::UNKNOWN;
66use crate::pickle::PendingMessage;
67use crate::pickle::PicklingState;
68use crate::pytokio::PyPythonTask;
69use crate::pytokio::PythonTask;
70use crate::runtime::GilSite;
71use crate::runtime::monarch_with_gil_blocking;
72use crate::shape::PyExtent;
73use crate::shape::PyShape;
74use crate::supervision::Supervisable;
75use crate::supervision::SupervisionError;
76use crate::value_mesh::PyValueMesh;
77
78py_global!(get_context, "monarch._src.actor.actor_mesh", "context");
79py_global!(
80 create_endpoint_message,
81 "monarch._src.actor.actor_mesh",
82 "_create_endpoint_message"
83);
84py_global!(
85 dispatch_actor_rref,
86 "monarch._src.actor.actor_mesh",
87 "_dispatch_actor_rref"
88);
89py_global!(make_future, "monarch._src.actor.future", "Future");
90
91#[derive(Clone, Copy, Debug)]
95pub(crate) enum EndpointAdverb {
96 Call,
97 CallOne,
98 Choose,
99 Stream,
100 Broadcast,
101}
102
103impl EndpointAdverb {
104 fn as_str(self) -> &'static str {
105 match self {
106 Self::Call => "call",
107 Self::CallOne => "call_one",
108 Self::Choose => "choose",
109 Self::Stream => "stream",
110 Self::Broadcast => "broadcast",
111 }
112 }
113}
114
115pub struct RecordEndpointGuard {
120 start: tokio::time::Instant,
121 attrs: Arc<EndpointAttrs>,
122 adverb: EndpointAdverb,
123 error_occurred: Cell<bool>,
124}
125
126impl RecordEndpointGuard {
127 fn new(start: tokio::time::Instant, attrs: Arc<EndpointAttrs>, adverb: EndpointAdverb) -> Self {
128 let attributes = attrs.as_slice();
129 match adverb {
130 EndpointAdverb::Call => {
131 ENDPOINT_CALL_THROUGHPUT.add(1, attributes);
132 }
133 EndpointAdverb::CallOne => {
134 ENDPOINT_CALL_ONE_THROUGHPUT.add(1, attributes);
135 }
136 EndpointAdverb::Choose => {
137 ENDPOINT_CHOOSE_THROUGHPUT.add(1, attributes);
138 }
139 EndpointAdverb::Stream | EndpointAdverb::Broadcast => {
140 }
142 }
143
144 Self {
145 start,
146 attrs,
147 adverb,
148 error_occurred: Cell::new(false),
149 }
150 }
151
152 fn mark_error(&self) {
153 self.error_occurred.set(true);
154 }
155}
156
157impl Drop for RecordEndpointGuard {
158 fn drop(&mut self) {
159 let attributes = self.attrs.as_slice();
160 let duration_us = self.start.elapsed().as_micros();
161
162 match self.adverb {
163 EndpointAdverb::Call => {
164 ENDPOINT_CALL_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
165 }
166 EndpointAdverb::CallOne => {
167 ENDPOINT_CALL_ONE_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
168 }
169 EndpointAdverb::Choose => {
170 ENDPOINT_CHOOSE_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
171 }
172 EndpointAdverb::Stream => {
173 ENDPOINT_STREAM_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
174 }
175 EndpointAdverb::Broadcast => {}
176 }
177
178 if self.error_occurred.get() {
179 match self.adverb {
180 EndpointAdverb::Call => {
181 ENDPOINT_CALL_ERROR.add(1, attributes);
182 }
183 EndpointAdverb::CallOne => {
184 ENDPOINT_CALL_ONE_ERROR.add(1, attributes);
185 }
186 EndpointAdverb::Choose => {
187 ENDPOINT_CHOOSE_ERROR.add(1, attributes);
188 }
189 EndpointAdverb::Stream => {
190 ENDPOINT_STREAM_ERROR.add(1, attributes);
191 }
192 EndpointAdverb::Broadcast => {}
193 }
194 }
195 }
196}
197
198pub(crate) struct SpanGuard {
205 id: u64,
206}
207
208impl SpanGuard {
209 fn actor_endpoint(
210 name: &'static str,
211 actor_id: &ActorAddr,
212 mesh: &str,
213 method: &str,
214 correlation_id: u64,
215 ) -> Self {
216 Self {
217 id: hyperactor_telemetry::start_user_span(
218 name,
219 hyperactor_telemetry::sinks::perfetto::ENDPOINT_TELEMETRY_TARGET,
220 [
221 (
222 "actor_id",
223 hyperactor_telemetry::trace_dispatcher::FieldValue::Str(
224 actor_id.to_string(),
225 ),
226 ),
227 (
228 "mesh",
229 hyperactor_telemetry::trace_dispatcher::FieldValue::Str(mesh.to_string()),
230 ),
231 (
232 "method",
233 hyperactor_telemetry::trace_dispatcher::FieldValue::Str(method.to_string()),
234 ),
235 (
236 "correlation_id",
237 hyperactor_telemetry::trace_dispatcher::FieldValue::U64(correlation_id),
238 ),
239 ],
240 ),
241 }
242 }
243
244 fn remote(
245 name: &'static str,
246 actor_id: &ActorAddr,
247 call_name: &str,
248 correlation_id: u64,
249 ) -> Self {
250 Self {
251 id: hyperactor_telemetry::start_user_span(
252 name,
253 hyperactor_telemetry::sinks::perfetto::ENDPOINT_TELEMETRY_TARGET,
254 [
255 (
256 "actor_id",
257 hyperactor_telemetry::trace_dispatcher::FieldValue::Str(
258 actor_id.to_string(),
259 ),
260 ),
261 (
262 "call_name",
263 hyperactor_telemetry::trace_dispatcher::FieldValue::Str(
264 call_name.to_string(),
265 ),
266 ),
267 (
268 "correlation_id",
269 hyperactor_telemetry::trace_dispatcher::FieldValue::U64(correlation_id),
270 ),
271 ],
272 ),
273 }
274 }
275}
276
277impl Drop for SpanGuard {
278 fn drop(&mut self) {
279 hyperactor_telemetry::end_user_span(self.id);
280 }
281}
282
283fn supervision_error_to_pyerr(err: PyErr, qualified_endpoint_name: &Option<String>) -> PyErr {
284 match qualified_endpoint_name {
285 Some(endpoint) => monarch_with_gil_blocking(GilSite::Supervise, |py| {
286 SupervisionError::set_endpoint_on_err(py, err, endpoint.clone())
287 }),
288 None => err,
289 }
290}
291
292async fn collect_value(
293 rx: &mut PortReceiver<PythonMessage>,
294 supervision_monitor: &Option<Arc<dyn Supervisable>>,
295 instance: &Instance<PythonActor>,
296 qualified_endpoint_name: &Option<String>,
297) -> PyResult<(Part, Vec<MeshRef>, Option<usize>)> {
298 enum RaceResult {
299 Message(Box<PythonMessage>),
300 SupervisionError(PyErr),
301 RecvError(String),
302 }
303
304 let race_result = match supervision_monitor {
305 Some(sup) => {
306 tokio::select! {
307 biased;
308 result = sup.supervision_event(instance) => {
309 match result {
310 Some(err) => RaceResult::SupervisionError(err),
311 None => {
312 match rx.recv().await {
313 Ok(msg) => RaceResult::Message(Box::new(msg)),
314 Err(e) => RaceResult::RecvError(e.to_string()),
315 }
316 }
317 }
318 }
319 msg = rx.recv() => {
320 match msg {
321 Ok(m) => RaceResult::Message(Box::new(m)),
322 Err(e) => RaceResult::RecvError(e.to_string()),
323 }
324 }
325 }
326 }
327 _ => match rx.recv().await {
328 Ok(msg) => RaceResult::Message(Box::new(msg)),
329 Err(e) => RaceResult::RecvError(e.to_string()),
330 },
331 };
332
333 match race_result {
334 RaceResult::Message(boxed) => {
335 let PythonMessage {
336 kind,
337 message,
338 refs,
339 } = *boxed;
340 match kind {
341 PythonMessageKind::Result { rank, .. } => Ok((message, refs, rank)),
342 PythonMessageKind::Exception { .. } => {
343 monarch_with_gil_blocking(GilSite::Traceback, |py| {
344 let mesh_references: VecDeque<Option<MeshRef>> =
345 refs.into_iter().map(Some).collect();
346 let mut state =
347 PicklingState::from_parts(message, VecDeque::new(), mesh_references);
348 Err(PyErr::from_value(
349 state.unpickle_with_receiver(py, instance)?.into_bound(py),
350 ))
351 })
352 }
353 other => Err(pyo3::exceptions::PyValueError::new_err(format!(
354 "unexpected message kind {:?}",
355 other
356 ))),
357 }
358 }
359 RaceResult::RecvError(e) => Err(pyo3::exceptions::PyEOFError::new_err(format!(
360 "Port closed: {}",
361 e
362 ))),
363 RaceResult::SupervisionError(err) => {
364 Err(supervision_error_to_pyerr(err, qualified_endpoint_name))
365 }
366 }
367}
368
369#[tracing::instrument(level = "debug", skip_all)]
373async fn collect_valuemesh(
374 extent: Extent,
375 rx: OncePortReceiver<PythonMessage>,
376 attrs: Arc<EndpointAttrs>,
377 supervision_monitor: Option<Arc<dyn Supervisable>>,
378 instance: &Instance<PythonActor>,
379 qualified_endpoint_name: Option<String>,
380) -> PyResult<Py<PyAny>> {
381 let start = tokio::time::Instant::now();
382
383 let expected_count = extent.num_ranks();
384
385 let record_guard = RecordEndpointGuard::new(start, attrs, EndpointAdverb::Call);
386
387 enum RaceResult {
388 Collected(Box<PythonMessage>),
389 SupervisionError(PyErr),
390 RecvError(String),
391 }
392
393 let race_result = match &supervision_monitor {
394 Some(sup) => {
395 tokio::select! {
396 biased;
397 result = sup.supervision_event(instance) => {
398 match result {
399 Some(err) => RaceResult::SupervisionError(err),
400 None => RaceResult::RecvError(
401 "supervision monitor closed unexpectedly".to_string()
402 ),
403 }
404 }
405 batch = rx.recv() => {
406 match batch {
407 Ok(b) => RaceResult::Collected(Box::new(b)),
408 Err(e) => RaceResult::RecvError(e.to_string()),
409 }
410 }
411 }
412 }
413 None => match rx.recv().await {
414 Ok(batch) => RaceResult::Collected(Box::new(batch)),
415 Err(e) => RaceResult::RecvError(e.to_string()),
416 },
417 };
418
419 match race_result {
420 RaceResult::Collected(boxed) => {
421 let msg = *boxed;
422 let overlay = msg.into_overlay().map_err(|e| {
423 pyo3::exceptions::PyRuntimeError::new_err(format!(
424 "failed to extract overlay from collected responses: {e}"
425 ))
426 })?;
427 monarch_with_gil_blocking(GilSite::ReplyConvert, |py| {
428 let has_refs = overlay.runs().any(|(_, payload)| {
445 let (PythonResponseMessage::Result { refs, .. }
446 | PythonResponseMessage::Exception { refs, .. }) = payload;
447 !refs.is_empty()
448 });
449
450 if !has_refs {
451 let mut parts = Vec::with_capacity(expected_count);
452 for (range, payload) in overlay.runs() {
453 match payload {
454 PythonResponseMessage::Result { part, .. } => {
455 parts.extend(range.clone().map(|_| part.clone()));
456 }
457 PythonResponseMessage::Exception { .. } => {
458 record_guard.mark_error();
459 return Err(PyErr::from_value(
460 payload.decode(py, instance)?.into_bound(py),
461 ));
462 }
463 }
464 }
465 return Ok(PyValueMesh::build_from_parts(&extent, parts)?
466 .into_pyobject(py)?
467 .into_any()
468 .unbind());
469 }
470
471 let mut objects: Vec<Py<PyAny>> = Vec::with_capacity(expected_count);
472 for (range, payload) in overlay.runs() {
473 match payload {
474 PythonResponseMessage::Result { .. } => {
475 let obj = payload.decode(py, instance)?;
476 objects.extend(range.clone().map(|_| obj.clone_ref(py)));
477 }
478 PythonResponseMessage::Exception { .. } => {
479 record_guard.mark_error();
480 return Err(PyErr::from_value(
481 payload.decode(py, instance)?.into_bound(py),
482 ));
483 }
484 }
485 }
486 Ok(PyValueMesh::build_from_objects(&extent, objects)?
487 .into_pyobject(py)?
488 .into_any()
489 .unbind())
490 })
491 }
492 RaceResult::RecvError(e) => {
493 record_guard.mark_error();
494 Err(pyo3::exceptions::PyEOFError::new_err(format!(
495 "Port closed: {}",
496 e
497 )))
498 }
499 RaceResult::SupervisionError(err) => {
500 record_guard.mark_error();
501 Err(supervision_error_to_pyerr(err, &qualified_endpoint_name))
502 }
503 }
504}
505
506fn value_collector(
507 mut receiver: PortReceiver<PythonMessage>,
508 attrs: Arc<EndpointAttrs>,
509 supervision_monitor: Option<Arc<dyn Supervisable>>,
510 instance: Instance<PythonActor>,
511 qualified_endpoint_name: Option<String>,
512 adverb: EndpointAdverb,
513 span_guard: SpanGuard,
514) -> PyResult<PyPythonTask> {
515 Ok(PythonTask::new(async move {
516 let _span_guard = span_guard;
517 let start = tokio::time::Instant::now();
518
519 let record_guard = RecordEndpointGuard::new(start, attrs, adverb);
520
521 match collect_value(
522 &mut receiver,
523 &supervision_monitor,
524 &instance,
525 &qualified_endpoint_name,
526 )
527 .await
528 {
529 Ok((message, refs, _)) => monarch_with_gil_blocking(GilSite::ReplyConvert, |py| {
530 let mesh_references: VecDeque<Option<MeshRef>> =
531 refs.into_iter().map(Some).collect();
532 let mut state =
533 PicklingState::from_parts(message, VecDeque::new(), mesh_references);
534 state.unpickle_with_receiver(py, &instance)
535 }),
536 Err(e) => {
537 record_guard.mark_error();
538 Err(e)
539 }
540 }
541 })?
542 .into())
543}
544
545#[pyclass(
550 name = "ValueStream",
551 module = "monarch._rust_bindings.monarch_hyperactor.endpoint"
552)]
553pub struct PyValueStream {
554 receiver: Arc<tokio::sync::Mutex<PortReceiver<PythonMessage>>>,
555 supervision_monitor: Option<Arc<dyn Supervisable>>,
557 instance: Instance<PythonActor>,
558 remaining: AtomicUsize,
559 attrs: Arc<EndpointAttrs>,
560 qualified_endpoint_name: Option<String>,
561 start: tokio::time::Instant,
562 future_class: Py<PyAny>,
563}
564
565#[pymethods]
566impl PyValueStream {
567 fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
568 slf
569 }
570
571 fn __next__(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
572 let remaining = self.remaining.load(Ordering::Relaxed);
573 if remaining == 0 {
574 return Ok(None);
575 }
576 self.remaining.store(remaining - 1, Ordering::Relaxed);
577
578 let receiver = self.receiver.clone();
579 let supervision_monitor = self.supervision_monitor.clone();
580 let instance = self.instance.clone_for_py();
581 let qualified_endpoint_name = self.qualified_endpoint_name.clone();
582 let start = self.start;
583 let attrs = self.attrs.clone();
584
585 let task: PyPythonTask = PythonTask::new(async move {
586 let record_guard = RecordEndpointGuard::new(start, attrs, EndpointAdverb::Stream);
587
588 let mut rx_guard = receiver.lock().await;
589
590 match collect_value(
591 &mut rx_guard,
592 &supervision_monitor,
593 &instance,
594 &qualified_endpoint_name,
595 )
596 .await
597 {
598 Ok((message, refs, _)) => monarch_with_gil_blocking(GilSite::ReplyConvert, |py| {
599 let mesh_references: VecDeque<Option<MeshRef>> =
600 refs.into_iter().map(Some).collect();
601 let mut state =
602 PicklingState::from_parts(message, VecDeque::new(), mesh_references);
603 state.unpickle_with_receiver(py, &instance)
604 }),
605 Err(e) => {
606 record_guard.mark_error();
607 Err(e)
608 }
609 }
610 })?
611 .into();
612
613 let future = self.future_class.call_method1(py, "_from_coro", (task,))?;
614 Ok(Some(future))
615 }
616}
617
618fn wrap_in_future(py: Python<'_>, task: PyPythonTask) -> PyResult<Py<PyAny>> {
619 let future = make_future(py).call_method1("_from_coro", (task,))?;
620 Ok(future.unbind())
621}
622
623pub(crate) trait Endpoint {
626 fn get_extent(&self, py: Python<'_>) -> PyResult<Extent>;
628
629 fn get_method_name(&self) -> &str;
631
632 fn metric_attrs(&self) -> &Arc<EndpointAttrs>;
636
637 fn send_message<'py>(
639 &self,
640 py: Python<'py>,
641 args: &Bound<'py, PyTuple>,
642 kwargs: Option<&Bound<'py, PyDict>>,
643 port_ref: Option<EitherPortRef>,
644 selection: AllOrChoose,
645 instance: &Instance<PythonActor>,
646 correlation_id: Option<u64>,
647 ) -> PyResult<()>;
648
649 fn send_message_with_headers<'py>(
654 &self,
655 py: Python<'py>,
656 args: &Bound<'py, PyTuple>,
657 kwargs: Option<&Bound<'py, PyDict>>,
658 port_ref: Option<EitherPortRef>,
659 selection: AllOrChoose,
660 instance: &Instance<PythonActor>,
661 _caller_headers: hyperactor_config::Flattrs,
662 correlation_id: Option<u64>,
663 ) -> PyResult<()> {
664 self.send_message(
665 py,
666 args,
667 kwargs,
668 port_ref,
669 selection,
670 instance,
671 correlation_id,
672 )
673 }
674
675 fn build_operation_context_headers(
681 &self,
682 adverb: EndpointAdverb,
683 ) -> hyperactor_config::Flattrs {
684 let adverb_str = adverb.as_str();
685 let attrs = crate::operation_context::build_operation_context_attrs(
686 self.get_qualified_name(),
687 Some(adverb_str),
688 );
689 let mut headers = hyperactor_config::Flattrs::new();
690 crate::operation_context::stamp_operation_context(&mut headers, &attrs);
691 headers
692 }
693
694 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>>;
696
697 fn get_qualified_name(&self) -> Option<String>;
699
700 fn enter_endpoint_span(
707 &self,
708 adverb: EndpointAdverb,
709 actor_id: &ActorAddr,
710 correlation_id: u64,
711 ) -> SpanGuard;
712
713 fn get_current_instance(&self, py: Python<'_>) -> PyResult<Instance<PythonActor>> {
714 let context = get_context(py).call0()?;
715 let py_instance: PyRef<PyInstance> = context.getattr("actor_instance")?.extract()?;
716 Ok(py_instance.clone().into_instance())
717 }
718
719 fn open_response_port(
720 &self,
721 instance: &Instance<PythonActor>,
722 ) -> (PythonPortRef, PortReceiver<PythonMessage>) {
723 let (p, receiver) = instance.mailbox_for_py().open_port::<PythonMessage>();
724 (PythonPortRef { inner: p.bind() }, receiver)
725 }
726
727 fn open_reduce_response_port(
728 &self,
729 instance: &Instance<PythonActor>,
730 ) -> (PythonOncePortRef, OncePortReceiver<PythonMessage>) {
731 let (p, receiver) = instance
732 .mailbox_for_py()
733 .open_reduce_port(PythonResponseMessageAccumulator);
734 (PythonOncePortRef::from(p.bind()), receiver)
735 }
736
737 #[tracing::instrument(level = "debug", skip_all)]
742 fn call<'py>(
743 &self,
744 py: Python<'py>,
745 args: &Bound<'py, PyTuple>,
746 kwargs: Option<&Bound<'py, PyDict>>,
747 ) -> PyResult<Py<PyAny>> {
748 let instance = self.get_current_instance(py)?;
749 let correlation_id: u64 = fastrand::u64(..);
750 let span_guard =
751 self.enter_endpoint_span(EndpointAdverb::Call, instance.self_addr(), correlation_id);
752
753 let extent = self.get_extent(py)?;
754 let attrs = self.metric_attrs().clone();
755 let (port_ref, receiver) = self.open_reduce_response_port(&instance);
756
757 let supervision_monitor = self.get_supervision_monitor();
758 let qualified_endpoint_name = self.get_qualified_name();
759
760 let caller_headers = self.build_operation_context_headers(EndpointAdverb::Call);
761 self.send_message_with_headers(
762 py,
763 args,
764 kwargs,
765 Some(EitherPortRef::Once(port_ref)),
766 AllOrChoose::All,
767 &instance,
768 caller_headers,
769 Some(correlation_id),
770 )?;
771
772 let instance_for_task = instance.clone_for_py();
773 let task: PyPythonTask = PythonTask::new(async move {
774 let _span_guard = span_guard;
775 collect_valuemesh(
776 extent,
777 receiver,
778 attrs,
779 supervision_monitor,
780 &instance_for_task,
781 qualified_endpoint_name,
782 )
783 .await
784 })?
785 .into();
786
787 wrap_in_future(py, task)
788 }
789
790 fn choose<'py>(
796 &self,
797 py: Python<'py>,
798 args: &Bound<'py, PyTuple>,
799 kwargs: Option<&Bound<'py, PyDict>>,
800 ) -> PyResult<Py<PyAny>> {
801 let instance = self.get_current_instance(py)?;
802 let correlation_id: u64 = fastrand::u64(..);
803 let span_guard =
804 self.enter_endpoint_span(EndpointAdverb::Choose, instance.self_addr(), correlation_id);
805 let (port_ref, receiver) = self.open_response_port(&instance);
806
807 let caller_headers = self.build_operation_context_headers(EndpointAdverb::Choose);
808 self.send_message_with_headers(
809 py,
810 args,
811 kwargs,
812 Some(EitherPortRef::Unbounded(port_ref)),
813 AllOrChoose::Choose,
814 &instance,
815 caller_headers,
816 Some(correlation_id),
817 )?;
818
819 let task = value_collector(
820 receiver,
821 self.metric_attrs().clone(),
822 self.get_supervision_monitor(),
823 instance.clone_for_py(),
824 self.get_qualified_name(),
825 EndpointAdverb::Choose,
826 span_guard,
827 )?;
828
829 wrap_in_future(py, task)
830 }
831
832 fn call_one<'py>(
834 &self,
835 py: Python<'py>,
836 args: &Bound<'py, PyTuple>,
837 kwargs: Option<&Bound<'py, PyDict>>,
838 ) -> PyResult<Py<PyAny>> {
839 let extent = self.get_extent(py)?;
840
841 if extent.num_ranks() != 1 {
842 return Err(pyo3::exceptions::PyValueError::new_err(format!(
843 "call_one requires exactly 1 actor, but mesh has {}",
844 extent.num_ranks()
845 )));
846 }
847
848 let instance = self.get_current_instance(py)?;
849 let correlation_id: u64 = fastrand::u64(..);
850 let span_guard = self.enter_endpoint_span(
851 EndpointAdverb::CallOne,
852 instance.self_addr(),
853 correlation_id,
854 );
855 let (port_ref, receiver) = self.open_response_port(&instance);
856
857 let caller_headers = self.build_operation_context_headers(EndpointAdverb::CallOne);
858 self.send_message_with_headers(
859 py,
860 args,
861 kwargs,
862 Some(EitherPortRef::Unbounded(port_ref)),
863 AllOrChoose::All,
864 &instance,
865 caller_headers,
866 Some(correlation_id),
867 )?;
868
869 let task = value_collector(
870 receiver,
871 self.metric_attrs().clone(),
872 self.get_supervision_monitor(),
873 instance.clone_for_py(),
874 self.get_qualified_name(),
875 EndpointAdverb::CallOne,
876 span_guard,
877 )?;
878
879 wrap_in_future(py, task)
880 }
881
882 fn stream<'py>(
884 &self,
885 py: Python<'py>,
886 args: &Bound<'py, PyTuple>,
887 kwargs: Option<&Bound<'py, PyDict>>,
888 ) -> PyResult<Py<PyAny>> {
889 let extent = self.get_extent(py)?;
890
891 let instance = self.get_current_instance(py)?;
892 let (port_ref, receiver) = self.open_response_port(&instance);
893
894 let correlation_id: u64 = fastrand::u64(..);
895 let _span_guard =
898 self.enter_endpoint_span(EndpointAdverb::Stream, instance.self_addr(), correlation_id);
899 let caller_headers = self.build_operation_context_headers(EndpointAdverb::Stream);
900 self.send_message_with_headers(
901 py,
902 args,
903 kwargs,
904 Some(EitherPortRef::Unbounded(port_ref)),
905 AllOrChoose::All,
906 &instance,
907 caller_headers,
908 Some(correlation_id),
909 )?;
910
911 let actor_count = extent.num_ranks();
912 let start = tokio::time::Instant::now();
913 let supervision_monitor = self.get_supervision_monitor();
914 let qualified_endpoint_name = self.get_qualified_name();
915 let future_class = make_future(py).unbind();
916
917 ENDPOINT_STREAM_THROUGHPUT.add(1, self.metric_attrs().as_slice());
918
919 let stream = PyValueStream {
920 receiver: Arc::new(tokio::sync::Mutex::new(receiver)),
921 supervision_monitor,
922 instance: instance.clone_for_py(),
923 remaining: AtomicUsize::new(actor_count),
924 attrs: self.metric_attrs().clone(),
925 qualified_endpoint_name,
926 start,
927 future_class,
928 };
929
930 Ok(stream.into_pyobject(py)?.unbind().into())
931 }
932
933 fn broadcast<'py>(
935 &self,
936 py: Python<'py>,
937 args: &Bound<'py, PyTuple>,
938 kwargs: Option<&Bound<'py, PyDict>>,
939 ) -> PyResult<()> {
940 let instance = self.get_current_instance(py)?;
941 let correlation_id: u64 = fastrand::u64(..);
942 let _span_guard = self.enter_endpoint_span(
943 EndpointAdverb::Broadcast,
944 instance.self_addr(),
945 correlation_id,
946 );
947 let attributes = self.metric_attrs().as_slice();
948
949 match self.send_message(
950 py,
951 args,
952 kwargs,
953 None,
954 AllOrChoose::All,
955 &instance,
956 Some(correlation_id),
957 ) {
958 Ok(()) => {
959 ENDPOINT_BROADCAST_THROUGHPUT.add(1, attributes);
960 Ok(())
961 }
962 Err(e) => {
963 ENDPOINT_BROADCAST_ERROR.add(1, attributes);
964 Err(e)
965 }
966 }
967 }
968}
969
970#[pyclass(
971 name = "ActorEndpoint",
972 module = "monarch._rust_bindings.monarch_hyperactor.endpoint"
973)]
974pub struct ActorEndpoint {
975 inner: Arc<dyn SupervisableActorMesh>,
976 shape: Shape,
977 method: MethodSpecifier,
978 mesh_name: String,
979 signature: Option<Py<PyAny>>,
980 proc_mesh: Option<Py<PyAny>>,
981 propagator: Option<Py<PyAny>>,
982 attrs: Arc<EndpointAttrs>,
985}
986
987impl ActorEndpoint {
988 fn create_message<'py>(
992 &self,
993 py: Python<'py>,
994 args: &Bound<'py, PyTuple>,
995 kwargs: Option<&Bound<'py, PyDict>>,
996 port_ref: Option<EitherPortRef>,
997 correlation_id: Option<u64>,
998 ) -> PyResult<PendingMessage> {
999 let port_ref_py: Py<PyAny> = match port_ref {
1000 Some(pr) => pr.clone().into_pyobject(py)?.unbind(),
1001 None => py.None(),
1002 };
1003
1004 let result = create_endpoint_message(py).call1((
1005 self.method.clone(),
1006 self.signature
1007 .as_ref()
1008 .map_or_else(|| py.None(), |s| s.clone_ref(py)),
1009 args,
1010 kwargs
1011 .map_or_else(|| PyDict::new(py), |d| d.clone())
1012 .into_any(),
1013 port_ref_py,
1014 self.proc_mesh
1015 .as_ref()
1016 .map_or_else(|| py.None(), |p| p.clone_ref(py)),
1017 correlation_id,
1018 ))?;
1019 let mut pending: PyRefMut<'_, PendingMessage> = result.extract()?;
1020 let message = pending.take()?;
1021
1022 ENDPOINT_MESSAGE_SIZE_HISTOGRAM.record(message.payload_len() as f64, self.attrs.as_slice());
1023
1024 Ok(message)
1025 }
1026}
1027
1028impl Endpoint for ActorEndpoint {
1029 fn get_extent(&self, _py: Python<'_>) -> PyResult<Extent> {
1030 Ok(self.shape.extent())
1031 }
1032
1033 fn get_method_name(&self) -> &str {
1034 self.method.name()
1035 }
1036
1037 fn metric_attrs(&self) -> &Arc<EndpointAttrs> {
1038 &self.attrs
1039 }
1040
1041 fn send_message<'py>(
1042 &self,
1043 py: Python<'py>,
1044 args: &Bound<'py, PyTuple>,
1045 kwargs: Option<&Bound<'py, PyDict>>,
1046 port_ref: Option<EitherPortRef>,
1047 selection: AllOrChoose,
1048 instance: &Instance<PythonActor>,
1049 correlation_id: Option<u64>,
1050 ) -> PyResult<()> {
1051 let message = self.create_message(py, args, kwargs, port_ref, correlation_id)?;
1052 self.inner.cast_unresolved(message, selection, instance)
1053 }
1054
1055 fn send_message_with_headers<'py>(
1056 &self,
1057 py: Python<'py>,
1058 args: &Bound<'py, PyTuple>,
1059 kwargs: Option<&Bound<'py, PyDict>>,
1060 port_ref: Option<EitherPortRef>,
1061 selection: AllOrChoose,
1062 instance: &Instance<PythonActor>,
1063 caller_headers: hyperactor_config::Flattrs,
1064 correlation_id: Option<u64>,
1065 ) -> PyResult<()> {
1066 let message = self.create_message(py, args, kwargs, port_ref, correlation_id)?;
1067 self.inner
1068 .cast_unresolved_with_headers(message, selection, instance, caller_headers)
1069 }
1070
1071 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>> {
1072 Some(self.inner.clone())
1073 }
1074
1075 fn get_qualified_name(&self) -> Option<String> {
1076 Some(format!("{}.{}()", self.mesh_name, self.method.name()))
1077 }
1078
1079 fn enter_endpoint_span(
1080 &self,
1081 adverb: EndpointAdverb,
1082 actor_id: &ActorAddr,
1083 correlation_id: u64,
1084 ) -> SpanGuard {
1085 let mesh = self.mesh_name.as_str();
1086 let method = self.get_method_name();
1087 SpanGuard::actor_endpoint(adverb.as_str(), actor_id, mesh, method, correlation_id)
1088 }
1089}
1090
1091#[pymethods]
1092impl ActorEndpoint {
1093 #[new]
1095 #[pyo3(signature = (actor_mesh, method, shape, mesh_name, signature=None, proc_mesh=None, propagator=None))]
1096 fn new(
1097 actor_mesh: PythonActorMesh,
1098 method: MethodSpecifier,
1099 shape: PyShape,
1100 mesh_name: String,
1101 signature: Option<Py<PyAny>>,
1102 proc_mesh: Option<Py<PyAny>>,
1103 propagator: Option<Py<PyAny>>,
1104 ) -> Self {
1105 let actor = Label::strip(&mesh_name);
1110 let attrs = Arc::new(EndpointAttrs::new(method.name(), Some(&actor)));
1111 Self {
1112 inner: actor_mesh.get_inner(),
1113 shape: shape.get_inner().clone(),
1114 method,
1115 mesh_name,
1116 signature,
1117 proc_mesh,
1118 propagator,
1119 attrs,
1120 }
1121 }
1122
1123 #[getter]
1125 fn _name(&self) -> MethodSpecifier {
1126 self.method.clone()
1127 }
1128
1129 #[getter]
1131 fn _signature(&self, py: Python<'_>) -> Py<PyAny> {
1132 self.signature
1133 .clone()
1134 .unwrap_or_else(|| py.None().into_any())
1135 }
1136
1137 #[getter]
1139 fn _actor_mesh(&self) -> PythonActorMesh {
1140 PythonActorMesh::from_impl(self.inner.clone())
1141 }
1142
1143 fn _propagate<'py>(
1146 &self,
1147 py: Python<'py>,
1148 args: &Bound<'py, PyAny>,
1149 kwargs: &Bound<'py, PyAny>,
1150 fake_args: &Bound<'py, PyAny>,
1151 fake_kwargs: &Bound<'py, PyAny>,
1152 ) -> PyResult<Py<PyAny>> {
1153 let do_propagate = py
1154 .import("monarch._src.actor.endpoint")?
1155 .getattr("_do_propagate")?;
1156 let propagator = self
1157 .propagator
1158 .as_ref()
1159 .map(|p| p.clone_ref(py).into_bound(py))
1160 .unwrap_or_else(|| py.None().into_bound(py));
1161 let cache = PyDict::new(py);
1162 do_propagate
1163 .call1((&propagator, args, kwargs, fake_args, fake_kwargs, cache))?
1164 .extract()
1165 .map_err(Into::into)
1166 }
1167
1168 fn _fetch_propagate<'py>(
1171 &self,
1172 py: Python<'py>,
1173 args: &Bound<'py, PyAny>,
1174 kwargs: &Bound<'py, PyAny>,
1175 fake_args: &Bound<'py, PyAny>,
1176 fake_kwargs: &Bound<'py, PyAny>,
1177 ) -> PyResult<Py<PyAny>> {
1178 if self.propagator.is_none() {
1179 return Ok(py.None());
1180 }
1181 self._propagate(py, args, kwargs, fake_args, fake_kwargs)
1182 }
1183
1184 fn _pipe_propagate<'py>(
1187 &self,
1188 py: Python<'py>,
1189 args: &Bound<'py, PyAny>,
1190 kwargs: &Bound<'py, PyAny>,
1191 fake_args: &Bound<'py, PyAny>,
1192 fake_kwargs: &Bound<'py, PyAny>,
1193 ) -> PyResult<Py<PyAny>> {
1194 let is_callable = self
1196 .propagator
1197 .as_ref()
1198 .map(|p| p.bind(py).is_callable())
1199 .unwrap_or(false);
1200 if !is_callable {
1201 return Err(pyo3::exceptions::PyValueError::new_err(
1202 "Must specify explicit callable for pipe",
1203 ));
1204 }
1205 self._propagate(py, args, kwargs, fake_args, fake_kwargs)
1206 }
1207
1208 #[pyo3(signature = (*args, **kwargs))]
1210 fn rref<'py>(
1211 slf: PyRef<'py, Self>,
1212 py: Python<'py>,
1213 args: &Bound<'py, PyTuple>,
1214 kwargs: Option<&Bound<'py, PyDict>>,
1215 ) -> PyResult<Py<PyAny>> {
1216 let kwargs_dict = kwargs.map_or_else(|| PyDict::new(py), |d| d.clone());
1217
1218 let result = dispatch_actor_rref(py).call1((slf.into_pyobject(py)?, args, kwargs_dict))?;
1220
1221 Ok(result.unbind())
1222 }
1223
1224 #[pyo3(signature = (*args, **kwargs), name = "call")]
1226 fn py_call<'py>(
1227 &self,
1228 py: Python<'py>,
1229 args: &Bound<'py, PyTuple>,
1230 kwargs: Option<&Bound<'py, PyDict>>,
1231 ) -> PyResult<Py<PyAny>> {
1232 self.call(py, args, kwargs)
1233 }
1234
1235 #[pyo3(signature = (*args, **kwargs), name = "choose")]
1241 fn py_choose<'py>(
1242 &self,
1243 py: Python<'py>,
1244 args: &Bound<'py, PyTuple>,
1245 kwargs: Option<&Bound<'py, PyDict>>,
1246 ) -> PyResult<Py<PyAny>> {
1247 self.choose(py, args, kwargs)
1248 }
1249
1250 #[pyo3(signature = (*args, **kwargs), name = "call_one")]
1252 fn py_call_one<'py>(
1253 &self,
1254 py: Python<'py>,
1255 args: &Bound<'py, PyTuple>,
1256 kwargs: Option<&Bound<'py, PyDict>>,
1257 ) -> PyResult<Py<PyAny>> {
1258 self.call_one(py, args, kwargs)
1259 }
1260
1261 #[pyo3(signature = (*args, **kwargs), name = "stream")]
1263 fn py_stream<'py>(
1264 &self,
1265 py: Python<'py>,
1266 args: &Bound<'py, PyTuple>,
1267 kwargs: Option<&Bound<'py, PyDict>>,
1268 ) -> PyResult<Py<PyAny>> {
1269 self.stream(py, args, kwargs)
1270 }
1271
1272 #[pyo3(signature = (*args, **kwargs), name = "broadcast")]
1274 fn py_broadcast<'py>(
1275 &self,
1276 py: Python<'py>,
1277 args: &Bound<'py, PyTuple>,
1278 kwargs: Option<&Bound<'py, PyDict>>,
1279 ) -> PyResult<()> {
1280 self.broadcast(py, args, kwargs)
1281 }
1282
1283 fn _send<'py>(
1285 &self,
1286 py: Python<'py>,
1287 args: &Bound<'py, PyTuple>,
1288 kwargs: &Bound<'py, PyDict>,
1289 port: Option<EitherPortRef>,
1290 selection: &str,
1291 ) -> PyResult<()> {
1292 let instance = self.get_current_instance(py)?;
1293 let sel = to_all_or_choose(selection)?;
1294 let correlation_id: u64 = fastrand::u64(..);
1295 tracing::info!(
1296 target: hyperactor_telemetry::sinks::perfetto::ENDPOINT_TELEMETRY_TARGET,
1297 actor_id = %instance.self_addr(),
1298 mesh = self.mesh_name.as_str(),
1299 method = self.get_method_name(),
1300 correlation_id,
1301 "send"
1302 );
1303 self.send_message(
1304 py,
1305 args,
1306 Some(kwargs),
1307 port,
1308 sel,
1309 &instance,
1310 Some(correlation_id),
1311 )
1312 }
1313}
1314
1315#[pyclass(
1320 name = "Remote",
1321 module = "monarch._rust_bindings.monarch_hyperactor.endpoint"
1322)]
1323pub struct Remote {
1324 inner: Py<PyAny>,
1326 attrs: Arc<EndpointAttrs>,
1327}
1328
1329impl Endpoint for Remote {
1330 fn get_extent(&self, py: Python<'_>) -> PyResult<Extent> {
1331 let extent: PyExtent = self.inner.call_method0(py, "_get_extent")?.extract(py)?;
1332 Ok(extent.into())
1333 }
1334
1335 fn get_method_name(&self) -> &str {
1336 UNKNOWN
1337 }
1338
1339 fn metric_attrs(&self) -> &Arc<EndpointAttrs> {
1340 &self.attrs
1341 }
1342
1343 fn send_message<'py>(
1344 &self,
1345 py: Python<'py>,
1346 args: &Bound<'py, PyTuple>,
1347 kwargs: Option<&Bound<'py, PyDict>>,
1348 port_ref: Option<EitherPortRef>,
1349 selection: AllOrChoose,
1350 _instance: &Instance<PythonActor>,
1351 correlation_id: Option<u64>,
1352 ) -> PyResult<()> {
1353 let send_kwargs = PyDict::new(py);
1354 match port_ref {
1355 Some(pr) => send_kwargs.set_item("port", pr.clone())?,
1356 None => send_kwargs.set_item("port", py.None())?,
1357 }
1358
1359 send_kwargs.set_item("selection", selection.as_str())?;
1360 send_kwargs.set_item("correlation_id", correlation_id)?;
1361
1362 let kwargs_dict = kwargs.map_or_else(|| PyDict::new(py), |d| d.clone());
1363 self.inner
1364 .call_method(py, "_send", (args.clone(), kwargs_dict), Some(&send_kwargs))?;
1365
1366 Ok(())
1367 }
1368
1369 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>> {
1370 None }
1372
1373 fn get_qualified_name(&self) -> Option<String> {
1374 None }
1376
1377 fn enter_endpoint_span(
1378 &self,
1379 adverb: EndpointAdverb,
1380 actor_id: &ActorAddr,
1381 correlation_id: u64,
1382 ) -> SpanGuard {
1383 let call_name = monarch_with_gil_blocking(GilSite::DisplayName, |py| {
1384 self.inner
1385 .call_method0(py, "_call_name")
1386 .ok()
1387 .and_then(|v| v.extract::<String>(py).ok())
1388 });
1389 let call_name = call_name.as_deref().unwrap_or("");
1390 SpanGuard::remote(adverb.as_str(), actor_id, call_name, correlation_id)
1391 }
1392}
1393
1394#[pymethods]
1395impl Remote {
1396 #[new]
1398 fn new(remote: Py<PyAny>) -> Self {
1399 let attrs = Arc::new(EndpointAttrs::new(UNKNOWN, None));
1400 Self {
1401 inner: remote,
1402 attrs,
1403 }
1404 }
1405
1406 #[pyo3(signature = (*args, **kwargs), name = "call")]
1408 fn py_call<'py>(
1409 &self,
1410 py: Python<'py>,
1411 args: &Bound<'py, PyTuple>,
1412 kwargs: Option<&Bound<'py, PyDict>>,
1413 ) -> PyResult<Py<PyAny>> {
1414 self.call(py, args, kwargs)
1415 }
1416
1417 #[pyo3(signature = (*args, **kwargs), name = "choose")]
1423 fn py_choose<'py>(
1424 &self,
1425 py: Python<'py>,
1426 args: &Bound<'py, PyTuple>,
1427 kwargs: Option<&Bound<'py, PyDict>>,
1428 ) -> PyResult<Py<PyAny>> {
1429 self.choose(py, args, kwargs)
1430 }
1431
1432 #[pyo3(signature = (*args, **kwargs), name = "call_one")]
1434 fn py_call_one<'py>(
1435 &self,
1436 py: Python<'py>,
1437 args: &Bound<'py, PyTuple>,
1438 kwargs: Option<&Bound<'py, PyDict>>,
1439 ) -> PyResult<Py<PyAny>> {
1440 self.call_one(py, args, kwargs)
1441 }
1442
1443 #[pyo3(signature = (*args, **kwargs), name = "stream")]
1445 fn py_stream<'py>(
1446 &self,
1447 py: Python<'py>,
1448 args: &Bound<'py, PyTuple>,
1449 kwargs: Option<&Bound<'py, PyDict>>,
1450 ) -> PyResult<Py<PyAny>> {
1451 self.stream(py, args, kwargs)
1452 }
1453
1454 #[pyo3(signature = (*args, **kwargs), name = "broadcast")]
1456 fn py_broadcast<'py>(
1457 &self,
1458 py: Python<'py>,
1459 args: &Bound<'py, PyTuple>,
1460 kwargs: Option<&Bound<'py, PyDict>>,
1461 ) -> PyResult<()> {
1462 self.broadcast(py, args, kwargs)
1463 }
1464
1465 #[pyo3(signature = (*args, **kwargs))]
1467 fn rref<'py>(
1468 &self,
1469 py: Python<'py>,
1470 args: &Bound<'py, PyTuple>,
1471 kwargs: Option<&Bound<'py, PyDict>>,
1472 ) -> PyResult<Py<PyAny>> {
1473 let kwargs_dict = kwargs.map_or_else(|| PyDict::new(py), |d| d.clone());
1474 self.inner.call_method(py, "rref", args, Some(&kwargs_dict))
1475 }
1476
1477 fn _call_name(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1479 self.inner.call_method0(py, "_call_name")
1480 }
1481
1482 #[getter]
1484 fn _maybe_resolvable(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1485 self.inner.getattr(py, "_maybe_resolvable")
1486 }
1487
1488 #[getter]
1490 fn _resolvable(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1491 self.inner.getattr(py, "_resolvable")
1492 }
1493
1494 #[getter]
1497 fn _remote_impl(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1498 self.inner.getattr(py, "_remote_impl")
1499 }
1500
1501 fn _propagate<'py>(
1504 &self,
1505 py: Python<'py>,
1506 args: &Bound<'py, PyAny>,
1507 kwargs: &Bound<'py, PyAny>,
1508 fake_args: &Bound<'py, PyAny>,
1509 fake_kwargs: &Bound<'py, PyAny>,
1510 ) -> PyResult<Py<PyAny>> {
1511 self.inner
1512 .call_method1(py, "_propagate", (args, kwargs, fake_args, fake_kwargs))
1513 }
1514
1515 fn _fetch_propagate<'py>(
1518 &self,
1519 py: Python<'py>,
1520 args: &Bound<'py, PyAny>,
1521 kwargs: &Bound<'py, PyAny>,
1522 fake_args: &Bound<'py, PyAny>,
1523 fake_kwargs: &Bound<'py, PyAny>,
1524 ) -> PyResult<Py<PyAny>> {
1525 self.inner.call_method1(
1526 py,
1527 "_fetch_propagate",
1528 (args, kwargs, fake_args, fake_kwargs),
1529 )
1530 }
1531
1532 fn _pipe_propagate<'py>(
1535 &self,
1536 py: Python<'py>,
1537 args: &Bound<'py, PyAny>,
1538 kwargs: &Bound<'py, PyAny>,
1539 fake_args: &Bound<'py, PyAny>,
1540 fake_kwargs: &Bound<'py, PyAny>,
1541 ) -> PyResult<Py<PyAny>> {
1542 self.inner.call_method1(
1543 py,
1544 "_pipe_propagate",
1545 (args, kwargs, fake_args, fake_kwargs),
1546 )
1547 }
1548
1549 fn _send<'py>(
1552 &self,
1553 py: Python<'py>,
1554 args: &Bound<'py, PyTuple>,
1555 kwargs: &Bound<'py, PyDict>,
1556 port: Option<Py<PyAny>>,
1557 selection: &str,
1558 ) -> PyResult<()> {
1559 self.inner.call_method(
1560 py,
1561 "_send",
1562 (args, kwargs),
1563 Some(&{
1564 let d = PyDict::new(py);
1565 d.set_item("port", port.unwrap_or_else(|| py.None()))?;
1566 d.set_item("selection", selection)?;
1567 d
1568 }),
1569 )?;
1570 Ok(())
1571 }
1572
1573 #[pyo3(signature = (*args, **kwargs))]
1575 fn __call__<'py>(
1576 &self,
1577 py: Python<'py>,
1578 args: &Bound<'py, PyTuple>,
1579 kwargs: Option<&Bound<'py, PyDict>>,
1580 ) -> PyResult<Py<PyAny>> {
1581 self.rref(py, args, kwargs)
1582 }
1583}
1584
1585pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
1586 module.add_class::<PyValueStream>()?;
1587 module.add_class::<ActorEndpoint>()?;
1588 module.add_class::<Remote>()?;
1589
1590 Ok(())
1591}
1592
1593#[derive(Named)]
1594struct PythonResponseMessageReducer;
1595
1596impl CommReducer for PythonResponseMessageReducer {
1597 type Update = PythonMessage;
1598
1599 fn reduce(&self, left: Self::Update, right: Self::Update) -> anyhow::Result<Self::Update> {
1600 Ok(ValueOverlay::try_from_runs(rle::merge_value_runs(
1601 left.into_overlay()?.into_runs(),
1602 right.into_overlay()?.into_runs(),
1603 ))?
1604 .into())
1605 }
1606}
1607
1608inventory::submit! {
1609 ReducerFactory {
1610 typehash_f: <PythonResponseMessageReducer as Named>::typehash,
1611 builder_f: |_| Ok(Box::new(PythonResponseMessageReducer)),
1612 }
1613}
1614
1615struct PythonResponseMessageAccumulator;
1616
1617impl Accumulator for PythonResponseMessageAccumulator {
1618 type State = PythonMessage;
1619 type Update = PythonMessage;
1620
1621 fn accumulate(&self, state: &mut Self::State, update: Self::Update) -> anyhow::Result<()> {
1622 *state = ValueOverlay::try_from_runs(rle::merge_value_runs(
1623 std::mem::take(state).into_overlay()?.into_runs(),
1624 update.into_overlay()?.into_runs(),
1625 ))?
1626 .into();
1627
1628 Ok(())
1629 }
1630
1631 fn reducer_spec(&self) -> Option<ReducerSpec> {
1632 Some(ReducerSpec {
1633 typehash: <PythonResponseMessageReducer as Named>::typehash(),
1634 builder_params: None,
1635 })
1636 }
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641 use std::sync::Mutex;
1642 use std::sync::atomic::AtomicUsize;
1643 use std::sync::atomic::Ordering;
1644 use std::time::Duration;
1645
1646 use hyperactor::ActorAddr;
1647 use hyperactor::OncePortRef;
1648 use hyperactor::Proc;
1649 use hyperactor::mailbox::MessageEnvelope;
1650 use hyperactor::mailbox::PortSender as _;
1651 use hyperactor::mailbox::Undeliverable;
1652 use hyperactor::mailbox::headers::OPERATION_ADVERB;
1653 use hyperactor::mailbox::headers::OPERATION_ENDPOINT;
1654 use pyo3::PyTypeInfo;
1655 use pyo3::exceptions::PyValueError;
1656
1657 use super::*;
1658
1659 struct TestEndpoint {
1663 qualified_name: Option<String>,
1664 }
1665
1666 impl Endpoint for TestEndpoint {
1667 fn get_extent(&self, _py: Python<'_>) -> PyResult<Extent> {
1668 unreachable!()
1669 }
1670 fn get_method_name(&self) -> &str {
1671 unreachable!()
1672 }
1673 fn metric_attrs(&self) -> &Arc<EndpointAttrs> {
1674 unreachable!()
1675 }
1676 fn send_message<'py>(
1677 &self,
1678 _py: Python<'py>,
1679 _args: &Bound<'py, PyTuple>,
1680 _kwargs: Option<&Bound<'py, PyDict>>,
1681 _port_ref: Option<EitherPortRef>,
1682 _selection: AllOrChoose,
1683 _instance: &Instance<PythonActor>,
1684 _correlation_id: Option<u64>,
1685 ) -> PyResult<()> {
1686 unreachable!()
1687 }
1688 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>> {
1689 None
1690 }
1691 fn get_qualified_name(&self) -> Option<String> {
1692 self.qualified_name.clone()
1693 }
1694 fn enter_endpoint_span(
1695 &self,
1696 _adverb: EndpointAdverb,
1697 _actor_id: &ActorAddr,
1698 _correlation_id: u64,
1699 ) -> SpanGuard {
1700 unreachable!()
1701 }
1702 }
1703
1704 #[test]
1708 fn test_rc1_build_operation_context_headers_stamps_each_adverb() {
1709 let ep = TestEndpoint {
1710 qualified_name: Some("training.Philosopher.ping()".to_string()),
1711 };
1712 for (adverb, expected_adverb) in [
1713 (EndpointAdverb::Call, "call"),
1714 (EndpointAdverb::CallOne, "call_one"),
1715 (EndpointAdverb::Choose, "choose"),
1716 (EndpointAdverb::Stream, "stream"),
1717 ] {
1718 let headers = ep.build_operation_context_headers(adverb);
1719 assert_eq!(
1720 headers.get(OPERATION_ENDPOINT).as_deref(),
1721 Some("training.Philosopher.ping()"),
1722 "adverb {:?}: OPERATION_ENDPOINT",
1723 adverb,
1724 );
1725 assert_eq!(
1726 headers.get(OPERATION_ADVERB).as_deref(),
1727 Some(expected_adverb),
1728 "adverb {:?}: OPERATION_ADVERB",
1729 adverb,
1730 );
1731 }
1732 }
1733
1734 #[test]
1739 fn test_rc1_build_operation_context_headers_omits_endpoint_when_no_qualified_name() {
1740 let ep = TestEndpoint {
1741 qualified_name: None,
1742 };
1743 let headers = ep.build_operation_context_headers(EndpointAdverb::CallOne);
1744 assert!(
1745 headers.get(OPERATION_ENDPOINT).is_none(),
1746 "OPERATION_ENDPOINT must be absent when endpoint has no qualified name",
1747 );
1748 assert_eq!(
1749 headers.get(OPERATION_ADVERB).as_deref(),
1750 Some("call_one"),
1751 "OPERATION_ADVERB should still be stamped",
1752 );
1753 }
1754
1755 const WAIT: Duration = Duration::from_secs(30);
1756
1757 fn test_instance(name: &str) -> Instance<PythonActor> {
1758 Proc::isolated()
1759 .actor_instance::<PythonActor>(name)
1760 .expect("the test actor instance should be created")
1761 .instance
1762 }
1763
1764 fn result_message(rank: Option<usize>) -> PythonMessage {
1765 PythonMessage {
1766 kind: PythonMessageKind::Result { rank },
1767 ..Default::default()
1768 }
1769 }
1770
1771 fn unexpected_message(name: &str) -> (PythonMessage, String) {
1772 let kind = PythonMessageKind::CallMethod {
1773 name: MethodSpecifier::ReturnsResponse {
1774 name: name.to_string(),
1775 },
1776 response_port: None,
1777 correlation_id: None,
1778 };
1779 let error = format!("unexpected message kind {kind:?}");
1780 (
1781 PythonMessage {
1782 kind,
1783 ..Default::default()
1784 },
1785 error,
1786 )
1787 }
1788
1789 fn test_span(instance: &Instance<PythonActor>, adverb: EndpointAdverb) -> SpanGuard {
1790 SpanGuard::actor_endpoint(
1791 adverb.as_str(),
1792 instance.self_addr(),
1793 "ownership_test_mesh",
1794 "ownership_test",
1795 0,
1796 )
1797 }
1798
1799 #[pyclass]
1800 struct IdentityFuture;
1801
1802 #[pymethods]
1803 impl IdentityFuture {
1804 #[staticmethod]
1805 fn _from_coro(task: Py<PyPythonTask>) -> Py<PyPythonTask> {
1806 task
1807 }
1808 }
1809
1810 fn install_future_test_module(py: Python<'_>) -> PyResult<()> {
1814 let modules = py
1815 .import("sys")?
1816 .getattr("modules")?
1817 .cast_into::<PyDict>()
1818 .map_err(PyErr::from)?;
1819 let mut path = String::new();
1820 let mut parent: Option<Bound<'_, PyModule>> = None;
1821 for part in "monarch._src.actor.future".split('.') {
1822 if !path.is_empty() {
1823 path.push('.');
1824 }
1825 path.push_str(part);
1826 let module = match modules.get_item(&path)? {
1827 Some(module) => module.cast_into::<PyModule>().map_err(PyErr::from)?,
1828 None => {
1829 let module = PyModule::new(py, &path)?;
1830 modules.set_item(&path, &module)?;
1831 module
1832 }
1833 };
1834 if let Some(parent) = &parent
1835 && parent.getattr(part).is_err()
1836 {
1837 parent.setattr(part, &module)?;
1838 }
1839 parent = Some(module);
1840 }
1841 let module = parent.expect("the dotted module path is not empty");
1842 if module.getattr("Future").is_err() {
1843 module.setattr("Future", py.get_type::<IdentityFuture>())?;
1844 }
1845 Ok(())
1846 }
1847
1848 async fn drive_task(task: Py<PyPythonTask>) -> PyResult<Py<PyAny>> {
1849 let task = monarch_with_gil_blocking(GilSite::Test, |py| task.borrow_mut(py).take_task())
1850 .expect("the task should be unconsumed");
1851 tokio::time::timeout(WAIT, task)
1852 .await
1853 .expect("timed out driving the task")
1854 }
1855
1856 fn assert_message_error(error: PyErr, expected: &str) {
1857 monarch_with_gil_blocking(GilSite::Test, |py| {
1858 assert!(error.get_type(py).is(PyValueError::type_object(py)));
1859 assert_eq!(error.value(py).to_string(), expected);
1860 });
1861 }
1862
1863 async fn assert_task_message_error(task: Py<PyPythonTask>, expected: &str) {
1864 let error = match drive_task(task).await {
1865 Ok(_) => panic!("the unexpected message kind should be rejected"),
1866 Err(error) => error,
1867 };
1868 assert_message_error(error, expected);
1869 }
1870
1871 fn value_stream(
1872 instance: &Instance<PythonActor>,
1873 receiver: PortReceiver<PythonMessage>,
1874 remaining: usize,
1875 ) -> PyValueStream {
1876 let future_class = monarch_with_gil_blocking(GilSite::Test, |py| {
1877 py.get_type::<IdentityFuture>().into_any().unbind()
1878 });
1879 PyValueStream {
1880 receiver: Arc::new(tokio::sync::Mutex::new(receiver)),
1881 supervision_monitor: None,
1882 instance: instance.clone_for_py(),
1883 remaining: AtomicUsize::new(remaining),
1884 attrs: Arc::new(EndpointAttrs::new("ownership_test", None)),
1885 qualified_endpoint_name: None,
1886 start: tokio::time::Instant::now(),
1887 future_class,
1888 }
1889 }
1890
1891 fn next_task(stream: &PyValueStream) -> Option<Py<PyPythonTask>> {
1892 monarch_with_gil_blocking(GilSite::Test, |py| -> PyResult<Option<Py<PyPythonTask>>> {
1893 stream
1894 .__next__(py)?
1895 .map(|task| task.extract(py).map_err(Into::<PyErr>::into))
1896 .transpose()
1897 })
1898 .expect("ValueStream.__next__ should construct its task")
1899 }
1900
1901 #[tokio::test]
1902 async fn value_collector_drop_discards_its_reply() {
1903 pyo3::Python::initialize();
1904 let instance = test_instance("value_collector_drop_discards_its_reply");
1905 let (handle, receiver) = instance.mailbox_for_py().open_port::<PythonMessage>();
1906 handle
1907 .try_post(&instance, result_message(None))
1908 .expect("the positive-control reply should be queued");
1909
1910 let task = value_collector(
1911 receiver,
1912 Arc::new(EndpointAttrs::new("ownership_test", None)),
1913 None,
1914 instance.clone_for_py(),
1915 None,
1916 EndpointAdverb::Choose,
1917 test_span(&instance, EndpointAdverb::Choose),
1918 )
1919 .expect("value_collector should return a task");
1920 drop(task);
1921
1922 assert!(
1923 handle.try_post(&instance, result_message(None)).is_err(),
1924 "dropping the task should close its sole response receiver"
1925 );
1926 }
1927
1928 #[tokio::test]
1929 async fn value_stream_observers_receive_in_observation_order() {
1930 pyo3::Python::initialize();
1931 let instance = test_instance("value_stream_observers_receive_in_observation_order");
1932 let (handle, receiver) = instance.mailbox_for_py().open_port::<PythonMessage>();
1933 let (first_reply, first_error) = unexpected_message("first reply");
1934 let (second_reply, second_error) = unexpected_message("second reply");
1935 handle
1936 .try_post(&instance, first_reply)
1937 .expect("the first reply should be queued");
1938 handle
1939 .try_post(&instance, second_reply)
1940 .expect("the second reply should be queued");
1941 let stream = value_stream(&instance, receiver, 2);
1942
1943 let first = next_task(&stream).expect("the first slot should yield a task");
1944 let second = next_task(&stream).expect("the second slot should yield a task");
1945
1946 assert_task_message_error(second, &first_error).await;
1947 assert_task_message_error(first, &second_error).await;
1948 assert!(
1949 next_task(&stream).is_none(),
1950 "the two constructed observers should exhaust the stream"
1951 );
1952 }
1953
1954 #[tokio::test]
1955 async fn dropped_value_stream_observer_leaves_reply_but_burns_slot_stranding_tail() {
1956 pyo3::Python::initialize();
1957 let instance = test_instance("dropped_value_stream_observer_leaves_reply_but_burns_slot");
1958 let (handle, receiver) = instance.mailbox_for_py().open_port::<PythonMessage>();
1959 let (first_reply, first_error) = unexpected_message("first reply");
1960 let (second_reply, _) = unexpected_message("second reply");
1961 let second_kind = second_reply.kind.clone();
1962 handle
1963 .try_post(&instance, first_reply)
1964 .expect("the first reply should be queued");
1965 handle
1966 .try_post(&instance, second_reply)
1967 .expect("the second reply should be queued");
1968 let stream = value_stream(&instance, receiver, 2);
1969
1970 let discarded = next_task(&stream).expect("the first slot should yield a task");
1971 monarch_with_gil_blocking(GilSite::Test, |_py| drop(discarded));
1972
1973 let retained = next_task(&stream).expect("the second slot should yield a task");
1974 assert_task_message_error(retained, &first_error).await;
1975 assert!(
1976 next_task(&stream).is_none(),
1977 "discarding an observer should still consume its iteration slot"
1978 );
1979
1980 let stranded = stream
1981 .receiver
1982 .lock()
1983 .await
1984 .try_recv()
1985 .expect("the shared receiver should remain usable")
1986 .expect("the final reply should remain queued but unreachable by iteration");
1987 assert_eq!(stranded.kind, second_kind);
1988 }
1989
1990 struct RecordingCallEndpoint {
1991 instance: Instance<PythonActor>,
1992 attrs: Arc<EndpointAttrs>,
1993 submissions: AtomicUsize,
1994 response_ports: Mutex<Vec<OncePortRef<PythonMessage>>>,
1995 }
1996
1997 impl RecordingCallEndpoint {
1998 fn new(instance: Instance<PythonActor>) -> Self {
1999 Self {
2000 instance,
2001 attrs: Arc::new(EndpointAttrs::new("ownership_test", None)),
2002 submissions: AtomicUsize::new(0),
2003 response_ports: Mutex::new(Vec::new()),
2004 }
2005 }
2006
2007 fn take_response_port(&self) -> OncePortRef<PythonMessage> {
2008 self.response_ports
2009 .lock()
2010 .expect("the response-port recorder should not be poisoned")
2011 .pop()
2012 .expect("call should have supplied a response port")
2013 }
2014 }
2015
2016 impl Endpoint for RecordingCallEndpoint {
2017 fn get_extent(&self, _py: Python<'_>) -> PyResult<Extent> {
2018 Ok(Extent::unity())
2019 }
2020
2021 fn get_method_name(&self) -> &str {
2022 "ownership_test"
2023 }
2024
2025 fn metric_attrs(&self) -> &Arc<EndpointAttrs> {
2026 &self.attrs
2027 }
2028
2029 fn send_message<'py>(
2030 &self,
2031 _py: Python<'py>,
2032 _args: &Bound<'py, PyTuple>,
2033 _kwargs: Option<&Bound<'py, PyDict>>,
2034 port_ref: Option<EitherPortRef>,
2035 _selection: AllOrChoose,
2036 _instance: &Instance<PythonActor>,
2037 _correlation_id: Option<u64>,
2038 ) -> PyResult<()> {
2039 let Some(EitherPortRef::Once(PythonOncePortRef { inner: Some(port) })) = port_ref
2040 else {
2041 panic!("call should submit one reducible response port");
2042 };
2043 self.response_ports
2044 .lock()
2045 .expect("the response-port recorder should not be poisoned")
2046 .push(port);
2047 self.submissions.fetch_add(1, Ordering::SeqCst);
2048 Ok(())
2049 }
2050
2051 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>> {
2052 None
2053 }
2054
2055 fn get_qualified_name(&self) -> Option<String> {
2056 Some("ownership_test.call()".to_string())
2057 }
2058
2059 fn enter_endpoint_span(
2060 &self,
2061 adverb: EndpointAdverb,
2062 actor_id: &ActorAddr,
2063 correlation_id: u64,
2064 ) -> SpanGuard {
2065 SpanGuard::actor_endpoint(
2066 adverb.as_str(),
2067 actor_id,
2068 "ownership_test_mesh",
2069 "ownership_test",
2070 correlation_id,
2071 )
2072 }
2073
2074 fn get_current_instance(&self, _py: Python<'_>) -> PyResult<Instance<PythonActor>> {
2075 Ok(self.instance.clone_for_py())
2076 }
2077 }
2078
2079 fn call_observer(endpoint: &RecordingCallEndpoint) -> Py<PyPythonTask> {
2080 monarch_with_gil_blocking(GilSite::Test, |py| -> PyResult<Py<PyPythonTask>> {
2081 install_future_test_module(py)?;
2082 endpoint
2083 .call(py, &PyTuple::empty(py), None)?
2084 .extract(py)
2085 .map_err(Into::<PyErr>::into)
2086 })
2087 .expect("call should return an observer")
2088 }
2089
2090 #[tokio::test]
2091 async fn call_submits_cast_before_observer_and_drop_closes_response_port() {
2092 pyo3::Python::initialize();
2093 let instance = test_instance("call_submits_cast_before_observer_and_drop_closes_port");
2094 let endpoint = RecordingCallEndpoint::new(instance.clone_for_py());
2095
2096 let retained = call_observer(&endpoint);
2097 assert_eq!(
2098 endpoint.submissions.load(Ordering::SeqCst),
2099 1,
2100 "submission should happen before the observer is driven"
2101 );
2102 let retained_port = endpoint.take_response_port();
2103 let (return_handle, _return_receiver) = instance
2104 .mailbox_for_py()
2105 .open_port::<Undeliverable<MessageEnvelope>>();
2106 instance
2107 .mailbox_for_py()
2108 .serialize_and_send_once(retained_port, result_message(Some(0)), return_handle)
2109 .expect("the retained observer's response should serialize");
2110 let collected = drive_task(retained)
2111 .await
2112 .expect("the retained observer should collect its response");
2113 monarch_with_gil_blocking(GilSite::Test, |py| {
2114 collected
2115 .extract::<Py<PyValueMesh>>(py)
2116 .expect("the retained observer should collect its response");
2117 });
2118
2119 let discarded = call_observer(&endpoint);
2120 assert_eq!(
2121 endpoint.submissions.load(Ordering::SeqCst),
2122 2,
2123 "the second operation should also be submitted before observation"
2124 );
2125 let dropped_port = endpoint.take_response_port();
2126 let dropped_destination = dropped_port.port_addr().clone();
2127 monarch_with_gil_blocking(GilSite::Test, |_py| drop(discarded));
2128
2129 let (return_handle, mut return_receiver) = instance
2130 .mailbox_for_py()
2131 .open_port::<Undeliverable<MessageEnvelope>>();
2132 instance
2133 .mailbox_for_py()
2134 .serialize_and_send_once(dropped_port, result_message(Some(0)), return_handle)
2135 .expect("the post should serialize before delivery is rejected");
2136 let undeliverable = tokio::time::timeout(WAIT, return_receiver.recv())
2137 .await
2138 .expect("timed out waiting for the dropped port's delivery failure")
2139 .expect("the return port should receive the delivery failure");
2140 let Undeliverable::Returned(envelope) = undeliverable else {
2141 panic!("the failed response should retain its original envelope");
2142 };
2143 assert_eq!(envelope.dest(), &dropped_destination);
2144 assert_eq!(endpoint.submissions.load(Ordering::SeqCst), 2);
2145 }
2146}