Skip to main content

commonware_runtime/utils/buffer/paged/
read.rs

1use super::Checksum;
2use crate::{Blob, Error, Handle, IoBuf, IoBufsMut, ReadOptions};
3use bytes::{BufMut, Bytes, BytesMut, TryGetError};
4use commonware_codec::{Buf, FixedSize};
5use commonware_utils::Widen;
6use futures::FutureExt as _;
7use std::{collections::VecDeque, num::NonZeroU16, ops::Range, sync::Arc};
8use tracing::error;
9
10/// Buffered pages from storage or a frozen logical tail.
11///
12/// Storage batches contain pages with interleaved CRCs. The frozen tail contains one partial
13/// logical page without CRCs. Navigation uses offsets rather than separate `Bytes` slices per page.
14pub(super) struct BufferState {
15    /// Page bytes, with interleaved CRCs when read from storage.
16    buffer: Bytes,
17    /// Number of pages in this buffer.
18    num_pages: usize,
19    /// Logical length of the last page (may be partial).
20    last_page_len: usize,
21}
22
23/// Batches kept in flight ahead of the one being decoded.
24///
25/// The replay buffer fixes the bytes in flight. The depth decides how many requests they are
26/// split into, which the buffer cannot. One request at a time leaves the device idle for a
27/// request latency at every batch boundary, while many small requests pay a fixed cost each.
28const MAX_READ_AHEAD: usize = 3;
29
30/// A batch started ahead of the decoder.
31struct Inflight {
32    /// First page of the batch.
33    first_page: u64,
34    /// Number of pages in the batch.
35    pages: usize,
36    /// The started read of the batch's pages, as one range.
37    read: Handle<Vec<IoBufsMut>>,
38}
39
40impl Inflight {
41    /// The page after the batch's last page.
42    fn end(&self) -> u64 {
43        self.first_page + Widen::widen(self.pages)
44    }
45
46    /// Wait for the batch's bytes.
47    async fn wait(self) -> Result<IoBufsMut, Error> {
48        self.read.await?.pop().ok_or(Error::ReadFailed)
49    }
50}
51
52/// Async I/O component that prefetches pages and validates CRCs.
53///
54/// Pages are read in batches of `batch_pages` pages, each started through
55/// [`Blob::start_read_many`] and held as a [`Handle`] until the decoder reaches it, with up to
56/// [`MAX_READ_AHEAD`] batches after the one being decoded in flight so the device works while
57/// the decoder does. The batches in flight and the one being decoded together span the replay
58/// buffer, each rounded up to whole pages, so they exceed it by less than one page each.
59///
60/// The decoder's position advances only after a batch passes checksum validation, so a corrupt
61/// page fails the fill that reaches it and no later page is yielded. When the batch at that
62/// position is missing from the queue or its read failed, the queue is discarded and the batch
63/// is read on demand. A failed or dropped fill therefore costs the reads ahead of it and nothing
64/// else.
65pub(super) struct PageReader<B: Blob> {
66    /// The underlying blob to read from.
67    blob: Arc<B>,
68    /// Physical page size (page_size + CHECKSUM_SIZE).
69    physical_page_size: usize,
70    /// Logical page size (data bytes per page, not including CRC).
71    page_size: usize,
72    /// Physical bytes to read from storage, excluding the frozen partial page.
73    physical_blob_size: u64,
74    /// The size of the blob.
75    logical_blob_size: u64,
76    /// Immutable logical bytes of the final partial page.
77    partial_page: Option<Bytes>,
78    /// Next page index to hand to the decoder.
79    blob_page: u64,
80    /// Number of pages per batch.
81    batch_pages: usize,
82    /// The batches read ahead of the decoder, contiguous and in page order. The first starts at
83    /// `blob_page` unless a fill failed or was dropped after taking the batch before it.
84    inflight: VecDeque<Inflight>,
85    /// Options applied to every blob read.
86    read_options: ReadOptions,
87}
88
89impl<B: Blob> PageReader<B> {
90    /// Creates a new PageReader.
91    ///
92    /// The `physical_blob_size` must already exclude any trailing invalid data
93    /// (e.g., junk pages from an interrupted write). Each physical page is the same
94    /// size on disk, but the CRC record indicates how much logical data it contains.
95    /// Every stored page must validate in full, since the final partial page is frozen in
96    /// memory rather than read. A page validating fewer bytes fails the fill that reaches it
97    /// with `Error::InvalidChecksum`.
98    ///
99    /// A frozen `partial_page` contains exactly the logical bytes of the final partial page.
100    /// Its physical page is included in `physical_blob_size` but is not read from storage.
101    pub(super) fn new(
102        blob: Arc<B>,
103        mut physical_blob_size: u64,
104        logical_blob_size: u64,
105        partial_page: Option<IoBuf>,
106        buffer_pages: usize,
107        page_size: NonZeroU16,
108        read_options: ReadOptions,
109    ) -> Self {
110        let page_size = page_size.get() as usize;
111        let physical_page_size = page_size + Checksum::SIZE;
112        let physical_pages = physical_blob_size / physical_page_size as u64;
113        let logical_pages = if logical_blob_size == 0 {
114            0
115        } else {
116            ((logical_blob_size - 1) / page_size as u64) + 1
117        };
118        assert_eq!(physical_blob_size % physical_page_size as u64, 0);
119        assert_eq!(physical_pages, logical_pages);
120        if let Some(partial_page) = &partial_page {
121            assert!(
122                !partial_page.is_empty()
123                    && Widen::widen(partial_page.len())
124                        == logical_blob_size % Widen::widen(page_size),
125                "frozen tail must match the final partial page"
126            );
127            physical_blob_size -= Widen::widen(physical_page_size);
128        }
129
130        Self {
131            blob,
132            physical_page_size,
133            page_size,
134            physical_blob_size,
135            logical_blob_size,
136            partial_page: partial_page.map(Bytes::from),
137            blob_page: 0,
138            batch_pages: buffer_pages.div_ceil(MAX_READ_AHEAD + 1),
139            inflight: VecDeque::new(),
140            read_options,
141        }
142    }
143
144    /// Position the reader at `page`, discarding the batches read ahead.
145    fn seek_to_page(&mut self, page: u64) {
146        self.blob_page = page;
147        self.inflight.clear();
148    }
149
150    /// Start batches after the queued ones, or after `consumed_end` when none is queued, until
151    /// [`MAX_READ_AHEAD`] are in flight or the blob ends.
152    async fn top_up(&mut self, consumed_end: u64) -> Result<(), Error> {
153        let mut next = self.inflight.back().map_or(consumed_end, Inflight::end);
154        while self.inflight.len() < MAX_READ_AHEAD {
155            let Some(batch) = self.start_batch(next).await? else {
156                break;
157            };
158            next = batch.end();
159            self.inflight.push_back(batch);
160        }
161        Ok(())
162    }
163
164    /// Start a read of up to `batch_pages` consecutive physical pages from `first_page`, or
165    /// `None` when no stored page remains there.
166    async fn start_batch(&self, first_page: u64) -> Result<Option<Inflight>, Error> {
167        let physical_page_size: u64 = Widen::widen(self.physical_page_size);
168        let start = first_page
169            .checked_mul(physical_page_size)
170            .ok_or(Error::OffsetOverflow)?;
171        if start >= self.physical_blob_size {
172            return Ok(None);
173        }
174        let pages = ((self.physical_blob_size - start) / physical_page_size)
175            .min(Widen::widen(self.batch_pages)) as usize;
176        let len = pages * self.physical_page_size;
177
178        // Boxing keeps the blob's start future out of the replay's state machine. With it
179        // inlined, the decoders' hot loops compiled differently and the variable decoder ran 8%
180        // slower.
181        let read = self
182            .blob
183            .start_read_many(&[(start, len)], self.read_options)
184            .boxed()
185            .await;
186        Ok(Some(Inflight {
187            first_page,
188            pages,
189            read,
190        }))
191    }
192
193    /// Returns the size of the blob.
194    pub(super) const fn blob_size(&self) -> u64 {
195        self.logical_blob_size
196    }
197
198    /// Returns the physical page size.
199    pub(super) const fn physical_page_size(&self) -> usize {
200        self.physical_page_size
201    }
202
203    /// Returns the logical page size.
204    pub(super) const fn page_size(&self) -> usize {
205        self.page_size
206    }
207
208    /// Read the batch at the decoder's position, taking the one read ahead for it when its read
209    /// lands and reading on demand otherwise.
210    ///
211    /// A batch read ahead whose read failed, or a queue that does not start at the decoder's
212    /// position, is discarded before the demand read is started and the queue topped up again.
213    /// The read ahead may have failed because whoever started it is gone, and nothing queued may
214    /// stand between the decoder and the bytes it needs.
215    async fn read_batch(&mut self) -> Result<(usize, IoBufsMut), Error> {
216        if let Some(batch) = self
217            .inflight
218            .pop_front_if(|batch| batch.first_page == self.blob_page)
219        {
220            let pages = batch.pages;
221            self.top_up(batch.end()).await?;
222            if let Ok(bufs) = batch.wait().await {
223                return Ok((pages, bufs));
224            }
225        }
226
227        self.inflight.clear();
228        let batch = self
229            .start_batch(self.blob_page)
230            .await?
231            .expect("pages remain to be issued");
232        let pages = batch.pages;
233        self.top_up(batch.end()).await?;
234        Ok((pages, batch.wait().await?))
235    }
236
237    /// Fills a buffer with the next batch of pages.
238    ///
239    /// Returns `Some((BufferState, logical_bytes))` if data was loaded,
240    /// `None` if no more data available.
241    pub(super) async fn fill(&mut self) -> Result<Option<(BufferState, usize)>, Error> {
242        // Calculate physical read offset
243        let start_offset = match self.blob_page.checked_mul(self.physical_page_size as u64) {
244            Some(o) => o,
245            None => return Err(Error::OffsetOverflow),
246        };
247        if start_offset == self.physical_blob_size
248            && let Some(partial_page) = &self.partial_page
249        {
250            self.blob_page += 1;
251            let len = partial_page.len();
252            return Ok(Some((
253                BufferState {
254                    buffer: partial_page.clone(),
255                    num_pages: 1,
256                    last_page_len: len,
257                },
258                len,
259            )));
260        }
261        if start_offset >= self.physical_blob_size {
262            return Ok(None); // No more data
263        }
264
265        let (pages_to_read, physical_buf) = self.read_batch().await?;
266        let physical_buf = Bytes::from(physical_buf.coalesce().freeze());
267
268        // Validate CRCs and compute total logical bytes
269        let mut total_logical = 0usize;
270        let mut last_len = 0usize;
271        for page_idx in 0..pages_to_read {
272            let page_start = page_idx * self.physical_page_size;
273            let page_slice =
274                &physical_buf.as_ref()[page_start..page_start + self.physical_page_size];
275            let Some(checksum) = Checksum::validate_page(page_slice) else {
276                error!(page = self.blob_page + page_idx as u64, "CRC mismatch");
277                return Err(Error::InvalidChecksum);
278            };
279            let len = checksum.len as usize;
280
281            // Every stored page is a full page, since the partial tail is frozen in memory.
282            if len < self.page_size {
283                error!(
284                    page = self.blob_page + Widen::widen(page_idx),
285                    expected = self.page_size,
286                    actual = len,
287                    "page validates fewer bytes than the replay reads"
288                );
289                return Err(Error::InvalidChecksum);
290            }
291            total_logical += self.page_size;
292            last_len = self.page_size;
293        }
294        self.blob_page += Widen::widen(pages_to_read);
295
296        let state = BufferState {
297            buffer: physical_buf,
298            num_pages: pages_to_read,
299            last_page_len: last_len,
300        };
301
302        Ok(Some((state, total_logical)))
303    }
304}
305
306/// Sync buffering component that implements the `Buf` trait.
307///
308/// This accumulates `BufferState` from multiple fills and provides navigation
309/// across pages while skipping CRCs. Consumed buffers are cleaned up in
310/// `advance()`.
311struct ReplayBuf {
312    /// Physical page size (page_size + CHECKSUM_SIZE).
313    physical_page_size: usize,
314    /// Logical page size (data bytes per page, not including CRC).
315    page_size: usize,
316    /// Accumulated buffers from fills.
317    buffers: VecDeque<BufferState>,
318    /// Current page index within the front buffer.
319    current_page: usize,
320    /// Current offset within the current page's logical data.
321    offset_in_page: usize,
322    /// Total remaining logical bytes across all buffers.
323    remaining: usize,
324}
325
326impl ReplayBuf {
327    /// Creates a new ReplayBuf.
328    const fn new(physical_page_size: usize, page_size: usize) -> Self {
329        Self {
330            physical_page_size,
331            page_size,
332            buffers: VecDeque::new(),
333            current_page: 0,
334            offset_in_page: 0,
335            remaining: 0,
336        }
337    }
338
339    /// Clears the buffer and resets the read offset to 0.
340    fn clear(&mut self) {
341        self.buffers.clear();
342        self.current_page = 0;
343        self.offset_in_page = 0;
344        self.remaining = 0;
345    }
346
347    /// Adds a buffer from a fill operation.
348    fn push(&mut self, state: BufferState, logical_bytes: usize) {
349        // If buffers is empty, this is the first fill after a seek.
350        // Skip bytes before the seek offset (offset_in_page).
351        let skip = if self.buffers.is_empty() {
352            self.offset_in_page
353        } else {
354            0
355        };
356        self.buffers.push_back(state);
357        self.remaining += logical_bytes.saturating_sub(skip);
358    }
359
360    /// Returns the logical length of the given page in the given buffer.
361    const fn page_len(buf: &BufferState, page_idx: usize, page_size: usize) -> usize {
362        if page_idx + 1 == buf.num_pages {
363            buf.last_page_len
364        } else {
365            page_size
366        }
367    }
368
369    /// Returns the front buffer and the physical range of the current page's unread logical
370    /// bytes, which is empty once the page is consumed, or `None` when no page is buffered.
371    fn chunk_range(&self) -> Option<(&BufferState, Range<usize>)> {
372        let buf = self.buffers.front()?;
373        if self.current_page >= buf.num_pages {
374            return None;
375        }
376        let page_len = Self::page_len(buf, self.current_page, self.page_size);
377        let page_start = self.current_page * self.physical_page_size;
378        Some((buf, page_start + self.offset_in_page..page_start + page_len))
379    }
380}
381
382impl Buf for ReplayBuf {}
383
384impl bytes::Buf for ReplayBuf {
385    fn copy_to_bytes(&mut self, len: usize) -> Bytes {
386        assert!(len <= self.remaining, "copy_to_bytes out of bounds");
387        if len == 0 {
388            return Bytes::new();
389        }
390        if let Some((buf, range)) = self.chunk_range()
391            && len <= range.len()
392        {
393            let bytes = buf.buffer.slice(range.start..range.start + len);
394            self.advance(len);
395            return bytes;
396        }
397
398        // A field spanning pages must be coalesced around the interleaved checksums
399        let mut bytes = BytesMut::with_capacity(len);
400        bytes.put(self.take(len));
401        bytes.freeze()
402    }
403
404    fn remaining(&self) -> usize {
405        self.remaining
406    }
407
408    #[inline(always)]
409    fn try_copy_to_slice(&mut self, mut dst: &mut [u8]) -> Result<(), TryGetError> {
410        if dst.len() > self.remaining {
411            return Err(TryGetError {
412                requested: dst.len(),
413                available: self.remaining,
414            });
415        }
416
417        // Fast path: the request ends strictly inside the current page, so the cursor
418        // stays on this page and no page or buffer transition is needed.
419        let chunk = self.chunk();
420        if dst.len() < chunk.len() {
421            dst.copy_from_slice(&chunk[..dst.len()]);
422            self.offset_in_page += dst.len();
423            self.remaining -= dst.len();
424            return Ok(());
425        }
426
427        while !dst.is_empty() {
428            let src = self.chunk();
429            let cnt = usize::min(src.len(), dst.len());
430            dst[..cnt].copy_from_slice(&src[..cnt]);
431            dst = &mut dst[cnt..];
432            self.advance(cnt);
433        }
434        Ok(())
435    }
436
437    fn chunk(&self) -> &[u8] {
438        self.chunk_range()
439            .map_or(&[], |(buf, range)| &buf.buffer.as_ref()[range])
440    }
441
442    fn advance(&mut self, mut cnt: usize) {
443        self.remaining = self.remaining.saturating_sub(cnt);
444
445        while cnt > 0 {
446            let Some(buf) = self.buffers.front() else {
447                break;
448            };
449
450            // Advance within current buffer
451            while cnt > 0 && self.current_page < buf.num_pages {
452                let page_len = Self::page_len(buf, self.current_page, self.page_size);
453                let available = page_len - self.offset_in_page;
454                if cnt < available {
455                    self.offset_in_page += cnt;
456                    return;
457                }
458                cnt -= available;
459                self.current_page += 1;
460                self.offset_in_page = 0;
461            }
462
463            // Current buffer exhausted, move to next
464            if self.current_page >= buf.num_pages {
465                self.buffers.pop_front();
466                self.current_page = 0;
467                self.offset_in_page = 0;
468            }
469        }
470    }
471}
472
473/// Replays logical data from a blob containing pages with interleaved CRCs.
474///
475/// This combines async I/O (`PageReader`) with sync buffering (`ReplayBuf`)
476/// to provide an `ensure(n)` + `Buf` interface for codec decoding.
477///
478/// A nonempty byte field within one page shares the backing of the batch that page was read in, or
479/// of the in-memory final partial page. Retaining the field keeps that backing alive after the
480/// replay advances or is dropped. Fields spanning pages are copied into separate allocations.
481pub struct Replay<B: Blob> {
482    /// Async I/O component.
483    reader: PageReader<B>,
484    /// Sync buffering component.
485    buffer: ReplayBuf,
486    /// Whether the blob has been fully read.
487    exhausted: bool,
488}
489
490impl<B: Blob> Replay<B> {
491    /// Creates a new Replay from a PageReader.
492    pub(super) const fn new(reader: PageReader<B>) -> Self {
493        let physical_page_size = reader.physical_page_size();
494        let page_size = reader.page_size();
495        Self {
496            reader,
497            buffer: ReplayBuf::new(physical_page_size, page_size),
498            exhausted: false,
499        }
500    }
501
502    /// Returns the size of the blob.
503    pub const fn blob_size(&self) -> u64 {
504        self.reader.blob_size()
505    }
506
507    /// Returns true if the reader has been exhausted (no more pages to read).
508    ///
509    /// When exhausted, the buffer may still contain data that hasn't been consumed.
510    /// Callers should check `remaining()` to see if there's data left to process.
511    pub const fn is_exhausted(&self) -> bool {
512        self.exhausted
513    }
514
515    /// Ensures at least `n` bytes are available in the buffer.
516    ///
517    /// This method fills the buffer from the blob until either:
518    /// - At least `n` bytes are available (returns `Ok(true)`)
519    /// - The blob is exhausted with fewer than `n` bytes (returns `Ok(false)`)
520    /// - A read error occurs (returns `Err`)
521    ///
522    /// When `Ok(false)` is returned, callers should still attempt to process
523    /// the remaining bytes in the buffer (check `remaining()`), as they may
524    /// contain valid data that doesn't require the full `n` bytes.
525    pub async fn ensure(&mut self, n: usize) -> Result<bool, Error> {
526        while self.buffer.remaining < n && !self.exhausted {
527            match self.reader.fill().await? {
528                Some((state, logical_bytes)) => {
529                    self.buffer.push(state, logical_bytes);
530                }
531                None => {
532                    self.exhausted = true;
533                }
534            }
535        }
536        Ok(self.buffer.remaining >= n)
537    }
538
539    /// Seeks to `offset` in the blob, returning `Err(BlobInsufficientLength)` if `offset` exceeds
540    /// the blob size.
541    pub fn seek_to(&mut self, offset: u64) -> Result<(), Error> {
542        if offset > self.reader.blob_size() {
543            return Err(Error::BlobInsufficientLength);
544        }
545
546        self.buffer.clear();
547        self.exhausted = false;
548
549        let page_size = self.reader.page_size as u64;
550        self.reader.seek_to_page(offset / page_size);
551        self.buffer.current_page = 0;
552        self.buffer.offset_in_page = (offset % page_size) as usize;
553
554        Ok(())
555    }
556}
557
558impl<B: Blob> Buf for Replay<B> {}
559
560impl<B: Blob> bytes::Buf for Replay<B> {
561    fn copy_to_bytes(&mut self, len: usize) -> Bytes {
562        self.buffer.copy_to_bytes(len)
563    }
564
565    fn remaining(&self) -> usize {
566        self.buffer.remaining()
567    }
568
569    #[inline(always)]
570    fn try_copy_to_slice(&mut self, dst: &mut [u8]) -> Result<(), TryGetError> {
571        self.buffer.try_copy_to_slice(dst)
572    }
573
574    fn chunk(&self) -> &[u8] {
575        self.buffer.chunk()
576    }
577
578    fn advance(&mut self, cnt: usize) {
579        self.buffer.advance(cnt);
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::{super::writer::Writer, *};
586    use crate::{
587        Handle, IoBufs, Runner as _, Storage as _, WriteOptions,
588        buffer::paged::{CacheRef, Sealed},
589        deterministic,
590    };
591    use bytes::Buf as _;
592    use commonware_macros::test_traced;
593    use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
594    use futures::task::noop_waker_ref;
595    use std::{
596        sync::atomic::{AtomicUsize, Ordering},
597        task::{Context, Poll},
598    };
599
600    /// Every read in call order. Each records whether it was a started read, which every replay
601    /// batch must be, and its ranges.
602    type Calls = Arc<Mutex<Vec<(bool, Vec<(u64, usize)>)>>>;
603
604    /// How a [GroupingBlob] serves the reads it records.
605    #[derive(Default)]
606    struct Reads {
607        /// Fail reads at or past this offset.
608        fail_from: Option<u64>,
609        /// Times each read yields after being recorded, before it completes.
610        yields: usize,
611        /// Fail every started read after it has read its bytes.
612        trailing_error: bool,
613        /// Hold every started read until the test releases its offset through [Gates].
614        gated: bool,
615        /// Serve started reads only while holding one of these permits, in request order, and
616        /// fail the first read at offset 0. The counter records the attempts at that offset.
617        admission: Option<(Arc<tokio::sync::Semaphore>, Arc<AtomicUsize>)>,
618        /// Fail the first read at this offset only. The counter records the attempts there.
619        fail_once: Option<(u64, Arc<AtomicUsize>)>,
620    }
621
622    /// The offset of a read held by a [GroupingBlob] and the release that completes it.
623    type Gate = (u64, oneshot::Sender<()>);
624
625    /// Reads held by a [GroupingBlob] until released.
626    #[derive(Clone, Default)]
627    struct Gates(Arc<Mutex<Vec<Gate>>>);
628
629    impl Gates {
630        /// Release the read at `offset` that is still awaited.
631        fn release(&self, offset: u64) {
632            let mut gates = self.0.lock();
633            let i = gates
634                .iter()
635                .position(|(o, release)| *o == offset && !release.is_closed())
636                .unwrap();
637            let _ = gates.remove(i).1.send(());
638        }
639
640        /// Offsets of the held reads that are still awaited.
641        fn open(&self) -> Vec<u64> {
642            self.0
643                .lock()
644                .iter()
645                .filter(|(_, release)| !release.is_closed())
646                .map(|(offset, _)| *offset)
647                .collect()
648        }
649    }
650
651    /// Records every read and serves it as its [Reads] say.
652    struct GroupingBlob<B> {
653        inner: B,
654        calls: Calls,
655        reads: Reads,
656        gates: Gates,
657    }
658
659    impl<B> GroupingBlob<B> {
660        /// Record a read and yield `yields` times so a test can observe it before it completes.
661        async fn begin(&self, many: bool, ranges: &[(u64, usize)]) -> Result<(), Error> {
662            self.record(many, ranges)?;
663            for _ in 0..self.reads.yields {
664                crate::reschedule().await;
665            }
666            Ok(())
667        }
668
669        fn record(&self, many: bool, ranges: &[(u64, usize)]) -> Result<(), Error> {
670            self.calls.lock().push((many, ranges.to_vec()));
671            if self
672                .reads
673                .fail_once
674                .as_ref()
675                .is_some_and(|(offset, attempts)| {
676                    ranges[0].0 == *offset && attempts.fetch_add(1, Ordering::SeqCst) == 0
677                })
678            {
679                return Err(Error::ReadFailed);
680            }
681            if self
682                .reads
683                .fail_from
684                .is_some_and(|fail| ranges.iter().any(|(offset, _)| *offset >= fail))
685            {
686                return Err(Error::ReadFailed);
687            }
688            Ok(())
689        }
690    }
691
692    impl<B: Blob> Blob for GroupingBlob<B> {
693        async fn read_at_buf(
694            &self,
695            offset: u64,
696            len: usize,
697            bufs: impl Into<IoBufsMut> + Send,
698            options: ReadOptions,
699        ) -> Result<IoBufsMut, Error> {
700            self.begin(false, &[(offset, len)]).await?;
701            self.inner.read_at_buf(offset, len, bufs, options).await
702        }
703
704        async fn read_at(
705            &self,
706            offset: u64,
707            len: usize,
708            options: ReadOptions,
709        ) -> Result<IoBufsMut, Error> {
710            self.begin(false, &[(offset, len)]).await?;
711            self.inner.read_at(offset, len, options).await
712        }
713
714        /// Start the read as the [Reads] say: record it, fail it outright, or hand back bytes
715        /// that land only once the test releases their gate, a permit admits them, and they
716        /// have yielded.
717        async fn start_read_many(
718            &self,
719            ranges: &[(u64, usize)],
720            options: ReadOptions,
721        ) -> Handle<Vec<IoBufsMut>> {
722            if let Err(err) = self.record(true, ranges) {
723                return Handle::ready(Err(err));
724            }
725            let bytes = self.inner.start_read_many(ranges, options).await;
726            let offset = ranges[0].0;
727            let gate = self.reads.gated.then(|| {
728                let (release, released) = oneshot::channel();
729                self.gates.0.lock().push((offset, release));
730                released
731            });
732            let admission = self.reads.admission.clone();
733            let yields = self.reads.yields;
734            let trailing_error = self.reads.trailing_error;
735            Handle::from_future(async move {
736                if let Some(released) = gate {
737                    let _ = released.await;
738                }
739                let _permit = match &admission {
740                    Some((permits, _)) => Some(permits.acquire().await.unwrap()),
741                    None => None,
742                };
743                if let Some((_, attempts)) = &admission {
744                    let fail = offset == 0 && attempts.fetch_add(1, Ordering::SeqCst) == 0;
745                    crate::reschedule().await;
746                    crate::reschedule().await;
747                    if fail {
748                        return Err(Error::ReadFailed);
749                    }
750                }
751                for _ in 0..yields {
752                    crate::reschedule().await;
753                }
754                let bufs = bytes.await?;
755                if trailing_error {
756                    return Err(Error::ReadFailed);
757                }
758                Ok(bufs)
759            })
760        }
761
762        async fn write_at(
763            &self,
764            offset: u64,
765            bufs: impl Into<IoBufs> + Send,
766            options: WriteOptions,
767        ) -> Result<(), Error> {
768            self.inner.write_at(offset, bufs, options).await
769        }
770        async fn resize(&self, len: u64) -> Result<(), Error> {
771            self.inner.resize(len).await
772        }
773        async fn sync(&self) -> Result<(), Error> {
774            self.inner.sync().await
775        }
776        async fn start_sync(&self) -> Handle<()> {
777            self.inner.start_sync().await
778        }
779    }
780
781    /// The pipelining tests use 64-byte pages and a replay buffer of 64 pages, split into
782    /// [MAX_READ_AHEAD] + 1 batches, over a blob of [PIPE_BLOB_BATCHES] batches plus a partial
783    /// page.
784    const PIPE_PAGE_SIZE: NonZeroU16 = NZU16!(64);
785    const PIPE_PHYSICAL_PAGE: usize = 64 + Checksum::SIZE;
786    const PIPE_BUFFER_PAGES: usize = 64;
787    const PIPE_BUFFER_BYTES: usize = PIPE_BUFFER_PAGES * PIPE_PHYSICAL_PAGE;
788    const PIPE_BATCH_PAGES: usize = PIPE_BUFFER_PAGES / (MAX_READ_AHEAD + 1);
789    const PIPE_BATCH_BYTES: usize = PIPE_BATCH_PAGES * PIPE_PHYSICAL_PAGE;
790    const PIPE_BATCH_LOGICAL: usize = PIPE_BATCH_PAGES * 64;
791    const PIPE_BLOB_BATCHES: usize = MAX_READ_AHEAD + 2;
792    const PIPE_BLOB_BYTES: usize = PIPE_BLOB_BATCHES * PIPE_BATCH_LOGICAL + 10;
793
794    /// Write `len` patterned bytes through a recording blob with `page_size` pages and a write
795    /// buffer of `buffer` bytes that serves reads as `reads` say, and return its sealed view, the
796    /// recorded reads, and the bytes.
797    async fn recorded_blob(
798        context: &deterministic::Context,
799        name: &[u8],
800        page_size: NonZeroU16,
801        buffer: usize,
802        len: usize,
803        reads: Reads,
804    ) -> (
805        Sealed<GroupingBlob<<deterministic::Context as crate::Storage>::Blob>>,
806        Calls,
807        Vec<u8>,
808        Gates,
809    ) {
810        let calls: Calls = Arc::new(Mutex::new(Vec::new()));
811        let gates = Gates::default();
812        let (blob, size) = context.open("pipelining", name).await.unwrap();
813        let blob = GroupingBlob {
814            inner: blob,
815            calls: calls.clone(),
816            reads,
817            gates: gates.clone(),
818        };
819        let cache = CacheRef::new(page_size, NZUsize!(4));
820        let writer = Writer::new(blob, size, buffer, cache).await.unwrap();
821        let data: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
822        let (writer, _) = writer.append(&data).await.unwrap();
823        let (_, sealed) = writer.snapshot().await.unwrap();
824        calls.lock().clear();
825        (sealed, calls, data, gates)
826    }
827
828    /// A [recorded_blob] of [PIPE_BLOB_BYTES] bytes with the pipelining tests' sizes.
829    async fn piped_blob(
830        context: &deterministic::Context,
831        name: &[u8],
832        reads: Reads,
833    ) -> (
834        Sealed<GroupingBlob<<deterministic::Context as crate::Storage>::Blob>>,
835        Calls,
836        Vec<u8>,
837        Gates,
838    ) {
839        recorded_blob(
840            context,
841            name,
842            PIPE_PAGE_SIZE,
843            PIPE_BUFFER_BYTES,
844            PIPE_BLOB_BYTES,
845            reads,
846        )
847        .await
848    }
849
850    /// The reads a replay of [PIPE_BLOB_BYTES] bytes starts, one per batch.
851    fn piped_calls(batches: std::ops::Range<usize>) -> Vec<(bool, Vec<(u64, usize)>)> {
852        batches
853            .map(|i| {
854                (
855                    true,
856                    vec![((i * PIPE_BATCH_BYTES) as u64, PIPE_BATCH_BYTES)],
857                )
858            })
859            .collect()
860    }
861
862    /// The reads the first fill of a replay issues with batches of `batch` pipelining-test pages,
863    /// one for the decoder and [MAX_READ_AHEAD] ahead of it.
864    fn first_fill_calls(batch: usize) -> Vec<(bool, Vec<(u64, usize)>)> {
865        let len = batch * PIPE_PHYSICAL_PAGE;
866        (0..=MAX_READ_AHEAD)
867            .map(|i| (true, vec![((i * len) as u64, len)]))
868            .collect()
869    }
870
871    /// Consume `expected.len()` buffered bytes, checking them against `expected`.
872    async fn take<B: Blob>(replay: &mut Replay<B>, expected: &[u8]) {
873        assert!(replay.ensure(expected.len()).await.unwrap());
874        assert_eq!(replay.copy_to_bytes(expected.len()).as_ref(), expected);
875    }
876
877    /// Holding reads in flight must not cost the replay its `Sync` bound.
878    #[test]
879    fn test_replay_is_sync() {
880        fn assert_sync<T: Sync>() {}
881        assert_sync::<Replay<<deterministic::Context as crate::Storage>::Blob>>();
882    }
883
884    /// A read ahead that fails is read again on demand, so the replay sees every byte.
885    #[test]
886    fn test_failed_read_ahead_is_read_again_on_demand() {
887        deterministic::Runner::default().start(|context| async move {
888            let reads = Reads {
889                fail_once: Some((PIPE_BATCH_BYTES as u64, Arc::new(AtomicUsize::new(0)))),
890                ..Reads::default()
891            };
892            let (sealed, calls, data, _) =
893                piped_blob(&context, b"speculative-failure", reads).await;
894            let mut replay = sealed
895                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
896                .unwrap();
897            take(&mut replay, &data[..PIPE_BATCH_LOGICAL]).await;
898            take(&mut replay, &data[PIPE_BATCH_LOGICAL..]).await;
899            assert!(!replay.ensure(1).await.unwrap());
900            assert_eq!(
901                calls
902                    .lock()
903                    .iter()
904                    .filter(|(_, ranges)| ranges[0].0 == PIPE_BATCH_BYTES as u64)
905                    .count(),
906                2
907            );
908        });
909    }
910
911    /// A blob whose started reads take turns only while their handles are polled models reads
912    /// ahead that can no longer complete, so a retry must not wait on them.
913    #[rstest::rstest]
914    #[case(1)]
915    #[case(2)]
916    #[case(3)]
917    #[case(4)]
918    fn test_retry_does_not_wait_on_queued_reads(
919        #[case] permits: usize,
920        #[values(false, true)] cancel: bool,
921    ) {
922        use crate::Clock as _;
923        deterministic::Runner::default().start(|context| async move {
924            let admission = Arc::new(tokio::sync::Semaphore::new(permits));
925            let attempts = Arc::new(AtomicUsize::new(0));
926            let reads = Reads {
927                admission: Some((admission, attempts.clone())),
928                ..Reads::default()
929            };
930            let (sealed, _, data, _) = piped_blob(&context, b"permit-retry", reads).await;
931            let mut replay = sealed
932                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
933                .unwrap();
934            if cancel {
935                let mut first = Box::pin(replay.ensure(1));
936                assert!(
937                    first
938                        .as_mut()
939                        .poll(&mut Context::from_waker(noop_waker_ref()))
940                        .is_pending()
941                );
942                drop(first);
943            } else {
944                assert!(matches!(replay.ensure(1).await, Err(Error::ReadFailed)));
945            }
946            assert!(attempts.load(Ordering::SeqCst) >= 1);
947            commonware_macros::select! {
948                result = replay.ensure(1) => assert!(result.unwrap()),
949                _ = context.sleep(std::time::Duration::from_secs(1)) => {
950                    panic!("retry hung with {permits} permits, cancellation {cancel}");
951                },
952            }
953            take(&mut replay, &data[..PIPE_BATCH_LOGICAL]).await;
954        });
955    }
956
957    #[test_traced]
958    fn test_batches_split_the_buffer() {
959        let executor = deterministic::Runner::default();
960        executor.start(|context| async move {
961            // A buffer splits into MAX_READ_AHEAD + 1 batches, each rounded up to whole pages, so
962            // a buffer of fewer pages than batches keeps single pages in flight.
963            let full = MAX_READ_AHEAD + 1;
964            for (pages, batch) in [
965                (1, 1),
966                (full - 1, 1),
967                (full, 1),
968                (full + 1, 2),
969                (2 * full - 1, 2),
970                (2 * full + 1, 3),
971                (PIPE_BUFFER_PAGES, PIPE_BATCH_PAGES),
972            ] {
973                let (sealed, calls, _, _) = recorded_blob(
974                    &context,
975                    format!("depth-{pages}").as_bytes(),
976                    PIPE_PAGE_SIZE,
977                    pages * PIPE_PHYSICAL_PAGE,
978                    PIPE_BLOB_BYTES,
979                    Reads::default(),
980                )
981                .await;
982                let mut replay = sealed
983                    .replay(NZUsize!(pages * PIPE_PHYSICAL_PAGE), ReadOptions::default())
984                    .unwrap();
985                assert!(replay.ensure(1).await.unwrap());
986                assert_eq!(*calls.lock(), first_fill_calls(batch), "{pages} pages");
987            }
988        });
989    }
990
991    #[test_traced]
992    fn test_fill_keeps_batches_in_flight() {
993        let executor = deterministic::Runner::default();
994        executor.start(|context| async move {
995            let (sealed, calls, data, _) =
996                piped_blob(&context, b"pipeline", Reads::default()).await;
997            let mut replay = sealed
998                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
999                .unwrap();
1000
1001            // The first fill issues the first batch and MAX_READ_AHEAD more before waiting, all
1002            // of which fit within the buffer together.
1003            let ahead = MAX_READ_AHEAD;
1004            assert!(replay.ensure(1).await.unwrap());
1005            assert_eq!(*calls.lock(), piped_calls(0..ahead + 1));
1006
1007            // Decoding a batch issues nothing, and reaching the next batch tops the queue up.
1008            take(&mut replay, &data[..PIPE_BATCH_LOGICAL]).await;
1009            assert_eq!(calls.lock().len(), ahead + 1);
1010            take(
1011                &mut replay,
1012                &data[PIPE_BATCH_LOGICAL..2 * PIPE_BATCH_LOGICAL],
1013            )
1014            .await;
1015            assert_eq!(*calls.lock(), piped_calls(0..ahead + 2));
1016            take(
1017                &mut replay,
1018                &data[2 * PIPE_BATCH_LOGICAL..PIPE_BLOB_BATCHES * PIPE_BATCH_LOGICAL],
1019            )
1020            .await;
1021            assert_eq!(*calls.lock(), piped_calls(0..PIPE_BLOB_BATCHES));
1022
1023            // The frozen partial page needs no read.
1024            take(&mut replay, &data[PIPE_BLOB_BATCHES * PIPE_BATCH_LOGICAL..]).await;
1025            assert!(!replay.ensure(1).await.unwrap());
1026            assert!(replay.is_exhausted());
1027        });
1028    }
1029
1030    #[test_traced]
1031    fn test_seek_discards_the_batches_in_flight() {
1032        let executor = deterministic::Runner::default();
1033        executor.start(|context| async move {
1034            // Reads complete only when released, so the batches read ahead stay in flight until
1035            // the seek discards them.
1036            let reads = Reads {
1037                gated: true,
1038                ..Reads::default()
1039            };
1040            let (sealed, calls, data, gates) = piped_blob(&context, b"seek", reads).await;
1041            let mut replay = sealed
1042                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
1043                .unwrap();
1044            let ahead = MAX_READ_AHEAD;
1045            {
1046                let mut fill = Box::pin(replay.ensure(1));
1047                assert!(
1048                    fill.as_mut()
1049                        .poll(&mut Context::from_waker(noop_waker_ref()))
1050                        .is_pending()
1051                );
1052                gates.release(0);
1053                assert!(fill.await.unwrap());
1054            }
1055            assert_eq!(calls.lock().len(), ahead + 1);
1056            let in_flight: Vec<u64> = (1..=ahead).map(|i| (i * PIPE_BATCH_BYTES) as u64).collect();
1057            assert_eq!(gates.open(), in_flight);
1058
1059            // Seeking into the second-to-last batch discards the batches in flight, which
1060            // nothing awaits any more, and reads from there.
1061            let batch = PIPE_BLOB_BATCHES - 2;
1062            let offset = batch * PIPE_BATCH_LOGICAL + 5;
1063            replay.seek_to(offset as u64).unwrap();
1064            assert!(gates.open().is_empty());
1065            {
1066                let mut fill = Box::pin(replay.ensure(1));
1067                assert!(
1068                    fill.as_mut()
1069                        .poll(&mut Context::from_waker(noop_waker_ref()))
1070                        .is_pending()
1071                );
1072                for i in batch..PIPE_BLOB_BATCHES {
1073                    gates.release((i * PIPE_BATCH_BYTES) as u64);
1074                }
1075                assert!(fill.await.unwrap());
1076            }
1077            take(&mut replay, &data[offset..]).await;
1078            assert!(!replay.ensure(1).await.unwrap());
1079            assert_eq!(
1080                calls.lock()[ahead + 1..],
1081                piped_calls(batch..PIPE_BLOB_BATCHES)
1082            );
1083        });
1084    }
1085
1086    #[test_traced]
1087    fn test_failed_fill_reads_the_same_batch_again() {
1088        let executor = deterministic::Runner::default();
1089        executor.start(|context| async move {
1090            let fail_from = Some(PIPE_BATCH_BYTES as u64);
1091            let reads = Reads {
1092                fail_from,
1093                ..Reads::default()
1094            };
1095            let (sealed, calls, data, _) = piped_blob(&context, b"retry", reads).await;
1096            let mut replay = sealed
1097                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
1098                .unwrap();
1099            take(&mut replay, &data[..PIPE_BATCH_LOGICAL]).await;
1100
1101            // A failed speculative read is reissued on demand. Persistent errors leave the
1102            // decoder's position unchanged for a subsequent fill.
1103            for _ in 0..2 {
1104                assert!(matches!(replay.ensure(1).await, Err(Error::ReadFailed)));
1105            }
1106            let ahead = MAX_READ_AHEAD;
1107            let mut expected = piped_calls(0..ahead + 2);
1108            expected.extend(piped_calls(1..ahead + 2));
1109            expected.extend(piped_calls(1..ahead + 2));
1110            assert_eq!(*calls.lock(), expected);
1111        });
1112    }
1113
1114    #[test_traced]
1115    fn test_dropped_fill_reads_the_same_batch_again() {
1116        let executor = deterministic::Runner::default();
1117        executor.start(|context| async move {
1118            // Reads yield before completing, so a fill can be dropped while it waits on one.
1119            let reads = Reads {
1120                yields: 2,
1121                ..Reads::default()
1122            };
1123            let (sealed, calls, data, _) = piped_blob(&context, b"dropped", reads).await;
1124            let mut replay = sealed
1125                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
1126                .unwrap();
1127
1128            // Drop a fill while its batch is still being read.
1129            {
1130                let mut fill = Box::pin(replay.ensure(1));
1131                assert!(
1132                    fill.as_mut()
1133                        .poll(&mut Context::from_waker(noop_waker_ref()))
1134                        .is_pending()
1135                );
1136            }
1137            let ahead = MAX_READ_AHEAD;
1138            assert_eq!(*calls.lock(), piped_calls(0..ahead + 1));
1139
1140            // The next fill discards the queued reads and starts again at the decoder's
1141            // position, so the decoder sees the right bytes.
1142            take(&mut replay, &data[..2 * PIPE_BATCH_LOGICAL]).await;
1143            let mut expected = piped_calls(0..ahead + 1);
1144            expected.extend(piped_calls(0..ahead + 2));
1145            assert_eq!(*calls.lock(), expected);
1146        });
1147    }
1148
1149    #[test_traced]
1150    fn test_batches_landing_before_and_after_their_wait_are_served() {
1151        let executor = deterministic::Runner::default();
1152        executor.start(|context| async move {
1153            // Reads complete only when released, so a batch read ahead can land before the
1154            // decoder asks for it or while the decoder is already waiting on it.
1155            let reads = Reads {
1156                gated: true,
1157                ..Reads::default()
1158            };
1159            let (sealed, calls, data, gates) = piped_blob(&context, b"gated", reads).await;
1160            let mut replay = sealed
1161                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
1162                .unwrap();
1163
1164            // The first fill issues the first batch and MAX_READ_AHEAD more, then waits on the
1165            // first. Releasing the second before the first lands it while nobody awaits it.
1166            {
1167                let mut fill = Box::pin(replay.ensure(1));
1168                assert!(
1169                    fill.as_mut()
1170                        .poll(&mut Context::from_waker(noop_waker_ref()))
1171                        .is_pending()
1172                );
1173                assert_eq!(calls.lock().len(), MAX_READ_AHEAD + 1);
1174                gates.release(PIPE_BATCH_BYTES as u64);
1175                gates.release(0);
1176                assert!(fill.await.unwrap());
1177            }
1178            take(&mut replay, &data[..PIPE_BATCH_LOGICAL]).await;
1179
1180            // The landed second batch is served without another read.
1181            take(
1182                &mut replay,
1183                &data[PIPE_BATCH_LOGICAL..2 * PIPE_BATCH_LOGICAL],
1184            )
1185            .await;
1186            assert_eq!(calls.lock().len(), MAX_READ_AHEAD + 2);
1187
1188            // The third is released while a fill waits on it with the decoder's waker, which
1189            // the completion must wake.
1190            let wakes = Arc::new(Wakes::default());
1191            let waker = futures::task::waker(wakes.clone());
1192            {
1193                let mut fill = Box::pin(replay.ensure(1));
1194                assert!(
1195                    fill.as_mut()
1196                        .poll(&mut Context::from_waker(&waker))
1197                        .is_pending()
1198                );
1199                gates.release(2 * PIPE_BATCH_BYTES as u64);
1200                assert_eq!(wakes.0.load(Ordering::SeqCst), 1);
1201                assert!(matches!(
1202                    fill.as_mut().poll(&mut Context::from_waker(&waker)),
1203                    Poll::Ready(Ok(true))
1204                ));
1205            }
1206            take(
1207                &mut replay,
1208                &data[2 * PIPE_BATCH_LOGICAL..3 * PIPE_BATCH_LOGICAL],
1209            )
1210            .await;
1211        });
1212    }
1213
1214    /// Counts the wakes a pending fill receives.
1215    #[derive(Default)]
1216    struct Wakes(AtomicUsize);
1217
1218    impl futures::task::ArcWake for Wakes {
1219        fn wake_by_ref(wakes: &Arc<Self>) {
1220            wakes.0.fetch_add(1, Ordering::SeqCst);
1221        }
1222    }
1223
1224    #[test_traced]
1225    fn test_read_error_after_the_batch_fails_the_fill() {
1226        let executor = deterministic::Runner::default();
1227        executor.start(|context| async move {
1228            // A started read that reads its bytes and then fails has failed.
1229            let reads = Reads {
1230                trailing_error: true,
1231                ..Reads::default()
1232            };
1233            let (sealed, _, _, _) = piped_blob(&context, b"late-error", reads).await;
1234            let mut replay = sealed
1235                .replay(NZUsize!(PIPE_BUFFER_BYTES), ReadOptions::default())
1236                .unwrap();
1237            assert!(matches!(replay.ensure(1).await, Err(Error::ReadFailed)));
1238        });
1239    }
1240
1241    #[test]
1242    fn test_replay_buf_bytes_share_pages() {
1243        let source = bytes::Bytes::from_static(b"abcd............efgh............");
1244        let range = source.as_ptr_range();
1245        let mut replay = ReplayBuf::new(16, 4);
1246        replay.push(
1247            BufferState {
1248                buffer: source,
1249                num_pages: 2,
1250                last_page_len: 4,
1251            },
1252            8,
1253        );
1254
1255        let first = replay.copy_to_bytes(3);
1256        assert_eq!(first.as_ref(), b"abc");
1257        assert!(range.contains(&first.as_ptr()));
1258
1259        let crossing = replay.copy_to_bytes(3);
1260        assert_eq!(crossing.as_ref(), b"def");
1261        assert!(!range.contains(&crossing.as_ptr()));
1262
1263        let last = replay.copy_to_bytes(2);
1264        assert_eq!(last.as_ref(), b"gh");
1265        assert!(range.contains(&last.as_ptr()));
1266        assert_eq!(replay.remaining(), 0);
1267    }
1268
1269    #[test]
1270    fn test_replay_buf_copy_to_slice_page_boundaries() {
1271        let source = bytes::Bytes::from_static(b"abcd............efgh............ijkl............");
1272        let mut replay = ReplayBuf::new(16, 4);
1273        replay.push(
1274            BufferState {
1275                buffer: source,
1276                num_pages: 3,
1277                last_page_len: 4,
1278            },
1279            12,
1280        );
1281
1282        // Ends inside the first page.
1283        let mut inside = [0u8; 3];
1284        replay.try_copy_to_slice(&mut inside).unwrap();
1285        assert_eq!(&inside, b"abc");
1286        assert_eq!(replay.chunk(), b"d");
1287        assert_eq!(replay.remaining(), 9);
1288
1289        // Ends exactly at the end of a non-final page.
1290        let mut boundary = [0u8; 1];
1291        replay.try_copy_to_slice(&mut boundary).unwrap();
1292        assert_eq!(&boundary, b"d");
1293        assert_eq!(replay.chunk(), b"efgh");
1294        assert_eq!(replay.remaining(), 8);
1295
1296        // Spans the page boundary.
1297        let mut spanning = [0u8; 6];
1298        replay.try_copy_to_slice(&mut spanning).unwrap();
1299        assert_eq!(&spanning, b"efghij");
1300        assert_eq!(replay.chunk(), b"kl");
1301        assert_eq!(replay.remaining(), 2);
1302
1303        // Ends exactly at the end of the last page.
1304        let mut tail = [0u8; 2];
1305        replay.try_copy_to_slice(&mut tail).unwrap();
1306        assert_eq!(&tail, b"kl");
1307        assert_eq!(replay.chunk(), b"");
1308        assert_eq!(replay.remaining(), 0);
1309
1310        let err = replay.try_copy_to_slice(&mut [0u8; 1]).unwrap_err();
1311        assert_eq!(err.requested, 1);
1312        assert_eq!(err.available, 0);
1313    }
1314
1315    const PAGE_SIZE: NonZeroU16 = NZU16!(103);
1316    const BUFFER_PAGES: usize = 2;
1317
1318    #[test_traced("DEBUG")]
1319    fn test_replay_basic() {
1320        let executor = deterministic::Runner::default();
1321        executor.start(|context: deterministic::Context| async move {
1322            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1323            assert_eq!(blob_size, 0);
1324
1325            let cache_ref = super::super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_PAGES));
1326            let mut append = Writer::new(blob, blob_size, BUFFER_PAGES * 115, cache_ref)
1327                .await
1328                .unwrap();
1329
1330            // Write data spanning multiple pages
1331            let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
1332            (append, _) = append.append(&data).await.unwrap();
1333            append = append.sync().await.unwrap();
1334
1335            // Create Replay
1336            let (_, mut replay) = append
1337                .replay(NZUsize!(BUFFER_PAGES), ReadOptions::default())
1338                .await
1339                .unwrap();
1340
1341            // Ensure all data is available
1342            replay.ensure(300).await.unwrap();
1343
1344            // Verify we got all the data
1345            assert_eq!(replay.remaining(), 300);
1346
1347            // Read all data via Buf interface
1348            let mut collected = Vec::new();
1349            while replay.remaining() > 0 {
1350                let chunk = replay.chunk();
1351                collected.extend_from_slice(chunk);
1352                let len = chunk.len();
1353                replay.advance(len);
1354            }
1355            assert_eq!(collected, data);
1356        });
1357    }
1358
1359    #[test_traced("DEBUG")]
1360    fn test_replay_partial_page() {
1361        let executor = deterministic::Runner::default();
1362        executor.start(|context: deterministic::Context| async move {
1363            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1364
1365            let cache_ref = super::super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_PAGES));
1366            let mut append = Writer::new(blob, blob_size, BUFFER_PAGES * 115, cache_ref)
1367                .await
1368                .unwrap();
1369
1370            // Write data that doesn't fill the last page
1371            let data: Vec<u8> = (1u8..=(PAGE_SIZE.get() + 10) as u8).collect();
1372            (append, _) = append.append(&data).await.unwrap();
1373            append = append.sync().await.unwrap();
1374
1375            let (_, mut replay) = append
1376                .replay(NZUsize!(BUFFER_PAGES), ReadOptions::default())
1377                .await
1378                .unwrap();
1379
1380            // Ensure all data is available
1381            replay.ensure(data.len()).await.unwrap();
1382
1383            assert_eq!(replay.remaining(), data.len());
1384        });
1385    }
1386
1387    #[test_traced("DEBUG")]
1388    fn test_replay_cross_buffer_boundary() {
1389        // Use a one-page buffer to force separate BufferStates per page.
1390        // This tests navigation across multiple BufferStates in the VecDeque.
1391        let executor = deterministic::Runner::default();
1392        executor.start(|context: deterministic::Context| async move {
1393            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1394            assert_eq!(blob_size, 0);
1395
1396            let cache_ref = super::super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_PAGES));
1397            let mut append = Writer::new(blob, blob_size, BUFFER_PAGES * 115, cache_ref)
1398                .await
1399                .unwrap();
1400
1401            // Write data spanning 4 pages (4 * 103 = 412 bytes, with last page partial)
1402            let data: Vec<u8> = (0u8..=255).cycle().take(400).collect();
1403            (append, _) = append.append(&data).await.unwrap();
1404            append = append.sync().await.unwrap();
1405
1406            // Create Replay with a buffer that holds one page, so every batch is one page.
1407            // Physical page size = 103 + 12 = 115 bytes.
1408            // Buffer size of 115 gives 115/115 = 1 page.
1409            let (_, mut replay) = append
1410                .replay(NZUsize!(115), ReadOptions::default())
1411                .await
1412                .unwrap();
1413
1414            // Load all four logical pages with one-page prefetches and a frozen partial tail.
1415            assert!(replay.ensure(400).await.unwrap());
1416            assert_eq!(replay.remaining(), 400);
1417
1418            // Read all data via Buf interface, verifying navigation across BufferStates.
1419            let mut collected = Vec::new();
1420            let mut chunks_read = 0;
1421            while replay.remaining() > 0 {
1422                let chunk = replay.chunk();
1423                assert!(
1424                    !chunk.is_empty(),
1425                    "chunk() returned empty but remaining > 0"
1426                );
1427                collected.extend_from_slice(chunk);
1428                let len = chunk.len();
1429                replay.advance(len);
1430                chunks_read += 1;
1431            }
1432
1433            assert_eq!(collected, data);
1434
1435            // With one-page batches and 4 pages, we expect at least 4 chunks
1436            // (one per page, though partial reads could result in more).
1437            assert!(
1438                chunks_read >= 4,
1439                "Expected at least 4 chunks for 4 pages, got {}",
1440                chunks_read
1441            );
1442        });
1443    }
1444
1445    #[test_traced("DEBUG")]
1446    fn test_replay_empty_blob() {
1447        // Test that replaying an empty blob works correctly.
1448        // ensure() should return Ok(false) when no data is available.
1449        let executor = deterministic::Runner::default();
1450        executor.start(|context: deterministic::Context| async move {
1451            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1452            assert_eq!(blob_size, 0);
1453
1454            let cache_ref = super::super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_PAGES));
1455            let append = Writer::new(blob, blob_size, BUFFER_PAGES * 115, cache_ref)
1456                .await
1457                .unwrap();
1458
1459            // Don't write any data - blob remains empty
1460            assert_eq!(append.size(), 0);
1461
1462            // Create Replay on empty blob
1463            let (_, mut replay) = append
1464                .replay(NZUsize!(BUFFER_PAGES), ReadOptions::default())
1465                .await
1466                .unwrap();
1467
1468            // Verify initial state - remaining is 0, but not yet marked exhausted
1469            // (exhausted is set after first fill attempt)
1470            assert_eq!(replay.remaining(), 0);
1471
1472            // ensure(0) should succeed (we have >= 0 bytes)
1473            assert!(replay.ensure(0).await.unwrap());
1474
1475            // ensure(1) should return Ok(false) - not enough data, and marks exhausted
1476            assert!(!replay.ensure(1).await.unwrap());
1477
1478            // Now should be marked as exhausted after the fill attempt
1479            assert!(replay.is_exhausted());
1480
1481            // chunk() should return empty slice
1482            assert!(replay.chunk().is_empty());
1483
1484            // remaining should still be 0
1485            assert_eq!(replay.remaining(), 0);
1486        });
1487    }
1488
1489    #[test_traced("DEBUG")]
1490    fn test_replay_seek_to() {
1491        let executor = deterministic::Runner::default();
1492        executor.start(|context: deterministic::Context| async move {
1493            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1494
1495            let cache_ref = super::super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_PAGES));
1496            let mut append = Writer::new(blob, blob_size, BUFFER_PAGES * 115, cache_ref)
1497                .await
1498                .unwrap();
1499
1500            // Write data spanning multiple pages
1501            let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
1502            (append, _) = append.append(&data).await.unwrap();
1503            append = append.sync().await.unwrap();
1504
1505            let (_, mut replay) = append
1506                .replay(NZUsize!(BUFFER_PAGES), ReadOptions::default())
1507                .await
1508                .unwrap();
1509
1510            // Seek forward, read, then seek backward
1511            replay.seek_to(150).unwrap();
1512            replay.ensure(50).await.unwrap();
1513            assert_eq!(replay.get_u8(), data[150]);
1514
1515            // Seek back to start
1516            replay.seek_to(0).unwrap();
1517            replay.ensure(1).await.unwrap();
1518            assert_eq!(replay.get_u8(), data[0]);
1519
1520            // Seek beyond blob size should error
1521            assert!(replay.seek_to(data.len() as u64 + 1).is_err());
1522
1523            // Seek into the blob, then drain the remaining bytes and verify their exact contents.
1524            let seek_offset = 150usize;
1525            replay.seek_to(seek_offset as u64).unwrap();
1526            let expected_remaining = data.len() - seek_offset;
1527            let mut collected = Vec::new();
1528            loop {
1529                // Load more data if needed
1530                if !replay.ensure(1).await.unwrap() {
1531                    break; // No more data available
1532                }
1533                let chunk = replay.chunk();
1534                if chunk.is_empty() {
1535                    break;
1536                }
1537                collected.extend_from_slice(chunk);
1538                let len = chunk.len();
1539                replay.advance(len);
1540            }
1541            assert_eq!(
1542                collected.len(),
1543                expected_remaining,
1544                "After seeking to {}, should read {} bytes but got {}",
1545                seek_offset,
1546                expected_remaining,
1547                collected.len()
1548            );
1549            assert_eq!(collected, &data[seek_offset..]);
1550
1551            // Seeking into the frozen tail must work after exhaustion, including at EOF.
1552            for offset in [data.len(), 250, 299] {
1553                replay.seek_to(Widen::widen(offset)).unwrap();
1554                let remaining = data.len() - offset;
1555                assert!(!replay.ensure(remaining + 1).await.unwrap());
1556                assert_eq!(replay.copy_to_bytes(remaining).as_ref(), &data[offset..]);
1557            }
1558        });
1559    }
1560}