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