Skip to main content

moirai_transport/
router.rs

1use crate::{lock_mutex, transport::Address, Transport, TransportResult};
2use std::{
3    collections::HashMap,
4    fmt,
5    sync::{Arc, Mutex},
6};
7
8/// Remote address for cross-machine communication
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct RemoteAddress {
11    /// Remote host name or IP address.
12    pub host: String,
13    /// Remote TCP port.
14    pub port: u16,
15    /// Service label carried in the address display form.
16    pub service: String,
17}
18
19impl fmt::Display for RemoteAddress {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(f, "{}://{}:{}", self.service, self.host, self.port)
22    }
23}
24
25/// Topic-based pub/sub router that delivers published messages to every
26/// subscribed [`Address`] over a shared transport.
27///
28/// The router is generic over the backing [`Transport`] so delivery is
29/// monomorphized and zero-cost; the transport must be the *same instance* the
30/// subscribers receive from (e.g. one `Arc<crate::InMemoryTransport>`), since
31/// in-memory channels are keyed by address within a single transport instance.
32/// The prior implementation constructed a throwaway `InMemoryTransport` per
33/// send and so silently discarded every message.
34pub struct MessageRouter<T: Transport> {
35    transport: Arc<T>,
36    subscriptions: Mutex<HashMap<String, Vec<Address>>>,
37}
38
39impl<T: Transport> MessageRouter<T> {
40    /// Create a router that delivers over `transport`.
41    pub fn new(transport: Arc<T>) -> Self {
42        Self {
43            transport,
44            subscriptions: Mutex::new(HashMap::new()),
45        }
46    }
47
48    /// Subscribe `address` to `topic`. Duplicate (topic, address) pairs are
49    /// ignored so a message is delivered to each subscriber exactly once.
50    pub fn subscribe(&self, topic: &str, address: Address) {
51        let mut subs = lock_mutex(&self.subscriptions);
52        let entry = subs.entry(topic.to_string()).or_default();
53        if !entry.contains(&address) {
54            entry.push(address);
55        }
56    }
57
58    /// Remove `address` from `topic`. Returns `true` if a subscription was
59    /// removed.
60    pub fn unsubscribe(&self, topic: &str, address: &Address) -> bool {
61        let mut subs = lock_mutex(&self.subscriptions);
62        if let Some(entry) = subs.get_mut(topic) {
63            let before = entry.len();
64            entry.retain(|a| a != address);
65            let removed = entry.len() != before;
66            if entry.is_empty() {
67                subs.remove(topic);
68            }
69            return removed;
70        }
71        false
72    }
73
74    /// Publish `data` to every subscriber of `topic` via the shared transport.
75    ///
76    /// Returns the number of subscribers the message was delivered to. Delivery
77    /// is fail-fast: the first transport error is propagated (after the
78    /// subscribers ahead of it have already received the message).
79    ///
80    /// # Errors
81    /// Propagates the first per-subscriber transport send error.
82    pub fn publish(&self, topic: &str, data: Vec<u8>) -> TransportResult<usize> {
83        // Snapshot the subscriber list so the transport sends happen without the
84        // subscriptions lock held (a subscriber's send must not block resubscribe).
85        let targets: Vec<Address> = {
86            let subs = lock_mutex(&self.subscriptions);
87            match subs.get(topic) {
88                Some(addresses) => addresses.clone(),
89                None => return Ok(0),
90            }
91        };
92
93        // `Transport::send` takes ownership (each in-memory subscriber channel
94        // stores its own `Vec<u8>`), so N subscribers need N owned buffers —
95        // but only N-1 copies: the caller's original buffer is moved to the
96        // final subscriber instead of being cloned and dropped.
97        let mut delivered = 0;
98        let Some(last) = targets.len().checked_sub(1) else {
99            return Ok(0);
100        };
101        let mut data = Some(data);
102        for (index, addr) in targets.iter().enumerate() {
103            let payload = if index == last {
104                data.take()
105                    .expect("invariant: original buffer moved exactly once, at the last subscriber")
106            } else {
107                data.as_ref()
108                    .expect("invariant: original buffer present until the last subscriber")
109                    .clone()
110            };
111            self.transport.send(addr, payload)?;
112            delivered += 1;
113        }
114        Ok(delivered)
115    }
116
117    /// Number of distinct addresses subscribed to `topic`.
118    pub fn subscriber_count(&self, topic: &str) -> usize {
119        lock_mutex(&self.subscriptions)
120            .get(topic)
121            .map_or(0, Vec::len)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::{InMemoryTransport, Transport};
129
130    #[test]
131    fn message_router_delivers_to_each_subscriber_once() {
132        let transport = Arc::new(InMemoryTransport::new());
133        let router = MessageRouter::new(Arc::clone(&transport));
134        let sub_a = Address::Local("sub_a".to_string());
135        let sub_b = Address::Local("sub_b".to_string());
136
137        router.subscribe("topic", sub_a.clone());
138        router.subscribe("topic", sub_b.clone());
139        router.subscribe("topic", sub_a.clone()); // duplicate ignored
140        assert_eq!(router.subscriber_count("topic"), 2);
141
142        let delivered = router.publish("topic", vec![1, 2, 3]).unwrap();
143        assert_eq!(delivered, 2);
144
145        assert_eq!(transport.recv(&sub_a).unwrap(), vec![1, 2, 3]);
146        assert_eq!(transport.recv(&sub_b).unwrap(), vec![1, 2, 3]);
147    }
148
149    #[test]
150    fn message_router_single_subscriber_receives_moved_buffer() {
151        let transport = Arc::new(InMemoryTransport::new());
152        let router = MessageRouter::new(Arc::clone(&transport));
153        let sub = Address::Local("solo".to_string());
154        router.subscribe("t", sub.clone());
155
156        let delivered = router.publish("t", vec![7, 8, 9]).unwrap();
157        assert_eq!(delivered, 1);
158        assert_eq!(transport.recv(&sub).unwrap(), vec![7, 8, 9]);
159    }
160
161    #[test]
162    fn message_router_unknown_topic_delivers_nothing() {
163        let transport = Arc::new(InMemoryTransport::new());
164        let router = MessageRouter::new(transport);
165        assert_eq!(router.publish("absent", vec![0]).unwrap(), 0);
166    }
167
168    #[test]
169    fn message_router_unsubscribe_stops_delivery() {
170        let transport = Arc::new(InMemoryTransport::new());
171        let router = MessageRouter::new(Arc::clone(&transport));
172        let sub = Address::Local("s".to_string());
173
174        router.subscribe("t", sub.clone());
175        assert!(router.unsubscribe("t", &sub));
176        assert!(
177            !router.unsubscribe("t", &sub),
178            "second unsubscribe is a no-op"
179        );
180        assert_eq!(router.subscriber_count("t"), 0);
181        assert_eq!(router.publish("t", vec![9]).unwrap(), 0);
182    }
183}