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::mailbox::OncePortReceiver;
22use hyperactor::mailbox::PortReceiver;
23use hyperactor_mesh::value_mesh::ValueOverlay;
24use hyperactor_mesh::value_mesh::rle;
25use monarch_types::py_global;
26use ndslice::Extent;
27use ndslice::Shape;
28use pyo3::prelude::*;
29use pyo3::types::PyDict;
30use pyo3::types::PyTuple;
31use serde_multipart::Part;
32use typeuri::Named;
33
34use crate::actor::MeshRef;
35use crate::actor::MethodSpecifier;
36use crate::actor::PythonActor;
37use crate::actor::PythonMessage;
38use crate::actor::PythonMessageKind;
39use crate::actor::PythonResponseMessage;
40use crate::actor_mesh::AllOrChoose;
41use crate::actor_mesh::PythonActorMesh;
42use crate::actor_mesh::SupervisableActorMesh;
43use crate::actor_mesh::to_all_or_choose;
44use crate::context::PyInstance;
45use crate::mailbox::EitherPortRef;
46use crate::mailbox::PythonOncePortRef;
47use crate::mailbox::PythonPortRef;
48use crate::metrics::ENDPOINT_BROADCAST_ERROR;
49use crate::metrics::ENDPOINT_BROADCAST_THROUGHPUT;
50use crate::metrics::ENDPOINT_CALL_ERROR;
51use crate::metrics::ENDPOINT_CALL_LATENCY_US_HISTOGRAM;
52use crate::metrics::ENDPOINT_CALL_ONE_ERROR;
53use crate::metrics::ENDPOINT_CALL_ONE_LATENCY_US_HISTOGRAM;
54use crate::metrics::ENDPOINT_CALL_ONE_THROUGHPUT;
55use crate::metrics::ENDPOINT_CALL_THROUGHPUT;
56use crate::metrics::ENDPOINT_CHOOSE_ERROR;
57use crate::metrics::ENDPOINT_CHOOSE_LATENCY_US_HISTOGRAM;
58use crate::metrics::ENDPOINT_CHOOSE_THROUGHPUT;
59use crate::metrics::ENDPOINT_STREAM_ERROR;
60use crate::metrics::ENDPOINT_STREAM_LATENCY_US_HISTOGRAM;
61use crate::metrics::ENDPOINT_STREAM_THROUGHPUT;
62use crate::pickle::PendingMessage;
63use crate::pickle::PicklingState;
64use crate::pytokio::PyPythonTask;
65use crate::pytokio::PythonTask;
66use crate::runtime::GilSite;
67use crate::runtime::monarch_with_gil_blocking;
68use crate::shape::PyExtent;
69use crate::shape::PyShape;
70use crate::supervision::Supervisable;
71use crate::supervision::SupervisionError;
72use crate::value_mesh::PyValueMesh;
73
74py_global!(get_context, "monarch._src.actor.actor_mesh", "context");
75py_global!(
76 create_endpoint_message,
77 "monarch._src.actor.actor_mesh",
78 "_create_endpoint_message"
79);
80py_global!(
81 dispatch_actor_rref,
82 "monarch._src.actor.actor_mesh",
83 "_dispatch_actor_rref"
84);
85py_global!(make_future, "monarch._src.actor.future", "Future");
86
87#[derive(Clone, Copy, Debug)]
91pub(crate) enum EndpointAdverb {
92 Call,
93 CallOne,
94 Choose,
95 Stream,
96}
97
98impl EndpointAdverb {
99 fn as_str(self) -> &'static str {
100 match self {
101 Self::Call => "call",
102 Self::CallOne => "call_one",
103 Self::Choose => "choose",
104 Self::Stream => "stream",
105 }
106 }
107}
108
109pub struct RecordEndpointGuard {
114 start: tokio::time::Instant,
115 method_name: String,
116 actor_count: usize,
117 adverb: EndpointAdverb,
118 error_occurred: Cell<bool>,
119}
120
121impl RecordEndpointGuard {
122 fn new(
123 start: tokio::time::Instant,
124 method_name: String,
125 actor_count: usize,
126 adverb: EndpointAdverb,
127 ) -> Self {
128 let attributes = hyperactor_telemetry::kv_pairs!(
129 "method" => method_name.clone()
130 );
131 match adverb {
132 EndpointAdverb::Call => {
133 ENDPOINT_CALL_THROUGHPUT.add(1, attributes);
134 }
135 EndpointAdverb::CallOne => {
136 ENDPOINT_CALL_ONE_THROUGHPUT.add(1, attributes);
137 }
138 EndpointAdverb::Choose => {
139 ENDPOINT_CHOOSE_THROUGHPUT.add(1, attributes);
140 }
141 EndpointAdverb::Stream => {
142 }
144 }
145
146 Self {
147 start,
148 method_name,
149 actor_count,
150 adverb,
151 error_occurred: Cell::new(false),
152 }
153 }
154
155 fn mark_error(&self) {
156 self.error_occurred.set(true);
157 }
158}
159
160impl Drop for RecordEndpointGuard {
161 fn drop(&mut self) {
162 let actor_count_str = self.actor_count.to_string();
163 let attributes = hyperactor_telemetry::kv_pairs!(
164 "method" => self.method_name.clone(),
165 "actor_count" => actor_count_str
166 );
167
168 let duration_us = self.start.elapsed().as_micros();
169
170 match self.adverb {
171 EndpointAdverb::Call => {
172 ENDPOINT_CALL_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
173 }
174 EndpointAdverb::CallOne => {
175 ENDPOINT_CALL_ONE_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
176 }
177 EndpointAdverb::Choose => {
178 ENDPOINT_CHOOSE_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
179 }
180 EndpointAdverb::Stream => {
181 ENDPOINT_STREAM_LATENCY_US_HISTOGRAM.record(duration_us as f64, attributes);
182 }
183 }
184
185 if self.error_occurred.get() {
186 match self.adverb {
187 EndpointAdverb::Call => {
188 ENDPOINT_CALL_ERROR.add(1, attributes);
189 }
190 EndpointAdverb::CallOne => {
191 ENDPOINT_CALL_ONE_ERROR.add(1, attributes);
192 }
193 EndpointAdverb::Choose => {
194 ENDPOINT_CHOOSE_ERROR.add(1, attributes);
195 }
196 EndpointAdverb::Stream => {
197 ENDPOINT_STREAM_ERROR.add(1, attributes);
198 }
199 }
200 }
201 }
202}
203
204pub(crate) struct SpanGuard {
211 id: u64,
212}
213
214impl SpanGuard {
215 fn actor_endpoint(name: &'static str, actor_id: &ActorAddr, mesh: &str, method: &str) -> 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 ),
237 }
238 }
239
240 fn remote(name: &'static str, actor_id: &ActorAddr, call_name: &str) -> Self {
241 Self {
242 id: hyperactor_telemetry::start_user_span(
243 name,
244 hyperactor_telemetry::sinks::perfetto::ENDPOINT_TELEMETRY_TARGET,
245 [
246 (
247 "actor_id",
248 hyperactor_telemetry::trace_dispatcher::FieldValue::Str(
249 actor_id.to_string(),
250 ),
251 ),
252 (
253 "call_name",
254 hyperactor_telemetry::trace_dispatcher::FieldValue::Str(
255 call_name.to_string(),
256 ),
257 ),
258 ],
259 ),
260 }
261 }
262}
263
264impl Drop for SpanGuard {
265 fn drop(&mut self) {
266 hyperactor_telemetry::end_user_span(self.id);
267 }
268}
269
270fn supervision_error_to_pyerr(err: PyErr, qualified_endpoint_name: &Option<String>) -> PyErr {
271 match qualified_endpoint_name {
272 Some(endpoint) => monarch_with_gil_blocking(GilSite::Supervise, |py| {
273 SupervisionError::set_endpoint_on_err(py, err, endpoint.clone())
274 }),
275 None => err,
276 }
277}
278
279async fn collect_value(
280 rx: &mut PortReceiver<PythonMessage>,
281 supervision_monitor: &Option<Arc<dyn Supervisable>>,
282 instance: &Instance<PythonActor>,
283 qualified_endpoint_name: &Option<String>,
284) -> PyResult<(Part, Vec<MeshRef>, Option<usize>)> {
285 enum RaceResult {
286 Message(Box<PythonMessage>),
287 SupervisionError(PyErr),
288 RecvError(String),
289 }
290
291 let race_result = match supervision_monitor {
292 Some(sup) => {
293 tokio::select! {
294 biased;
295 result = sup.supervision_event(instance) => {
296 match result {
297 Some(err) => RaceResult::SupervisionError(err),
298 None => {
299 match rx.recv().await {
300 Ok(msg) => RaceResult::Message(Box::new(msg)),
301 Err(e) => RaceResult::RecvError(e.to_string()),
302 }
303 }
304 }
305 }
306 msg = rx.recv() => {
307 match msg {
308 Ok(m) => RaceResult::Message(Box::new(m)),
309 Err(e) => RaceResult::RecvError(e.to_string()),
310 }
311 }
312 }
313 }
314 _ => match rx.recv().await {
315 Ok(msg) => RaceResult::Message(Box::new(msg)),
316 Err(e) => RaceResult::RecvError(e.to_string()),
317 },
318 };
319
320 match race_result {
321 RaceResult::Message(boxed) => {
322 let PythonMessage {
323 kind,
324 message,
325 refs,
326 } = *boxed;
327 match kind {
328 PythonMessageKind::Result { rank, .. } => Ok((message, refs, rank)),
329 PythonMessageKind::Exception { .. } => {
330 monarch_with_gil_blocking(GilSite::Traceback, |py| {
331 let mesh_references: VecDeque<Option<MeshRef>> =
332 refs.into_iter().map(Some).collect();
333 let mut state =
334 PicklingState::from_parts(message, VecDeque::new(), mesh_references);
335 Err(PyErr::from_value(state.unpickle(py)?.into_bound(py)))
336 })
337 }
338 other => Err(pyo3::exceptions::PyValueError::new_err(format!(
339 "unexpected message kind {:?}",
340 other
341 ))),
342 }
343 }
344 RaceResult::RecvError(e) => Err(pyo3::exceptions::PyEOFError::new_err(format!(
345 "Port closed: {}",
346 e
347 ))),
348 RaceResult::SupervisionError(err) => {
349 Err(supervision_error_to_pyerr(err, qualified_endpoint_name))
350 }
351 }
352}
353
354#[tracing::instrument(level = "debug", skip_all)]
355async fn collect_valuemesh(
356 extent: Extent,
357 rx: OncePortReceiver<PythonMessage>,
358 method_name: String,
359 supervision_monitor: Option<Arc<dyn Supervisable>>,
360 instance: &Instance<PythonActor>,
361 qualified_endpoint_name: Option<String>,
362) -> PyResult<Py<PyAny>> {
363 let start = tokio::time::Instant::now();
364
365 let expected_count = extent.num_ranks();
366
367 let record_guard = RecordEndpointGuard::new(
368 start,
369 method_name.clone(),
370 expected_count,
371 EndpointAdverb::Call,
372 );
373
374 enum RaceResult {
375 Collected(Box<PythonMessage>),
376 SupervisionError(PyErr),
377 RecvError(String),
378 }
379
380 let race_result = match &supervision_monitor {
381 Some(sup) => {
382 tokio::select! {
383 biased;
384 result = sup.supervision_event(instance) => {
385 match result {
386 Some(err) => RaceResult::SupervisionError(err),
387 None => RaceResult::RecvError(
388 "supervision monitor closed unexpectedly".to_string()
389 ),
390 }
391 }
392 batch = rx.recv() => {
393 match batch {
394 Ok(b) => RaceResult::Collected(Box::new(b)),
395 Err(e) => RaceResult::RecvError(e.to_string()),
396 }
397 }
398 }
399 }
400 None => match rx.recv().await {
401 Ok(batch) => RaceResult::Collected(Box::new(batch)),
402 Err(e) => RaceResult::RecvError(e.to_string()),
403 },
404 };
405
406 match race_result {
407 RaceResult::Collected(boxed) => {
408 let msg = *boxed;
409 let overlay = msg.into_overlay().map_err(|e| {
410 pyo3::exceptions::PyRuntimeError::new_err(format!(
411 "failed to extract overlay from collected responses: {e}"
412 ))
413 })?;
414 monarch_with_gil_blocking(GilSite::ReplyConvert, |py| {
415 let has_refs = overlay.runs().any(|(_, payload)| {
432 let (PythonResponseMessage::Result { refs, .. }
433 | PythonResponseMessage::Exception { refs, .. }) = payload;
434 !refs.is_empty()
435 });
436
437 if !has_refs {
438 let mut parts = Vec::with_capacity(expected_count);
439 for (range, payload) in overlay.runs() {
440 match payload {
441 PythonResponseMessage::Result { part, .. } => {
442 parts.extend(range.clone().map(|_| part.clone()));
443 }
444 PythonResponseMessage::Exception { .. } => {
445 record_guard.mark_error();
446 return Err(PyErr::from_value(payload.decode(py)?.into_bound(py)));
447 }
448 }
449 }
450 return Ok(PyValueMesh::build_from_parts(&extent, parts)?
451 .into_pyobject(py)?
452 .into_any()
453 .unbind());
454 }
455
456 let mut objects: Vec<Py<PyAny>> = Vec::with_capacity(expected_count);
457 for (range, payload) in overlay.runs() {
458 match payload {
459 PythonResponseMessage::Result { .. } => {
460 let obj = payload.decode(py)?;
461 objects.extend(range.clone().map(|_| obj.clone_ref(py)));
462 }
463 PythonResponseMessage::Exception { .. } => {
464 record_guard.mark_error();
465 return Err(PyErr::from_value(payload.decode(py)?.into_bound(py)));
466 }
467 }
468 }
469 Ok(PyValueMesh::build_from_objects(&extent, objects)?
470 .into_pyobject(py)?
471 .into_any()
472 .unbind())
473 })
474 }
475 RaceResult::RecvError(e) => {
476 record_guard.mark_error();
477 Err(pyo3::exceptions::PyEOFError::new_err(format!(
478 "Port closed: {}",
479 e
480 )))
481 }
482 RaceResult::SupervisionError(err) => {
483 record_guard.mark_error();
484 Err(supervision_error_to_pyerr(err, &qualified_endpoint_name))
485 }
486 }
487}
488
489fn value_collector(
490 mut receiver: PortReceiver<PythonMessage>,
491 method_name: String,
492 supervision_monitor: Option<Arc<dyn Supervisable>>,
493 instance: Instance<PythonActor>,
494 qualified_endpoint_name: Option<String>,
495 adverb: EndpointAdverb,
496 span_guard: SpanGuard,
497) -> PyResult<PyPythonTask> {
498 Ok(PythonTask::new(async move {
499 let _span_guard = span_guard;
500 let start = tokio::time::Instant::now();
501
502 let record_guard = RecordEndpointGuard::new(start, method_name, 1, adverb);
503
504 match collect_value(
505 &mut receiver,
506 &supervision_monitor,
507 &instance,
508 &qualified_endpoint_name,
509 )
510 .await
511 {
512 Ok((message, refs, _)) => monarch_with_gil_blocking(GilSite::ReplyConvert, |py| {
513 let mesh_references: VecDeque<Option<MeshRef>> =
514 refs.into_iter().map(Some).collect();
515 let mut state =
516 PicklingState::from_parts(message, VecDeque::new(), mesh_references);
517 state.unpickle(py)
518 }),
519 Err(e) => {
520 record_guard.mark_error();
521 Err(e)
522 }
523 }
524 })?
525 .into())
526}
527
528#[pyclass(
533 name = "ValueStream",
534 module = "monarch._rust_bindings.monarch_hyperactor.endpoint"
535)]
536pub struct PyValueStream {
537 receiver: Arc<tokio::sync::Mutex<PortReceiver<PythonMessage>>>,
538 supervision_monitor: Option<Arc<dyn Supervisable>>,
540 instance: Instance<PythonActor>,
541 remaining: AtomicUsize,
542 method_name: String,
543 qualified_endpoint_name: Option<String>,
544 start: tokio::time::Instant,
545 actor_count: usize,
546 future_class: Py<PyAny>,
547}
548
549#[pymethods]
550impl PyValueStream {
551 fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
552 slf
553 }
554
555 fn __next__(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
556 let remaining = self.remaining.load(Ordering::Relaxed);
557 if remaining == 0 {
558 return Ok(None);
559 }
560 self.remaining.store(remaining - 1, Ordering::Relaxed);
561
562 let receiver = self.receiver.clone();
563 let supervision_monitor = self.supervision_monitor.clone();
564 let instance = self.instance.clone_for_py();
565 let qualified_endpoint_name = self.qualified_endpoint_name.clone();
566 let start = self.start;
567 let method_name = self.method_name.clone();
568 let actor_count = self.actor_count;
569
570 let task: PyPythonTask = PythonTask::new(async move {
571 let record_guard =
572 RecordEndpointGuard::new(start, method_name, actor_count, EndpointAdverb::Stream);
573
574 let mut rx_guard = receiver.lock().await;
575
576 match collect_value(
577 &mut rx_guard,
578 &supervision_monitor,
579 &instance,
580 &qualified_endpoint_name,
581 )
582 .await
583 {
584 Ok((message, refs, _)) => monarch_with_gil_blocking(GilSite::ReplyConvert, |py| {
585 let mesh_references: VecDeque<Option<MeshRef>> =
586 refs.into_iter().map(Some).collect();
587 let mut state =
588 PicklingState::from_parts(message, VecDeque::new(), mesh_references);
589 state.unpickle(py)
590 }),
591 Err(e) => {
592 record_guard.mark_error();
593 Err(e)
594 }
595 }
596 })?
597 .into();
598
599 let kwargs = PyDict::new(py);
600 kwargs.set_item("coro", task)?;
601 let future = self.future_class.call(py, (), Some(&kwargs))?;
602 Ok(Some(future))
603 }
604}
605
606fn wrap_in_future(py: Python<'_>, task: PyPythonTask) -> PyResult<Py<PyAny>> {
607 let kwargs = PyDict::new(py);
608 kwargs.set_item("coro", task)?;
609 let future = make_future(py).call((), Some(&kwargs))?;
610 Ok(future.unbind())
611}
612
613pub(crate) trait Endpoint {
616 fn get_extent(&self, py: Python<'_>) -> PyResult<Extent>;
618
619 fn get_method_name(&self) -> &str;
621
622 fn send_message<'py>(
624 &self,
625 py: Python<'py>,
626 args: &Bound<'py, PyTuple>,
627 kwargs: Option<&Bound<'py, PyDict>>,
628 port_ref: Option<EitherPortRef>,
629 selection: AllOrChoose,
630 instance: &Instance<PythonActor>,
631 ) -> PyResult<()>;
632
633 fn send_message_with_headers<'py>(
638 &self,
639 py: Python<'py>,
640 args: &Bound<'py, PyTuple>,
641 kwargs: Option<&Bound<'py, PyDict>>,
642 port_ref: Option<EitherPortRef>,
643 selection: AllOrChoose,
644 instance: &Instance<PythonActor>,
645 _caller_headers: hyperactor_config::Flattrs,
646 ) -> PyResult<()> {
647 self.send_message(py, args, kwargs, port_ref, selection, instance)
648 }
649
650 fn build_operation_context_headers(
656 &self,
657 adverb: EndpointAdverb,
658 ) -> hyperactor_config::Flattrs {
659 let adverb_str = match adverb {
660 EndpointAdverb::Call => "call",
661 EndpointAdverb::CallOne => "call_one",
662 EndpointAdverb::Choose => "choose",
663 EndpointAdverb::Stream => "stream",
664 };
665 let attrs = crate::operation_context::build_operation_context_attrs(
666 self.get_qualified_name(),
667 Some(adverb_str),
668 );
669 let mut headers = hyperactor_config::Flattrs::new();
670 crate::operation_context::stamp_operation_context(&mut headers, &attrs);
671 headers
672 }
673
674 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>>;
676
677 fn get_qualified_name(&self) -> Option<String>;
679
680 fn enter_endpoint_span(&self, adverb: EndpointAdverb, actor_id: &ActorAddr) -> SpanGuard;
687
688 fn get_current_instance(&self, py: Python<'_>) -> PyResult<Instance<PythonActor>> {
689 let context = get_context(py).call0()?;
690 let py_instance: PyRef<PyInstance> = context.getattr("actor_instance")?.extract()?;
691 Ok(py_instance.clone().into_instance())
692 }
693
694 fn open_response_port(
695 &self,
696 instance: &Instance<PythonActor>,
697 ) -> (PythonPortRef, PortReceiver<PythonMessage>) {
698 let (p, receiver) = instance.mailbox_for_py().open_port::<PythonMessage>();
699 (PythonPortRef { inner: p.bind() }, receiver)
700 }
701
702 fn open_reduce_response_port(
703 &self,
704 instance: &Instance<PythonActor>,
705 ) -> (PythonOncePortRef, OncePortReceiver<PythonMessage>) {
706 let (p, receiver) = instance
707 .mailbox_for_py()
708 .open_reduce_port(PythonResponseMessageAccumulator);
709 (PythonOncePortRef::from(p.bind()), receiver)
710 }
711
712 #[tracing::instrument(level = "debug", skip_all)]
714 fn call<'py>(
715 &self,
716 py: Python<'py>,
717 args: &Bound<'py, PyTuple>,
718 kwargs: Option<&Bound<'py, PyDict>>,
719 ) -> PyResult<Py<PyAny>> {
720 let instance = self.get_current_instance(py)?;
721 let span_guard = self.enter_endpoint_span(EndpointAdverb::Call, instance.self_addr());
722
723 let extent = self.get_extent(py)?;
724 let method_name = self.get_method_name().to_string();
725 let (port_ref, receiver) = self.open_reduce_response_port(&instance);
726
727 let supervision_monitor = self.get_supervision_monitor();
728 let qualified_endpoint_name = self.get_qualified_name();
729
730 let caller_headers = self.build_operation_context_headers(EndpointAdverb::Call);
731 self.send_message_with_headers(
732 py,
733 args,
734 kwargs,
735 Some(EitherPortRef::Once(port_ref)),
736 AllOrChoose::All,
737 &instance,
738 caller_headers,
739 )?;
740
741 let instance_for_task = instance.clone_for_py();
742 let task: PyPythonTask = PythonTask::new(async move {
743 let _span_guard = span_guard;
744 collect_valuemesh(
745 extent,
746 receiver,
747 method_name,
748 supervision_monitor,
749 &instance_for_task,
750 qualified_endpoint_name,
751 )
752 .await
753 })?
754 .into();
755
756 wrap_in_future(py, task)
757 }
758
759 fn choose<'py>(
761 &self,
762 py: Python<'py>,
763 args: &Bound<'py, PyTuple>,
764 kwargs: Option<&Bound<'py, PyDict>>,
765 ) -> PyResult<Py<PyAny>> {
766 let instance = self.get_current_instance(py)?;
767 let span_guard = self.enter_endpoint_span(EndpointAdverb::Choose, instance.self_addr());
768 let (port_ref, receiver) = self.open_response_port(&instance);
769
770 let caller_headers = self.build_operation_context_headers(EndpointAdverb::Choose);
771 self.send_message_with_headers(
772 py,
773 args,
774 kwargs,
775 Some(EitherPortRef::Unbounded(port_ref)),
776 AllOrChoose::Choose,
777 &instance,
778 caller_headers,
779 )?;
780
781 let task = value_collector(
782 receiver,
783 self.get_method_name().to_string(),
784 self.get_supervision_monitor(),
785 instance.clone_for_py(),
786 self.get_qualified_name(),
787 EndpointAdverb::Choose,
788 span_guard,
789 )?;
790
791 wrap_in_future(py, task)
792 }
793
794 fn call_one<'py>(
796 &self,
797 py: Python<'py>,
798 args: &Bound<'py, PyTuple>,
799 kwargs: Option<&Bound<'py, PyDict>>,
800 ) -> PyResult<Py<PyAny>> {
801 let extent = self.get_extent(py)?;
802
803 if extent.num_ranks() != 1 {
804 return Err(pyo3::exceptions::PyValueError::new_err(format!(
805 "call_one requires exactly 1 actor, but mesh has {}",
806 extent.num_ranks()
807 )));
808 }
809
810 let instance = self.get_current_instance(py)?;
811 let span_guard = self.enter_endpoint_span(EndpointAdverb::CallOne, instance.self_addr());
812 let (port_ref, receiver) = self.open_response_port(&instance);
813
814 let caller_headers = self.build_operation_context_headers(EndpointAdverb::CallOne);
815 self.send_message_with_headers(
816 py,
817 args,
818 kwargs,
819 Some(EitherPortRef::Unbounded(port_ref)),
820 AllOrChoose::All,
821 &instance,
822 caller_headers,
823 )?;
824
825 let task = value_collector(
826 receiver,
827 self.get_method_name().to_string(),
828 self.get_supervision_monitor(),
829 instance.clone_for_py(),
830 self.get_qualified_name(),
831 EndpointAdverb::CallOne,
832 span_guard,
833 )?;
834
835 wrap_in_future(py, task)
836 }
837
838 fn stream<'py>(
840 &self,
841 py: Python<'py>,
842 args: &Bound<'py, PyTuple>,
843 kwargs: Option<&Bound<'py, PyDict>>,
844 ) -> PyResult<Py<PyAny>> {
845 let extent = self.get_extent(py)?;
846 let method_name = self.get_method_name().to_string();
847
848 let instance = self.get_current_instance(py)?;
849 let (port_ref, receiver) = self.open_response_port(&instance);
850
851 let caller_headers = self.build_operation_context_headers(EndpointAdverb::Stream);
852 self.send_message_with_headers(
853 py,
854 args,
855 kwargs,
856 Some(EitherPortRef::Unbounded(port_ref)),
857 AllOrChoose::All,
858 &instance,
859 caller_headers,
860 )?;
861
862 let actor_count = extent.num_ranks();
863 let start = tokio::time::Instant::now();
864 let supervision_monitor = self.get_supervision_monitor();
865 let qualified_endpoint_name = self.get_qualified_name();
866 let future_class = make_future(py).unbind();
867
868 let attributes = hyperactor_telemetry::kv_pairs!(
869 "method" => method_name.clone()
870 );
871 ENDPOINT_STREAM_THROUGHPUT.add(1, attributes);
872
873 let stream = PyValueStream {
874 receiver: Arc::new(tokio::sync::Mutex::new(receiver)),
875 supervision_monitor,
876 instance: instance.clone_for_py(),
877 remaining: AtomicUsize::new(actor_count),
878 method_name,
879 qualified_endpoint_name,
880 start,
881 actor_count,
882 future_class,
883 };
884
885 Ok(stream.into_pyobject(py)?.unbind().into())
886 }
887
888 fn broadcast<'py>(
890 &self,
891 py: Python<'py>,
892 args: &Bound<'py, PyTuple>,
893 kwargs: Option<&Bound<'py, PyDict>>,
894 ) -> PyResult<()> {
895 let instance = self.get_current_instance(py)?;
896 let method_name = self.get_method_name();
897 let attributes = hyperactor_telemetry::kv_pairs!(
898 "method" => method_name.to_string()
899 );
900
901 match self.send_message(py, args, kwargs, None, AllOrChoose::All, &instance) {
902 Ok(()) => {
903 ENDPOINT_BROADCAST_THROUGHPUT.add(1, attributes);
904 Ok(())
905 }
906 Err(e) => {
907 ENDPOINT_BROADCAST_ERROR.add(1, attributes);
908 Err(e)
909 }
910 }
911 }
912}
913
914#[pyclass(
915 name = "ActorEndpoint",
916 module = "monarch._rust_bindings.monarch_hyperactor.endpoint"
917)]
918pub struct ActorEndpoint {
919 inner: Arc<dyn SupervisableActorMesh>,
920 shape: Shape,
921 method: MethodSpecifier,
922 mesh_name: String,
923 signature: Option<Py<PyAny>>,
924 proc_mesh: Option<Py<PyAny>>,
925 propagator: Option<Py<PyAny>>,
926}
927
928impl ActorEndpoint {
929 fn create_message<'py>(
930 &self,
931 py: Python<'py>,
932 args: &Bound<'py, PyTuple>,
933 kwargs: Option<&Bound<'py, PyDict>>,
934 port_ref: Option<EitherPortRef>,
935 ) -> PyResult<PendingMessage> {
936 let port_ref_py: Py<PyAny> = match port_ref {
937 Some(pr) => pr.clone().into_pyobject(py)?.unbind(),
938 None => py.None(),
939 };
940
941 let result = create_endpoint_message(py).call1((
942 self.method.clone(),
943 self.signature
944 .as_ref()
945 .map_or_else(|| py.None(), |s| s.clone_ref(py)),
946 args,
947 kwargs
948 .map_or_else(|| PyDict::new(py), |d| d.clone())
949 .into_any(),
950 port_ref_py,
951 self.proc_mesh
952 .as_ref()
953 .map_or_else(|| py.None(), |p| p.clone_ref(py)),
954 ))?;
955 let mut pending: PyRefMut<'_, PendingMessage> = result.extract()?;
956 pending.take()
957 }
958}
959
960impl Endpoint for ActorEndpoint {
961 fn get_extent(&self, _py: Python<'_>) -> PyResult<Extent> {
962 Ok(self.shape.extent())
963 }
964
965 fn get_method_name(&self) -> &str {
966 self.method.name()
967 }
968
969 fn send_message<'py>(
970 &self,
971 py: Python<'py>,
972 args: &Bound<'py, PyTuple>,
973 kwargs: Option<&Bound<'py, PyDict>>,
974 port_ref: Option<EitherPortRef>,
975 selection: AllOrChoose,
976 instance: &Instance<PythonActor>,
977 ) -> PyResult<()> {
978 let message = self.create_message(py, args, kwargs, port_ref)?;
979 self.inner.cast_unresolved(message, selection, instance)
980 }
981
982 fn send_message_with_headers<'py>(
983 &self,
984 py: Python<'py>,
985 args: &Bound<'py, PyTuple>,
986 kwargs: Option<&Bound<'py, PyDict>>,
987 port_ref: Option<EitherPortRef>,
988 selection: AllOrChoose,
989 instance: &Instance<PythonActor>,
990 caller_headers: hyperactor_config::Flattrs,
991 ) -> PyResult<()> {
992 let message = self.create_message(py, args, kwargs, port_ref)?;
993 self.inner
994 .cast_unresolved_with_headers(message, selection, instance, caller_headers)
995 }
996
997 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>> {
998 Some(self.inner.clone())
999 }
1000
1001 fn get_qualified_name(&self) -> Option<String> {
1002 Some(format!("{}.{}()", self.mesh_name, self.method.name()))
1003 }
1004
1005 fn enter_endpoint_span(&self, adverb: EndpointAdverb, actor_id: &ActorAddr) -> SpanGuard {
1006 let mesh = self.mesh_name.as_str();
1007 let method = self.method.name();
1008 SpanGuard::actor_endpoint(adverb.as_str(), actor_id, mesh, method)
1009 }
1010}
1011
1012#[pymethods]
1013impl ActorEndpoint {
1014 #[new]
1016 #[pyo3(signature = (actor_mesh, method, shape, mesh_name, signature=None, proc_mesh=None, propagator=None))]
1017 fn new(
1018 actor_mesh: PythonActorMesh,
1019 method: MethodSpecifier,
1020 shape: PyShape,
1021 mesh_name: String,
1022 signature: Option<Py<PyAny>>,
1023 proc_mesh: Option<Py<PyAny>>,
1024 propagator: Option<Py<PyAny>>,
1025 ) -> Self {
1026 Self {
1027 inner: actor_mesh.get_inner(),
1028 shape: shape.get_inner().clone(),
1029 method,
1030 mesh_name,
1031 signature,
1032 proc_mesh,
1033 propagator,
1034 }
1035 }
1036
1037 #[getter]
1039 fn _name(&self) -> MethodSpecifier {
1040 self.method.clone()
1041 }
1042
1043 #[getter]
1045 fn _signature(&self, py: Python<'_>) -> Py<PyAny> {
1046 self.signature
1047 .clone()
1048 .unwrap_or_else(|| py.None().into_any())
1049 }
1050
1051 #[getter]
1053 fn _actor_mesh(&self) -> PythonActorMesh {
1054 PythonActorMesh::from_impl(self.inner.clone())
1055 }
1056
1057 fn _propagate<'py>(
1060 &self,
1061 py: Python<'py>,
1062 args: &Bound<'py, PyAny>,
1063 kwargs: &Bound<'py, PyAny>,
1064 fake_args: &Bound<'py, PyAny>,
1065 fake_kwargs: &Bound<'py, PyAny>,
1066 ) -> PyResult<Py<PyAny>> {
1067 let do_propagate = py
1068 .import("monarch._src.actor.endpoint")?
1069 .getattr("_do_propagate")?;
1070 let propagator = self
1071 .propagator
1072 .as_ref()
1073 .map(|p| p.clone_ref(py).into_bound(py))
1074 .unwrap_or_else(|| py.None().into_bound(py));
1075 let cache = PyDict::new(py);
1076 do_propagate
1077 .call1((&propagator, args, kwargs, fake_args, fake_kwargs, cache))?
1078 .extract()
1079 }
1080
1081 fn _fetch_propagate<'py>(
1084 &self,
1085 py: Python<'py>,
1086 args: &Bound<'py, PyAny>,
1087 kwargs: &Bound<'py, PyAny>,
1088 fake_args: &Bound<'py, PyAny>,
1089 fake_kwargs: &Bound<'py, PyAny>,
1090 ) -> PyResult<Py<PyAny>> {
1091 if self.propagator.is_none() {
1092 return Ok(py.None());
1093 }
1094 self._propagate(py, args, kwargs, fake_args, fake_kwargs)
1095 }
1096
1097 fn _pipe_propagate<'py>(
1100 &self,
1101 py: Python<'py>,
1102 args: &Bound<'py, PyAny>,
1103 kwargs: &Bound<'py, PyAny>,
1104 fake_args: &Bound<'py, PyAny>,
1105 fake_kwargs: &Bound<'py, PyAny>,
1106 ) -> PyResult<Py<PyAny>> {
1107 let is_callable = self
1109 .propagator
1110 .as_ref()
1111 .map(|p| p.bind(py).is_callable())
1112 .unwrap_or(false);
1113 if !is_callable {
1114 return Err(pyo3::exceptions::PyValueError::new_err(
1115 "Must specify explicit callable for pipe",
1116 ));
1117 }
1118 self._propagate(py, args, kwargs, fake_args, fake_kwargs)
1119 }
1120
1121 #[pyo3(signature = (*args, **kwargs))]
1123 fn rref<'py>(
1124 slf: PyRef<'py, Self>,
1125 py: Python<'py>,
1126 args: &Bound<'py, PyTuple>,
1127 kwargs: Option<&Bound<'py, PyDict>>,
1128 ) -> PyResult<Py<PyAny>> {
1129 let kwargs_dict = kwargs.map_or_else(|| PyDict::new(py), |d| d.clone());
1130
1131 let result = dispatch_actor_rref(py).call1((slf.into_pyobject(py)?, args, kwargs_dict))?;
1133
1134 Ok(result.unbind())
1135 }
1136
1137 #[pyo3(signature = (*args, **kwargs), name = "call")]
1139 fn py_call<'py>(
1140 &self,
1141 py: Python<'py>,
1142 args: &Bound<'py, PyTuple>,
1143 kwargs: Option<&Bound<'py, PyDict>>,
1144 ) -> PyResult<Py<PyAny>> {
1145 self.call(py, args, kwargs)
1146 }
1147
1148 #[pyo3(signature = (*args, **kwargs), name = "choose")]
1150 fn py_choose<'py>(
1151 &self,
1152 py: Python<'py>,
1153 args: &Bound<'py, PyTuple>,
1154 kwargs: Option<&Bound<'py, PyDict>>,
1155 ) -> PyResult<Py<PyAny>> {
1156 self.choose(py, args, kwargs)
1157 }
1158
1159 #[pyo3(signature = (*args, **kwargs), name = "call_one")]
1161 fn py_call_one<'py>(
1162 &self,
1163 py: Python<'py>,
1164 args: &Bound<'py, PyTuple>,
1165 kwargs: Option<&Bound<'py, PyDict>>,
1166 ) -> PyResult<Py<PyAny>> {
1167 self.call_one(py, args, kwargs)
1168 }
1169
1170 #[pyo3(signature = (*args, **kwargs), name = "stream")]
1172 fn py_stream<'py>(
1173 &self,
1174 py: Python<'py>,
1175 args: &Bound<'py, PyTuple>,
1176 kwargs: Option<&Bound<'py, PyDict>>,
1177 ) -> PyResult<Py<PyAny>> {
1178 self.stream(py, args, kwargs)
1179 }
1180
1181 #[pyo3(signature = (*args, **kwargs), name = "broadcast")]
1183 fn py_broadcast<'py>(
1184 &self,
1185 py: Python<'py>,
1186 args: &Bound<'py, PyTuple>,
1187 kwargs: Option<&Bound<'py, PyDict>>,
1188 ) -> PyResult<()> {
1189 self.broadcast(py, args, kwargs)
1190 }
1191
1192 fn _send<'py>(
1194 &self,
1195 py: Python<'py>,
1196 args: &Bound<'py, PyTuple>,
1197 kwargs: &Bound<'py, PyDict>,
1198 port: Option<EitherPortRef>,
1199 selection: &str,
1200 ) -> PyResult<()> {
1201 let instance = self.get_current_instance(py)?;
1202 let sel = to_all_or_choose(selection)?;
1203 self.send_message(py, args, Some(kwargs), port, sel, &instance)
1204 }
1205}
1206
1207#[pyclass(
1212 name = "Remote",
1213 module = "monarch._rust_bindings.monarch_hyperactor.endpoint"
1214)]
1215pub struct Remote {
1216 inner: Py<PyAny>,
1218}
1219
1220impl Endpoint for Remote {
1221 fn get_extent(&self, py: Python<'_>) -> PyResult<Extent> {
1222 let extent: PyExtent = self.inner.call_method0(py, "_get_extent")?.extract(py)?;
1223 Ok(extent.into())
1224 }
1225
1226 fn get_method_name(&self) -> &str {
1227 "unknown"
1228 }
1229
1230 fn send_message<'py>(
1231 &self,
1232 py: Python<'py>,
1233 args: &Bound<'py, PyTuple>,
1234 kwargs: Option<&Bound<'py, PyDict>>,
1235 port_ref: Option<EitherPortRef>,
1236 selection: AllOrChoose,
1237 _instance: &Instance<PythonActor>,
1238 ) -> PyResult<()> {
1239 let send_kwargs = PyDict::new(py);
1240 match port_ref {
1241 Some(pr) => send_kwargs.set_item("port", pr.clone())?,
1242 None => send_kwargs.set_item("port", py.None())?,
1243 }
1244
1245 send_kwargs.set_item("selection", selection.as_str())?;
1246
1247 let kwargs_dict = kwargs.map_or_else(|| PyDict::new(py), |d| d.clone());
1248 self.inner
1249 .call_method(py, "_send", (args.clone(), kwargs_dict), Some(&send_kwargs))?;
1250
1251 Ok(())
1252 }
1253
1254 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>> {
1255 None }
1257
1258 fn get_qualified_name(&self) -> Option<String> {
1259 None }
1261
1262 fn enter_endpoint_span(&self, adverb: EndpointAdverb, actor_id: &ActorAddr) -> SpanGuard {
1263 let call_name = monarch_with_gil_blocking(GilSite::DisplayName, |py| {
1264 self.inner
1265 .call_method0(py, "_call_name")
1266 .ok()
1267 .and_then(|v| v.extract::<String>(py).ok())
1268 });
1269 let call_name = call_name.as_deref().unwrap_or("");
1270 SpanGuard::remote(adverb.as_str(), actor_id, call_name)
1271 }
1272}
1273
1274#[pymethods]
1275impl Remote {
1276 #[new]
1278 fn new(remote: Py<PyAny>) -> Self {
1279 Self { inner: remote }
1280 }
1281
1282 #[pyo3(signature = (*args, **kwargs), name = "call")]
1284 fn py_call<'py>(
1285 &self,
1286 py: Python<'py>,
1287 args: &Bound<'py, PyTuple>,
1288 kwargs: Option<&Bound<'py, PyDict>>,
1289 ) -> PyResult<Py<PyAny>> {
1290 self.call(py, args, kwargs)
1291 }
1292
1293 #[pyo3(signature = (*args, **kwargs), name = "choose")]
1295 fn py_choose<'py>(
1296 &self,
1297 py: Python<'py>,
1298 args: &Bound<'py, PyTuple>,
1299 kwargs: Option<&Bound<'py, PyDict>>,
1300 ) -> PyResult<Py<PyAny>> {
1301 self.choose(py, args, kwargs)
1302 }
1303
1304 #[pyo3(signature = (*args, **kwargs), name = "call_one")]
1306 fn py_call_one<'py>(
1307 &self,
1308 py: Python<'py>,
1309 args: &Bound<'py, PyTuple>,
1310 kwargs: Option<&Bound<'py, PyDict>>,
1311 ) -> PyResult<Py<PyAny>> {
1312 self.call_one(py, args, kwargs)
1313 }
1314
1315 #[pyo3(signature = (*args, **kwargs), name = "stream")]
1317 fn py_stream<'py>(
1318 &self,
1319 py: Python<'py>,
1320 args: &Bound<'py, PyTuple>,
1321 kwargs: Option<&Bound<'py, PyDict>>,
1322 ) -> PyResult<Py<PyAny>> {
1323 self.stream(py, args, kwargs)
1324 }
1325
1326 #[pyo3(signature = (*args, **kwargs), name = "broadcast")]
1328 fn py_broadcast<'py>(
1329 &self,
1330 py: Python<'py>,
1331 args: &Bound<'py, PyTuple>,
1332 kwargs: Option<&Bound<'py, PyDict>>,
1333 ) -> PyResult<()> {
1334 self.broadcast(py, args, kwargs)
1335 }
1336
1337 #[pyo3(signature = (*args, **kwargs))]
1339 fn rref<'py>(
1340 &self,
1341 py: Python<'py>,
1342 args: &Bound<'py, PyTuple>,
1343 kwargs: Option<&Bound<'py, PyDict>>,
1344 ) -> PyResult<Py<PyAny>> {
1345 let kwargs_dict = kwargs.map_or_else(|| PyDict::new(py), |d| d.clone());
1346 self.inner.call_method(py, "rref", args, Some(&kwargs_dict))
1347 }
1348
1349 fn _call_name(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1351 self.inner.call_method0(py, "_call_name")
1352 }
1353
1354 #[getter]
1356 fn _maybe_resolvable(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1357 self.inner.getattr(py, "_maybe_resolvable")
1358 }
1359
1360 #[getter]
1362 fn _resolvable(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1363 self.inner.getattr(py, "_resolvable")
1364 }
1365
1366 #[getter]
1369 fn _remote_impl(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1370 self.inner.getattr(py, "_remote_impl")
1371 }
1372
1373 fn _propagate<'py>(
1376 &self,
1377 py: Python<'py>,
1378 args: &Bound<'py, PyAny>,
1379 kwargs: &Bound<'py, PyAny>,
1380 fake_args: &Bound<'py, PyAny>,
1381 fake_kwargs: &Bound<'py, PyAny>,
1382 ) -> PyResult<Py<PyAny>> {
1383 self.inner
1384 .call_method1(py, "_propagate", (args, kwargs, fake_args, fake_kwargs))
1385 }
1386
1387 fn _fetch_propagate<'py>(
1390 &self,
1391 py: Python<'py>,
1392 args: &Bound<'py, PyAny>,
1393 kwargs: &Bound<'py, PyAny>,
1394 fake_args: &Bound<'py, PyAny>,
1395 fake_kwargs: &Bound<'py, PyAny>,
1396 ) -> PyResult<Py<PyAny>> {
1397 self.inner.call_method1(
1398 py,
1399 "_fetch_propagate",
1400 (args, kwargs, fake_args, fake_kwargs),
1401 )
1402 }
1403
1404 fn _pipe_propagate<'py>(
1407 &self,
1408 py: Python<'py>,
1409 args: &Bound<'py, PyAny>,
1410 kwargs: &Bound<'py, PyAny>,
1411 fake_args: &Bound<'py, PyAny>,
1412 fake_kwargs: &Bound<'py, PyAny>,
1413 ) -> PyResult<Py<PyAny>> {
1414 self.inner.call_method1(
1415 py,
1416 "_pipe_propagate",
1417 (args, kwargs, fake_args, fake_kwargs),
1418 )
1419 }
1420
1421 fn _send<'py>(
1424 &self,
1425 py: Python<'py>,
1426 args: &Bound<'py, PyTuple>,
1427 kwargs: &Bound<'py, PyDict>,
1428 port: Option<Py<PyAny>>,
1429 selection: &str,
1430 ) -> PyResult<()> {
1431 self.inner.call_method(
1432 py,
1433 "_send",
1434 (args, kwargs),
1435 Some(&{
1436 let d = PyDict::new(py);
1437 d.set_item("port", port.unwrap_or_else(|| py.None()))?;
1438 d.set_item("selection", selection)?;
1439 d
1440 }),
1441 )?;
1442 Ok(())
1443 }
1444
1445 #[pyo3(signature = (*args, **kwargs))]
1447 fn __call__<'py>(
1448 &self,
1449 py: Python<'py>,
1450 args: &Bound<'py, PyTuple>,
1451 kwargs: Option<&Bound<'py, PyDict>>,
1452 ) -> PyResult<Py<PyAny>> {
1453 self.rref(py, args, kwargs)
1454 }
1455}
1456
1457pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
1458 module.add_class::<PyValueStream>()?;
1459 module.add_class::<ActorEndpoint>()?;
1460 module.add_class::<Remote>()?;
1461
1462 Ok(())
1463}
1464
1465#[derive(Named)]
1466struct PythonResponseMessageReducer;
1467
1468impl CommReducer for PythonResponseMessageReducer {
1469 type Update = PythonMessage;
1470
1471 fn reduce(&self, left: Self::Update, right: Self::Update) -> anyhow::Result<Self::Update> {
1472 Ok(ValueOverlay::try_from_runs(rle::merge_value_runs(
1473 left.into_overlay()?.into_runs(),
1474 right.into_overlay()?.into_runs(),
1475 ))?
1476 .into())
1477 }
1478}
1479
1480inventory::submit! {
1481 ReducerFactory {
1482 typehash_f: <PythonResponseMessageReducer as Named>::typehash,
1483 builder_f: |_| Ok(Box::new(PythonResponseMessageReducer)),
1484 }
1485}
1486
1487struct PythonResponseMessageAccumulator;
1488
1489impl Accumulator for PythonResponseMessageAccumulator {
1490 type State = PythonMessage;
1491 type Update = PythonMessage;
1492
1493 fn accumulate(&self, state: &mut Self::State, update: Self::Update) -> anyhow::Result<()> {
1494 *state = ValueOverlay::try_from_runs(rle::merge_value_runs(
1495 std::mem::take(state).into_overlay()?.into_runs(),
1496 update.into_overlay()?.into_runs(),
1497 ))?
1498 .into();
1499
1500 Ok(())
1501 }
1502
1503 fn reducer_spec(&self) -> Option<ReducerSpec> {
1504 Some(ReducerSpec {
1505 typehash: <PythonResponseMessageReducer as Named>::typehash(),
1506 builder_params: None,
1507 })
1508 }
1509}
1510
1511#[cfg(test)]
1512mod tests {
1513 use hyperactor::ActorAddr;
1514 use hyperactor::mailbox::headers::OPERATION_ADVERB;
1515 use hyperactor::mailbox::headers::OPERATION_ENDPOINT;
1516
1517 use super::*;
1518
1519 struct TestEndpoint {
1523 qualified_name: Option<String>,
1524 }
1525
1526 impl Endpoint for TestEndpoint {
1527 fn get_extent(&self, _py: Python<'_>) -> PyResult<Extent> {
1528 unreachable!()
1529 }
1530 fn get_method_name(&self) -> &str {
1531 unreachable!()
1532 }
1533 fn send_message<'py>(
1534 &self,
1535 _py: Python<'py>,
1536 _args: &Bound<'py, PyTuple>,
1537 _kwargs: Option<&Bound<'py, PyDict>>,
1538 _port_ref: Option<EitherPortRef>,
1539 _selection: AllOrChoose,
1540 _instance: &Instance<PythonActor>,
1541 ) -> PyResult<()> {
1542 unreachable!()
1543 }
1544 fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>> {
1545 None
1546 }
1547 fn get_qualified_name(&self) -> Option<String> {
1548 self.qualified_name.clone()
1549 }
1550 fn enter_endpoint_span(&self, _adverb: EndpointAdverb, _actor_id: &ActorAddr) -> SpanGuard {
1551 unreachable!()
1552 }
1553 }
1554
1555 #[test]
1559 fn test_rc1_build_operation_context_headers_stamps_each_adverb() {
1560 let ep = TestEndpoint {
1561 qualified_name: Some("training.Philosopher.ping()".to_string()),
1562 };
1563 for (adverb, expected_adverb) in [
1564 (EndpointAdverb::Call, "call"),
1565 (EndpointAdverb::CallOne, "call_one"),
1566 (EndpointAdverb::Choose, "choose"),
1567 (EndpointAdverb::Stream, "stream"),
1568 ] {
1569 let headers = ep.build_operation_context_headers(adverb);
1570 assert_eq!(
1571 headers.get(OPERATION_ENDPOINT).as_deref(),
1572 Some("training.Philosopher.ping()"),
1573 "adverb {:?}: OPERATION_ENDPOINT",
1574 adverb,
1575 );
1576 assert_eq!(
1577 headers.get(OPERATION_ADVERB).as_deref(),
1578 Some(expected_adverb),
1579 "adverb {:?}: OPERATION_ADVERB",
1580 adverb,
1581 );
1582 }
1583 }
1584
1585 #[test]
1590 fn test_rc1_build_operation_context_headers_omits_endpoint_when_no_qualified_name() {
1591 let ep = TestEndpoint {
1592 qualified_name: None,
1593 };
1594 let headers = ep.build_operation_context_headers(EndpointAdverb::CallOne);
1595 assert!(
1596 headers.get(OPERATION_ENDPOINT).is_none(),
1597 "OPERATION_ENDPOINT must be absent when endpoint has no qualified name",
1598 );
1599 assert_eq!(
1600 headers.get(OPERATION_ADVERB).as_deref(),
1601 Some("call_one"),
1602 "OPERATION_ADVERB should still be stamped",
1603 );
1604 }
1605}