Skip to main content

rskit_messaging/
memory.rs

1//! In-memory message broker, producer, and consumer for testing.
2
3use std::collections::{HashSet, VecDeque};
4use std::sync::Arc;
5use std::time::Duration;
6
7use async_trait::async_trait;
8use rskit_errors::{AppError, AppResult, ErrorCode};
9use tokio::sync::{Mutex, broadcast};
10
11use crate::config::BrokerConfig;
12use crate::event::Event;
13use crate::message::Message;
14use crate::registry::{MessagingFactory, MessagingRegistry};
15use crate::traits::{EventConsumer, EventProducer, MessageConsumer, MessageProducer};
16
17const ADAPTER_NAME: &str = "memory";
18
19/// An in-memory message broker backed by a `tokio::sync::broadcast` channel.
20///
21/// Create one broker and hand out producers / consumers via
22/// [`InMemoryBroker::producer`] and [`InMemoryBroker::consumer`].
23///
24/// Every message sent through the broker is recorded in an internal history
25/// so that test assertion helpers can inspect what was published.
26#[derive(Debug, Clone)]
27pub struct InMemoryBroker<T: Clone + Send + Sync + 'static> {
28    tx: broadcast::Sender<Message<T>>,
29    history: Arc<Mutex<VecDeque<Message<T>>>>,
30    history_limit: Option<usize>,
31    topics: Arc<Mutex<HashSet<String>>>,
32    /// Notified after every publish so that [`wait_for_message`] can wake
33    /// without polling.
34    notify: Arc<tokio::sync::Notify>,
35}
36
37impl<T: Clone + Send + Sync + 'static> InMemoryBroker<T> {
38    /// Create a broker with the given channel capacity.
39    pub fn new(capacity: usize) -> Self {
40        let limit = capacity.max(1);
41        let (tx, _) = broadcast::channel(limit);
42        Self {
43            tx,
44            history: Arc::new(Mutex::new(VecDeque::with_capacity(limit))),
45            history_limit: Some(limit),
46            topics: Arc::new(Mutex::new(HashSet::new())),
47            notify: Arc::new(tokio::sync::Notify::new()),
48        }
49    }
50
51    /// Create a producer attached to this broker.
52    pub fn producer(&self) -> InMemoryProducer<T> {
53        InMemoryProducer {
54            tx: self.tx.clone(),
55            history: self.history.clone(),
56            history_limit: self.history_limit,
57            topics: self.topics.clone(),
58            notify: self.notify.clone(),
59        }
60    }
61
62    /// Create a broker with explicit channel capacity and bounded history limit.
63    ///
64    /// The default [`InMemoryBroker::new`] bounds history by channel capacity. Use this
65    /// constructor when tests need a different history limit.
66    #[must_use]
67    pub fn with_history_limit(capacity: usize, history_limit: usize) -> Self {
68        let capacity = capacity.max(1);
69        let limit = history_limit.max(1);
70        let (tx, _) = broadcast::channel(capacity);
71        Self {
72            tx,
73            history: Arc::new(Mutex::new(VecDeque::with_capacity(limit))),
74            history_limit: Some(limit),
75            topics: Arc::new(Mutex::new(HashSet::new())),
76            notify: Arc::new(tokio::sync::Notify::new()),
77        }
78    }
79
80    /// Create a consumer attached to this broker.
81    pub fn consumer(&self) -> InMemoryConsumer<T> {
82        InMemoryConsumer {
83            rx: Arc::new(Mutex::new(self.tx.subscribe())),
84            topics: Arc::new(Mutex::new(HashSet::new())),
85        }
86    }
87
88    /// Return a clone of all messages published to `topic`.
89    pub async fn messages(&self, topic: &str) -> Vec<Message<T>> {
90        self.history
91            .lock()
92            .await
93            .iter()
94            .filter(|m| m.topic == topic)
95            .cloned()
96            .collect()
97    }
98
99    /// Return a clone of every message published to any topic.
100    pub async fn all_messages(&self) -> Vec<Message<T>> {
101        self.history.lock().await.iter().cloned().collect()
102    }
103
104    /// Return the number of messages published to `topic`.
105    pub async fn message_count(&self, topic: &str) -> usize {
106        self.history
107            .lock()
108            .await
109            .iter()
110            .filter(|m| m.topic == topic)
111            .count()
112    }
113
114    /// Clear the recorded message history.
115    pub async fn reset(&self) {
116        self.history.lock().await.clear();
117    }
118
119    /// Pre-create a topic so that it appears in [`InMemoryBroker::topic_names`].
120    pub async fn create_topic(&self, topic: &str) {
121        self.topics.lock().await.insert(topic.to_string());
122    }
123
124    /// Return the sorted set of topic names that have been created or published to.
125    pub async fn topic_names(&self) -> Vec<String> {
126        let mut set: HashSet<String> = self.topics.lock().await.clone();
127        {
128            let hist = self.history.lock().await;
129            for m in hist.iter() {
130                set.insert(m.topic.clone());
131            }
132        }
133        let mut out: Vec<String> = set.into_iter().collect();
134        out.sort();
135        out
136    }
137}
138
139impl<T: Clone + Send + Sync + 'static> Default for InMemoryBroker<T> {
140    fn default() -> Self {
141        Self::new(256)
142    }
143}
144
145/// Register in-memory producer and consumer factories.
146pub fn register<T: Clone + Send + Sync + 'static>(
147    registry: &mut MessagingRegistry<T>,
148    broker: InMemoryBroker<T>,
149) -> AppResult<()> {
150    registry.register_backend(ADAPTER_NAME, Arc::new(MemoryFactory { broker }))
151}
152
153struct MemoryFactory<T: Clone + Send + Sync + 'static> {
154    broker: InMemoryBroker<T>,
155}
156
157impl<T: Clone + Send + Sync + 'static> MessagingFactory<T> for MemoryFactory<T> {
158    fn create_producer(&self, _config: &BrokerConfig) -> AppResult<Arc<dyn MessageProducer<T>>> {
159        Ok(Arc::new(self.broker.producer()))
160    }
161
162    fn create_consumer(&self, _config: &BrokerConfig) -> AppResult<Arc<dyn MessageConsumer<T>>> {
163        Ok(Arc::new(self.broker.consumer()))
164    }
165}
166
167/// An in-memory message producer.
168#[derive(Debug, Clone)]
169pub struct InMemoryProducer<T: Clone + Send + Sync + 'static> {
170    tx: broadcast::Sender<Message<T>>,
171    history: Arc<Mutex<VecDeque<Message<T>>>>,
172    history_limit: Option<usize>,
173    topics: Arc<Mutex<HashSet<String>>>,
174    notify: Arc<tokio::sync::Notify>,
175}
176
177#[async_trait]
178impl<T: Clone + Send + Sync + 'static> MessageProducer<T> for InMemoryProducer<T> {
179    async fn send(&self, msg: Message<T>) -> AppResult<()> {
180        // Record in history before broadcasting.
181        {
182            let mut hist = self.history.lock().await;
183            if let Some(limit) = self.history_limit
184                && hist.len() == limit
185            {
186                hist.pop_front();
187            }
188            hist.push_back(msg.clone());
189        }
190        {
191            let mut set = self.topics.lock().await;
192            set.insert(msg.topic.clone());
193        }
194
195        self.tx.send(msg).map_err(|_| {
196            AppError::new(ErrorCode::ExternalService, "no active consumers on channel")
197        })?;
198
199        self.notify.notify_waiters();
200        Ok(())
201    }
202
203    async fn send_batch(&self, msgs: Vec<Message<T>>) -> AppResult<()> {
204        for msg in msgs {
205            self.send(msg).await?;
206        }
207        Ok(())
208    }
209
210    async fn flush(&self, _timeout: Duration) -> AppResult<()> {
211        // In-memory delivery is instant; nothing to flush.
212        Ok(())
213    }
214}
215
216/// An in-memory message consumer.
217#[derive(Debug)]
218pub struct InMemoryConsumer<T: Clone + Send + Sync + 'static> {
219    rx: Arc<Mutex<broadcast::Receiver<Message<T>>>>,
220    topics: Arc<Mutex<HashSet<String>>>,
221}
222
223// Manual Clone because broadcast::Receiver is not Clone but we can resubscribe.
224impl<T: Clone + Send + Sync + 'static> Clone for InMemoryConsumer<T> {
225    fn clone(&self) -> Self {
226        Self {
227            rx: self.rx.clone(),
228            topics: self.topics.clone(),
229        }
230    }
231}
232
233#[async_trait]
234impl<T: Clone + Send + Sync + 'static> MessageConsumer<T> for InMemoryConsumer<T> {
235    async fn subscribe(&self, topics: &[&str]) -> AppResult<()> {
236        {
237            let mut set = self.topics.lock().await;
238            for t in topics {
239                set.insert((*t).to_string());
240            }
241        }
242        Ok(())
243    }
244
245    async fn recv(&self) -> AppResult<Message<T>> {
246        loop {
247            let msg = {
248                let mut rx = self.rx.lock().await;
249                rx.recv().await.map_err(|e| {
250                    AppError::new(ErrorCode::ExternalService, format!("receive failed: {e}"))
251                })?
252            };
253
254            let topics = self.topics.lock().await;
255            // If no explicit subscription, accept all messages.
256            if topics.is_empty() || topics.contains(&msg.topic) {
257                return Ok(msg);
258            }
259            // Otherwise loop to skip messages for other topics.
260        }
261    }
262}
263
264#[async_trait]
265impl EventProducer for InMemoryProducer<serde_json::Value> {
266    async fn publish(&self, topic: &str, event: Event) -> AppResult<()> {
267        let value = serde_json::to_value(&event).map_err(|e| {
268            AppError::new(
269                ErrorCode::Internal,
270                format!("Failed to serialize event: {e}"),
271            )
272        })?;
273        self.send(Message::new(topic, value)).await
274    }
275
276    async fn publish_batch(&self, topic: &str, events: Vec<Event>) -> AppResult<()> {
277        for event in events {
278            self.publish(topic, event).await?;
279        }
280        Ok(())
281    }
282}
283
284#[async_trait]
285impl EventConsumer for InMemoryConsumer<serde_json::Value> {
286    async fn subscribe(&self, topics: &[&str]) -> AppResult<()> {
287        MessageConsumer::subscribe(self, topics).await
288    }
289
290    async fn recv_event(&self) -> AppResult<Event> {
291        let msg = self.recv().await?;
292        serde_json::from_value(msg.payload).map_err(|e| {
293            AppError::new(
294                ErrorCode::Internal,
295                format!("Failed to deserialize event: {e}"),
296            )
297        })
298    }
299}
300
301/// Assert that at least one message on `topic` satisfies the predicate.
302///
303/// # Panics
304///
305/// Panics when no matching message is found.
306pub async fn assert_published<T: Clone + Send + Sync + 'static>(
307    broker: &InMemoryBroker<T>,
308    topic: &str,
309    predicate: impl Fn(&Message<T>) -> bool,
310) {
311    let msgs = broker.messages(topic).await;
312    assert!(
313        msgs.iter().any(&predicate),
314        "assert_published: no message on topic {topic:?} matched the predicate ({} checked)",
315        msgs.len(),
316    );
317}
318
319/// Assert that exactly `n` messages were published to `topic`.
320///
321/// # Panics
322///
323/// Panics when the count does not match.
324pub async fn assert_published_n<T: Clone + Send + Sync + 'static>(
325    broker: &InMemoryBroker<T>,
326    topic: &str,
327    n: usize,
328) {
329    let got = broker.message_count(topic).await;
330    assert_eq!(
331        got, n,
332        "assert_published_n: topic {topic:?} has {got} messages, want {n}",
333    );
334}
335
336/// Wait until at least one message appears on `topic` or the timeout expires.
337///
338/// Returns the first message on the topic.
339///
340/// # Panics
341///
342/// Panics if the timeout elapses before any message arrives.
343pub async fn wait_for_message<T: Clone + Send + Sync + 'static>(
344    broker: &InMemoryBroker<T>,
345    topic: &str,
346    timeout: Duration,
347) -> Message<T> {
348    let deadline = tokio::time::Instant::now() + timeout;
349
350    loop {
351        let msgs = broker.messages(topic).await;
352        if let Some(m) = msgs.into_iter().next() {
353            return m;
354        }
355        tokio::select! {
356            () = broker.notify.notified() => { /* re-check */ }
357            () = tokio::time::sleep_until(deadline) => {
358                panic!("wait_for_message: timed out after {timeout:?} waiting for message on topic {topic:?}");
359            }
360        }
361    }
362}
363
364/// Assert that zero messages were published to `topic`.
365///
366/// # Panics
367///
368/// Panics when the topic is not empty.
369pub async fn assert_no_messages<T: Clone + Send + Sync + 'static>(
370    broker: &InMemoryBroker<T>,
371    topic: &str,
372) {
373    let n = broker.message_count(topic).await;
374    assert_eq!(
375        n, 0,
376        "assert_no_messages: topic {topic:?} has {n} messages, want 0",
377    );
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[tokio::test]
385    async fn send_and_receive() {
386        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
387        let producer = broker.producer();
388        let consumer = broker.consumer();
389
390        consumer.subscribe(&["test-topic"]).await.unwrap();
391
392        let msg = Message::new("test-topic", "hello".to_string());
393        producer.send(msg).await.unwrap();
394
395        let received = consumer.recv().await.unwrap();
396        assert_eq!(received.topic, "test-topic");
397        assert_eq!(received.payload, "hello");
398    }
399
400    #[tokio::test]
401    async fn register_memory_adapter_explicitly() {
402        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
403        let mut registry = MessagingRegistry::new();
404
405        register(&mut registry, broker).unwrap();
406
407        assert_eq!(registry.adapters(), vec!["memory"]);
408        let config = BrokerConfig::default();
409        let producer = registry.producer(&config).unwrap();
410        let consumer = registry.consumer(&config).unwrap();
411        consumer.subscribe(&["events"]).await.unwrap();
412        producer
413            .send(Message::new("events", "registered".to_string()))
414            .await
415            .unwrap();
416        let received = consumer.recv().await.unwrap();
417        assert_eq!(received.payload, "registered");
418    }
419
420    #[tokio::test]
421    async fn send_batch_and_receive() {
422        let broker: InMemoryBroker<i32> = InMemoryBroker::new(16);
423        let producer = broker.producer();
424        let consumer = broker.consumer();
425
426        consumer.subscribe(&["numbers"]).await.unwrap();
427
428        let msgs = vec![
429            Message::new("numbers", 1),
430            Message::new("numbers", 2),
431            Message::new("numbers", 3),
432        ];
433        producer.send_batch(msgs).await.unwrap();
434
435        let a = consumer.recv().await.unwrap();
436        let b = consumer.recv().await.unwrap();
437        let c = consumer.recv().await.unwrap();
438        assert_eq!(a.payload, 1);
439        assert_eq!(b.payload, 2);
440        assert_eq!(c.payload, 3);
441    }
442
443    #[tokio::test]
444    async fn topic_filtering() {
445        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
446        let producer = broker.producer();
447        let consumer = broker.consumer();
448
449        consumer.subscribe(&["wanted"]).await.unwrap();
450
451        producer
452            .send(Message::new("ignored", "nope".to_string()))
453            .await
454            .unwrap();
455        producer
456            .send(Message::new("wanted", "yes".to_string()))
457            .await
458            .unwrap();
459
460        let received = consumer.recv().await.unwrap();
461        assert_eq!(received.topic, "wanted");
462        assert_eq!(received.payload, "yes");
463    }
464
465    #[tokio::test]
466    async fn flush_is_noop() {
467        let broker: InMemoryBroker<()> = InMemoryBroker::new(4);
468        let producer = broker.producer();
469        producer.flush(Duration::from_secs(1)).await.unwrap();
470    }
471
472    #[tokio::test]
473    async fn event_publish_and_receive() {
474        let broker: InMemoryBroker<serde_json::Value> = InMemoryBroker::new(16);
475        let producer = broker.producer();
476        let consumer = broker.consumer();
477
478        EventConsumer::subscribe(&consumer, &["events"])
479            .await
480            .unwrap();
481
482        let event = Event::new("user.created", "auth-service")
483            .with_subject("user-42")
484            .with_data(serde_json::json!({"name": "Alice"}))
485            .unwrap();
486        let original_id = event.id.clone();
487
488        producer.publish("events", event).await.unwrap();
489
490        let received = consumer.recv_event().await.unwrap();
491        assert_eq!(received.id, original_id);
492        assert_eq!(received.event_type, "user.created");
493        assert_eq!(received.source, "auth-service");
494        assert_eq!(received.subject, "user-42");
495        assert_eq!(received.data, serde_json::json!({"name": "Alice"}));
496    }
497
498    #[tokio::test]
499    async fn event_publish_batch_and_receive() {
500        let broker: InMemoryBroker<serde_json::Value> = InMemoryBroker::new(16);
501        let producer = broker.producer();
502        let consumer = broker.consumer();
503
504        EventConsumer::subscribe(&consumer, &["batch"])
505            .await
506            .unwrap();
507
508        let events = vec![
509            Event::new("a", "src"),
510            Event::new("b", "src"),
511            Event::new("c", "src"),
512        ];
513        producer.publish_batch("batch", events).await.unwrap();
514
515        let a = consumer.recv_event().await.unwrap();
516        let b = consumer.recv_event().await.unwrap();
517        let c = consumer.recv_event().await.unwrap();
518        assert_eq!(a.event_type, "a");
519        assert_eq!(b.event_type, "b");
520        assert_eq!(c.event_type, "c");
521    }
522
523    // ── History & topic helper tests ────────────────────────────────────────
524
525    #[tokio::test]
526    async fn messages_returns_topic_history() {
527        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
528        let producer = broker.producer();
529        // Need a consumer so broadcast::send succeeds.
530        let _consumer = broker.consumer();
531
532        producer
533            .send(Message::new("t1", "a".to_string()))
534            .await
535            .unwrap();
536        producer
537            .send(Message::new("t1", "b".to_string()))
538            .await
539            .unwrap();
540        producer
541            .send(Message::new("t2", "c".to_string()))
542            .await
543            .unwrap();
544
545        let t1 = broker.messages("t1").await;
546        assert_eq!(t1.len(), 2);
547        assert_eq!(t1[0].payload, "a");
548        assert_eq!(t1[1].payload, "b");
549
550        let all = broker.all_messages().await;
551        assert_eq!(all.len(), 3);
552    }
553
554    #[tokio::test]
555    async fn in_memory_history_is_bounded() {
556        let broker = InMemoryBroker::with_history_limit(8, 2);
557        let producer = broker.producer();
558        let _consumer = broker.consumer();
559
560        producer.send(Message::new("events", 1_u32)).await.unwrap();
561        producer.send(Message::new("events", 2_u32)).await.unwrap();
562        producer.send(Message::new("events", 3_u32)).await.unwrap();
563
564        let messages = broker.messages("events").await;
565        assert_eq!(messages.len(), 2);
566        assert_eq!(messages[0].payload, 2);
567        assert_eq!(messages[1].payload, 3);
568    }
569
570    #[tokio::test]
571    async fn message_count_and_reset() {
572        let broker: InMemoryBroker<i32> = InMemoryBroker::new(16);
573        let producer = broker.producer();
574        let _consumer = broker.consumer();
575
576        assert_eq!(broker.message_count("t").await, 0);
577        producer.send(Message::new("t", 1)).await.unwrap();
578        assert_eq!(broker.message_count("t").await, 1);
579
580        broker.reset().await;
581        assert_eq!(broker.message_count("t").await, 0);
582    }
583
584    #[tokio::test]
585    async fn create_topic_and_topic_names() {
586        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
587        let _consumer = broker.consumer();
588
589        broker.create_topic("z-topic").await;
590        broker.create_topic("a-topic").await;
591
592        producer_send_helper(&broker, "m-topic").await;
593
594        let names = broker.topic_names().await;
595        assert_eq!(names, vec!["a-topic", "m-topic", "z-topic"]);
596    }
597
598    /// Helper: send a dummy message so that the topic appears in history.
599    async fn producer_send_helper(broker: &InMemoryBroker<String>, topic: &str) {
600        let producer = broker.producer();
601        producer
602            .send(Message::new(topic, "x".to_string()))
603            .await
604            .unwrap();
605    }
606
607    // ── Assertion helper tests ──────────────────────────────────────────────
608
609    #[tokio::test]
610    async fn test_assert_published() {
611        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
612        let producer = broker.producer();
613        let _consumer = broker.consumer();
614
615        producer
616            .send(Message::new("t1", "hello".to_string()))
617            .await
618            .unwrap();
619        producer
620            .send(Message::new("t1", "world".to_string()))
621            .await
622            .unwrap();
623
624        assert_published(&broker, "t1", |m| m.payload == "world").await;
625    }
626
627    #[tokio::test]
628    async fn test_assert_published_n() {
629        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
630        let producer = broker.producer();
631        let _consumer = broker.consumer();
632
633        producer
634            .send(Message::new("t1", "a".to_string()))
635            .await
636            .unwrap();
637        producer
638            .send(Message::new("t1", "b".to_string()))
639            .await
640            .unwrap();
641
642        assert_published_n(&broker, "t1", 2).await;
643    }
644
645    #[tokio::test]
646    async fn test_assert_no_messages() {
647        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
648        assert_no_messages(&broker, "empty-topic").await;
649    }
650
651    #[tokio::test]
652    async fn test_wait_for_message() {
653        let broker: InMemoryBroker<String> = InMemoryBroker::new(16);
654        let _consumer = broker.consumer();
655
656        let broker_clone = broker.clone();
657        tokio::spawn(async move {
658            tokio::time::sleep(Duration::from_millis(20)).await;
659            let producer = broker_clone.producer();
660            producer
661                .send(Message::new("t1", "delayed".to_string()))
662                .await
663                .unwrap();
664        });
665
666        let msg = wait_for_message(&broker, "t1", Duration::from_secs(2)).await;
667        assert_eq!(msg.payload, "delayed");
668    }
669    #[tokio::test]
670    async fn default_history_is_bounded_by_capacity() {
671        let broker: InMemoryBroker<usize> = InMemoryBroker::new(8);
672        let producer = broker.producer();
673        let _consumer = broker.consumer();
674
675        for value in 0..1030 {
676            producer.send(Message::new("history", value)).await.unwrap();
677        }
678
679        let messages = broker.messages("history").await;
680        assert_eq!(messages.len(), 8);
681        assert_eq!(messages.first().map(|msg| msg.payload), Some(1022));
682    }
683
684    #[tokio::test]
685    async fn bounded_history_limit_is_opt_in() {
686        let broker: InMemoryBroker<usize> = InMemoryBroker::with_history_limit(8, 2);
687        let producer = broker.producer();
688        let _consumer = broker.consumer();
689
690        for value in 0..4 {
691            producer.send(Message::new("history", value)).await.unwrap();
692        }
693
694        let payloads = broker
695            .messages("history")
696            .await
697            .into_iter()
698            .map(|msg| msg.payload)
699            .collect::<Vec<_>>();
700        assert_eq!(payloads, vec![2, 3]);
701    }
702
703    #[tokio::test]
704    async fn zero_capacity_is_clamped() {
705        let broker: InMemoryBroker<usize> = InMemoryBroker::new(0);
706        let producer = broker.producer();
707        let _consumer = broker.consumer();
708
709        producer.send(Message::new("history", 1)).await.unwrap();
710
711        let messages = broker.messages("history").await;
712        assert_eq!(messages.len(), 1);
713    }
714}