Skip to main content

pamoja_loopback/
transport.rs

1//! The loopback transport itself.
2
3use std::sync::{Arc, Mutex};
4
5use tokio::sync::mpsc::{self, UnboundedReceiver};
6
7use pamoja_core::{Error, Message, Receive, Result, Transport};
8
9use crate::broker::LoopbackBroker;
10
11/// An in-process transport that routes through a shared [`LoopbackBroker`].
12///
13/// A transport is created disconnected; [`connect`](Transport::connect) registers
14/// it with the broker so it can publish and receive. Inbound messages are read
15/// with [`recv`](Receive::recv).
16///
17/// # Examples
18///
19/// ```
20/// use pamoja_core::{Receive, Transport};
21/// use pamoja_loopback::{LoopbackBroker, LoopbackTransport};
22///
23/// # async fn run() -> pamoja_core::Result<()> {
24/// let broker = LoopbackBroker::new();
25/// let mut subscriber = LoopbackTransport::new(broker.clone());
26/// let mut publisher = LoopbackTransport::new(broker);
27/// subscriber.connect().await?;
28/// publisher.connect().await?;
29///
30/// subscriber.subscribe("sensors/+/temperature").await?;
31/// publisher.send("sensors/1/temperature", b"21.5").await?;
32///
33/// let message = subscriber.recv().await?.expect("a message");
34/// assert_eq!(message.topic, "sensors/1/temperature");
35/// assert_eq!(message.payload, b"21.5");
36/// # Ok(())
37/// # }
38/// ```
39pub struct LoopbackTransport {
40    broker: LoopbackBroker,
41    filters: Arc<Mutex<Vec<String>>>,
42    incoming: Option<UnboundedReceiver<Message>>,
43}
44
45impl LoopbackTransport {
46    /// Creates a disconnected transport bound to `broker`.
47    ///
48    /// # Arguments
49    ///
50    /// * `broker` - the shared broker this transport publishes to and receives from.
51    ///
52    /// # Returns
53    ///
54    /// A disconnected transport ready for [`connect`](Transport::connect).
55    pub fn new(broker: LoopbackBroker) -> Self {
56        Self {
57            broker,
58            filters: Arc::new(Mutex::new(Vec::new())),
59            incoming: None,
60        }
61    }
62
63    /// Reports whether the transport is connected to its broker.
64    ///
65    /// # Returns
66    ///
67    /// `true` once [`connect`](Transport::connect) has succeeded and before
68    /// [`disconnect`](LoopbackTransport::disconnect) is called.
69    pub fn is_connected(&self) -> bool {
70        self.incoming.is_some()
71    }
72
73    /// Disconnects the transport from the broker.
74    ///
75    /// Messages already queued for it are dropped, and its registration is pruned
76    /// from the broker on the next publish. Its filters are kept, so a later
77    /// [`connect`](Transport::connect) hears the same topics again.
78    pub fn disconnect(&mut self) {
79        self.incoming = None;
80    }
81}
82
83impl Transport for LoopbackTransport {
84    async fn connect(&mut self) -> Result<()> {
85        self.broker.in_reach()?;
86        let (sender, receiver) = mpsc::unbounded_channel();
87        self.broker.register(Arc::clone(&self.filters), sender);
88        self.incoming = Some(receiver);
89        Ok(())
90    }
91
92    async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
93        if self.incoming.is_none() {
94            return Err(Error::Closed);
95        }
96        self.broker.in_reach()?;
97        self.broker.publish(&Message::new(topic, payload));
98        Ok(())
99    }
100
101    async fn subscribe(&mut self, topic: &str) -> Result<()> {
102        if self.incoming.is_none() {
103            return Err(Error::Closed);
104        }
105        self.broker.in_reach()?;
106        self.filters
107            .lock()
108            .expect("filters lock")
109            .push(topic.to_owned());
110        Ok(())
111    }
112}
113
114impl Receive for LoopbackTransport {
115    /// Awaits the next message from any subscribed topic.
116    ///
117    /// A link holds its broker, so a connected link never ends on its own: this
118    /// waits until a message arrives, however long that takes. Dropping the future
119    /// before then leaves the next message queued.
120    ///
121    /// # Returns
122    ///
123    /// `Some(message)` for the next message.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`Error::Closed`](pamoja_core::Error::Closed) if the transport is
128    /// not connected.
129    async fn recv(&mut self) -> Result<Option<Message>> {
130        let incoming = self.incoming.as_mut().ok_or(Error::Closed)?;
131        Ok(incoming.recv().await)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[tokio::test]
140    async fn publish_and_subscribe_round_trip() {
141        let broker = LoopbackBroker::new();
142        let mut subscriber = LoopbackTransport::new(broker.clone());
143        let mut publisher = LoopbackTransport::new(broker);
144        subscriber.connect().await.expect("connect");
145        publisher.connect().await.expect("connect");
146
147        subscriber
148            .subscribe("sensors/+/temperature")
149            .await
150            .expect("subscribe");
151        publisher
152            .send("sensors/1/temperature", b"21.5")
153            .await
154            .expect("send");
155
156        let message = subscriber.recv().await.expect("recv").expect("a message");
157        assert_eq!(message.topic, "sensors/1/temperature");
158        assert_eq!(message.payload, b"21.5");
159    }
160
161    #[tokio::test]
162    async fn non_matching_topics_are_not_delivered() {
163        let broker = LoopbackBroker::new();
164        let mut subscriber = LoopbackTransport::new(broker.clone());
165        let mut publisher = LoopbackTransport::new(broker);
166        subscriber.connect().await.expect("connect");
167        publisher.connect().await.expect("connect");
168
169        subscriber
170            .subscribe("sensors/1/#")
171            .await
172            .expect("subscribe");
173        publisher
174            .send("sensors/2/temperature", b"x")
175            .await
176            .expect("send");
177        publisher
178            .send("sensors/1/humidity", b"y")
179            .await
180            .expect("send");
181
182        let message = subscriber.recv().await.expect("recv").expect("a message");
183        assert_eq!(message.topic, "sensors/1/humidity");
184    }
185
186    #[tokio::test]
187    async fn a_connected_link_waits_for_a_message_rather_than_ending() {
188        use std::future::Future;
189        use std::task::{Context, Waker};
190
191        let mut link = LoopbackTransport::new(LoopbackBroker::new());
192        link.connect().await.expect("connect");
193        link.subscribe("t").await.expect("subscribe");
194        {
195            let mut waiting = std::pin::pin!(link.recv());
196            let mut context = Context::from_waker(Waker::noop());
197            assert!(waiting.as_mut().poll(&mut context).is_pending());
198        }
199
200        link.send("t", b"1").await.expect("send");
201        let message = link.recv().await.expect("recv").expect("a message");
202        assert_eq!(message.payload, b"1", "a link hears its own publishes");
203    }
204
205    #[tokio::test]
206    async fn a_message_matching_two_filters_arrives_once() {
207        let broker = LoopbackBroker::new();
208        let mut subscriber = LoopbackTransport::new(broker.clone());
209        let mut publisher = LoopbackTransport::new(broker);
210        subscriber.connect().await.expect("connect");
211        publisher.connect().await.expect("connect");
212        subscriber.subscribe("a/+").await.expect("subscribe");
213        subscriber.subscribe("a/#").await.expect("subscribe");
214
215        publisher.send("a/b", b"1").await.expect("send");
216        publisher.send("a/c", b"2").await.expect("send");
217        let first = subscriber.recv().await.expect("recv").expect("a message");
218        let second = subscriber.recv().await.expect("recv").expect("a message");
219        assert_eq!(first.topic, "a/b");
220        assert_eq!(second.topic, "a/c");
221    }
222
223    #[tokio::test]
224    async fn a_subscription_hears_only_what_is_published_after_it() {
225        let broker = LoopbackBroker::new();
226        let mut subscriber = LoopbackTransport::new(broker.clone());
227        let mut publisher = LoopbackTransport::new(broker);
228        subscriber.connect().await.expect("connect");
229        publisher.connect().await.expect("connect");
230
231        publisher.send("t", b"before").await.expect("send");
232        subscriber.subscribe("t").await.expect("subscribe");
233        publisher.send("t", b"after").await.expect("send");
234        let message = subscriber.recv().await.expect("recv").expect("a message");
235        assert_eq!(message.payload, b"after");
236    }
237
238    #[tokio::test]
239    async fn a_reconnected_link_keeps_its_filters_and_drops_what_was_queued() {
240        let broker = LoopbackBroker::new();
241        let mut subscriber = LoopbackTransport::new(broker.clone());
242        let mut publisher = LoopbackTransport::new(broker);
243        subscriber.connect().await.expect("connect");
244        publisher.connect().await.expect("connect");
245        subscriber.subscribe("t").await.expect("subscribe");
246
247        publisher.send("t", b"queued").await.expect("send");
248        subscriber.disconnect();
249        publisher.send("t", b"missed").await.expect("send");
250        subscriber.connect().await.expect("reconnect");
251        publisher.send("t", b"heard").await.expect("send");
252        let message = subscriber.recv().await.expect("recv").expect("a message");
253        assert_eq!(message.payload, b"heard");
254    }
255
256    #[tokio::test]
257    async fn links_on_separate_brokers_never_hear_each_other() {
258        let mut subscriber = LoopbackTransport::new(LoopbackBroker::new());
259        let mut publisher = LoopbackTransport::new(LoopbackBroker::new());
260        subscriber.connect().await.expect("connect");
261        publisher.connect().await.expect("connect");
262        subscriber.subscribe("t").await.expect("subscribe");
263        publisher.send("t", b"elsewhere").await.expect("send");
264        subscriber.send("t", b"here").await.expect("send");
265        let message = subscriber.recv().await.expect("recv").expect("a message");
266        assert_eq!(message.payload, b"here");
267    }
268
269    #[tokio::test]
270    async fn an_unreachable_broker_refuses_its_links_until_it_is_back() {
271        let broker = LoopbackBroker::new();
272        let mut listener = LoopbackTransport::new(broker.clone());
273        let mut node = LoopbackTransport::new(broker.clone());
274        listener.connect().await.expect("connect");
275        node.connect().await.expect("connect");
276        listener.subscribe("t").await.expect("subscribe");
277
278        broker.set_reachable(false);
279        assert!(!broker.is_reachable());
280        for refused in [
281            node.send("t", b"lost").await,
282            node.subscribe("u").await,
283            LoopbackTransport::new(broker.clone()).connect().await,
284        ] {
285            match refused {
286                Err(Error::Transport(reason)) => assert_eq!(reason, "the broker is out of reach"),
287                other => panic!("an unreachable broker allowed {other:?}"),
288            }
289        }
290        assert!(node.is_connected(), "an outage keeps the connection");
291
292        broker.set_reachable(true);
293        node.send("t", b"back").await.expect("send");
294        let message = listener.recv().await.expect("recv").expect("a message");
295        assert_eq!(
296            message.payload, b"back",
297            "nothing sent during the outage arrives"
298        );
299    }
300
301    #[tokio::test]
302    async fn operations_before_connect_report_closed() {
303        let broker = LoopbackBroker::new();
304        let mut transport = LoopbackTransport::new(broker);
305        assert!(matches!(
306            transport.send("t", b"x").await,
307            Err(Error::Closed)
308        ));
309        assert!(matches!(transport.subscribe("t").await, Err(Error::Closed)));
310        assert!(matches!(transport.recv().await, Err(Error::Closed)));
311        assert!(!transport.is_connected());
312    }
313}