Skip to main content

mq_bridge/
event_store.rs

1//  mq-bridge
2//  © Copyright 2026, by Marco Mengelkoch
3//  Licensed under MIT License, see License file for more details
4
5use crate::canonical_message::tracing_support::LazyMessageIds;
6use crate::traits::{
7    BatchCommitFunc, ConsumerError, EndpointStatus, MessageConsumer, MessageDisposition,
8    ReceivedBatch,
9};
10use crate::CanonicalMessage;
11use async_trait::async_trait;
12use once_cell::sync::Lazy;
13use std::any::Any;
14use std::collections::{HashMap, VecDeque};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, RwLock};
17use std::time::{Duration, SystemTime, UNIX_EPOCH};
18use tokio::sync::watch;
19use tracing::{debug, info, trace, warn};
20
21#[derive(Debug, Clone)]
22pub struct RetentionPolicy {
23    /// Maximum age of an event before it is eligible for removal.
24    pub max_age: Option<Duration>,
25    /// Maximum number of events to keep in the store.
26    pub max_count: Option<usize>,
27    /// Time after which a subscriber is considered inactive and ignored for GC purposes.
28    pub subscriber_timeout: Duration,
29    /// Minimum interval between Garbage Collection runs.
30    pub gc_interval: Duration,
31}
32
33impl Default for RetentionPolicy {
34    fn default() -> Self {
35        Self {
36            max_age: Some(Duration::from_secs(3600 * 24)), // 24 hours
37            max_count: Some(100_000),
38            subscriber_timeout: Duration::from_secs(300), // 5 minutes
39            gc_interval: Duration::from_secs(1),
40        }
41    }
42}
43
44/// A map to hold event stores for the duration of the bridge setup.
45static RUNTIME_EVENT_STORES: Lazy<Mutex<HashMap<String, Arc<EventStore>>>> =
46    Lazy::new(|| Mutex::new(HashMap::new()));
47
48/// Gets a shared `EventStore` for a given topic, creating it if it doesn't exist.
49pub fn get_or_create_event_store(topic: &str) -> Arc<EventStore> {
50    let mut stores = RUNTIME_EVENT_STORES.lock().unwrap();
51    stores
52        .entry(topic.to_string())
53        .or_insert_with(|| {
54            info!(topic = %topic, "Creating new runtime event store");
55            Arc::new(EventStore::new(Default::default()))
56        })
57        .clone()
58}
59
60/// Checks if an EventStore exists for the given topic.
61pub fn event_store_exists(topic: &str) -> bool {
62    let stores = RUNTIME_EVENT_STORES.lock().unwrap();
63    stores.contains_key(topic)
64}
65
66#[derive(Debug)]
67struct SubscriberState {
68    last_ack_offset: AtomicU64,
69    last_seen: Mutex<SystemTime>,
70}
71
72#[derive(Debug, Clone)]
73pub struct StoredEvent {
74    pub message: CanonicalMessage,
75    pub offset: u64,
76    pub stored_at: u64,
77}
78
79/// A robust, in-memory event store.
80/// Supports append-only storage, subscriber tracking, and garbage collection.
81pub struct EventStore {
82    events: RwLock<VecDeque<StoredEvent>>,
83    subscribers: RwLock<HashMap<String, Arc<SubscriberState>>>,
84    next_offset: AtomicU64,
85    base_offset: AtomicU64,
86    retention_policy: RetentionPolicy,
87    offset_update: watch::Sender<u64>,
88    dropped_events: AtomicU64,
89    last_gc: RwLock<SystemTime>,
90    on_drop: Option<Box<dyn Fn(Vec<StoredEvent>) + Send + Sync>>,
91}
92
93impl std::fmt::Debug for EventStore {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("EventStore")
96            .field("next_offset", &self.next_offset)
97            .field("base_offset", &self.base_offset)
98            .field("retention_policy", &self.retention_policy)
99            .finish_non_exhaustive()
100    }
101}
102
103impl EventStore {
104    pub fn new(retention_policy: RetentionPolicy) -> Self {
105        let (offset_update, _) = watch::channel(0);
106        Self {
107            events: RwLock::new(VecDeque::new()),
108            subscribers: RwLock::new(HashMap::new()),
109            next_offset: AtomicU64::new(1), // Start offsets at 1
110            base_offset: AtomicU64::new(1),
111            retention_policy,
112            offset_update,
113            dropped_events: AtomicU64::new(0),
114            last_gc: RwLock::new(SystemTime::now()),
115            on_drop: None,
116        }
117    }
118
119    /// Appends an event to the store.
120    /// Assigns an offset and stored_at timestamp to the message metadata.
121    pub async fn append(&self, event: CanonicalMessage) -> u64 {
122        let timestamp = SystemTime::now()
123            .duration_since(UNIX_EPOCH)
124            .unwrap_or_default()
125            .as_millis() as u64;
126
127        let mut events = self.events.write().unwrap();
128        let offset = self.next_offset.fetch_add(1, Ordering::SeqCst);
129        let stored = StoredEvent {
130            message: event,
131            offset,
132            stored_at: timestamp,
133        };
134
135        events.push_back(stored);
136
137        let mut removed = Vec::new();
138        let mut remove_count = 0;
139        let mut total_dropped = 0;
140        let mut max_count = 0;
141
142        // Enforce max_count immediately
143        if let Some(max) = self.retention_policy.max_count {
144            max_count = max;
145            if events.len() > max {
146                remove_count = events.len() - max;
147                removed = events.drain(0..remove_count).collect();
148                self.base_offset
149                    .fetch_add(remove_count as u64, Ordering::SeqCst);
150                total_dropped = self
151                    .dropped_events
152                    .fetch_add(remove_count as u64, Ordering::SeqCst)
153                    + remove_count as u64;
154            }
155        }
156        drop(events);
157
158        if remove_count > 0 {
159            if let Some(cb) = &self.on_drop {
160                cb(removed);
161            }
162            warn!(
163                "Retention policy enforced (max_count={}): dropped {} events (total dropped: {}). Slow subscribers may miss events.",
164                max_count, remove_count, total_dropped
165            );
166        }
167
168        trace!("Appended event offset {}", offset);
169        // Notify waiting subscribers
170        let _ = self.offset_update.send(offset);
171
172        offset
173    }
174
175    /// Appends a batch of events to the store.
176    pub async fn append_batch(&self, messages: Vec<CanonicalMessage>) -> u64 {
177        if messages.is_empty() {
178            return self.next_offset.load(Ordering::SeqCst).saturating_sub(1);
179        }
180
181        let timestamp = SystemTime::now()
182            .duration_since(UNIX_EPOCH)
183            .unwrap_or_default()
184            .as_millis() as u64;
185
186        let count = messages.len() as u64;
187
188        let mut events = self.events.write().unwrap();
189        let start_offset = self.next_offset.fetch_add(count, Ordering::SeqCst);
190
191        let stored_events = messages
192            .into_iter()
193            .enumerate()
194            .map(|(i, event)| StoredEvent {
195                message: event,
196                offset: start_offset + i as u64,
197                stored_at: timestamp,
198            });
199
200        events.extend(stored_events);
201
202        let last_offset = start_offset + count - 1;
203
204        let mut removed = Vec::new();
205        let mut remove_count = 0;
206        let mut total_dropped = 0;
207        let mut max_count = 0;
208
209        // Enforce max_count immediately
210        if let Some(max) = self.retention_policy.max_count {
211            max_count = max;
212            if events.len() > max {
213                remove_count = events.len() - max;
214                removed = events.drain(0..remove_count).collect();
215                self.base_offset
216                    .fetch_add(remove_count as u64, Ordering::SeqCst);
217                total_dropped = self
218                    .dropped_events
219                    .fetch_add(remove_count as u64, Ordering::SeqCst)
220                    + remove_count as u64;
221            }
222        }
223        drop(events);
224
225        if remove_count > 0 {
226            if let Some(cb) = &self.on_drop {
227                cb(removed);
228            }
229            warn!(
230                "Retention policy enforced (max_count={}): dropped {} events (total dropped: {}). Slow subscribers may miss events.",
231                max_count, remove_count, total_dropped
232            );
233        }
234
235        trace!("Appended batch up to offset {}", last_offset);
236        // Notify waiting subscribers
237        let _ = self.offset_update.send(last_offset);
238
239        last_offset
240    }
241
242    pub fn with_drop_callback(
243        mut self,
244        callback: impl Fn(Vec<StoredEvent>) + Send + Sync + 'static,
245    ) -> Self {
246        self.on_drop = Some(Box::new(callback));
247        self
248    }
249
250    /// Registers a subscriber or updates its liveness.
251    pub async fn register_subscriber(&self, subscriber_id: String) {
252        // Fast path: try to update existing subscriber with a read lock
253        {
254            let subs = self.subscribers.read().unwrap();
255            if let Some(state) = subs.get(&subscriber_id) {
256                if let Ok(mut last_seen) = state.last_seen.lock() {
257                    *last_seen = SystemTime::now();
258                }
259                return;
260            }
261        }
262
263        // Slow path: insert new subscriber with a write lock
264        let mut subs = self.subscribers.write().unwrap();
265        subs.entry(subscriber_id).or_insert_with(|| {
266            Arc::new(SubscriberState {
267                last_ack_offset: AtomicU64::new(0),
268                last_seen: Mutex::new(SystemTime::now()),
269            })
270        });
271    }
272
273    /// Acknowledges processing of events up to `offset` for a subscriber.
274    /// Triggers Garbage Collection.
275    pub async fn ack(&self, subscriber_id: &str, offset: u64) {
276        {
277            let subs = self.subscribers.read().unwrap();
278            if let Some(s) = subs.get(subscriber_id) {
279                s.last_ack_offset.fetch_max(offset, Ordering::SeqCst);
280                if let Ok(mut last_seen) = s.last_seen.lock() {
281                    *last_seen = SystemTime::now();
282                }
283            }
284        }
285
286        trace!("Subscriber {} acked offset {}", subscriber_id, offset);
287
288        let now = SystemTime::now();
289        let should_gc = {
290            let last = self.last_gc.read().unwrap();
291            now.duration_since(*last).unwrap_or(Duration::ZERO) >= self.retention_policy.gc_interval
292        };
293
294        if should_gc {
295            let perform_gc = {
296                let mut last = self.last_gc.write().unwrap();
297                if now.duration_since(*last).unwrap_or(Duration::ZERO)
298                    >= self.retention_policy.gc_interval
299                {
300                    *last = now;
301                    true
302                } else {
303                    false
304                }
305            };
306
307            if perform_gc {
308                self.run_gc().await;
309            }
310        }
311    }
312
313    /// Returns events with offset > last_known_offset.
314    ///
315    /// If the requested events have already been GC'd (i.e. there's a gap between
316    /// `last_known_offset + 1` and the current `base_offset`), returns `Err(ConsumerError::Gap)`.
317    pub async fn get_events_since(
318        &self,
319        last_known_offset: u64,
320        limit: usize,
321    ) -> Result<Vec<StoredEvent>, crate::errors::ConsumerError> {
322        let events = self.events.read().unwrap();
323        let base = self.base_offset.load(Ordering::SeqCst);
324
325        // We want events starting from offset: last_known_offset + 1
326        let start_offset = last_known_offset + 1;
327
328        // If the requested start_offset is older than base, the caller has a gap.
329        if start_offset < base {
330            return Err(crate::errors::ConsumerError::Gap {
331                requested: start_offset,
332                base,
333            });
334        }
335
336        // Calculate index relative to the vector
337        let start_index = if start_offset > base {
338            (start_offset - base) as usize
339        } else {
340            0
341        };
342
343        if start_index >= events.len() {
344            return Ok(Vec::new());
345        }
346
347        let count = std::cmp::min(limit, events.len() - start_index);
348        let mut result = Vec::with_capacity(count);
349
350        let (s1, s2) = events.as_slices();
351
352        if start_index < s1.len() {
353            let take_s1 = std::cmp::min(count, s1.len() - start_index);
354            result.extend_from_slice(&s1[start_index..start_index + take_s1]);
355
356            let remaining = count - take_s1;
357            if remaining > 0 {
358                result.extend_from_slice(&s2[0..remaining]);
359            }
360        } else {
361            let s2_index = start_index - s1.len();
362            result.extend_from_slice(&s2[s2_index..s2_index + count]);
363        }
364
365        Ok(result)
366    }
367
368    /// Waits for new events if none are currently available after last_known_offset.
369    pub async fn wait_for_events(&self, last_known_offset: u64) {
370        let mut rx = self.offset_update.subscribe();
371        if *rx.borrow() > last_known_offset {
372            return;
373        }
374        while rx.changed().await.is_ok() {
375            if *rx.borrow() > last_known_offset {
376                return;
377            }
378        }
379    }
380
381    async fn run_gc(&self) {
382        let subs = self.subscribers.read().unwrap();
383        if subs.is_empty() {
384            return;
385        }
386
387        let now = SystemTime::now();
388        let timeout = self.retention_policy.subscriber_timeout;
389
390        // Calculate min_ack only from active subscribers
391        let active_acks: Vec<u64> = subs
392            .values()
393            .filter(|s| {
394                let last_seen = s.last_seen.lock().unwrap();
395                if let Ok(age) = now.duration_since(*last_seen) {
396                    age < timeout
397                } else {
398                    false
399                }
400            })
401            .map(|s| s.last_ack_offset.load(Ordering::SeqCst))
402            .collect();
403
404        drop(subs);
405
406        // If no active subscribers, we default to 0 (don't remove based on ack)
407        let min_ack = if active_acks.is_empty() {
408            0
409        } else {
410            *active_acks.iter().min().unwrap()
411        };
412
413        let mut events = self.events.write().unwrap();
414        let base = self.base_offset.load(Ordering::SeqCst);
415        let max_age = self.retention_policy.max_age;
416        let mut remove_count = 0;
417
418        for (i, event) in events.iter().enumerate() {
419            let mut remove = false;
420            let offset = base + i as u64;
421
422            // Condition 1: All active subscribers have acked this event
423            if !active_acks.is_empty() && offset <= min_ack {
424                remove = true;
425            }
426
427            // Condition 2: Max age exceeded (Secondary GC)
428            if !remove {
429                if let Some(max_age) = max_age {
430                    let event_time = UNIX_EPOCH + Duration::from_millis(event.stored_at);
431                    if let Ok(age) = now.duration_since(event_time) {
432                        if age > max_age {
433                            remove = true;
434                        }
435                    }
436                }
437            }
438
439            if remove {
440                remove_count += 1;
441            } else {
442                break;
443            }
444        }
445
446        let mut removed = Vec::new();
447        if remove_count > 0 {
448            debug!("GC removing {} events", remove_count);
449            removed = events.drain(0..remove_count).collect();
450            self.base_offset
451                .fetch_add(remove_count as u64, Ordering::SeqCst);
452        }
453        drop(events);
454
455        if remove_count > 0 {
456            if let Some(cb) = &self.on_drop {
457                cb(removed);
458            }
459        }
460    }
461
462    pub fn consumer(self: &Arc<Self>, subscriber_id: String) -> EventStoreConsumer {
463        EventStoreConsumer {
464            store: self.clone(),
465            subscriber_id,
466            last_offset: Arc::new(AtomicU64::new(0)),
467        }
468    }
469}
470
471#[derive(Debug)]
472pub struct EventStoreConsumer {
473    store: Arc<EventStore>,
474    subscriber_id: String,
475    last_offset: Arc<AtomicU64>,
476}
477
478#[async_trait]
479impl MessageConsumer for EventStoreConsumer {
480    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
481        self.store
482            .register_subscriber(self.subscriber_id.clone())
483            .await;
484
485        let last_offset_val = self.last_offset.load(Ordering::SeqCst);
486
487        let stored_events = match self
488            .store
489            .get_events_since(last_offset_val, max_messages)
490            .await
491        {
492            Ok(ev) => ev,
493            Err(e) => return Err(e),
494        };
495
496        let stored_events = if stored_events.is_empty() {
497            // Wait for new events
498            self.store.wait_for_events(last_offset_val).await;
499            match self
500                .store
501                .get_events_since(last_offset_val, max_messages)
502                .await
503            {
504                Ok(ev) => ev,
505                Err(e) => return Err(e),
506            }
507        } else {
508            stored_events
509        };
510
511        let mut new_offset = last_offset_val;
512        if let Some(last) = stored_events.last() {
513            new_offset = last.offset;
514        }
515
516        // Advance offset immediately to support async commit pipelines (like Route)
517        self.last_offset.store(new_offset, Ordering::SeqCst);
518
519        // Convert to CanonicalMessage for the consumer
520        let events: Vec<CanonicalMessage> = stored_events.into_iter().map(|e| e.message).collect();
521        trace!(count = events.len(), subscriber_id = %self.subscriber_id, message_ids = ?LazyMessageIds(&events), "Received batch of events from store");
522
523        let store = self.store.clone();
524        let subscriber_id = self.subscriber_id.clone();
525        let last_offset_arc = self.last_offset.clone();
526
527        let commit: BatchCommitFunc = Box::new(move |dispositions| {
528            Box::pin(async move {
529                if dispositions
530                    .iter()
531                    .any(|d| matches!(d, MessageDisposition::Nack))
532                {
533                    last_offset_arc.fetch_min(last_offset_val, Ordering::SeqCst);
534                } else {
535                    store.ack(&subscriber_id, new_offset).await;
536                }
537                Ok(())
538            })
539        });
540
541        Ok(ReceivedBatch {
542            messages: events,
543            commit,
544        })
545    }
546
547    async fn status(&self) -> EndpointStatus {
548        let last = self.last_offset.load(Ordering::SeqCst);
549        let next = self.store.next_offset.load(Ordering::SeqCst);
550        // Offsets are 1-based. next_offset is the ID of the *next* event to be written.
551        let pending = next.saturating_sub(last).saturating_sub(1) as usize;
552
553        EndpointStatus {
554            healthy: true,
555            target: self.subscriber_id.clone(),
556            pending: Some(pending),
557            details: serde_json::json!({
558                "mode": "event_store",
559                "current_offset": last,
560                "head_offset": next.saturating_sub(1)
561            }),
562            ..Default::default()
563        }
564    }
565
566    fn as_any(&self) -> &dyn Any {
567        self
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use crate::errors::ConsumerError;
575    use std::sync::Arc;
576    use tokio::time::{sleep, Duration};
577
578    #[tokio::test]
579    async fn test_event_store_lifecycle() {
580        let store = Arc::new(EventStore::new(RetentionPolicy::default()));
581
582        // Append
583        let id1 = store.append(CanonicalMessage::from("msg1")).await;
584        assert_eq!(id1, 1);
585        let id2 = store.append(CanonicalMessage::from("msg2")).await;
586        assert_eq!(id2, 2);
587
588        // Retrieve
589        let events = store.get_events_since(0, 10).await.unwrap();
590        assert_eq!(events.len(), 2);
591        assert_eq!(events[0].offset, 1);
592        assert_eq!(events[1].offset, 2);
593
594        // Retrieve partial
595        let events = store.get_events_since(1, 10).await.unwrap();
596        assert_eq!(events.len(), 1);
597        assert_eq!(events[0].offset, 2);
598    }
599
600    #[tokio::test]
601    async fn test_retention_policy_max_count() {
602        let policy = RetentionPolicy {
603            max_count: Some(2),
604            ..Default::default()
605        };
606        let store = EventStore::new(policy);
607
608        store.append(CanonicalMessage::from("1")).await;
609        store.append(CanonicalMessage::from("2")).await;
610        store.append(CanonicalMessage::from("3")).await;
611
612        // Should have dropped "1" (offset 1). Base offset should be 2.
613        // get_events_since(0) asks for offset 1.
614        let res = store.get_events_since(0, 10).await;
615        match res {
616            Err(ConsumerError::Gap { requested, base }) => {
617                assert_eq!(requested, 1);
618                assert_eq!(base, 2);
619            }
620            _ => panic!("Expected Gap error"),
621        }
622
623        let events = store.get_events_since(1, 10).await.unwrap();
624        assert_eq!(events.len(), 2);
625        assert_eq!(events[0].message.get_payload_str(), "2");
626        assert_eq!(events[1].message.get_payload_str(), "3");
627    }
628
629    #[tokio::test]
630    async fn test_subscriber_ack_gc() {
631        let policy = RetentionPolicy {
632            max_count: None,
633            gc_interval: Duration::from_millis(10), // Fast GC
634            subscriber_timeout: Duration::from_secs(60),
635            max_age: None,
636        };
637        let store = Arc::new(EventStore::new(policy));
638
639        store.append(CanonicalMessage::from("1")).await; // Offset 1
640        store.append(CanonicalMessage::from("2")).await; // Offset 2
641
642        // Register subscriber
643        store.register_subscriber("subA".into()).await;
644
645        // Ack offset 1
646        store.ack("subA", 1).await;
647
648        // Force GC (store logic checks interval)
649        // We need to wait a bit to satisfy the duration check
650        sleep(Duration::from_millis(20)).await;
651        store.run_gc().await;
652
653        // Offset 1 should be removed
654        let res = store.get_events_since(0, 10).await;
655        match res {
656            Err(ConsumerError::Gap { requested, base }) => {
657                assert_eq!(requested, 1);
658                assert_eq!(base, 2);
659            }
660            _ => panic!("Expected Gap error, got {:?}", res),
661        }
662
663        let events = store.get_events_since(1, 10).await.unwrap();
664        assert_eq!(events.len(), 1);
665        assert_eq!(events[0].offset, 2);
666    }
667
668    #[tokio::test]
669    async fn test_append_batch() {
670        let store = Arc::new(EventStore::new(RetentionPolicy::default()));
671        let messages = vec![
672            CanonicalMessage::from("batch1"),
673            CanonicalMessage::from("batch2"),
674        ];
675        let last_offset = store.append_batch(messages).await;
676        assert_eq!(last_offset, 2);
677
678        let events = store.get_events_since(0, 10).await.unwrap();
679        assert_eq!(events.len(), 2);
680        assert_eq!(events[0].offset, 1);
681        assert_eq!(events[1].offset, 2);
682    }
683
684    #[tokio::test]
685    async fn test_retention_policy_max_age() {
686        let policy = RetentionPolicy {
687            max_age: Some(Duration::from_millis(50)),
688            gc_interval: Duration::from_millis(10),
689            max_count: None,
690            ..Default::default()
691        };
692        let store = Arc::new(EventStore::new(policy));
693        store.register_subscriber("subA".into()).await;
694
695        store.append(CanonicalMessage::from("1")).await;
696        sleep(Duration::from_millis(60)).await;
697        store.append(CanonicalMessage::from("2")).await;
698
699        // Acking offset 0 for subA will not remove anything based on acks, but it will trigger GC.
700        store.ack("subA", 0).await;
701        sleep(Duration::from_millis(20)).await; // ensure gc_interval is met
702        store.run_gc().await;
703
704        // "1" should be removed due to max_age.
705        let res = store.get_events_since(0, 10).await;
706        match res {
707            Err(ConsumerError::Gap { requested, base }) => {
708                assert_eq!(requested, 1);
709                assert_eq!(base, 2);
710            }
711            _ => panic!("Expected Gap error, got {:?}", res),
712        }
713    }
714
715    #[tokio::test]
716    async fn test_subscriber_timeout_gc() {
717        let policy = RetentionPolicy {
718            max_count: None,
719            gc_interval: Duration::from_millis(10),
720            subscriber_timeout: Duration::from_millis(50), // Short timeout
721            max_age: None,
722        };
723        let store = Arc::new(EventStore::new(policy));
724
725        store.append(CanonicalMessage::from("1")).await; // Offset 1
726        store.append(CanonicalMessage::from("2")).await; // Offset 2
727
728        // Register two subscribers
729        store.register_subscriber("subA".into()).await; // Will time out
730        store.register_subscriber("subB".into()).await; // Will stay active
731
732        // Ack offset 1 for subA
733        store.ack("subA", 1).await;
734
735        // Wait for subA to time out
736        sleep(Duration::from_millis(60)).await;
737
738        // Ack offset 2 for subB
739        store.ack("subB", 2).await;
740
741        // Force GC. It should ignore subA's ack state (stuck at 1) and GC up to subB's ack (2).
742        store.run_gc().await;
743
744        // Both messages should be gone.
745        let res = store.get_events_since(0, 10).await;
746        match res {
747            Err(ConsumerError::Gap { requested, base }) => {
748                assert_eq!(requested, 1);
749                assert_eq!(base, 3); // Base offset is next_offset (1-based)
750            }
751            _ => panic!("Expected Gap error, got {:?}", res),
752        }
753    }
754
755    #[tokio::test]
756    async fn test_event_store_consumer_waits_for_events() {
757        let store = Arc::new(EventStore::new(RetentionPolicy::default()));
758        let mut consumer = store.consumer("consumer1".to_string());
759
760        let producer_task = tokio::spawn({
761            let store = store.clone();
762            async move {
763                sleep(Duration::from_millis(50)).await;
764                store.append(CanonicalMessage::from("event1")).await;
765            }
766        });
767
768        // Consumer should wait for the event
769        let batch = consumer.receive_batch(1).await.unwrap();
770        assert_eq!(batch.messages.len(), 1);
771        assert_eq!(batch.messages[0].get_payload_str(), "event1");
772
773        // Ack the batch
774        (batch.commit)(vec![MessageDisposition::Ack]).await.unwrap();
775
776        producer_task.await.unwrap();
777    }
778
779    #[tokio::test]
780    async fn test_event_store_consumer_nack() {
781        let store = Arc::new(EventStore::new(RetentionPolicy::default()));
782        let mut consumer = store.consumer("consumer_nack".to_string());
783
784        store.append(CanonicalMessage::from("event1")).await;
785
786        // Receive first batch and Nack it
787        let batch1 = consumer.receive_batch(1).await.unwrap();
788        (batch1.commit)(vec![MessageDisposition::Nack])
789            .await
790            .unwrap();
791
792        // Receive again, should get the same message
793        let batch2 = consumer.receive_batch(1).await.unwrap();
794        assert_eq!(batch2.messages[0].get_payload_str(), "event1");
795
796        // Ack it this time
797        (batch2.commit)(vec![MessageDisposition::Ack])
798            .await
799            .unwrap();
800    }
801}