Skip to main content

commonware_storage/queue/
storage.rs

1//! Queue storage implementation.
2
3use super::{Error, cursor::Cursor, metrics::Metrics};
4use crate::{
5    Context,
6    journal::contiguous::{Contiguous as _, variable},
7};
8use commonware_codec::CodecShared;
9use commonware_macros::boxed;
10use commonware_runtime::{
11    buffer::paged::CacheRef,
12    telemetry::metrics::{Gauge, GaugeExt as _},
13};
14use commonware_utils::channel::watch;
15use std::{
16    borrow::Borrow,
17    num::{NonZeroU64, NonZeroUsize},
18    ops::Range,
19    sync::{
20        Arc,
21        atomic::{AtomicU64, Ordering},
22    },
23};
24use tracing::debug;
25
26/// Configuration for [Queue].
27#[derive(Clone)]
28pub struct Config<C> {
29    /// The storage partition name for the queue's journal.
30    pub partition: String,
31
32    /// The number of items to store in each journal section.
33    ///
34    /// Larger values reduce file overhead but increase minimum pruning granularity.
35    /// Once set, this value cannot be changed across restarts.
36    pub items_per_section: NonZeroU64,
37
38    /// Optional zstd compression level for stored items.
39    ///
40    /// If set, items will be compressed before storage. Higher values provide
41    /// better compression but use more CPU.
42    ///
43    /// Keep the choice between `None` and `Some(_)` fixed while stored items are retained.
44    /// Only the compression level may change between initializations when compression is enabled.
45    pub compression: Option<u8>,
46
47    /// Codec configuration for encoding/decoding items.
48    pub codec_config: C,
49
50    /// Page cache for buffering reads from the underlying journal.
51    pub page_cache: CacheRef,
52
53    /// Write buffer size for each section.
54    pub write_buffer: NonZeroUsize,
55
56    /// Buffer size for sequential reads during recovery.
57    pub replay_buffer: NonZeroUsize,
58}
59
60/// A published view of the queue's items.
61type Snapshot<E, V> = Arc<variable::Reader<'static, E, V>>;
62
63/// A durable, at-least-once delivery queue with per-item acknowledgment.
64///
65/// Items are durably stored in a journal and survive crashes. [Queue::init] returns the queue,
66/// which writes items, and its [Reader], which delivers them. The reader must acknowledge each
67/// item individually after processing. Items can be acknowledged out of order, enabling parallel
68/// processing.
69///
70/// # Operations
71///
72/// - [append](Self::append) / [commit](Self::commit): Write items to the journal, then persist
73///   them and publish them to the reader. Appended items are lost on restart if not committed.
74/// - [enqueue](Self::enqueue): Append + commit in one step. The item is durable before return.
75/// - [sync](Self::sync): Commit, then prune completed sections below the reader's ack floor.
76/// - [Reader::recv] / [Reader::try_recv]: Return the next unacked committed item in FIFO order.
77/// - [Reader::ack] / [Reader::ack_up_to]: Mark items as processed (in-memory only).
78///
79/// # Acknowledgment
80///
81/// Acks are tracked in-memory with an `ack_floor` (all positions below are acked)
82/// plus an [RMap](crate::rmap::RMap) of acked positions above the floor. When items are acked
83/// contiguously from the floor, the floor advances automatically.
84///
85/// Acks are **not** persisted. The durable equivalent is the journal's pruning
86/// boundary, advanced by [sync](Self::sync). On restart, all non-pruned
87/// items are re-delivered regardless of prior ack state.
88///
89/// # Crash Recovery
90///
91/// On restart, `ack_floor` is set to the journal's pruning boundary.
92/// Items that were pruned are gone. Everything else is re-delivered.
93/// Applications must handle duplicates (idempotent processing).
94///
95/// Storage-mutating functions consume the queue and return it only on success: an error (or a
96/// dropped future) destroys the handle. The reader then delivers every published item and returns
97/// `None`.
98pub struct Queue<E: Context, V: CodecShared> {
99    /// The underlying journal storing queue items.
100    journal: variable::Journal<E, V>,
101
102    /// Total enqueued items.
103    tip: Gauge,
104
105    /// Bounds of the last published snapshot.
106    published: Range<u64>,
107
108    /// Publishes snapshots to the reader.
109    snapshots: watch::Sender<Snapshot<E, V>>,
110
111    /// The reader's ack floor.
112    floor: Arc<AtomicU64>,
113}
114
115impl<E: Context, V: CodecShared> Queue<E, V> {
116    /// Initialize a queue from storage, returning it with its [Reader].
117    ///
118    /// On first initialization, creates an empty queue. On restart, the reader begins from the
119    /// journal's pruning boundary (providing at-least-once delivery for all non-pruned items).
120    /// Both handles must be dropped before the queue is reopened.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the underlying journal cannot be initialized.
125    #[boxed]
126    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<(Self, Reader<E, V>), Error> {
127        // Initialize metrics before creating sub-contexts
128        let metrics = Metrics::init(&context);
129
130        // Recover the journal that backs the queue's retained items.
131        let journal = variable::Journal::init(
132            context.child("journal"),
133            variable::Config {
134                partition: cfg.partition,
135                items_per_section: cfg.items_per_section,
136                compression: cfg.compression,
137                codec_config: cfg.codec_config,
138                page_cache: cfg.page_cache,
139                write_buffer: cfg.write_buffer,
140                replay_buffer: cfg.replay_buffer,
141            },
142        )
143        .await?;
144
145        // On restart, the ack floor is the pruning boundary (items below are deleted).
146        // In-memory acknowledgements are lost on restart.
147        let bounds = journal.bounds();
148        debug!(floor = bounds.start, size = bounds.end, "queue initialized");
149        let _ = metrics.tip.try_set(bounds.end);
150        let cursor = Cursor::new(bounds.start, metrics.next, metrics.floor);
151
152        // Share the recovered view and the acknowledgement floor between the queue and its
153        // reader.
154        let (journal, snapshot) = journal.snapshot().await?;
155        let snapshot = Arc::new(snapshot);
156        let (sender, receiver) = watch::channel(snapshot.clone());
157        let floor = Arc::new(AtomicU64::new(bounds.start));
158        let queue = Self {
159            journal,
160            tip: metrics.tip,
161            published: bounds,
162            snapshots: sender,
163            floor: floor.clone(),
164        };
165        let reader = Reader {
166            snapshot,
167            snapshots: receiver,
168            cursor,
169            floor,
170        };
171        Ok((queue, reader))
172    }
173
174    /// Append and commit an item, returning its position. The reader can receive the item once
175    /// this returns.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if the underlying storage operation fails.
180    pub async fn enqueue(self, item: &V) -> Result<(Self, u64), Error> {
181        let (queue, pos) = self.append(item).await?;
182        let queue = queue.commit().await?;
183        debug!(position = pos, "enqueued item");
184        Ok((queue, pos))
185    }
186
187    /// Append a batch of items with a single commit, returning positions `[start, end)`. The
188    /// reader can receive the items once this returns.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if any append or the final commit fails.
193    pub async fn enqueue_bulk(
194        mut self,
195        items: impl IntoIterator<Item = impl Borrow<V>>,
196    ) -> Result<(Self, Range<u64>), Error> {
197        // Append the batch before publishing it to the reader.
198        let start = self.journal.size();
199        for item in items {
200            (self, _) = self.append(item.borrow()).await?;
201        }
202
203        // Only a nonempty batch needs a commit.
204        let end = self.journal.size();
205        if end > start {
206            self = self.commit().await?;
207        }
208        debug!(start, end, "enqueued bulk");
209        Ok((self, start..end))
210    }
211
212    /// Append an item without committing, returning its position. The reader can receive the
213    /// item after the next [Self::commit] or [Self::sync].
214    ///
215    /// # Errors
216    ///
217    /// Returns an error if the underlying storage operation fails.
218    pub async fn append(mut self, item: &V) -> Result<(Self, u64), Error> {
219        let pos;
220        (self.journal, pos) = self.journal.append(item).await?;
221        let _ = self.tip.try_set(pos + 1);
222        debug!(position = pos, "appended item");
223        Ok((self, pos))
224    }
225
226    /// Durably persist appended items and publish them to the reader.
227    ///
228    /// This does not persist acknowledgements. For a stronger guarantee that eliminates potential
229    /// recovery and prunes acknowledged items, use [Self::sync] instead.
230    pub async fn commit(mut self) -> Result<Self, Error> {
231        self.journal = self.journal.commit().await?;
232        self.publish().await
233    }
234
235    /// Durably persist the queue, guaranteeing the current state will survive a crash, and that
236    /// no recovery will be needed on startup.
237    ///
238    /// This also prunes items below the reader's ack floor and publishes the result to the reader.
239    pub async fn sync(mut self) -> Result<Self, Error> {
240        // Make appended items durable before pruning their predecessors.
241        self.journal = self.journal.sync().await?;
242
243        // Sample the reader's monotonic floor. A stale value only delays pruning.
244        let floor = self.floor.load(Ordering::Relaxed);
245        (self.journal, _) = self.journal.prune(floor).await?;
246
247        // Publish a view with the retained bounds.
248        self.publish().await
249    }
250
251    /// Returns the total number of items that have been appended.
252    pub fn size(&self) -> u64 {
253        self.journal.size()
254    }
255
256    /// Publish a snapshot of the journal if its bounds changed since the last one.
257    async fn publish(mut self) -> Result<Self, Error> {
258        let bounds = self.journal.bounds();
259        if bounds == self.published {
260            return Ok(self);
261        }
262
263        // Capture the new view before publishing its bounds and waking the reader.
264        let snapshot;
265        (self.journal, snapshot) = self.journal.snapshot().await?;
266        self.published = bounds;
267        self.snapshots.send_replace(Arc::new(snapshot));
268        Ok(self)
269    }
270
271    /// Destroy the queue, removing all data from disk.
272    #[boxed]
273    pub async fn destroy(self) -> Result<(), Error> {
274        self.journal.destroy().await?;
275        Ok(())
276    }
277}
278
279impl<E: Context, V: CodecShared> std::fmt::Debug for Queue<E, V> {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        f.debug_struct("Queue")
282            .field("size", &self.size())
283            .finish_non_exhaustive()
284    }
285}
286
287/// Delivers a [Queue]'s committed items and tracks their acknowledgement.
288///
289/// Acknowledgements are in-memory, and [Queue::sync] prunes items below the ack floor.
290pub struct Reader<E: Context, V: CodecShared> {
291    /// The snapshot items are delivered from.
292    snapshot: Snapshot<E, V>,
293
294    /// Receives snapshots from the queue.
295    snapshots: watch::Receiver<Snapshot<E, V>>,
296
297    /// Delivery and acknowledgement state.
298    cursor: Cursor,
299
300    /// The ack floor shared with the queue.
301    floor: Arc<AtomicU64>,
302}
303
304impl<E: Context, V: CodecShared> Reader<E, V> {
305    /// Receive the next unacknowledged item, waiting if necessary.
306    ///
307    /// This method is designed for use with `select!`. It will:
308    /// 1. Return immediately if an unacked item has been published
309    /// 2. Wait for the queue to publish new items otherwise
310    /// 3. Return `None` once the queue is dropped and every published item has been delivered
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if the underlying storage operation fails.
315    pub async fn recv(&mut self) -> Result<Option<(u64, V)>, Error> {
316        // Drain published items before waiting for the next snapshot or the queue's departure.
317        loop {
318            if let Some(item) = self.try_recv().await? {
319                return Ok(Some(item));
320            }
321
322            // `try_recv` has seen the newest snapshot, so wait for the next one. `changed` marks it
323            // seen, so move to it here.
324            if self.snapshots.changed().await.is_err() {
325                return Ok(None);
326            }
327            self.snapshot = self.snapshots.borrow_and_update().clone();
328        }
329    }
330
331    /// Try to dequeue the next unacknowledged item without waiting.
332    ///
333    /// Returns `None` immediately if no unacked item has been published.
334    ///
335    /// # Errors
336    ///
337    /// Returns an error if the underlying storage operation fails.
338    pub async fn try_recv(&mut self) -> Result<Option<(u64, V)>, Error> {
339        // Move to the newest snapshot before reading, releasing blobs the queue has pruned. A
340        // closed channel still holds the queue's final snapshot.
341        if self.snapshots.has_changed().unwrap_or(true) {
342            self.snapshot = self.snapshots.borrow_and_update().clone();
343        }
344        self.cursor.dequeue(&*self.snapshot).await
345    }
346
347    /// Returns whether a specific position has been acknowledged.
348    pub fn is_acked(&self, position: u64) -> bool {
349        self.cursor.is_acked(position)
350    }
351
352    /// Mark the item at `position` as processed (in-memory only). The item is skipped on
353    /// subsequent receives. If this creates a contiguous run from the ack floor, the floor
354    /// advances.
355    ///
356    /// # Errors
357    ///
358    /// Returns [Error::PositionOutOfRange] if `position` has not been published.
359    pub fn ack(&mut self, position: u64) -> Result<(), Error> {
360        self.cursor.ack(position, self.published())?;
361        self.floor.store(self.cursor.ack_floor(), Ordering::Relaxed);
362        Ok(())
363    }
364
365    /// Acknowledge all items in `[ack_floor, up_to)` by advancing the floor directly. More
366    /// efficient than calling [Self::ack] in a loop.
367    ///
368    /// # Errors
369    ///
370    /// Returns [Error::PositionOutOfRange] if `up_to` exceeds the published items.
371    pub fn ack_up_to(&mut self, up_to: u64) -> Result<(), Error> {
372        self.cursor.ack_up_to(up_to, self.published())?;
373        self.floor.store(self.cursor.ack_floor(), Ordering::Relaxed);
374        Ok(())
375    }
376
377    /// Returns the current ack floor.
378    ///
379    /// All items at positions less than this value are considered acknowledged.
380    pub const fn ack_floor(&self) -> u64 {
381        self.cursor.ack_floor()
382    }
383
384    /// Returns the current read position.
385    ///
386    /// This is the position of the next item [Self::try_recv] will check.
387    pub const fn read_position(&self) -> u64 {
388        self.cursor.read_position()
389    }
390
391    /// Returns whether every published item has been acknowledged.
392    pub fn is_empty(&self) -> bool {
393        self.cursor.is_empty(self.published())
394    }
395
396    /// Reset the read position to the ack floor so the reader re-delivers every unacknowledged
397    /// item.
398    pub fn reset(&mut self) {
399        self.cursor.reset();
400    }
401
402    /// Returns the number of published items.
403    fn published(&self) -> u64 {
404        self.snapshots.borrow().bounds().end
405    }
406
407    /// Returns the number of published items not yet read (test-only).
408    #[cfg(test)]
409    fn pending(&self) -> u64 {
410        self.published().saturating_sub(self.cursor.read_position())
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use commonware_codec::RangeCfg;
418    use commonware_macros::{select, test_traced};
419    use commonware_runtime::{
420        Metrics as _, Runner, Spawner, Supervisor as _,
421        buffer::paged::CacheRef,
422        deterministic,
423        mocks::{DelayedSyncContext, PendingSyncs},
424    };
425    use commonware_utils::{NZU16, NZU64, NZUsize};
426    use std::num::NonZeroU16;
427
428    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
429    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
430
431    fn test_config(partition: &str) -> Config<(RangeCfg<usize>, ())> {
432        Config {
433            partition: partition.into(),
434            items_per_section: NZU64!(10),
435            compression: None,
436            codec_config: ((0..).into(), ()),
437            page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
438            write_buffer: NZUsize!(4096),
439            replay_buffer: NZUsize!(4096),
440        }
441    }
442
443    async fn init<E: Context>(
444        context: E,
445        cfg: Config<(RangeCfg<usize>, ())>,
446    ) -> Result<(Queue<E, Vec<u8>>, Reader<E, Vec<u8>>), Error> {
447        Queue::init(context, cfg).await
448    }
449
450    fn acked_above_count<E: Context, V: CodecShared>(reader: &Reader<E, V>) -> usize {
451        reader.cursor.acked_above_count()
452    }
453
454    #[test_traced]
455    fn test_basic_enqueue_dequeue() {
456        let executor = deterministic::Runner::default();
457        executor.start(|context| async move {
458            let cfg = test_config("test_basic");
459            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
460                .await
461                .unwrap();
462
463            // Queue should be empty initially
464            assert!(reader.is_empty());
465            assert_eq!(reader.pending(), 0);
466            assert_eq!(queue.size(), 0);
467
468            // Enqueue items
469            let pos0;
470            (queue, pos0) = queue.enqueue(&b"item0".to_vec()).await.unwrap();
471            let pos1;
472            (queue, pos1) = queue.enqueue(&b"item1".to_vec()).await.unwrap();
473            let pos2;
474            (queue, pos2) = queue.enqueue(&b"item2".to_vec()).await.unwrap();
475
476            assert_eq!(pos0, 0);
477            assert_eq!(pos1, 1);
478            assert_eq!(pos2, 2);
479            assert_eq!(queue.size(), 3);
480            assert_eq!(reader.pending(), 3);
481            assert!(!reader.is_empty());
482
483            // Dequeue items
484            let (p, item) = reader.try_recv().await.unwrap().unwrap();
485            assert_eq!(p, 0);
486            assert_eq!(item, b"item0");
487            assert_eq!(reader.pending(), 2);
488
489            let (p, item) = reader.try_recv().await.unwrap().unwrap();
490            assert_eq!(p, 1);
491            assert_eq!(item, b"item1");
492            assert_eq!(reader.pending(), 1);
493
494            let (p, item) = reader.try_recv().await.unwrap().unwrap();
495            assert_eq!(p, 2);
496            assert_eq!(item, b"item2");
497            assert_eq!(reader.pending(), 0);
498
499            // Queue still has unacked items
500            assert!(!reader.is_empty());
501            assert!(reader.try_recv().await.unwrap().is_none());
502        });
503    }
504
505    #[test_traced]
506    fn test_append_commit_batch() {
507        let executor = deterministic::Runner::default();
508        executor.start(|context| async move {
509            let cfg = test_config("test_batch");
510            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
511                .await
512                .unwrap();
513
514            // Append multiple items, then commit once
515            for i in 0..5u8 {
516                (queue, _) = queue.append(&vec![i]).await.unwrap();
517            }
518            let mut queue = queue.commit().await.unwrap();
519            assert_eq!(queue.size(), 5);
520
521            // Dequeue and verify order
522            for i in 0..5 {
523                let (pos, item) = reader.try_recv().await.unwrap().unwrap();
524                assert_eq!(pos, i);
525                assert_eq!(item, vec![i as u8]);
526            }
527
528            // Mix batch and single enqueue
529            for i in 5..8u8 {
530                (queue, _) = queue.append(&vec![i]).await.unwrap();
531            }
532            let queue = queue.commit().await.unwrap();
533            let (queue, _) = queue.enqueue(&vec![8]).await.unwrap();
534            assert_eq!(queue.size(), 9);
535
536            reader.ack_up_to(9).unwrap();
537            assert!(reader.is_empty());
538        });
539    }
540
541    #[test_traced]
542    fn test_append_commit_persistence() {
543        let executor = deterministic::Runner::default();
544        executor.start(|context| async move {
545            let cfg = test_config("test_batch_persist");
546
547            {
548                let (mut queue, _reader) =
549                    Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
550                        .await
551                        .unwrap();
552                for i in 0..4u8 {
553                    (queue, _) = queue.append(&vec![i]).await.unwrap();
554                }
555                let queue = queue.commit().await.unwrap();
556                queue.sync().await.unwrap();
557            }
558
559            {
560                let (queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("second"), cfg)
561                    .await
562                    .unwrap();
563                assert_eq!(queue.size(), 4);
564                for i in 0..4 {
565                    let (pos, item) = reader.try_recv().await.unwrap().unwrap();
566                    assert_eq!(pos, i);
567                    assert_eq!(item, vec![i as u8]);
568                }
569            }
570        });
571    }
572
573    #[test_traced]
574    fn test_commit_after_sync_recovers_without_second_sync() {
575        let executor = deterministic::Runner::default();
576        executor.start(|context| async move {
577            let cfg = test_config("test_commit_after_sync_recovery");
578
579            {
580                let (mut queue, _reader) =
581                    Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
582                        .await
583                        .unwrap();
584
585                // Establish a synced baseline so the recovery watermark is behind the next commit.
586                (queue, _) = queue.append(&b"synced".to_vec()).await.unwrap();
587                queue = queue.commit().await.unwrap();
588                queue = queue.sync().await.unwrap();
589
590                // Commit later data without syncing; reopen must replay it from the old watermark.
591                (queue, _) = queue.append(&b"committed-a".to_vec()).await.unwrap();
592                (queue, _) = queue.append(&b"committed-b".to_vec()).await.unwrap();
593                queue.commit().await.unwrap();
594            }
595
596            let (queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("second"), cfg)
597                .await
598                .unwrap();
599            assert_eq!(queue.size(), 3);
600            for (expected_pos, expected_item) in [
601                (0, b"synced".to_vec()),
602                (1, b"committed-a".to_vec()),
603                (2, b"committed-b".to_vec()),
604            ] {
605                let (pos, item) = reader.try_recv().await.unwrap().unwrap();
606                assert_eq!(pos, expected_pos);
607                assert_eq!(item, expected_item);
608            }
609
610            queue.destroy().await.unwrap();
611        });
612    }
613
614    #[test_traced]
615    fn test_sequential_ack() {
616        let executor = deterministic::Runner::default();
617        executor.start(|context| async move {
618            let cfg = test_config("test_seq_ack");
619            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
620                .await
621                .unwrap();
622
623            // Enqueue items
624            for i in 0..5u8 {
625                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
626            }
627
628            // Dequeue and ack sequentially
629            for i in 0..5 {
630                let (pos, _) = reader.try_recv().await.unwrap().unwrap();
631                assert_eq!(pos, i);
632                reader.ack(pos).unwrap();
633                assert_eq!(reader.ack_floor(), i + 1);
634            }
635
636            // All items acked
637            assert!(reader.is_empty());
638            assert_eq!(reader.ack_floor(), 5);
639        });
640    }
641
642    #[test_traced]
643    fn test_out_of_order_ack() {
644        let executor = deterministic::Runner::default();
645        executor.start(|context| async move {
646            let cfg = test_config("test_ooo_ack");
647            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
648                .await
649                .unwrap();
650
651            // Enqueue items
652            for i in 0..5u8 {
653                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
654            }
655
656            // Dequeue all
657            for _ in 0..5 {
658                reader.try_recv().await.unwrap();
659            }
660
661            // Ack out of order: 2, 4, 1, 3, 0
662            reader.ack(2).unwrap();
663            assert_eq!(reader.ack_floor(), 0); // Floor doesn't move
664            assert!(reader.is_acked(2));
665
666            reader.ack(4).unwrap();
667            assert_eq!(reader.ack_floor(), 0);
668            assert!(reader.is_acked(4));
669
670            reader.ack(1).unwrap();
671            assert_eq!(reader.ack_floor(), 0);
672
673            reader.ack(3).unwrap();
674            assert_eq!(reader.ack_floor(), 0);
675
676            // Ack 0 - floor should advance to 5 (consuming 1,2,3,4)
677            reader.ack(0).unwrap();
678            assert_eq!(reader.ack_floor(), 5);
679            assert!(reader.is_empty());
680        });
681    }
682
683    #[test_traced]
684    fn test_ack_up_to() {
685        let executor = deterministic::Runner::default();
686        executor.start(|context| async move {
687            let cfg = test_config("test_ack_up_to");
688            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
689                .await
690                .unwrap();
691
692            // Enqueue items
693            for i in 0..10u8 {
694                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
695            }
696
697            // Batch ack items 0-4
698            reader.ack_up_to(5).unwrap();
699            assert_eq!(reader.ack_floor(), 5);
700
701            // Items 0-4 should be acked
702            for i in 0..5 {
703                assert!(reader.is_acked(i));
704            }
705            // Items 5-9 should not be acked
706            for i in 5..10 {
707                assert!(!reader.is_acked(i));
708            }
709
710            // Dequeue should start at 5
711            let (p, _) = reader.try_recv().await.unwrap().unwrap();
712            assert_eq!(p, 5);
713        });
714    }
715
716    #[test_traced]
717    fn test_ack_up_to_with_existing_acks() {
718        let executor = deterministic::Runner::default();
719        executor.start(|context| async move {
720            let cfg = test_config("test_ack_up_to_existing");
721            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
722                .await
723                .unwrap();
724
725            // Enqueue items
726            for i in 0..10u8 {
727                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
728            }
729
730            // Ack some items out of order first
731            reader.ack(7).unwrap();
732            reader.ack(8).unwrap();
733            assert_eq!(acked_above_count(&reader), 2);
734
735            // Batch ack up to 5
736            reader.ack_up_to(5).unwrap();
737            assert_eq!(reader.ack_floor(), 5);
738            assert_eq!(acked_above_count(&reader), 2);
739
740            // Now batch ack up to 9 - should consume the acked_above entries
741            reader.ack_up_to(9).unwrap();
742            assert_eq!(reader.ack_floor(), 9);
743            assert_eq!(acked_above_count(&reader), 0);
744        });
745    }
746
747    #[test_traced]
748    fn test_ack_up_to_coalesces_with_acked_above() {
749        let executor = deterministic::Runner::default();
750        executor.start(|context| async move {
751            let cfg = test_config("test_ack_up_to_coalesce");
752            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
753                .await
754                .unwrap();
755
756            // Enqueue items
757            for i in 0..10u8 {
758                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
759            }
760
761            // Ack items 5, 6, 7 first
762            reader.ack(5).unwrap();
763            reader.ack(6).unwrap();
764            reader.ack(7).unwrap();
765            assert_eq!(reader.ack_floor(), 0);
766
767            // Batch ack up to 5 - should coalesce with 5, 6, 7
768            reader.ack_up_to(5).unwrap();
769            assert_eq!(reader.ack_floor(), 8); // Consumed 5, 6, 7
770        });
771    }
772
773    #[test_traced]
774    fn test_ack_up_to_errors() {
775        let executor = deterministic::Runner::default();
776        executor.start(|context| async move {
777            let cfg = test_config("test_ack_up_to_errors");
778            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
779                .await
780                .unwrap();
781
782            (queue, _) = queue.enqueue(&b"item0".to_vec()).await.unwrap();
783            let (_queue, _) = queue.enqueue(&b"item1".to_vec()).await.unwrap();
784
785            // Can't ack_up_to beyond queue size
786            let err = reader.ack_up_to(5).unwrap_err();
787            assert!(matches!(err, Error::PositionOutOfRange(5, 2)));
788
789            // Can ack_up_to at queue size
790            reader.ack_up_to(2).unwrap();
791            assert_eq!(reader.ack_floor(), 2);
792
793            // Acking up_to at or below floor is a no-op
794            reader.ack_up_to(1).unwrap();
795            assert_eq!(reader.ack_floor(), 2);
796        });
797    }
798
799    #[test_traced]
800    fn test_dequeue_skips_acked() {
801        let executor = deterministic::Runner::default();
802        executor.start(|context| async move {
803            let cfg = test_config("test_skip_acked");
804            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
805                .await
806                .unwrap();
807
808            // Enqueue items 0-4
809            for i in 0..5u8 {
810                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
811            }
812
813            // Ack items 1 and 3 before reading
814            reader.ack(1).unwrap();
815            reader.ack(3).unwrap();
816
817            // Dequeue should skip 1 and 3
818            let (p, item) = reader.try_recv().await.unwrap().unwrap();
819            assert_eq!(p, 0);
820            assert_eq!(item, vec![0]);
821
822            let (p, item) = reader.try_recv().await.unwrap().unwrap();
823            assert_eq!(p, 2); // Skipped 1
824            assert_eq!(item, vec![2]);
825
826            let (p, item) = reader.try_recv().await.unwrap().unwrap();
827            assert_eq!(p, 4); // Skipped 3
828            assert_eq!(item, vec![4]);
829
830            assert!(reader.try_recv().await.unwrap().is_none());
831        });
832    }
833
834    #[test_traced]
835    fn test_ack_errors() {
836        let executor = deterministic::Runner::default();
837        executor.start(|context| async move {
838            let cfg = test_config("test_ack_errors");
839            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
840                .await
841                .unwrap();
842
843            (queue, _) = queue.enqueue(&b"item0".to_vec()).await.unwrap();
844            let (_queue, _) = queue.enqueue(&b"item1".to_vec()).await.unwrap();
845
846            // Can't ack position beyond queue size
847            let err = reader.ack(5).unwrap_err();
848            assert!(matches!(err, Error::PositionOutOfRange(5, 2)));
849
850            // Can ack unread items
851            reader.ack(1).unwrap();
852            assert!(reader.is_acked(1));
853
854            // Double ack is a no-op
855            reader.ack(1).unwrap();
856        });
857    }
858
859    #[test_traced]
860    fn test_prune() {
861        let executor = deterministic::Runner::default();
862        executor.start(|context| async move {
863            let cfg = test_config("test_prune");
864            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
865                .await
866                .unwrap();
867
868            // Enqueue items (more than items_per_section to test pruning)
869            for i in 0..25u8 {
870                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
871            }
872            let _queue = queue.sync().await.unwrap();
873
874            // Read and ack some items
875            for i in 0..15 {
876                reader.try_recv().await.unwrap();
877                reader.ack(i).unwrap();
878            }
879            assert_eq!(reader.ack_floor(), 15);
880
881            // Items 15+ should still be readable
882            let (p, item) = reader.try_recv().await.unwrap().unwrap();
883            assert_eq!(p, 15);
884            assert_eq!(item, vec![15]);
885        });
886    }
887
888    #[test_traced]
889    fn test_ack_across_sections() {
890        let executor = deterministic::Runner::default();
891        executor.start(|context| async move {
892            let cfg = test_config("test_multi_prune");
893            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
894                .await
895                .unwrap();
896
897            // Enqueue many items across multiple sections (items_per_section = 10)
898            for i in 0..50u8 {
899                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
900            }
901            let _queue = queue.sync().await.unwrap();
902
903            // First batch: ack items 0-14
904            for i in 0..15 {
905                reader.try_recv().await.unwrap();
906                reader.ack(i).unwrap();
907            }
908            assert_eq!(reader.ack_floor(), 15);
909
910            // Verify items 15+ still readable
911            let (p, item) = reader.try_recv().await.unwrap().unwrap();
912            assert_eq!(p, 15);
913            assert_eq!(item, vec![15]);
914
915            // Second batch: ack items 15-29
916            reader.ack(15).unwrap();
917            for i in 16..30 {
918                reader.try_recv().await.unwrap();
919                reader.ack(i).unwrap();
920            }
921            assert_eq!(reader.ack_floor(), 30);
922
923            // Verify items 30+ still readable
924            let (p, item) = reader.try_recv().await.unwrap().unwrap();
925            assert_eq!(p, 30);
926            assert_eq!(item, vec![30]);
927
928            // Third batch: ack remaining items
929            reader.ack(30).unwrap();
930            for i in 31..50 {
931                reader.try_recv().await.unwrap();
932                reader.ack(i).unwrap();
933            }
934            assert_eq!(reader.ack_floor(), 50);
935
936            // Queue should be empty now
937            assert!(reader.is_empty());
938            assert!(reader.try_recv().await.unwrap().is_none());
939        });
940    }
941
942    #[test_traced]
943    fn test_crash_recovery_replays_from_pruning_boundary() {
944        // On restart, ack_floor = pruning_boundary. Items not pruned are re-delivered.
945        let executor = deterministic::Runner::default();
946        executor.start(|context| async move {
947            let cfg = test_config("test_recovery_replay");
948
949            // First session: enqueue items, ack some (but not enough to prune)
950            {
951                let (mut queue, mut reader) =
952                    Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
953                        .await
954                        .unwrap();
955
956                for i in 0..5u8 {
957                    (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
958                }
959
960                // Ack items 0, 1, 2 - but items_per_section=10, so no pruning
961                reader.ack(0).unwrap();
962                reader.ack(1).unwrap();
963                reader.ack(2).unwrap();
964                assert_eq!(reader.ack_floor(), 3);
965
966                queue.sync().await.unwrap();
967            }
968
969            // Second session: all items are re-delivered (no pruning occurred)
970            {
971                let (_queue, mut reader) =
972                    Queue::<_, Vec<u8>>::init(context.child("second"), cfg.clone())
973                        .await
974                        .unwrap();
975
976                // ack_floor = pruning_boundary = 0 (nothing was pruned)
977                assert_eq!(reader.ack_floor(), 0);
978
979                // All items re-delivered
980                for i in 0..5 {
981                    let (p, _) = reader.try_recv().await.unwrap().unwrap();
982                    assert_eq!(p, i);
983                }
984            }
985        });
986    }
987
988    #[test_traced]
989    fn test_crash_recovery_with_pruning() {
990        // Items pruned before crash are not re-delivered.
991        let executor = deterministic::Runner::default();
992        executor.start(|context| async move {
993            let cfg = test_config("test_recovery_pruned");
994
995            // First session: enqueue many items, ack enough to trigger pruning
996            let expected_pruning_boundary = {
997                let (mut queue, mut reader) =
998                    Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
999                        .await
1000                        .unwrap();
1001
1002                // Enqueue items across multiple sections (items_per_section = 10)
1003                for i in 0..25u8 {
1004                    (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1005                }
1006
1007                // Ack items 0-14 to advance floor past section 0
1008                for i in 0..15 {
1009                    reader.ack(i).unwrap();
1010                }
1011                assert_eq!(reader.ack_floor(), 15);
1012
1013                // Sync triggers pruning
1014                queue = queue.sync().await.unwrap();
1015
1016                // Verify pruning occurred
1017                let pruning_boundary = queue.journal.bounds().start;
1018                assert!(pruning_boundary > 0, "expected some pruning to occur");
1019
1020                pruning_boundary
1021            };
1022
1023            // Second session: only non-pruned items are available
1024            {
1025                let (queue, mut reader) =
1026                    Queue::<_, Vec<u8>>::init(context.child("second"), cfg.clone())
1027                        .await
1028                        .unwrap();
1029
1030                // ack_floor = pruning_boundary (items 0-9 were pruned)
1031                let pruning_boundary = queue.journal.bounds().start;
1032                assert_eq!(reader.ack_floor(), pruning_boundary);
1033                assert_eq!(pruning_boundary, expected_pruning_boundary);
1034
1035                // Items from pruning_boundary to 24 are re-delivered
1036                for i in pruning_boundary..25 {
1037                    let (p, item) = reader.try_recv().await.unwrap().unwrap();
1038                    assert_eq!(p, i);
1039                    assert_eq!(item, vec![i as u8]);
1040                }
1041
1042                assert!(reader.try_recv().await.unwrap().is_none());
1043            }
1044        });
1045    }
1046
1047    #[test_traced]
1048    fn test_reset() {
1049        let executor = deterministic::Runner::default();
1050        executor.start(|context| async move {
1051            let cfg = test_config("test_reset");
1052            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1053                .await
1054                .unwrap();
1055
1056            // Enqueue items
1057            for i in 0..5u8 {
1058                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1059            }
1060
1061            // Read some
1062            reader.try_recv().await.unwrap();
1063            reader.try_recv().await.unwrap();
1064            reader.try_recv().await.unwrap();
1065            assert_eq!(reader.read_position(), 3);
1066
1067            // Reset without ack - should go back to 0
1068            reader.reset();
1069            assert_eq!(reader.read_position(), 0);
1070
1071            // Verify we can re-read
1072            let (p, item) = reader.try_recv().await.unwrap().unwrap();
1073            assert_eq!(p, 0);
1074            assert_eq!(item, vec![0]);
1075        });
1076    }
1077
1078    #[test_traced]
1079    fn test_reset_with_ack() {
1080        let executor = deterministic::Runner::default();
1081        executor.start(|context| async move {
1082            let cfg = test_config("test_reset_ack");
1083            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1084                .await
1085                .unwrap();
1086
1087            // Enqueue items
1088            for i in 0..10u8 {
1089                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1090            }
1091
1092            // Read and ack some
1093            for i in 0..5 {
1094                reader.try_recv().await.unwrap();
1095                reader.ack(i).unwrap();
1096            }
1097            assert_eq!(reader.ack_floor(), 5);
1098            assert_eq!(reader.read_position(), 5);
1099
1100            // Read a few more
1101            reader.try_recv().await.unwrap();
1102            reader.try_recv().await.unwrap();
1103            assert_eq!(reader.read_position(), 7);
1104
1105            // Reset - should go back to ack floor
1106            reader.reset();
1107            assert_eq!(reader.read_position(), 5);
1108
1109            // Next dequeue should return item 5
1110            let (p, item) = reader.try_recv().await.unwrap().unwrap();
1111            assert_eq!(p, 5);
1112            assert_eq!(item, vec![5]);
1113        });
1114    }
1115
1116    #[test_traced]
1117    fn test_empty_queue_operations() {
1118        let executor = deterministic::Runner::default();
1119        executor.start(|context| async move {
1120            let cfg = test_config("test_empty");
1121            let (queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1122                .await
1123                .unwrap();
1124
1125            // Operations on empty queue
1126            assert!(reader.is_empty());
1127            assert!(reader.try_recv().await.unwrap().is_none());
1128            let _queue = queue.sync().await.unwrap();
1129            reader.reset();
1130        });
1131    }
1132
1133    #[test_traced]
1134    fn test_persistence() {
1135        let executor = deterministic::Runner::default();
1136        executor.start(|context| async move {
1137            let cfg = test_config("test_persist");
1138
1139            // First session
1140            {
1141                let (mut queue, _reader) =
1142                    Queue::<_, Vec<u8>>::init(context.child("first"), cfg.clone())
1143                        .await
1144                        .unwrap();
1145
1146                (queue, _) = queue.enqueue(&b"item0".to_vec()).await.unwrap();
1147                (queue, _) = queue.enqueue(&b"item1".to_vec()).await.unwrap();
1148                queue.sync().await.unwrap();
1149            }
1150
1151            // Second session - data should persist
1152            {
1153                let (queue, mut reader) =
1154                    Queue::<_, Vec<u8>>::init(context.child("second"), cfg.clone())
1155                        .await
1156                        .unwrap();
1157
1158                assert_eq!(queue.size(), 2);
1159
1160                let (_, item) = reader.try_recv().await.unwrap().unwrap();
1161                assert_eq!(item, b"item0");
1162
1163                let (_, item) = reader.try_recv().await.unwrap().unwrap();
1164                assert_eq!(item, b"item1");
1165            }
1166        });
1167    }
1168
1169    #[test_traced]
1170    fn test_large_queue_with_sparse_acks() {
1171        let executor = deterministic::Runner::default();
1172        executor.start(|context| async move {
1173            let cfg = test_config("test_sparse");
1174            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1175                .await
1176                .unwrap();
1177
1178            // Enqueue many items
1179            for i in 0..100u8 {
1180                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1181            }
1182
1183            // Ack every 3rd item (sparse acking)
1184            for i in (0..100).step_by(3) {
1185                reader.ack(i).unwrap();
1186            }
1187
1188            // Dequeue should skip acked items
1189            let mut received = Vec::new();
1190            while let Some((pos, _)) = reader.try_recv().await.unwrap() {
1191                received.push(pos);
1192            }
1193
1194            // Should have received all items not divisible by 3
1195            let expected: Vec<u64> = (0..100).filter(|x| x % 3 != 0).collect();
1196            assert_eq!(received, expected);
1197        });
1198    }
1199
1200    #[test_traced]
1201    fn test_acked_above_coalescing() {
1202        let executor = deterministic::Runner::default();
1203        executor.start(|context| async move {
1204            let cfg = test_config("test_coalesce");
1205            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1206                .await
1207                .unwrap();
1208
1209            // Enqueue items
1210            for i in 0..10u8 {
1211                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1212            }
1213
1214            // Ack items 1-8 (not 0)
1215            for i in 1..9 {
1216                reader.ack(i).unwrap();
1217            }
1218
1219            // Acked_above should have items 1-8
1220            assert_eq!(reader.ack_floor(), 0);
1221            assert!(acked_above_count(&reader) > 0);
1222
1223            // Now ack 0 - floor should advance to 9, consuming all acked_above
1224            reader.ack(0).unwrap();
1225            assert_eq!(reader.ack_floor(), 9);
1226            assert_eq!(acked_above_count(&reader), 0);
1227        });
1228    }
1229
1230    #[test_traced]
1231    fn test_ack_up_to_past_read_pos() {
1232        let executor = deterministic::Runner::default();
1233        executor.start(|context| async move {
1234            let cfg = test_config("test_ack_up_to_past_read_pos");
1235            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(context.child("storage"), cfg)
1236                .await
1237                .unwrap();
1238
1239            for i in 0..10u8 {
1240                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1241            }
1242
1243            // Read only 3 items
1244            for _ in 0..3 {
1245                reader.try_recv().await.unwrap();
1246            }
1247            assert_eq!(reader.read_position(), 3);
1248
1249            // Batch ack past read position
1250            reader.ack_up_to(7).unwrap();
1251            assert_eq!(reader.ack_floor(), 7);
1252
1253            // Dequeue should skip 3-6 and return 7
1254            let (pos, item) = reader.try_recv().await.unwrap().unwrap();
1255            assert_eq!(pos, 7);
1256            assert_eq!(item, vec![7]);
1257        });
1258    }
1259
1260    #[test_traced]
1261    fn test_metrics() {
1262        let executor = deterministic::Runner::default();
1263        executor.start(|context| async move {
1264            let cfg = test_config("test-metrics");
1265            let ctx = context.child("test_metrics");
1266            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(ctx, cfg).await.unwrap();
1267
1268            let encoded = context.encode();
1269            assert!(
1270                encoded.contains("test_metrics_tip 0"),
1271                "expected tip 0: {encoded}"
1272            );
1273            assert!(
1274                encoded.contains("test_metrics_floor 0"),
1275                "expected floor 0: {encoded}"
1276            );
1277            assert!(
1278                encoded.contains("test_metrics_next 0"),
1279                "expected next 0: {encoded}"
1280            );
1281
1282            // Append updates tip without enqueue
1283            (queue, _) = queue.append(&vec![0]).await.unwrap();
1284            let encoded = context.encode();
1285            assert!(
1286                encoded.contains("test_metrics_tip 1"),
1287                "expected tip 1: {encoded}"
1288            );
1289            let mut queue = queue.commit().await.unwrap();
1290
1291            // Enqueue updates tip further
1292            for i in 1..10u8 {
1293                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1294            }
1295            let encoded = context.encode();
1296            assert!(
1297                encoded.contains("test_metrics_tip 10"),
1298                "expected tip 10: {encoded}"
1299            );
1300
1301            // Multiple dequeues advance next
1302            reader.try_recv().await.unwrap();
1303            reader.try_recv().await.unwrap();
1304            let encoded = context.encode();
1305            assert!(
1306                encoded.contains("test_metrics_next 2"),
1307                "expected next 2: {encoded}"
1308            );
1309
1310            // Sequential ack advances floor
1311            reader.ack(0).unwrap();
1312            reader.ack(1).unwrap();
1313            let encoded = context.encode();
1314            assert!(
1315                encoded.contains("test_metrics_floor 2"),
1316                "expected floor 2: {encoded}"
1317            );
1318
1319            // Out-of-order ack: floor stays until gap fills
1320            reader.ack(4).unwrap();
1321            reader.ack(6).unwrap();
1322            let encoded = context.encode();
1323            assert!(
1324                encoded.contains("test_metrics_floor 2"),
1325                "expected floor still 2: {encoded}"
1326            );
1327
1328            // Fill gap coalesces floor forward
1329            reader.ack(2).unwrap();
1330            reader.ack(3).unwrap();
1331            let encoded = context.encode();
1332            assert!(
1333                encoded.contains("test_metrics_floor 5"),
1334                "expected floor 5: {encoded}"
1335            );
1336
1337            // ack_up_to advances floor past sparse ack at 6
1338            reader.ack_up_to(8).unwrap();
1339            let encoded = context.encode();
1340            assert!(
1341                encoded.contains("test_metrics_floor 8"),
1342                "expected floor 8: {encoded}"
1343            );
1344
1345            // Ack remaining
1346            reader.ack(8).unwrap();
1347            reader.ack(9).unwrap();
1348            let encoded = context.encode();
1349            assert!(
1350                encoded.contains("test_metrics_floor 10"),
1351                "expected floor 10: {encoded}"
1352            );
1353
1354            // Reset brings next back to floor
1355            reader.reset();
1356            let encoded = context.encode();
1357            assert!(
1358                encoded.contains("test_metrics_next 10"),
1359                "expected next 10: {encoded}"
1360            );
1361        });
1362    }
1363
1364    #[test_traced]
1365    fn test_metrics_next_updates_on_fast_forward() {
1366        let executor = deterministic::Runner::default();
1367        executor.start(|context| async move {
1368            let cfg = test_config("test-ff");
1369            let ctx = context.child("test_ff");
1370            let (mut queue, mut reader) = Queue::<_, Vec<u8>>::init(ctx, cfg).await.unwrap();
1371
1372            // Enqueue 3 items, dequeue and ack only the first
1373            for i in 0..3u8 {
1374                (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1375            }
1376            let (pos, _) = reader.try_recv().await.unwrap().unwrap();
1377            reader.ack(pos).unwrap();
1378
1379            let encoded = context.encode();
1380            assert!(
1381                encoded.contains("test_ff_next 1"),
1382                "expected next 1: {encoded}"
1383            );
1384
1385            // Ack remaining items out-of-order to advance floor to 3
1386            reader.ack(2).unwrap();
1387            reader.ack(1).unwrap();
1388            assert_eq!(reader.ack_floor(), 3);
1389
1390            // next metric is still 1 (no dequeue yet)
1391            let encoded = context.encode();
1392            assert!(
1393                encoded.contains("test_ff_next 1"),
1394                "expected next still 1: {encoded}"
1395            );
1396
1397            // Dequeue returns None but fast-forwards read_pos to ack_floor
1398            assert!(reader.try_recv().await.unwrap().is_none());
1399            let encoded = context.encode();
1400            assert!(
1401                encoded.contains("test_ff_next 3"),
1402                "expected next 3 after fast-forward: {encoded}"
1403            );
1404        });
1405    }
1406
1407    /// An enqueued item reaches the reader and can be acknowledged.
1408    #[test_traced]
1409    fn test_reader_basic() {
1410        let executor = deterministic::Runner::default();
1411        executor.start(|context| async move {
1412            // Create independent handles for publishing, receiving, and acknowledging one item.
1413            let cfg = test_config("test_reader_basic");
1414            let (queue, mut reader) = init(context, cfg).await.unwrap();
1415
1416            // Enqueue from queue
1417            let (_queue, pos) = queue.enqueue(&b"hello".to_vec()).await.unwrap();
1418            assert_eq!(pos, 0);
1419
1420            // Receive from reader
1421            let (recv_pos, item) = reader.recv().await.unwrap().unwrap();
1422            assert_eq!(recv_pos, 0);
1423            assert_eq!(item, b"hello".to_vec());
1424
1425            // Ack the item
1426            reader.ack(recv_pos).unwrap();
1427            assert!(reader.is_empty());
1428        });
1429    }
1430
1431    /// Appended items reach the reader only once committed.
1432    #[test_traced]
1433    fn test_reader_append_commit() {
1434        let executor = deterministic::Runner::default();
1435        executor.start(|context| async move {
1436            // Initialize separate handles to observe the publication boundary.
1437            let cfg = test_config("test_reader_append_commit");
1438            let (mut queue, mut reader) = init(context, cfg).await.unwrap();
1439
1440            // Append several items without committing
1441            for i in 0..5u8 {
1442                let pos;
1443                (queue, pos) = queue.append(&vec![i]).await.unwrap();
1444                assert_eq!(pos, i as u64);
1445            }
1446
1447            // The reader cannot see or acknowledge them before commit
1448            assert!(reader.try_recv().await.unwrap().is_none());
1449            assert!(matches!(
1450                reader.ack(0),
1451                Err(Error::PositionOutOfRange(0, 0))
1452            ));
1453            assert!(reader.is_empty());
1454
1455            // Commit to publish
1456            let _queue = queue.commit().await.unwrap();
1457
1458            // Every item is readable
1459            for i in 0..5 {
1460                let (pos, item) = reader.recv().await.unwrap().unwrap();
1461                assert_eq!(pos, i);
1462                assert_eq!(item, vec![i as u8]);
1463                reader.ack(pos).unwrap();
1464            }
1465            assert!(reader.is_empty());
1466        });
1467    }
1468
1469    /// A bulk enqueue publishes its items, in order, with one commit.
1470    #[test_traced]
1471    fn test_reader_enqueue_bulk() {
1472        let executor = deterministic::Runner::default();
1473        executor.start(|context| async move {
1474            // Initialize a reader that will observe the entire batch after one commit.
1475            let cfg = test_config("test_reader_bulk");
1476            let (queue, mut reader) = init(context, cfg).await.unwrap();
1477
1478            // Publish the batch with a single commit.
1479            let items: Vec<_> = (0..5u8).map(|i| vec![i]).collect();
1480            let (_queue, range) = queue.enqueue_bulk(&items).await.unwrap();
1481            assert_eq!(range, 0..5);
1482
1483            // Verify FIFO delivery and acknowledge every item in the published batch.
1484            for i in 0..5 {
1485                let (pos, item) = reader.recv().await.unwrap().unwrap();
1486                assert_eq!(pos, i);
1487                assert_eq!(item, vec![i as u8]);
1488                reader.ack(pos).unwrap();
1489            }
1490            assert!(reader.is_empty());
1491        });
1492    }
1493
1494    /// Items enqueued by a queue task reach a concurrent reader in order.
1495    #[test_traced]
1496    fn test_reader_concurrent() {
1497        let executor = deterministic::Runner::default();
1498        executor.start(|context| async move {
1499            // Give concurrent producer and consumer tasks handles over the same journal.
1500            let cfg = test_config("test_reader_concurrent");
1501            let (queue, mut reader) = init(context.child("storage"), cfg).await.unwrap();
1502
1503            // Spawn queue task
1504            let queue_handle = context.child("queue").spawn(|_ctx| async move {
1505                let mut queue = queue;
1506                for i in 0..10u8 {
1507                    (queue, _) = queue.enqueue(&vec![i]).await.unwrap();
1508                }
1509                queue
1510            });
1511
1512            // Reader receives items as they come
1513            let mut received = Vec::new();
1514            for _ in 0..10 {
1515                let (pos, item) = reader.recv().await.unwrap().unwrap();
1516                received.push((pos, item.clone()));
1517                reader.ack(pos).unwrap();
1518            }
1519
1520            // Verify all items received in order
1521            for (i, (pos, item)) in received.iter().enumerate() {
1522                assert_eq!(*pos, i as u64);
1523                assert_eq!(*item, vec![i as u8]);
1524            }
1525
1526            let _ = queue_handle.await.unwrap();
1527        });
1528    }
1529
1530    /// `recv` completes inside `select!`.
1531    #[test_traced]
1532    fn test_reader_select() {
1533        let executor = deterministic::Runner::default();
1534        executor.start(|context| async move {
1535            // Prepare queue handles for receiving through select.
1536            let cfg = test_config("test_reader_select");
1537            let (queue, mut reader) = init(context.child("storage"), cfg).await.unwrap();
1538
1539            // Enqueue an item
1540            let (_queue, _) = queue.enqueue(&b"test".to_vec()).await.unwrap();
1541
1542            // Receive through select alongside a branch that never completes.
1543            let result = select! {
1544                item = reader.recv() => item,
1545                _ = futures::future::pending::<()>() => unreachable!(),
1546            };
1547
1548            let (pos, item) = result.unwrap().unwrap();
1549            assert_eq!(pos, 0);
1550            assert_eq!(item, b"test".to_vec());
1551
1552            reader.ack(pos).unwrap();
1553        });
1554    }
1555
1556    /// After the queue is dropped, the reader delivers the published items and then returns
1557    /// `None`.
1558    #[test_traced]
1559    fn test_reader_queue_dropped() {
1560        let executor = deterministic::Runner::default();
1561        executor.start(|context| async move {
1562            // Initialize both handles before testing delivery after the queue is dropped.
1563            let cfg = test_config("test_reader_queue_dropped");
1564            let (queue, mut reader) = init(context.child("storage"), cfg).await.unwrap();
1565
1566            // Enqueue items then drop queue
1567            let (queue, _) = queue.enqueue(&b"item1".to_vec()).await.unwrap();
1568            let (queue, _) = queue.enqueue(&b"item2".to_vec()).await.unwrap();
1569            drop(queue);
1570
1571            // Reader should still get existing items
1572            let (pos1, _) = reader.recv().await.unwrap().unwrap();
1573            reader.ack(pos1).unwrap();
1574
1575            let (pos2, _) = reader.recv().await.unwrap().unwrap();
1576            reader.ack(pos2).unwrap();
1577
1578            // Next recv should return None (queue dropped, queue empty)
1579            let result = reader.recv().await.unwrap();
1580            assert!(result.is_none());
1581        });
1582    }
1583
1584    /// `try_recv` returns `None` until an item is published, then returns the item.
1585    #[test_traced]
1586    fn test_reader_try_recv() {
1587        let executor = deterministic::Runner::default();
1588        executor.start(|context| async move {
1589            // Start with an empty published view for nonblocking receive checks.
1590            let cfg = test_config("test_reader_try_recv");
1591            let (queue, mut reader) = init(context, cfg).await.unwrap();
1592
1593            // try_recv on empty queue returns None
1594            let result = reader.try_recv().await.unwrap();
1595            assert!(result.is_none());
1596
1597            // Enqueue and try_recv
1598            let (_queue, _) = queue.enqueue(&b"item".to_vec()).await.unwrap();
1599            let (pos, item) = reader.try_recv().await.unwrap().unwrap();
1600            assert_eq!(pos, 0);
1601            assert_eq!(item, b"item".to_vec());
1602
1603            reader.ack(pos).unwrap();
1604        });
1605    }
1606
1607    /// `try_recv` alone drains every published item after the queue is dropped.
1608    #[test_traced]
1609    fn test_reader_try_recv_after_queue_dropped() {
1610        let executor = deterministic::Runner::default();
1611        executor.start(|context| async move {
1612            // Publish items and drop the queue before any read.
1613            let cfg = test_config("test_reader_try_recv_dropped");
1614            let (queue, mut reader) = init(context, cfg).await.unwrap();
1615            let (queue, _) = queue.enqueue_bulk((0..3u8).map(|i| vec![i])).await.unwrap();
1616            drop(queue);
1617
1618            // The closed channel still holds the final snapshot.
1619            for i in 0..3 {
1620                let (pos, item) = reader.try_recv().await.unwrap().unwrap();
1621                assert_eq!(pos, i);
1622                assert_eq!(item, vec![i as u8]);
1623            }
1624            assert!(reader.try_recv().await.unwrap().is_none());
1625        });
1626    }
1627
1628    /// A queue mutation dropped mid-flight destroys the queue. The reader delivers every
1629    /// published item, then reports the end of the queue.
1630    #[test_traced]
1631    fn test_reader_ends_after_interrupted_mutation() {
1632        let executor = deterministic::Runner::default();
1633        executor.start(|context| async move {
1634            // Wrap storage so a queue operation can be parked during its durability barrier.
1635            let pending = PendingSyncs::default();
1636            let context = DelayedSyncContext {
1637                inner: context,
1638                pending: pending.clone(),
1639            };
1640            let cfg = test_config("test_reader_interrupted");
1641            let (queue, mut reader) = init(context.child("storage"), cfg).await.unwrap();
1642
1643            // Let started syncs complete so the first enqueue publishes.
1644            pending.unblock();
1645            let (queue, _) = queue.enqueue(&b"first".to_vec()).await.unwrap();
1646
1647            // Park the next enqueue's durable commit, then drop it mid-flight.
1648            pending.arm();
1649            let second = b"second".to_vec();
1650            let mut enqueue = Box::pin(queue.enqueue(&second));
1651            assert!(futures::poll!(enqueue.as_mut()).is_pending());
1652            drop(enqueue);
1653
1654            // The reader delivers the published item, then returns None.
1655            let (pos, item) = reader.recv().await.unwrap().unwrap();
1656            assert_eq!(pos, 0);
1657            assert_eq!(item, b"first".to_vec());
1658            assert!(reader.recv().await.unwrap().is_none());
1659        });
1660    }
1661
1662    /// `Queue::sync` publishes appended items, like a commit.
1663    #[test_traced]
1664    fn test_reader_sync_publishes() {
1665        let executor = deterministic::Runner::default();
1666        executor.start(|context| async move {
1667            let cfg = test_config("test_reader_sync_publishes");
1668            let (queue, mut reader) = init(context, cfg).await.unwrap();
1669
1670            // An appended item stays unpublished until the sync.
1671            let (queue, _) = queue.append(&vec![7]).await.unwrap();
1672            assert!(reader.try_recv().await.unwrap().is_none());
1673            let _queue = queue.sync().await.unwrap();
1674
1675            // The reader receives the synced item.
1676            let (pos, item) = reader.try_recv().await.unwrap().unwrap();
1677            assert_eq!(pos, 0);
1678            assert_eq!(item, vec![7]);
1679        });
1680    }
1681
1682    /// `Reader::ack_up_to` shares its floor with the queue, so a sync prunes below it.
1683    #[test_traced]
1684    fn test_reader_ack_up_to_prunes() {
1685        let executor = deterministic::Runner::default();
1686        executor.start(|context| async move {
1687            // Publish three sections, then acknowledge the first 15 items without reading them.
1688            let cfg = test_config("test_reader_ack_up_to");
1689            let (queue, mut reader) = init(context.child("first"), cfg.clone()).await.unwrap();
1690            let (queue, _) = queue
1691                .enqueue_bulk((0..25u8).map(|i| vec![i]))
1692                .await
1693                .unwrap();
1694            reader.ack_up_to(15).unwrap();
1695            assert_eq!(reader.ack_floor(), 15);
1696
1697            // Syncing prunes whole sections below the floor.
1698            let queue = queue.sync().await.unwrap();
1699            drop(queue);
1700            drop(reader);
1701
1702            // A restart resumes delivery at the pruning boundary.
1703            let (_queue, reader) = init(context.child("second"), cfg).await.unwrap();
1704            assert_eq!(reader.ack_floor(), 10);
1705        });
1706    }
1707
1708    /// The queue prunes below the reader's ack floor on sync, the reader keeps receiving
1709    /// across the prune, and a restart re-delivers from the pruning boundary.
1710    #[test_traced]
1711    fn test_reader_sync_prunes_below_ack_floor() {
1712        let executor = deterministic::Runner::default();
1713        executor.start(|context| async move {
1714            // Populate multiple sections so acknowledgements can advance the pruning boundary.
1715            let cfg = test_config("test_reader_sync_prunes");
1716            let (queue, mut reader) = init(context.child("first"), cfg.clone()).await.unwrap();
1717            let (queue, _) = queue
1718                .enqueue_bulk((0..25u8).map(|i| vec![i]))
1719                .await
1720                .unwrap();
1721
1722            // Acknowledge the first 15 items, then sync. Sections hold 10 items, so the
1723            // journal prunes to position 10.
1724            for i in 0..15 {
1725                let (pos, _) = reader.recv().await.unwrap().unwrap();
1726                assert_eq!(pos, i);
1727                reader.ack(pos).unwrap();
1728            }
1729            assert_eq!(reader.ack_floor(), 15);
1730            let queue = queue.sync().await.unwrap();
1731            assert_eq!(queue.size(), 25);
1732
1733            // The reader moves to the pruned snapshot before its next read and receives the rest.
1734            let (pos, _) = reader.recv().await.unwrap().unwrap();
1735            assert_eq!(pos, 15);
1736            assert_eq!(reader.snapshot.bounds().start, 10);
1737            for i in 16..25 {
1738                let (pos, item) = reader.recv().await.unwrap().unwrap();
1739                assert_eq!(pos, i);
1740                assert_eq!(item, vec![i as u8]);
1741            }
1742            assert!(reader.try_recv().await.unwrap().is_none());
1743            drop(queue);
1744            drop(reader);
1745
1746            // A restart resumes delivery at the pruning boundary.
1747            let (_queue, mut reader) = init(context.child("second"), cfg).await.unwrap();
1748            assert_eq!(reader.ack_floor(), 10);
1749            let (pos, item) = reader.recv().await.unwrap().unwrap();
1750            assert_eq!(pos, 10);
1751            assert_eq!(item, vec![10]);
1752        });
1753    }
1754}