Skip to main content

monarch_hyperactor/
pympsc.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
9//! A mpsc channel that can is used to send messages from Rust to Python without acquiring
10//! the GIL on the sender side.
11//!
12//! [`channel`] pairs one queue with an asyncio-watched [`PyReceiver`].
13
14use std::sync::Arc;
15use std::sync::Mutex;
16use std::sync::mpsc;
17
18use monarch_types::MapPyErr;
19use pyo3::Bound;
20use pyo3::IntoPyObject;
21use pyo3::IntoPyObjectExt;
22use pyo3::Py;
23use pyo3::PyAny;
24use pyo3::PyResult;
25use pyo3::Python;
26use pyo3::pyclass;
27use pyo3::pymethods;
28use pyo3::types::PyModule;
29use pyo3::types::PyModuleMethods;
30use pyo3::wrap_pyfunction;
31
32use crate::py_cell::PyCell;
33use crate::pywaker::PyEvent;
34use crate::pywaker::{self};
35
36/// Create a new channel with a Rust sender and a Python receiver.
37pub fn channel() -> Result<(Sender, PyReceiver), nix::Error> {
38    let (tx, rx) = mpsc::channel();
39    let rx = Arc::new(Mutex::new(rx));
40
41    let (waker, event) = pywaker::event()?;
42
43    Ok((
44        Sender::new(tx, Arc::new(waker)),
45        PyReceiver {
46            rx,
47            event: PyCell::new(event),
48        },
49    ))
50}
51
52/// A blanket trait used to convert boxed objects into python objects.
53pub trait IntoPyObjectBox: Send {
54    fn into_py_object(self: Box<Self>, py: Python<'_>) -> PyResult<Py<PyAny>>;
55}
56
57impl<T> IntoPyObjectBox for T
58where
59    T: for<'py> IntoPyObject<'py> + Send,
60{
61    fn into_py_object(self: Box<Self>, py: Python<'_>) -> PyResult<Py<PyAny>> {
62        (*self).into_py_any(py)
63    }
64}
65
66/// Error type for send operations
67#[derive(Debug)]
68pub struct SendError;
69
70impl std::fmt::Display for SendError {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "SendError")
73    }
74}
75
76impl std::error::Error for SendError {}
77
78/// A channel that can be used to send messages from Rust to Python without acquiring
79/// the GIL on the sender side.
80#[derive(Clone)]
81pub struct Sender {
82    // Field order matters: fields drop in declaration order, so the final
83    // sender disconnects `tx` before its `waker` closes the pipe's write end,
84    // and the receiver woken by that EOF already sees the disconnect.
85    tx: mpsc::Sender<Box<dyn IntoPyObjectBox>>,
86    /// Shared owner of the pipe's write end, used to wake the receiver after
87    /// enqueueing.
88    waker: Arc<pywaker::Waker>,
89}
90
91impl std::fmt::Debug for Sender {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.debug_struct("Sender").finish_non_exhaustive()
94    }
95}
96
97impl Sender {
98    /// Pair a queue sender with the wake mechanism its receiver waits on.
99    /// Values are enqueued before waking.
100    pub(crate) fn new(
101        tx: mpsc::Sender<Box<dyn IntoPyObjectBox>>,
102        waker: Arc<pywaker::Waker>,
103    ) -> Self {
104        Self { tx, waker }
105    }
106
107    /// Send a message to the channel. The object must be convertible to a Python object;
108    /// conversion is deferred until the message is received in a Python context.
109    pub fn send<T>(&self, msg: T) -> Result<(), SendError>
110    where
111        T: IntoPyObjectBox + Send + 'static,
112    {
113        // Enqueue before waking so the receiver cannot consume the wake and
114        // then find the queue empty (`actor::sync_actor::inbox` SI-2).
115        self.tx.send(Box::new(msg)).map_err(|_| SendError)?;
116        let _ = self.waker.wake();
117        Ok(())
118    }
119}
120
121/// The receiver side of a channel. Objects are converted to Python heap objects when
122/// they are received.
123#[pyclass(name = "Receiver", module = "monarch._src.actor.mpsc")]
124pub struct PyReceiver {
125    rx: Arc<Mutex<mpsc::Receiver<Box<dyn IntoPyObjectBox>>>>,
126    event: PyCell<PyEvent>,
127}
128
129impl std::fmt::Debug for PyReceiver {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.debug_struct("PyReceiver").finish_non_exhaustive()
132    }
133}
134
135#[pymethods]
136impl PyReceiver {
137    fn try_recv(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
138        match self.rx.lock().unwrap().try_recv() {
139            Ok(boxed_msg) => Ok(Some(boxed_msg.into_py_object(py)?)),
140            Err(mpsc::TryRecvError::Empty) => Ok(None),
141            Err(mpsc::TryRecvError::Disconnected) => {
142                Err(pyo3::exceptions::PyEOFError::new_err("Channel closed"))
143            }
144        }
145    }
146
147    fn _event<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyEvent>> {
148        let py_event = self.event.clone_ref(py)?;
149        Ok(py_event.bind(py).clone())
150    }
151}
152
153pub(crate) mod testing {
154    use pyo3::PyErr;
155    use pyo3::exceptions::PyRuntimeError;
156    use pyo3::pyfunction;
157    use pyo3::types::PyAnyMethods;
158
159    use super::*;
160
161    /// An item whose conversion into a Python object fails, standing in for a
162    /// queued message that cannot be converted on the receiving thread.
163    pub(crate) struct Unconvertible;
164
165    impl<'py> IntoPyObject<'py> for Unconvertible {
166        type Target = PyAny;
167        type Output = Bound<'py, PyAny>;
168        type Error = PyErr;
169
170        fn into_pyobject(self, _py: Python<'py>) -> PyResult<Self::Output> {
171            Err(PyRuntimeError::new_err("unconvertible test item"))
172        }
173    }
174
175    // NOTE: We can't use a Python calss name that starts with "Test" since
176    // during Python testing, Pytest will inspect anything that starts with
177    // "Test" and check if its callable which in pyo3 >= 0.26 will raise
178    // a TypeError.
179    #[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.pympsc")]
180    pub(crate) struct PyTestSender {
181        sender: Arc<Mutex<Sender>>,
182    }
183
184    #[pymethods]
185    impl PyTestSender {
186        fn send(&self, _py: Python<'_>, obj: Py<PyAny>) -> PyResult<()> {
187            self.sender.lock().unwrap().send(obj).map_pyerr()?;
188            Ok(())
189        }
190
191        fn send_unconvertible(&self) -> PyResult<()> {
192            self.sender
193                .lock()
194                .expect("test sender lock should not be poisoned: sends do not panic")
195                .send(Unconvertible)
196                .map_pyerr()?;
197            Ok(())
198        }
199    }
200
201    impl PyTestSender {
202        pub(crate) fn new(sender: Sender) -> Self {
203            Self {
204                sender: Arc::new(Mutex::new(sender)),
205            }
206        }
207    }
208
209    #[pyfunction]
210    fn channel_for_test(_py: Python<'_>) -> PyResult<(PyTestSender, PyReceiver)> {
211        let (tx, rx) = channel().map_pyerr()?;
212        Ok((PyTestSender::new(tx), rx))
213    }
214
215    pub(super) fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
216        hyperactor_mod.add_class::<PyTestSender>()?;
217        let channel_for_test = wrap_pyfunction!(channel_for_test, hyperactor_mod)?;
218        channel_for_test.setattr(
219            "__module__",
220            "monarch._rust_bindings.monarch_hyperactor.pympsc",
221        )?;
222
223        hyperactor_mod.add_function(channel_for_test)?;
224        Ok(())
225    }
226}
227
228pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
229    hyperactor_mod.add_class::<PyReceiver>()?;
230    testing::register_python_bindings(hyperactor_mod)?;
231    Ok(())
232}