pamoja_loopback/broker.rs
1//! The shared in-memory broker that routes loopback messages.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5
6use pamoja_core::{topic_matches, Error, Message, Result};
7use tokio::sync::mpsc::UnboundedSender;
8
9/// A shared, in-process router for [`LoopbackTransport`](crate::LoopbackTransport)s.
10///
11/// Clone a single broker into every transport that should share a namespace; a
12/// publish on one transport is delivered to every transport whose subscriptions
13/// match the topic. The broker is cheap to clone, and all clones share one
14/// routing table.
15///
16/// A broker stands for the network its links share, so taking it out of reach
17/// with [`set_reachable`](LoopbackBroker::set_reachable) is how a test puts every
18/// link on it out of range at once, including links a ladder now owns.
19#[derive(Clone)]
20pub struct LoopbackBroker {
21 subscriptions: Arc<Mutex<Vec<Subscription>>>,
22 reachable: Arc<AtomicBool>,
23}
24
25/// One connected transport's topic filters and delivery channel.
26struct Subscription {
27 filters: Arc<Mutex<Vec<String>>>,
28 sender: UnboundedSender<Message>,
29}
30
31impl Default for LoopbackBroker {
32 fn default() -> Self {
33 Self {
34 subscriptions: Arc::default(),
35 reachable: Arc::new(AtomicBool::new(true)),
36 }
37 }
38}
39
40impl LoopbackBroker {
41 /// Creates an empty broker.
42 ///
43 /// # Returns
44 ///
45 /// A broker with no registered transports, in reach.
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 /// Takes the broker out of reach, or brings it back.
51 ///
52 /// Out of reach, every link on the broker fails to connect, send, or
53 /// subscribe with [`Error::Transport`], the error an out-of-range radio or a
54 /// dropped network raises, and nothing is delivered. A link keeps its
55 /// connection and its filters through the outage, so it carries traffic again
56 /// as soon as the broker is back.
57 ///
58 /// # Arguments
59 ///
60 /// * `reachable` - `false` to take the broker out of reach, `true` to bring it
61 /// back.
62 pub fn set_reachable(&self, reachable: bool) {
63 self.reachable.store(reachable, Ordering::SeqCst);
64 }
65
66 /// Reports whether the broker is in reach.
67 ///
68 /// # Returns
69 ///
70 /// `true` unless [`set_reachable`](LoopbackBroker::set_reachable) took it out.
71 pub fn is_reachable(&self) -> bool {
72 self.reachable.load(Ordering::SeqCst)
73 }
74
75 /// Fails with the error a link on an unreachable broker reports.
76 pub(crate) fn in_reach(&self) -> Result<()> {
77 if self.is_reachable() {
78 Ok(())
79 } else {
80 Err(Error::Transport("the broker is out of reach".to_owned()))
81 }
82 }
83
84 /// Registers a transport's filters and delivery channel.
85 pub(crate) fn register(
86 &self,
87 filters: Arc<Mutex<Vec<String>>>,
88 sender: UnboundedSender<Message>,
89 ) {
90 self.subscriptions
91 .lock()
92 .expect("broker lock")
93 .push(Subscription { filters, sender });
94 }
95
96 /// Delivers a message to every subscription whose filters match its topic,
97 /// pruning channels whose receiver has been dropped.
98 pub(crate) fn publish(&self, message: &Message) {
99 let mut subscriptions = self.subscriptions.lock().expect("broker lock");
100 subscriptions.retain(|subscription| {
101 if subscription.sender.is_closed() {
102 return false;
103 }
104 let matched = subscription
105 .filters
106 .lock()
107 .expect("filters lock")
108 .iter()
109 .any(|filter| topic_matches(filter, &message.topic));
110 if matched {
111 let _ = subscription.sender.send(message.clone());
112 }
113 true
114 });
115 }
116}