Skip to main content

commonware_storage/queue/
storage.rs

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