Skip to main content

moirai_transport/
lib.rs

1//! Unified transport layer for Moirai concurrency library.
2//!
3//! This module provides transport abstractions that work across different
4//! communication boundaries: threads, processes, and machines. It builds on
5//! top of the core channel primitives to provide location-transparent messaging.
6//!
7//! # Design Principles
8//! - Location transparency: same API for local and remote communication
9//! - Zero-copy optimization for local transport
10//! - Pluggable transport backends (in-memory, IPC, network)
11
12#![allow(clippy::new_without_default)]
13#![allow(clippy::unwrap_or_default)]
14#![deny(missing_docs)]
15//! - Integration with Moirai scheduler for optimal performance
16
17#[cfg(any(unix, windows))]
18mod ipc;
19mod network;
20pub mod payload;
21pub mod process;
22pub mod remote_task;
23#[cfg(feature = "scheduler-routes")]
24pub mod route;
25pub mod safe_channel;
26
27use moirai_core::channel::{mpmc, MpmcReceiver, MpmcSender};
28/// Default MPMC channel capacity
29const DEFAULT_MPMC_CAPACITY: usize = 1024;
30use std::{
31    collections::HashMap,
32    fmt,
33    sync::{Arc, Mutex, MutexGuard, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard},
34};
35
36/// Crate-wide lock policy: recover from poisoning instead of propagating the
37/// panic. Guarded state here (channel maps, subscription lists, connection
38/// states) stays structurally valid under a poisoned lock — a writer that
39/// panicked mid-critical-section cannot leave a torn invariant in these maps —
40/// so continuing with the recovered guard is sound. Matches the pal reactor
41/// backends' `lock_mutex` helpers.
42pub(crate) fn lock_mutex<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
43    mutex.lock().unwrap_or_else(PoisonError::into_inner)
44}
45
46/// Poison-recovering read lock; see [`lock_mutex`] for the policy rationale.
47fn read_rwlock<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
48    lock.read().unwrap_or_else(PoisonError::into_inner)
49}
50
51/// Poison-recovering write lock; see [`lock_mutex`] for the policy rationale.
52fn write_rwlock<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
53    lock.write().unwrap_or_else(PoisonError::into_inner)
54}
55
56// Re-export core channel types for compatibility
57/// Shared-memory same-machine IPC transport (Unix/Windows only).
58#[cfg(any(unix, windows))]
59pub use ipc::IpcTransport;
60pub use moirai_core::channel::{
61    ChannelError as TransportError, MpmcReceiver as Receiver, MpmcSender as Sender,
62};
63pub use network::NetworkTransport;
64#[cfg(feature = "network")]
65pub use network::TcpTransport;
66pub(crate) use network::{read_network_frame_from_stream, NETWORK_IO_TIMEOUT};
67// The canonical typed cross-boundary channel: rkyv-style archive serialization
68// over a transport (zero-copy borrowed views on receive).
69pub use safe_channel::{
70    ArchiveSerialize, ArchiveView, ArchivedMessage, ArchivedUniversalReceiver,
71    ArchivedUniversalSender,
72};
73
74/// Result type for transport operations
75pub type TransportResult<T> = Result<T, TransportError>;
76
77/// Address for identifying communication endpoints
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub enum Address {
80    /// Local in-process address
81    Local(String),
82    /// Remote network address
83    Remote(RemoteAddress),
84}
85
86impl fmt::Display for Address {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Address::Local(id) => write!(f, "local://{}", id),
90            Address::Remote(addr) => write!(f, "{}", addr),
91        }
92    }
93}
94
95/// Transport trait for different communication mechanisms
96pub trait Transport: Send + Sync {
97    /// Send a message to the specified address
98    fn send(&self, target: &Address, data: Vec<u8>) -> TransportResult<()>;
99
100    /// Receive a message from the specified address
101    fn recv(&self, source: &Address) -> TransportResult<Vec<u8>>;
102
103    /// Check if the transport supports the given address
104    fn supports(&self, address: &Address) -> bool;
105}
106
107/// A local in-memory channel: the sender/receiver pair for one `Address::Local` id.
108type LocalChannel = (MpmcSender<Vec<u8>>, MpmcReceiver<Vec<u8>>);
109
110/// In-memory transport for local communication
111pub struct InMemoryTransport {
112    /// One `RwLock`-guarded map of `id -> (sender, receiver)`. Steady-state
113    /// `send`/`recv` resolve an existing channel under a *concurrent read* lock
114    /// and clone the cloned handle (the MPMC channel itself is lock-free), so
115    /// they no longer serialize through a global mutex per message. The write
116    /// lock is taken only to create a new channel.
117    channels: Arc<RwLock<HashMap<String, LocalChannel>>>,
118}
119
120impl InMemoryTransport {
121    /// Create an empty in-memory transport with no registered channels.
122    pub fn new() -> Self {
123        Self {
124            channels: Arc::new(RwLock::new(HashMap::new())),
125        }
126    }
127
128    fn get_or_create_channel(&self, id: &str) -> LocalChannel {
129        // Fast path: an existing channel — the steady-state case after the first
130        // message — is resolved under a concurrent read lock.
131        if let Some(pair) = read_rwlock(&self.channels).get(id) {
132            return pair.clone();
133        }
134        // Slow path: create under the write lock, re-checking in case another
135        // thread created the same id while we waited for the lock.
136        let mut channels = write_rwlock(&self.channels);
137        if let Some(pair) = channels.get(id) {
138            return pair.clone();
139        }
140        let pair = mpmc(DEFAULT_MPMC_CAPACITY);
141        channels.insert(id.to_string(), pair.clone());
142        pair
143    }
144}
145
146impl Transport for InMemoryTransport {
147    fn send(&self, target: &Address, data: Vec<u8>) -> TransportResult<()> {
148        match target {
149            Address::Local(id) => {
150                let (tx, _) = self.get_or_create_channel(id);
151                tx.send(data)
152            }
153            _ => Err(TransportError::Closed),
154        }
155    }
156
157    fn recv(&self, source: &Address) -> TransportResult<Vec<u8>> {
158        match source {
159            Address::Local(id) => {
160                let (_, rx) = self.get_or_create_channel(id);
161                rx.recv()
162            }
163            _ => Err(TransportError::Closed),
164        }
165    }
166
167    fn supports(&self, address: &Address) -> bool {
168        matches!(address, Address::Local(_))
169    }
170}
171
172/// Transport manager that routes messages to appropriate transport
173pub struct TransportManager {
174    transports: Vec<Box<dyn Transport>>,
175}
176
177impl TransportManager {
178    /// Create a manager routing local addresses in-memory and remote addresses
179    /// over the network transport.
180    pub fn new() -> Self {
181        Self {
182            transports: vec![
183                Box::new(InMemoryTransport::new()),
184                Box::new(NetworkTransport {}),
185            ],
186        }
187    }
188
189    /// Send `data` via the first registered transport supporting `target`.
190    ///
191    /// # Errors
192    /// Returns [`TransportError::Closed`] when no transport supports `target`;
193    /// otherwise propagates the selected transport's send error.
194    pub fn send(&self, target: &Address, data: Vec<u8>) -> TransportResult<()> {
195        for transport in &self.transports {
196            if transport.supports(target) {
197                return transport.send(target, data);
198            }
199        }
200        Err(TransportError::Closed)
201    }
202
203    /// Receive from the first registered transport supporting `source`.
204    ///
205    /// # Errors
206    /// Returns [`TransportError::Closed`] when no transport supports `source`;
207    /// otherwise propagates the selected transport's receive error.
208    pub fn recv(&self, source: &Address) -> TransportResult<Vec<u8>> {
209        for transport in &self.transports {
210            if transport.supports(source) {
211                return transport.recv(source);
212            }
213        }
214        Err(TransportError::Closed)
215    }
216}
217
218// A typed cross-boundary channel over a transport is provided by the rkyv-style
219// archive channels in `safe_channel` (`ArchivedUniversalSender<T: ArchiveSerialize>`
220// / `ArchivedUniversalReceiver<T: ArchiveView>`), re-exported below. The previous
221// `UniversalChannel<T: Send>` / `UniversalSender` / `UniversalReceiver` were
222// non-functional placeholders (their `send`/`recv` ignored their argument and
223// returned `Closed`): a channel generic over an arbitrary `Send` `T` cannot
224// serialize the value for transport without a serialization bound, which is
225// exactly what the archive traits add. They were removed in favor of the working
226// archive channels rather than left as mocks.
227
228/// Remote address for cross-machine communication
229#[derive(Debug, Clone, PartialEq, Eq, Hash)]
230pub struct RemoteAddress {
231    /// Remote host name or IP address.
232    pub host: String,
233    /// Remote TCP port.
234    pub port: u16,
235    /// Service label carried in the address display form.
236    pub service: String,
237}
238
239impl fmt::Display for RemoteAddress {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        write!(f, "{}://{}:{}", self.service, self.host, self.port)
242    }
243}
244
245/// Topic-based pub/sub router that delivers published messages to every
246/// subscribed [`Address`] over a shared transport.
247///
248/// The router is generic over the backing [`Transport`] so delivery is
249/// monomorphized and zero-cost; the transport must be the *same instance* the
250/// subscribers receive from (e.g. one `Arc<InMemoryTransport>`), since in-memory
251/// channels are keyed by address within a single transport instance. The prior
252/// implementation constructed a throwaway `InMemoryTransport` per send and so
253/// silently discarded every message.
254pub struct MessageRouter<T: Transport> {
255    transport: Arc<T>,
256    subscriptions: Mutex<HashMap<String, Vec<Address>>>,
257}
258
259impl<T: Transport> MessageRouter<T> {
260    /// Create a router that delivers over `transport`.
261    pub fn new(transport: Arc<T>) -> Self {
262        Self {
263            transport,
264            subscriptions: Mutex::new(HashMap::new()),
265        }
266    }
267
268    /// Subscribe `address` to `topic`. Duplicate (topic, address) pairs are
269    /// ignored so a message is delivered to each subscriber exactly once.
270    pub fn subscribe(&self, topic: &str, address: Address) {
271        let mut subs = lock_mutex(&self.subscriptions);
272        let entry = subs.entry(topic.to_string()).or_default();
273        if !entry.contains(&address) {
274            entry.push(address);
275        }
276    }
277
278    /// Remove `address` from `topic`. Returns `true` if a subscription was
279    /// removed.
280    pub fn unsubscribe(&self, topic: &str, address: &Address) -> bool {
281        let mut subs = lock_mutex(&self.subscriptions);
282        if let Some(entry) = subs.get_mut(topic) {
283            let before = entry.len();
284            entry.retain(|a| a != address);
285            let removed = entry.len() != before;
286            if entry.is_empty() {
287                subs.remove(topic);
288            }
289            return removed;
290        }
291        false
292    }
293
294    /// Publish `data` to every subscriber of `topic` via the shared transport.
295    ///
296    /// Returns the number of subscribers the message was delivered to. Delivery
297    /// is fail-fast: the first transport error is propagated (after the
298    /// subscribers ahead of it have already received the message).
299    ///
300    /// # Errors
301    /// Propagates the first per-subscriber transport send error.
302    pub fn publish(&self, topic: &str, data: Vec<u8>) -> TransportResult<usize> {
303        // Snapshot the subscriber list so the transport sends happen without the
304        // subscriptions lock held (a subscriber's send must not block resubscribe).
305        let targets: Vec<Address> = {
306            let subs = lock_mutex(&self.subscriptions);
307            match subs.get(topic) {
308                Some(addresses) => addresses.clone(),
309                None => return Ok(0),
310            }
311        };
312
313        // `Transport::send` takes ownership (each in-memory subscriber channel
314        // stores its own `Vec<u8>`), so N subscribers need N owned buffers —
315        // but only N-1 copies: the caller's original buffer is moved to the
316        // final subscriber instead of being cloned and dropped.
317        let mut delivered = 0;
318        let Some(last) = targets.len().checked_sub(1) else {
319            return Ok(0);
320        };
321        let mut data = Some(data);
322        for (index, addr) in targets.iter().enumerate() {
323            let payload = if index == last {
324                data.take()
325                    .expect("invariant: original buffer moved exactly once, at the last subscriber")
326            } else {
327                data.as_ref()
328                    .expect("invariant: original buffer present until the last subscriber")
329                    .clone()
330            };
331            self.transport.send(addr, payload)?;
332            delivered += 1;
333        }
334        Ok(delivered)
335    }
336
337    /// Number of distinct addresses subscribed to `topic`.
338    pub fn subscriber_count(&self, topic: &str) -> usize {
339        lock_mutex(&self.subscriptions)
340            .get(topic)
341            .map_or(0, Vec::len)
342    }
343}
344
345/// Tracks the connection state of remote/local endpoints.
346pub struct ConnectionManager {
347    connections: Arc<Mutex<HashMap<Address, ConnectionState>>>,
348}
349
350/// Observable state of a tracked connection.
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum ConnectionState {
353    /// The endpoint is currently connected.
354    Connected,
355    /// The endpoint was connected and has since disconnected.
356    Disconnected,
357}
358
359impl ConnectionManager {
360    /// Create a manager tracking no endpoints.
361    pub fn new() -> Self {
362        Self {
363            connections: Arc::new(Mutex::new(HashMap::new())),
364        }
365    }
366
367    /// Mark `address` as connected.
368    pub fn connect(&self, address: &Address) -> TransportResult<()> {
369        let mut conns = lock_mutex(&self.connections);
370        conns.insert(address.clone(), ConnectionState::Connected);
371        Ok(())
372    }
373
374    /// Mark `address` as disconnected.
375    pub fn disconnect(&self, address: &Address) -> TransportResult<()> {
376        let mut conns = lock_mutex(&self.connections);
377        conns.insert(address.clone(), ConnectionState::Disconnected);
378        Ok(())
379    }
380
381    /// Current tracked state of `address`, or `None` if never seen.
382    #[must_use]
383    pub fn state(&self, address: &Address) -> Option<ConnectionState> {
384        lock_mutex(&self.connections).get(address).copied()
385    }
386
387    /// Whether `address` is currently connected.
388    #[must_use]
389    pub fn is_connected(&self, address: &Address) -> bool {
390        self.state(address) == Some(ConnectionState::Connected)
391    }
392
393    /// All currently-connected addresses.
394    #[must_use]
395    pub fn connected_addresses(&self) -> Vec<Address> {
396        lock_mutex(&self.connections)
397            .iter()
398            .filter(|(_, state)| **state == ConnectionState::Connected)
399            .map(|(addr, _)| addr.clone())
400            .collect()
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn test_channel_compatibility() {
410        let (tx, rx) = moirai_core::channel::mpmc::<i32>(10);
411
412        assert!(tx.send(42).is_ok());
413        assert_eq!(rx.recv().unwrap(), 42);
414    }
415
416    #[test]
417    fn test_in_memory_transport() {
418        let transport1 = InMemoryTransport::new();
419        let transport2 = InMemoryTransport::new();
420
421        // Register transports with each other for routing
422        // This would require a more robust mechanism for inter-transport communication
423        // For now, we'll just check if they can send/recv to/from themselves
424        assert!(transport1
425            .send(&Address::Local("t1".to_string()), vec![1])
426            .is_ok());
427        assert_eq!(
428            transport1.recv(&Address::Local("t1".to_string())).unwrap(),
429            vec![1]
430        );
431
432        assert!(transport2
433            .send(&Address::Local("t2".to_string()), vec![2])
434            .is_ok());
435        assert_eq!(
436            transport2.recv(&Address::Local("t2".to_string())).unwrap(),
437            vec![2]
438        );
439    }
440
441    #[test]
442    fn network_transport_transfers_length_prefixed_remote_bytes() {
443        let transport = NetworkTransport {};
444        let address = loopback_remote_address();
445        let payload = b"server route payload".to_vec();
446        let expected = payload.clone();
447        let receiver_address = Address::Remote(address.clone());
448        let receiver = std::thread::spawn(move || transport.recv(&receiver_address).unwrap());
449
450        std::thread::sleep(std::time::Duration::from_millis(10));
451        NetworkTransport {}
452            .send(&Address::Remote(address), payload)
453            .unwrap();
454
455        assert_eq!(receiver.join().unwrap(), expected);
456    }
457
458    #[test]
459    fn transport_manager_routes_remote_bytes_through_network_transport() {
460        let manager = TransportManager::new();
461        let address = loopback_remote_address();
462        let payload = b"transport manager remote payload".to_vec();
463        let expected = payload.clone();
464        let receiver_address = Address::Remote(address.clone());
465        let receiver =
466            std::thread::spawn(move || TransportManager::new().recv(&receiver_address).unwrap());
467
468        std::thread::sleep(std::time::Duration::from_millis(10));
469        manager.send(&Address::Remote(address), payload).unwrap();
470
471        assert_eq!(receiver.join().unwrap(), expected);
472    }
473
474    fn loopback_remote_address() -> RemoteAddress {
475        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
476        let port = listener.local_addr().unwrap().port();
477        drop(listener);
478        RemoteAddress {
479            host: "127.0.0.1".to_string(),
480            port,
481            service: "moirai-test".to_string(),
482        }
483    }
484
485    #[test]
486    fn message_router_delivers_to_each_subscriber_once() {
487        let transport = Arc::new(InMemoryTransport::new());
488        let router = MessageRouter::new(Arc::clone(&transport));
489        let sub_a = Address::Local("sub_a".to_string());
490        let sub_b = Address::Local("sub_b".to_string());
491
492        router.subscribe("topic", sub_a.clone());
493        router.subscribe("topic", sub_b.clone());
494        router.subscribe("topic", sub_a.clone()); // duplicate ignored
495        assert_eq!(router.subscriber_count("topic"), 2);
496
497        let delivered = router.publish("topic", vec![1, 2, 3]).unwrap();
498        assert_eq!(delivered, 2);
499
500        // Both subscribers actually receive the message through the shared
501        // transport (the prior throwaway-transport implementation delivered none).
502        assert_eq!(transport.recv(&sub_a).unwrap(), vec![1, 2, 3]);
503        assert_eq!(transport.recv(&sub_b).unwrap(), vec![1, 2, 3]);
504    }
505
506    #[test]
507    fn message_router_single_subscriber_receives_moved_buffer() {
508        // N = 1 exercises the zero-clone path: the caller's buffer is moved to
509        // the sole subscriber without an intermediate copy.
510        let transport = Arc::new(InMemoryTransport::new());
511        let router = MessageRouter::new(Arc::clone(&transport));
512        let sub = Address::Local("solo".to_string());
513        router.subscribe("t", sub.clone());
514
515        let delivered = router.publish("t", vec![7, 8, 9]).unwrap();
516        assert_eq!(delivered, 1);
517        assert_eq!(transport.recv(&sub).unwrap(), vec![7, 8, 9]);
518    }
519
520    #[test]
521    fn message_router_unknown_topic_delivers_nothing() {
522        let transport = Arc::new(InMemoryTransport::new());
523        let router = MessageRouter::new(transport);
524        assert_eq!(router.publish("absent", vec![0]).unwrap(), 0);
525    }
526
527    #[test]
528    fn message_router_unsubscribe_stops_delivery() {
529        let transport = Arc::new(InMemoryTransport::new());
530        let router = MessageRouter::new(Arc::clone(&transport));
531        let sub = Address::Local("s".to_string());
532
533        router.subscribe("t", sub.clone());
534        assert!(router.unsubscribe("t", &sub));
535        assert!(
536            !router.unsubscribe("t", &sub),
537            "second unsubscribe is a no-op"
538        );
539        assert_eq!(router.subscriber_count("t"), 0);
540        assert_eq!(router.publish("t", vec![9]).unwrap(), 0);
541    }
542
543    #[test]
544    fn connection_manager_tracks_and_reports_state() {
545        let mgr = ConnectionManager::new();
546        let addr = Address::Local("node".to_string());
547
548        assert_eq!(mgr.state(&addr), None);
549        assert!(!mgr.is_connected(&addr));
550        assert!(mgr.connected_addresses().is_empty());
551
552        mgr.connect(&addr).unwrap();
553        assert!(mgr.is_connected(&addr));
554        assert_eq!(mgr.state(&addr), Some(ConnectionState::Connected));
555        assert_eq!(mgr.connected_addresses(), vec![addr.clone()]);
556
557        mgr.disconnect(&addr).unwrap();
558        assert!(!mgr.is_connected(&addr));
559        assert_eq!(mgr.state(&addr), Some(ConnectionState::Disconnected));
560        assert!(mgr.connected_addresses().is_empty());
561    }
562}