Skip to main content

looprs_core/adapters/
channel_broker.rs

1//! ChannelBroker adapter — `MessageBroker` port backed by `tokio::sync::broadcast`.
2//!
3//! Each topic gets its own broadcast channel with capacity 64. Publishers fan-out
4//! to all subscribers; slow subscribers lag (messages are dropped) rather than
5//! blocking the publisher.
6
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9
10use tokio::sync::broadcast;
11
12use crate::ports::{Message, MessageBroker};
13
14const CHANNEL_CAPACITY: usize = 64;
15
16/// Thread-safe, cheaply cloneable pub/sub broker backed by tokio broadcast channels.
17///
18/// # Example
19///
20/// ```rust
21/// use looprs_core::adapters::channel_broker::ChannelBroker;
22/// use looprs_core::ports::{Message, MessageBroker};
23///
24/// let broker = ChannelBroker::new();
25/// let mut rx = broker.subscribe("my.topic");
26/// broker.publish(Message::new("test", "my.topic", 1, serde_json::Value::Null));
27/// // rx.try_recv() would return the message
28/// ```
29#[derive(Clone)]
30pub struct ChannelBroker {
31    inner: Arc<BrokerInner>,
32}
33
34struct BrokerInner {
35    /// Topic → broadcast sender.  A new sender is created on first subscribe/publish.
36    topics: Mutex<HashMap<String, broadcast::Sender<Message>>>,
37    closed: Mutex<bool>,
38}
39
40impl ChannelBroker {
41    /// Create a new, open broker.
42    pub fn new() -> Self {
43        Self {
44            inner: Arc::new(BrokerInner {
45                topics: Mutex::new(HashMap::new()),
46                closed: Mutex::new(false),
47            }),
48        }
49    }
50
51    /// Return (or create) the sender for `topic`.
52    fn sender_for(&self, topic: &str) -> Option<broadcast::Sender<Message>> {
53        let closed = self.inner.closed.lock().expect("closed lock poisoned");
54        if *closed {
55            return None;
56        }
57        drop(closed);
58
59        let mut topics = self.inner.topics.lock().expect("topics lock poisoned");
60        Some(
61            topics
62                .entry(topic.to_owned())
63                .or_insert_with(|| broadcast::channel(CHANNEL_CAPACITY).0)
64                .clone(),
65        )
66    }
67}
68
69impl Default for ChannelBroker {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl MessageBroker for ChannelBroker {
76    fn publish(&self, msg: Message) -> usize {
77        let topic = msg.topic.clone();
78        match self.sender_for(&topic) {
79            None => 0,
80            Some(tx) => tx.send(msg).unwrap_or_default(),
81        }
82    }
83
84    fn subscribe(&self, topic: &str) -> broadcast::Receiver<Message> {
85        match self.sender_for(topic) {
86            Some(tx) => tx.subscribe(),
87            None => {
88                // Broker is closed — return a receiver that immediately sees Closed.
89                let (tx, rx) = broadcast::channel(1);
90                drop(tx);
91                rx
92            }
93        }
94    }
95
96    fn close(&self) {
97        let mut closed = self.inner.closed.lock().expect("closed lock poisoned");
98        *closed = true;
99        // Dropping all senders causes receivers to drain then see RecvError::Closed.
100        let mut topics = self.inner.topics.lock().expect("topics lock poisoned");
101        topics.clear();
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use tokio::sync::broadcast::error::TryRecvError;
109
110    #[test]
111    fn publish_to_subscriber() {
112        let broker = ChannelBroker::new();
113        let mut rx = broker.subscribe("a");
114
115        let msg = Message::new("src", "a", 1, serde_json::json!({"x": 1}));
116        let n = broker.publish(msg.clone());
117
118        assert_eq!(n, 1);
119        let received = rx.try_recv().expect("should have message");
120        assert_eq!(received.topic, "a");
121        assert_eq!(received.source, "src");
122    }
123
124    #[test]
125    fn no_subscribers_returns_zero() {
126        let broker = ChannelBroker::new();
127        let n = broker.publish(Message::new(
128            "src",
129            "unsubscribed",
130            1,
131            serde_json::Value::Null,
132        ));
133        assert_eq!(n, 0);
134    }
135
136    #[test]
137    fn multiple_subscribers_same_topic() {
138        let broker = ChannelBroker::new();
139        let mut rx1 = broker.subscribe("t");
140        let mut rx2 = broker.subscribe("t");
141
142        let n = broker.publish(Message::new("src", "t", 1, serde_json::Value::Null));
143        assert_eq!(n, 2);
144        assert!(rx1.try_recv().is_ok());
145        assert!(rx2.try_recv().is_ok());
146    }
147
148    #[test]
149    fn independent_topics_do_not_cross() {
150        let broker = ChannelBroker::new();
151        let mut rx_a = broker.subscribe("a");
152        let mut _rx_b = broker.subscribe("b");
153
154        broker.publish(Message::new("src", "a", 1, serde_json::Value::Null));
155
156        assert!(rx_a.try_recv().is_ok());
157        // rx_b should see nothing
158        assert!(matches!(_rx_b.try_recv(), Err(TryRecvError::Empty)));
159    }
160
161    #[test]
162    fn close_makes_receiver_see_closed() {
163        let broker = ChannelBroker::new();
164        let mut rx = broker.subscribe("c");
165        broker.close();
166
167        // After close, publishing is a no-op.
168        let n = broker.publish(Message::new("src", "c", 1, serde_json::Value::Null));
169        assert_eq!(n, 0);
170
171        // Receiver sees Closed.
172        assert!(matches!(rx.try_recv(), Err(TryRecvError::Closed)));
173    }
174
175    #[test]
176    fn clone_shares_state() {
177        let broker = ChannelBroker::new();
178        let broker2 = broker.clone();
179        let mut rx = broker.subscribe("shared");
180
181        broker2.publish(Message::new("b2", "shared", 1, serde_json::Value::Null));
182        assert!(rx.try_recv().is_ok());
183    }
184
185    #[test]
186    fn conformance() {
187        crate::ports::test_contracts::assert_message_broker_contract(ChannelBroker::new());
188    }
189}