Skip to main content

commonware_storage/queue/
storage.rs

1//! Queue storage implementation.
2
3use super::{metrics, Error};
4use crate::{
5    journal::contiguous::{variable, Contiguous as _},
6    rmap::RMap,
7    Context,
8};
9use commonware_codec::CodecShared;
10use commonware_macros::boxed;
11use commonware_runtime::{buffer::paged::CacheRef, telemetry::metrics::GaugeExt};
12use std::num::{NonZeroU64, NonZeroUsize};
13use tracing::debug;
14
15/// Configuration for [Queue].
16#[derive(Clone)]
17pub struct Config<C> {
18    /// The storage partition name for the queue's journal.
19    pub partition: String,
20
21    /// The number of items to store in each journal section.
22    ///
23    /// Larger values reduce file overhead but increase minimum pruning granularity.
24    /// Once set, this value cannot be changed across restarts.
25    pub items_per_section: NonZeroU64,
26
27    /// Optional zstd compression level for stored items.
28    ///
29    /// If set, items will be compressed before storage. Higher values provide
30    /// better compression but use more CPU.
31    pub compression: Option<u8>,
32
33    /// Codec configuration for encoding/decoding items.
34    pub codec_config: C,
35
36    /// Page cache for buffering reads from the underlying journal.
37    pub page_cache: CacheRef,
38
39    /// Write buffer size for each section.
40    pub write_buffer: NonZeroUsize,
41}
42
43/// A durable, at-least-once delivery queue with per-item acknowledgment.
44///
45/// Items are durably stored in a journal and survive crashes. The reader must
46/// acknowledge each item individually after processing. Items can be acknowledged
47/// out of order, enabling parallel processing.
48///
49/// # Operations
50///
51/// - [append](Self::append) / [commit](Self::commit): Write items to the journal
52///   buffer, then persist. Items are readable immediately after append (before commit),
53///   but are lost on restart if not committed.
54/// - [enqueue](Self::enqueue): Append + commit in one step; the item is durable before return.
55/// - [dequeue](Self::dequeue): Return the next unacked item in FIFO order.
56/// - [ack](Self::ack) / [ack_up_to](Self::ack_up_to): Mark items as processed (in-memory only).
57/// - [sync](Self::sync): Commit, then prune completed sections below the ack floor.
58///
59/// # Acknowledgment
60///
61/// Acks are tracked in-memory with an `ack_floor` (all positions below are acked)
62/// plus an [RMap] of acked positions above the floor. When items are acked
63/// contiguously from the floor, the floor advances automatically.
64///
65/// Acks are **not** persisted. The durable equivalent is the journal's pruning
66/// boundary, advanced by [sync](Self::sync). On restart, all non-pruned
67/// items are re-delivered regardless of prior ack state.
68///
69/// # Crash Recovery
70///
71/// On restart, `ack_floor` is set to the journal's pruning boundary.
72/// Items that were pruned are gone; everything else is re-delivered.
73/// Applications must handle duplicates (idempotent processing).
74pub struct Queue<E: Context, V: CodecShared> {
75    /// The underlying journal storing queue items.
76    journal: variable::Journal<E, V>,
77
78    /// Position of the next item to dequeue.
79    ///
80    /// Invariant: `read_pos <= journal.size()`. Note that `ack_up_to` can advance
81    /// `ack_floor` past `read_pos`; in this case, `dequeue` skips the already-acked items.
82    read_pos: u64,
83
84    /// All items at positions < ack_floor are considered acknowledged.
85    ///
86    /// On restart, this is initialized to `journal.bounds().start`.
87    ack_floor: u64,
88
89    /// Ranges of acknowledged items at positions >= ack_floor (in-memory only).
90    ///
91    /// When an item at position == ack_floor is acked, the floor advances
92    /// and any contiguous acked items are consumed. Lost on restart.
93    acked_above: RMap,
94
95    /// Metrics for monitoring queue state.
96    metrics: metrics::Metrics,
97}
98
99impl<E: Context, V: CodecShared> Queue<E, V> {
100    /// Initialize a queue from storage.
101    ///
102    /// On first initialization, creates an empty queue. On restart, begins reading
103    /// from the journal's pruning boundary (providing at-least-once delivery for
104    /// all non-pruned items).
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the underlying journal cannot be initialized.
109    #[boxed]
110    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
111        // Initialize metrics before creating sub-contexts
112        let metrics = metrics::Metrics::init(&context);
113
114        let journal = variable::Journal::init(
115            context.child("journal"),
116            variable::Config {
117                partition: cfg.partition,
118                items_per_section: cfg.items_per_section,
119                compression: cfg.compression,
120                codec_config: cfg.codec_config,
121                page_cache: cfg.page_cache,
122                write_buffer: cfg.write_buffer,
123            },
124        )
125        .await?;
126
127        // On restart, ack_floor is the pruning boundary (items below are deleted).
128        // acked_above is empty (in-memory state lost on restart).
129        let bounds = journal.bounds();
130        let acked_above = RMap::new();
131
132        debug!(floor = bounds.start, size = bounds.end, "queue initialized");
133
134        // Set initial metric values
135        let _ = metrics.tip.try_set(bounds.end);
136        let _ = metrics.floor.try_set(bounds.start);
137        let _ = metrics.next.try_set(bounds.start);
138
139        Ok(Self {
140            journal,
141            read_pos: bounds.start,
142            ack_floor: bounds.start,
143            acked_above,
144            metrics,
145        })
146    }
147
148    /// Returns whether a specific position has been acknowledged.
149    pub fn is_acked(&self, position: u64) -> bool {
150        position < self.ack_floor || self.acked_above.get(&position).is_some()
151    }
152
153    /// Append an item without persisting. Call [Self::commit] or [Self::sync]
154    /// afterwards to make it durable. The item is readable immediately but
155    /// is not guaranteed to survive a crash until committed or synced.
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if the underlying storage operation fails.
160    pub async fn append(&mut self, item: V) -> Result<u64, Error> {
161        let pos = self.journal.append(&item).await?;
162        let _ = self.metrics.tip.try_set(pos + 1);
163        debug!(pos, "appended item");
164        Ok(pos)
165    }
166
167    /// Append and commit an item in one step, returning its position.
168    /// The item is durable before this method returns.
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if the underlying storage operation fails.
173    pub async fn enqueue(&mut self, item: V) -> Result<u64, Error> {
174        let pos = self.append(item).await?;
175        self.commit().await?;
176        Ok(pos)
177    }
178
179    /// Dequeue the next unacknowledged item, returning its position and value.
180    /// Returns `None` when all items have been read or acknowledged.
181    /// Already-acked items are skipped automatically.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the underlying storage operation fails.
186    pub async fn dequeue(&mut self) -> Result<Option<(u64, V)>, Error> {
187        let size = self.journal.bounds().end;
188
189        // Fast-forward above ack floor
190        if self.read_pos < self.ack_floor {
191            self.read_pos = self.ack_floor;
192        }
193
194        // Fast-forward past the ack range containing read_pos (if any).
195        if let Some((_, end)) = self.acked_above.get(&self.read_pos) {
196            self.read_pos = end.saturating_add(1);
197        }
198
199        // If the read position is greater than the size of the journal, return None.
200        let _ = self.metrics.next.try_set(self.read_pos);
201        if self.read_pos >= size {
202            return Ok(None);
203        }
204
205        let item = self.journal.read(self.read_pos).await?;
206        let pos = self.read_pos;
207        self.read_pos += 1;
208        let _ = self.metrics.next.try_set(self.read_pos);
209        debug!(position = pos, "dequeued item");
210        Ok(Some((pos, item)))
211    }
212
213    /// Mark the item at `position` as processed (in-memory only).
214    /// The item will be skipped on subsequent dequeues. If this creates a
215    /// contiguous run from the ack floor, the floor advances automatically.
216    ///
217    /// # Errors
218    ///
219    /// Returns [Error::PositionOutOfRange] if `position >= queue size`.
220    pub fn ack(&mut self, position: u64) -> Result<(), Error> {
221        let size = self.journal.size();
222        if position >= size {
223            return Err(Error::PositionOutOfRange(position, size));
224        }
225
226        // Already acked (below floor)
227        if position < self.ack_floor {
228            return Ok(());
229        }
230
231        // Already acked (above floor)
232        if self.acked_above.get(&position).is_some() {
233            return Ok(());
234        }
235
236        // Check if we can advance the floor
237        if position == self.ack_floor {
238            // Advance floor, consuming any contiguous acked items
239            let next = position + 1;
240            let final_floor = match self.acked_above.get(&next) {
241                Some((_, end)) => end + 1,
242                None => next,
243            };
244            self.acked_above.remove(next, final_floor - 1);
245            self.ack_floor = final_floor;
246            let _ = self.metrics.floor.try_set(self.ack_floor);
247            debug!(floor = self.ack_floor, "advanced ack floor");
248        } else {
249            // Floor is not advancing, so add to acked_above
250            self.acked_above.insert(position);
251            debug!(position, "acked item above floor");
252        }
253        Ok(())
254    }
255
256    /// Acknowledge all items in `[ack_floor, up_to)` by advancing the floor
257    /// directly. More efficient than calling [Self::ack] in a loop.
258    ///
259    /// # Errors
260    ///
261    /// Returns [Error::PositionOutOfRange] if `up_to > queue size`.
262    pub fn ack_up_to(&mut self, up_to: u64) -> Result<(), Error> {
263        let size = self.journal.size();
264        if up_to > size {
265            return Err(Error::PositionOutOfRange(up_to, size));
266        }
267
268        // Nothing to do if up_to is at or below current floor
269        if up_to <= self.ack_floor {
270            return Ok(());
271        }
272
273        // Determine final floor: either up_to, or past any contiguous acked range at up_to
274        let final_floor = match self.acked_above.get(&up_to) {
275            Some((_, end)) => end + 1,
276            None => up_to,
277        };
278
279        // Remove all entries covered by the new floor and advance
280        self.acked_above.remove(self.ack_floor, final_floor - 1);
281        self.ack_floor = final_floor;
282        let _ = self.metrics.floor.try_set(self.ack_floor);
283        debug!(floor = self.ack_floor, "batch acked up to");
284        Ok(())
285    }
286
287    /// Returns the current read position.
288    ///
289    /// This is the position of the next item that will be checked by [Queue::dequeue].
290    pub const fn read_position(&self) -> u64 {
291        self.read_pos
292    }
293
294    /// Returns the current ack floor.
295    ///
296    /// All items at positions less than this value are considered acknowledged.
297    pub const fn ack_floor(&self) -> u64 {
298        self.ack_floor
299    }
300
301    /// Returns the total number of items that have been enqueued.
302    ///
303    /// This count is not affected by pruning. It represents the position that the
304    /// next enqueued item will receive.
305    pub const fn size(&self) -> u64 {
306        self.journal.size()
307    }
308
309    /// Returns whether all enqueued items have been acknowledged.
310    pub const fn is_empty(&self) -> bool {
311        // If acked_above is non-empty, there's a gap at ack_floor (otherwise floor
312        // would have advanced). So all items acked implies ack_floor == size.
313        self.ack_floor >= self.journal.size()
314    }
315
316    /// Reset the read position to the ack floor so [Self::dequeue] re-delivers
317    /// all unacknowledged items from the beginning.
318    pub fn reset(&mut self) {
319        let old_pos = self.read_pos;
320        self.read_pos = self.ack_floor;
321        let _ = self.metrics.next.try_set(self.read_pos);
322        debug!(
323            old_read_pos = old_pos,
324            new_read_pos = self.read_pos,
325            "reset read position"
326        );
327    }
328
329    /// Returns the number of items not yet read (test-only).
330    #[cfg(test)]
331    pub(crate) const fn pending(&self) -> u64 {
332        self.journal.size().saturating_sub(self.read_pos)
333    }
334
335    /// Durably persist the queue, guaranteeing the current state will survive a crash.
336    ///
337    /// This does not persist acknowledgements. For a stronger guarantee that eliminates potential
338    /// recovery and prunes acknowledged items, use [Self::sync] instead.
339    pub async fn commit(&mut self) -> Result<(), Error> {
340        self.journal.commit().await?;
341        Ok(())
342    }
343
344    /// Durably persist the queue, guaranteeing the current state will survive a crash, and that
345    /// no recovery will be needed on startup.
346    ///
347    /// This also prunes acknowledged items.
348    pub async fn sync(&mut self) -> Result<(), Error> {
349        self.journal.sync().await?;
350        self.journal.prune(self.ack_floor).await?;
351        Ok(())
352    }
353
354    /// Destroy the queue, removing all data from disk.
355    #[boxed]
356    pub async fn destroy(self) -> Result<(), Error> {
357        self.journal.destroy().await?;
358        Ok(())
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use commonware_codec::RangeCfg;
366    use commonware_macros::test_traced;
367    use commonware_runtime::{
368        buffer::paged::CacheRef, deterministic, BufferPooler, Metrics as _, Runner, Supervisor as _,
369    };
370    use commonware_utils::{NZUsize, NZU16, NZU64};
371    use std::num::NonZeroU16;
372
373    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
374    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
375
376    fn test_config(partition: &str, pooler: &impl BufferPooler) -> Config<(RangeCfg<usize>, ())> {
377        Config {
378            partition: partition.into(),
379            items_per_section: NZU64!(10),
380            compression: None,
381            codec_config: ((0..).into(), ()),
382            page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
383            write_buffer: NZUsize!(4096),
384        }
385    }
386
387    fn acked_above_count<E: Context, V: CodecShared>(queue: &Queue<E, V>) -> usize {
388        queue
389            .acked_above
390            .iter()
391            .map(|(&s, &e)| (e - s + 1) as usize)
392            .sum()
393    }
394
395    #[test_traced]
396    fn test_basic_enqueue_dequeue() {
397        let executor = deterministic::Runner::default();
398        executor.start(|context| async move {
399            let cfg = test_config("test_basic", &context);
400            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
401                .await
402                .unwrap();
403
404            // Queue should be empty initially
405            assert!(queue.is_empty());
406            assert_eq!(queue.pending(), 0);
407            assert_eq!(queue.size(), 0);
408
409            // Enqueue items
410            let pos0 = queue.enqueue(b"item0".to_vec()).await.unwrap();
411            let pos1 = queue.enqueue(b"item1".to_vec()).await.unwrap();
412            let pos2 = queue.enqueue(b"item2".to_vec()).await.unwrap();
413
414            assert_eq!(pos0, 0);
415            assert_eq!(pos1, 1);
416            assert_eq!(pos2, 2);
417            assert_eq!(queue.size(), 3);
418            assert_eq!(queue.pending(), 3);
419            assert!(!queue.is_empty());
420
421            // Dequeue items
422            let (p, item) = queue.dequeue().await.unwrap().unwrap();
423            assert_eq!(p, 0);
424            assert_eq!(item, b"item0");
425            assert_eq!(queue.pending(), 2);
426
427            let (p, item) = queue.dequeue().await.unwrap().unwrap();
428            assert_eq!(p, 1);
429            assert_eq!(item, b"item1");
430            assert_eq!(queue.pending(), 1);
431
432            let (p, item) = queue.dequeue().await.unwrap().unwrap();
433            assert_eq!(p, 2);
434            assert_eq!(item, b"item2");
435            assert_eq!(queue.pending(), 0);
436
437            // Queue still has unacked items
438            assert!(!queue.is_empty());
439            assert!(queue.dequeue().await.unwrap().is_none());
440        });
441    }
442
443    #[test_traced]
444    fn test_append_commit_batch() {
445        let executor = deterministic::Runner::default();
446        executor.start(|context| async move {
447            let cfg = test_config("test_batch", &context);
448            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
449                .await
450                .unwrap();
451
452            // Append multiple items, then commit once
453            for i in 0..5u8 {
454                queue.append(vec![i]).await.unwrap();
455            }
456            queue.commit().await.unwrap();
457            assert_eq!(queue.size(), 5);
458
459            // Dequeue and verify order
460            for i in 0..5 {
461                let (pos, item) = queue.dequeue().await.unwrap().unwrap();
462                assert_eq!(pos, i);
463                assert_eq!(item, vec![i as u8]);
464            }
465
466            // Mix batch and single enqueue
467            for i in 5..8u8 {
468                queue.append(vec![i]).await.unwrap();
469            }
470            queue.commit().await.unwrap();
471            queue.enqueue(vec![8]).await.unwrap();
472            assert_eq!(queue.size(), 9);
473
474            queue.ack_up_to(9).unwrap();
475            assert!(queue.is_empty());
476        });
477    }
478
479    #[test_traced]
480    fn test_append_commit_persistence() {
481        let executor = deterministic::Runner::default();
482        executor.start(|context| async move {
483            let cfg = test_config("test_batch_persist", &context);
484
485            {
486                let mut queue = Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
487                    .await
488                    .unwrap();
489                for i in 0..4u8 {
490                    queue.append(vec![i]).await.unwrap();
491                }
492                queue.commit().await.unwrap();
493                queue.sync().await.unwrap();
494            }
495
496            {
497                let mut queue = Queue::<_, Vec<u8>>::init(context.child("second"), cfg)
498                    .await
499                    .unwrap();
500                assert_eq!(queue.size(), 4);
501                for i in 0..4 {
502                    let (pos, item) = queue.dequeue().await.unwrap().unwrap();
503                    assert_eq!(pos, i);
504                    assert_eq!(item, vec![i as u8]);
505                }
506            }
507        });
508    }
509
510    #[test_traced]
511    fn test_commit_after_sync_recovers_without_second_sync() {
512        let executor = deterministic::Runner::default();
513        executor.start(|context| async move {
514            let cfg = test_config("test_commit_after_sync_recovery", &context);
515
516            {
517                let mut queue = Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
518                    .await
519                    .unwrap();
520
521                // Establish a synced baseline so the recovery watermark is behind the next commit.
522                queue.append(b"synced".to_vec()).await.unwrap();
523                queue.commit().await.unwrap();
524                queue.sync().await.unwrap();
525
526                // Commit later data without syncing; reopen must replay it from the old watermark.
527                queue.append(b"committed-a".to_vec()).await.unwrap();
528                queue.append(b"committed-b".to_vec()).await.unwrap();
529                queue.commit().await.unwrap();
530            }
531
532            let mut queue = Queue::<_, Vec<u8>>::init(context.child("second"), cfg)
533                .await
534                .unwrap();
535            assert_eq!(queue.size(), 3);
536            for (expected_pos, expected_item) in [
537                (0, b"synced".to_vec()),
538                (1, b"committed-a".to_vec()),
539                (2, b"committed-b".to_vec()),
540            ] {
541                let (pos, item) = queue.dequeue().await.unwrap().unwrap();
542                assert_eq!(pos, expected_pos);
543                assert_eq!(item, expected_item);
544            }
545
546            queue.destroy().await.unwrap();
547        });
548    }
549
550    #[test_traced]
551    fn test_sequential_ack() {
552        let executor = deterministic::Runner::default();
553        executor.start(|context| async move {
554            let cfg = test_config("test_seq_ack", &context);
555            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
556                .await
557                .unwrap();
558
559            // Enqueue items
560            for i in 0..5u8 {
561                queue.enqueue(vec![i]).await.unwrap();
562            }
563
564            // Dequeue and ack sequentially
565            for i in 0..5 {
566                let (pos, _) = queue.dequeue().await.unwrap().unwrap();
567                assert_eq!(pos, i);
568                queue.ack(pos).unwrap();
569                assert_eq!(queue.ack_floor(), i + 1);
570            }
571
572            // All items acked
573            assert!(queue.is_empty());
574            assert_eq!(queue.ack_floor(), 5);
575        });
576    }
577
578    #[test_traced]
579    fn test_out_of_order_ack() {
580        let executor = deterministic::Runner::default();
581        executor.start(|context| async move {
582            let cfg = test_config("test_ooo_ack", &context);
583            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
584                .await
585                .unwrap();
586
587            // Enqueue items
588            for i in 0..5u8 {
589                queue.enqueue(vec![i]).await.unwrap();
590            }
591
592            // Dequeue all
593            for _ in 0..5 {
594                queue.dequeue().await.unwrap();
595            }
596
597            // Ack out of order: 2, 4, 1, 3, 0
598            queue.ack(2).unwrap();
599            assert_eq!(queue.ack_floor(), 0); // Floor doesn't move
600            assert!(queue.is_acked(2));
601
602            queue.ack(4).unwrap();
603            assert_eq!(queue.ack_floor(), 0);
604            assert!(queue.is_acked(4));
605
606            queue.ack(1).unwrap();
607            assert_eq!(queue.ack_floor(), 0);
608
609            queue.ack(3).unwrap();
610            assert_eq!(queue.ack_floor(), 0);
611
612            // Ack 0 - floor should advance to 5 (consuming 1,2,3,4)
613            queue.ack(0).unwrap();
614            assert_eq!(queue.ack_floor(), 5);
615            assert!(queue.is_empty());
616        });
617    }
618
619    #[test_traced]
620    fn test_ack_up_to() {
621        let executor = deterministic::Runner::default();
622        executor.start(|context| async move {
623            let cfg = test_config("test_ack_up_to", &context);
624            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
625                .await
626                .unwrap();
627
628            // Enqueue items
629            for i in 0..10u8 {
630                queue.enqueue(vec![i]).await.unwrap();
631            }
632
633            // Batch ack items 0-4
634            queue.ack_up_to(5).unwrap();
635            assert_eq!(queue.ack_floor(), 5);
636
637            // Items 0-4 should be acked
638            for i in 0..5 {
639                assert!(queue.is_acked(i));
640            }
641            // Items 5-9 should not be acked
642            for i in 5..10 {
643                assert!(!queue.is_acked(i));
644            }
645
646            // Dequeue should start at 5
647            let (p, _) = queue.dequeue().await.unwrap().unwrap();
648            assert_eq!(p, 5);
649        });
650    }
651
652    #[test_traced]
653    fn test_ack_up_to_with_existing_acks() {
654        let executor = deterministic::Runner::default();
655        executor.start(|context| async move {
656            let cfg = test_config("test_ack_up_to_existing", &context);
657            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
658                .await
659                .unwrap();
660
661            // Enqueue items
662            for i in 0..10u8 {
663                queue.enqueue(vec![i]).await.unwrap();
664            }
665
666            // Ack some items out of order first
667            queue.ack(7).unwrap();
668            queue.ack(8).unwrap();
669            assert_eq!(acked_above_count(&queue), 2);
670
671            // Batch ack up to 5
672            queue.ack_up_to(5).unwrap();
673            assert_eq!(queue.ack_floor(), 5);
674            assert_eq!(acked_above_count(&queue), 2);
675
676            // Now batch ack up to 9 - should consume the acked_above entries
677            queue.ack_up_to(9).unwrap();
678            assert_eq!(queue.ack_floor(), 9);
679            assert_eq!(acked_above_count(&queue), 0);
680        });
681    }
682
683    #[test_traced]
684    fn test_ack_up_to_coalesces_with_acked_above() {
685        let executor = deterministic::Runner::default();
686        executor.start(|context| async move {
687            let cfg = test_config("test_ack_up_to_coalesce", &context);
688            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
689                .await
690                .unwrap();
691
692            // Enqueue items
693            for i in 0..10u8 {
694                queue.enqueue(vec![i]).await.unwrap();
695            }
696
697            // Ack items 5, 6, 7 first
698            queue.ack(5).unwrap();
699            queue.ack(6).unwrap();
700            queue.ack(7).unwrap();
701            assert_eq!(queue.ack_floor(), 0);
702
703            // Batch ack up to 5 - should coalesce with 5, 6, 7
704            queue.ack_up_to(5).unwrap();
705            assert_eq!(queue.ack_floor(), 8); // Consumed 5, 6, 7
706        });
707    }
708
709    #[test_traced]
710    fn test_ack_up_to_errors() {
711        let executor = deterministic::Runner::default();
712        executor.start(|context| async move {
713            let cfg = test_config("test_ack_up_to_errors", &context);
714            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
715                .await
716                .unwrap();
717
718            queue.enqueue(b"item0".to_vec()).await.unwrap();
719            queue.enqueue(b"item1".to_vec()).await.unwrap();
720
721            // Can't ack_up_to beyond queue size
722            let err = queue.ack_up_to(5).unwrap_err();
723            assert!(matches!(err, Error::PositionOutOfRange(5, 2)));
724
725            // Can ack_up_to at queue size
726            queue.ack_up_to(2).unwrap();
727            assert_eq!(queue.ack_floor(), 2);
728
729            // Acking up_to at or below floor is a no-op
730            queue.ack_up_to(1).unwrap();
731            assert_eq!(queue.ack_floor(), 2);
732        });
733    }
734
735    #[test_traced]
736    fn test_dequeue_skips_acked() {
737        let executor = deterministic::Runner::default();
738        executor.start(|context| async move {
739            let cfg = test_config("test_skip_acked", &context);
740            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
741                .await
742                .unwrap();
743
744            // Enqueue items 0-4
745            for i in 0..5u8 {
746                queue.enqueue(vec![i]).await.unwrap();
747            }
748
749            // Ack items 1 and 3 before reading
750            queue.ack(1).unwrap();
751            queue.ack(3).unwrap();
752
753            // Dequeue should skip 1 and 3
754            let (p, item) = queue.dequeue().await.unwrap().unwrap();
755            assert_eq!(p, 0);
756            assert_eq!(item, vec![0]);
757
758            let (p, item) = queue.dequeue().await.unwrap().unwrap();
759            assert_eq!(p, 2); // Skipped 1
760            assert_eq!(item, vec![2]);
761
762            let (p, item) = queue.dequeue().await.unwrap().unwrap();
763            assert_eq!(p, 4); // Skipped 3
764            assert_eq!(item, vec![4]);
765
766            assert!(queue.dequeue().await.unwrap().is_none());
767        });
768    }
769
770    #[test_traced]
771    fn test_ack_errors() {
772        let executor = deterministic::Runner::default();
773        executor.start(|context| async move {
774            let cfg = test_config("test_ack_errors", &context);
775            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
776                .await
777                .unwrap();
778
779            queue.enqueue(b"item0".to_vec()).await.unwrap();
780            queue.enqueue(b"item1".to_vec()).await.unwrap();
781
782            // Can't ack position beyond queue size
783            let err = queue.ack(5).unwrap_err();
784            assert!(matches!(err, Error::PositionOutOfRange(5, 2)));
785
786            // Can ack unread items
787            queue.ack(1).unwrap();
788            assert!(queue.is_acked(1));
789
790            // Double ack is a no-op
791            queue.ack(1).unwrap();
792        });
793    }
794
795    #[test_traced]
796    fn test_prune() {
797        let executor = deterministic::Runner::default();
798        executor.start(|context| async move {
799            let cfg = test_config("test_prune", &context);
800            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
801                .await
802                .unwrap();
803
804            // Enqueue items (more than items_per_section to test pruning)
805            for i in 0..25u8 {
806                queue.enqueue(vec![i]).await.unwrap();
807            }
808            queue.sync().await.unwrap();
809
810            // Read and ack some items
811            for i in 0..15 {
812                queue.dequeue().await.unwrap();
813                queue.ack(i).unwrap();
814            }
815            assert_eq!(queue.ack_floor(), 15);
816
817            // Items 15+ should still be readable
818            let (p, item) = queue.dequeue().await.unwrap().unwrap();
819            assert_eq!(p, 15);
820            assert_eq!(item, vec![15]);
821        });
822    }
823
824    #[test_traced]
825    fn test_ack_across_sections() {
826        let executor = deterministic::Runner::default();
827        executor.start(|context| async move {
828            let cfg = test_config("test_multi_prune", &context);
829            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
830                .await
831                .unwrap();
832
833            // Enqueue many items across multiple sections (items_per_section = 10)
834            for i in 0..50u8 {
835                queue.enqueue(vec![i]).await.unwrap();
836            }
837            queue.sync().await.unwrap();
838
839            // First batch: ack items 0-14
840            for i in 0..15 {
841                queue.dequeue().await.unwrap();
842                queue.ack(i).unwrap();
843            }
844            assert_eq!(queue.ack_floor(), 15);
845
846            // Verify items 15+ still readable
847            let (p, item) = queue.dequeue().await.unwrap().unwrap();
848            assert_eq!(p, 15);
849            assert_eq!(item, vec![15]);
850
851            // Second batch: ack items 15-29
852            queue.ack(15).unwrap();
853            for i in 16..30 {
854                queue.dequeue().await.unwrap();
855                queue.ack(i).unwrap();
856            }
857            assert_eq!(queue.ack_floor(), 30);
858
859            // Verify items 30+ still readable
860            let (p, item) = queue.dequeue().await.unwrap().unwrap();
861            assert_eq!(p, 30);
862            assert_eq!(item, vec![30]);
863
864            // Third batch: ack remaining items
865            queue.ack(30).unwrap();
866            for i in 31..50 {
867                queue.dequeue().await.unwrap();
868                queue.ack(i).unwrap();
869            }
870            assert_eq!(queue.ack_floor(), 50);
871
872            // Queue should be empty now
873            assert!(queue.is_empty());
874            assert!(queue.dequeue().await.unwrap().is_none());
875        });
876    }
877
878    #[test_traced]
879    fn test_crash_recovery_replays_from_pruning_boundary() {
880        // On restart, ack_floor = pruning_boundary. Items not pruned are re-delivered.
881        let executor = deterministic::Runner::default();
882        executor.start(|context| async move {
883            let cfg = test_config("test_recovery_replay", &context);
884
885            // First session: enqueue items, ack some (but not enough to prune)
886            {
887                let mut queue = Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
888                    .await
889                    .unwrap();
890
891                for i in 0..5u8 {
892                    queue.enqueue(vec![i]).await.unwrap();
893                }
894
895                // Ack items 0, 1, 2 - but items_per_section=10, so no pruning
896                queue.ack(0).unwrap();
897                queue.ack(1).unwrap();
898                queue.ack(2).unwrap();
899                assert_eq!(queue.ack_floor(), 3);
900
901                queue.sync().await.unwrap();
902            }
903
904            // Second session: all items are re-delivered (no pruning occurred)
905            {
906                let mut queue = Queue::<_, Vec<u8>>::init(context.child("second"), cfg.clone())
907                    .await
908                    .unwrap();
909
910                // ack_floor = pruning_boundary = 0 (nothing was pruned)
911                assert_eq!(queue.ack_floor(), 0);
912
913                // All items re-delivered
914                for i in 0..5 {
915                    let (p, _) = queue.dequeue().await.unwrap().unwrap();
916                    assert_eq!(p, i);
917                }
918            }
919        });
920    }
921
922    #[test_traced]
923    fn test_crash_recovery_with_pruning() {
924        // Items pruned before crash are not re-delivered.
925        let executor = deterministic::Runner::default();
926        executor.start(|context| async move {
927            let cfg = test_config("test_recovery_pruned", &context);
928
929            // First session: enqueue many items, ack enough to trigger pruning
930            let expected_pruning_boundary = {
931                let mut queue = Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
932                    .await
933                    .unwrap();
934
935                // Enqueue items across multiple sections (items_per_section = 10)
936                for i in 0..25u8 {
937                    queue.enqueue(vec![i]).await.unwrap();
938                }
939
940                // Ack items 0-14 to advance floor past section 0
941                for i in 0..15 {
942                    queue.ack(i).unwrap();
943                }
944                assert_eq!(queue.ack_floor(), 15);
945
946                // Sync triggers pruning
947                queue.sync().await.unwrap();
948
949                // Verify pruning occurred
950                let pruning_boundary = queue.journal.bounds().start;
951                assert!(pruning_boundary > 0, "expected some pruning to occur");
952
953                pruning_boundary
954            };
955
956            // Second session: only non-pruned items are available
957            {
958                let mut queue = Queue::<_, Vec<u8>>::init(context.child("second"), cfg.clone())
959                    .await
960                    .unwrap();
961
962                // ack_floor = pruning_boundary (items 0-9 were pruned)
963                let pruning_boundary = queue.journal.bounds().start;
964                assert_eq!(queue.ack_floor(), pruning_boundary);
965                assert_eq!(pruning_boundary, expected_pruning_boundary);
966
967                // Items from pruning_boundary to 24 are re-delivered
968                for i in pruning_boundary..25 {
969                    let (p, item) = queue.dequeue().await.unwrap().unwrap();
970                    assert_eq!(p, i);
971                    assert_eq!(item, vec![i as u8]);
972                }
973
974                assert!(queue.dequeue().await.unwrap().is_none());
975            }
976        });
977    }
978
979    #[test_traced]
980    fn test_reset() {
981        let executor = deterministic::Runner::default();
982        executor.start(|context| async move {
983            let cfg = test_config("test_reset", &context);
984            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
985                .await
986                .unwrap();
987
988            // Enqueue items
989            for i in 0..5u8 {
990                queue.enqueue(vec![i]).await.unwrap();
991            }
992
993            // Read some
994            queue.dequeue().await.unwrap();
995            queue.dequeue().await.unwrap();
996            queue.dequeue().await.unwrap();
997            assert_eq!(queue.read_position(), 3);
998
999            // Reset without ack - should go back to 0
1000            queue.reset();
1001            assert_eq!(queue.read_position(), 0);
1002
1003            // Verify we can re-read
1004            let (p, item) = queue.dequeue().await.unwrap().unwrap();
1005            assert_eq!(p, 0);
1006            assert_eq!(item, vec![0]);
1007        });
1008    }
1009
1010    #[test_traced]
1011    fn test_reset_with_ack() {
1012        let executor = deterministic::Runner::default();
1013        executor.start(|context| async move {
1014            let cfg = test_config("test_reset_ack", &context);
1015            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1016                .await
1017                .unwrap();
1018
1019            // Enqueue items
1020            for i in 0..10u8 {
1021                queue.enqueue(vec![i]).await.unwrap();
1022            }
1023
1024            // Read and ack some
1025            for i in 0..5 {
1026                queue.dequeue().await.unwrap();
1027                queue.ack(i).unwrap();
1028            }
1029            assert_eq!(queue.ack_floor(), 5);
1030            assert_eq!(queue.read_position(), 5);
1031
1032            // Read a few more
1033            queue.dequeue().await.unwrap();
1034            queue.dequeue().await.unwrap();
1035            assert_eq!(queue.read_position(), 7);
1036
1037            // Reset - should go back to ack floor
1038            queue.reset();
1039            assert_eq!(queue.read_position(), 5);
1040
1041            // Next dequeue should return item 5
1042            let (p, item) = queue.dequeue().await.unwrap().unwrap();
1043            assert_eq!(p, 5);
1044            assert_eq!(item, vec![5]);
1045        });
1046    }
1047
1048    #[test_traced]
1049    fn test_empty_queue_operations() {
1050        let executor = deterministic::Runner::default();
1051        executor.start(|context| async move {
1052            let cfg = test_config("test_empty", &context);
1053            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1054                .await
1055                .unwrap();
1056
1057            // Operations on empty queue
1058            assert!(queue.is_empty());
1059            assert!(queue.dequeue().await.unwrap().is_none());
1060            queue.sync().await.unwrap();
1061            queue.reset();
1062        });
1063    }
1064
1065    #[test_traced]
1066    fn test_persistence() {
1067        let executor = deterministic::Runner::default();
1068        executor.start(|context| async move {
1069            let cfg = test_config("test_persist", &context);
1070
1071            // First session
1072            {
1073                let mut queue = Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
1074                    .await
1075                    .unwrap();
1076
1077                queue.enqueue(b"item0".to_vec()).await.unwrap();
1078                queue.enqueue(b"item1".to_vec()).await.unwrap();
1079                queue.sync().await.unwrap();
1080            }
1081
1082            // Second session - data should persist
1083            {
1084                let mut queue = Queue::<_, Vec<u8>>::init(context.child("second"), cfg.clone())
1085                    .await
1086                    .unwrap();
1087
1088                assert_eq!(queue.size(), 2);
1089
1090                let (_, item) = queue.dequeue().await.unwrap().unwrap();
1091                assert_eq!(item, b"item0");
1092
1093                let (_, item) = queue.dequeue().await.unwrap().unwrap();
1094                assert_eq!(item, b"item1");
1095            }
1096        });
1097    }
1098
1099    #[test_traced]
1100    fn test_large_queue_with_sparse_acks() {
1101        let executor = deterministic::Runner::default();
1102        executor.start(|context| async move {
1103            let cfg = test_config("test_sparse", &context);
1104            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1105                .await
1106                .unwrap();
1107
1108            // Enqueue many items
1109            for i in 0..100u8 {
1110                queue.enqueue(vec![i]).await.unwrap();
1111            }
1112
1113            // Ack every 3rd item (sparse acking)
1114            for i in (0..100).step_by(3) {
1115                queue.ack(i).unwrap();
1116            }
1117
1118            // Dequeue should skip acked items
1119            let mut received = Vec::new();
1120            while let Some((pos, _)) = queue.dequeue().await.unwrap() {
1121                received.push(pos);
1122            }
1123
1124            // Should have received all items not divisible by 3
1125            let expected: Vec<u64> = (0..100).filter(|x| x % 3 != 0).collect();
1126            assert_eq!(received, expected);
1127        });
1128    }
1129
1130    #[test_traced]
1131    fn test_acked_above_coalescing() {
1132        let executor = deterministic::Runner::default();
1133        executor.start(|context| async move {
1134            let cfg = test_config("test_coalesce", &context);
1135            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1136                .await
1137                .unwrap();
1138
1139            // Enqueue items
1140            for i in 0..10u8 {
1141                queue.enqueue(vec![i]).await.unwrap();
1142            }
1143
1144            // Ack items 1-8 (not 0)
1145            for i in 1..9 {
1146                queue.ack(i).unwrap();
1147            }
1148
1149            // Acked_above should have items 1-8
1150            assert_eq!(queue.ack_floor(), 0);
1151            assert!(acked_above_count(&queue) > 0);
1152
1153            // Now ack 0 - floor should advance to 9, consuming all acked_above
1154            queue.ack(0).unwrap();
1155            assert_eq!(queue.ack_floor(), 9);
1156            assert_eq!(acked_above_count(&queue), 0);
1157        });
1158    }
1159
1160    #[test_traced]
1161    fn test_ack_up_to_past_read_pos() {
1162        let executor = deterministic::Runner::default();
1163        executor.start(|context| async move {
1164            let cfg = test_config("test_ack_up_to_past_read_pos", &context);
1165            let mut queue = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1166                .await
1167                .unwrap();
1168
1169            for i in 0..10u8 {
1170                queue.enqueue(vec![i]).await.unwrap();
1171            }
1172
1173            // Read only 3 items
1174            for _ in 0..3 {
1175                queue.dequeue().await.unwrap();
1176            }
1177            assert_eq!(queue.read_position(), 3);
1178
1179            // Batch ack past read position
1180            queue.ack_up_to(7).unwrap();
1181            assert_eq!(queue.ack_floor(), 7);
1182
1183            // Dequeue should skip 3-6 and return 7
1184            let (pos, item) = queue.dequeue().await.unwrap().unwrap();
1185            assert_eq!(pos, 7);
1186            assert_eq!(item, vec![7]);
1187        });
1188    }
1189
1190    #[test_traced]
1191    fn test_metrics() {
1192        let executor = deterministic::Runner::default();
1193        executor.start(|context| async move {
1194            let cfg = test_config("test-metrics", &context);
1195            let ctx = context.child("test_metrics");
1196            let mut queue = Queue::<_, Vec<u8>>::init(ctx, cfg).await.unwrap();
1197
1198            let encoded = context.encode();
1199            assert!(
1200                encoded.contains("test_metrics_tip 0"),
1201                "expected tip 0: {encoded}"
1202            );
1203            assert!(
1204                encoded.contains("test_metrics_floor 0"),
1205                "expected floor 0: {encoded}"
1206            );
1207            assert!(
1208                encoded.contains("test_metrics_next 0"),
1209                "expected next 0: {encoded}"
1210            );
1211
1212            // Append updates tip without enqueue
1213            queue.append(vec![0]).await.unwrap();
1214            let encoded = context.encode();
1215            assert!(
1216                encoded.contains("test_metrics_tip 1"),
1217                "expected tip 1: {encoded}"
1218            );
1219            queue.commit().await.unwrap();
1220
1221            // Enqueue updates tip further
1222            for i in 1..10u8 {
1223                queue.enqueue(vec![i]).await.unwrap();
1224            }
1225            let encoded = context.encode();
1226            assert!(
1227                encoded.contains("test_metrics_tip 10"),
1228                "expected tip 10: {encoded}"
1229            );
1230
1231            // Multiple dequeues advance next
1232            queue.dequeue().await.unwrap();
1233            queue.dequeue().await.unwrap();
1234            let encoded = context.encode();
1235            assert!(
1236                encoded.contains("test_metrics_next 2"),
1237                "expected next 2: {encoded}"
1238            );
1239
1240            // Sequential ack advances floor
1241            queue.ack(0).unwrap();
1242            queue.ack(1).unwrap();
1243            let encoded = context.encode();
1244            assert!(
1245                encoded.contains("test_metrics_floor 2"),
1246                "expected floor 2: {encoded}"
1247            );
1248
1249            // Out-of-order ack: floor stays until gap fills
1250            queue.ack(4).unwrap();
1251            queue.ack(6).unwrap();
1252            let encoded = context.encode();
1253            assert!(
1254                encoded.contains("test_metrics_floor 2"),
1255                "expected floor still 2: {encoded}"
1256            );
1257
1258            // Fill gap coalesces floor forward
1259            queue.ack(2).unwrap();
1260            queue.ack(3).unwrap();
1261            let encoded = context.encode();
1262            assert!(
1263                encoded.contains("test_metrics_floor 5"),
1264                "expected floor 5: {encoded}"
1265            );
1266
1267            // ack_up_to advances floor past sparse ack at 6
1268            queue.ack_up_to(8).unwrap();
1269            let encoded = context.encode();
1270            assert!(
1271                encoded.contains("test_metrics_floor 8"),
1272                "expected floor 8: {encoded}"
1273            );
1274
1275            // Ack remaining
1276            queue.ack(8).unwrap();
1277            queue.ack(9).unwrap();
1278            let encoded = context.encode();
1279            assert!(
1280                encoded.contains("test_metrics_floor 10"),
1281                "expected floor 10: {encoded}"
1282            );
1283
1284            // Reset brings next back to floor
1285            queue.reset();
1286            let encoded = context.encode();
1287            assert!(
1288                encoded.contains("test_metrics_next 10"),
1289                "expected next 10: {encoded}"
1290            );
1291        });
1292    }
1293
1294    #[test_traced]
1295    fn test_metrics_next_updates_on_fast_forward() {
1296        let executor = deterministic::Runner::default();
1297        executor.start(|context| async move {
1298            let cfg = test_config("test-ff", &context);
1299            let ctx = context.child("test_ff");
1300            let mut queue = Queue::<_, Vec<u8>>::init(ctx, cfg).await.unwrap();
1301
1302            // Enqueue 3 items, dequeue and ack only the first
1303            for i in 0..3u8 {
1304                queue.enqueue(vec![i]).await.unwrap();
1305            }
1306            let (pos, _) = queue.dequeue().await.unwrap().unwrap();
1307            queue.ack(pos).unwrap();
1308
1309            let encoded = context.encode();
1310            assert!(
1311                encoded.contains("test_ff_next 1"),
1312                "expected next 1: {encoded}"
1313            );
1314
1315            // Ack remaining items out-of-order to advance floor to 3
1316            queue.ack(2).unwrap();
1317            queue.ack(1).unwrap();
1318            assert_eq!(queue.ack_floor(), 3);
1319
1320            // next metric is still 1 (no dequeue yet)
1321            let encoded = context.encode();
1322            assert!(
1323                encoded.contains("test_ff_next 1"),
1324                "expected next still 1: {encoded}"
1325            );
1326
1327            // Dequeue returns None but fast-forwards read_pos to ack_floor
1328            assert!(queue.dequeue().await.unwrap().is_none());
1329            let encoded = context.encode();
1330            assert!(
1331                encoded.contains("test_ff_next 3"),
1332                "expected next 3 after fast-forward: {encoded}"
1333            );
1334        });
1335    }
1336}