1use crate::pubsub::index::{PeerKey, SubscriptionIndex};
15use crate::router::PeerCmd;
16
17use bytes::Bytes;
18use flume::{Receiver, Sender};
19use hashbrown::HashMap;
20use std::collections::HashMap as StdHashMap;
21use std::collections::hash_map::RandomState;
22use std::sync::Arc;
23
24type RidMap<V> = StdHashMap<Bytes, V, RandomState>;
31
32#[derive(Debug)]
34pub enum PubSubCmd {
35 Publish(Vec<Bytes>),
37 Close,
39}
40
41#[derive(Debug)]
48pub enum PubSubEvent {
49 PeerUp {
50 routing_id: Bytes,
51 epoch: u64,
52 tx: Sender<PeerCmd>,
53 },
54 PeerDown {
55 routing_id: Bytes,
56 epoch: u64,
57 },
58 Subscribe {
59 routing_id: Bytes,
60 prefix: Bytes,
61 },
62 Unsubscribe {
63 routing_id: Bytes,
64 prefix: Bytes,
65 },
66}
67
68pub struct PubSubHub {
73 index: SubscriptionIndex,
75
76 rid_to_key: RidMap<PeerKey>,
78
79 key_to_rid: HashMap<PeerKey, Bytes>,
81
82 peers: HashMap<PeerKey, (u64, Sender<PeerCmd>)>,
84
85 next_key: PeerKey,
87
88 hub_rx: Receiver<PubSubEvent>,
90
91 user_tx_rx: Receiver<PubSubCmd>,
93}
94
95impl PubSubHub {
96 #[must_use]
97 pub fn new(hub_rx: Receiver<PubSubEvent>, user_tx_rx: Receiver<PubSubCmd>) -> Self {
98 Self {
99 index: SubscriptionIndex::new(),
100 rid_to_key: RidMap::default(),
101 key_to_rid: HashMap::new(),
102 peers: HashMap::new(),
103 next_key: 1, hub_rx,
105 user_tx_rx,
106 }
107 }
108
109 pub async fn run(mut self) {
111 use futures::FutureExt;
112 use futures::select;
113
114 loop {
115 select! {
117 msg = self.hub_rx.recv_async().fuse() => {
118 match msg {
119 Ok(ev) => self.on_hub_event(ev),
120 Err(_) => break, }
122 }
123 msg = self.user_tx_rx.recv_async().fuse() => {
124 match msg {
125 Ok(cmd) => self.on_user_cmd(cmd),
126 Err(_) => break, }
128 }
129 }
130 }
131 }
132
133 fn on_hub_event(&mut self, ev: PubSubEvent) {
134 match ev {
135 PubSubEvent::PeerUp {
136 routing_id,
137 epoch,
138 tx,
139 } => {
140 let key = if let Some(&k) = self.rid_to_key.get(&routing_id) {
142 k
143 } else {
144 let k = self.next_key;
145 self.next_key += 1;
146 self.key_to_rid.insert(k, routing_id.clone());
148 self.rid_to_key.insert(routing_id, k);
149 k
150 };
151
152 self.peers.insert(key, (epoch, tx));
154 }
155
156 PubSubEvent::PeerDown { routing_id, epoch } => {
157 if let Some(&key) = self.rid_to_key.get(&routing_id)
158 && let Some((current_epoch, _)) = self.peers.get(&key)
159 && *current_epoch == epoch
161 {
162 self.peers.remove(&key);
163 self.index.remove_peer_everywhere(key);
164 }
165 }
166
167 PubSubEvent::Subscribe { routing_id, prefix } => {
168 if let Some(&key) = self.rid_to_key.get(&routing_id)
169 && self.peers.contains_key(&key)
170 {
171 self.index.subscribe(key, prefix);
172 }
173 }
174
175 PubSubEvent::Unsubscribe { routing_id, prefix } => {
176 if let Some(&key) = self.rid_to_key.get(&routing_id) {
177 self.index.unsubscribe(key, &prefix);
178 }
179 }
180 }
181 }
182
183 fn on_user_cmd(&mut self, cmd: PubSubCmd) {
184 match cmd {
185 PubSubCmd::Publish(parts) => self.publish(parts),
186 PubSubCmd::Close => {
187 for (_, (_, tx)) in &self.peers {
189 let _ = tx.send(PeerCmd::Close);
190 }
191 }
192 }
193 }
194
195 fn publish(&mut self, parts: Vec<Bytes>) {
200 if parts.is_empty() || self.index.is_empty() {
201 return;
202 }
203
204 let topic = &parts[0];
205 let keys = self.index.match_topic(topic);
206
207 if keys.is_empty() {
208 return;
209 }
210
211 let msg = Arc::new(parts);
215 for key in keys {
216 if let Some((_, tx)) = self.peers.get(&key) {
217 let _ = tx.send(PeerCmd::SendBody(Arc::clone(&msg)));
218 }
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use std::time::Duration;
227
228 fn b(s: &str) -> Bytes {
229 Bytes::copy_from_slice(s.as_bytes())
230 }
231
232 async fn recv_arc(rx: &Receiver<PeerCmd>) -> Option<Arc<Vec<Bytes>>> {
234 match crate::rt::timeout(Duration::from_secs(1), rx.recv_async()).await {
235 Ok(Ok(PeerCmd::SendBody(parts))) => Some(parts),
236 _ => None,
237 }
238 }
239
240 async fn expect_no_body(rx: &Receiver<PeerCmd>) -> bool {
242 !matches!(
243 crate::rt::timeout(Duration::from_millis(150), rx.recv_async()).await,
244 Ok(Ok(PeerCmd::SendBody(_)))
245 )
246 }
247
248 #[test]
249 fn publishes_only_to_matching_subscribers() {
250 crate::rt::LocalRuntime::new().unwrap().block_on(async {
251 let (hub_tx, hub_rx) = flume::unbounded::<PubSubEvent>();
252 let (user_tx, user_rx) = flume::unbounded::<PubSubCmd>();
253 let hub = PubSubHub::new(hub_rx, user_rx);
254 let handle = crate::rt::spawn(hub.run());
255
256 let (peer_tx, peer_rx) = flume::unbounded::<PeerCmd>();
257 hub_tx
258 .send(PubSubEvent::PeerUp {
259 routing_id: b("sub1"),
260 epoch: 1,
261 tx: peer_tx,
262 })
263 .unwrap();
264 hub_tx
265 .send(PubSubEvent::Subscribe {
266 routing_id: b("sub1"),
267 prefix: b("weather."),
268 })
269 .unwrap();
270 crate::rt::sleep(Duration::from_millis(30)).await;
271
272 user_tx
274 .send(PubSubCmd::Publish(vec![b("weather.london"), b("sunny")]))
275 .unwrap();
276 let got = recv_arc(&peer_rx).await.expect("matching topic delivered");
277 assert_eq!(*got, vec![b("weather.london"), b("sunny")]);
278
279 user_tx
281 .send(PubSubCmd::Publish(vec![b("stocks.aapl"), b("100")]))
282 .unwrap();
283 assert!(
284 expect_no_body(&peer_rx).await,
285 "non-matching topic must not be delivered"
286 );
287
288 drop(hub_tx);
289 drop(user_tx);
290 crate::rt::join(handle).await;
291 });
292 }
293
294 #[test]
295 fn fanout_shares_one_allocation_across_peers() {
296 crate::rt::LocalRuntime::new().unwrap().block_on(async {
297 let (hub_tx, hub_rx) = flume::unbounded::<PubSubEvent>();
298 let (user_tx, user_rx) = flume::unbounded::<PubSubCmd>();
299 let hub = PubSubHub::new(hub_rx, user_rx);
300 let handle = crate::rt::spawn(hub.run());
301
302 let (p1_tx, p1_rx) = flume::unbounded::<PeerCmd>();
303 let (p2_tx, p2_rx) = flume::unbounded::<PeerCmd>();
304 for (rid, tx) in [(b("s1"), p1_tx), (b("s2"), p2_tx)] {
305 hub_tx
306 .send(PubSubEvent::PeerUp {
307 routing_id: rid.clone(),
308 epoch: 1,
309 tx,
310 })
311 .unwrap();
312 hub_tx
313 .send(PubSubEvent::Subscribe {
314 routing_id: rid,
315 prefix: b(""), })
317 .unwrap();
318 }
319 crate::rt::sleep(Duration::from_millis(30)).await;
320
321 user_tx
322 .send(PubSubCmd::Publish(vec![b("news"), b("hello")]))
323 .unwrap();
324
325 let a = recv_arc(&p1_rx).await.expect("peer 1 delivered");
326 let c = recv_arc(&p2_rx).await.expect("peer 2 delivered");
327 assert_eq!(*a, vec![b("news"), b("hello")]);
328 assert_eq!(*c, vec![b("news"), b("hello")]);
329 assert!(
332 Arc::ptr_eq(&a, &c),
333 "fan-out must share one Arc allocation across peers"
334 );
335
336 drop(hub_tx);
337 drop(user_tx);
338 crate::rt::join(handle).await;
339 });
340 }
341
342 #[test]
343 fn peer_down_with_stale_epoch_is_ignored() {
344 crate::rt::LocalRuntime::new().unwrap().block_on(async {
345 let (hub_tx, hub_rx) = flume::unbounded::<PubSubEvent>();
346 let (user_tx, user_rx) = flume::unbounded::<PubSubCmd>();
347 let hub = PubSubHub::new(hub_rx, user_rx);
348 let handle = crate::rt::spawn(hub.run());
349
350 let (peer_tx, peer_rx) = flume::unbounded::<PeerCmd>();
351 hub_tx
352 .send(PubSubEvent::PeerUp {
353 routing_id: b("sub1"),
354 epoch: 2,
355 tx: peer_tx,
356 })
357 .unwrap();
358 hub_tx
359 .send(PubSubEvent::Subscribe {
360 routing_id: b("sub1"),
361 prefix: b(""),
362 })
363 .unwrap();
364 hub_tx
366 .send(PubSubEvent::PeerDown {
367 routing_id: b("sub1"),
368 epoch: 1,
369 })
370 .unwrap();
371 crate::rt::sleep(Duration::from_millis(30)).await;
372
373 user_tx
374 .send(PubSubCmd::Publish(vec![b("x"), b("still-here")]))
375 .unwrap();
376 let got = recv_arc(&peer_rx)
377 .await
378 .expect("stale-epoch PeerDown must not evict the peer");
379 assert_eq!(*got, vec![b("x"), b("still-here")]);
380
381 drop(hub_tx);
382 drop(user_tx);
383 crate::rt::join(handle).await;
384 });
385 }
386}