1use async_trait::async_trait;
49use bytes::Bytes;
50use klieo_core::bus::{AckHandle, AckHandleImpl, Headers, Msg, MsgStream, Pubsub};
51use klieo_core::error::BusError;
52use klieo_core::ids::DurableName;
53use std::collections::HashMap;
54use std::sync::Arc;
55use std::time::Duration;
56use tokio::sync::{broadcast, Mutex};
57use tokio_stream::wrappers::BroadcastStream;
58use tokio_stream::StreamExt;
59
60const DEFAULT_CAPACITY: usize = 1024;
61
62type PatternSender = broadcast::Sender<(String, Bytes, Headers)>;
63
64struct State {
65 concretes: HashMap<String, PatternSender>,
72 wildcards: Vec<(String, PatternSender)>,
76 capacity: usize,
78}
79
80impl Default for State {
81 fn default() -> Self {
82 Self {
83 concretes: HashMap::new(),
84 wildcards: Vec::new(),
85 capacity: DEFAULT_CAPACITY,
86 }
87 }
88}
89
90fn is_wildcard_pattern(pattern: &str) -> bool {
95 pattern.contains('*') || pattern.contains('>')
96}
97
98#[derive(Clone)]
100pub struct MemoryPubsub {
101 state: Arc<Mutex<State>>,
102}
103
104impl MemoryPubsub {
105 pub fn new() -> Self {
107 Self::with_buffer_size(DEFAULT_CAPACITY)
108 }
109
110 pub fn with_buffer_size(buffer: usize) -> Self {
115 Self {
116 state: Arc::new(Mutex::new(State {
117 concretes: HashMap::new(),
118 wildcards: Vec::new(),
119 capacity: buffer.max(1),
120 })),
121 }
122 }
123
124 async fn subscribe_sender(&self, pattern: &str) -> PatternSender {
131 let mut g = self.state.lock().await;
132 let cap = g.capacity;
133 if is_wildcard_pattern(pattern) {
134 if let Some((_, tx)) = g.wildcards.iter().find(|(p, _)| p == pattern) {
135 return tx.clone();
136 }
137 let (tx, _rx) = broadcast::channel(cap);
138 g.wildcards.push((pattern.to_string(), tx.clone()));
139 tx
140 } else {
141 g.concretes
142 .entry(pattern.to_string())
143 .or_insert_with(|| {
144 let (tx, _rx) = broadcast::channel(cap);
145 tx
146 })
147 .clone()
148 }
149 }
150
151 pub async fn remove_subject(&self, pattern: &str) {
160 let mut g = self.state.lock().await;
161 if g.concretes.remove(pattern).is_some() {
162 return;
163 }
164 g.wildcards.retain(|(p, _)| p != pattern);
165 }
166
167 pub async fn subject_count(&self) -> usize {
171 let g = self.state.lock().await;
172 g.concretes.len() + g.wildcards.len()
173 }
174}
175
176impl Default for MemoryPubsub {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182#[async_trait]
183impl Pubsub for MemoryPubsub {
184 async fn publish(
185 &self,
186 subject: &str,
187 payload: Bytes,
188 headers: Headers,
189 ) -> Result<(), BusError> {
190 let matched: Vec<PatternSender> = {
196 let g = self.state.lock().await;
197 let mut out: Vec<PatternSender> = Vec::new();
198 if let Some(tx) = g.concretes.get(subject) {
199 out.push(tx.clone());
200 }
201 for (pattern, tx) in &g.wildcards {
202 if subject_matches(pattern, subject) {
203 out.push(tx.clone());
204 }
205 }
206 out
207 };
208 for tx in matched {
209 let _ = tx.send((subject.to_string(), payload.clone(), headers.clone()));
212 }
213 Ok(())
214 }
215
216 async fn subscribe(&self, subject: &str, _durable: DurableName) -> Result<MsgStream, BusError> {
217 let tx = self.subscribe_sender(subject).await;
218 let rx = tx.subscribe();
219 let pattern = subject.to_string();
220 let stream = BroadcastStream::new(rx).map(move |res| match res {
221 Ok((concrete_subject, payload, headers)) => Ok(Msg {
222 subject: concrete_subject,
227 payload,
228 headers,
229 ack: AckHandle::new(Box::new(NoopAck)),
230 }),
231 Err(_) => {
232 tracing::warn!(
233 target: "klieo.bus.memory",
234 pattern = %pattern,
235 "subscriber lagged - increase MemoryPubsub buffer size or speed up consumer"
236 );
237 Err(BusError::Retryable("subscriber lagged".into()))
238 }
239 });
240 Ok(Box::pin(stream))
241 }
242}
243
244fn subject_matches(pattern: &str, concrete: &str) -> bool {
254 if pattern.is_empty() || concrete.is_empty() {
255 return false;
256 }
257 let mut p = pattern.split('.');
258 let mut c = concrete.split('.');
259 loop {
260 match (p.next(), c.next()) {
261 (Some(""), _) | (_, Some("")) => return false,
264 (Some(">"), Some(_)) => return p.next().is_none(),
268 (Some(">"), None) => return false,
271 (Some("*"), Some(_)) => continue,
273 (Some("*"), None) => return false,
274 (Some(pt), Some(ct)) if pt == ct => continue,
276 (Some(_), Some(_)) => return false,
277 (None, None) => return true,
279 (Some(_), None) | (None, Some(_)) => return false,
282 }
283 }
284}
285
286pub(crate) struct NoopAck;
289
290#[async_trait]
291impl AckHandleImpl for NoopAck {
292 async fn ack(self: Box<Self>) -> Result<(), BusError> {
293 Ok(())
294 }
295 async fn nak(self: Box<Self>, _delay: Duration) -> Result<(), BusError> {
296 Ok(())
297 }
298 async fn term(self: Box<Self>) -> Result<(), BusError> {
299 Ok(())
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use klieo_core::Pubsub;
307 use tokio_stream::StreamExt;
308
309 #[tokio::test]
310 async fn publish_delivers_to_subscriber() {
311 let bus = MemoryPubsub::new();
312 let mut sub = bus
313 .subscribe("subject.a", DurableName::new("d1"))
314 .await
315 .unwrap();
316 bus.publish("subject.a", Bytes::from_static(b"hi"), Headers::new())
317 .await
318 .unwrap();
319 let msg = sub.next().await.unwrap().unwrap();
320 assert_eq!(msg.subject, "subject.a");
321 assert_eq!(msg.payload, Bytes::from_static(b"hi"));
322 msg.ack.ack().await.unwrap();
324 }
325
326 #[tokio::test]
327 async fn two_subscribers_both_receive() {
328 let bus = MemoryPubsub::new();
329 let mut s1 = bus
330 .subscribe("subject.b", DurableName::new("d1"))
331 .await
332 .unwrap();
333 let mut s2 = bus
334 .subscribe("subject.b", DurableName::new("d2"))
335 .await
336 .unwrap();
337 bus.publish("subject.b", Bytes::from_static(b"x"), Headers::new())
338 .await
339 .unwrap();
340 assert_eq!(
341 s1.next().await.unwrap().unwrap().payload,
342 Bytes::from_static(b"x")
343 );
344 assert_eq!(
345 s2.next().await.unwrap().unwrap().payload,
346 Bytes::from_static(b"x")
347 );
348 }
349
350 #[tokio::test]
351 async fn publish_with_no_subscribers_succeeds() {
352 let bus = MemoryPubsub::new();
353 bus.publish("nobody.home", Bytes::from_static(b"x"), Headers::new())
354 .await
355 .unwrap();
356 }
357
358 #[tokio::test]
359 async fn headers_preserved() {
360 let bus = MemoryPubsub::new();
361 let mut sub = bus
362 .subscribe("subject.h", DurableName::new("d"))
363 .await
364 .unwrap();
365 let mut h = Headers::new();
366 h.insert("k".into(), "v".into());
367 bus.publish("subject.h", Bytes::from_static(b"hi"), h)
368 .await
369 .unwrap();
370 let msg = sub.next().await.unwrap().unwrap();
371 assert_eq!(msg.headers.get("k").map(|s| s.as_str()), Some("v"));
372 }
373
374 #[tokio::test]
375 async fn small_buffer_lags_on_burst() {
376 let bus = MemoryPubsub::with_buffer_size(2);
377 let mut sub = bus
378 .subscribe("subject.lag", DurableName::new("d"))
379 .await
380 .unwrap();
381 for i in 0..4u8 {
383 bus.publish("subject.lag", Bytes::from(vec![i]), Headers::new())
384 .await
385 .unwrap();
386 }
387 let mut saw_lag = false;
390 for _ in 0..6 {
391 let next = sub.next().await.unwrap();
392 if let Err(BusError::Retryable(ref m)) = next {
393 if m.contains("lagged") {
394 saw_lag = true;
395 break;
396 }
397 }
398 }
399 assert!(saw_lag, "expected lagged err within the burst");
400 }
401
402 #[tokio::test]
403 async fn large_buffer_does_not_lag() {
404 let bus = MemoryPubsub::with_buffer_size(64);
405 let mut sub = bus
406 .subscribe("subject.nolag", DurableName::new("d"))
407 .await
408 .unwrap();
409 for i in 0..10u8 {
410 bus.publish("subject.nolag", Bytes::from(vec![i]), Headers::new())
411 .await
412 .unwrap();
413 }
414 for _ in 0..10 {
415 let msg = sub.next().await.unwrap().expect("no lag");
416 let _ = msg.payload;
417 }
418 }
419
420 #[test]
421 fn concrete_matches_self() {
422 assert!(subject_matches("a.b.c", "a.b.c"));
423 assert!(!subject_matches("a.b.c", "a.b"));
424 assert!(!subject_matches("a.b.c", "a.b.c.d"));
425 assert!(!subject_matches("a.b.c", "a.x.c"));
426 }
427
428 #[test]
429 fn single_token_wildcard() {
430 assert!(subject_matches("a.*.c", "a.x.c"));
431 assert!(subject_matches("a.*.c", "a.42.c"));
432 assert!(!subject_matches("a.*.c", "a.b.c.d"));
433 assert!(!subject_matches("a.*.c", "a.c"));
434 assert!(!subject_matches("*", "a.b"));
435 assert!(subject_matches("*", "a"));
436 }
437
438 #[test]
439 fn greedy_wildcard_matches_remaining_tokens() {
440 assert!(subject_matches("a.>", "a.b"));
441 assert!(subject_matches("a.>", "a.b.c.d"));
442 assert!(!subject_matches("a.>", "a"));
443 assert!(!subject_matches("a.>", "b.x"));
444 assert!(subject_matches("a.b.>", "a.b.x"));
446 assert!(!subject_matches("a.b.>", "a.b"));
447 }
448
449 #[test]
450 fn greedy_only_valid_as_last_token() {
451 assert!(!subject_matches("a.>.c", "a.x.c"));
453 assert!(!subject_matches("a.>.c", "a.b.c"));
454 assert!(!subject_matches("a.>.c", "anything"));
455 assert!(!subject_matches(">.a", "x.a"));
456 }
457
458 #[test]
459 fn bare_greedy_matches_anything_with_one_plus_token() {
460 assert!(subject_matches(">", "a"));
461 assert!(subject_matches(">", "a.b.c"));
462 assert!(!subject_matches(">", ""));
463 }
464
465 #[test]
466 fn prefix_does_not_partially_match() {
467 assert!(!subject_matches("a.b", "a"));
468 assert!(!subject_matches("a.b", "a.b.c"));
469 }
470
471 #[test]
472 fn empty_token_rejected() {
473 assert!(!subject_matches("a..b", "a..b"));
474 assert!(!subject_matches("a..b", "a.x.b"));
475 assert!(!subject_matches("a.b", ""));
476 assert!(!subject_matches("", "a.b"));
477 assert!(!subject_matches(".a", "x.a"));
478 assert!(!subject_matches("a.", "a.x"));
479 }
480
481 #[tokio::test]
482 async fn greedy_wildcard_subscriber_receives_matching_publishes() {
483 let bus = MemoryPubsub::new();
484 let mut sub = bus
485 .subscribe("klieo.a2a.cancel.>", DurableName::new("d"))
486 .await
487 .unwrap();
488 bus.publish(
489 "klieo.a2a.cancel.t-1",
490 Bytes::from_static(b"signal"),
491 Headers::new(),
492 )
493 .await
494 .unwrap();
495 let msg = sub.next().await.unwrap().unwrap();
496 assert_eq!(msg.subject, "klieo.a2a.cancel.t-1");
497 assert_eq!(msg.payload, Bytes::from_static(b"signal"));
498 }
499}