Skip to main content

commonware_runtime/utils/buffer/paged/
writer.rs

1//! Single-owner, append-only access to a [Blob].
2//!
3//! A [Writer] exclusively owns its blob and cannot be cloned. Appended bytes can be read back
4//! immediately but are not durable until [Writer::sync].
5//!
6//! # Snapshot
7//!
8//! [Writer::snapshot] captures a logical read view without consuming the writer.
9//!
10//! # Seal
11//!
12//! [Writer::seal] consumes the writer and turns it into an immutable [super::Sealed] view.
13//!
14//! # Paging
15//!
16//! Callers append and read logical bytes; the blob stores physical pages in the format described
17//! in [`super`]. Appends accumulate in a write buffer and reach the blob in pages. Buffered bytes
18//! are readable immediately but durable only after `sync`. Full pages read from the blob are
19//! cached in a shared page cache, so reads are served from the write buffer, the page cache, or
20//! the blob itself. Large appends bypass the write buffer and write whole pages directly to the
21//! blob.
22//!
23//! # Checksums
24//!
25//! Each physical page ends in a two-slot CRC record. The slots let a partial page be rewritten
26//! without clobbering its previously committed contents, so an interrupted write loses at most
27//! the bytes it was writing. [Writer::new] backs up over any trailing bytes not covered by a
28//! valid checksum, treating them as an incomplete write.
29//!
30//! # Raw [Blob] handles
31//!
32//! The [Writer] owns the page layout, page cache entries, and durability bookkeeping of its
33//! [Blob]. Raw handles cloned before the writer existed see physical bytes, including CRC
34//! records, and do not observe buffered bytes until they are flushed. They must not mutate the
35//! blob while a [Writer] exists: such writes bypass the write buffer and page cache and can
36//! invalidate checksum recovery.
37
38use super::{
39    read::{PageReader, Replay},
40    view::View,
41};
42use crate::{
43    buffer::{
44        paged::{CacheRef, Checksum, Slot, CHECKSUM_SIZE, CHECKSUM_SLOT_SIZE},
45        tip::Buffer,
46        SyncState,
47    },
48    Blob, Error, Handle, IoBuf, IoBufMut, IoBufs,
49};
50use bytes::BufMut;
51use commonware_cryptography::Crc32;
52use std::num::{NonZeroU16, NonZeroUsize};
53use tracing::warn;
54
55/// Adjusts a requested write-buffer `capacity` upward to the value the buffer actually uses,
56/// applying two upward adjustments:
57///
58/// - Rounds up to a whole multiple of `page_size`, so the buffer always holds an exact number of
59///   pages. Callers can then drain and bulk-cache full pages without re-rounding the capacity.
60/// - Raises the result to a floor of two pages, so the buffer can hold at least one full page of
61///   new data even while caching a nearly-full page of already written data.
62fn adjusted_capacity(capacity: usize, page_size: u64) -> usize {
63    let page_size = page_size as usize;
64    let rounded = capacity.next_multiple_of(page_size);
65    let floor = page_size * 2;
66    if rounded < floor {
67        warn!(
68            floor,
69            "requested buffer capacity is too low, increasing it to floor"
70        );
71    }
72    rounded.max(floor)
73}
74
75/// Returns whether appending `append_len` bytes should bypass the write buffer and write whole
76/// pages directly: the append would overflow capacity, and at least one whole page remains to
77/// write after filling the current page up to a boundary.
78///
79/// Larger appends bypass the buffer, so a buffered append exceeds `capacity` by less than one
80/// page (given `capacity` is a whole number of pages; see [adjusted_capacity]). The write
81/// buffer's peak size therefore stays under `capacity + logical_page_size`.
82const fn too_big_for_buffer(
83    buffer_len: usize,
84    buffer_capacity: usize,
85    append_len: usize,
86    logical_page_size: usize,
87) -> bool {
88    let fill = buffer_len.next_multiple_of(logical_page_size) - buffer_len;
89    let overflows_capacity = buffer_len + append_len > buffer_capacity;
90    let has_full_page_after_fill = append_len >= fill + logical_page_size;
91
92    overflows_capacity && has_full_page_after_fill
93}
94
95/// Unique writer to a cache-wrapped [Blob].
96pub struct Writer<B: Blob> {
97    /// The underlying blob being wrapped.
98    blob: B,
99
100    /// The page where the next appended byte will be written to.
101    current_page: u64,
102
103    /// The state of the partial page in the blob. If it was written due to a sync call, then this
104    /// will contain its CRC record.
105    partial_page_state: Option<Checksum>,
106
107    /// Durability state for plain writes, resizes, and range-sync writes.
108    sync_state: SyncState,
109
110    /// Unique id assigned to this blob by the page cache.
111    id: u64,
112
113    /// A reference to the page cache that manages read caching for this blob.
114    cache_ref: CacheRef,
115
116    /// The write buffer containing any logical bytes following the last full page boundary in the
117    /// underlying blob.
118    buffer: Buffer,
119}
120
121impl<B: Blob> Writer<B> {
122    /// Write bytes to the underlying blob and mark them as needing sync.
123    async fn write_at(&mut self, offset: u64, bufs: impl Into<IoBufs> + Send) -> Result<(), Error> {
124        self.sync_state.write_at(&self.blob, offset, bufs).await
125    }
126
127    /// Write bytes to the underlying blob and make them durable.
128    ///
129    /// Uses [`Blob::write_at_sync`] when there are no earlier unsynced
130    /// mutations. Otherwise, writes the bytes and then syncs the blob.
131    async fn write_at_sync(
132        &mut self,
133        offset: u64,
134        bufs: impl Into<IoBufs> + Send,
135    ) -> Result<(), Error> {
136        self.sync_state
137            .write_at_sync(&self.blob, offset, bufs)
138            .await
139    }
140
141    /// Write bytes to the underlying blob, optionally making them durable.
142    async fn write_at_maybe_sync(
143        &mut self,
144        offset: u64,
145        bufs: impl Into<IoBufs> + Send,
146        sync: bool,
147    ) -> Result<(), Error> {
148        if sync {
149            self.write_at_sync(offset, bufs).await
150        } else {
151            self.write_at(offset, bufs).await
152        }
153    }
154
155    /// Wrap `blob` in a [Writer]. `blob` must already hold `original_blob_size` physical bytes;
156    /// reads are cached through `cache_ref` and appends stage in a write buffer of capacity
157    /// `capacity`. Rewinds the blob if necessary so it only contains checksum-validated data.
158    pub async fn new(
159        blob: B,
160        original_blob_size: u64,
161        capacity: usize,
162        cache_ref: CacheRef,
163    ) -> Result<Self, Error> {
164        let (partial_page_state, pages, invalid_data_found) =
165            Self::read_last_valid_page(&blob, original_blob_size, cache_ref.page_size()).await?;
166        if invalid_data_found {
167            // Invalid data was detected, trim it from the blob.
168            let new_blob_size = pages * (cache_ref.page_size() + CHECKSUM_SIZE);
169            warn!(
170                original_blob_size,
171                new_blob_size, "truncating blob to remove invalid data"
172            );
173            blob.resize(new_blob_size).await?;
174            blob.sync().await?;
175        }
176
177        let capacity = adjusted_capacity(capacity, cache_ref.page_size());
178        let needs_sync = !invalid_data_found; // ensure pending writes on the wrapped blob are synced
179
180        let (current_page, partial_page_state, partial_data) = match partial_page_state {
181            Some((partial_page, crc_record)) => (pages - 1, Some(crc_record), Some(partial_page)),
182            None => (pages, None, None),
183        };
184
185        let buffer = Buffer::from(
186            current_page * cache_ref.page_size(),
187            partial_data.unwrap_or_default(),
188            capacity,
189            cache_ref.pool().clone(),
190        );
191
192        Ok(Self {
193            blob,
194            current_page,
195            partial_page_state,
196            sync_state: if needs_sync {
197                SyncState::Dirty
198            } else {
199                SyncState::Clean
200            },
201            id: cache_ref.next_id(),
202            cache_ref,
203            buffer,
204        })
205    }
206
207    /// Scans backwards from the end of the blob, stopping when it finds a valid page.
208    ///
209    /// # Returns
210    ///
211    /// A tuple of `(partial_page, page_count, invalid_data_found)`:
212    ///
213    /// - `partial_page`: If the last valid page is partial (contains fewer than `page_size` logical
214    ///   bytes), returns `Some((data, crc_record))` containing the logical data and its CRC record.
215    ///   Returns `None` if the last valid page is full or if no valid pages exist.
216    ///
217    /// - `page_count`: The number of pages in the blob up to and including the last valid page
218    ///   found (whether or not it's partial). Note that it's possible earlier pages may be invalid
219    ///   since this function stops scanning when it finds one valid page.
220    ///
221    /// - `invalid_data_found`: `true` if there are any bytes in the blob that follow the last valid
222    ///   page. Typically the blob should be resized to eliminate them since their integrity cannot
223    ///   be guaranteed.
224    async fn read_last_valid_page(
225        blob: &B,
226        blob_size: u64,
227        page_size: u64,
228    ) -> Result<(Option<(IoBuf, Checksum)>, u64, bool), Error> {
229        let physical_page_size = page_size + CHECKSUM_SIZE;
230        let partial_bytes = blob_size % physical_page_size;
231        let mut last_page_end = blob_size - partial_bytes;
232
233        // If the last physical page in the blob is truncated, it can't have a valid CRC record and
234        // must be invalid.
235        let mut invalid_data_found = partial_bytes != 0;
236
237        while last_page_end != 0 {
238            // Read the last page and parse its CRC record.
239            let page_start = last_page_end - physical_page_size;
240            let buf = blob
241                .read_at(page_start, physical_page_size as usize)
242                .await?
243                .coalesce()
244                .freeze();
245
246            match Checksum::validate_page(buf.as_ref()) {
247                Some(crc_record) => {
248                    // Found a valid page.
249                    let (len, _) = crc_record.get_crc();
250                    let len = len as u64;
251                    if len != page_size {
252                        // The page is partial (logical data doesn't fill the page).
253                        let logical_bytes = buf.slice(..len as usize);
254                        return Ok((
255                            Some((logical_bytes, crc_record)),
256                            last_page_end / physical_page_size,
257                            invalid_data_found,
258                        ));
259                    }
260                    // The page is full.
261                    return Ok((None, last_page_end / physical_page_size, invalid_data_found));
262                }
263                None => {
264                    // The page is invalid.
265                    last_page_end = page_start;
266                    invalid_data_found = true;
267                }
268            }
269        }
270
271        // No valid page exists in the blob.
272        Ok((None, 0, invalid_data_found))
273    }
274
275    /// Append all bytes in `buf` to the tip of the blob, returning the logical offset at which
276    /// the first byte was written.
277    pub async fn append(&mut self, buf: &[u8]) -> Result<u64, Error> {
278        let logical_page_size = self.cache_ref.page_size() as usize;
279
280        // Bypass the write buffer and write whole pages directly when `buf` is large.
281        if too_big_for_buffer(
282            self.buffer.len(),
283            self.buffer.capacity,
284            buf.len(),
285            logical_page_size,
286        ) {
287            return self.append_owned(IoBuf::copy_from_slice(buf)).await;
288        }
289
290        let offset = self.buffer.size();
291        if self.buffer.append(buf) {
292            self.flush_internal(false, false).await?;
293        }
294        Ok(offset)
295    }
296
297    /// Append owned bytes to the tip of the blob.
298    ///
299    /// Large appends fill the current tip to a page boundary, write complete pages directly to the
300    /// blob, and leave only a sub-page suffix in the write buffer. This avoids copying full-page
301    /// payloads while preserving the invariant that the buffer starts at `current_page`.
302    pub async fn append_owned(&mut self, buf: IoBuf) -> Result<u64, Error> {
303        let logical_page_size = self.cache_ref.page_size() as usize;
304        let offset = self.buffer.size();
305
306        // Buffer the append unless `buf` is too big for the buffer.
307        if !too_big_for_buffer(
308            self.buffer.len(),
309            self.buffer.capacity,
310            buf.len(),
311            logical_page_size,
312        ) {
313            if self.buffer.append(buf.as_ref()) {
314                self.flush_internal(false, false).await?;
315            }
316            return Ok(offset);
317        }
318
319        // Bytes needed to fill current page to a page boundary (0 if already aligned).
320        let fill = self.buffer.len().next_multiple_of(logical_page_size) - self.buffer.len();
321
322        // Top up the tip to a page boundary so its contents flush as full pages, leaving any
323        // partial-page CRC handling to the regular flush path.
324        if fill > 0 {
325            self.buffer.append(&buf.as_ref()[..fill]);
326        }
327        let boundary = self.buffer.size();
328        if !self.buffer.is_empty() {
329            self.flush_internal(false, false).await?;
330            assert!(
331                self.buffer.size() == boundary && self.buffer.is_empty(),
332                "flush left unexpected buffered bytes before a direct-path append"
333            );
334        }
335
336        // Prepare physical pages for the whole pages remaining in `buf` without copying logical
337        // payload bytes.
338        let bulk_len = (buf.len() - fill) / logical_page_size * logical_page_size;
339        let bulk = buf.slice(fill..fill + bulk_len);
340        let mut physical_pages = IoBufs::default();
341        self.append_full_pages(&bulk, None, &mut physical_pages);
342
343        assert!(
344            self.partial_page_state.is_none(),
345            "an empty tip implies no partial page state"
346        );
347
348        // Direct blob writes must not overtake an earlier started sync barrier.
349        self.sync_state.wait_for_pending().await?;
350
351        // Cache the pages before `replace` publishes the new size, so reads of the bulk range are
352        // served from the cache while the blob write is still in flight. Insert in
353        // write-buffer-sized chunks. The capacity is a whole number of pages (see
354        // [adjusted_capacity]), so each chunk is page-aligned.
355        let chunk_len = self.buffer.capacity;
356        let mut cache_offset = boundary;
357        for chunk in bulk.as_ref().chunks(chunk_len) {
358            let remaining = self.cache_ref.cache(self.id, chunk, cache_offset);
359            assert_eq!(remaining, 0, "cached bulk pages must be page-aligned");
360            cache_offset += chunk.len() as u64;
361        }
362
363        // Update state before writing, seeding the tip with the partial-page suffix of `buf`.
364        // The suffix (less than one page) is copied: a sub-page tip is never drained by flush,
365        // so seeding it with a view of `buf` would pin the entire backing allocation until the
366        // next append to this blob (or forever, if there is none).
367        self.current_page += (bulk_len / logical_page_size) as u64;
368        let suffix = buf.slice(fill + bulk_len..);
369        let suffix = if suffix.is_empty() {
370            suffix
371        } else {
372            let mut copied = self.cache_ref.pool().alloc(suffix.len());
373            copied.put_slice(suffix.as_ref());
374            copied.freeze()
375        };
376        self.buffer.replace(boundary + bulk_len as u64, suffix);
377
378        // Make sure the buffer offset and underlying blob agree on the state of the tip.
379        assert_eq!(
380            self.current_page * self.cache_ref.page_size(),
381            self.buffer.offset
382        );
383
384        let physical_page_size = logical_page_size as u64 + CHECKSUM_SIZE;
385        let write_at_offset = boundary / logical_page_size as u64 * physical_page_size;
386        self.write_at(write_at_offset, physical_pages).await?;
387
388        Ok(offset)
389    }
390
391    /// Flush all full pages from the buffer to disk, resetting the buffer to contain only the bytes
392    /// in any final partial page.
393    ///
394    /// If `write_partial_page` is true, the partial page will be written to the blob as well along
395    /// with a CRC record.
396    ///
397    /// If `sync` is true and the flush emits a single write, that write is made durable
398    /// immediately: with [`Blob::write_at_sync`] when there are no earlier unsynced mutations, or
399    /// by writing it and syncing the blob when there are. Flushes split around a protected CRC use
400    /// plain writes so the caller can make them durable with one sync.
401    ///
402    /// Returns `true` if the flush made its writes durable, so no additional sync is needed.
403    async fn flush_internal(
404        &mut self,
405        write_partial_page: bool,
406        sync: bool,
407    ) -> Result<bool, Error> {
408        // Prepare the *physical* pages corresponding to the data in the buffer.
409        // Pass the old partial page state so the CRC record is constructed correctly.
410        let (mut physical_pages, partial_page_state) = self.to_physical_pages(
411            &self.buffer,
412            write_partial_page,
413            self.partial_page_state.as_ref(),
414        );
415
416        // If there's nothing to write, return early.
417        if physical_pages.is_empty() {
418            return Ok(false);
419        }
420
421        // A flush mutates the blob, so first resolve any outstanding start_sync barrier.
422        self.sync_state.wait_for_pending().await?;
423
424        // Split buffered bytes into full logical pages to hand off now, leaving any trailing
425        // partial page in tip for continued buffering.
426        let logical_page_size = self.cache_ref.page_size() as usize;
427        let pages_to_cache = self.buffer.len() / logical_page_size;
428        let bytes_to_drain = pages_to_cache * logical_page_size;
429
430        // Remember the logical start offset and page bytes for caching of flushed full pages.
431        let cache_pages = if pages_to_cache > 0 {
432            Some((self.buffer.offset, self.buffer.slice(..bytes_to_drain)))
433        } else {
434            None
435        };
436
437        // Drain full pages from the buffered logical data. If the tip is fully drained, detach its
438        // backing so empty append buffers don't retain pooled storage.
439        if bytes_to_drain == self.buffer.len() && bytes_to_drain != 0 {
440            let _ = self
441                .buffer
442                .take()
443                .expect("take must succeed when flush drains all buffered bytes");
444        } else if bytes_to_drain != 0 {
445            self.buffer.drop_prefix(bytes_to_drain);
446            self.buffer.offset += bytes_to_drain as u64;
447        }
448        let new_offset = self.buffer.offset;
449
450        // Cache full pages before publishing the new blob state so reads don't observe stale
451        // persisted bytes during the handoff from tip to cache.
452        if let Some((cache_offset, pages)) = cache_pages {
453            let remaining = self.cache_ref.cache(self.id, pages.as_ref(), cache_offset);
454            assert_eq!(remaining, 0, "cached full-page prefix must be page-aligned");
455        }
456
457        let physical_page_size = logical_page_size + CHECKSUM_SIZE as usize;
458        let write_at_offset = self.current_page * physical_page_size as u64;
459
460        // Identify protected regions based on the OLD partial page state.
461        let protected_regions = Self::identify_protected_regions(self.partial_page_state.as_ref());
462
463        // Update state before writing. This may appear to risk data loss if writes fail,
464        // but write failures are fatal per this codebase's design: callers must not use
465        // the blob after any mutable method returns an error.
466        self.current_page += pages_to_cache as u64;
467        self.partial_page_state = partial_page_state;
468
469        // Make sure the buffer offset and underlying blob agree on the state of the tip.
470        assert_eq!(self.current_page * self.cache_ref.page_size(), new_offset);
471
472        // Write the physical pages to the blob.
473        // If there are protected regions in the first page, we need to write around them.
474        match protected_regions {
475            Some((prefix_len, Slot::First)) => {
476                // Protected CRC is first: [page_size..page_size+6].
477                //
478                // If only one of these writes is emitted, it can be made durable here. If
479                // both are emitted, keep them plain so one later sync covers both.
480                //
481                // Write 1: new data in first page [prefix_len..page_size].
482                let has_first_write = prefix_len < logical_page_size;
483                if has_first_write {
484                    let _ = physical_pages.split_to(prefix_len);
485                    let first_payload = physical_pages.split_to(logical_page_size - prefix_len);
486                    let has_second_write = physical_pages.len() > CHECKSUM_SLOT_SIZE;
487                    self.write_at_maybe_sync(
488                        write_at_offset + prefix_len as u64,
489                        first_payload,
490                        sync && !has_second_write,
491                    )
492                    .await?;
493                    if !has_second_write {
494                        return Ok(sync);
495                    }
496                } else {
497                    // Skip the protected first page bytes when they are fully covered.
498                    let _ = physical_pages.split_to(logical_page_size);
499                }
500
501                // Write 2: second CRC of first page + all remaining pages [page_size+6..end].
502                if physical_pages.len() > CHECKSUM_SLOT_SIZE {
503                    let _ = physical_pages.split_to(CHECKSUM_SLOT_SIZE);
504                    self.write_at_maybe_sync(
505                        write_at_offset + (logical_page_size + CHECKSUM_SLOT_SIZE) as u64,
506                        physical_pages,
507                        sync && !has_first_write,
508                    )
509                    .await?;
510                    if !has_first_write {
511                        return Ok(sync);
512                    }
513                }
514
515                Ok(false)
516            }
517            Some((prefix_len, Slot::Second)) => {
518                // Protected CRC is second: [page_size+6..page_size+12].
519                //
520                // If only one of these writes is emitted, it can be made durable here. If
521                // both are emitted, keep them plain so one later sync covers both.
522                //
523                // Write 1: new data + first CRC of first page [prefix_len..page_size+6].
524                let first_crc_end = logical_page_size + CHECKSUM_SLOT_SIZE;
525                let skip = physical_page_size - first_crc_end;
526                let has_first_write = prefix_len < first_crc_end;
527                if has_first_write {
528                    let _ = physical_pages.split_to(prefix_len);
529                    let first_payload = physical_pages.split_to(first_crc_end - prefix_len);
530                    let has_second_write = physical_pages.len() > skip;
531                    self.write_at_maybe_sync(
532                        write_at_offset + prefix_len as u64,
533                        first_payload,
534                        sync && !has_second_write,
535                    )
536                    .await?;
537                    if !has_second_write {
538                        return Ok(sync);
539                    }
540                } else {
541                    // Skip the fully protected first segment when no bytes from it need update.
542                    let _ = physical_pages.split_to(first_crc_end);
543                }
544
545                // Write 2: all remaining pages (if any) [physical_page_size..end].
546                if physical_pages.len() > skip {
547                    let _ = physical_pages.split_to(skip);
548                    self.write_at_maybe_sync(
549                        write_at_offset + physical_page_size as u64,
550                        physical_pages,
551                        sync && !has_first_write,
552                    )
553                    .await?;
554                    if !has_first_write {
555                        return Ok(sync);
556                    }
557                }
558
559                Ok(false)
560            }
561            None => {
562                // No protected regions, write everything in one operation
563                self.write_at_maybe_sync(write_at_offset, physical_pages, sync)
564                    .await?;
565                Ok(sync)
566            }
567        }
568    }
569
570    /// Returns the size of the blob.
571    pub const fn size(&self) -> u64 {
572        self.buffer.size()
573    }
574
575    /// Returns a borrowed view over this blob.
576    fn view(&self) -> View<'_, B> {
577        View {
578            blob: &self.blob,
579            cache_ref: &self.cache_ref,
580            id: self.id,
581            size: self.buffer.size(),
582            tail_offset: self.buffer.offset,
583            tail: self.buffer.as_ref(),
584        }
585    }
586
587    /// Read into `buf` if it can be done synchronously without I/O. Returns `true` only if all
588    /// `buf.len()` bytes were satisfied from the page cache and/or the in-memory tail. When `false`
589    /// is returned, the contents of `buf` are unspecified.
590    pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
591        self.view().try_read_sync_into(buf, offset)
592    }
593
594    /// Read exactly `len` immutable bytes starting at `offset`.
595    pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
596        self.view().read_at(offset, len).await
597    }
598
599    /// Reads up to `len` bytes starting at `offset`, but only as many as are available.
600    ///
601    /// Returns the buffer (truncated to actual bytes read) and the number of bytes read. Returns
602    /// an error if no bytes are available at the given offset.
603    pub async fn read_up_to(
604        &self,
605        offset: u64,
606        len: usize,
607        bufs: impl Into<IoBufMut> + Send,
608    ) -> Result<(IoBufMut, usize), Error> {
609        self.view().read_up_to(offset, len, bufs).await
610    }
611
612    /// Read multiple fixed-size items at sorted byte offsets into a contiguous caller buffer.
613    ///
614    /// `buf` must be exactly `offsets.len() * item_size` bytes. All offsets must be sorted,
615    /// non-overlapping, and within bounds.
616    ///
617    /// Returns the number of items fully served without a blob read (from the in-memory tail and the
618    /// page cache). The remaining items required at least one blob read.
619    pub async fn read_many_into(
620        &self,
621        buf: &mut [u8],
622        offsets: &[u64],
623        item_size: NonZeroUsize,
624    ) -> Result<usize, Error> {
625        self.view().read_many_into(buf, offsets, item_size).await
626    }
627
628    /// Like [`Self::read_many_into`], but synchronous and cache-only. Returns the indices of
629    /// items that require a blob read. Their slots in `buf` hold unspecified bytes.
630    pub fn try_read_many_sync_into(
631        &self,
632        buf: &mut [u8],
633        offsets: &[u64],
634        item_size: NonZeroUsize,
635    ) -> Vec<usize> {
636        self.view().try_read_many_sync_into(buf, offsets, item_size)
637    }
638
639    /// Like [`Self::try_read_many_sync_into`], but for variable-length `(offset, len)` ranges:
640    /// `buf` holds one slot per range, back to back.
641    pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
642        self.view().try_read_ranges_sync_into(buf, ranges)
643    }
644
645    /// Reads bytes starting at `offset` into `buf`.
646    pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
647        self.view().read_into(buf, offset).await
648    }
649
650    /// Return the first-page region that must be skipped to preserve a committed partial page.
651    ///
652    /// # Returns
653    ///
654    /// `None` if there's no existing partial page.
655    ///
656    /// `Some((prefix_len, protected_crc))` where:
657    /// - `prefix_len`: bytes `[0, prefix_len)` are committed logical data already covered by the
658    ///   protected CRC and do not need to be rewritten
659    /// - `protected_crc`: which CRC slot must not be overwritten by the next flush
660    fn identify_protected_regions(partial_page_state: Option<&Checksum>) -> Option<(usize, Slot)> {
661        let crc_record = partial_page_state?;
662        let (old_len, _) = crc_record.get_crc();
663        // The protected CRC is the authoritative (longer) slot.
664        Some((old_len as usize, crc_record.authoritative()))
665    }
666
667    /// Prepare physical-page writes from buffered logical bytes.
668    ///
669    /// Each physical page contains one logical page plus CRC record. If the last page is not yet
670    /// full, it will be included only if `include_partial_page` is true.
671    ///
672    /// # Arguments
673    ///
674    /// * `buffer` - The buffer containing logical page data
675    /// * `include_partial_page` - Whether to include a partial page if one exists
676    /// * `old_crc_record` - The CRC record from a previously committed partial page, if any.
677    ///   When present, the first page's CRC record will preserve the old CRC in its original slot
678    ///   and place the new CRC in the other slot.
679    fn to_physical_pages(
680        &self,
681        buffer: &Buffer,
682        include_partial_page: bool,
683        old_crc_record: Option<&Checksum>,
684    ) -> (IoBufs, Option<Checksum>) {
685        let logical_page_size = self.cache_ref.page_size() as usize;
686        let physical_page_size = logical_page_size + CHECKSUM_SIZE as usize;
687        let pages_to_write = buffer.len() / logical_page_size;
688        let mut write_buffer = IoBufs::default();
689        let buffer_data = buffer.as_ref();
690
691        if pages_to_write > 0 {
692            self.append_full_pages(
693                &buffer.slice(..pages_to_write * logical_page_size),
694                old_crc_record,
695                &mut write_buffer,
696            );
697        }
698
699        if !include_partial_page {
700            return (write_buffer, None);
701        }
702
703        let partial_page = &buffer_data[pages_to_write * logical_page_size..];
704        if partial_page.is_empty() {
705            // No partial page data to write.
706            return (write_buffer, None);
707        }
708
709        // If there are no full pages and the partial page length matches what was already
710        // written, there's nothing new to write.
711        if pages_to_write == 0 {
712            if let Some(old_crc) = old_crc_record {
713                let (old_len, _) = old_crc.get_crc();
714                if partial_page.len() == old_len as usize {
715                    return (write_buffer, None);
716                }
717            }
718        }
719        let partial_len = partial_page.len();
720        let crc = Crc32::checksum(partial_page);
721
722        // For partial pages: if this is the first page and there's an old CRC, preserve it.
723        // Otherwise just use the new CRC in slot 0.
724        let crc_record = if let (0, Some(old_crc)) = (pages_to_write, old_crc_record) {
725            Self::build_crc_record_preserving_old(partial_len as u16, crc, old_crc)
726        } else {
727            Checksum::new(partial_len as u16, crc)
728        };
729
730        // A persisted partial page still occupies one full physical page:
731        // [partial logical bytes, zero padding, crc record].
732        let mut padded = self.cache_ref.pool().alloc(physical_page_size);
733        padded.put_slice(partial_page);
734        let zero_count = logical_page_size - partial_len;
735        if zero_count > 0 {
736            padded.put_bytes(0, zero_count);
737        }
738        padded.put_slice(&crc_record.to_bytes());
739        write_buffer.append(padded.freeze());
740
741        // Return the CRC record that matches what we wrote to disk, so that future flushes
742        // correctly identify which slot is protected.
743        (write_buffer, Some(crc_record))
744    }
745
746    /// Appends each page of `data` to `write_buffer` in on-disk format: its payload (a zero-copy
747    /// slice of `data`) followed by a CRC record.
748    ///
749    /// `data.len()` must be a non-zero multiple of the page size. When `old_crc_record` is present,
750    /// the first page's record preserves the old CRC in its original slot.
751    fn append_full_pages(
752        &self,
753        data: &IoBuf,
754        old_crc_record: Option<&Checksum>,
755        write_buffer: &mut IoBufs,
756    ) {
757        let logical_page_size = self.cache_ref.page_size() as usize;
758        let pages = data.len() / logical_page_size;
759        debug_assert!(pages > 0);
760        debug_assert_eq!(data.len() % logical_page_size, 0);
761        let logical_page_size_u16 =
762            u16::try_from(logical_page_size).expect("page size must fit in u16 for CRC record");
763
764        // Build CRC bytes for full pages once. Full-page payload bytes are appended below as
765        // slices from `data`, so we avoid copying logical payload here.
766        let mut crcs = self.cache_ref.pool().alloc(CHECKSUM_SIZE as usize * pages);
767        let data_bytes = data.as_ref();
768        for page in 0..pages {
769            let start_read_idx = page * logical_page_size;
770            let end_read_idx = start_read_idx + logical_page_size;
771            let logical_page = &data_bytes[start_read_idx..end_read_idx];
772            let crc = Crc32::checksum(logical_page);
773
774            // For the first page, if there's an old partial page CRC, construct the record
775            // to preserve the old CRC in its original slot.
776            let crc_record = if let (0, Some(old_crc)) = (page, old_crc_record) {
777                Self::build_crc_record_preserving_old(logical_page_size_u16, crc, old_crc)
778            } else {
779                Checksum::new(logical_page_size_u16, crc)
780            };
781            crcs.put_slice(&crc_record.to_bytes());
782        }
783        let crc_blob = crcs.freeze();
784
785        // Physical full-page layout is [logical_page_bytes, crc_record_bytes].
786        for page in 0..pages {
787            let start_read_idx = page * logical_page_size;
788            let end_read_idx = start_read_idx + logical_page_size;
789            write_buffer.append(data.slice(start_read_idx..end_read_idx));
790
791            let crc_start = page * CHECKSUM_SIZE as usize;
792            write_buffer.append(crc_blob.slice(crc_start..crc_start + CHECKSUM_SIZE as usize));
793        }
794    }
795
796    /// Build a CRC record that preserves the old CRC in its original slot and places the new CRC
797    /// in the other slot.
798    ///
799    /// A subsequent flush writes around the preserved slot, so an interrupted rewrite can recover
800    /// either the old partial page or the new one.
801    const fn build_crc_record_preserving_old(
802        new_len: u16,
803        new_crc: u32,
804        old_crc: &Checksum,
805    ) -> Checksum {
806        let (old_len, old_crc_val) = old_crc.get_crc();
807        // Keep the old CRC in its slot and place the new CRC in the free one.
808        match old_crc.authoritative() {
809            Slot::First => Checksum {
810                len1: old_len,
811                crc1: old_crc_val,
812                len2: new_len,
813                crc2: new_crc,
814            },
815            Slot::Second => Checksum {
816                len1: new_len,
817                crc1: new_crc,
818                len2: old_len,
819                crc2: old_crc_val,
820            },
821        }
822    }
823
824    /// Durably rewrite a committed page to a shorter partial length.
825    async fn sync_partial_page_shrink(
826        &mut self,
827        page: u64,
828        logical_page_size: u64,
829        new_len: u16,
830        new_crc: u32,
831        old_crc: &Checksum,
832    ) -> Result<Checksum, Error> {
833        // Recovery chooses the valid slot with the larger length. While shrinking, the new
834        // checksum must be made durable without becoming authoritative until the old longer slot
835        // can be disabled. The sequence below therefore lets recovery observe either the old page
836        // or the new shorter page, but not a footer where both slots were damaged by one torn write.
837        let physical_page_size = logical_page_size
838            .checked_add(CHECKSUM_SIZE)
839            .ok_or(Error::OffsetOverflow)?;
840        let crc_start = page
841            .checked_mul(physical_page_size)
842            .and_then(|start| start.checked_add(logical_page_size))
843            .ok_or(Error::OffsetOverflow)?;
844        let old_slot = old_crc.authoritative();
845        let new_slot = old_slot.other();
846
847        // Stage the new slot with a 0 length and the shrunken page CRC. A crash here leaves the
848        // old slot as the only non-zero valid slot.
849        let new_slot_offset = crc_start
850            .checked_add(new_slot.offset() as u64)
851            .ok_or(Error::OffsetOverflow)?;
852        let staged_slot = Checksum::slot_bytes(0, new_crc);
853        self.write_at_sync(new_slot_offset, staged_slot.to_vec())
854            .await?;
855
856        // Publish the new shrunken length. If a crash happens before the old slot is invalidated,
857        // both slots may be valid, but recovery still chooses the old longer length.
858        let published_len = Checksum::slot_len_bytes(new_len);
859        self.write_at_sync(new_slot_offset, published_len.to_vec())
860            .await?;
861
862        // Clear only the old slot's length bytes. Rewriting the whole footer here could tear across
863        // both slots and lose the already-durable shorter checksum. Once this lands, length 0 is
864        // never authoritative, so the shrunken slot wins.
865        let old_slot_offset = crc_start
866            .checked_add(old_slot.offset() as u64)
867            .ok_or(Error::OffsetOverflow)?;
868        self.write_at_sync(old_slot_offset, Checksum::slot_len_bytes(0).to_vec())
869            .await?;
870
871        Ok(Checksum::in_slot(new_slot, new_len, new_crc))
872    }
873
874    /// Flushes any buffered data, then returns a [Replay] for the underlying blob.
875    ///
876    /// The returned replay can be used to sequentially read all pages from the blob while ensuring
877    /// all data passes integrity verification. CRCs are validated but not included in the output.
878    ///
879    /// This is not a durable operation. Buffered data may be plainly written so the replay can
880    /// read it, but callers must still use [`sync`](Self::sync) if that data must survive a crash.
881    pub async fn replay(&mut self, buffer_size: NonZeroUsize) -> Result<Replay<B>, Error> {
882        let logical_page_size = self.cache_ref.page_size();
883        let logical_page_size_nz =
884            NonZeroU16::new(logical_page_size as u16).expect("page_size is non-zero");
885
886        // Flush any buffered data (without fsync) so the reader sees all written data.
887        self.flush_internal(true, false).await?;
888
889        // Convert buffer size (bytes) to page count
890        let physical_page_size = logical_page_size + CHECKSUM_SIZE;
891        let prefetch_pages = buffer_size.get() / physical_page_size as usize;
892        let prefetch_pages = prefetch_pages.max(1); // At least 1 page
893
894        // Compute both physical and logical blob sizes.
895        let (physical_blob_size, logical_blob_size) = self.partial_page_state.as_ref().map_or_else(
896            || {
897                // All pages are full.
898                let physical = physical_page_size * self.current_page;
899                let logical = logical_page_size * self.current_page;
900                (physical, logical)
901            },
902            |crc_record| {
903                // There's a partial page with a checksum.
904                let (partial_len, _) = crc_record.get_crc();
905                let partial_len = partial_len as u64;
906                // Physical: all pages including the partial one (which is padded to full size).
907                let physical = physical_page_size * (self.current_page + 1);
908                // Logical: full pages before this + partial page's actual data length.
909                let logical = logical_page_size * self.current_page + partial_len;
910                (physical, logical)
911            },
912        );
913
914        let reader = PageReader::new(
915            self.blob.clone(),
916            physical_blob_size,
917            logical_blob_size,
918            prefetch_pages,
919            logical_page_size_nz,
920        );
921        Ok(Replay::new(reader))
922    }
923
924    /// Flush buffered data and capture an immutable [`super::Sealed`] view without consuming the
925    /// writer.
926    ///
927    /// This writes buffered bytes to the blob layout but does not make them durable. Call
928    /// [`Self::sync`] or [`super::Sealed::sync`] if the returned handle's bytes must survive a
929    /// crash.
930    ///
931    /// If this writer later rewinds or truncates into the returned handle's range, reads from that
932    /// handle may observe unspecified contents.
933    pub async fn snapshot(&mut self) -> Result<super::Sealed<B>, Error> {
934        self.flush_internal(true, false).await?;
935        Ok(self.sealed_handle(self.cache_ref.next_id()))
936    }
937
938    /// Flushes buffered data and makes all pending mutations durable.
939    ///
940    /// A single physical write can be persisted with [`Blob::write_at_sync`]. If there
941    /// are earlier unsynced mutations, or if the flush emits multiple physical writes,
942    /// durability is completed with [`Blob::sync`].
943    pub async fn sync(&mut self) -> Result<(), Error> {
944        // Flush any buffered data, including any partial page.
945        // A single emitted write can be made durable directly during the flush.
946        if self.flush_internal(true, true).await? {
947            return Ok(());
948        }
949
950        // Otherwise, the flush either had no bytes to write or used plain writes. Sync only if a
951        // durability barrier is still pending.
952        self.sync_state.sync(&self.blob).await
953    }
954
955    /// Flushes buffered data and begins making all pending mutations durable, returning a
956    /// completion handle.
957    ///
958    /// Awaiting the returned [`Handle`] waits for the same durability guarantee as [`Self::sync`]
959    /// for the state flushed by this call. Later calls to [`Self::sync`] and writer methods that
960    /// mutate the blob first wait for any outstanding start_sync handles.
961    pub async fn start_sync(&mut self) -> Handle<()> {
962        if let Err(err) = self.flush_internal(true, false).await {
963            return Handle::ready(Err(err));
964        }
965        self.sync_state.start_sync(&self.blob).await
966    }
967
968    /// Wait for any started sync to complete without starting a new sync.
969    pub async fn wait_for_sync(&mut self) -> Result<(), Error> {
970        self.sync_state.wait_for_pending().await
971    }
972
973    /// Resize the blob to the provided logical `size`.
974    ///
975    /// This truncates the blob to contain only `size` logical bytes. The physical blob size will
976    /// be adjusted to include the necessary CRC records for the remaining pages.
977    ///
978    /// # Warning
979    ///
980    /// - Concurrent mutable operations (append, resize) are not supported and will cause data loss.
981    /// - Concurrent readers which try to read past the new size during the resize may error.
982    /// - The resize is not guaranteed durable until the next sync.
983    pub async fn resize(&mut self, size: u64) -> Result<(), Error> {
984        let current_size = self.buffer.size();
985        if size == current_size {
986            return Ok(());
987        }
988
989        // Handle growing by appending zero bytes.
990        if size > current_size {
991            let zeros_needed = (size - current_size) as usize;
992            let mut zeros = self.cache_ref.pool().alloc(zeros_needed);
993            zeros.put_bytes(0, zeros_needed);
994            self.append_owned(zeros.freeze()).await?;
995            return Ok(());
996        }
997
998        self.shrink(size).await
999    }
1000
1001    /// Coordinate the dispatch logic for shrinking the blob.
1002    async fn shrink(&mut self, target_size: u64) -> Result<(), Error> {
1003        let logical_page_size = self.cache_ref.page_size();
1004        let physical_page_size = logical_page_size
1005            .checked_add(CHECKSUM_SIZE)
1006            .ok_or(Error::OffsetOverflow)?;
1007
1008        // Flush any buffered data first to ensure we have a consistent state on disk.
1009        self.sync().await?;
1010
1011        // Calculate the physical size needed for the new size.
1012        let full_pages = target_size / logical_page_size;
1013        let partial_bytes = target_size % logical_page_size;
1014        let physical_pages = full_pages
1015            .checked_add(u64::from(partial_bytes > 0))
1016            .ok_or(Error::OffsetOverflow)?;
1017        let new_physical_size = physical_pages
1018            .checked_mul(physical_page_size)
1019            .ok_or(Error::OffsetOverflow)?;
1020        let tail_offset = full_pages
1021            .checked_mul(logical_page_size)
1022            .ok_or(Error::OffsetOverflow)?;
1023        let current_physical_size = if self.partial_page_state.is_some() {
1024            self.current_page
1025                .checked_add(1)
1026                .and_then(|pages| pages.checked_mul(physical_page_size))
1027                .ok_or(Error::OffsetOverflow)?
1028        } else {
1029            self.current_page
1030                .checked_mul(physical_page_size)
1031                .ok_or(Error::OffsetOverflow)?
1032        };
1033
1034        // A logical shrink can leave the physical page count unchanged. Only real physical
1035        // resizes need to create a pending sync.
1036        if new_physical_size != current_physical_size {
1037            self.sync_state
1038                .resize(&self.blob, new_physical_size)
1039                .await?;
1040        }
1041
1042        // Evict cached pages at or beyond the new full-page boundary. The page at
1043        // `full_pages` (if partial) is now owned by the tip buffer, and anything above is
1044        // beyond the new size. Leaving their pre-resize contents in the cache
1045        // lets `try_read_sync_into` (whose reads below the tip boundary come straight from
1046        // the page cache) observe stale bytes once
1047        // the tip is repopulated.
1048        self.cache_ref.invalidate_from(self.id, full_pages);
1049
1050        if partial_bytes > 0 {
1051            return self
1052                .shrink_to_partial(full_pages, partial_bytes, logical_page_size, tail_offset)
1053                .await;
1054        }
1055
1056        // Shrink the blob to a page boundary, which requires no CRC-slot rewrite.
1057        self.partial_page_state = None;
1058        self.current_page = full_pages;
1059        self.buffer.offset = tail_offset;
1060        self.buffer.clear();
1061
1062        Ok(())
1063    }
1064
1065    /// Perform a shrink to a partial page tip and make the shorter CRC slot authoritative.
1066    async fn shrink_to_partial(
1067        &mut self,
1068        full_pages: u64,
1069        partial_bytes: u64,
1070        logical_page_size: u64,
1071        tail_offset: u64,
1072    ) -> Result<(), Error> {
1073        // Update blob state and buffer based on the desired size. The page data is
1074        // read with CRC validation, then durably rewritten below with a shorter CRC.
1075        self.current_page = full_pages;
1076        self.buffer.offset = tail_offset;
1077
1078        let (page_data, old_crc) =
1079            super::get_page_with_checksum_from_blob(&self.blob, full_pages, logical_page_size)
1080                .await?;
1081
1082        // Ensure the validated data covers what we need.
1083        if (page_data.len() as u64) < partial_bytes {
1084            return Err(Error::InvalidChecksum);
1085        }
1086
1087        self.buffer.clear();
1088        let new_data = &page_data.as_ref()[..partial_bytes as usize];
1089        let over_capacity = self.buffer.append(new_data);
1090        assert!(!over_capacity);
1091
1092        let final_record = self
1093            .sync_partial_page_shrink(
1094                full_pages,
1095                logical_page_size,
1096                partial_bytes as u16,
1097                Crc32::checksum(new_data),
1098                &old_crc,
1099            )
1100            .await?;
1101        self.partial_page_state = Some(final_record);
1102
1103        Ok(())
1104    }
1105
1106    /// Page-cache id used for reads. Exposed for tests.
1107    #[cfg(test)]
1108    pub(super) const fn cache_id(&self) -> u64 {
1109        self.id
1110    }
1111
1112    /// Construct an immutable read handle for the current blob state.
1113    fn sealed_handle(&self, id: u64) -> super::Sealed<B> {
1114        let logical_page_size = self.cache_ref.page_size();
1115        let full_pages = self.current_page;
1116        assert_eq!(
1117            full_pages.checked_mul(logical_page_size),
1118            Some(self.buffer.offset),
1119            "flushed page count is inconsistent with the buffer offset"
1120        );
1121        let partial_page = if self.buffer.is_empty() {
1122            None
1123        } else {
1124            Some(self.buffer.slice(..))
1125        };
1126        super::Sealed::new(
1127            self.blob.clone(),
1128            self.buffer.size(),
1129            partial_page,
1130            self.cache_ref.clone(),
1131            id,
1132        )
1133    }
1134
1135    /// Consume the write handle and return an immutable [`super::Sealed`] handle for the same
1136    /// blob.
1137    ///
1138    /// Buffered bytes (full and partial pages) are written to the underlying blob, but the blob is
1139    /// not fsynced. The returned [`super::Sealed`] handle can be made durable later via
1140    /// [`super::Sealed::sync`].
1141    pub async fn seal(mut self) -> Result<super::Sealed<B>, Error> {
1142        self.sync_state.wait_for_pending().await?;
1143        self.flush_internal(true, false).await?;
1144        Ok(self.sealed_handle(self.id))
1145    }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150    use super::*;
1151    use crate::{
1152        buffer::{paged::CHECKSUM_SLOT_LEN_SIZE, tests::SyncTrackingBlob},
1153        deterministic,
1154        mocks::{next_pending_sync, DelayedSyncBlob},
1155        telemetry::metrics::Registry,
1156        Buf, BufferPool, BufferPoolConfig, Handle, IoBufsMut, Runner as _, Spawner as _,
1157        Storage as _, Supervisor as _,
1158    };
1159    use commonware_codec::ReadExt;
1160    use commonware_macros::test_traced;
1161    use commonware_utils::{channel::oneshot, sync::Mutex, NZUsize, NZU16, NZU32};
1162    use futures::FutureExt as _;
1163    use std::{
1164        num::NonZeroU16,
1165        sync::{
1166            atomic::{AtomicUsize, Ordering},
1167            Arc,
1168        },
1169    };
1170
1171    const PAGE_SIZE: NonZeroU16 = NZU16!(103); // janky size to ensure we test page alignment
1172    const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
1173
1174    #[test_traced("DEBUG")]
1175    fn test_read_many_into_empty() {
1176        let executor = deterministic::Runner::default();
1177        executor.start(|context: deterministic::Context| async move {
1178            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1179            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1180            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1181                .await
1182                .unwrap();
1183
1184            append.append(&[0u8; 8]).await.unwrap();
1185            assert_eq!(append.size(), 8);
1186
1187            // Empty offsets should succeed immediately.
1188            let mut buf = [];
1189            append
1190                .read_many_into(&mut buf, &[], NZUsize!(4))
1191                .await
1192                .unwrap();
1193        });
1194    }
1195
1196    #[test_traced("DEBUG")]
1197    fn test_read_many_into_all_in_tip() {
1198        // All items reside in the unflushed tip buffer.
1199        let executor = deterministic::Runner::default();
1200        executor.start(|context: deterministic::Context| async move {
1201            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1202            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1203            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1204                .await
1205                .unwrap();
1206
1207            let data: Vec<u8> = (0..20).collect();
1208            append.append(&data).await.unwrap();
1209            assert_eq!(append.size(), 20);
1210
1211            // Read 4-byte items at offsets 0, 4, 8, 12, 16.
1212            let offsets = [0u64, 4, 8, 12, 16];
1213            let mut buf = vec![0u8; 5 * 4];
1214            append
1215                .read_many_into(&mut buf, &offsets, NZUsize!(4))
1216                .await
1217                .unwrap();
1218
1219            for (i, &off) in offsets.iter().enumerate() {
1220                assert_eq!(
1221                    &buf[i * 4..(i + 1) * 4],
1222                    &data[off as usize..off as usize + 4],
1223                );
1224            }
1225        });
1226    }
1227
1228    #[test_traced("DEBUG")]
1229    fn test_try_read_sync_all_in_tip() {
1230        let executor = deterministic::Runner::default();
1231        executor.start(|context: deterministic::Context| async move {
1232            let (blob, blob_size) = context
1233                .open("test_partition", b"try_read_sync_tip")
1234                .await
1235                .unwrap();
1236            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1237            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1238                .await
1239                .unwrap();
1240
1241            let data: Vec<u8> = (0..20).collect();
1242            append.append(&data).await.unwrap();
1243
1244            let mut buf = vec![0u8; data.len()];
1245            assert!(append.try_read_sync_into(&mut buf, 0));
1246            assert_eq!(buf, data);
1247        });
1248    }
1249
1250    #[test_traced("DEBUG")]
1251    fn test_try_read_sync_cache_miss() {
1252        let executor = deterministic::Runner::default();
1253        executor.start(|context: deterministic::Context| async move {
1254            let (blob, blob_size) = context
1255                .open("test_partition", b"try_read_sync_cache_miss")
1256                .await
1257                .unwrap();
1258            // A one-page cache lets us prime the first page while leaving the second uncached.
1259            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
1260            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1261                .await
1262                .unwrap();
1263
1264            let page_size = PAGE_SIZE.get() as usize;
1265            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
1266            append.append(&data).await.unwrap();
1267            append.sync().await.unwrap();
1268
1269            let _ = append.read_at(0, page_size).await.unwrap();
1270
1271            // A read straddling the cached first page and the uncached second page misses.
1272            let mut buf = vec![0xAA; 4];
1273            assert!(!append.try_read_sync_into(&mut buf, (page_size - 2) as u64));
1274        });
1275    }
1276
1277    #[test_traced("DEBUG")]
1278    fn test_read_many_into_all_from_cache() {
1279        // Sync data to disk so tip buffer is empty; reads go through page cache / blob.
1280        let executor = deterministic::Runner::default();
1281        executor.start(|context: deterministic::Context| async move {
1282            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1283            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1284            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1285                .await
1286                .unwrap();
1287
1288            let data: Vec<u8> = (0..20).collect();
1289            append.append(&data).await.unwrap();
1290            append.sync().await.unwrap();
1291            assert_eq!(append.size(), 20);
1292
1293            let offsets = [0u64, 8, 16];
1294            let mut buf = vec![0u8; 3 * 4];
1295            append
1296                .read_many_into(&mut buf, &offsets, NZUsize!(4))
1297                .await
1298                .unwrap();
1299
1300            for (i, &off) in offsets.iter().enumerate() {
1301                assert_eq!(
1302                    &buf[i * 4..(i + 1) * 4],
1303                    &data[off as usize..off as usize + 4],
1304                );
1305            }
1306        });
1307    }
1308
1309    #[test_traced("DEBUG")]
1310    fn test_read_many_into_mixed_tip_and_cache() {
1311        // First chunk synced to disk, second chunk still in tip buffer.
1312        let executor = deterministic::Runner::default();
1313        executor.start(|context: deterministic::Context| async move {
1314            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1315            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1316            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1317                .await
1318                .unwrap();
1319
1320            let first: Vec<u8> = (0..16).collect();
1321            append.append(&first).await.unwrap();
1322            append.sync().await.unwrap();
1323
1324            let second: Vec<u8> = (16..32).collect();
1325            append.append(&second).await.unwrap();
1326            assert_eq!(append.size(), 32);
1327
1328            // Offsets span both synced and unsynced regions.
1329            let offsets = [0u64, 4, 16, 24];
1330            let mut buf = vec![0u8; 4 * 4];
1331            append
1332                .read_many_into(&mut buf, &offsets, NZUsize!(4))
1333                .await
1334                .unwrap();
1335
1336            let all: Vec<u8> = (0..32).collect();
1337            for (i, &off) in offsets.iter().enumerate() {
1338                assert_eq!(
1339                    &buf[i * 4..(i + 1) * 4],
1340                    &all[off as usize..off as usize + 4],
1341                );
1342            }
1343        });
1344    }
1345
1346    #[test_traced("DEBUG")]
1347    fn test_read_many_into_out_of_bounds() {
1348        let executor = deterministic::Runner::default();
1349        executor.start(|context: deterministic::Context| async move {
1350            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1351            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1352            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1353                .await
1354                .unwrap();
1355
1356            append.append(&[0u8; 8]).await.unwrap();
1357            assert_eq!(append.size(), 8);
1358
1359            // Last offset's end (8 + 4 = 12) exceeds size (8).
1360            let mut buf = vec![0u8; 4];
1361            let err = append
1362                .read_many_into(&mut buf, &[8], NZUsize!(4))
1363                .await
1364                .unwrap_err();
1365            assert!(matches!(err, Error::BlobInsufficientLength));
1366        });
1367    }
1368
1369    #[test_traced("DEBUG")]
1370    fn test_read_many_into_single_item() {
1371        let executor = deterministic::Runner::default();
1372        executor.start(|context: deterministic::Context| async move {
1373            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1374            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1375            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1376                .await
1377                .unwrap();
1378
1379            let data = vec![0xAA; 8];
1380            append.append(&data).await.unwrap();
1381            assert_eq!(append.size(), 8);
1382
1383            let mut buf = vec![0u8; 8];
1384            append
1385                .read_many_into(&mut buf, &[0], NZUsize!(8))
1386                .await
1387                .unwrap();
1388            assert_eq!(&buf, &data);
1389        });
1390    }
1391
1392    #[test_traced("DEBUG")]
1393    #[should_panic(expected = "buf must hold one slot per range totaling its length")]
1394    fn test_read_many_into_rejects_invalid_buffer_len() {
1395        let executor = deterministic::Runner::default();
1396        executor.start(|context: deterministic::Context| async move {
1397            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1398            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1399            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1400                .await
1401                .unwrap();
1402
1403            let data: Vec<u8> = (0..16).collect();
1404            append.append(&data).await.unwrap();
1405
1406            let offsets = [0u64, 4];
1407            let mut buf = vec![0u8; 7];
1408            let _ = append.read_many_into(&mut buf, &offsets, NZUsize!(4)).await;
1409        });
1410    }
1411
1412    #[test_traced("DEBUG")]
1413    #[should_panic(expected = "ranges must be sorted and non-overlapping")]
1414    fn test_read_many_into_rejects_unsorted_offsets() {
1415        let executor = deterministic::Runner::default();
1416        executor.start(|context: deterministic::Context| async move {
1417            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1418            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1419            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1420                .await
1421                .unwrap();
1422
1423            let data: Vec<u8> = (0..16).collect();
1424            append.append(&data).await.unwrap();
1425
1426            let mut buf = vec![0u8; 8];
1427            let _ = append.read_many_into(&mut buf, &[8, 4], NZUsize!(4)).await;
1428        });
1429    }
1430
1431    #[test_traced("DEBUG")]
1432    #[should_panic(expected = "ranges must be sorted and non-overlapping")]
1433    fn test_read_many_into_rejects_overlapping_offsets() {
1434        let executor = deterministic::Runner::default();
1435        executor.start(|context: deterministic::Context| async move {
1436            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1437            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1438            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1439                .await
1440                .unwrap();
1441
1442            let data: Vec<u8> = (0..16).collect();
1443            append.append(&data).await.unwrap();
1444
1445            let mut buf = vec![0u8; 8];
1446            let _ = append.read_many_into(&mut buf, &[2, 4], NZUsize!(4)).await;
1447        });
1448    }
1449
1450    #[test_traced("DEBUG")]
1451    fn test_read_many_into_rejects_offset_overflow() {
1452        let executor = deterministic::Runner::default();
1453        executor.start(|context: deterministic::Context| async move {
1454            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1455            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1456            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1457                .await
1458                .unwrap();
1459
1460            let data: Vec<u8> = (0..16).collect();
1461            append.append(&data).await.unwrap();
1462
1463            let mut buf = vec![0u8; 8];
1464            let err = append
1465                .read_many_into(&mut buf, &[u64::MAX - 1, 4], NZUsize!(4))
1466                .await
1467                .unwrap_err();
1468            assert!(matches!(err, Error::OffsetOverflow));
1469        });
1470    }
1471
1472    #[test_traced("DEBUG")]
1473    fn test_read_many_into_matches_read_at() {
1474        // Verify read_many_into returns the same bytes as individual read_at calls.
1475        let executor = deterministic::Runner::default();
1476        executor.start(|context: deterministic::Context| async move {
1477            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1478            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1479            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1480                .await
1481                .unwrap();
1482
1483            // Write enough data to span multiple pages (PAGE_SIZE=103).
1484            let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
1485            append.append(&data).await.unwrap();
1486            append.sync().await.unwrap();
1487            // Add more in tip buffer.
1488            let more: Vec<u8> = (0u8..50).collect();
1489            append.append(&more).await.unwrap();
1490            assert_eq!(append.size(), 350);
1491
1492            let item_size = 10;
1493            let offsets: Vec<u64> = (0..35).map(|i| i * item_size as u64).collect();
1494            let mut batch_buf = vec![0u8; offsets.len() * item_size];
1495            append
1496                .read_many_into(&mut batch_buf, &offsets, NZUsize!(item_size))
1497                .await
1498                .unwrap();
1499
1500            // Compare each item against individual read_at.
1501            for (i, &off) in offsets.iter().enumerate() {
1502                let single = append.read_at(off, item_size).await.unwrap().coalesce();
1503                assert_eq!(
1504                    &batch_buf[i * item_size..(i + 1) * item_size],
1505                    single.as_ref(),
1506                    "mismatch at offset {off}",
1507                );
1508            }
1509        });
1510    }
1511
1512    #[test_traced("DEBUG")]
1513    fn test_read_many_into_scattered_cache_misses() {
1514        // Exercises all three source paths in a single read_many_into call:
1515        // tip buffer, page cache hit, and page cache miss (blob I/O).
1516        // The tip holds a partial page so one item straddles the tip boundary.
1517        let executor = deterministic::Runner::default();
1518        executor.start(|context: deterministic::Context| async move {
1519            let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
1520            // Small cache: only 2 pages, so we can force eviction.
1521            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
1522            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1523                .await
1524                .unwrap();
1525
1526            // Write 3 pages of data and sync to disk.
1527            let synced: Vec<u8> = (0u8..=255)
1528                .cycle()
1529                .take(PAGE_SIZE.get() as usize * 3)
1530                .collect();
1531            append.append(&synced).await.unwrap();
1532            append.sync().await.unwrap();
1533
1534            // Write a partial page that stays in the tip buffer. The item_size
1535            // is chosen so the last item straddles the synced/tip boundary.
1536            let item_size = 10;
1537            let tip_len = PAGE_SIZE.get() as usize / 2;
1538            let tip: Vec<u8> = (100u8..=255).cycle().take(tip_len).collect();
1539            append.append(&tip).await.unwrap();
1540
1541            // Prime pages 0 and 2 into cache, leaving page 1 uncached.
1542            let _ = append.read_at(0, item_size).await.unwrap();
1543            let _ = append
1544                .read_at(PAGE_SIZE.get() as u64 * 2, item_size)
1545                .await
1546                .unwrap();
1547
1548            // Offset that straddles the synced/tip boundary: starts in the last
1549            // synced page, ends in the tip buffer.
1550            let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
1551            let tip_off = synced.len() as u64 + item_size as u64;
1552            let offsets = [
1553                0u64,                       // page 0 (cached)
1554                PAGE_SIZE.get() as u64,     // page 1 (not cached - blob I/O)
1555                PAGE_SIZE.get() as u64 * 2, // page 2 (cached)
1556                straddle_off,               // straddles synced/tip boundary
1557                tip_off,                    // entirely in tip buffer
1558            ];
1559            let mut buf = vec![0u8; offsets.len() * item_size];
1560            append
1561                .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
1562                .await
1563                .unwrap();
1564
1565            let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
1566            for (i, &off) in offsets.iter().enumerate() {
1567                assert_eq!(
1568                    &buf[i * item_size..(i + 1) * item_size],
1569                    &read[off as usize..off as usize + item_size],
1570                );
1571            }
1572        });
1573    }
1574
1575    #[test_traced("DEBUG")]
1576    fn test_read_many_into_straddle_prefix_miss() {
1577        // A straddling item whose synced prefix page is NOT in the page cache: the
1578        // suffix is copied from the tip buffer and the prefix is read from the blob
1579        // without clobbering it, and the item is counted as a blob read.
1580        let executor = deterministic::Runner::default();
1581        executor.start(|context: deterministic::Context| async move {
1582            let (blob, blob_size) = context
1583                .open("test_partition", b"rmany_smiss")
1584                .await
1585                .unwrap();
1586            // Single-page cache so residency is deterministic.
1587            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(1));
1588            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1589                .await
1590                .unwrap();
1591
1592            // Write 3 pages and sync, then a partial page that stays in the tip.
1593            let synced: Vec<u8> = (0u8..=255)
1594                .cycle()
1595                .take(PAGE_SIZE.get() as usize * 3)
1596                .collect();
1597            append.append(&synced).await.unwrap();
1598            append.sync().await.unwrap();
1599            let item_size = 10;
1600            let tip: Vec<u8> = (100u8..=255)
1601                .cycle()
1602                .take(PAGE_SIZE.get() as usize / 2)
1603                .collect();
1604            append.append(&tip).await.unwrap();
1605
1606            // Fault page 0 in, evicting whatever sync left resident, so the straddle
1607            // prefix page (page 2) is guaranteed not cached.
1608            let _ = append.read_at(0, item_size).await.unwrap();
1609
1610            let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
1611            let tip_off = synced.len() as u64 + item_size as u64;
1612            let offsets = [straddle_off, tip_off];
1613            let mut buf = vec![0u8; offsets.len() * item_size];
1614            let hits = append
1615                .read_many_into(&mut buf, &offsets, NZUsize!(item_size))
1616                .await
1617                .unwrap();
1618
1619            // The tip-only item is a hit; the straddle item required a blob read.
1620            assert_eq!(hits, 1);
1621            let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
1622            for (i, &off) in offsets.iter().enumerate() {
1623                assert_eq!(
1624                    &buf[i * item_size..(i + 1) * item_size],
1625                    &read[off as usize..off as usize + item_size],
1626                );
1627            }
1628        });
1629    }
1630
1631    #[test_traced("DEBUG")]
1632    fn test_append_crc_empty() {
1633        let executor = deterministic::Runner::default();
1634        executor.start(|context: deterministic::Context| async move {
1635            // Open a new blob.
1636            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1637            assert_eq!(blob_size, 0);
1638
1639            // Create a page cache reference.
1640            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1641
1642            // Create a Writer.
1643            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1644                .await
1645                .unwrap();
1646
1647            // Verify initial size is 0.
1648            assert_eq!(append.size(), 0);
1649
1650            // Close & re-open.
1651            append.sync().await.unwrap();
1652            drop(append);
1653
1654            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1655            assert_eq!(blob_size, 0); // There was no need to write a crc since there was no data.
1656
1657            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1658                .await
1659                .unwrap();
1660
1661            assert_eq!(append.size(), 0);
1662        });
1663    }
1664
1665    #[test_traced("DEBUG")]
1666    fn test_append_crc_basic() {
1667        let executor = deterministic::Runner::default();
1668        executor.start(|context: deterministic::Context| async move {
1669            // Open a new blob.
1670            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1671            assert_eq!(blob_size, 0);
1672
1673            // Create a page cache reference.
1674            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1675
1676            // Create a Writer.
1677            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1678                .await
1679                .unwrap();
1680
1681            // Verify initial size is 0.
1682            assert_eq!(append.size(), 0);
1683
1684            // Append some bytes.
1685            let data = vec![1, 2, 3, 4, 5];
1686            append.append(&data).await.unwrap();
1687
1688            // Verify size reflects appended data.
1689            assert_eq!(append.size(), 5);
1690
1691            // Append more bytes.
1692            let more_data = vec![6, 7, 8, 9, 10];
1693            append.append(&more_data).await.unwrap();
1694
1695            // Verify size is cumulative.
1696            assert_eq!(append.size(), 10);
1697
1698            // Read back the first chunk and verify.
1699            let read_buf = append.read_at(0, 5).await.unwrap().coalesce();
1700            assert_eq!(read_buf, &data[..]);
1701
1702            // Read back the second chunk and verify.
1703            let read_buf = append.read_at(5, 5).await.unwrap().coalesce();
1704            assert_eq!(read_buf, &more_data[..]);
1705
1706            // Read all data at once and verify.
1707            let read_buf = append.read_at(0, 10).await.unwrap().coalesce();
1708            assert_eq!(read_buf, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
1709
1710            // Close and reopen the blob and make sure the data is still there and the trailing
1711            // checksum is written & stripped as expected.
1712            append.sync().await.unwrap();
1713            drop(append);
1714
1715            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1716            // Physical page = 103 logical + 12 Checksum = 115 bytes (padded partial page)
1717            assert_eq!(blob_size, 115);
1718            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1719                .await
1720                .unwrap();
1721            assert_eq!(append.size(), 10); // CRC should be stripped after verification
1722
1723            // Append data that spans a page boundary.
1724            // PAGE_SIZE=103 is the logical page size. We have 10 bytes, so writing
1725            // 100 more bytes (total 110) will cross the page boundary at byte 103.
1726            let spanning_data: Vec<u8> = (11..=110).collect();
1727            append.append(&spanning_data).await.unwrap();
1728            assert_eq!(append.size(), 110);
1729
1730            // Read back data that spans the page boundary.
1731            let read_buf = append.read_at(10, 100).await.unwrap().coalesce();
1732            assert_eq!(read_buf, &spanning_data[..]);
1733
1734            // Read all 110 bytes at once.
1735            let read_buf = append.read_at(0, 110).await.unwrap().coalesce();
1736            let expected: Vec<u8> = (1..=110).collect();
1737            assert_eq!(read_buf, &expected[..]);
1738
1739            // Drop and re-open and make sure bytes are still there.
1740            append.sync().await.unwrap();
1741            drop(append);
1742
1743            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1744            // 2 physical pages: 2 * 115 = 230 bytes
1745            assert_eq!(blob_size, 230);
1746            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1747                .await
1748                .unwrap();
1749            assert_eq!(append.size(), 110);
1750
1751            // Append data to reach exactly a page boundary.
1752            // Logical page size is 103. We have 110 bytes, next boundary is 206 (103 * 2).
1753            // So we need 96 more bytes.
1754            let boundary_data: Vec<u8> = (111..=206).collect();
1755            assert_eq!(boundary_data.len(), 96);
1756            append.append(&boundary_data).await.unwrap();
1757            assert_eq!(append.size(), 206);
1758
1759            // Verify we can read it back.
1760            let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
1761            let expected: Vec<u8> = (1..=206).collect();
1762            assert_eq!(read_buf, &expected[..]);
1763
1764            // Drop and re-open at the page boundary.
1765            append.sync().await.unwrap();
1766            drop(append);
1767
1768            let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
1769            // Physical size should be exactly 2 pages: 115 * 2 = 230 bytes
1770            assert_eq!(blob_size, 230);
1771            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1772                .await
1773                .unwrap();
1774            assert_eq!(append.size(), 206);
1775
1776            // Verify data is still readable after reopen.
1777            let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
1778            assert_eq!(read_buf, &expected[..]);
1779        });
1780    }
1781
1782    #[test_traced("DEBUG")]
1783    fn test_append_owned_bypass_from_empty_tip() {
1784        // A large owned append from an empty, page-aligned tip writes whole pages directly to the
1785        // blob and leaves the partial-page suffix buffered.
1786        let executor = deterministic::Runner::default();
1787        executor.start(|context: deterministic::Context| async move {
1788            let (blob, blob_size) = context
1789                .open("test_partition", b"owned_empty")
1790                .await
1791                .unwrap();
1792            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1793            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1794                .await
1795                .unwrap();
1796
1797            // 500 bytes = 4 full pages (412 bytes) + 88-byte remainder.
1798            let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
1799            let src = IoBuf::from(data.clone());
1800            let src_start = src.as_ptr() as usize;
1801            let src_range = src_start..src_start + src.len();
1802            append.append_owned(src.clone()).await.unwrap();
1803            assert_eq!(append.size(), 500);
1804
1805            // The buffered suffix is a copy, not a view that would pin the input allocation.
1806            let tip_ptr = append.buffer.as_ref().as_ptr() as usize;
1807            assert!(!src_range.contains(&tip_ptr));
1808
1809            // The directly written pages populate the page cache, exactly as a buffered flush
1810            // would.
1811            let mut probe = vec![0u8; PAGE_SIZE.get() as usize];
1812            assert_eq!(
1813                append.cache_ref.read_cached(append.id, &mut probe, 0),
1814                PAGE_SIZE.get() as usize
1815            );
1816            assert_eq!(probe, &data[..PAGE_SIZE.get() as usize]);
1817
1818            // All bytes are readable before any sync (bulk from the cache, suffix from tip).
1819            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1820            assert_eq!(read_buf, &data[..]);
1821
1822            // Everything becomes durable with a single sync.
1823            append.sync().await.unwrap();
1824            drop(append);
1825
1826            let (blob, blob_size) = context
1827                .open("test_partition", b"owned_empty")
1828                .await
1829                .unwrap();
1830            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1831                .await
1832                .unwrap();
1833            assert_eq!(append.size(), 500);
1834            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1835            assert_eq!(read_buf, &data[..]);
1836        });
1837    }
1838
1839    #[test_traced("DEBUG")]
1840    fn test_append_owned_bypass_with_synced_partial_page() {
1841        // A large owned append on top of a synced partial page must run the protected-CRC
1842        // handling for the first page before writing the bulk directly.
1843        let executor = deterministic::Runner::default();
1844        executor.start(|context: deterministic::Context| async move {
1845            let (blob, blob_size) = context
1846                .open("test_partition", b"owned_partial")
1847                .await
1848                .unwrap();
1849            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1850            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1851                .await
1852                .unwrap();
1853
1854            // Durably write a 50-byte partial page.
1855            let all: Vec<u8> = (0..500).map(|i| (i % 247) as u8).collect();
1856            append.append(&all[..50]).await.unwrap();
1857            append.sync().await.unwrap();
1858
1859            // 450 more bytes: 53 fill the first page (protected CRC), 3 whole pages (309 bytes)
1860            // bypass the buffer, 88 remain in the tip.
1861            append
1862                .append_owned(IoBuf::from(all[50..].to_vec()))
1863                .await
1864                .unwrap();
1865            assert_eq!(append.size(), 500);
1866            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1867            assert_eq!(read_buf, &all[..]);
1868
1869            // The direct write is not durable until sync: dropping without one preserves only the
1870            // synced 50-byte prefix.
1871            drop(append);
1872            let (blob, blob_size) = context
1873                .open("test_partition", b"owned_partial")
1874                .await
1875                .unwrap();
1876            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1877                .await
1878                .unwrap();
1879            assert_eq!(append.size(), 50);
1880            let read_buf = append.read_at(0, 50).await.unwrap().coalesce();
1881            assert_eq!(read_buf, &all[..50]);
1882
1883            // Repeating the owned append after recovery and syncing makes everything durable,
1884            // exercising the protected-CRC handling for the recovered partial page.
1885            append
1886                .append_owned(IoBuf::from(all[50..].to_vec()))
1887                .await
1888                .unwrap();
1889            append.sync().await.unwrap();
1890            drop(append);
1891
1892            let (blob, blob_size) = context
1893                .open("test_partition", b"owned_partial")
1894                .await
1895                .unwrap();
1896            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1897                .await
1898                .unwrap();
1899            assert_eq!(append.size(), 500);
1900            let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
1901            assert_eq!(read_buf, &all[..]);
1902        });
1903    }
1904
1905    #[test_traced("DEBUG")]
1906    fn test_append_owned_bypass_with_buffered_tip() {
1907        // A large owned append merges with unsynced buffered bytes: the fill completes the
1908        // current page, the bulk bypasses the buffer, and everything is readable.
1909        let executor = deterministic::Runner::default();
1910        executor.start(|context: deterministic::Context| async move {
1911            let (blob, blob_size) = context
1912                .open("test_partition", b"owned_buffered")
1913                .await
1914                .unwrap();
1915            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1916            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1917                .await
1918                .unwrap();
1919
1920            let all: Vec<u8> = (0..430).map(|i| (i % 239) as u8).collect();
1921            append.append(&all[..30]).await.unwrap();
1922            append
1923                .append_owned(IoBuf::from(all[30..].to_vec()))
1924                .await
1925                .unwrap();
1926            assert_eq!(append.size(), 430);
1927            let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
1928            assert_eq!(read_buf, &all[..]);
1929
1930            append.sync().await.unwrap();
1931            drop(append);
1932
1933            let (blob, blob_size) = context
1934                .open("test_partition", b"owned_buffered")
1935                .await
1936                .unwrap();
1937            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1938                .await
1939                .unwrap();
1940            assert_eq!(append.size(), 430);
1941            let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
1942            assert_eq!(read_buf, &all[..]);
1943        });
1944    }
1945
1946    #[test_traced("DEBUG")]
1947    fn test_append_owned_exact_page_multiple_and_small() {
1948        // An owned append of an exact page multiple leaves an empty tip that later buffered and
1949        // small owned appends continue from; small owned appends use the buffered path.
1950        let executor = deterministic::Runner::default();
1951        executor.start(|context: deterministic::Context| async move {
1952            let (blob, blob_size) = context
1953                .open("test_partition", b"owned_exact")
1954                .await
1955                .unwrap();
1956            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
1957            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
1958                .await
1959                .unwrap();
1960
1961            // Exactly 4 pages: no remainder.
1962            let bulk: Vec<u8> = (0..412).map(|i| (i % 233) as u8).collect();
1963            append
1964                .append_owned(IoBuf::from(bulk.clone()))
1965                .await
1966                .unwrap();
1967            assert_eq!(append.size(), 412);
1968
1969            // A small owned append takes the buffered path.
1970            let small: Vec<u8> = (0..10).map(|i| (i % 229) as u8).collect();
1971            append
1972                .append_owned(IoBuf::from(small.clone()))
1973                .await
1974                .unwrap();
1975            assert_eq!(append.size(), 422);
1976
1977            let read_buf = append.read_at(0, 422).await.unwrap().coalesce();
1978            assert_eq!(&read_buf.as_ref()[..412], &bulk[..]);
1979            assert_eq!(&read_buf.as_ref()[412..], &small[..]);
1980
1981            append.sync().await.unwrap();
1982            drop(append);
1983
1984            let (blob, blob_size) = context
1985                .open("test_partition", b"owned_exact")
1986                .await
1987                .unwrap();
1988            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
1989                .await
1990                .unwrap();
1991            assert_eq!(append.size(), 422);
1992        });
1993    }
1994
1995    #[test_traced("DEBUG")]
1996    fn test_append_owned_physical_bytes_match_buffered() {
1997        // The direct path must produce byte-identical physical output (page layout, CRC slot
1998        // placement, zero padding) to the buffered path for the same logical content.
1999        let executor = deterministic::Runner::default();
2000        executor.start(|context: deterministic::Context| async move {
2001            let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
2002            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2003
2004            let (blob, size) = context
2005                .open("test_partition", b"phys_direct")
2006                .await
2007                .unwrap();
2008            let mut direct = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
2009                .await
2010                .unwrap();
2011            direct
2012                .append_owned(IoBuf::from(data.clone()))
2013                .await
2014                .unwrap();
2015            direct.sync().await.unwrap();
2016            drop(direct);
2017
2018            // Small appends always stay on the buffered path and force intermediate flushes.
2019            let (blob, size) = context
2020                .open("test_partition", b"phys_buffered")
2021                .await
2022                .unwrap();
2023            let mut buffered = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
2024                .await
2025                .unwrap();
2026            for chunk in data.chunks(10) {
2027                buffered.append(chunk).await.unwrap();
2028            }
2029            buffered.sync().await.unwrap();
2030            drop(buffered);
2031
2032            let (blob_a, size_a) = context
2033                .open("test_partition", b"phys_direct")
2034                .await
2035                .unwrap();
2036            let (blob_b, size_b) = context
2037                .open("test_partition", b"phys_buffered")
2038                .await
2039                .unwrap();
2040            assert_eq!(size_a, size_b);
2041            let bytes_a = blob_a.read_at(0, size_a as usize).await.unwrap().coalesce();
2042            let bytes_b = blob_b.read_at(0, size_b as usize).await.unwrap().coalesce();
2043            assert_eq!(bytes_a.as_ref(), bytes_b.as_ref());
2044        });
2045    }
2046
2047    #[test_traced("DEBUG")]
2048    fn test_append_borrowed_large_takes_direct_path() {
2049        // A plain `append` larger than the write buffer is routed through the direct path, so the
2050        // write buffer holds only the partial-page suffix afterwards instead of the whole input.
2051        let executor = deterministic::Runner::default();
2052        executor.start(|context: deterministic::Context| async move {
2053            let (blob, blob_size) = context
2054                .open("test_partition", b"borrowed_large")
2055                .await
2056                .unwrap();
2057            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2058            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
2059                .await
2060                .unwrap();
2061
2062            // Start misaligned with a small buffered prefix.
2063            let all: Vec<u8> = (0..530).map(|i| (i % 241) as u8).collect();
2064            append.append(&all[..30]).await.unwrap();
2065
2066            // 500 more bytes exceed the 206-byte write buffer and take the direct path.
2067            append.append(&all[30..]).await.unwrap();
2068            assert_eq!(append.size(), 530);
2069
2070            // Only the partial-page suffix remains buffered (530 = 5 full pages + 15 bytes).
2071            assert_eq!(append.buffer.len(), 15);
2072
2073            let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2074            assert_eq!(read_buf, &all[..]);
2075
2076            append.sync().await.unwrap();
2077            drop(append);
2078
2079            let (blob, blob_size) = context
2080                .open("test_partition", b"borrowed_large")
2081                .await
2082                .unwrap();
2083            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2084                .await
2085                .unwrap();
2086            assert_eq!(append.size(), 530);
2087            let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
2088            assert_eq!(read_buf, &all[..]);
2089        });
2090    }
2091
2092    #[test_traced("DEBUG")]
2093    fn test_sync_releases_tip_pool_slot_after_full_drain() {
2094        let executor = deterministic::Runner::default();
2095        executor.start(|context: deterministic::Context| async move {
2096            let mut registry = Registry::default();
2097            let pool = BufferPool::new(
2098                BufferPoolConfig::for_storage()
2099                    .with_pool_min_size(PAGE_SIZE.get() as usize)
2100                    .with_max_per_class(NZU32!(2)),
2101                &mut registry,
2102            );
2103            let cache_ref = CacheRef::new(pool.clone(), PAGE_SIZE, NZUsize!(1));
2104
2105            let (blob, blob_size) = context
2106                .open("test_partition", b"release_tip_backing")
2107                .await
2108                .unwrap();
2109            assert_eq!(blob_size, 0);
2110
2111            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2112                .await
2113                .unwrap();
2114
2115            append
2116                .append(&vec![7; PAGE_SIZE.get() as usize])
2117                .await
2118                .unwrap();
2119
2120            // One pooled slot backs the page cache and one backs the mutable tip.
2121            assert!(
2122                matches!(
2123                    pool.try_alloc(BUFFER_SIZE),
2124                    Err(crate::iobuf::PoolError::Exhausted)
2125                ),
2126                "full-page tip should occupy the remaining pooled slot before sync"
2127            );
2128
2129            append.sync().await.unwrap();
2130
2131            // After a full drain, the tip should no longer pin that slot.
2132            assert!(
2133                pool.try_alloc(BUFFER_SIZE).is_ok(),
2134                "sync should release pooled backing when no partial tail remains"
2135            );
2136        });
2137    }
2138
2139    #[test_traced("DEBUG")]
2140    fn test_sync_uses_range_sync_for_single_flush() {
2141        let executor = deterministic::Runner::default();
2142        executor.start(|context: deterministic::Context| async move {
2143            let blob = SyncTrackingBlob::new();
2144            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2145            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2146                .await
2147                .unwrap();
2148
2149            // A newly wrapped blob preserves one full barrier before range sync is used.
2150            append.sync().await.unwrap();
2151            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2152            assert_eq!(writes, 0);
2153            assert_eq!(full_syncs, 1);
2154            assert_eq!(range_syncs, 0);
2155
2156            // A single buffered write with no remaining dirty state can be made durable directly.
2157            let data = b"hello world";
2158            append.append(data).await.unwrap();
2159            append.sync().await.unwrap();
2160
2161            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2162            assert_eq!(writes, 1);
2163            assert_eq!(full_syncs, 1);
2164            assert_eq!(range_syncs, 1);
2165
2166            // With no new writes and no pending full-sync barrier, sync has no work left.
2167            append.sync().await.unwrap();
2168            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2169            assert_eq!(writes, 1);
2170            assert_eq!(full_syncs, 1);
2171            assert_eq!(range_syncs, 1);
2172
2173            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2174            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2175                .await
2176                .unwrap();
2177            let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
2178            assert_eq!(read.as_ref(), data);
2179        });
2180    }
2181
2182    #[test_traced("DEBUG")]
2183    // Verifies a successful start_sync marks the writer clean.
2184    fn test_start_sync_persists_and_marks_clean() {
2185        let executor = deterministic::Runner::default();
2186        executor.start(|context: deterministic::Context| async move {
2187            let blob = SyncTrackingBlob::new();
2188            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2189            let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2190                .await
2191                .unwrap();
2192
2193            // A fresh writer is dirty, so start_sync does one full fsync; nothing is buffered to write.
2194            let handle = writer.start_sync().await;
2195            // Let the started sync finish.
2196            handle.await.unwrap();
2197            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2198            assert_eq!(writes, 0);
2199            assert_eq!(full_syncs, 1);
2200            assert_eq!(range_syncs, 0);
2201
2202            // Now clean, so the next write syncs just its range instead of the whole blob.
2203            let data = b"hello world";
2204            writer.append(data).await.unwrap();
2205            writer.sync().await.unwrap();
2206            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2207            assert_eq!(writes, 1);
2208            assert_eq!(full_syncs, 1);
2209            assert_eq!(range_syncs, 1);
2210
2211            // Nothing left to sync, so start_sync does nothing.
2212            let handle = writer.start_sync().await;
2213            handle.await.unwrap();
2214            let (_, _, full_syncs, range_syncs) = blob.snapshot();
2215            assert_eq!(full_syncs, 1);
2216            assert_eq!(range_syncs, 1);
2217
2218            // Durable and readable after reopening.
2219            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2220            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2221                .await
2222                .unwrap();
2223            let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
2224            assert_eq!(read.as_ref(), data);
2225        });
2226    }
2227
2228    #[test_traced("DEBUG")]
2229    // Verifies sync waits for a pending start_sync with no new writes.
2230    fn test_sync_waits_for_outstanding_start_sync() {
2231        let executor = deterministic::Runner::default();
2232        executor.start(|context: deterministic::Context| async move {
2233            let inner = SyncTrackingBlob::new();
2234            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2235            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2236            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2237
2238            let handle = writer.start_sync().await;
2239            let deferred = next_pending_sync(&pending);
2240
2241            // Try to sync while the started sync is still blocked.
2242            let mut sync = Box::pin(writer.sync());
2243            assert!(
2244                sync.as_mut().now_or_never().is_none(),
2245                "sync must wait for the outstanding start_sync handle"
2246            );
2247            drop(sync);
2248            let (_, _, full_syncs, range_syncs) = inner.snapshot();
2249            assert_eq!(full_syncs, 0);
2250            assert_eq!(range_syncs, 0);
2251
2252            // Release the started sync and retry.
2253            deferred.release.send(Ok(())).unwrap();
2254            writer.sync().await.unwrap();
2255            handle.await.unwrap();
2256            let (_, _, full_syncs, range_syncs) = inner.snapshot();
2257            assert_eq!(full_syncs, 1);
2258            assert_eq!(range_syncs, 0);
2259        });
2260    }
2261
2262    #[test_traced("DEBUG")]
2263    // Verifies a small buffered write cannot range-sync before pending start_sync finishes.
2264    fn test_sync_after_start_sync_and_small_write_waits_before_range_sync() {
2265        let executor = deterministic::Runner::default();
2266        executor.start(|context: deterministic::Context| async move {
2267            let inner = SyncTrackingBlob::new();
2268            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2269            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2270            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2271
2272            let handle = writer.start_sync().await;
2273            let deferred = next_pending_sync(&pending);
2274            writer.append(b"hello world").await.unwrap();
2275
2276            // Sync must wait before flushing the buffered write.
2277            let mut sync = Box::pin(writer.sync());
2278            assert!(
2279                sync.as_mut().now_or_never().is_none(),
2280                "sync must join the outstanding barrier before flushing the small write"
2281            );
2282            drop(sync);
2283            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2284            assert_eq!(writes, 0);
2285            assert_eq!(full_syncs, 0);
2286            assert_eq!(range_syncs, 0);
2287
2288            // Release the started sync, then flush the buffered write.
2289            deferred.release.send(Ok(())).unwrap();
2290            writer.sync().await.unwrap();
2291            handle.await.unwrap();
2292            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2293            assert_eq!(writes, 1);
2294            assert_eq!(full_syncs, 1);
2295            assert_eq!(range_syncs, 1);
2296        });
2297    }
2298
2299    #[test_traced("DEBUG")]
2300    // Verifies a large append cannot flush before pending start_sync finishes.
2301    fn test_write_flush_waits_for_outstanding_start_sync() {
2302        let executor = deterministic::Runner::default();
2303        executor.start(|context: deterministic::Context| async move {
2304            let inner = SyncTrackingBlob::new();
2305            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2306            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2307            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2308
2309            let handle = writer.start_sync().await;
2310            let deferred = next_pending_sync(&pending);
2311
2312            let data = vec![7; BUFFER_SIZE + PAGE_SIZE.get() as usize];
2313            let append = context.child("append").spawn(move |_| async move {
2314                writer.append(&data).await.unwrap();
2315                writer
2316            });
2317            // The append has reached the pending sync wait.
2318            deferred
2319                .blocked
2320                .await
2321                .expect("append never waited on start_sync");
2322            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2323            assert_eq!(writes, 0);
2324            assert_eq!(full_syncs, 0);
2325            assert_eq!(range_syncs, 0);
2326
2327            // Release the started sync so the append can flush.
2328            deferred.release.send(Ok(())).unwrap();
2329            let mut writer = append.await.unwrap();
2330            handle.await.unwrap();
2331            writer.sync().await.unwrap();
2332            let (_, writes, full_syncs, _) = inner.snapshot();
2333            assert!(writes > 0);
2334            assert!(full_syncs > 0);
2335        });
2336    }
2337
2338    #[test_traced("DEBUG")]
2339    // Verifies seal cannot flush buffered bytes before pending start_sync finishes.
2340    fn test_seal_waits_for_outstanding_start_sync_before_flushing() {
2341        let executor = deterministic::Runner::default();
2342        executor.start(|context: deterministic::Context| async move {
2343            let inner = SyncTrackingBlob::new();
2344            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2345            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2346            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2347
2348            let prior = writer.start_sync().await;
2349            let deferred = next_pending_sync(&pending);
2350            writer.append(b"hello world").await.unwrap();
2351
2352            let seal = context
2353                .child("seal")
2354                .spawn(move |_| async move { writer.seal().await });
2355            // The seal has reached the pending sync wait.
2356            deferred
2357                .blocked
2358                .await
2359                .expect("seal never waited on start_sync");
2360            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2361            assert_eq!(writes, 0);
2362            assert_eq!(full_syncs, 0);
2363            assert_eq!(range_syncs, 0);
2364
2365            // Release the started sync so seal can flush.
2366            deferred.release.send(Ok(())).unwrap();
2367            let sealed = seal.await.unwrap().unwrap();
2368            prior.await.unwrap();
2369            let read = sealed
2370                .read_at(0, b"hello world".len())
2371                .await
2372                .unwrap()
2373                .coalesce();
2374            assert_eq!(read.as_ref(), b"hello world");
2375            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2376            assert_eq!(writes, 1);
2377            assert_eq!(full_syncs, 1);
2378            assert_eq!(range_syncs, 0);
2379        });
2380    }
2381
2382    #[test_traced("DEBUG")]
2383    // Verifies snapshot cannot flush buffered bytes before pending start_sync finishes.
2384    fn test_snapshot_waits_for_outstanding_start_sync_before_flushing() {
2385        let executor = deterministic::Runner::default();
2386        executor.start(|context: deterministic::Context| async move {
2387            let inner = SyncTrackingBlob::new();
2388            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2389            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2390            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2391
2392            // Start a sync, then buffer newer bytes not covered by it.
2393            let prior = writer.start_sync().await;
2394            let deferred = next_pending_sync(&pending);
2395            writer.append(b"hello world").await.unwrap();
2396
2397            let snapshot = context.child("snapshot").spawn(move |_| async move {
2398                let snapshot = writer.snapshot().await.unwrap();
2399                let read = snapshot
2400                    .read_at(0, b"hello world".len())
2401                    .await
2402                    .unwrap()
2403                    .coalesce();
2404                assert_eq!(read.as_ref(), b"hello world");
2405                writer
2406            });
2407
2408            // Snapshot must wait before flushing buffered bytes.
2409            deferred
2410                .blocked
2411                .await
2412                .expect("snapshot never waited on start_sync");
2413            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2414            assert_eq!(writes, 0);
2415            assert_eq!(full_syncs, 0);
2416            assert_eq!(range_syncs, 0);
2417
2418            // Releasing the sync lets snapshot flush and read the buffered bytes.
2419            deferred.release.send(Ok(())).unwrap();
2420            let _writer = snapshot.await.unwrap();
2421            prior.await.unwrap();
2422            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2423            assert_eq!(writes, 1);
2424            assert_eq!(full_syncs, 1);
2425            assert_eq!(range_syncs, 0);
2426        });
2427    }
2428
2429    #[test_traced("DEBUG")]
2430    // Verifies replay cannot flush buffered bytes before pending start_sync finishes.
2431    fn test_replay_waits_for_outstanding_start_sync_before_flushing() {
2432        let executor = deterministic::Runner::default();
2433        executor.start(|context: deterministic::Context| async move {
2434            let inner = SyncTrackingBlob::new();
2435            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2436            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2437            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2438
2439            // Start a sync, then buffer newer bytes not covered by it.
2440            let prior = writer.start_sync().await;
2441            let deferred = next_pending_sync(&pending);
2442            writer.append(b"hello world").await.unwrap();
2443
2444            let replay = context.child("replay").spawn(move |_| async move {
2445                {
2446                    let mut replay = writer.replay(NZUsize!(BUFFER_SIZE)).await.unwrap();
2447                    assert!(replay.ensure(1).await.unwrap());
2448                    assert_eq!(replay.chunk()[0], b'h');
2449                }
2450                writer
2451            });
2452
2453            // Replay must wait before flushing buffered bytes.
2454            deferred
2455                .blocked
2456                .await
2457                .expect("replay never waited on start_sync");
2458            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2459            assert_eq!(writes, 0);
2460            assert_eq!(full_syncs, 0);
2461            assert_eq!(range_syncs, 0);
2462
2463            // Releasing the sync lets replay flush and read the buffered bytes.
2464            deferred.release.send(Ok(())).unwrap();
2465            let _writer = replay.await.unwrap();
2466            prior.await.unwrap();
2467            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2468            assert_eq!(writes, 1);
2469            assert_eq!(full_syncs, 1);
2470            assert_eq!(range_syncs, 0);
2471        });
2472    }
2473
2474    #[test_traced("DEBUG")]
2475    // Verifies resize growth cannot write zeros before pending start_sync finishes.
2476    fn test_resize_grow_waits_for_outstanding_start_sync_before_writing() {
2477        let executor = deterministic::Runner::default();
2478        executor.start(|context: deterministic::Context| async move {
2479            let inner = SyncTrackingBlob::new();
2480            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2481            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2482            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2483
2484            // Start a sync before growing into the direct-write path.
2485            let prior = writer.start_sync().await;
2486            let deferred = next_pending_sync(&pending);
2487
2488            let target_size = (BUFFER_SIZE + PAGE_SIZE.get() as usize) as u64;
2489            let resize = context.child("resize_grow").spawn(move |_| async move {
2490                writer.resize(target_size).await.unwrap();
2491                writer
2492            });
2493
2494            // Growth must wait before writing zero-filled pages.
2495            deferred
2496                .blocked
2497                .await
2498                .expect("resize grow never waited on start_sync");
2499            let (_, writes, full_syncs, range_syncs) = inner.snapshot();
2500            assert_eq!(writes, 0);
2501            assert_eq!(full_syncs, 0);
2502            assert_eq!(range_syncs, 0);
2503
2504            // Releasing the sync lets the resize complete.
2505            deferred.release.send(Ok(())).unwrap();
2506            let mut writer = resize.await.unwrap();
2507            prior.await.unwrap();
2508            assert_eq!(writer.size(), target_size);
2509            writer.sync().await.unwrap();
2510            let (_, writes, full_syncs, _) = inner.snapshot();
2511            assert!(writes > 0);
2512            assert!(full_syncs > 0);
2513        });
2514    }
2515
2516    #[test_traced("DEBUG")]
2517    // Verifies shrink cannot resize the blob before pending start_sync finishes.
2518    fn test_resize_shrink_waits_for_outstanding_start_sync_before_resizing() {
2519        let executor = deterministic::Runner::default();
2520        executor.start(|context: deterministic::Context| async move {
2521            let inner = SyncTrackingBlob::new();
2522            let (blob, pending) = DelayedSyncBlob::new(inner.clone());
2523            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2524            let mut writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
2525
2526            // Build durable pages, then start a sync for a newer partial page.
2527            let data = vec![3; PAGE_SIZE.get() as usize * 2];
2528            writer.append(&data).await.unwrap();
2529            writer.sync().await.unwrap();
2530            writer.append(b"x").await.unwrap();
2531            let prior = writer.start_sync().await;
2532            let deferred = next_pending_sync(&pending);
2533            let physical_size = inner.size();
2534
2535            let resize = context.child("resize_shrink").spawn(move |_| async move {
2536                writer.resize(PAGE_SIZE.get() as u64).await.unwrap();
2537                writer
2538            });
2539
2540            // Shrink must wait before truncating the physical blob.
2541            deferred
2542                .blocked
2543                .await
2544                .expect("resize shrink never waited on start_sync");
2545            assert_eq!(
2546                inner.size(),
2547                physical_size,
2548                "resize must not shrink the blob before the pending sync finishes"
2549            );
2550
2551            // Releasing the sync lets the shrink truncate the blob.
2552            deferred.release.send(Ok(())).unwrap();
2553            let writer = resize.await.unwrap();
2554            prior.await.unwrap();
2555            assert_eq!(writer.size(), PAGE_SIZE.get() as u64);
2556            assert!(inner.size() < physical_size);
2557        });
2558    }
2559
2560    #[test_traced("DEBUG")]
2561    fn test_sync_failed_range_sync_does_not_mark_clean() {
2562        let executor = deterministic::Runner::default();
2563        executor.start(|context: deterministic::Context| async move {
2564            let name = b"failed_range_sync";
2565            let (blob, size) = context.open("test_partition", name).await.unwrap();
2566            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2567            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
2568                .await
2569                .unwrap();
2570
2571            // Keep the write buffered so sync attempts the clean `write_at_sync` path.
2572            append.append(b"abc").await.unwrap();
2573
2574            // Removing the blob makes the range-sync flush fail.
2575            context.remove("test_partition", Some(name)).await.unwrap();
2576            assert!(append.sync().await.is_err());
2577
2578            // The failed `write_at_sync` must leave a pending full-sync barrier, so a
2579            // later sync cannot report success.
2580            assert!(append.sync().await.is_err());
2581        });
2582    }
2583
2584    #[test_traced("DEBUG")]
2585    fn test_sync_uses_full_sync_after_prior_plain_flush() {
2586        let executor = deterministic::Runner::default();
2587        executor.start(|context: deterministic::Context| async move {
2588            let blob = SyncTrackingBlob::new();
2589            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2590            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2591                .await
2592                .unwrap();
2593
2594            // This append overflows the buffer, so a plain flush happens before sync writes the
2595            // remaining tip.
2596            let data = vec![7u8; BUFFER_SIZE + 1];
2597            append.append(&data).await.unwrap();
2598            append.sync().await.unwrap();
2599
2600            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2601            assert_eq!(writes, 2);
2602            assert_eq!(full_syncs, 1);
2603            assert_eq!(range_syncs, 0);
2604
2605            // With no new work, sync should not issue another durability operation.
2606            append.sync().await.unwrap();
2607            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2608            assert_eq!(writes, 2);
2609            assert_eq!(full_syncs, 1);
2610            assert_eq!(range_syncs, 0);
2611
2612            // The next sync still needs a full barrier because the append path flushed the full
2613            // page before the final partial tip.
2614            append.append(b"tip").await.unwrap();
2615            append.sync().await.unwrap();
2616
2617            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2618            assert_eq!(writes, 4);
2619            assert_eq!(full_syncs, 2);
2620            assert_eq!(range_syncs, 0);
2621
2622            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2623            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2624                .await
2625                .unwrap();
2626            let mut expected = data;
2627            expected.extend_from_slice(b"tip");
2628            let read = reopened
2629                .read_at(0, expected.len())
2630                .await
2631                .unwrap()
2632                .coalesce();
2633            assert_eq!(read.as_ref(), expected.as_slice());
2634        });
2635    }
2636
2637    #[test_traced("DEBUG")]
2638    fn test_sync_uses_full_sync_after_replay_plain_flush() {
2639        let executor = deterministic::Runner::default();
2640        executor.start(|context: deterministic::Context| async move {
2641            let blob = SyncTrackingBlob::new();
2642            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2643            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2644                .await
2645                .unwrap();
2646
2647            // Keep data buffered so replay has to flush it without syncing.
2648            append.append(b"replayed").await.unwrap();
2649
2650            // Replay flushes buffered data for reading, but does not make that write durable.
2651            let mut replay = append.replay(NZUsize!(1024)).await.unwrap();
2652            assert!(replay.ensure(b"replayed".len()).await.unwrap());
2653            assert_eq!(replay.remaining(), b"replayed".len());
2654            assert_eq!(replay.chunk(), b"replayed");
2655
2656            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2657            assert_eq!(writes, 1);
2658            assert_eq!(full_syncs, 0);
2659            assert_eq!(range_syncs, 0);
2660
2661            // A later sync must use a full barrier for the plain replay flush.
2662            append.sync().await.unwrap();
2663            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2664            assert_eq!(writes, 1);
2665            assert_eq!(full_syncs, 1);
2666            assert_eq!(range_syncs, 0);
2667        });
2668    }
2669
2670    #[test_traced("DEBUG")]
2671    fn test_recreated_sync_preserves_replay_plain_flush_barrier() {
2672        let executor = deterministic::Runner::default();
2673        executor.start(|context: deterministic::Context| async move {
2674            let blob = SyncTrackingBlob::new();
2675            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2676            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2677                .await
2678                .unwrap();
2679
2680            append.append(b"replayed").await.unwrap();
2681            let mut replay = append.replay(NZUsize!(1024)).await.unwrap();
2682            assert!(replay.ensure(b"replayed".len()).await.unwrap());
2683            assert_eq!(replay.remaining(), b"replayed".len());
2684            assert_eq!(replay.chunk(), b"replayed");
2685            drop(replay);
2686            drop(append);
2687
2688            let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
2689            assert!(durable.is_empty());
2690            assert_eq!(writes, 1);
2691            assert_eq!(full_syncs, 0);
2692            assert_eq!(range_syncs, 0);
2693
2694            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2695            let mut reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2696                .await
2697                .unwrap();
2698            assert_eq!(reopened.size(), b"replayed".len() as u64);
2699            reopened.sync().await.unwrap();
2700
2701            let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
2702            assert_eq!(durable.len(), blob.size() as usize);
2703            assert_eq!(writes, 1);
2704            assert_eq!(full_syncs, 1);
2705            assert_eq!(range_syncs, 0);
2706        });
2707    }
2708
2709    #[test_traced("DEBUG")]
2710    fn test_recreated_sync_skips_barrier_after_invalid_truncation() {
2711        let executor = deterministic::Runner::default();
2712        executor.start(|context: deterministic::Context| async move {
2713            let blob = SyncTrackingBlob::new();
2714            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2715            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2716                .await
2717                .unwrap();
2718            append.sync().await.unwrap();
2719            append.append(b"valid").await.unwrap();
2720            append.sync().await.unwrap();
2721            drop(append);
2722
2723            blob.write_at(blob.size(), b"junk").await.unwrap();
2724
2725            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2726            let mut reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2727                .await
2728                .unwrap();
2729            assert_eq!(reopened.size(), b"valid".len() as u64);
2730
2731            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2732            assert_eq!(writes, 2);
2733            assert_eq!(full_syncs, 2);
2734            assert_eq!(range_syncs, 1);
2735
2736            reopened.sync().await.unwrap();
2737
2738            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2739            assert_eq!(writes, 2);
2740            assert_eq!(full_syncs, 2);
2741            assert_eq!(range_syncs, 1);
2742        });
2743    }
2744
2745    #[test_traced("DEBUG")]
2746    fn test_sync_batches_split_protected_writes_with_full_sync() {
2747        let executor = deterministic::Runner::default();
2748        executor.start(|context: deterministic::Context| async move {
2749            let blob = SyncTrackingBlob::new();
2750            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2751            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
2752                .await
2753                .unwrap();
2754            append.sync().await.unwrap();
2755
2756            // Establish a persisted partial page with one authoritative CRC slot.
2757            append.append(b"abc").await.unwrap();
2758            append.sync().await.unwrap();
2759
2760            // Extending that partial page must write around the protected slot, so the two emitted
2761            // writes are batched behind one full sync.
2762            append.append(b"de").await.unwrap();
2763            append.sync().await.unwrap();
2764
2765            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2766            assert_eq!(writes, 3);
2767            assert_eq!(full_syncs, 2);
2768            assert_eq!(range_syncs, 1);
2769
2770            // On the next extension, the protected slot is the second CRC, so only the prefix
2771            // write is needed.
2772            append.append(b"fg").await.unwrap();
2773            append.sync().await.unwrap();
2774
2775            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
2776            assert_eq!(writes, 4);
2777            assert_eq!(full_syncs, 2);
2778            assert_eq!(range_syncs, 2);
2779
2780            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2781            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
2782                .await
2783                .unwrap();
2784            let read = reopened.read_at(0, 7).await.unwrap().coalesce();
2785            assert_eq!(read.as_ref(), b"abcdefg");
2786        });
2787    }
2788
2789    #[test_traced("DEBUG")]
2790    fn test_read_up_to_zero_len_truncates_buffer() {
2791        let executor = deterministic::Runner::default();
2792        executor.start(|context: deterministic::Context| async move {
2793            // Open a new blob.
2794            let (blob, blob_size) = context
2795                .open("test_partition", b"read_up_to_zero_len")
2796                .await
2797                .unwrap();
2798            assert_eq!(blob_size, 0);
2799
2800            // Create a page cache reference.
2801            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
2802
2803            // Create a Writer and write some data.
2804            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
2805                .await
2806                .unwrap();
2807            append.append(&[1, 2, 3, 4]).await.unwrap();
2808
2809            // Request a zero-length read with a reused, non-empty buffer.
2810            let stale = vec![9, 8, 7, 6];
2811            let (buf, read) = append.read_up_to(0, 0, stale).await.unwrap();
2812
2813            assert_eq!(read, 0);
2814            assert_eq!(buf.len(), 0, "read_up_to must truncate returned buffer");
2815            assert_eq!(buf.freeze().as_ref(), b"");
2816        });
2817    }
2818
2819    /// Helper to read the CRC record from raw blob bytes at the end of a physical page.
2820    fn read_crc_record_from_page(page_bytes: &[u8]) -> Checksum {
2821        let crc_start = page_bytes.len() - CHECKSUM_SIZE as usize;
2822        Checksum::read(&mut &page_bytes[crc_start..]).unwrap()
2823    }
2824
2825    /// Blob wrapper that turns one write into a durable partial write followed by an error.
2826    #[derive(Clone)]
2827    struct PartialWriteBlob<B: Blob> {
2828        inner: B,
2829        writes: Arc<AtomicUsize>,
2830        failed_write_len: Arc<AtomicUsize>,
2831        fail_on: usize,
2832        partial_len: usize,
2833    }
2834
2835    impl<B: Blob> PartialWriteBlob<B> {
2836        fn new(inner: B, fail_on: usize, partial_len: usize) -> Self {
2837            Self {
2838                inner,
2839                writes: Arc::new(AtomicUsize::new(0)),
2840                failed_write_len: Arc::new(AtomicUsize::new(0)),
2841                fail_on,
2842                partial_len,
2843            }
2844        }
2845
2846        fn failed_write_len(&self) -> Arc<AtomicUsize> {
2847            self.failed_write_len.clone()
2848        }
2849
2850        fn write_count(&self) -> Arc<AtomicUsize> {
2851            self.writes.clone()
2852        }
2853    }
2854
2855    impl<B: Blob> crate::Blob for PartialWriteBlob<B> {
2856        async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
2857            self.inner.read_at(offset, len).await
2858        }
2859
2860        async fn read_at_buf(
2861            &self,
2862            offset: u64,
2863            len: usize,
2864            bufs: impl Into<IoBufsMut> + Send,
2865        ) -> Result<IoBufsMut, Error> {
2866            self.inner.read_at_buf(offset, len, bufs).await
2867        }
2868
2869        async fn write_at(&self, offset: u64, bufs: impl Into<IoBufs> + Send) -> Result<(), Error> {
2870            let bufs = bufs.into();
2871            let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
2872            if write == self.fail_on {
2873                let bytes = bufs.coalesce();
2874                self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
2875                let partial_len = self.partial_len.min(bytes.len());
2876                self.inner
2877                    .write_at(offset, bytes.slice(..partial_len))
2878                    .await?;
2879                self.inner.sync().await?;
2880                return Err(Error::Io(
2881                    std::io::Error::other("injected partial write").into(),
2882                ));
2883            }
2884
2885            self.inner.write_at(offset, bufs).await
2886        }
2887
2888        async fn write_at_sync(
2889            &self,
2890            offset: u64,
2891            bufs: impl Into<IoBufs> + Send,
2892        ) -> Result<(), Error> {
2893            let bufs = bufs.into();
2894            let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
2895            if write == self.fail_on {
2896                let bytes = bufs.coalesce();
2897                self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
2898                let partial_len = self.partial_len.min(bytes.len());
2899                self.inner
2900                    .write_at_sync(offset, bytes.slice(..partial_len))
2901                    .await?;
2902                return Err(Error::Io(
2903                    std::io::Error::other("injected partial write").into(),
2904                ));
2905            }
2906
2907            self.inner.write_at_sync(offset, bufs).await
2908        }
2909
2910        async fn resize(&self, len: u64) -> Result<(), Error> {
2911            self.inner.resize(len).await
2912        }
2913
2914        async fn sync(&self) -> Result<(), Error> {
2915            self.inner.sync().await
2916        }
2917
2918        async fn start_sync(&self) -> Handle<()> {
2919            self.inner.start_sync().await
2920        }
2921    }
2922
2923    /// Blob wrapper that delays one selected read after capturing its current bytes.
2924    #[derive(Clone)]
2925    struct DelayedReadBlob<B: Blob> {
2926        inner: B,
2927        offset: u64,
2928        len: usize,
2929        reads: Arc<AtomicUsize>,
2930        started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
2931        release: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
2932    }
2933
2934    impl<B: Blob> DelayedReadBlob<B> {
2935        fn new(
2936            inner: B,
2937            offset: u64,
2938            len: usize,
2939            started: oneshot::Sender<()>,
2940            release: oneshot::Receiver<()>,
2941        ) -> Self {
2942            Self {
2943                inner,
2944                offset,
2945                len,
2946                reads: Arc::new(AtomicUsize::new(0)),
2947                started: Arc::new(Mutex::new(Some(started))),
2948                release: Arc::new(Mutex::new(Some(release))),
2949            }
2950        }
2951    }
2952
2953    impl<B: Blob> crate::Blob for DelayedReadBlob<B> {
2954        async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufsMut, Error> {
2955            if offset == self.offset
2956                && len == self.len
2957                && self.reads.fetch_add(1, Ordering::SeqCst) == 0
2958            {
2959                let bytes = self.inner.read_at(offset, len).await?;
2960
2961                let sender = self
2962                    .started
2963                    .lock()
2964                    .take()
2965                    .expect("delayed read start signal consumed more than once");
2966                let _ = sender.send(());
2967
2968                let release = self
2969                    .release
2970                    .lock()
2971                    .take()
2972                    .expect("delayed read release receiver consumed more than once");
2973                release.await.expect("release signal dropped");
2974
2975                return Ok(bytes);
2976            }
2977
2978            self.inner.read_at(offset, len).await
2979        }
2980
2981        async fn read_at_buf(
2982            &self,
2983            offset: u64,
2984            len: usize,
2985            bufs: impl Into<IoBufsMut> + Send,
2986        ) -> Result<IoBufsMut, Error> {
2987            if offset == self.offset
2988                && len == self.len
2989                && self.reads.fetch_add(1, Ordering::SeqCst) == 0
2990            {
2991                let bytes = self.inner.read_at_buf(offset, len, bufs).await?;
2992
2993                let sender = self
2994                    .started
2995                    .lock()
2996                    .take()
2997                    .expect("delayed read start signal consumed more than once");
2998                let _ = sender.send(());
2999
3000                let release = self
3001                    .release
3002                    .lock()
3003                    .take()
3004                    .expect("delayed read release receiver consumed more than once");
3005                release.await.expect("release signal dropped");
3006
3007                return Ok(bytes);
3008            }
3009
3010            self.inner.read_at_buf(offset, len, bufs).await
3011        }
3012
3013        async fn write_at(&self, offset: u64, bufs: impl Into<IoBufs> + Send) -> Result<(), Error> {
3014            self.inner.write_at(offset, bufs).await
3015        }
3016
3017        async fn write_at_sync(
3018            &self,
3019            offset: u64,
3020            bufs: impl Into<IoBufs> + Send,
3021        ) -> Result<(), Error> {
3022            self.inner.write_at_sync(offset, bufs).await
3023        }
3024
3025        async fn resize(&self, len: u64) -> Result<(), Error> {
3026            self.inner.resize(len).await
3027        }
3028
3029        async fn sync(&self) -> Result<(), Error> {
3030            self.inner.sync().await
3031        }
3032
3033        async fn start_sync(&self) -> Handle<()> {
3034            self.inner.start_sync().await
3035        }
3036    }
3037
3038    /// Dummy marker bytes with len=0 so the mangled slot is never authoritative.
3039    /// Format: [len_hi=0, len_lo=0, 0xDE, 0xAD, 0xBE, 0xEF]
3040    const DUMMY_MARKER: [u8; 6] = [0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
3041
3042    #[test]
3043    fn test_identify_protected_regions_equal_lengths() {
3044        // When lengths are equal, the first CRC should be protected (tie-breaking rule).
3045        let record = Checksum {
3046            len1: 50,
3047            crc1: 0xAAAAAAAA,
3048            len2: 50,
3049            crc2: 0xBBBBBBBB,
3050        };
3051
3052        let result =
3053            Writer::<crate::storage::memory::Blob>::identify_protected_regions(Some(&record));
3054        assert!(result.is_some());
3055        let (prefix_len, protected_crc) = result.unwrap();
3056        assert_eq!(prefix_len, 50);
3057        assert!(
3058            matches!(protected_crc, Slot::First),
3059            "First CRC should be protected when lengths are equal"
3060        );
3061    }
3062
3063    #[test]
3064    fn test_identify_protected_regions_len1_larger() {
3065        // When len1 > len2, the first CRC should be protected.
3066        let record = Checksum {
3067            len1: 100,
3068            crc1: 0xAAAAAAAA,
3069            len2: 50,
3070            crc2: 0xBBBBBBBB,
3071        };
3072
3073        let result =
3074            Writer::<crate::storage::memory::Blob>::identify_protected_regions(Some(&record));
3075        assert!(result.is_some());
3076        let (prefix_len, protected_crc) = result.unwrap();
3077        assert_eq!(prefix_len, 100);
3078        assert!(
3079            matches!(protected_crc, Slot::First),
3080            "First CRC should be protected when len1 > len2"
3081        );
3082    }
3083
3084    #[test]
3085    fn test_identify_protected_regions_len2_larger() {
3086        // When len2 > len1, the second CRC should be protected.
3087        let record = Checksum {
3088            len1: 50,
3089            crc1: 0xAAAAAAAA,
3090            len2: 100,
3091            crc2: 0xBBBBBBBB,
3092        };
3093
3094        let result =
3095            Writer::<crate::storage::memory::Blob>::identify_protected_regions(Some(&record));
3096        assert!(result.is_some());
3097        let (prefix_len, protected_crc) = result.unwrap();
3098        assert_eq!(prefix_len, 100);
3099        assert!(
3100            matches!(protected_crc, Slot::Second),
3101            "Second CRC should be protected when len2 > len1"
3102        );
3103    }
3104
3105    /// Test that `to_physical_pages` emits full pages zero-copy while still materializing the
3106    /// trailing partial page into one padded physical page.
3107    #[test_traced("DEBUG")]
3108    fn test_to_physical_pages_zero_copy_full_pages_and_materialized_partial() {
3109        // Build a tip buffer containing two full logical pages plus a trailing partial
3110        // page, convert it with `to_physical_pages`, then verify:
3111        // - the result is chunked rather than one contiguous buffer for the full-page portion
3112        // - the logical payload bytes for the first two pages are preserved in order
3113        // - the partial page is padded with zeros up to one full logical page
3114        // - all three resulting physical pages validate their CRC records
3115        let executor = deterministic::Runner::default();
3116        executor.start(|context: deterministic::Context| async move {
3117            // Open a new blob.
3118            let (blob, blob_size) = context
3119                .open("test_partition", b"to_physical_pages_zero_copy")
3120                .await
3121                .unwrap();
3122            assert_eq!(blob_size, 0);
3123
3124            // Create a page cache reference.
3125            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3126
3127            // Create a Writer.
3128            let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
3129                .await
3130                .unwrap();
3131
3132            // Build logical data with exactly two full pages followed by one trailing partial page.
3133            // This lets us verify that only the partial page is materialized.
3134            let logical_page_size = PAGE_SIZE.get() as usize;
3135            let partial_len = 17usize;
3136            let data: Vec<u8> = (0..(logical_page_size * 2 + partial_len))
3137                .map(|i| (i % 251) as u8)
3138                .collect();
3139
3140            // Seed a tip buffer with the logical bytes exactly as flush_internal would see them.
3141            let mut buffer = Buffer::new(0, data.len(), cache_ref.pool().clone());
3142            let over_capacity = buffer.append(&data);
3143            assert!(!over_capacity);
3144
3145            // Convert buffered logical bytes into physical-page writes.
3146            let (physical_pages, partial_page_state) =
3147                append.to_physical_pages(&buffer, true, None);
3148
3149            // Two full pages should each contribute a logical slice and a CRC slice, and the
3150            // trailing partial page should contribute one materialized padded physical page.
3151            assert_eq!(physical_pages.chunk_count(), 5);
3152
3153            // The returned partial-page CRC state must describe the exact trailing logical length.
3154            let crc_record = partial_page_state.expect("partial page state must be returned");
3155            let (len, _) = crc_record.get_crc();
3156            assert_eq!(len as usize, partial_len);
3157
3158            // Coalesce for easier content inspection. The assembled bytes should still form three
3159            // full physical pages on disk.
3160            let physical_page_size = logical_page_size + CHECKSUM_SIZE as usize;
3161            let coalesced = physical_pages.coalesce();
3162            assert_eq!(coalesced.len(), physical_page_size * 3);
3163
3164            // The first two physical pages must preserve the two full logical pages verbatim.
3165            assert_eq!(
3166                &coalesced.as_ref()[..logical_page_size],
3167                &data[..logical_page_size]
3168            );
3169            assert_eq!(
3170                &coalesced.as_ref()[physical_page_size..physical_page_size + logical_page_size],
3171                &data[logical_page_size..logical_page_size * 2],
3172            );
3173
3174            // The trailing partial page must contain the remaining logical bytes followed by zero
3175            // padding up to one full logical page.
3176            let partial_start = physical_page_size * 2;
3177            assert_eq!(
3178                &coalesced.as_ref()[partial_start..partial_start + partial_len],
3179                &data[logical_page_size * 2..],
3180            );
3181            assert!(coalesced.as_ref()
3182                [partial_start + partial_len..partial_start + logical_page_size]
3183                .iter()
3184                .all(|byte| *byte == 0));
3185
3186            // Each assembled physical page must carry a valid CRC record.
3187            assert!(Checksum::validate_page(&coalesced.as_ref()[..physical_page_size]).is_some());
3188            assert!(Checksum::validate_page(
3189                &coalesced.as_ref()[physical_page_size..physical_page_size * 2]
3190            )
3191            .is_some());
3192            assert!(Checksum::validate_page(
3193                &coalesced.as_ref()[physical_page_size * 2..physical_page_size * 3]
3194            )
3195            .is_some());
3196        });
3197    }
3198
3199    /// Test that slot 1 is NOT overwritten when it's the protected slot.
3200    ///
3201    /// Strategy: After extending twice (so slot 1 becomes authoritative with larger len),
3202    /// mangle the non-authoritative slot 0. Then extend again - slot 0 should be overwritten
3203    /// with the new CRC, while slot 1 (protected) should remain untouched.
3204    #[test_traced("DEBUG")]
3205    fn test_crc_slot1_protected() {
3206        let executor = deterministic::Runner::default();
3207        executor.start(|context: deterministic::Context| async move {
3208            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3209            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3210            let slot0_offset = PAGE_SIZE.get() as u64;
3211            let slot1_offset = PAGE_SIZE.get() as u64 + 6;
3212
3213            // === Step 1: Write 10 bytes → slot 0 authoritative (len=10) ===
3214            let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
3215            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3216                .await
3217                .unwrap();
3218            append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
3219            append.sync().await.unwrap();
3220            drop(append);
3221
3222            // === Step 2: Extend to 30 bytes → slot 1 authoritative (len=30) ===
3223            let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
3224            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3225                .await
3226                .unwrap();
3227            append
3228                .append(&(11..=30).collect::<Vec<u8>>())
3229                .await
3230                .unwrap();
3231            append.sync().await.unwrap();
3232            drop(append);
3233
3234            // Verify slot 1 is now authoritative
3235            let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
3236            let page = blob
3237                .read_at(0, physical_page_size)
3238                .await
3239                .unwrap()
3240                .coalesce();
3241            let crc = read_crc_record_from_page(page.as_ref());
3242            assert!(
3243                crc.len2 > crc.len1,
3244                "Slot 1 should be authoritative (len2={} > len1={})",
3245                crc.len2,
3246                crc.len1
3247            );
3248
3249            // Capture slot 1 bytes before mangling slot 0
3250            let slot1_before: Vec<u8> = blob
3251                .read_at(slot1_offset, 6)
3252                .await
3253                .unwrap()
3254                .coalesce()
3255                .freeze()
3256                .into();
3257
3258            // === Step 3: Mangle slot 0 (non-authoritative) ===
3259            blob.write_at(slot0_offset, DUMMY_MARKER.to_vec())
3260                .await
3261                .unwrap();
3262            blob.sync().await.unwrap();
3263
3264            // Verify mangle worked
3265            let slot0_mangled: Vec<u8> = blob
3266                .read_at(slot0_offset, 6)
3267                .await
3268                .unwrap()
3269                .coalesce()
3270                .freeze()
3271                .into();
3272            assert_eq!(slot0_mangled, DUMMY_MARKER, "Mangle failed");
3273
3274            // === Step 4: Extend to 50 bytes → new CRC goes to slot 0, slot 1 protected ===
3275            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3276                .await
3277                .unwrap();
3278            append
3279                .append(&(31..=50).collect::<Vec<u8>>())
3280                .await
3281                .unwrap();
3282            append.sync().await.unwrap();
3283            drop(append);
3284
3285            // === Step 5: Verify slot 0 was overwritten, slot 1 unchanged ===
3286            let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
3287
3288            // Slot 0 should have new CRC (not our dummy marker)
3289            let slot0_after: Vec<u8> = blob
3290                .read_at(slot0_offset, 6)
3291                .await
3292                .unwrap()
3293                .coalesce()
3294                .freeze()
3295                .into();
3296            assert_ne!(
3297                slot0_after, DUMMY_MARKER,
3298                "Slot 0 should have been overwritten with new CRC"
3299            );
3300
3301            // Slot 1 should be UNCHANGED (protected)
3302            let slot1_after: Vec<u8> = blob
3303                .read_at(slot1_offset, 6)
3304                .await
3305                .unwrap()
3306                .coalesce()
3307                .freeze()
3308                .into();
3309            assert_eq!(
3310                slot1_before, slot1_after,
3311                "Slot 1 was modified! Protected region violated."
3312            );
3313
3314            // Verify the new CRC in slot 0 has len=50
3315            let page = blob
3316                .read_at(0, physical_page_size)
3317                .await
3318                .unwrap()
3319                .coalesce();
3320            let crc = read_crc_record_from_page(page.as_ref());
3321            assert_eq!(crc.len1, 50, "Slot 0 should have len=50");
3322        });
3323    }
3324
3325    /// Test that slot 0 is NOT overwritten when it's the protected slot.
3326    ///
3327    /// Strategy: After extending three times (slot 0 becomes authoritative again with largest len),
3328    /// mangle the non-authoritative slot 1. Then extend again - slot 1 should be overwritten
3329    /// with the new CRC, while slot 0 (protected) should remain untouched.
3330    #[test_traced("DEBUG")]
3331    fn test_crc_slot0_protected() {
3332        let executor = deterministic::Runner::default();
3333        executor.start(|context: deterministic::Context| async move {
3334            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3335            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3336            let slot0_offset = PAGE_SIZE.get() as u64;
3337            let slot1_offset = PAGE_SIZE.get() as u64 + 6;
3338
3339            // === Step 1: Write 10 bytes → slot 0 authoritative (len=10) ===
3340            let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
3341            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3342                .await
3343                .unwrap();
3344            append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
3345            append.sync().await.unwrap();
3346            drop(append);
3347
3348            // === Step 2: Extend to 30 bytes → slot 1 authoritative (len=30) ===
3349            let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
3350            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3351                .await
3352                .unwrap();
3353            append
3354                .append(&(11..=30).collect::<Vec<u8>>())
3355                .await
3356                .unwrap();
3357            append.sync().await.unwrap();
3358            drop(append);
3359
3360            // === Step 3: Extend to 50 bytes → slot 0 authoritative (len=50) ===
3361            let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
3362            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3363                .await
3364                .unwrap();
3365            append
3366                .append(&(31..=50).collect::<Vec<u8>>())
3367                .await
3368                .unwrap();
3369            append.sync().await.unwrap();
3370            drop(append);
3371
3372            // Verify slot 0 is now authoritative
3373            let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
3374            let page = blob
3375                .read_at(0, physical_page_size)
3376                .await
3377                .unwrap()
3378                .coalesce();
3379            let crc = read_crc_record_from_page(page.as_ref());
3380            assert!(
3381                crc.len1 > crc.len2,
3382                "Slot 0 should be authoritative (len1={} > len2={})",
3383                crc.len1,
3384                crc.len2
3385            );
3386
3387            // Capture slot 0 bytes before mangling slot 1
3388            let slot0_before: Vec<u8> = blob
3389                .read_at(slot0_offset, 6)
3390                .await
3391                .unwrap()
3392                .coalesce()
3393                .freeze()
3394                .into();
3395
3396            // === Step 4: Mangle slot 1 (non-authoritative) ===
3397            blob.write_at(slot1_offset, DUMMY_MARKER.to_vec())
3398                .await
3399                .unwrap();
3400            blob.sync().await.unwrap();
3401
3402            // Verify mangle worked
3403            let slot1_mangled: Vec<u8> = blob
3404                .read_at(slot1_offset, 6)
3405                .await
3406                .unwrap()
3407                .coalesce()
3408                .freeze()
3409                .into();
3410            assert_eq!(slot1_mangled, DUMMY_MARKER, "Mangle failed");
3411
3412            // === Step 5: Extend to 70 bytes → new CRC goes to slot 1, slot 0 protected ===
3413            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3414                .await
3415                .unwrap();
3416            append
3417                .append(&(51..=70).collect::<Vec<u8>>())
3418                .await
3419                .unwrap();
3420            append.sync().await.unwrap();
3421            drop(append);
3422
3423            // === Step 6: Verify slot 1 was overwritten, slot 0 unchanged ===
3424            let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
3425
3426            // Slot 1 should have new CRC (not our dummy marker)
3427            let slot1_after: Vec<u8> = blob
3428                .read_at(slot1_offset, 6)
3429                .await
3430                .unwrap()
3431                .coalesce()
3432                .freeze()
3433                .into();
3434            assert_ne!(
3435                slot1_after, DUMMY_MARKER,
3436                "Slot 1 should have been overwritten with new CRC"
3437            );
3438
3439            // Slot 0 should be UNCHANGED (protected)
3440            let slot0_after: Vec<u8> = blob
3441                .read_at(slot0_offset, 6)
3442                .await
3443                .unwrap()
3444                .coalesce()
3445                .freeze()
3446                .into();
3447            assert_eq!(
3448                slot0_before, slot0_after,
3449                "Slot 0 was modified! Protected region violated."
3450            );
3451
3452            // Verify the new CRC in slot 1 has len=70
3453            let page = blob
3454                .read_at(0, physical_page_size)
3455                .await
3456                .unwrap()
3457                .coalesce();
3458            let crc = read_crc_record_from_page(page.as_ref());
3459            assert_eq!(crc.len2, 70, "Slot 1 should have len=70");
3460        });
3461    }
3462
3463    /// Test that the data prefix is NOT overwritten when extending a partial page.
3464    ///
3465    /// Strategy: Write data, then mangle the padding area (between data end and CRC start).
3466    /// After extending, the original data should be unchanged but the mangled padding
3467    /// should be overwritten with new data.
3468    #[test_traced("DEBUG")]
3469    fn test_data_prefix_not_overwritten() {
3470        let executor = deterministic::Runner::default();
3471        executor.start(|context: deterministic::Context| async move {
3472            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3473            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3474
3475            // === Step 1: Write 20 bytes ===
3476            let (blob, _) = context
3477                .open("test_partition", b"prefix_test")
3478                .await
3479                .unwrap();
3480            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3481                .await
3482                .unwrap();
3483            let data1: Vec<u8> = (1..=20).collect();
3484            append.append(&data1).await.unwrap();
3485            append.sync().await.unwrap();
3486            drop(append);
3487
3488            // === Step 2: Capture the first 20 bytes and mangle bytes 25-30 (in padding area) ===
3489            let (blob, size) = context
3490                .open("test_partition", b"prefix_test")
3491                .await
3492                .unwrap();
3493            assert_eq!(size, physical_page_size as u64);
3494
3495            let prefix_before: Vec<u8> = blob
3496                .read_at(0, 20)
3497                .await
3498                .unwrap()
3499                .coalesce()
3500                .freeze()
3501                .into();
3502
3503            // Mangle bytes 25-30 (safely in the padding area, after our 20 bytes of data)
3504            blob.write_at(25, DUMMY_MARKER.to_vec()).await.unwrap();
3505            blob.sync().await.unwrap();
3506
3507            // === Step 3: Extend to 40 bytes ===
3508            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3509                .await
3510                .unwrap();
3511            append
3512                .append(&(21..=40).collect::<Vec<u8>>())
3513                .await
3514                .unwrap();
3515            append.sync().await.unwrap();
3516            drop(append);
3517
3518            // === Step 4: Verify prefix unchanged, mangled area overwritten ===
3519            let (blob, _) = context
3520                .open("test_partition", b"prefix_test")
3521                .await
3522                .unwrap();
3523
3524            // Original 20 bytes should be unchanged
3525            let prefix_after: Vec<u8> = blob
3526                .read_at(0, 20)
3527                .await
3528                .unwrap()
3529                .coalesce()
3530                .freeze()
3531                .into();
3532            assert_eq!(prefix_before, prefix_after, "Data prefix was modified!");
3533
3534            // Bytes at offset 25-30: data (21..=40) starts at offset 20, so offset 25 has value 26
3535            let overwritten: Vec<u8> = blob
3536                .read_at(25, 6)
3537                .await
3538                .unwrap()
3539                .coalesce()
3540                .freeze()
3541                .into();
3542            assert_eq!(
3543                overwritten,
3544                vec![26, 27, 28, 29, 30, 31],
3545                "New data should overwrite padding area"
3546            );
3547        });
3548    }
3549
3550    /// Test CRC slot protection when extending past a page boundary.
3551    ///
3552    /// Strategy: Write partial page, mangle slot 0 (non-authoritative after we do first extend),
3553    /// then extend past page boundary. Verify slot 0 gets new full-page CRC while
3554    /// the mangled marker is overwritten, and second page is written correctly.
3555    #[test_traced("DEBUG")]
3556    fn test_crc_slot_protection_across_page_boundary() {
3557        let executor = deterministic::Runner::default();
3558        executor.start(|context: deterministic::Context| async move {
3559            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3560            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3561            let slot0_offset = PAGE_SIZE.get() as u64;
3562            let slot1_offset = PAGE_SIZE.get() as u64 + 6;
3563
3564            // === Step 1: Write 50 bytes → slot 0 authoritative ===
3565            let (blob, _) = context.open("test_partition", b"boundary").await.unwrap();
3566            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3567                .await
3568                .unwrap();
3569            append.append(&(1..=50).collect::<Vec<u8>>()).await.unwrap();
3570            append.sync().await.unwrap();
3571            drop(append);
3572
3573            // === Step 2: Extend to 80 bytes → slot 1 authoritative ===
3574            let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
3575            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3576                .await
3577                .unwrap();
3578            append
3579                .append(&(51..=80).collect::<Vec<u8>>())
3580                .await
3581                .unwrap();
3582            append.sync().await.unwrap();
3583            drop(append);
3584
3585            // Verify slot 1 is authoritative
3586            let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
3587            let page = blob
3588                .read_at(0, physical_page_size)
3589                .await
3590                .unwrap()
3591                .coalesce();
3592            let crc = read_crc_record_from_page(page.as_ref());
3593            assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
3594
3595            // Capture slot 1 before extending past page boundary
3596            let slot1_before: Vec<u8> = blob
3597                .read_at(slot1_offset, 6)
3598                .await
3599                .unwrap()
3600                .coalesce()
3601                .freeze()
3602                .into();
3603
3604            // Mangle slot 0 (non-authoritative)
3605            blob.write_at(slot0_offset, DUMMY_MARKER.to_vec())
3606                .await
3607                .unwrap();
3608            blob.sync().await.unwrap();
3609
3610            // === Step 3: Extend past page boundary (80 + 40 = 120, PAGE_SIZE=103) ===
3611            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3612                .await
3613                .unwrap();
3614            append
3615                .append(&(81..=120).collect::<Vec<u8>>())
3616                .await
3617                .unwrap();
3618            append.sync().await.unwrap();
3619            drop(append);
3620
3621            // === Step 4: Verify results ===
3622            let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
3623            assert_eq!(size, (physical_page_size * 2) as u64, "Should have 2 pages");
3624
3625            // Slot 0 should have been overwritten with full-page CRC (not dummy marker)
3626            let slot0_after: Vec<u8> = blob
3627                .read_at(slot0_offset, 6)
3628                .await
3629                .unwrap()
3630                .coalesce()
3631                .freeze()
3632                .into();
3633            assert_ne!(
3634                slot0_after, DUMMY_MARKER,
3635                "Slot 0 should have full-page CRC"
3636            );
3637
3638            // Slot 1 should be UNCHANGED (protected during boundary crossing)
3639            let slot1_after: Vec<u8> = blob
3640                .read_at(slot1_offset, 6)
3641                .await
3642                .unwrap()
3643                .coalesce()
3644                .freeze()
3645                .into();
3646            assert_eq!(
3647                slot1_before, slot1_after,
3648                "Slot 1 was modified during page boundary crossing!"
3649            );
3650
3651            // Verify page 0 has correct CRC structure
3652            let page0 = blob
3653                .read_at(0, physical_page_size)
3654                .await
3655                .unwrap()
3656                .coalesce();
3657            let crc0 = read_crc_record_from_page(page0.as_ref());
3658            assert_eq!(
3659                crc0.len1,
3660                PAGE_SIZE.get(),
3661                "Slot 0 should have full page length"
3662            );
3663
3664            // Verify data integrity
3665            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3666                .await
3667                .unwrap();
3668            assert_eq!(append.size(), 120);
3669            let all_data: Vec<u8> = append.read_at(0, 120).await.unwrap().coalesce().into();
3670            let expected: Vec<u8> = (1..=120).collect();
3671            assert_eq!(all_data, expected);
3672        });
3673    }
3674
3675    /// Test that corrupting the primary CRC (but not its length) causes fallback to the previous
3676    /// partial page contents.
3677    ///
3678    /// Strategy:
3679    /// 1. Write 10 bytes → slot 0 authoritative (len=10, valid crc)
3680    /// 2. Extend to 30 bytes → slot 1 authoritative (len=30, valid crc)
3681    /// 3. Corrupt ONLY the crc2 value in slot 1 (not the length)
3682    /// 4. Re-open and verify we fall back to slot 0's 10 bytes
3683    #[test_traced("DEBUG")]
3684    fn test_crc_fallback_on_corrupted_primary() {
3685        let executor = deterministic::Runner::default();
3686        executor.start(|context: deterministic::Context| async move {
3687            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3688            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3689            // crc2 is at offset: PAGE_SIZE + 6 (for len2) + 2 (skip len2 bytes) = PAGE_SIZE + 8
3690            let crc2_offset = PAGE_SIZE.get() as u64 + 8;
3691
3692            // === Step 1: Write 10 bytes → slot 0 authoritative (len=10) ===
3693            let (blob, _) = context
3694                .open("test_partition", b"crc_fallback")
3695                .await
3696                .unwrap();
3697            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3698                .await
3699                .unwrap();
3700            let data1: Vec<u8> = (1..=10).collect();
3701            append.append(&data1).await.unwrap();
3702            append.sync().await.unwrap();
3703            drop(append);
3704
3705            // === Step 2: Extend to 30 bytes → slot 1 authoritative (len=30) ===
3706            let (blob, size) = context
3707                .open("test_partition", b"crc_fallback")
3708                .await
3709                .unwrap();
3710            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3711                .await
3712                .unwrap();
3713            append
3714                .append(&(11..=30).collect::<Vec<u8>>())
3715                .await
3716                .unwrap();
3717            append.sync().await.unwrap();
3718            drop(append);
3719
3720            // Verify slot 1 is now authoritative and data reads correctly
3721            let (blob, size) = context
3722                .open("test_partition", b"crc_fallback")
3723                .await
3724                .unwrap();
3725            assert_eq!(size, physical_page_size as u64);
3726
3727            let page = blob
3728                .read_at(0, physical_page_size)
3729                .await
3730                .unwrap()
3731                .coalesce();
3732            let crc = read_crc_record_from_page(page.as_ref());
3733            assert!(
3734                crc.len2 > crc.len1,
3735                "Slot 1 should be authoritative (len2={} > len1={})",
3736                crc.len2,
3737                crc.len1
3738            );
3739            assert_eq!(crc.len2, 30, "Slot 1 should have len=30");
3740            assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
3741
3742            // Verify we can read all 30 bytes before corruption
3743            let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
3744                .await
3745                .unwrap();
3746            assert_eq!(append.size(), 30);
3747            let all_data: Vec<u8> = append.read_at(0, 30).await.unwrap().coalesce().into();
3748            let expected: Vec<u8> = (1..=30).collect();
3749            assert_eq!(all_data, expected);
3750            drop(append);
3751
3752            // === Step 3: Corrupt ONLY crc2 (not len2) ===
3753            // crc2 is 4 bytes at offset PAGE_SIZE + 8
3754            blob.write_at(crc2_offset, vec![0xDE, 0xAD, 0xBE, 0xEF])
3755                .await
3756                .unwrap();
3757            blob.sync().await.unwrap();
3758
3759            // Verify corruption: len2 should still be 30, but crc2 is now garbage
3760            let page = blob
3761                .read_at(0, physical_page_size)
3762                .await
3763                .unwrap()
3764                .coalesce();
3765            let crc = read_crc_record_from_page(page.as_ref());
3766            assert_eq!(crc.len2, 30, "len2 should still be 30 after corruption");
3767            assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be our corrupted value");
3768
3769            // === Step 4: Re-open and verify fallback to slot 0's 10 bytes ===
3770            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3771                .await
3772                .unwrap();
3773
3774            // Should fall back to 10 bytes (slot 0's length)
3775            assert_eq!(
3776                append.size(),
3777                10,
3778                "Should fall back to slot 0's 10 bytes after primary CRC corruption"
3779            );
3780
3781            // Verify the data is the original 10 bytes
3782            let fallback_data: Vec<u8> = append.read_at(0, 10).await.unwrap().coalesce().into();
3783            assert_eq!(
3784                fallback_data, data1,
3785                "Fallback data should match original 10 bytes"
3786            );
3787
3788            // Reading beyond 10 bytes should fail
3789            let result = append.read_at(0, 11).await;
3790            assert!(result.is_err(), "Reading beyond fallback size should fail");
3791        });
3792    }
3793
3794    /// Test that corrupting a non-last page's primary CRC fails even if fallback is valid.
3795    ///
3796    /// Non-last pages must always be full. If the primary CRC is corrupted and the fallback
3797    /// indicates a partial page, validation should fail entirely (not fall back to partial).
3798    ///
3799    /// Strategy:
3800    /// 1. Write 10 bytes → slot 0 has len=10 (partial)
3801    /// 2. Extend to full page (103 bytes) → slot 1 has len=103 (full, authoritative)
3802    /// 3. Extend past page boundary (e.g., 110 bytes) → page 0 is now non-last
3803    /// 4. Corrupt the primary CRC of page 0 (slot 1's crc, which has len=103)
3804    /// 5. Re-open and verify that reading from page 0 fails (fallback has len=10, not full)
3805    #[test_traced("DEBUG")]
3806    fn test_non_last_page_rejects_partial_fallback() {
3807        let executor = deterministic::Runner::default();
3808        executor.start(|context: deterministic::Context| async move {
3809            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3810            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3811            // crc2 for page 0 is at offset: PAGE_SIZE + 8
3812            let page0_crc2_offset = PAGE_SIZE.get() as u64 + 8;
3813
3814            // === Step 1: Write 10 bytes → slot 0 has len=10 ===
3815            let (blob, _) = context
3816                .open("test_partition", b"non_last_page")
3817                .await
3818                .unwrap();
3819            let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
3820                .await
3821                .unwrap();
3822            append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
3823            append.sync().await.unwrap();
3824            drop(append);
3825
3826            // === Step 2: Extend to exactly full page (103 bytes) → slot 1 has len=103 ===
3827            let (blob, size) = context
3828                .open("test_partition", b"non_last_page")
3829                .await
3830                .unwrap();
3831            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3832                .await
3833                .unwrap();
3834            // Add bytes 11 through 103 (93 more bytes)
3835            append
3836                .append(&(11..=PAGE_SIZE.get() as u8).collect::<Vec<u8>>())
3837                .await
3838                .unwrap();
3839            append.sync().await.unwrap();
3840            drop(append);
3841
3842            // Verify page 0 slot 1 is authoritative with len=103 (full page)
3843            let (blob, size) = context
3844                .open("test_partition", b"non_last_page")
3845                .await
3846                .unwrap();
3847            let page = blob
3848                .read_at(0, physical_page_size)
3849                .await
3850                .unwrap()
3851                .coalesce();
3852            let crc = read_crc_record_from_page(page.as_ref());
3853            assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
3854            assert_eq!(
3855                crc.len2,
3856                PAGE_SIZE.get(),
3857                "Slot 1 should have len=103 (full page)"
3858            );
3859            assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
3860
3861            // === Step 3: Extend past page boundary (add 10 more bytes for total of 113) ===
3862            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3863                .await
3864                .unwrap();
3865            // Add bytes 104 through 113 (10 more bytes, now on page 1)
3866            append
3867                .append(&(104..=113).collect::<Vec<u8>>())
3868                .await
3869                .unwrap();
3870            append.sync().await.unwrap();
3871            drop(append);
3872
3873            // Verify we now have 2 pages
3874            let (blob, size) = context
3875                .open("test_partition", b"non_last_page")
3876                .await
3877                .unwrap();
3878            assert_eq!(
3879                size,
3880                (physical_page_size * 2) as u64,
3881                "Should have 2 physical pages"
3882            );
3883
3884            // Verify data is readable before corruption
3885            let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
3886                .await
3887                .unwrap();
3888            assert_eq!(append.size(), 113);
3889            let all_data: Vec<u8> = append.read_at(0, 113).await.unwrap().coalesce().into();
3890            let expected: Vec<u8> = (1..=113).collect();
3891            assert_eq!(all_data, expected);
3892            drop(append);
3893
3894            // === Step 4: Corrupt page 0's primary CRC (slot 1's crc2) ===
3895            blob.write_at(page0_crc2_offset, vec![0xDE, 0xAD, 0xBE, 0xEF])
3896                .await
3897                .unwrap();
3898            blob.sync().await.unwrap();
3899
3900            // Verify corruption: page 0's slot 1 still has len=103 but bad CRC
3901            let page = blob
3902                .read_at(0, physical_page_size)
3903                .await
3904                .unwrap()
3905                .coalesce();
3906            let crc = read_crc_record_from_page(page.as_ref());
3907            assert_eq!(crc.len2, PAGE_SIZE.get(), "len2 should still be 103");
3908            assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be corrupted");
3909            // Slot 0 fallback has len=10 (partial), which is invalid for non-last page
3910            assert_eq!(crc.len1, 10, "Fallback slot 0 has partial length");
3911
3912            // === Step 5: Re-open and try to read from page 0 ===
3913            // The first page's primary CRC is bad, and fallback indicates partial (len=10).
3914            // Since page 0 is not the last page, a partial fallback is invalid.
3915            // Reading from page 0 should fail because the fallback CRC indicates a partial
3916            // page, which is not allowed for non-last pages.
3917            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3918                .await
3919                .unwrap();
3920
3921            // The blob still reports 113 bytes because init only validates the last page.
3922            // But reading from page 0 should fail because the CRC fallback is partial.
3923            assert_eq!(append.size(), 113);
3924
3925            // Try to read from page 0 - this should fail with InvalidChecksum because
3926            // the fallback CRC has len=10 (partial), which is invalid for a non-last page.
3927            let result = append.read_at(0, 10).await;
3928            assert!(
3929                result.is_err(),
3930                "Reading from corrupted non-last page via Append should fail, but got: {:?}",
3931                result
3932            );
3933            drop(append);
3934
3935            // Also verify that reading via Replay fails the same way.
3936            let (blob, size) = context
3937                .open("test_partition", b"non_last_page")
3938                .await
3939                .unwrap();
3940            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3941                .await
3942                .unwrap();
3943            let mut replay = append.replay(NZUsize!(1024)).await.unwrap();
3944
3945            // Try to fill pages - should fail on CRC validation.
3946            let result = replay.ensure(1).await;
3947            assert!(
3948                result.is_err(),
3949                "Reading from corrupted non-last page via Replay should fail, but got: {:?}",
3950                result
3951            );
3952        });
3953    }
3954
3955    #[test]
3956    fn test_resize_shrink_validates_crc() {
3957        // Verify that shrinking a blob to a partial page validates the CRC, rather than
3958        // blindly reading raw bytes which could silently load corrupted data.
3959        let executor = deterministic::Runner::default();
3960
3961        executor.start(|context| async move {
3962            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
3963            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
3964
3965            let (blob, size) = context
3966                .open("test_partition", b"resize_crc_test")
3967                .await
3968                .unwrap();
3969
3970            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3971                .await
3972                .unwrap();
3973
3974            // Write data across 3 pages: page 0 (full), page 1 (full), page 2 (partial).
3975            // PAGE_SIZE = 103, so 250 bytes = 103 + 103 + 44.
3976            let data: Vec<u8> = (0..=249).collect();
3977            append.append(&data).await.unwrap();
3978            append.sync().await.unwrap();
3979            assert_eq!(append.size(), 250);
3980            drop(append);
3981
3982            // Corrupt the CRC record of page 1 (middle page).
3983            let (blob, size) = context
3984                .open("test_partition", b"resize_crc_test")
3985                .await
3986                .unwrap();
3987            assert_eq!(size as usize, physical_page_size * 3);
3988
3989            // Page 1 CRC record is at the end of the second physical page.
3990            let page1_crc_offset = (physical_page_size * 2 - CHECKSUM_SIZE as usize) as u64;
3991            blob.write_at(page1_crc_offset, vec![0xFF; CHECKSUM_SIZE as usize])
3992                .await
3993                .unwrap();
3994            blob.sync().await.unwrap();
3995
3996            // Open the blob - Writer::new() validates the LAST page (page 2), which is still valid.
3997            // So it should open successfully with size 250.
3998            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
3999                .await
4000                .unwrap();
4001            assert_eq!(append.size(), 250);
4002
4003            // Try to shrink to 150 bytes, which ends in page 1 (the corrupted page).
4004            // 150 bytes = page 0 (103 full) + page 1 (47 partial).
4005            // This should fail because page 1's CRC is corrupted.
4006            let result = append.resize(150).await;
4007            assert!(
4008                matches!(result, Err(crate::Error::InvalidChecksum)),
4009                "Expected InvalidChecksum when shrinking to corrupted page, got: {:?}",
4010                result
4011            );
4012        });
4013    }
4014
4015    #[test]
4016    fn test_resize_invalidates_cache() {
4017        // Regression: shrinking a blob across a page boundary must drop cached pages for the
4018        // truncated region. Before the fix, `try_read_sync_into` (whose reads below the tip
4019        // boundary come straight from the page cache)
4020        // would observe pre-resize bytes at offsets later reclaimed by new appends.
4021        let executor = deterministic::Runner::default();
4022        executor.start(|context: deterministic::Context| async move {
4023            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4024            let (blob, blob_size) = context
4025                .open("test_partition", b"resize_invalidates_cache")
4026                .await
4027                .unwrap();
4028            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4029                .await
4030                .unwrap();
4031
4032            // Write + sync a full page so it lands in the page cache. Use a distinct byte
4033            // pattern so a stale cache read would be obvious.
4034            let page_size = PAGE_SIZE.get() as usize;
4035            let old_bytes = vec![0xAAu8; page_size];
4036            append.append(&old_bytes).await.unwrap();
4037            append.sync().await.unwrap();
4038
4039            // Confirm page 0 is reachable via the cache-only fast path.
4040            let mut probe = vec![0u8; 16];
4041            assert!(append.try_read_sync_into(&mut probe, 0));
4042            assert_eq!(probe, vec![0xAAu8; 16]);
4043
4044            // Rewind to 0 (crossing the page boundary) and append a new, distinct pattern.
4045            append.resize(0).await.unwrap();
4046            let new_bytes = vec![0xBBu8; 16];
4047            append.append(&new_bytes).await.unwrap();
4048
4049            // The cache must not serve pre-resize bytes. Either try_read_sync_into misses (cache
4050            // was invalidated) or it returns the new pattern; it must never return 0xAA.
4051            let mut probe = vec![0u8; 16];
4052            let hit = append.try_read_sync_into(&mut probe, 0);
4053            assert!(
4054                !hit || probe == new_bytes,
4055                "try_read_sync_into served stale pre-resize bytes: {probe:?}"
4056            );
4057        });
4058    }
4059
4060    #[test]
4061    fn test_snapshot_fetch_cannot_repopulate_live_cache_after_resize() {
4062        let executor = deterministic::Runner::default();
4063        executor.start(|context: deterministic::Context| async move {
4064            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4065            let page_size = PAGE_SIZE.get() as usize;
4066            let physical_page_size = page_size + CHECKSUM_SIZE as usize;
4067            let (inner, blob_size) = context
4068                .open("test_partition", b"snapshot_resize_cache")
4069                .await
4070                .unwrap();
4071            let (started_tx, started_rx) = oneshot::channel();
4072            let (release_tx, release_rx) = oneshot::channel();
4073            let blob = DelayedReadBlob::new(
4074                inner,
4075                physical_page_size as u64,
4076                physical_page_size,
4077                started_tx,
4078                release_rx,
4079            );
4080            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4081                .await
4082                .unwrap();
4083
4084            let old_page0 = vec![0x11u8; page_size];
4085            let old_page1 = vec![0x22u8; page_size];
4086            writer.append(&old_page0).await.unwrap();
4087            writer.append(&old_page1).await.unwrap();
4088            writer.sync().await.unwrap();
4089
4090            writer.cache_ref.invalidate_from(writer.id, 1);
4091
4092            let snapshot = writer.snapshot().await.unwrap();
4093            let snapshot_task = context
4094                .child("snapshot")
4095                .spawn(move |_| async move { snapshot.read_at(page_size as u64, page_size).await });
4096            started_rx.await.expect("snapshot read never started");
4097
4098            writer.resize(page_size as u64).await.unwrap();
4099            let new_page1 = vec![0x33u8; page_size];
4100            writer.append(&new_page1).await.unwrap();
4101            writer.sync().await.unwrap();
4102
4103            let _ = release_tx.send(());
4104            let stale = snapshot_task
4105                .await
4106                .expect("snapshot task failed")
4107                .expect("snapshot read failed")
4108                .coalesce();
4109            assert_eq!(stale.as_ref(), old_page1.as_slice());
4110
4111            let mut probe = vec![0u8; page_size];
4112            assert!(writer.try_read_sync_into(&mut probe, page_size as u64));
4113            assert_eq!(probe, new_page1);
4114        });
4115    }
4116
4117    #[test]
4118    fn test_resize_shrink_allowed_while_snapshot_alive() {
4119        let executor = deterministic::Runner::default();
4120        executor.start(|context: deterministic::Context| async move {
4121            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4122            let (blob, blob_size) = context
4123                .open("test_partition", b"snapshot_blocks_shrink")
4124                .await
4125                .unwrap();
4126            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4127                .await
4128                .unwrap();
4129
4130            let page_size = PAGE_SIZE.get() as usize;
4131            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2 + 7).collect();
4132            append.append(&data).await.unwrap();
4133            append.sync().await.unwrap();
4134
4135            let snapshot = append.snapshot().await.unwrap();
4136            let snapshot_clone = snapshot.clone();
4137            let snapshot_size = snapshot.size();
4138
4139            let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
4140            assert_eq!(read.as_ref(), data.as_slice());
4141
4142            // Growing appends after the snapshot's frozen range, so it cannot invalidate it.
4143            append.resize(snapshot_size + 3).await.unwrap();
4144            assert_eq!(append.size(), snapshot_size + 3);
4145
4146            // Shrinking while old handles exist is allowed. Those handles remain memory-safe, but
4147            // future reads from ranges reused by the writer are unspecified.
4148            append.resize(snapshot_size - 1).await.unwrap();
4149            assert_eq!(append.size(), snapshot_size - 1);
4150            assert_eq!(snapshot_clone.size(), snapshot_size);
4151        });
4152    }
4153
4154    #[test]
4155    fn test_snapshot_read_many_into_matches_read_at() {
4156        let executor = deterministic::Runner::default();
4157        executor.start(|context: deterministic::Context| async move {
4158            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4159            let (blob, blob_size) = context
4160                .open("test_partition", b"snapshot_read_many")
4161                .await
4162                .unwrap();
4163            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4164                .await
4165                .unwrap();
4166
4167            let item_size = 4;
4168            let page_size = PAGE_SIZE.get() as usize;
4169            let total = page_size * 3 + 13;
4170            let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
4171            append.append(&data).await.unwrap();
4172            append.sync().await.unwrap();
4173
4174            let snapshot = append.snapshot().await.unwrap();
4175            let offsets = [
4176                0,
4177                (page_size - item_size) as u64,
4178                (page_size + 5) as u64,
4179                (page_size * 3 + 2) as u64,
4180            ];
4181            // Cover page-aligned, boundary-crossing, and snapshot-tail reads in one batch.
4182            let mut batch = vec![0u8; offsets.len() * item_size];
4183            snapshot
4184                .read_many_into(&mut batch, &offsets, NZUsize!(item_size))
4185                .await
4186                .unwrap();
4187
4188            for (item, offset) in batch.chunks_exact(item_size).zip(offsets) {
4189                let single = snapshot
4190                    .read_at(offset, item_size)
4191                    .await
4192                    .unwrap()
4193                    .coalesce();
4194                assert_eq!(item, single.as_ref());
4195                assert_eq!(item, &data[offset as usize..offset as usize + item_size]);
4196            }
4197        });
4198    }
4199
4200    #[test]
4201    fn test_snapshot_try_read_sync_prefers_snapshot_tail() {
4202        let executor = deterministic::Runner::default();
4203        executor.start(|context: deterministic::Context| async move {
4204            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4205            let (blob, blob_size) = context
4206                .open("test_partition", b"snapshot_try_read_tail")
4207                .await
4208                .unwrap();
4209            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4210                .await
4211                .unwrap();
4212
4213            let page_size = PAGE_SIZE.get() as usize;
4214            append.append(&vec![0xAA; page_size]).await.unwrap();
4215            append.sync().await.unwrap();
4216
4217            let tail = b"oldtail";
4218            append.append(tail).await.unwrap();
4219            let snapshot = append.snapshot().await.unwrap();
4220
4221            let poison = vec![0xBB; page_size];
4222            // If the snapshot consulted the page cache for its tail, this would leak in.
4223            assert_eq!(
4224                append.cache_ref.cache(append.id, &poison, page_size as u64),
4225                0
4226            );
4227
4228            let mut read = vec![0; tail.len()];
4229            assert!(snapshot.try_read_sync_into(&mut read, page_size as u64));
4230            assert_eq!(read.as_slice(), tail);
4231        });
4232    }
4233
4234    #[test]
4235    fn test_resize_same_size_is_noop() {
4236        let executor = deterministic::Runner::default();
4237        executor.start(|context: deterministic::Context| async move {
4238            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4239            let (blob, blob_size) = context
4240                .open("test_partition", b"resize_same_size")
4241                .await
4242                .unwrap();
4243            let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4244                .await
4245                .unwrap();
4246
4247            append.append(b"hello world").await.unwrap();
4248            assert_eq!(append.size(), 11);
4249
4250            // Resize to same size. Should succeed.
4251            append.resize(11).await.unwrap();
4252            assert_eq!(append.size(), 11);
4253
4254            // Verify content is still readable and intact.
4255            let read = append.read_at(0, 11).await.unwrap().coalesce();
4256            assert_eq!(read.as_ref(), b"hello world");
4257        });
4258    }
4259
4260    #[test]
4261    fn test_resize_same_page_shrink_reopens_at_shorter_size() {
4262        let executor = deterministic::Runner::default();
4263
4264        executor.start(|context| async move {
4265            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4266            let data: Vec<u8> = (0..50).collect();
4267
4268            let (blob, size) = context
4269                .open("test_partition", b"same_page_shrink")
4270                .await
4271                .unwrap();
4272            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4273                .await
4274                .unwrap();
4275
4276            // Create a partial page whose authoritative CRC is in the first slot. The interrupted
4277            // tests below exercise the opposite slot orientation.
4278            append.append(&data).await.unwrap();
4279            append.sync().await.unwrap();
4280
4281            append.resize(45).await.unwrap();
4282            drop(append);
4283
4284            let (blob, size) = context
4285                .open("test_partition", b"same_page_shrink")
4286                .await
4287                .unwrap();
4288            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4289                .await
4290                .unwrap();
4291            assert_eq!(append.size(), 45);
4292            let read = append.read_at(0, 45).await.unwrap().coalesce();
4293            assert_eq!(read.as_ref(), &data[..45]);
4294        });
4295    }
4296
4297    #[test]
4298    fn test_resize_same_page_shrink_survives_interrupted_crc_stage() {
4299        let executor = deterministic::Runner::default();
4300
4301        executor.start(|context| async move {
4302            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4303            let data: Vec<u8> = (0..50).collect();
4304
4305            let (blob, size) = context
4306                .open("test_partition", b"same_page_shrink_interrupted")
4307                .await
4308                .unwrap();
4309            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4310                .await
4311                .unwrap();
4312            append.append(&data[..40]).await.unwrap();
4313            append.sync().await.unwrap();
4314            append.append(&data[40..]).await.unwrap();
4315            append.sync().await.unwrap();
4316            drop(append);
4317
4318            let (blob, size) = context
4319                .open("test_partition", b"same_page_shrink_interrupted")
4320                .await
4321                .unwrap();
4322            let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
4323            let write_count = faulty_blob.write_count();
4324            let failed_write_len = faulty_blob.failed_write_len();
4325            let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
4326                .await
4327                .unwrap();
4328
4329            assert!(
4330                append.resize(45).await.is_err(),
4331                "phase-1 partial write should fail"
4332            );
4333            assert_eq!(write_count.load(Ordering::SeqCst), 1);
4334            assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
4335            drop(append);
4336
4337            let (blob, size) = context
4338                .open("test_partition", b"same_page_shrink_interrupted")
4339                .await
4340                .unwrap();
4341            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4342                .await
4343                .unwrap();
4344            assert_eq!(append.size(), 50);
4345            let read = append.read_at(0, 50).await.unwrap().coalesce();
4346            assert_eq!(read.as_ref(), &data);
4347        });
4348    }
4349
4350    #[test]
4351    fn test_resize_same_page_shrink_survives_interrupted_len_stage() {
4352        let executor = deterministic::Runner::default();
4353
4354        executor.start(|context| async move {
4355            const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
4356            const LARGE_BUFFER_SIZE: usize = 1_200;
4357
4358            let cache_ref =
4359                CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
4360            let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
4361
4362            let (blob, size) = context
4363                .open("test_partition", b"same_page_shrink_len_stage")
4364                .await
4365                .unwrap();
4366            let mut append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4367                .await
4368                .unwrap();
4369            append.append(&data[..255]).await.unwrap();
4370            append.sync().await.unwrap();
4371            append.append(&data[255..]).await.unwrap();
4372            append.sync().await.unwrap();
4373            drop(append);
4374
4375            let (blob, size) = context
4376                .open("test_partition", b"same_page_shrink_len_stage")
4377                .await
4378                .unwrap();
4379            let faulty_blob = PartialWriteBlob::new(blob, 2, 1);
4380            let write_count = faulty_blob.write_count();
4381            let failed_write_len = faulty_blob.failed_write_len();
4382            let mut append = Writer::new(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4383                .await
4384                .unwrap();
4385
4386            assert!(
4387                append.resize(257).await.is_err(),
4388                "length-stage partial write should fail"
4389            );
4390            assert_eq!(write_count.load(Ordering::SeqCst), 2);
4391            assert_eq!(failed_write_len.load(Ordering::SeqCst), 2);
4392            drop(append);
4393
4394            let (blob, size) = context
4395                .open("test_partition", b"same_page_shrink_len_stage")
4396                .await
4397                .unwrap();
4398            let append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref)
4399                .await
4400                .unwrap();
4401            assert_eq!(append.size(), 300);
4402            let read = append.read_at(0, 300).await.unwrap().coalesce();
4403            assert_eq!(read.as_ref(), &data);
4404        });
4405    }
4406
4407    #[test]
4408    fn test_resize_same_page_shrink_preserves_validated_fallback_slot() {
4409        let executor = deterministic::Runner::default();
4410
4411        executor.start(|context| async move {
4412            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4413            let data: Vec<u8> = (0..52).collect();
4414
4415            let (blob, size) = context
4416                .open("test_partition", b"same_page_shrink_fallback_slot")
4417                .await
4418                .unwrap();
4419            let faulty_blob = PartialWriteBlob::new(blob.clone(), 5, 3);
4420            let write_count = faulty_blob.write_count();
4421            let failed_write_len = faulty_blob.failed_write_len();
4422            let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
4423                .await
4424                .unwrap();
4425            append.append(&data[..48]).await.unwrap();
4426            append.sync().await.unwrap();
4427            assert_eq!(write_count.load(Ordering::SeqCst), 1);
4428
4429            append.append(&data[48..50]).await.unwrap();
4430            append.sync().await.unwrap();
4431            assert_eq!(write_count.load(Ordering::SeqCst), 3);
4432
4433            append.append(&data[50..]).await.unwrap();
4434            append.sync().await.unwrap();
4435            assert_eq!(write_count.load(Ordering::SeqCst), 4);
4436
4437            // Corrupt the newer authoritative slot. The older slot still covers the shrink target.
4438            // `resize()` first syncs the live buffer, which writes a valid fallback slot but leaves
4439            // the cached footer stale. A torn phase-1 shrink write must preserve that validated
4440            // fallback slot.
4441            let slot0_offset = PAGE_SIZE.get() as u64;
4442            blob.write_at(slot0_offset, DUMMY_MARKER.to_vec())
4443                .await
4444                .unwrap();
4445            blob.sync().await.unwrap();
4446
4447            assert!(
4448                append.resize(45).await.is_err(),
4449                "phase-1 partial write should fail"
4450            );
4451            assert_eq!(write_count.load(Ordering::SeqCst), 5);
4452            assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
4453            drop(append);
4454
4455            let (blob, size) = context
4456                .open("test_partition", b"same_page_shrink_fallback_slot")
4457                .await
4458                .unwrap();
4459            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4460                .await
4461                .unwrap();
4462            assert_eq!(append.size(), 50);
4463            let read = append.read_at(0, 50).await.unwrap().coalesce();
4464            assert_eq!(read.as_ref(), &data[..50]);
4465        });
4466    }
4467
4468    #[test]
4469    fn test_resize_full_page_to_partial_reopens_at_shorter_size() {
4470        let executor = deterministic::Runner::default();
4471
4472        executor.start(|context| async move {
4473            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4474            let page_size = PAGE_SIZE.get() as u64;
4475            let target = page_size + 45;
4476            let data: Vec<u8> = (0..page_size * 2).map(|i| (i % 251) as u8).collect();
4477
4478            let (blob, size) = context
4479                .open("test_partition", b"full_page_to_partial")
4480                .await
4481                .unwrap();
4482            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4483                .await
4484                .unwrap();
4485            append.append(&data).await.unwrap();
4486            append.sync().await.unwrap();
4487
4488            append.resize(target).await.unwrap();
4489            drop(append);
4490
4491            let (blob, size) = context
4492                .open("test_partition", b"full_page_to_partial")
4493                .await
4494                .unwrap();
4495            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4496                .await
4497                .unwrap();
4498            assert_eq!(append.size(), target);
4499            let read = append.read_at(0, target as usize).await.unwrap().coalesce();
4500            assert_eq!(read.as_ref(), &data[..target as usize]);
4501        });
4502    }
4503
4504    #[test]
4505    fn test_resize_full_page_to_partial_survives_interrupted_crc_stage() {
4506        let executor = deterministic::Runner::default();
4507
4508        executor.start(|context| async move {
4509            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4510            let page_size = PAGE_SIZE.get() as u64;
4511            let target = page_size + 45;
4512            let data: Vec<u8> = (0..page_size * 3).map(|i| (i % 251) as u8).collect();
4513
4514            let (blob, size) = context
4515                .open("test_partition", b"full_page_to_partial_interrupted")
4516                .await
4517                .unwrap();
4518            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4519                .await
4520                .unwrap();
4521            append.append(&data).await.unwrap();
4522            append.sync().await.unwrap();
4523            drop(append);
4524
4525            let (blob, size) = context
4526                .open("test_partition", b"full_page_to_partial_interrupted")
4527                .await
4528                .unwrap();
4529            let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
4530            let write_count = faulty_blob.write_count();
4531            let failed_write_len = faulty_blob.failed_write_len();
4532            let mut append = Writer::new(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
4533                .await
4534                .unwrap();
4535
4536            assert!(
4537                append.resize(target).await.is_err(),
4538                "phase-1 partial write should fail"
4539            );
4540            assert_eq!(write_count.load(Ordering::SeqCst), 1);
4541            assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
4542            drop(append);
4543
4544            let (blob, size) = context
4545                .open("test_partition", b"full_page_to_partial_interrupted")
4546                .await
4547                .unwrap();
4548            let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
4549                .await
4550                .unwrap();
4551            assert_eq!(append.size(), page_size * 2);
4552            let read = append
4553                .read_at(0, (page_size * 2) as usize)
4554                .await
4555                .unwrap()
4556                .coalesce();
4557            assert_eq!(read.as_ref(), &data[..(page_size * 2) as usize]);
4558        });
4559    }
4560
4561    #[test]
4562    fn test_resize_same_page_shrink_survives_interrupted_length_invalidation() {
4563        let executor = deterministic::Runner::default();
4564
4565        executor.start(|context| async move {
4566            const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
4567            const LARGE_BUFFER_SIZE: usize = 1_200;
4568
4569            let cache_ref =
4570                CacheRef::from_pooler(&context, LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
4571            let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
4572
4573            let (blob, size) = context
4574                .open(
4575                    "test_partition",
4576                    b"same_page_shrink_interrupted_len_invalidation",
4577                )
4578                .await
4579                .unwrap();
4580            let mut append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4581                .await
4582                .unwrap();
4583            // Put the old authoritative CRC in slot 1, so the shorter CRC will be staged in slot
4584            // 0. The old length is above 255, so a one-byte tear changes the decoded length.
4585            append.append(&data[..255]).await.unwrap();
4586            append.sync().await.unwrap();
4587            append.append(&data[255..]).await.unwrap();
4588            append.sync().await.unwrap();
4589            drop(append);
4590
4591            let (blob, size) = context
4592                .open(
4593                    "test_partition",
4594                    b"same_page_shrink_interrupted_len_invalidation",
4595                )
4596                .await
4597                .unwrap();
4598            let faulty_blob = PartialWriteBlob::new(blob, 3, 1);
4599            let write_count = faulty_blob.write_count();
4600            let failed_write_len = faulty_blob.failed_write_len();
4601            let mut append = Writer::new(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
4602                .await
4603                .unwrap();
4604
4605            assert!(
4606                append.resize(40).await.is_err(),
4607                "old-slot length invalidation should fail"
4608            );
4609            assert_eq!(write_count.load(Ordering::SeqCst), 3);
4610            assert_eq!(
4611                failed_write_len.load(Ordering::SeqCst),
4612                CHECKSUM_SLOT_LEN_SIZE
4613            );
4614            drop(append);
4615
4616            let (blob, size) = context
4617                .open(
4618                    "test_partition",
4619                    b"same_page_shrink_interrupted_len_invalidation",
4620                )
4621                .await
4622                .unwrap();
4623            let append = Writer::new(blob, size, LARGE_BUFFER_SIZE, cache_ref)
4624                .await
4625                .unwrap();
4626            assert_eq!(append.size(), 40);
4627            let read = append.read_at(0, 40).await.unwrap().coalesce();
4628            assert_eq!(read.as_ref(), &data[..40]);
4629        });
4630    }
4631
4632    #[test_traced("DEBUG")]
4633    fn test_resize_partial_shrink_without_physical_resize_uses_range_sync() {
4634        let executor = deterministic::Runner::default();
4635        executor.start(|context: deterministic::Context| async move {
4636            let blob = SyncTrackingBlob::new();
4637            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4638            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
4639                .await
4640                .unwrap();
4641            append.sync().await.unwrap();
4642
4643            let data = vec![5u8; PAGE_SIZE.get() as usize];
4644            append.append(&data).await.unwrap();
4645            append.sync().await.unwrap();
4646
4647            // Shrinking within the same physical page only rewrites CRC metadata.
4648            append.resize(50).await.unwrap();
4649            append.sync().await.unwrap();
4650
4651            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4652            assert_eq!(writes, 4);
4653            assert_eq!(full_syncs, 1);
4654            assert_eq!(range_syncs, 4);
4655        });
4656    }
4657
4658    #[test_traced("DEBUG")]
4659    fn test_resize_partial_shrink_with_physical_resize_clears_full_sync_requirement() {
4660        let executor = deterministic::Runner::default();
4661        executor.start(|context: deterministic::Context| async move {
4662            let blob = SyncTrackingBlob::new();
4663            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4664            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
4665                .await
4666                .unwrap();
4667            append.sync().await.unwrap();
4668
4669            let data = vec![9u8; PAGE_SIZE.get() as usize * 2];
4670            append.append(&data).await.unwrap();
4671            append.sync().await.unwrap();
4672
4673            // Shrinking from two physical pages to one partial page must also make the resize
4674            // durable.
4675            append.resize(50).await.unwrap();
4676            append.sync().await.unwrap();
4677
4678            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4679            assert_eq!(writes, 4);
4680            assert_eq!(full_syncs, 2);
4681            assert_eq!(range_syncs, 3);
4682
4683            // Once the resize barrier is cleared, the next single flush can use range sync again.
4684            append.append(b"x").await.unwrap();
4685            append.sync().await.unwrap();
4686
4687            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4688            assert_eq!(writes, 5);
4689            assert_eq!(full_syncs, 2);
4690            assert_eq!(range_syncs, 4);
4691
4692            let mut expected = data[..50].to_vec();
4693            expected.push(b'x');
4694            let read = append.read_at(0, expected.len()).await.unwrap().coalesce();
4695            assert_eq!(read.as_ref(), expected.as_slice());
4696        });
4697    }
4698
4699    #[test_traced("DEBUG")]
4700    fn test_resize_page_boundary_shrink_uses_full_sync() {
4701        let executor = deterministic::Runner::default();
4702        executor.start(|context: deterministic::Context| async move {
4703            let blob = SyncTrackingBlob::new();
4704            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4705            let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
4706                .await
4707                .unwrap();
4708            append.sync().await.unwrap();
4709
4710            // Start with two durable full pages. After clearing the wrapper barrier, the data sync
4711            // can persist them with one range-sync write.
4712            let page_size = PAGE_SIZE.get() as usize;
4713            let data = vec![11u8; page_size * 2];
4714            append.append(&data).await.unwrap();
4715            append.sync().await.unwrap();
4716
4717            // Shrinking to a page boundary resizes the blob but does not rewrite CRC metadata.
4718            append.resize(PAGE_SIZE.get() as u64).await.unwrap();
4719            append.sync().await.unwrap();
4720
4721            // Only the resize needs a full sync, no additional writes are emitted by the shrink.
4722            let (_, writes, full_syncs, range_syncs) = blob.snapshot();
4723            assert_eq!(writes, 1);
4724            assert_eq!(full_syncs, 2);
4725            assert_eq!(range_syncs, 1);
4726
4727            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4728            let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
4729                .await
4730                .unwrap();
4731            assert_eq!(reopened.size(), PAGE_SIZE.get() as u64);
4732            let read = reopened.read_at(0, page_size).await.unwrap().coalesce();
4733            assert_eq!(read.as_ref(), &data[..page_size]);
4734        });
4735    }
4736
4737    #[test]
4738    fn test_reopen_partial_tail_append_and_resize() {
4739        let executor = deterministic::Runner::default();
4740
4741        executor.start(|context| async move {
4742            const PAGE_SIZE: NonZeroU16 = NZU16!(64);
4743            const BUFFER_SIZE: usize = 256;
4744
4745            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(4));
4746
4747            let (blob, size) = context
4748                .open("test_partition", b"partial_tail_test")
4749                .await
4750                .unwrap();
4751
4752            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4753                .await
4754                .unwrap();
4755
4756            // Write some initial data.
4757            append.append(&[1, 2, 3, 4, 5]).await.unwrap();
4758            append.sync().await.unwrap();
4759            assert_eq!(append.size(), 5);
4760            drop(append);
4761
4762            let (blob, size) = context
4763                .open("test_partition", b"partial_tail_test")
4764                .await
4765                .unwrap();
4766
4767            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4768                .await
4769                .unwrap();
4770            assert_eq!(append.size(), 5);
4771
4772            append.append(&[6, 7, 8]).await.unwrap();
4773            append.resize(6).await.unwrap();
4774            append.sync().await.unwrap();
4775
4776            let data: Vec<u8> = append.read_at(0, 6).await.unwrap().coalesce().into();
4777            assert_eq!(data, vec![1, 2, 3, 4, 5, 6]);
4778        });
4779    }
4780
4781    #[test]
4782    fn test_corrupted_crc_len_too_large() {
4783        let executor = deterministic::Runner::default();
4784
4785        executor.start(|context| async move {
4786            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4787            let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
4788
4789            // Step 1: Create blob with valid data
4790            let (blob, size) = context
4791                .open("test_partition", b"crc_len_test")
4792                .await
4793                .unwrap();
4794
4795            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4796                .await
4797                .unwrap();
4798
4799            append.append(&[0x42; 50]).await.unwrap();
4800            append.sync().await.unwrap();
4801            drop(append);
4802
4803            // Step 2: Corrupt the CRC record to have len > page_size
4804            let (blob, size) = context
4805                .open("test_partition", b"crc_len_test")
4806                .await
4807                .unwrap();
4808            assert_eq!(size as usize, physical_page_size);
4809
4810            // CRC record is at the end of the physical page
4811            let crc_offset = PAGE_SIZE.get() as u64;
4812
4813            // Create a CRC record with len1 = 0xFFFF (65535), which is >> page_size (103)
4814            // Format: [len1_hi, len1_lo, crc1 (4 bytes), len2_hi, len2_lo, crc2 (4 bytes)]
4815            let bad_crc_record: [u8; 12] = [
4816                0xFF, 0xFF, // len1 = 65535 (way too large)
4817                0xDE, 0xAD, 0xBE, 0xEF, // crc1 (garbage)
4818                0x00, 0x00, // len2 = 0
4819                0x00, 0x00, 0x00, 0x00, // crc2 = 0
4820            ];
4821            blob.write_at(crc_offset, bad_crc_record.to_vec())
4822                .await
4823                .unwrap();
4824            blob.sync().await.unwrap();
4825
4826            // Step 3: Try to open the blob - should NOT panic, should return error or handle gracefully
4827            let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
4828
4829            // Either returns InvalidChecksum error OR truncates the corrupted data
4830            // (both are acceptable behaviors - panicking is NOT acceptable)
4831            match result {
4832                Ok(append) => {
4833                    // If it opens successfully, the corrupted page should have been truncated
4834                    let recovered_size = append.size();
4835                    assert_eq!(
4836                        recovered_size, 0,
4837                        "Corrupted page should be truncated, size should be 0"
4838                    );
4839                }
4840                Err(e) => {
4841                    // Error is also acceptable
4842                    assert!(
4843                        matches!(e, crate::Error::InvalidChecksum),
4844                        "Expected InvalidChecksum error, got: {:?}",
4845                        e
4846                    );
4847                }
4848            }
4849        });
4850    }
4851
4852    #[test]
4853    fn test_corrupted_crc_both_slots_len_too_large() {
4854        let executor = deterministic::Runner::default();
4855
4856        executor.start(|context| async move {
4857            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4858
4859            // Step 1: Create blob with valid data
4860            let (blob, size) = context
4861                .open("test_partition", b"crc_both_bad")
4862                .await
4863                .unwrap();
4864
4865            let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
4866                .await
4867                .unwrap();
4868
4869            append.append(&[0x42; 50]).await.unwrap();
4870            append.sync().await.unwrap();
4871            drop(append);
4872
4873            // Step 2: Corrupt BOTH CRC slots to have len > page_size
4874            let (blob, size) = context
4875                .open("test_partition", b"crc_both_bad")
4876                .await
4877                .unwrap();
4878
4879            let crc_offset = PAGE_SIZE.get() as u64;
4880
4881            // Both slots have len > page_size
4882            let bad_crc_record: [u8; 12] = [
4883                0x01, 0x00, // len1 = 256 (> 103)
4884                0xDE, 0xAD, 0xBE, 0xEF, // crc1 (garbage)
4885                0x02, 0x00, // len2 = 512 (> 103)
4886                0xCA, 0xFE, 0xBA, 0xBE, // crc2 (garbage)
4887            ];
4888            blob.write_at(crc_offset, bad_crc_record.to_vec())
4889                .await
4890                .unwrap();
4891            blob.sync().await.unwrap();
4892
4893            // Step 3: Try to open - should NOT panic
4894            let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
4895
4896            match result {
4897                Ok(append) => {
4898                    // Corrupted page truncated
4899                    assert_eq!(append.size(), 0);
4900                }
4901                Err(e) => {
4902                    assert!(
4903                        matches!(e, crate::Error::InvalidChecksum),
4904                        "Expected InvalidChecksum, got: {:?}",
4905                        e
4906                    );
4907                }
4908            }
4909        });
4910    }
4911
4912    /// Readers observe buffered (not yet flushed) bytes through both async and sync read paths.
4913    #[test_traced("DEBUG")]
4914    fn test_reader_sees_buffered_bytes() {
4915        let executor = deterministic::Runner::default();
4916        executor.start(|context: deterministic::Context| async move {
4917            let (blob, blob_size) = context.open("test_partition", b"rdr_buf").await.unwrap();
4918            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4919            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4920                .await
4921                .unwrap();
4922
4923            let data: Vec<u8> = (0u8..50).collect();
4924            writer.append(&data).await.unwrap();
4925
4926            // No flush or sync has happened; reads must still see the buffered bytes.
4927            assert_eq!(writer.size(), 50);
4928            let read = writer.read_at(0, 50).await.unwrap().coalesce();
4929            assert_eq!(read.as_ref(), data.as_slice());
4930
4931            let mut buf = vec![0u8; 10];
4932            assert!(writer.try_read_sync_into(&mut buf, 20));
4933            assert_eq!(buf, data[20..30]);
4934        });
4935    }
4936
4937    /// A resize racing a reader yields clean errors or valid pre/post-resize bytes, never
4938    /// out-of-bounds garbage.
4939    #[test_traced("DEBUG")]
4940    fn test_reader_read_past_resize_errors_cleanly() {
4941        let executor = deterministic::Runner::default();
4942        executor.start(|context: deterministic::Context| async move {
4943            let (blob, blob_size) = context.open("test_partition", b"rdr_rsz").await.unwrap();
4944            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(BUFFER_SIZE));
4945            let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
4946                .await
4947                .unwrap();
4948
4949            let page_size = PAGE_SIZE.get() as usize;
4950            let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
4951            writer.append(&data).await.unwrap();
4952            writer.sync().await.unwrap();
4953            assert_eq!(writer.size(), (page_size * 2) as u64);
4954
4955            // Shrink below the last observed size.
4956            let new_size = (page_size / 2) as u64;
4957            writer.resize(new_size).await.unwrap();
4958
4959            // Reads past the new size fail cleanly.
4960            let err = writer
4961                .read_at(new_size, page_size)
4962                .await
4963                .expect_err("read past resized end must fail");
4964            assert!(matches!(err, crate::Error::BlobInsufficientLength));
4965
4966            // Reads within the new size return the retained prefix, not stale cached bytes.
4967            let read = writer
4968                .read_at(0, new_size as usize)
4969                .await
4970                .unwrap()
4971                .coalesce();
4972            assert_eq!(read.as_ref(), &data[..new_size as usize]);
4973        });
4974    }
4975}