1use std::hash::DefaultHasher;
10use std::hash::Hash;
11use std::hash::Hasher;
12use std::time::Duration;
13
14use anyhow::Result;
15use hyperactor::Client;
16use hyperactor::RemoteMessage;
17use hyperactor::channel::ChannelAddr;
18use hyperactor::mailbox::PortReceiver;
19use hyperactor::proc::Proc;
20use monarch_types::PickledPyObject;
21use pyo3::exceptions::PyRuntimeError;
22use pyo3::exceptions::PyValueError;
23use pyo3::prelude::*;
24use pyo3::types::PyList;
25use pyo3::types::PyType;
26
27use crate::actor::PythonActor;
28use crate::actor::PythonActorHandle;
29use crate::runtime::signal_safe_block_on;
30
31#[derive(Clone, Debug)]
33#[pyclass(
34 name = "Proc",
35 module = "monarch._rust_bindings.monarch_hyperactor.proc"
36)]
37pub struct PyProc {
38 pub(super) inner: Proc,
39}
40
41#[pymethods]
42impl PyProc {
43 #[new]
44 #[pyo3(signature = ())]
45 fn new() -> PyResult<Self> {
46 Ok(Self {
47 inner: Proc::isolated(),
48 })
49 }
50
51 #[getter]
52 fn addr(&self) -> String {
53 self.inner.proc_addr().addr().to_string()
54 }
55
56 #[getter]
57 fn name(&self) -> String {
58 self.inner
59 .proc_addr()
60 .label()
61 .map(|l: &hyperactor::id::Label| l.as_str().to_string())
62 .unwrap_or_else(|| self.inner.proc_addr().id().to_string())
63 }
64
65 #[getter]
66 fn id(&self) -> String {
67 self.inner.proc_addr().to_string()
68 }
69
70 fn destroy<'py>(
71 &mut self,
72 timeout_in_secs: u64,
73 py: Python<'py>,
74 ) -> PyResult<Bound<'py, PyList>> {
75 let mut inner = self.inner.clone();
76 let (_stopped, aborted) = signal_safe_block_on(py, async move {
77 inner
78 .destroy_and_wait(Duration::from_secs(timeout_in_secs), "destroy")
79 .await
80 .map_err(|e| PyRuntimeError::new_err(e.to_string()))
81 })??;
82 let aborted_actors = aborted
83 .into_iter()
84 .map(|actor_id| format!("{}", actor_id))
85 .collect::<Vec<_>>();
86 PyList::new(py, aborted_actors)
89 }
90
91 #[pyo3(signature = (actor, name=None))]
92 fn spawn<'py>(
93 &self,
94 py: Python<'py>,
95 actor: &Bound<'py, PyType>,
96 name: Option<String>,
97 ) -> PyResult<Bound<'py, PyAny>> {
98 let proc = self.inner.clone();
99 let pickled_type = PickledPyObject::pickle(actor.as_any())?;
100 crate::runtime::future_into_py(py, async move {
101 let actor = PythonActor::new(pickled_type, None, None, None)?;
102 Ok(PythonActorHandle {
103 inner: proc.spawn_with_label(name.as_deref().unwrap_or("anon"), actor),
104 })
105 })
106 }
107
108 #[pyo3(signature = (actor, name=None))]
109 fn spawn_blocking<'py>(
110 &self,
111 py: Python<'py>,
112 actor: &Bound<'py, PyType>,
113 name: Option<String>,
114 ) -> PyResult<PythonActorHandle> {
115 let proc = self.inner.clone();
116 let pickled_type = PickledPyObject::pickle(actor.as_any())?;
117 Ok(PythonActorHandle {
118 inner: signal_safe_block_on(py, async move {
119 let actor = PythonActor::new(pickled_type, None, None, None)?;
120 Ok(proc.spawn_with_label(name.as_deref().unwrap_or("anon"), actor))
121 })
122 .map_err(|e| PyRuntimeError::new_err(e.to_string()))?
123 .map_err(|e: anyhow::Error| PyRuntimeError::new_err(e.to_string()))?,
124 })
125 }
126}
127
128impl PyProc {
129 pub fn new_from_proc(proc: Proc) -> Self {
130 Self { inner: proc }
131 }
132}
133
134#[pyclass(
135 frozen,
136 name = "ActorAddr",
137 module = "monarch._rust_bindings.monarch_hyperactor.proc"
138)]
139#[derive(Clone)]
140pub struct PyActorAddr {
141 pub(super) inner: hyperactor::ActorAddr,
142}
143
144impl From<hyperactor::ActorAddr> for PyActorAddr {
145 fn from(actor_id: hyperactor::ActorAddr) -> Self {
146 Self { inner: actor_id }
147 }
148}
149
150impl From<PyActorAddr> for hyperactor::ActorAddr {
151 fn from(val: PyActorAddr) -> Self {
152 val.inner
153 }
154}
155
156#[pymethods]
157impl PyActorAddr {
158 #[new]
159 #[pyo3(signature = (*, addr, proc_name, actor_name))]
160 fn new(addr: &str, proc_name: &str, actor_name: &str) -> PyResult<Self> {
161 let addr: ChannelAddr = addr.parse().map_err(|e| {
162 PyValueError::new_err(format!("Failed to parse channel address '{}': {}", addr, e))
163 })?;
164 Ok(Self {
165 inner: hyperactor::ProcAddr::singleton(addr, proc_name).actor_addr(actor_name),
166 })
167 }
168
169 #[staticmethod]
170 fn from_string(actor_id: &str) -> PyResult<Self> {
171 Ok(Self {
172 inner: actor_id.parse().map_err(|e| {
173 PyValueError::new_err(format!(
174 "Failed to extract actor id from {}: {}",
175 actor_id, e
176 ))
177 })?,
178 })
179 }
180
181 #[getter]
182 fn addr(&self) -> String {
183 self.inner.proc_addr().addr().to_string()
184 }
185
186 #[getter]
187 fn proc_name(&self) -> String {
188 self.inner
189 .proc_addr()
190 .label()
191 .map(|l: &hyperactor::id::Label| l.as_str().to_string())
192 .unwrap_or_else(|| self.inner.proc_addr().id().to_string())
193 }
194
195 #[getter]
196 fn actor_name(&self) -> String {
197 self.inner
198 .label()
199 .map(|l: &hyperactor::id::Label| l.as_str().to_string())
200 .unwrap_or_else(|| self.inner.uid().to_string())
201 }
202
203 #[getter]
204 fn label(&self) -> Option<String> {
205 self.inner
206 .label()
207 .map(|l: &hyperactor::id::Label| l.as_str().to_string())
208 }
209
210 #[getter]
211 fn proc_label(&self) -> Option<String> {
212 self.inner
213 .proc_addr()
214 .label()
215 .map(|l: &hyperactor::id::Label| l.as_str().to_string())
216 }
217
218 #[getter]
219 fn uid(&self) -> String {
220 self.inner.uid().to_string()
221 }
222
223 #[getter]
224 fn pid(&self) -> String {
225 self.uid()
226 }
227
228 #[getter]
229 fn proc_id(&self) -> String {
230 self.inner.proc_addr().to_string()
231 }
232
233 #[getter]
234 fn is_root(&self) -> bool {
235 self.inner.is_root()
236 }
237
238 fn __str__(&self) -> String {
239 self.inner.to_string()
240 }
241
242 fn __hash__(&self) -> u64 {
243 let mut hasher = DefaultHasher::new();
244 self.inner.to_string().hash(&mut hasher);
245 hasher.finish()
246 }
247
248 fn __eq__(&self, other: &Bound<'_, PyAny>) -> PyResult<bool> {
249 if let Ok(other) = other.extract::<PyActorAddr>() {
250 Ok(self.inner == other.inner)
251 } else {
252 Ok(false)
253 }
254 }
255
256 fn __reduce__<'py>(slf: &Bound<'py, Self>) -> PyResult<(Bound<'py, PyAny>, (String,))> {
257 Ok((slf.getattr("from_string")?, (slf.borrow().__str__(),)))
258 }
259}
260
261impl From<&PyActorAddr> for hyperactor::ActorAddr {
262 fn from(actor_id: &PyActorAddr) -> Self {
263 actor_id.inner.clone()
264 }
265}
266
267impl std::fmt::Debug for PyActorAddr {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 self.inner.fmt(f)
270 }
271}
272
273#[derive(Clone, Copy, PartialEq, Eq)]
274enum InstanceStatus {
275 Running,
276 Stopped,
277}
278
279#[pyclass(
282 frozen,
283 name = "Serialized",
284 module = "monarch._rust_bindings.monarch_hyperactor.proc"
285)]
286#[derive(Debug)]
287pub struct PySerialized {
288 inner: wirevalue::Any,
289 port: u64,
291}
292
293impl PySerialized {
294 pub fn new<M: RemoteMessage>(message: &M) -> PyResult<Self> {
295 Ok(Self {
296 inner: wirevalue::Any::serialize(message).map_err(|err| {
297 PyRuntimeError::new_err(format!(
298 "failed to serialize message of type {} to Any: {}",
299 std::any::type_name::<M>(),
300 err
301 ))
302 })?,
303 port: M::port(),
304 })
305 }
306
307 pub fn deserialized<M: RemoteMessage>(&self) -> PyResult<M> {
308 self.inner.deserialized().map_err(|err| {
309 PyRuntimeError::new_err(format!("failed to deserialize message: {}", err))
310 })
311 }
312
313 pub fn port(&self) -> u64 {
315 self.port
316 }
317}
318
319pub struct InstanceWrapper<M: RemoteMessage> {
323 instance: Client,
324 message_receiver: PortReceiver<M>,
325 status: InstanceStatus,
326 actor_id: hyperactor::ActorAddr,
327}
328
329impl<M: RemoteMessage> InstanceWrapper<M> {
330 pub fn new(proc: &PyProc, actor_name: &str) -> Result<Self> {
331 let instance = proc.inner.client(actor_name);
332 let (_handler_port, message_receiver) = instance.bind_handler_port::<M>();
334
335 let actor_id = instance.self_addr().clone();
336
337 Ok(Self {
338 instance,
339 message_receiver,
340 status: InstanceStatus::Running,
341 actor_id,
342 })
343 }
344
345 pub fn send(&self, actor_id: &PyActorAddr, message: &PySerialized) -> PyResult<()> {
348 hyperactor::internal_macro_support::tracing::debug!(
349 name = "py_send_message",
350 actor_id =
351 hyperactor::internal_macro_support::tracing::field::display(self.actor_addr()),
352 receiver_actor_id = tracing::field::display(&actor_id.inner),
353 ?message,
354 );
355 actor_id
356 .inner
357 .port_addr(hyperactor::Port::handler_id(message.port(), None))
358 .send(&self.instance, message.inner.clone());
359 Ok(())
360 }
361
362 fn ensure_alive(&self) -> Result<()> {
364 anyhow::ensure!(
365 self.status == InstanceStatus::Running,
366 "actor is not running"
367 );
368 Ok(())
369 }
370
371 #[hyperactor::instrument(level = "trace", fields(actor_id = hyperactor::internal_macro_support::tracing::field::display(self.actor_addr())))]
374 pub async fn next_message(&mut self, timeout_msec: Option<u64>) -> Result<Option<M>> {
375 hyperactor::declare_static_timer!(
376 PY_NEXT_MESSAGE_TIMER,
377 "py_next_message",
378 hyperactor_telemetry::TimeUnit::Nanos
379 );
380 let _ = PY_NEXT_MESSAGE_TIMER
381 .start(hyperactor::kv_pairs!("actor_id" => self.actor_addr().to_string(), "mode" => match timeout_msec{
382 None => "blocking",
383 Some(0) => "polling",
384 Some(_) => "blocking_with_timeout",
385 }));
386 self.ensure_alive()?;
387 match timeout_msec {
388 None => {
390 self.message_receiver.recv().await.map(Some)},
391 Some(0) => {
392 self.message_receiver.try_recv()
395 }
396 Some(timeout_msec) => {
397 match tokio::time::timeout(
399 Duration::from_millis(timeout_msec),
400 self.message_receiver.recv(),
401 )
402 .await
403 {
404 Ok(output) => output.map(Some),
405 Err(_) => Ok(None), }
407 }
408 }
409 .map_err(|err| err.into())
410 .inspect_err(|err| {
411 hyperactor::metrics::ACTOR_MESSAGE_RECEIVE_ERRORS.add(1, hyperactor::kv_pairs!("actor_id" => self.actor_addr().to_string()));
412 tracing::error!(err=?err, actor_id=%self.actor_addr(), "unable to receive next py message");
413 })
414 .inspect(|_|{
415 hyperactor::metrics::ACTOR_MESSAGES_RECEIVED.add(1, hyperactor::kv_pairs!("actor_id" => self.actor_addr().to_string()));
416 })
417 }
418
419 #[hyperactor::instrument(fields(actor_id=hyperactor::internal_macro_support::tracing::field::display(self.actor_addr())))]
421 pub fn drain_and_stop(&mut self) -> Result<Vec<M>> {
422 self.ensure_alive()?;
423 let messages: Vec<M> = self.message_receiver.drain().into_iter().collect();
424 tracing::info!("stopping the client actor in Python client");
425 self.status = InstanceStatus::Stopped;
426 Ok(messages)
427 }
428
429 pub fn instance(&self) -> &Client {
430 &self.instance
431 }
432
433 pub fn actor_addr(&self) -> &hyperactor::ActorAddr {
434 &self.actor_id
435 }
436}
437
438pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
439 hyperactor_mod.add_class::<PyProc>()?;
440 hyperactor_mod.add_class::<PyActorAddr>()?;
441 hyperactor_mod.add_class::<PySerialized>()?;
442 Ok(())
443}