1use std::str::FromStr;
10
11use hyperactor::channel::BindSpec;
12use hyperactor::channel::ChannelAddr;
13use hyperactor::channel::ChannelTransport;
14use hyperactor::channel::TcpMode;
15use hyperactor::channel::TlsAddr;
16use hyperactor::channel::TlsMode;
17use pyo3::IntoPyObjectExt;
18use pyo3::exceptions::PyRuntimeError;
19use pyo3::exceptions::PyTypeError;
20use pyo3::exceptions::PyValueError;
21use pyo3::prelude::*;
22
23#[pyclass(
28 name = "ChannelTransport",
29 module = "monarch._rust_bindings.monarch_hyperactor.channel",
30 eq
31)]
32#[derive(PartialEq, Clone, Copy, Debug)]
33pub enum PyChannelTransport {
34 TcpWithLocalhost,
35 TcpWithHostname,
36 MetaTlsWithHostname,
37 MetaTlsWithIpV6,
38 Tls,
39 Quic,
40 MetaQuicWithHostname,
41 MetaQuicWithIpV6,
42 Local,
43 Unix,
44 }
46
47#[pymethods]
48impl PyChannelTransport {
49 fn get(&self) -> Self {
50 *self
51 }
52
53 fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
54 let getattr_fn = py.import("builtins")?.getattr("getattr")?;
55 let variant_name = match self {
56 PyChannelTransport::TcpWithLocalhost => "TcpWithLocalhost",
57 PyChannelTransport::TcpWithHostname => "TcpWithHostname",
58 PyChannelTransport::MetaTlsWithHostname => "MetaTlsWithHostname",
59 PyChannelTransport::MetaTlsWithIpV6 => "MetaTlsWithIpV6",
60 PyChannelTransport::Tls => "Tls",
61 PyChannelTransport::Quic => "Quic",
62 PyChannelTransport::MetaQuicWithHostname => "MetaQuicWithHostname",
63 PyChannelTransport::MetaQuicWithIpV6 => "MetaQuicWithIpV6",
64 PyChannelTransport::Local => "Local",
65 PyChannelTransport::Unix => "Unix",
66 };
67 let cls = py
68 .import("monarch._rust_bindings.monarch_hyperactor.channel")?
69 .getattr("ChannelTransport")?;
70 let args = (cls, variant_name).into_bound_py_any(py)?;
71 Ok((getattr_fn, args))
72 }
73}
74
75impl TryFrom<ChannelTransport> for PyChannelTransport {
76 type Error = PyErr;
77
78 fn try_from(transport: ChannelTransport) -> PyResult<Self> {
79 match transport {
80 ChannelTransport::Tcp(TcpMode::Localhost) => Ok(PyChannelTransport::TcpWithLocalhost),
81 ChannelTransport::Tcp(TcpMode::Hostname) => Ok(PyChannelTransport::TcpWithHostname),
82 ChannelTransport::MetaTls(TlsMode::Hostname) => {
83 Ok(PyChannelTransport::MetaTlsWithHostname)
84 }
85 ChannelTransport::MetaTls(TlsMode::IpV6) => Ok(PyChannelTransport::MetaTlsWithIpV6),
86 ChannelTransport::Tls => Ok(PyChannelTransport::Tls),
87 ChannelTransport::Quic => Ok(PyChannelTransport::Quic),
88 ChannelTransport::MetaQuic(TlsMode::Hostname) => {
89 Ok(PyChannelTransport::MetaQuicWithHostname)
90 }
91 ChannelTransport::MetaQuic(TlsMode::IpV6) => Ok(PyChannelTransport::MetaQuicWithIpV6),
92 ChannelTransport::Local => Ok(PyChannelTransport::Local),
93 ChannelTransport::Unix => Ok(PyChannelTransport::Unix),
94 }
95 }
96}
97
98#[pyclass(
100 name = "BindSpec",
101 module = "monarch._rust_bindings.monarch_hyperactor.channel"
102)]
103#[derive(Clone, Debug, PartialEq)]
104pub struct PyBindSpec {
105 inner: BindSpec,
106}
107
108#[pymethods]
109impl PyBindSpec {
110 #[new]
118 pub fn new(spec: &Bound<'_, PyAny>) -> PyResult<Self> {
119 if let Ok(bind_spec) = spec.extract::<PyBindSpec>() {
121 return Ok(bind_spec);
122 }
123
124 if let Ok(py_transport) = spec.extract::<PyChannelTransport>() {
126 let transport: ChannelTransport = py_transport.into();
127 return Ok(PyBindSpec {
128 inner: BindSpec::Any(transport),
129 });
130 }
131
132 if let Ok(spec_str) = spec.extract::<String>() {
134 let bind_spec = BindSpec::from_str(&spec_str).map_err(|e| {
135 PyValueError::new_err(format!("invalid str for BindSpec '{}': {}", spec_str, e))
136 })?;
137 return Ok(PyBindSpec { inner: bind_spec });
138 }
139
140 Err(PyTypeError::new_err(
141 "expected ChannelTransport enum, BindSpec, or str",
142 ))
143 }
144
145 fn __str__(&self) -> String {
146 self.inner.to_string()
147 }
148
149 fn __repr__(&self) -> String {
150 format!("PyBindSpec({:?})", self.inner)
151 }
152
153 fn __eq__(&self, other: &Self) -> bool {
154 self.inner == other.inner
155 }
156}
157
158impl From<PyBindSpec> for BindSpec {
159 fn from(spec: PyBindSpec) -> Self {
160 spec.inner
161 }
162}
163
164impl From<BindSpec> for PyBindSpec {
165 fn from(spec: BindSpec) -> Self {
166 PyBindSpec { inner: spec }
167 }
168}
169
170#[pyclass(
171 name = "ChannelAddr",
172 module = "monarch._rust_bindings.monarch_hyperactor.channel"
173)]
174pub struct PyChannelAddr {
175 inner: ChannelAddr,
176}
177
178impl FromStr for PyChannelAddr {
179 type Err = anyhow::Error;
180 fn from_str(addr: &str) -> anyhow::Result<Self> {
181 let inner = ChannelAddr::from_str(addr)?;
182 Ok(Self { inner })
183 }
184}
185
186#[pymethods]
187impl PyChannelAddr {
188 #[staticmethod]
193 pub fn any(transport: PyChannelTransport) -> PyResult<String> {
194 Ok(ChannelAddr::any(transport.into()).to_string())
195 }
196
197 #[staticmethod]
198 pub fn parse(addr: &str) -> PyResult<Self> {
199 Ok(PyChannelAddr::from_str(addr)?)
200 }
201
202 pub fn get_port(&self) -> PyResult<u16> {
205 match &self.inner {
206 ChannelAddr::Tcp(socket_addr) => Ok(socket_addr.port()),
207 ChannelAddr::MetaTls(TlsAddr { port, .. })
208 | ChannelAddr::Tls(TlsAddr { port, .. })
209 | ChannelAddr::Quic(TlsAddr { port, .. })
210 | ChannelAddr::MetaQuic(TlsAddr { port, .. }) => Ok(*port),
211 ChannelAddr::Unix(_) | ChannelAddr::Local(_) => Ok(0),
212 _ => Err(PyRuntimeError::new_err(format!(
213 "unsupported transport: `{:?}` for channel address: `{}`",
214 self.inner.transport(),
215 self.inner
216 ))),
217 }
218 }
219
220 pub fn get_transport(&self) -> PyResult<PyChannelTransport> {
222 let transport = self.inner.transport();
223 match transport {
224 ChannelTransport::Tcp(mode) => match mode {
225 TcpMode::Localhost => Ok(PyChannelTransport::TcpWithLocalhost),
226 TcpMode::Hostname => Ok(PyChannelTransport::TcpWithHostname),
227 },
228 ChannelTransport::MetaTls(mode) => match mode {
229 TlsMode::Hostname => Ok(PyChannelTransport::MetaTlsWithHostname),
230 TlsMode::IpV6 => Ok(PyChannelTransport::MetaTlsWithIpV6),
231 },
232 ChannelTransport::Tls => Ok(PyChannelTransport::Tls),
233 ChannelTransport::Quic => Ok(PyChannelTransport::Quic),
234 ChannelTransport::MetaQuic(mode) => match mode {
235 TlsMode::Hostname => Ok(PyChannelTransport::MetaQuicWithHostname),
236 TlsMode::IpV6 => Ok(PyChannelTransport::MetaQuicWithIpV6),
237 },
238 ChannelTransport::Local => Ok(PyChannelTransport::Local),
239 ChannelTransport::Unix => Ok(PyChannelTransport::Unix),
240 }
241 }
242}
243
244impl From<PyChannelTransport> for ChannelTransport {
245 fn from(val: PyChannelTransport) -> Self {
246 match val {
247 PyChannelTransport::TcpWithLocalhost => ChannelTransport::Tcp(TcpMode::Localhost),
248 PyChannelTransport::TcpWithHostname => ChannelTransport::Tcp(TcpMode::Hostname),
249 PyChannelTransport::MetaTlsWithHostname => ChannelTransport::MetaTls(TlsMode::Hostname),
250 PyChannelTransport::MetaTlsWithIpV6 => ChannelTransport::MetaTls(TlsMode::IpV6),
251 PyChannelTransport::Tls => ChannelTransport::Tls,
252 PyChannelTransport::Quic => ChannelTransport::Quic,
253 PyChannelTransport::MetaQuicWithHostname => {
254 ChannelTransport::MetaQuic(TlsMode::Hostname)
255 }
256 PyChannelTransport::MetaQuicWithIpV6 => ChannelTransport::MetaQuic(TlsMode::IpV6),
257 PyChannelTransport::Local => ChannelTransport::Local,
258 PyChannelTransport::Unix => ChannelTransport::Unix,
259 }
260 }
261}
262
263pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
264 hyperactor_mod.add_class::<PyChannelTransport>()?;
265 hyperactor_mod.add_class::<PyBindSpec>()?;
266 hyperactor_mod.add_class::<PyChannelAddr>()?;
267 Ok(())
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 #[cfg_attr(not(fbcode_build), ignore)]
277 fn test_channel_any_and_parse() -> PyResult<()> {
278 for transport in [
280 PyChannelTransport::TcpWithLocalhost,
281 PyChannelTransport::TcpWithHostname,
282 PyChannelTransport::Unix,
283 PyChannelTransport::MetaTlsWithHostname,
284 PyChannelTransport::MetaTlsWithIpV6,
285 PyChannelTransport::Tls,
286 PyChannelTransport::Quic,
287 PyChannelTransport::MetaQuicWithHostname,
288 PyChannelTransport::MetaQuicWithIpV6,
289 PyChannelTransport::Local,
290 ] {
291 let address = PyChannelAddr::any(transport)?;
292 let _ = PyChannelAddr::parse(&address)?;
293 }
294 Ok(())
295 }
296
297 #[test]
298 fn test_channel_addr_get_port() -> PyResult<()> {
299 assert_eq!(PyChannelAddr::parse("tcp![::]:26600")?.get_port()?, 26600);
300 assert_eq!(
301 PyChannelAddr::parse("metatls!devgpu1.pci.facebook.com:26600")?.get_port()?,
302 26600
303 );
304 assert_eq!(
305 PyChannelAddr::parse("quic!devgpu1.pci.facebook.com:26600")?.get_port()?,
306 26600
307 );
308 assert_eq!(
309 PyChannelAddr::parse("metaquic!devgpu1.pci.facebook.com:26600")?.get_port()?,
310 26600
311 );
312 assert_eq!(PyChannelAddr::parse("local!12345")?.get_port()?, 0);
313 assert_eq!(PyChannelAddr::parse("unix!@1a2b3c")?.get_port()?, 0);
314 Ok(())
315 }
316
317 #[test]
318 fn test_channel_addr_get_transport() -> PyResult<()> {
319 assert_eq!(
320 PyChannelAddr::parse("tcp![::1]:26600")?.get_transport()?,
321 PyChannelTransport::TcpWithLocalhost,
322 );
323 assert_eq!(
324 PyChannelAddr::parse("tcp![::]:26600")?.get_transport()?,
325 PyChannelTransport::TcpWithHostname,
326 );
327 assert_eq!(
328 PyChannelAddr::parse("metatls!devgpu001.pci.facebook.com:26600")?.get_transport()?,
329 PyChannelTransport::MetaTlsWithHostname
330 );
331 assert_eq!(
332 PyChannelAddr::parse("metatls!::1:26600")?.get_transport()?,
333 PyChannelTransport::MetaTlsWithIpV6
334 );
335 assert_eq!(
336 PyChannelAddr::parse("metatls!127.0.0.1:26600")?.get_transport()?,
338 PyChannelTransport::MetaTlsWithHostname
339 );
340 assert_eq!(
341 PyChannelAddr::parse("quic!devgpu001.pci.facebook.com:26600")?.get_transport()?,
342 PyChannelTransport::Quic
343 );
344 assert_eq!(
345 PyChannelAddr::parse("metaquic!devgpu001.pci.facebook.com:26600")?.get_transport()?,
346 PyChannelTransport::MetaQuicWithHostname
347 );
348 assert_eq!(
349 PyChannelAddr::parse("metaquic!::1:26600")?.get_transport()?,
350 PyChannelTransport::MetaQuicWithIpV6
351 );
352 assert_eq!(
353 PyChannelAddr::parse("local!12345")?.get_transport()?,
354 PyChannelTransport::Local
355 );
356 assert_eq!(
357 PyChannelAddr::parse("unix!@1a2b3c")?.get_transport()?,
358 PyChannelTransport::Unix
359 );
360 Ok(())
361 }
362}