Skip to main content

monarch_hyperactor/
endpoint.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use 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/// The type of endpoint operation being performed.
88///
89/// Used to select the appropriate telemetry metrics for each operation type.
90#[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
109/// RAII guard for recording endpoint call telemetry.
110///
111/// Records latency on drop, similar to Python's `@_with_telemetry` decorator.
112/// Call `mark_error()` before dropping to also record an error.
113pub 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                // Throughput already recorded once at stream creation in py_stream_collector
143            }
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
204/// Send-safe RAII guard for an OTEL-style endpoint span.
205///
206/// We only need endpoint spans for telemetry slices, not for `tracing` context
207/// propagation. So this guard emits synthetic trace events directly into the
208/// unified telemetry dispatcher instead of holding a real `tracing::Span`
209/// across `.await` points.
210pub(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                // Out-of-band mesh refs reunite only while the `PicklingState` is
416                // live, i.e. during decode, so a ref-carrying response must be
417                // decoded here (eagerly, at accumulation): a lazy decode on access
418                // would have no state to reunite against, the REFS-1 failure. But
419                // decoding *every* response here would revert D96180139, which made
420                // valuemesh values unpickle lazily on access so a large
421                // OnceBuffer-accumulated `.call()` does not pay one big unpickle
422                // burst at the end of collection.
423                //
424                // Reconcile the two by gating on refs. A `.call()` fans one
425                // endpoint over the mesh, so the batch is uniform: either every
426                // response carries refs (the endpoint returns a mesh, the minority)
427                // or none does (plain values, the common case). With no refs we
428                // keep the raw parts and let them unpickle on access (D96180139,
429                // preserved); only with refs present do we decode eagerly to
430                // reunite them.
431                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/// A streaming iterator that yields futures for each response from actors.
529///
530/// Implements Python's iterator protocol (`__iter__`/`__next__`) to yield
531/// `Future` objects that resolve to individual actor responses.
532#[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    /// Supervisor for monitoring actor health during streaming.
539    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
613/// Trait that defines the core operations an endpoint must provide.
614/// Both ActorEndpoint and RemoteEndpoint implement this trait.
615pub(crate) trait Endpoint {
616    /// Get the extent of the endpoint's targets.
617    fn get_extent(&self, py: Python<'_>) -> PyResult<Extent>;
618
619    /// Get the method name for this endpoint.
620    fn get_method_name(&self) -> &str;
621
622    /// Create and send a message with the given args/kwargs.
623    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    /// Like `send_message` but stamps `caller_headers` onto the
634    /// outgoing request envelope. Implementations that can carry
635    /// headers override this; the default delegates to `send_message`
636    /// and drops them.
637    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    /// Build the operation-context envelope headers to stamp on an
651    /// outgoing request for this endpoint invocation. The result is
652    /// empty when the endpoint cannot supply a qualified name
653    /// (e.g. `RemoteEndpoint`'s `get_qualified_name` returns `None`),
654    /// in which case callers see the unchanged dispatch surface.
655    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    /// Get the supervision_monitor for this endpoint (if any).
675    fn get_supervision_monitor(&self) -> Option<Arc<dyn Supervisable>>;
676
677    /// Get the qualified endpoint name for error messages (if any).
678    fn get_qualified_name(&self) -> Option<String>;
679
680    /// Open an OTEL-style span for an endpoint invocation. Each impl attaches
681    /// the fields the perfetto sink needs to synthesize the display name
682    /// (`{mesh}.{method}.{adverb}` for ActorEndpoint, `{call_name}.{adverb}`
683    /// for Remote) and route the slice to an actor-specific track. The adverb is the span name,
684    /// so no formatting happens at the call site and the sink formats only when
685    /// it renders the slice.
686    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    /// Call the endpoint on all actors and collect all responses into a ValueMesh.
713    #[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    /// Load balanced sends a message to one chosen actor and awaits a result.
760    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    /// Call the endpoint on exactly one actor (the mesh must have exactly one actor).
795    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    /// Call the endpoint on all actors and return an iterator of Futures.
839    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    /// Send a message to all actors without waiting for responses (fire-and-forget).
889    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    /// Create a new ActorEndpoint.
1015    #[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    /// Get the method specifier (used by actor_rref for tensor dispatch).
1038    #[getter]
1039    fn _name(&self) -> MethodSpecifier {
1040        self.method.clone()
1041    }
1042
1043    /// Get the signature (used for argument checking in _dispatch_actor_rref).
1044    #[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    /// Get the actor mesh (used by actor_rref for sending messages).
1052    #[getter]
1053    fn _actor_mesh(&self) -> PythonActorMesh {
1054        PythonActorMesh::from_impl(self.inner.clone())
1055    }
1056
1057    /// Propagation method for tensor shape inference.
1058    /// Delegates to Python _do_propagate helper.
1059    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    /// Propagation for fetch operations.
1082    /// Returns None if no propagator is provided, otherwise calls _propagate.
1083    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    /// Propagation for pipe operations.
1098    /// Requires an explicit callable propagator.
1099    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        // Check if propagator is callable
1108        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    /// Get the rref result by calling the Python dispatch helper.
1122    #[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        // Call _dispatch_actor_rref(endpoint, args, kwargs)
1132        let result = dispatch_actor_rref(py).call1((slf.into_pyobject(py)?, args, kwargs_dict))?;
1133
1134        Ok(result.unbind())
1135    }
1136
1137    /// Call the endpoint on all actors and collect all responses into a ValueMesh.
1138    #[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    /// Load balanced sends a message to one chosen actor and awaits a result.
1149    #[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    /// Call the endpoint on exactly one actor (the mesh must have exactly one actor).
1160    #[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    /// Call the endpoint on all actors and return an iterator of Futures.
1171    #[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    /// Send a message to all actors without waiting for responses (fire-and-forget).
1182    #[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    /// Send a message with optional port for response (used by actor_mesh.send).
1193    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/// A Rust wrapper for Python's RemoteImpl endpoint.
1208///
1209/// This allows us to implement the adverb methods (call, choose, call_one, stream, broadcast)
1210/// in Rust while delegating the actual send logic to the Python RemoteImpl._send() method.
1211#[pyclass(
1212    name = "Remote",
1213    module = "monarch._rust_bindings.monarch_hyperactor.endpoint"
1214)]
1215pub struct Remote {
1216    /// The wrapped Python RemoteImpl object
1217    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 // Remote endpoints don't have supervision_monitors
1256    }
1257
1258    fn get_qualified_name(&self) -> Option<String> {
1259        None // Remote endpoints don't have qualified names
1260    }
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    /// Create a new Remote wrapping a Python RemoteImpl object.
1277    #[new]
1278    fn new(remote: Py<PyAny>) -> Self {
1279        Self { inner: remote }
1280    }
1281
1282    /// Call the endpoint on all actors and collect all responses into a ValueMesh.
1283    #[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    /// Load balanced sends a message to one chosen actor and awaits a result.
1294    #[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    /// Call the endpoint on exactly one actor (the mesh must have exactly one actor).
1305    #[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    /// Call the endpoint on all actors and return an iterator of Futures.
1316    #[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    /// Send a message to all actors without waiting for responses (fire-and-forget).
1327    #[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    /// Get the rref result by calling the wrapped Remote's rref method.
1338    #[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    /// Get the call name by delegating to the wrapped Remote's _call_name.
1350    fn _call_name(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1351        self.inner.call_method0(py, "_call_name")
1352    }
1353
1354    /// Get the maybe_resolvable property from the wrapped RemoteImpl.
1355    #[getter]
1356    fn _maybe_resolvable(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1357        self.inner.getattr(py, "_maybe_resolvable")
1358    }
1359
1360    /// Get the resolvable property from the wrapped RemoteImpl.
1361    #[getter]
1362    fn _resolvable(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1363        self.inner.getattr(py, "_resolvable")
1364    }
1365
1366    /// Get the remote_impl from the wrapped RemoteImpl.
1367    /// This is needed for function_to_import_path() in function.py to work correctly.
1368    #[getter]
1369    fn _remote_impl(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1370        self.inner.getattr(py, "_remote_impl")
1371    }
1372
1373    /// Propagation method for tensor shape inference.
1374    /// Delegates to the wrapped Remote's _propagate.
1375    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    /// Propagation for fetch operations.
1388    /// Delegates to the wrapped Remote's _fetch_propagate.
1389    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    /// Propagation for pipe operations.
1405    /// Delegates to the wrapped Remote's _pipe_propagate.
1406    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    /// Send a message with optional port for response.
1422    /// Delegates to the wrapped RemoteImpl's _send.
1423    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    /// Make RemoteEndpoint callable - delegates to rref() like Remote.__call__.
1446    #[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    /// Minimal `Endpoint` impl that only serves `get_qualified_name`.
1520    /// The default `build_operation_context_headers` consults no other
1521    /// method, so the rest are unreachable.
1522    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    /// OC-1 request-side producer: each `EndpointAdverb` maps to the
1556    /// expected wire adverb string, and the qualified endpoint name
1557    /// from `get_qualified_name()` flows through to `OPERATION_ENDPOINT`.
1558    #[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    /// OC-1 request-side producer: when the endpoint has no
1586    /// qualified name (e.g. `Remote::get_qualified_name` returns
1587    /// `None`), `OPERATION_ENDPOINT` is omitted while `OPERATION_ADVERB`
1588    /// is still stamped.
1589    #[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}