Skip to main content

commonware_runtime/utils/buffer/paged/
sealed.rs

1//! Read-only counterpart to [`super::Writer`]: an immutable, page-cache-backed read handle for
2//! a blob whose logical content will no longer change.
3//!
4//! # Sealing
5//!
6//! [`super::Writer::seal`] returns a [`Sealed`] read handle and starts an fsync. Reads observe
7//! flushed bytes immediately, while durability waits for the sync handle.
8//!
9//! # Cheap sharing
10//!
11//! [`Sealed`] is `Clone` and shares its state via `Arc<SealedInner>`. Clones do not coordinate via
12//! any lock; they share the underlying [`Blob`] handle (which provides its own synchronization)
13//! and the page cache.
14
15use super::{
16    CHECKSUM_SIZE, CacheRef, Replay,
17    read::PageReader,
18    view::{Tail, View},
19};
20use crate::{Blob, Error, IoBuf, IoBufMut, IoBufs, ReadOptions};
21use commonware_utils::Widen;
22use std::{num::NonZeroUsize, sync::Arc};
23
24/// An immutable, page-cache-backed read handle for a [Blob]. The read-only counterpart to
25/// [`super::Writer`].
26pub struct Sealed<B: Blob> {
27    inner: Arc<SealedInner<B>>,
28}
29
30impl<B: Blob> Clone for Sealed<B> {
31    fn clone(&self) -> Self {
32        Self {
33            inner: self.inner.clone(),
34        }
35    }
36}
37
38struct SealedInner<B: Blob> {
39    /// The underlying blob being wrapped.
40    blob: Arc<B>,
41
42    /// Size of the sealed view, in bytes.
43    size: u64,
44
45    /// Logical bytes of the partial last page, if the blob ends in one. Bytes at offsets
46    /// `[size - partial_page.len(), size)` come from here; bytes below come from full pages on the
47    /// blob (via the page cache).
48    partial_page: Option<IoBuf>,
49
50    /// Reference to the page cache used for reads of full pages.
51    cache_ref: CacheRef,
52
53    /// Page-cache id. [`super::Writer::seal`] preserves the writer id so hot full pages remain
54    /// valid across the transition. Snapshots share this identity. Full pages stay immutable within
55    /// one writer incarnation, and each snapshot owns its frozen partial page.
56    id: u64,
57}
58
59impl<B: Blob> Sealed<B> {
60    /// Construct a [`Sealed`] from already-validated parts. Invoked by [`super::Writer::seal`].
61    pub(super) fn new(
62        blob: Arc<B>,
63        size: u64,
64        partial_page: Option<IoBuf>,
65        cache_ref: CacheRef,
66        id: u64,
67    ) -> Self {
68        Self {
69            inner: Arc::new(SealedInner {
70                blob,
71                size,
72                partial_page,
73                cache_ref,
74                id,
75            }),
76        }
77    }
78
79    /// Returns the size of the blob.
80    pub fn size(&self) -> u64 {
81        self.inner.size
82    }
83
84    /// Logical offset at which the partial-page bytes begin. Equal to `size` when there is no
85    /// partial page.
86    fn partial_offset(&self) -> u64 {
87        self.inner.size
88            - self
89                .inner
90                .partial_page
91                .as_ref()
92                .map_or(0, |p| p.len() as u64)
93    }
94
95    /// Returns a borrowed view over this blob.
96    fn view(&self) -> View<'_, B> {
97        View {
98            blob: &self.inner.blob,
99            cache_ref: &self.inner.cache_ref,
100            id: self.inner.id,
101            size: self.inner.size,
102            tail_offset: self.partial_offset(),
103            tail: Tail::Sealed(
104                self.inner
105                    .partial_page
106                    .as_ref()
107                    .map_or(&[][..], |p| p.as_ref()),
108            ),
109        }
110    }
111
112    /// Read exactly `len` immutable bytes starting at `offset`.
113    pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
114        self.view().read_at(offset, len).await
115    }
116
117    /// Read into `buf` if it can be done synchronously without I/O. Returns `true` only if all
118    /// `buf.len()` bytes were satisfied from the page cache and/or the in-memory tail. When `false`
119    /// is returned, the contents of `buf` are unspecified.
120    pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
121        self.view().try_read_sync_into(buf, offset)
122    }
123
124    /// Reads bytes starting at `offset` into `buf`.
125    pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
126        self.view().read_into(buf, offset).await
127    }
128
129    /// Reads up to `len` bytes starting at `offset`, but only as many as are available.
130    ///
131    /// Returns the buffer (truncated to actual bytes read) and the number of bytes read. Returns
132    /// an error if no bytes are available at the given offset.
133    pub async fn read_up_to(
134        &self,
135        offset: u64,
136        len: usize,
137        bufs: impl Into<IoBufMut> + Send,
138    ) -> Result<(IoBufMut, usize), Error> {
139        self.view().read_up_to(offset, len, bufs).await
140    }
141
142    /// Read multiple fixed-size items at sorted byte offsets into a contiguous caller buffer.
143    ///
144    /// `buf` must be exactly `offsets.len() * item_size` bytes. All offsets must be sorted,
145    /// non-overlapping, and within bounds.
146    ///
147    /// Returns the number of items fully served without a blob read (from the in-memory tail and the
148    /// page cache). The remaining items required at least one blob read.
149    pub async fn read_many_into(
150        &self,
151        buf: &mut [u8],
152        offsets: &[u64],
153        item_size: NonZeroUsize,
154    ) -> Result<usize, Error> {
155        self.view().read_many_into(buf, offsets, item_size).await
156    }
157
158    /// Like [`Self::read_many_into`], but each `(offset, len)` range has its own length. `buf`
159    /// holds the ranges back to back.
160    ///
161    /// Returns how many ranges were served entirely from the in-memory tail and page cache before
162    /// any blob read. Empty ranges count as served.
163    ///
164    /// # Panics
165    ///
166    /// Panics if a range starts before the previous range ends, or if `buf` is not the ranges'
167    /// total length.
168    pub async fn read_ranges_into(
169        &self,
170        buf: &mut [u8],
171        ranges: &[(u64, usize)],
172    ) -> Result<usize, Error> {
173        self.view().read_ranges_into(buf, ranges).await
174    }
175
176    /// Like [`Self::read_many_into`], but synchronous and cache-only. Returns the indices of
177    /// items that require a blob read. Their slots in `buf` hold unspecified bytes.
178    pub fn try_read_many_sync_into(
179        &self,
180        buf: &mut [u8],
181        offsets: &[u64],
182        item_size: NonZeroUsize,
183    ) -> Vec<usize> {
184        self.view().try_read_many_sync_into(buf, offsets, item_size)
185    }
186
187    /// Like [`Self::try_read_many_sync_into`], but for variable-length `(offset, len)` ranges:
188    /// `buf` holds one slot per range, back to back.
189    pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
190        self.view().try_read_ranges_sync_into(buf, ranges)
191    }
192
193    /// Returns a [Replay] for sequentially reading all logical bytes of the sealed view.
194    ///
195    /// Sealed values have no write buffer to flush, so unlike [`super::Writer::replay`] this method
196    /// is not async. Replay validates full pages read from storage and uses the frozen partial
197    /// page. Every underlying blob read uses `read_options`, including refills after seeking.
198    ///
199    /// Pages are read in batches ahead of the decoder. The batch being decoded and the batches in
200    /// flight together span `buffer_size`, each rounded up to whole pages, so they exceed it by
201    /// less than one page each. A fill that crosses into the next batch keeps the earlier batch
202    /// alive until its last bytes are consumed, and a large fill or a retained byte field keeps
203    /// more alive. A seek or a dropped replay abandons the batches read ahead, as
204    /// [`Blob::start_read_many`] describes.
205    pub fn replay(
206        &self,
207        buffer_size: NonZeroUsize,
208        read_options: ReadOptions,
209    ) -> Result<Replay<B>, Error> {
210        let page_size_nz = self.inner.cache_ref.page_size();
211        let page_size: u64 = page_size_nz.widen();
212        let physical_page_size = page_size
213            .checked_add(CHECKSUM_SIZE)
214            .ok_or(Error::OffsetOverflow)?;
215        let buffer_pages = (buffer_size.get() / physical_page_size as usize).max(1);
216
217        let partial_len = self
218            .inner
219            .partial_page
220            .as_ref()
221            .map_or(0, |p| p.len() as u64);
222        let full_pages = (self.inner.size - partial_len) / page_size;
223        let pages = full_pages + u64::from(partial_len > 0);
224        let physical_blob_size = physical_page_size
225            .checked_mul(pages)
226            .ok_or(Error::OffsetOverflow)?;
227        let logical_blob_size = self.inner.size;
228
229        let reader = PageReader::new(
230            self.inner.blob.clone(),
231            physical_blob_size,
232            logical_blob_size,
233            self.inner.partial_page.clone(),
234            buffer_pages,
235            page_size_nz,
236            read_options,
237        );
238        Ok(Replay::new(reader))
239    }
240
241    /// Page-cache id used for reads. Exposed for tests that verify the id is preserved across
242    /// [`super::Writer::seal`].
243    #[cfg(test)]
244    pub(super) fn cache_id(&self) -> u64 {
245        self.inner.id
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::{
253        Buf, Handle, IoBufsMut, Runner as _, Storage as _, WriteOptions,
254        buffer::{
255            paged::{CHECKSUM_SLOT_LEN_SIZE, Checksum, Writer},
256            tests::SyncTrackingBlob,
257        },
258        deterministic,
259        mocks::{DelayedSyncBlob, next_pending_sync},
260    };
261    use commonware_macros::test_traced;
262    use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
263    use std::num::NonZeroU16;
264
265    /// Replaying sealed pages with `DONT_CACHE` admits nothing to the page cache, whether the
266    /// view was sealed or captured by `try_snapshot`.
267    #[rstest::rstest]
268    fn test_replay_with_dont_cache_leaves_cache_cold(#[values(false, true)] snapshot: bool) {
269        deterministic::Runner::default().start(|context| async move {
270            let (blob, size) = context.open("review-cold", b"blob").await.unwrap();
271            let cache = CacheRef::new(PAGE_SIZE, NZUsize!(32));
272            let writer = Writer::new(blob, size, BUFFER_SIZE, cache.clone())
273                .await
274                .unwrap();
275            let page = PAGE_SIZE.get() as usize;
276            let total = page * 9 + 25;
277            let data: Vec<u8> = (0..total).map(|i| (i % 251) as u8).collect();
278            let (writer, _) = writer.append(&data).await.unwrap();
279            let writer = writer.sync().await.unwrap();
280            let sealed = if snapshot {
281                writer.try_snapshot().unwrap()
282            } else {
283                let (sealed, sync) = writer.seal().await.unwrap();
284                sync.await.unwrap();
285                sealed
286            };
287            cache.clear();
288            let mut probe = vec![0; page];
289            for i in 0..9 {
290                assert!(!sealed.try_read_sync_into(&mut probe, (i * page) as u64));
291            }
292            let mut replay = sealed
293                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::DONT_CACHE)
294                .unwrap();
295            assert!(replay.ensure(total).await.unwrap());
296            assert_eq!(replay.copy_to_bytes(total).as_ref(), data);
297            assert!(!replay.ensure(1).await.unwrap());
298            for i in 0..9 {
299                assert!(!sealed.try_read_sync_into(&mut probe, (i * page) as u64));
300            }
301            assert_eq!(
302                sealed.read_at(0, page).await.unwrap().coalesce().as_ref(),
303                &data[..page]
304            );
305            assert!(sealed.try_read_sync_into(&mut probe, 0));
306            assert_eq!(probe, data[..page]);
307        });
308    }
309
310    const PAGE_SIZE: NonZeroU16 = NZU16!(103); // janky page size to test alignment
311    const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
312
313    struct WritePause {
314        offset: u64,
315        prefix_len: usize,
316        started: oneshot::Sender<()>,
317        resume: oneshot::Receiver<()>,
318    }
319
320    /// Exposes a successful write's prefix while its remaining bytes are still pending.
321    struct SplitWriteBlob<B> {
322        inner: B,
323        pause: Mutex<Option<WritePause>>,
324    }
325
326    impl<B: Blob> Blob for SplitWriteBlob<B> {
327        async fn read_at_buf(
328            &self,
329            offset: u64,
330            len: usize,
331            bufs: impl Into<IoBufsMut> + Send,
332            options: ReadOptions,
333        ) -> Result<IoBufsMut, Error> {
334            self.inner.read_at_buf(offset, len, bufs, options).await
335        }
336
337        async fn read_at(
338            &self,
339            offset: u64,
340            len: usize,
341            options: ReadOptions,
342        ) -> Result<IoBufsMut, Error> {
343            self.inner.read_at(offset, len, options).await
344        }
345
346        async fn write_at(
347            &self,
348            offset: u64,
349            bufs: impl Into<IoBufs> + Send,
350            options: WriteOptions,
351        ) -> Result<(), Error> {
352            let mut bufs = bufs.into();
353            let pause = self.pause.lock().take();
354            let Some(pause) = pause else {
355                return self.inner.write_at(offset, bufs, options).await;
356            };
357            assert_eq!(offset, pause.offset);
358            assert!(pause.prefix_len < bufs.len());
359            self.inner
360                .write_at(offset, bufs.split_to(pause.prefix_len), options)
361                .await?;
362            pause.started.send(()).unwrap();
363            pause.resume.await.unwrap();
364            self.inner
365                .write_at(offset + Widen::widen(pause.prefix_len), bufs, options)
366                .await
367        }
368
369        async fn resize(&self, len: u64) -> Result<(), Error> {
370            self.inner.resize(len).await
371        }
372
373        async fn sync(&self) -> Result<(), Error> {
374            self.inner.sync().await
375        }
376
377        async fn start_sync(&self) -> Handle<()> {
378            self.inner.start_sync().await
379        }
380    }
381
382    /// Seal a [Writer] and assert the returned sync handle makes it durable.
383    #[test_traced("DEBUG")]
384    fn test_seal_starts_sync() {
385        let executor = deterministic::Runner::default();
386        executor.start(|_: deterministic::Context| async move {
387            let blob = SyncTrackingBlob::new();
388            let blob = Arc::new(blob);
389            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
390            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
391                .await
392                .unwrap();
393
394            // Append some data crossing several pages but don't sync.
395            let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
396            (append, _) = append.append(&data).await.unwrap();
397
398            let (durable_before, _writes_before, full_before, range_before) = blob.snapshot();
399            assert!(
400                durable_before.is_empty(),
401                "no bytes should be durable before the seal's sync"
402            );
403
404            let (sealed, sync) = append.seal().await.unwrap();
405            sync.await.unwrap();
406
407            let (durable_after, _writes_after, full_after, range_after) = blob.snapshot();
408            assert_eq!(full_after, full_before + 1);
409            assert!(
410                !durable_after.is_empty(),
411                "the seal's sync handle must make the appended bytes durable"
412            );
413            assert_eq!(
414                range_after, range_before,
415                "seal must not invoke a range-scoped write"
416            );
417
418            assert_eq!(sealed.size(), 300);
419        });
420    }
421
422    /// Sealing consumes the unique write handle; outstanding readers remain valid and agree
423    /// with the sealed view.
424    #[test_traced("DEBUG")]
425    fn test_seal_succeeds_with_readers() {
426        let executor = deterministic::Runner::default();
427        executor.start(|context: deterministic::Context| async move {
428            let (blob, blob_size) = context.open("test_partition", b"readers").await.unwrap();
429            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
430            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
431                .await
432                .unwrap();
433            (writer, _) = writer.append(b"hello world").await.unwrap();
434
435            // A snapshot captures the buffered bytes as an owned, frozen read handle.
436            let (writer, reader) = writer.snapshot().await.unwrap();
437            let reader_clone = reader.clone();
438            assert_eq!(reader.size(), 11);
439
440            // Seal succeeds while snapshots exist.
441            let (sealed, sync) = writer.seal().await.unwrap();
442            sync.await.unwrap();
443            assert_eq!(sealed.size(), 11);
444
445            // Both snapshot handles keep reading the frozen state and agree with the sealed view.
446            for r in [&reader, &reader_clone] {
447                assert_eq!(r.size(), 11);
448                let via_reader = r.read_at(0, 11).await.unwrap().coalesce();
449                let via_sealed = sealed.read_at(0, 11).await.unwrap().coalesce();
450                assert_eq!(via_reader.as_ref(), b"hello world");
451                assert_eq!(via_sealed.as_ref(), via_reader.as_ref());
452            }
453        });
454    }
455
456    /// A reader created before sealing reads full pages and the partial page after the seal,
457    /// from both the page cache and the blob.
458    #[test_traced("DEBUG")]
459    fn test_reader_full_pages_after_seal() {
460        let executor = deterministic::Runner::default();
461        executor.start(|context: deterministic::Context| async move {
462            let (blob, blob_size) = context.open("test_partition", b"rdr_pages").await.unwrap();
463
464            // A single-page cache forces most full-page reads to miss and hit the blob.
465            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
466            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
467                .await
468                .unwrap();
469
470            let page_size = PAGE_SIZE.get() as usize;
471            let total = page_size * 3 + 7;
472            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
473            (writer, _) = writer.append(&data).await.unwrap();
474
475            let (writer, reader) = writer.snapshot().await.unwrap();
476            let (sealed, sync) = writer.seal().await.unwrap();
477            sync.await.unwrap();
478            assert_eq!(reader.size(), total as u64);
479
480            // Full range, a page-straddling range, and the partial page, each compared
481            // against the sealed view.
482            let cases = [
483                (0u64, total),
484                (page_size as u64 - 3, 6),
485                ((page_size * 3) as u64, 7),
486            ];
487            for (offset, len) in cases {
488                let via_reader = reader.read_at(offset, len).await.unwrap().coalesce();
489                let via_sealed = sealed.read_at(offset, len).await.unwrap().coalesce();
490                assert_eq!(
491                    via_reader.as_ref(),
492                    &data[offset as usize..offset as usize + len]
493                );
494                assert_eq!(via_sealed.as_ref(), via_reader.as_ref());
495            }
496        });
497    }
498
499    /// Sealing preserves the originating [Writer]'s page-cache id.
500    #[test_traced("DEBUG")]
501    fn test_seal_preserves_cache_id() {
502        let executor = deterministic::Runner::default();
503        executor.start(|context: deterministic::Context| async move {
504            let (blob, blob_size) = context.open("test_partition", b"cache_id").await.unwrap();
505            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
506            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
507                .await
508                .unwrap();
509            let append_id = append.cache_id();
510            let (sealed, sync) = append.seal().await.unwrap();
511            sync.await.unwrap();
512            assert_eq!(sealed.cache_id(), append_id);
513        });
514    }
515
516    /// Sealing an empty blob yields an empty sealed view.
517    #[test_traced("DEBUG")]
518    fn test_seal_empty_blob() {
519        let executor = deterministic::Runner::default();
520        executor.start(|context: deterministic::Context| async move {
521            let (blob, blob_size) = context.open("test_partition", b"empty").await.unwrap();
522            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
523            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
524                .await
525                .unwrap();
526            let (sealed, sync) = append.seal().await.unwrap();
527            sync.await.unwrap();
528
529            assert_eq!(sealed.size(), 0);
530
531            // Out-of-bounds reads error.
532            let mut buf = [0u8; 1];
533            let err = sealed.read_into(&mut buf, 0).await.unwrap_err();
534            assert!(matches!(err, Error::BlobInsufficientLength));
535        });
536    }
537
538    /// Sealing a blob whose size is exactly a page-multiple has no partial page.
539    #[test_traced("DEBUG")]
540    fn test_seal_full_pages_only() {
541        let executor = deterministic::Runner::default();
542        executor.start(|context: deterministic::Context| async move {
543            let (blob, blob_size) = context.open("test_partition", b"full").await.unwrap();
544            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
545            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
546                .await
547                .unwrap();
548
549            // Append exactly two pages.
550            let page_size = PAGE_SIZE.get() as usize;
551            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
552            (append, _) = append.append(&data).await.unwrap();
553            let (sealed, sync) = append.seal().await.unwrap();
554            sync.await.unwrap();
555
556            assert_eq!(sealed.size(), data.len() as u64);
557
558            // Read everything back.
559            let mut buf = vec![0u8; data.len()];
560            sealed.read_into(&mut buf, 0).await.unwrap();
561            assert_eq!(buf, data);
562        });
563    }
564
565    /// Sealing a blob whose size is smaller than one page yields only a partial page.
566    #[test_traced("DEBUG")]
567    fn test_seal_partial_only() {
568        let executor = deterministic::Runner::default();
569        executor.start(|context: deterministic::Context| async move {
570            let (blob, blob_size) = context.open("test_partition", b"partial").await.unwrap();
571            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
572            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
573                .await
574                .unwrap();
575
576            // Append fewer than one page of data.
577            let data: Vec<u8> = (0u8..=50).collect();
578            (append, _) = append.append(&data).await.unwrap();
579            let (sealed, sync) = append.seal().await.unwrap();
580            sync.await.unwrap();
581
582            assert_eq!(sealed.size(), data.len() as u64);
583
584            let mut buf = vec![0u8; data.len()];
585            sealed.read_into(&mut buf, 0).await.unwrap();
586            assert_eq!(buf, data);
587        });
588    }
589
590    /// Reads that straddle the partial-page boundary stitch together cache and partial bytes.
591    #[test_traced("DEBUG")]
592    fn test_seal_full_plus_partial_straddle() {
593        let executor = deterministic::Runner::default();
594        executor.start(|context: deterministic::Context| async move {
595            let (blob, blob_size) = context.open("test_partition", b"straddle").await.unwrap();
596            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
597            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
598                .await
599                .unwrap();
600
601            // One full page + a partial.
602            let page_size = PAGE_SIZE.get() as usize;
603            let total = page_size + 17;
604            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
605            (append, _) = append.append(&data).await.unwrap();
606            let (sealed, sync) = append.seal().await.unwrap();
607            sync.await.unwrap();
608
609            assert_eq!(sealed.size(), total as u64);
610
611            // Straddle read: 5 bytes before the boundary and 10 after.
612            let off = (page_size - 5) as u64;
613            let len = 15usize;
614            let mut buf = vec![0u8; len];
615            sealed.read_into(&mut buf, off).await.unwrap();
616            assert_eq!(buf, data[page_size - 5..page_size - 5 + len]);
617
618            // Read fully within partial.
619            let off = page_size as u64;
620            let mut buf = vec![0u8; 10];
621            sealed.read_into(&mut buf, off).await.unwrap();
622            assert_eq!(buf, data[page_size..page_size + 10]);
623
624            // Read fully within first full page.
625            let mut buf = vec![0u8; 20];
626            sealed.read_into(&mut buf, 0).await.unwrap();
627            assert_eq!(buf, data[..20]);
628        });
629    }
630
631    /// `Sealed::read_at` exposes the same data as `read_into`.
632    #[test_traced("DEBUG")]
633    fn test_sealed_read_at() {
634        let executor = deterministic::Runner::default();
635        executor.start(|context: deterministic::Context| async move {
636            let (blob, blob_size) = context.open("test_partition", b"read_at").await.unwrap();
637            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
638            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
639                .await
640                .unwrap();
641
642            let data: Vec<u8> = (0u8..=255).cycle().take(250).collect();
643            (append, _) = append.append(&data).await.unwrap();
644            let (sealed, sync) = append.seal().await.unwrap();
645            sync.await.unwrap();
646
647            let bufs = sealed.read_at(0, data.len()).await.unwrap();
648            let coalesced = bufs.coalesce();
649            assert_eq!(coalesced.as_ref(), data.as_slice());
650        });
651    }
652
653    /// `Sealed::read_many_into` returns items at sorted, possibly straddling, offsets.
654    #[test_traced("DEBUG")]
655    fn test_sealed_read_many_into() {
656        let executor = deterministic::Runner::default();
657        executor.start(|context: deterministic::Context| async move {
658            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
659            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
660            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
661                .await
662                .unwrap();
663
664            // Two pages worth so reads exercise both cache and partial.
665            let page_size = PAGE_SIZE.get() as usize;
666            let total = page_size + 50;
667            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
668            (append, _) = append.append(&data).await.unwrap();
669            let (sealed, sync) = append.seal().await.unwrap();
670            sync.await.unwrap();
671
672            // 4-byte items at three positions: pure cache, straddling boundary, pure partial.
673            let offsets = [0u64, (page_size - 2) as u64, (page_size + 10) as u64];
674            let item_size = 4usize;
675            let mut out = vec![0u8; offsets.len() * item_size];
676            sealed
677                .read_many_into(&mut out, &offsets, NZUsize!(item_size))
678                .await
679                .unwrap();
680
681            for (i, &off) in offsets.iter().enumerate() {
682                assert_eq!(
683                    &out[i * item_size..(i + 1) * item_size],
684                    &data[off as usize..off as usize + item_size],
685                );
686            }
687        });
688    }
689
690    /// `Sealed::try_read_many_sync_into` serves cached pages and the in-memory tail, and maps
691    /// missed ranges (including straddling prefixes) back to item indices.
692    #[test_traced("DEBUG")]
693    fn test_sealed_try_read_many_sync_into() {
694        let executor = deterministic::Runner::default();
695        executor.start(|context: deterministic::Context| async move {
696            let (blob, blob_size) = context.open("test_partition", b"rmany_sync").await.unwrap();
697
698            // Capacity of one page makes hit/miss behavior deterministic: the cache holds
699            // exactly the last page touched.
700            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
701            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
702                .await
703                .unwrap();
704
705            // Two full pages plus a partial tail page held in memory by the sealed view.
706            let page_size = PAGE_SIZE.get() as usize;
707            let total = page_size * 2 + 50;
708            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
709            (append, _) = append.append(&data).await.unwrap();
710            let (sealed, sync) = append.seal().await.unwrap();
711            sync.await.unwrap();
712
713            // Items: page 0, straddling pages 0 and 1, page 1, straddling page 1 and the tail,
714            // and pure tail.
715            let offsets = [
716                0u64,
717                (page_size - 2) as u64,
718                (page_size + 2) as u64,
719                (page_size * 2 - 2) as u64,
720                (page_size * 2 + 10) as u64,
721            ];
722            let item_size = 4usize;
723            let check = |out: &[u8], indices: &[usize]| {
724                for &i in indices {
725                    let off = offsets[i] as usize;
726                    assert_eq!(
727                        &out[i * item_size..(i + 1) * item_size],
728                        &data[off..off + item_size],
729                    );
730                }
731            };
732
733            // With only page 0 cached, items touching page 1 are misses. The tail item is
734            // served from the sealed view's in-memory bytes.
735            sealed.read_at(0, page_size).await.unwrap();
736            let mut out = vec![0u8; offsets.len() * item_size];
737            let misses = sealed.try_read_many_sync_into(&mut out, &offsets, NZUsize!(item_size));
738            assert_eq!(misses, vec![1, 2, 3]);
739            check(&out, &[0, 4]);
740
741            // With only page 1 cached, the first two items need page 0.
742            sealed.read_at(page_size as u64, page_size).await.unwrap();
743            let mut out = vec![0u8; offsets.len() * item_size];
744            let misses = sealed.try_read_many_sync_into(&mut out, &offsets, NZUsize!(item_size));
745            assert_eq!(misses, vec![0, 1]);
746            check(&out, &[2, 3, 4]);
747        });
748    }
749
750    /// `Sealed::try_read_ranges_sync_into` serves cached pages and the in-memory tail for
751    /// variable-length ranges, and maps missed ranges back to range indices, including when a
752    /// zero-length range shares its offset with the missed range that follows it.
753    #[test_traced("DEBUG")]
754    fn test_sealed_try_read_ranges_sync_into() {
755        let executor = deterministic::Runner::default();
756        executor.start(|context: deterministic::Context| async move {
757            let (blob, blob_size) = context
758                .open("test_partition", b"rranges_sync")
759                .await
760                .unwrap();
761
762            // Capacity of one page makes hit/miss behavior deterministic: the cache holds
763            // exactly the last page touched.
764            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
765            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
766                .await
767                .unwrap();
768
769            // Two full pages plus a partial tail page held in memory by the sealed view.
770            let page_size = PAGE_SIZE.get() as usize;
771            let total = page_size * 2 + 50;
772            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
773            (append, _) = append.append(&data).await.unwrap();
774            let (sealed, sync) = append.seal().await.unwrap();
775            sync.await.unwrap();
776
777            // Ranges: page 0, a zero-length range sharing its offset with the page 1 range
778            // that follows it, page 1, straddling page 1 and the tail, pure tail.
779            let ranges = [
780                (0u64, 3usize),
781                (page_size as u64 + 5, 0),
782                (page_size as u64 + 5, 7),
783                ((page_size * 2 - 2) as u64, 4),
784                ((page_size * 2 + 10) as u64, 4),
785            ];
786            let total_len: usize = ranges.iter().map(|&(_, len)| len).sum();
787            let check = |out: &[u8], indices: &[usize]| {
788                let mut start = 0;
789                for (i, &(off, len)) in ranges.iter().enumerate() {
790                    if indices.contains(&i) {
791                        let off = off as usize;
792                        assert_eq!(&out[start..start + len], &data[off..off + len]);
793                    }
794                    start += len;
795                }
796            };
797
798            // With only page 0 cached, the page 1 range and the straddler's prefix miss. The
799            // zero-length range never misses. The tail range is served in memory.
800            sealed.read_at(0, page_size).await.unwrap();
801            let mut out = vec![0u8; total_len];
802            let misses = sealed.try_read_ranges_sync_into(&mut out, &ranges);
803            assert_eq!(misses, vec![2, 3]);
804            check(&out, &[0, 4]);
805
806            // With only page 1 cached, range 0 becomes the miss and the rest are served.
807            sealed.read_at(page_size as u64, page_size).await.unwrap();
808            let mut out = vec![0u8; total_len];
809            let misses = sealed.try_read_ranges_sync_into(&mut out, &ranges);
810            assert_eq!(misses, vec![0]);
811            check(&out, &[1, 2, 3, 4]);
812        });
813    }
814
815    /// `Sealed::read_many_into` falls back to blob reads for full-page cache misses.
816    #[test_traced("DEBUG")]
817    fn test_sealed_read_many_into_cache_miss() {
818        let executor = deterministic::Runner::default();
819        executor.start(|context: deterministic::Context| async move {
820            let (blob, blob_size) = context.open("test_partition", b"rmany_miss").await.unwrap();
821            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
822            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
823                .await
824                .unwrap();
825
826            let page_size = PAGE_SIZE.get() as usize;
827            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
828            (append, _) = append.append(&data).await.unwrap();
829            let (sealed, sync) = append.seal().await.unwrap();
830            sync.await.unwrap();
831
832            let offsets = [0u64, page_size as u64];
833            let item_size = 4usize;
834            let mut out = vec![0u8; offsets.len() * item_size];
835            sealed
836                .read_many_into(&mut out, &offsets, NZUsize!(item_size))
837                .await
838                .unwrap();
839
840            for (i, &off) in offsets.iter().enumerate() {
841                assert_eq!(
842                    &out[i * item_size..(i + 1) * item_size],
843                    &data[off as usize..off as usize + item_size],
844                );
845            }
846        });
847    }
848
849    /// `Sealed::read_ranges_into` counts only wholly cached ranges as served without a blob read.
850    #[test_traced("DEBUG")]
851    fn test_sealed_read_ranges_into_reports_misses() {
852        let executor = deterministic::Runner::default();
853        executor.start(|context: deterministic::Context| async move {
854            let (blob, blob_size) = context.open("test_partition", b"ranges").await.unwrap();
855            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(1));
856            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
857                .await
858                .unwrap();
859
860            let page_size = PAGE_SIZE.get() as u64;
861            let data: Vec<u8> = (0u8..=255).cycle().take(page_size as usize * 3).collect();
862            (append, _) = append.append(&data).await.unwrap();
863            let (sealed, sync) = append.seal().await.unwrap();
864            sync.await.unwrap();
865
866            // Leave only page 1 cached.
867            sealed.read_at(page_size, 1).await.unwrap();
868
869            // Page 0 misses, page 1 hits, the range straddling pages 1 and 2 misses, and page 2
870            // misses, so only one range is served without a blob read.
871            let ranges = [
872                (1, 3),
873                (page_size + 1, 5),
874                (2 * page_size - 1, 4),
875                (2 * page_size + 4, 7),
876            ];
877            let mut out = vec![0u8; ranges.iter().map(|&(_, len)| len).sum()];
878            let served = sealed.read_ranges_into(&mut out, &ranges).await.unwrap();
879            assert_eq!(served, 1);
880
881            // Hits and misses alike read back the written bytes.
882            let mut slot = 0;
883            for (offset, len) in ranges {
884                let offset = offset as usize;
885                assert_eq!(&out[slot..slot + len], &data[offset..offset + len]);
886                slot += len;
887            }
888        });
889    }
890
891    #[test_traced("DEBUG")]
892    #[should_panic(expected = "ranges must be sorted and non-overlapping")]
893    fn test_sealed_read_many_into_rejects_unsorted_offsets() {
894        let executor = deterministic::Runner::default();
895        executor.start(|context: deterministic::Context| async move {
896            let (blob, blob_size) = context.open("test_partition", b"rmany_bad").await.unwrap();
897            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
898            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
899                .await
900                .unwrap();
901            (append, _) = append.append(&[7; 32]).await.unwrap();
902            let (sealed, sync) = append.seal().await.unwrap();
903            sync.await.unwrap();
904
905            let mut out = vec![0u8; 8];
906            let _ = sealed.read_many_into(&mut out, &[8, 4], NZUsize!(4)).await;
907        });
908    }
909
910    /// `Sealed::read_many_into` validates all caller-provided offsets before reading.
911    #[test_traced("DEBUG")]
912    fn test_sealed_read_many_into_rejects_invalid_offsets() {
913        let executor = deterministic::Runner::default();
914        executor.start(|context: deterministic::Context| async move {
915            let (blob, blob_size) = context.open("test_partition", b"rmany_bad").await.unwrap();
916            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
917            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
918                .await
919                .unwrap();
920            (append, _) = append.append(&[7; 32]).await.unwrap();
921            let (sealed, sync) = append.seal().await.unwrap();
922            sync.await.unwrap();
923
924            let mut out = vec![0u8; 8];
925            let err = sealed
926                .read_many_into(&mut out, &[u64::MAX - 1, 8], NZUsize!(4))
927                .await
928                .unwrap_err();
929            assert!(matches!(err, Error::OffsetOverflow));
930
931            let err = sealed
932                .read_many_into(&mut out, &[28, 32], NZUsize!(4))
933                .await
934                .unwrap_err();
935            assert!(matches!(err, Error::BlobInsufficientLength));
936        });
937    }
938
939    /// `try_read_sync_into` succeeds when bytes come purely from the in-memory partial page.
940    #[test_traced("DEBUG")]
941    fn test_sealed_try_read_sync_partial() {
942        let executor = deterministic::Runner::default();
943        executor.start(|context: deterministic::Context| async move {
944            let (blob, blob_size) = context
945                .open("test_partition", b"trs_partial")
946                .await
947                .unwrap();
948            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
949            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
950                .await
951                .unwrap();
952
953            let page_size = PAGE_SIZE.get() as usize;
954            let total = page_size + 30;
955            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
956            (append, _) = append.append(&data).await.unwrap();
957            let (sealed, sync) = append.seal().await.unwrap();
958            sync.await.unwrap();
959
960            // Read fully within partial.
961            let mut buf = vec![0u8; 10];
962            assert!(sealed.try_read_sync_into(&mut buf, page_size as u64));
963            assert_eq!(buf, data[page_size..page_size + 10]);
964
965            // Out of bounds returns false.
966            let mut buf = vec![0u8; 10];
967            assert!(!sealed.try_read_sync_into(&mut buf, total as u64));
968        });
969    }
970
971    /// `try_read_sync_into` can stitch a cached full-page prefix to in-memory partial bytes.
972    #[test_traced("DEBUG")]
973    fn test_sealed_try_read_sync_straddles_cached_and_partial() {
974        let executor = deterministic::Runner::default();
975        executor.start(|context: deterministic::Context| async move {
976            let (blob, blob_size) = context
977                .open("test_partition", b"trs_straddle")
978                .await
979                .unwrap();
980            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
981            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
982                .await
983                .unwrap();
984
985            let page_size = PAGE_SIZE.get() as usize;
986            let total = page_size + 30;
987            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
988            (append, _) = append.append(&data).await.unwrap();
989            let (sealed, sync) = append.seal().await.unwrap();
990            sync.await.unwrap();
991
992            let mut buf = vec![0u8; 12];
993            assert!(sealed.try_read_sync_into(&mut buf, (page_size - 4) as u64));
994            assert_eq!(buf, data[page_size - 4..page_size + 8]);
995        });
996    }
997
998    /// Synchronous reads past the sealed size are rejected.
999    #[test_traced("DEBUG")]
1000    fn test_sealed_try_read_sync_out_of_bounds() {
1001        let executor = deterministic::Runner::default();
1002        executor.start(|context: deterministic::Context| async move {
1003            let (blob, blob_size) = context.open("test_partition", b"trs_fail").await.unwrap();
1004            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1005            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1006                .await
1007                .unwrap();
1008
1009            let page_size = PAGE_SIZE.get() as usize;
1010            let data: Vec<u8> = (0u8..=255).cycle().take(page_size + 5).collect();
1011            (append, _) = append.append(&data).await.unwrap();
1012            let (sealed, sync) = append.seal().await.unwrap();
1013            sync.await.unwrap();
1014
1015            let mut buf = vec![9u8; 10];
1016            assert!(!sealed.try_read_sync_into(&mut buf, data.len() as u64));
1017        });
1018    }
1019
1020    /// `Sealed::replay` streams all logical bytes including the partial page.
1021    #[test_traced("DEBUG")]
1022    fn test_sealed_replay() {
1023        let executor = deterministic::Runner::default();
1024        executor.start(|context: deterministic::Context| async move {
1025            let (blob, blob_size) = context.open("test_partition", b"replay").await.unwrap();
1026            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1027            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1028                .await
1029                .unwrap();
1030
1031            // Two pages + a partial, synced so the bytes are on disk before sealing.
1032            let page_size = PAGE_SIZE.get() as usize;
1033            let total = page_size * 2 + 25;
1034            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1035            (append, _) = append.append(&data).await.unwrap();
1036            append = append.sync().await.unwrap();
1037            let (sealed, sync) = append.seal().await.unwrap();
1038            sync.await.unwrap();
1039
1040            let mut replay = sealed
1041                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1042                .unwrap();
1043            assert_eq!(replay.blob_size(), total as u64);
1044
1045            // Drain all logical bytes.
1046            let mut out = Vec::with_capacity(total);
1047            while replay.ensure(1).await.unwrap() {
1048                let chunk = replay.chunk();
1049                let copy_len = chunk.len();
1050                out.extend_from_slice(chunk);
1051                replay.advance(copy_len);
1052            }
1053            assert_eq!(out, data);
1054        });
1055    }
1056
1057    /// Replaying a snapshot must stop at the snapshot's logical boundary, even if the live writer
1058    /// later extends the same physical page.
1059    #[test_traced("DEBUG")]
1060    fn test_snapshot_replay_stays_frozen_after_writer_growth() {
1061        let executor = deterministic::Runner::default();
1062        executor.start(|context: deterministic::Context| async move {
1063            let (blob, blob_size) = context
1064                .open("test_partition", b"snapshot_replay_growth")
1065                .await
1066                .unwrap();
1067            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1068            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1069                .await
1070                .unwrap();
1071
1072            let page_size = PAGE_SIZE.get() as usize;
1073            let mut original = vec![0xAA; page_size];
1074            original.extend_from_slice(b"old");
1075            (writer, _) = writer.append(&original).await.unwrap();
1076            writer = writer.sync().await.unwrap();
1077
1078            let (mut writer, snapshot) = writer.snapshot().await.unwrap();
1079            let snapshot_bytes = snapshot
1080                .read_at(0, snapshot.size() as usize)
1081                .await
1082                .unwrap()
1083                .coalesce();
1084            let mut replay = snapshot
1085                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1086                .unwrap();
1087            assert_eq!(replay.blob_size(), original.len() as u64);
1088
1089            (writer, _) = writer.append(b"newtail").await.unwrap();
1090            writer.sync().await.unwrap();
1091
1092            let mut out = Vec::new();
1093            while replay.ensure(1).await.unwrap() {
1094                let chunk = replay.chunk();
1095                let copy_len = chunk.len();
1096                out.extend_from_slice(chunk);
1097                replay.advance(copy_len);
1098            }
1099
1100            assert_eq!(out.as_slice(), snapshot_bytes.as_ref());
1101            assert_eq!(out, original);
1102        });
1103    }
1104
1105    /// Both replay constructors preserve the captured tail during a partial-page rewrite.
1106    /// The synced snapshot is a control whose fallback already covers the captured tail.
1107    /// The fallback length assertion verifies the expected disk state was actually reached.
1108    #[rstest::rstest]
1109    #[case::partial(0, 13, false)]
1110    #[case::page_boundary(1, PAGE_SIZE.get() as usize, false)]
1111    #[case::next_page(1, PAGE_SIZE.get() as usize + 7, false)]
1112    #[case::synced_snapshot(1, 13, true)]
1113    fn test_replay_preserves_tail_during_partial_page_rewrite(
1114        #[case] full_pages: usize,
1115        #[case] next_len: usize,
1116        #[case] sync_snapshot: bool,
1117        #[values(false, true)] writer_replay: bool,
1118    ) {
1119        deterministic::Runner::default().start(|context| async move {
1120            const DURABLE_TAIL: usize = 3;
1121            const SNAPSHOT_TAIL: usize = 7;
1122
1123            // Persist a short partial page, then extend it without advancing its durable slot.
1124            // The snapshot must retain the longer tail even if disk validation falls back.
1125            let page_size = PAGE_SIZE.get() as usize;
1126            let physical_page_size = page_size + CHECKSUM_SIZE as usize;
1127            let offset = Widen::widen(full_pages * physical_page_size);
1128            let (inner, size) = context.open("snapshot-replay-race", b"blob").await.unwrap();
1129            let blob = Arc::new(SplitWriteBlob {
1130                inner,
1131                pause: Mutex::new(None),
1132            });
1133            let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1134            let mut writer = Writer::new(blob.clone(), size, BUFFER_SIZE, cache)
1135                .await
1136                .unwrap();
1137            let mut expected = vec![0xAA; full_pages * page_size + DURABLE_TAIL];
1138            (writer, _) = writer.append(&expected).await.unwrap();
1139            writer = writer.sync().await.unwrap();
1140            let extension = vec![0xBB; SNAPSHOT_TAIL - DURABLE_TAIL];
1141            (writer, _) = writer.append(&extension).await.unwrap();
1142            expected.extend_from_slice(&extension);
1143            let (writer, snapshot) = writer.snapshot().await.unwrap();
1144
1145            // Write the extension to disk without syncing it.
1146            let (mut writer, _) = writer
1147                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1148                .await
1149                .unwrap();
1150
1151            // The synced control has a durable fallback that already includes the snapshot.
1152            if sync_snapshot {
1153                writer = writer.sync().await.unwrap();
1154            }
1155
1156            // Locate the checksum slot the next flush will rewrite so the pause exposes the
1157            // new length before its matching checksum, leaving only the other slot valid.
1158            let page = blob
1159                .read_at(offset, physical_page_size, ReadOptions::default())
1160                .await
1161                .unwrap()
1162                .coalesce();
1163            let active = Checksum::validate_page(page.as_ref()).unwrap();
1164            assert_eq!(active.len as usize, SNAPSHOT_TAIL);
1165            let rewritten_slot = if sync_snapshot {
1166                active.slot.other()
1167            } else {
1168                active.slot
1169            };
1170
1171            // Capture each replay before the rewrite, including its frozen partial tail.
1172            let replay_len = expected.len();
1173            let mut replay = if writer_replay {
1174                let replay;
1175                (writer, replay) = writer
1176                    .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1177                    .await
1178                    .unwrap();
1179                replay
1180            } else {
1181                snapshot
1182                    .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1183                    .unwrap()
1184            };
1185            assert_eq!(replay.blob_size(), Widen::widen(replay_len));
1186
1187            // The writer supplies every byte. A short backend write exposes the new slot length
1188            // before its CRC, while the other slot still validates the durable prefix.
1189            (writer, _) = writer
1190                .append(&vec![0xCC; next_len - SNAPSHOT_TAIL])
1191                .await
1192                .unwrap();
1193            let (started, entered) = oneshot::channel();
1194            let (resume, released) = oneshot::channel();
1195            *blob.pause.lock() = Some(WritePause {
1196                offset,
1197                prefix_len: page_size + rewritten_slot.offset() + CHECKSUM_SLOT_LEN_SIZE,
1198                started,
1199                resume: released,
1200            });
1201            let mut flushing =
1202                Box::pin(writer.replay(NZUsize!(BUFFER_SIZE), ReadOptions::default()));
1203            commonware_macros::select! {
1204                _ = entered => {},
1205                _ = flushing.as_mut() => panic!("write completed before its suffix was released"),
1206            }
1207
1208            // Inspect the paused disk image and both read paths before the write can finish.
1209            let page = blob
1210                .read_at(offset, physical_page_size, ReadOptions::default())
1211                .await
1212                .unwrap()
1213                .coalesce();
1214            let fallback_len = Checksum::validate_page(page.as_ref()).unwrap().len as usize;
1215            let read_at = snapshot
1216                .read_at(0, expected.len())
1217                .await
1218                .unwrap()
1219                .coalesce();
1220            let replayed = async {
1221                let mut out = Vec::new();
1222                while replay.ensure(1).await? {
1223                    let chunk = replay.chunk();
1224                    let len = chunk.len();
1225                    out.extend_from_slice(chunk);
1226                    replay.advance(len);
1227                }
1228                Ok::<_, Error>(out)
1229            }
1230            .await;
1231
1232            // Complete the successful mutation before checking the independent snapshot read.
1233            resume.send(()).unwrap();
1234            (writer, _) = flushing.await.unwrap();
1235            writer.sync().await.unwrap();
1236            assert_eq!(
1237                fallback_len,
1238                if sync_snapshot {
1239                    SNAPSHOT_TAIL
1240                } else {
1241                    DURABLE_TAIL
1242                }
1243            );
1244            assert_eq!(read_at.as_ref(), expected);
1245            let replayed = replayed.unwrap();
1246            assert_eq!(
1247                replayed.len(),
1248                replay_len,
1249                "replay shortened the immutable snapshot"
1250            );
1251            assert_eq!(replayed, expected[..replay_len]);
1252        });
1253    }
1254
1255    /// `Sealed::replay` works without a prior `Append::sync` because `Append::seal` writes bytes
1256    /// to the blob before starting its sync.
1257    #[test_traced("DEBUG")]
1258    fn test_seal_replay_without_sync() {
1259        let executor = deterministic::Runner::default();
1260        executor.start(|_: deterministic::Context| async move {
1261            let blob = SyncTrackingBlob::new();
1262            let blob = Arc::new(blob);
1263            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1264            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
1265                .await
1266                .unwrap();
1267
1268            let page_size = PAGE_SIZE.get() as usize;
1269            let total = page_size * 2 + 25;
1270            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1271            (append, _) = append.append(&data).await.unwrap();
1272            let (sealed, sync) = append.seal().await.unwrap();
1273
1274            let (_durable, _writes, full_syncs, range_syncs) = blob.snapshot();
1275            assert_eq!(full_syncs, 1);
1276            assert_eq!(range_syncs, 0);
1277            sync.await.unwrap();
1278
1279            let mut replay = sealed
1280                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1281                .unwrap();
1282            assert_eq!(replay.blob_size(), total as u64);
1283
1284            let mut out = Vec::with_capacity(total);
1285            while replay.ensure(1).await.unwrap() {
1286                let chunk = replay.chunk();
1287                let copy_len = chunk.len();
1288                out.extend_from_slice(chunk);
1289                replay.advance(copy_len);
1290            }
1291            assert_eq!(out, data);
1292        });
1293    }
1294
1295    /// Reads and replay through `Sealed` observe flushed bytes while the seal's sync handle is
1296    /// still pending; they never wait for durability.
1297    #[test_traced("DEBUG")]
1298    fn test_sealed_reads_while_seal_sync_pending() {
1299        let executor = deterministic::Runner::default();
1300        executor.start(|_: deterministic::Context| async move {
1301            let inner = SyncTrackingBlob::new();
1302            let (blob, pending) = DelayedSyncBlob::new(inner);
1303            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1304            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
1305
1306            let page_size = PAGE_SIZE.get() as usize;
1307            let total = page_size * 2 + 50;
1308            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
1309            (append, _) = append.append(&data).await.unwrap();
1310
1311            // Seal parks its sync; leave it parked while reading.
1312            let (sealed, sync) = append.seal().await.unwrap();
1313            assert_eq!(pending.lock().len(), 1, "the seal sync should be parked");
1314
1315            let read = sealed.read_at(0, total).await.unwrap().coalesce();
1316            assert_eq!(read.as_ref(), &data[..]);
1317
1318            let mut replay = sealed
1319                .replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
1320                .unwrap();
1321            assert_eq!(replay.blob_size(), total as u64);
1322            let mut replayed = Vec::new();
1323            while replay.ensure(1).await.unwrap() {
1324                let n = {
1325                    let chunk = replay.chunk();
1326                    replayed.extend_from_slice(chunk);
1327                    chunk.len()
1328                };
1329                replay.advance(n);
1330            }
1331            assert_eq!(replayed, data);
1332            assert_eq!(
1333                pending.lock().len(),
1334                1,
1335                "reads must not consume the pending sync"
1336            );
1337
1338            // Release the sync and confirm the handle completes.
1339            next_pending_sync(&pending).release.send(Ok(())).unwrap();
1340            sync.await.unwrap();
1341        });
1342    }
1343
1344    /// Sealing a recovered, already-synced partial page must not rewrite it.
1345    #[test_traced("DEBUG")]
1346    fn test_seal_recovered_synced_partial_page_no_write() {
1347        let executor = deterministic::Runner::default();
1348        executor.start(|_: deterministic::Context| async move {
1349            let blob = SyncTrackingBlob::new();
1350            let blob = Arc::new(blob);
1351            let cache_ref = super::CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1352            let data: Vec<u8> = (0u8..=255)
1353                .cycle()
1354                .take(PAGE_SIZE.get() as usize - 17)
1355                .collect();
1356
1357            {
1358                let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref.clone())
1359                    .await
1360                    .unwrap();
1361                (writer, _) = writer.append(&data).await.unwrap();
1362                writer.sync().await.unwrap();
1363            }
1364
1365            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
1366            let mut recovered = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
1367                .await
1368                .unwrap();
1369            assert_eq!(recovered.size(), data.len() as u64);
1370
1371            recovered = recovered.sync().await.unwrap();
1372            let (_, writes_after_sync, full_after_sync, range_after_sync) = blob.snapshot();
1373            assert_eq!(
1374                writes_after_sync, writes,
1375                "syncing an unchanged recovered partial page must not rewrite it"
1376            );
1377            assert_eq!(full_after_sync, full_syncs + 1);
1378            assert_eq!(range_after_sync, range_syncs);
1379
1380            let (sealed, sync) = recovered.seal().await.unwrap();
1381            sync.await.unwrap();
1382            let (_, writes_after_seal, full_after_seal, range_after_seal) = blob.snapshot();
1383            assert_eq!(
1384                writes_after_seal, writes_after_sync,
1385                "sealing an unchanged recovered partial page must not rewrite it"
1386            );
1387            assert_eq!(full_after_seal, full_after_sync);
1388            assert_eq!(range_after_seal, range_after_sync);
1389
1390            let read = sealed.read_at(0, data.len()).await.unwrap().coalesce();
1391            assert_eq!(read.as_ref(), data.as_slice());
1392        });
1393    }
1394}