Skip to main content

commonware_storage/journal/segmented/
variable.rs

1//! An append-only log for storing arbitrary variable length items.
2//!
3//! `segmented::Journal` is an append-only log for storing arbitrary variable length data on disk. In
4//! addition to replay, stored items can be directly retrieved given their section number and offset
5//! within the section.
6//!
7//! # Format
8//!
9//! Data stored in `Journal` is persisted in one of many Blobs within a caller-provided `partition`.
10//! The particular [Blob] in which data is stored is identified by a `section` number (`u64`).
11//! Within a `section`, data is appended as an `item` with the following format:
12//!
13//! ```text
14//! +---+---+---+---+---+---+---+---+
15//! |       0 ~ 4       |    ...    |
16//! +---+---+---+---+---+---+---+---+
17//! | Size (varint u32) |   Data    |
18//! +---+---+---+---+---+---+---+---+
19//! ```
20//!
21//! # Open Blobs
22//!
23//! `Journal` uses 1 `commonware-storage::Blob` per `section` to store data. All `Blobs` in a given
24//! `partition` are kept open during the lifetime of `Journal`. If the caller wishes to bound the
25//! number of open `Blobs`, they can group data into fewer `sections` and/or prune unused
26//! `sections`.
27//!
28//! # Sync
29//!
30//! Data written to `Journal` may not be immediately persisted to `Storage`. It is up to the caller
31//! to determine when to force pending data to be written to `Storage` using the `sync` (or
32//! `sync_all`) method.
33//!
34//! # Pruning
35//!
36//! All data appended to `Journal` must be assigned to some `section` (`u64`). This assignment
37//! allows the caller to prune data from `Journal` by specifying a minimum `section` number. This
38//! could be used, for example, by some blockchain application to prune old blocks.
39//!
40//! # Replay
41//!
42//! During application initialization, it is very common to replay data from `Journal` to recover
43//! some in-memory state. `Journal` is heavily optimized for this pattern and provides a `replay`
44//! method that consumes the journal into an owned [Replay] reader yielding all items in order
45//! of their `section` and `offset`. [Replay::finish] returns the journal once exhausted.
46//!
47//! # Compression
48//!
49//! [Journal] supports optional zstd compression through [Config::compression]. Keep the choice
50//! between `None` and `Some(_)` fixed while stored items are retained. Only the compression level
51//! may change between initializations when compression is enabled.
52//!
53//! # Example
54//!
55//! ```rust
56//! use commonware_runtime::{Spawner, Runner, deterministic, buffer::paged::CacheRef};
57//! use commonware_storage::journal::segmented::variable::{Journal, Config};
58//! use commonware_utils::{NZUsize, NZU16};
59//!
60//! let executor = deterministic::Runner::default();
61//! executor.start(|context| async move {
62//!     // Create a page cache
63//!     let page_cache = CacheRef::new(NZU16!(1024), NZUsize!(10));
64//!
65//!     // Create a journal
66//!     let journal = Journal::init(context, Config {
67//!         partition: "partition".into(),
68//!         compression: None,
69//!         codec_config: (),
70//!         page_cache,
71//!         write_buffer: NZUsize!(1024 * 1024),
72//!     }).await.unwrap();
73//!
74//!     // Append data to the journal
75//!     let (journal, _, _) = journal.append(1, &128).await.unwrap();
76//!
77//!     // Sync the journal
78//!     journal.sync_all().await.unwrap();
79//! });
80//! ```
81
82use super::manager::{AppendFactory, Config as ManagerConfig, Manager};
83use crate::journal::{
84    Error,
85    frame::{
86        FrameInfo, Limited, UncompressedFrame, decode_item, decode_length_prefix,
87        encode_compressed_frame_into, find_frame, read_frame_at,
88    },
89};
90use bytes::Bytes;
91use commonware_codec::{Codec, CodecShared, Copying, Encode as _, varint::MAX_U32_VARINT_SIZE};
92use commonware_runtime::{
93    Blob, Buf, Error as RError, Handle, IoBuf, Metrics, ReadOptions, Storage,
94    buffer::paged::{CacheRef, Recovery as PagedRecovery, Replay as BlobReplay},
95};
96use std::{
97    collections::{BTreeSet, VecDeque},
98    num::NonZeroUsize,
99};
100use tracing::{trace, warn};
101
102/// Configuration for `Journal` storage.
103#[derive(Clone)]
104pub struct Config<C> {
105    /// The `commonware-runtime::Storage` partition to use
106    /// for storing journal blobs.
107    pub partition: String,
108
109    /// Optional compression level (using `zstd`) to apply to data before storing.
110    pub compression: Option<u8>,
111
112    /// The codec configuration to use for encoding and decoding items.
113    pub codec_config: C,
114
115    /// The page cache to use for caching data.
116    pub page_cache: CacheRef,
117
118    /// The size of the write buffer to use for each blob.
119    pub write_buffer: NonZeroUsize,
120}
121
122/// State for replaying a single section's blob.
123struct SectionReplay<B: Blob> {
124    section: u64,
125    reader: BlobReplay<B>,
126    skip_bytes: u64,
127    offset: u64,
128    valid_offset: u64,
129    pending: Option<(usize, usize)>,
130}
131
132/// The journal's state, boxed so the public [Journal] handle stays pointer-sized.
133struct Inner<E: Storage + Metrics, V: Codec> {
134    manager: Manager<E, AppendFactory>,
135
136    /// Nonempty sections opened at initialization that have not been replayed from offset zero.
137    /// Public replay may repair them, and appends are blocked until their full replay succeeds.
138    unrecovered: BTreeSet<u64>,
139
140    /// Compression level (if enabled).
141    compression: Option<u8>,
142
143    /// Codec configuration.
144    codec_config: V::Cfg,
145}
146
147impl<E: Storage + Metrics, V: Codec> Inner<E, V> {
148    /// The section's writer. Only a failed or cancelled repair removes a replayed section, and
149    /// either ends the replay.
150    fn writer(&self, section: u64) -> Result<&PagedRecovery<E::Blob>, Error> {
151        Ok(self
152            .manager
153            .get(section)?
154            .expect("replayed section is present"))
155    }
156}
157
158impl<E: Storage + Metrics, V: CodecShared> Inner<E, V> {
159    /// See [Journal::init].
160    async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
161        let manager_cfg = ManagerConfig {
162            partition: cfg.partition,
163            factory: AppendFactory {
164                write_buffer: cfg.write_buffer,
165                page_cache_ref: cfg.page_cache,
166            },
167        };
168        let manager = Manager::init(context, manager_cfg).await?;
169        let mut unrecovered = BTreeSet::new();
170        for section in manager.sections() {
171            if manager.size(section)? != 0 {
172                unrecovered.insert(section);
173            }
174        }
175
176        Ok(Self {
177            manager,
178            unrecovered,
179            compression: cfg.compression,
180            codec_config: cfg.codec_config,
181        })
182    }
183
184    /// Reads an item from the blob at the given offset.
185    async fn read(
186        compressed: bool,
187        cfg: &V::Cfg,
188        blob: &PagedRecovery<E::Blob>,
189        offset: u64,
190    ) -> Result<(u64, u32, V), Error> {
191        read_frame_at(blob, offset, cfg, compressed).await
192    }
193
194    /// See [Journal::append].
195    async fn append(&mut self, section: u64, item: &V) -> Result<(u64, u32), Error> {
196        assert!(
197            !self.unrecovered.contains(&section),
198            "section {section} must be replayed before append"
199        );
200
201        // Size and validate the frame before creating the section so a rejected item leaves no
202        // empty section.
203        let (offset, item_len) = if let Some(level) = self.compression {
204            // Buffer compressed output to determine its length before appending the frame.
205            let mut buf = Vec::new();
206            let item_len = encode_compressed_frame_into(level, item, &mut buf)?;
207            let blob = self.manager.get_or_create(section).await?;
208            let offset = match blob.try_append(&buf) {
209                Some(offset) => offset,
210                None => {
211                    // Return the writer to the manager only after the owned append succeeds.
212                    let blob = self.manager.take(section).await?;
213                    let (blob, offset) = blob.append_owned(IoBuf::from(buf)).await?;
214                    self.manager.put(section, blob);
215                    offset
216                }
217            };
218            (offset, item_len)
219        } else {
220            // Encode directly into the write buffer when the frame fits.
221            let frame = UncompressedFrame::new(item)?;
222            let blob = self.manager.get_or_create(section).await?;
223            let offset = match blob.try_append_value(&frame) {
224                Some(offset) => offset,
225                None => {
226                    // Return the writer to the manager only after the owned append succeeds.
227                    let blob = self.manager.take(section).await?;
228                    let (blob, offset) = blob.append_owned(frame.encode_mut().into()).await?;
229                    self.manager.put(section, blob);
230                    offset
231                }
232            };
233            (offset, frame.item_len)
234        };
235        trace!(blob = section, offset, "appended item");
236        Ok((offset, item_len))
237    }
238
239    /// See [Journal::get].
240    async fn get(&self, section: u64, offset: u64) -> Result<V, Error> {
241        let blob = self
242            .manager
243            .get(section)?
244            .ok_or(Error::SectionOutOfRange(section))?;
245
246        // Perform a multi-op read.
247        let (_, _, item) =
248            Self::read(self.compression.is_some(), &self.codec_config, blob, offset).await?;
249        Ok(item)
250    }
251
252    /// See [Journal::get_many].
253    async fn get_many(&self, section: u64, offsets: &[u64]) -> Result<Vec<V>, Error> {
254        if offsets.is_empty() {
255            return Ok(Vec::new());
256        }
257        let blob = self
258            .manager
259            .get(section)?
260            .ok_or(Error::SectionOutOfRange(section))?;
261
262        let compressed = self.compression.is_some();
263        let cfg = &self.codec_config;
264        let mut items = Vec::with_capacity(offsets.len());
265        for &offset in offsets {
266            let (_, _, item) = Self::read(compressed, cfg, blob, offset).await?;
267            items.push(item);
268        }
269        Ok(items)
270    }
271
272    /// See [Journal::try_get_sync].
273    fn try_get_sync(&self, section: u64, offset: u64) -> Option<V> {
274        let blob = self.manager.get(section).ok()??;
275        let remaining = blob.size().checked_sub(offset)?;
276        let header_len = usize::try_from(remaining.min(MAX_U32_VARINT_SIZE as u64)).ok()?;
277        if header_len == 0 {
278            return None;
279        }
280
281        // Read the varint header to determine item size.
282        let mut header = [0u8; MAX_U32_VARINT_SIZE];
283        if !blob.try_read_sync_into(&mut header[..header_len], offset) {
284            return None;
285        }
286        let mut cursor = Copying(&header[..header_len]);
287        let (_, frame_info) = find_frame(&mut cursor, offset).ok()?;
288        let (varint_len, data_len) = match frame_info {
289            FrameInfo::Complete {
290                varint_len,
291                data_len,
292            } => (varint_len, data_len),
293            FrameInfo::Incomplete {
294                varint_len,
295                total_len,
296                ..
297            } => (varint_len, total_len),
298        };
299        let item_len = varint_len.checked_add(data_len)?;
300        if item_len > usize::try_from(remaining).ok()? {
301            return None;
302        }
303
304        // If the full item fits in the header read, decode directly.
305        let compressed = self.compression.is_some();
306        if item_len <= header_len {
307            return decode_item::<V>(
308                Copying(&header[varint_len..varint_len + data_len]),
309                &self.codec_config,
310                compressed,
311            )
312            .ok();
313        }
314
315        // Otherwise try reading the full item from cache. The buffer holds exactly the frame, so
316        // skipping the varint leaves the item.
317        let mut buf = vec![0u8; item_len];
318        if !blob.try_read_sync_into(&mut buf, offset) {
319            return None;
320        }
321        let mut buf = Bytes::from(buf);
322        buf.advance(varint_len);
323        decode_item::<V>(buf, &self.codec_config, compressed).ok()
324    }
325
326    /// See [Journal::size].
327    fn size(&self, section: u64) -> Result<u64, Error> {
328        self.manager.size(section)
329    }
330
331    /// See [Journal::sync].
332    async fn sync(mut self: Box<Self>, sections: impl crate::Sections) -> Result<Box<Self>, Error> {
333        self.manager = self.manager.sync(sections).await?;
334        Ok(self)
335    }
336
337    /// See [Journal::start_sync].
338    async fn start_sync(
339        mut self: Box<Self>,
340        sections: impl crate::Sections,
341    ) -> Result<(Box<Self>, Handle<()>), Error> {
342        let (manager, handle) = self.manager.start_sync(sections).await?;
343        self.manager = manager;
344        Ok((self, handle))
345    }
346
347    /// See [Journal::sync_all].
348    async fn sync_all(mut self: Box<Self>) -> Result<Box<Self>, Error> {
349        self.manager = self.manager.sync_all().await?;
350        Ok(self)
351    }
352
353    /// See [Journal::prune].
354    async fn prune(mut self: Box<Self>, min: u64) -> Result<(Box<Self>, bool), Error> {
355        let (manager, pruned) = self.manager.prune(min).await?;
356        self.manager = manager;
357        if pruned {
358            self.unrecovered.retain(|section| *section >= min);
359        }
360        Ok((self, pruned))
361    }
362
363    /// See [Journal::pruned].
364    const fn pruned(&self, section: u64) -> bool {
365        self.manager.pruned(section)
366    }
367
368    /// See [Journal::oldest_section].
369    fn oldest_section(&self) -> Option<u64> {
370        self.manager.oldest_section()
371    }
372
373    /// See [Journal::newest_section].
374    fn newest_section(&self) -> Option<u64> {
375        self.manager.newest_section()
376    }
377
378    /// See [Journal::is_empty].
379    fn is_empty(&self) -> bool {
380        self.manager.is_empty()
381    }
382
383    /// See [Journal::num_sections].
384    fn num_sections(&self) -> usize {
385        self.manager.num_sections()
386    }
387
388    /// See [Journal::destroy].
389    async fn destroy(self) -> Result<(), Error> {
390        self.manager.destroy().await
391    }
392
393    /// See [Journal::clear].
394    async fn clear(mut self: Box<Self>) -> Result<Box<Self>, Error> {
395        self.manager = self.manager.clear().await?;
396        self.unrecovered.clear();
397        Ok(self)
398    }
399}
400
401/// A segmented journal with variable-size entries.
402///
403/// Each section is stored in a separate blob. Items are length-prefixed with a varint.
404///
405/// # Repair
406///
407/// Like
408/// [sqlite](https://github.com/sqlite/sqlite/blob/8658a8df59f00ec8fcfea336a2a6a4b5ef79d2ee/src/wal.c#L1504-L1505)
409/// and
410/// [rocksdb](https://github.com/facebook/rocksdb/blob/0c533e61bc6d89fdf1295e8e0bcee4edb3aef401/include/rocksdb/options.h#L441-L445),
411/// the first invalid data read will be considered the new end of the journal (and the
412/// underlying [Blob] will be truncated to the last valid item). Repair occurs during
413/// replay (not init) because any blob could have trailing bytes.
414/// A nonempty section opened during initialization must be replayed from offset zero before it
415/// accepts new appends. Sections created during the current execution can be appended immediately.
416///
417/// Mutating functions consume the journal and return it only on success: an error (or a dropped
418/// future) destroys the handle. [Journal::replay] consumes the journal into an owned [Replay]
419/// reader, which returns it via [Replay::finish] once exhausted. Mutations on pruned sections
420/// fail with [Error::AlreadyPrunedToSection] without mutating. Check [Journal::pruned] first to
421/// keep the handle.
422pub struct Journal<E: Storage + Metrics, V: Codec>(Box<Inner<E, V>>);
423
424impl<E: Storage + Metrics, V: CodecShared> std::fmt::Debug for Journal<E, V> {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        f.debug_struct("Journal")
427            .field("oldest_section", &self.oldest_section())
428            .field("newest_section", &self.newest_section())
429            .finish_non_exhaustive()
430    }
431}
432
433impl<E: Storage + Metrics, V: CodecShared> Journal<E, V> {
434    /// Open through `(section, logical byte end)` and remove later sections.
435    ///
436    /// An end inside an item rounds down. All retained data is validated before returning. Payload
437    /// reads stop at the bound, but every section name is validated, including names above it.
438    pub async fn init_at_most(
439        context: E,
440        cfg: Config<V::Cfg>,
441        section: u64,
442        end: u64,
443    ) -> Result<Self, Error> {
444        let replay_buffer = cfg.write_buffer;
445        let mut end = end;
446        super::manager::truncate_paged_tail(
447            &context,
448            &cfg.partition,
449            cfg.page_cache.page_size(),
450            section,
451            end,
452        )
453        .await?;
454        let mut journal = Self::init(context, cfg).await?;
455
456        // Resolve a torn page before truncation tries to rewrite its partial checksum.
457        if let Some(blob) = journal.0.manager.get(section)?
458            && end > 0
459            && end < blob.size()
460        {
461            end = end.min(
462                blob.recoverable_prefix_len_at_most(0, end, replay_buffer, ReadOptions::default())
463                    .await?,
464            );
465        }
466        journal.0.manager = journal.0.manager.truncate_pending(section, end).await?;
467        let mut replay = journal
468            .replay(0, 0, replay_buffer, ReadOptions::default())
469            .await?;
470        while let Some(item) = replay.next().await {
471            item?;
472        }
473        replay.finish()
474    }
475
476    /// Initialize a new `Journal` instance.
477    ///
478    /// All backing blobs are opened but not read during
479    /// initialization. The `replay` method can be used
480    /// to iterate over all items in the `Journal`.
481    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
482        Ok(Self(Box::new(Inner::init(context, cfg).await?)))
483    }
484
485    /// Consumes the journal and returns an owned [Replay] reader over all items starting
486    /// with the item at the given `start_section` and `start_offset` into that section.
487    ///
488    /// Setup flushes buffered pages so the reader observes every accepted write. It
489    /// validates the requested start bound and copies each replayed section's partial tail
490    /// page, but does not allocate `buffer` bytes per blob. Read buffers are allocated
491    /// lazily as the reader advances. Every backing blob read performed by the returned
492    /// replay uses `read_options`, including reads after advancing to another section.
493    ///
494    /// A nonzero start must be a boundary already validated by a prior replay or a durable
495    /// marker: torn-page repair treats everything below it as proven.
496    pub async fn replay(
497        mut self,
498        start_section: u64,
499        start_offset: u64,
500        buffer: NonZeroUsize,
501        read_options: ReadOptions,
502    ) -> Result<Replay<E, V>, Error> {
503        // Capture independent replay views while returning each writer to the manager.
504        let mut sections = VecDeque::new();
505        let replayed: Vec<_> = self.0.manager.sections_from(start_section).collect();
506        for section in replayed {
507            let blob = self.0.manager.take(section).await?;
508            let (blob, reader) = blob.replay(buffer, read_options).await?;
509            self.0.manager.put(section, blob);
510
511            // Only the first section skips bytes preceding the validated starting boundary.
512            let skip_bytes = if section == start_section {
513                start_offset
514            } else {
515                0
516            };
517            sections.push_back(SectionReplay {
518                section,
519                reader,
520                skip_bytes,
521                offset: 0,
522                valid_offset: skip_bytes,
523                pending: None,
524            });
525        }
526        let finished = sections.is_empty();
527        let replay = Replay {
528            journal: self,
529            sections,
530            recovered_from: if start_offset == 0 {
531                Some(start_section)
532            } else {
533                start_section.checked_add(1)
534            },
535            buffer,
536            read_options,
537            finished,
538            errored: false,
539            repairing: false,
540        };
541
542        // A start offset beyond the front section's apparent tail can never resolve to an
543        // item boundary. Reject it up front rather than yielding a silently empty replay:
544        // the offset is caller-supplied and unvalidated, so it must never be adopted.
545        if let Some(current) = replay.sections.front()
546            && current.section == start_section
547            && start_offset > current.reader.blob_size()
548        {
549            return Err(Error::ItemOutOfRange(start_offset));
550        }
551        Ok(replay)
552    }
553
554    /// Appends an item to `Journal` in a given `section`, returning the offset
555    /// where the item was written and the size of the item (which may differ
556    /// from the raw encoded size if compression is enabled).
557    ///
558    /// # Panics
559    ///
560    /// Panics when `section` contained data at initialization and has not completed a replay
561    /// from offset zero.
562    pub async fn append(mut self, section: u64, item: &V) -> Result<(Self, u64, u32), Error> {
563        let (offset, item_len) = self.0.append(section, item).await?;
564        Ok((self, offset, item_len))
565    }
566
567    /// Retrieves an item from `Journal` at a given `section` and `offset`.
568    ///
569    /// # Errors
570    ///  - [Error::AlreadyPrunedToSection] if the requested `section` has been pruned during the
571    ///    current execution.
572    ///  - [Error::SectionOutOfRange] if the requested `section` is empty (i.e. has never had any
573    ///    data appended to it, or has been pruned in a previous execution).
574    ///  - An invalid `offset` for a given section (that is, an offset that doesn't correspond to a
575    ///    previously appended item) will result in an error, with the specific type being
576    ///    undefined.
577    pub async fn get(&self, section: u64, offset: u64) -> Result<V, Error> {
578        self.0.get(section, offset).await
579    }
580
581    /// Read multiple items from the same section.
582    ///
583    /// Offsets should be sorted in ascending order.
584    pub async fn get_many(&self, section: u64, offsets: &[u64]) -> Result<Vec<V>, Error> {
585        self.0.get_many(section, offsets).await
586    }
587
588    /// Get an item if it can be done synchronously (e.g. without I/O), returning `None` otherwise.
589    pub fn try_get_sync(&self, section: u64, offset: u64) -> Option<V> {
590        self.0.try_get_sync(section, offset)
591    }
592
593    /// Gets the size of the journal for a specific section.
594    ///
595    /// Returns 0 if the section does not exist.
596    pub fn size(&self, section: u64) -> Result<u64, Error> {
597        self.0.size(section)
598    }
599
600    /// Ensures the given `sections` are synced to the underlying store.
601    ///
602    /// If a selected section does not exist (and has not been pruned), no error will be
603    /// returned.
604    pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
605        self.0 = self.0.sync(sections).await?;
606        Ok(self)
607    }
608
609    /// Start syncing the given `sections` to storage.
610    ///
611    /// An error reported by the returned [Handle] is fatal to the journal: the caller
612    /// must stop using the returned journal.
613    pub async fn start_sync(
614        mut self,
615        sections: impl crate::Sections,
616    ) -> Result<(Self, Handle<()>), Error> {
617        let (inner, handle) = self.0.start_sync(sections).await?;
618        self.0 = inner;
619        Ok((self, handle))
620    }
621
622    /// Syncs all open sections.
623    pub async fn sync_all(mut self) -> Result<Self, Error> {
624        self.0 = self.0.sync_all().await?;
625        Ok(self)
626    }
627
628    /// Prunes all `sections` less than `min`. Returns true if any sections were pruned.
629    pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
630        let (inner, pruned) = self.0.prune(min).await?;
631        self.0 = inner;
632        Ok((self, pruned))
633    }
634
635    /// Returns true when `section` is below the prune floor.
636    ///
637    /// The floor only tracks prunes from the current execution and resets at init, so a
638    /// section pruned in a previous execution reports false.
639    pub fn pruned(&self, section: u64) -> bool {
640        self.0.pruned(section)
641    }
642
643    /// Returns the number of the oldest section in the journal.
644    pub fn oldest_section(&self) -> Option<u64> {
645        self.0.oldest_section()
646    }
647
648    /// Returns the number of the newest section in the journal.
649    pub fn newest_section(&self) -> Option<u64> {
650        self.0.newest_section()
651    }
652
653    /// Returns true if no sections exist.
654    pub fn is_empty(&self) -> bool {
655        self.0.is_empty()
656    }
657
658    /// Returns the number of sections.
659    pub fn num_sections(&self) -> usize {
660        self.0.num_sections()
661    }
662
663    /// Removes any underlying blobs created by the journal.
664    pub async fn destroy(self) -> Result<(), Error> {
665        self.0.destroy().await
666    }
667
668    /// Clear all data, resetting the journal to an empty state.
669    ///
670    /// Unlike `destroy`, this keeps the journal alive so it can be reused.
671    pub async fn clear(mut self) -> Result<Self, Error> {
672        self.0 = self.0.clear().await?;
673        Ok(self)
674    }
675}
676
677/// Owned replay reader over a [Journal]'s items.
678///
679/// Yields `(section, offset, size, item)` in order and repairs invalid trailing data as it is
680/// encountered. Dropping the reader before it is exhausted destroys the journal (leaving later
681/// sections unrepaired): recovery is re-initialization. Call [Replay::finish] on an exhausted
682/// reader to get the journal back.
683pub struct Replay<E: Storage + Metrics, V: Codec> {
684    journal: Journal<E, V>,
685    sections: VecDeque<SectionReplay<E::Blob>>,
686    /// The first section this replay fully covers: [Replay::finish] marks it and every
687    /// later section recovered.
688    recovered_from: Option<u64>,
689    buffer: NonZeroUsize,
690    read_options: ReadOptions,
691    finished: bool,
692    errored: bool,
693    repairing: bool,
694}
695
696impl<E: Storage + Metrics, V: CodecShared> Replay<E, V> {
697    /// Validate that the front section's checksum failure is a repairable torn page, returning
698    /// the truncation target.
699    async fn plan_repair(&mut self, source: RError) -> Result<u64, Error> {
700        // Only a checksum failure is repairable: it marks a torn write, while any other error
701        // is an I/O failure this repair must not mask.
702        if !matches!(source, RError::InvalidChecksum) {
703            return Err(source.into());
704        }
705
706        // The bytes already replayed are validated: they bound the truncation from below.
707        let current = self.sections.front().expect("replayed section is present");
708        let section = current.section;
709        if !self.journal.0.unrecovered.contains(&section) {
710            return Err(source.into());
711        }
712        let size = current.reader.blob_size();
713        let valid_offset = current.valid_offset;
714
715        // Forward-validate from the replayed prefix to find where well-formed pages end.
716        let recoverable = self
717            .journal
718            .0
719            .writer(section)?
720            .recoverable_prefix_len(valid_offset, self.buffer, self.read_options)
721            .await?;
722
723        // A whole-blob recoverable prefix means the checksum failure did not come from a torn
724        // page: surface the original error instead of truncating valid data.
725        if recoverable >= size {
726            return Err(source.into());
727        }
728
729        // The cut must not drop below the validated replay prefix: that would lose data the
730        // replay already handed out.
731        if recoverable < valid_offset {
732            return Err(Error::ItemOutOfRange(valid_offset));
733        }
734
735        Ok(recoverable)
736    }
737
738    /// Repair a torn page discovered by ordered replay and resume at the last complete item.
739    async fn repair(&mut self, source: RError) -> Result<(), Error> {
740        // A rejected plan mutates nothing: drop the damaged section and surface its error.
741        let recoverable = match self.plan_repair(source).await {
742            Ok(target) => target,
743            Err(err) => {
744                self.sections.pop_front();
745                return Err(err);
746            }
747        };
748
749        let current = self.sections.front().expect("replayed section is present");
750        let (section, valid_offset) = (current.section, current.valid_offset);
751        warn!(
752            section,
753            invalid_size = current.reader.blob_size(),
754            new_size = recoverable,
755            "torn page detected: truncating"
756        );
757
758        // Once mutation begins, a dropped future drops the section's writer. Keep the interruption
759        // guard set until the repaired reader has replaced the stale one.
760        self.repairing = true;
761        let current = self
762            .sections
763            .pop_front()
764            .expect("repaired section is present");
765        drop(current.reader);
766        let writer = self.journal.0.manager.take(section).await?;
767        let writer = writer.truncate(recoverable).await?;
768        let (writer, mut reader) = writer.replay(self.buffer, self.read_options).await?;
769        self.journal.0.manager.put(section, writer);
770        reader.seek_to(valid_offset)?;
771        self.sections.push_front(SectionReplay {
772            section,
773            reader,
774            skip_bytes: 0,
775            offset: valid_offset,
776            valid_offset,
777            pending: None,
778        });
779        self.repairing = false;
780        Ok(())
781    }
782
783    /// Truncate the front section to its validated prefix and make the repair durable.
784    async fn repair_tail(&mut self, message: &'static str) -> Result<(), Error> {
785        let current = self.sections.front().expect("replayed section is present");
786        let (section, offset, valid_offset) =
787            (current.section, current.offset, current.valid_offset);
788        if !self.journal.0.unrecovered.contains(&section) {
789            return Err(Error::ItemOutOfRange(offset));
790        }
791        warn!(
792            blob = section,
793            bad_offset = offset,
794            new_size = valid_offset,
795            "{message}"
796        );
797
798        // Tail repair is exceptional. Make it durable immediately so callers do not need to
799        // track replay-time repaired sections separately. Keep the interruption guard set
800        // until the repair is durable.
801        self.repairing = true;
802        let writer = self.journal.0.manager.take(section).await?;
803        let writer = writer.truncate(valid_offset).await?;
804        self.journal.0.manager.put(section, writer);
805        self.repairing = false;
806        Ok(())
807    }
808
809    /// Returns the next `(section, offset, size, item)`, or `None` once every section is
810    /// exhausted.
811    ///
812    /// An error ends the section that produced it, and iteration continues with the next section.
813    /// Errors while mutating storage to repair a section, and [Error::ReplayInterrupted], end the
814    /// replay.
815    pub async fn next(&mut self) -> Option<Result<(u64, u64, u32, V), Error>> {
816        // An unfinished repair may have dropped the section's writer. A cancelled repair still
817        // needs an error. A completed failure already yielded one.
818        if self.repairing {
819            self.repairing = false;
820            self.sections.clear();
821            if !self.errored {
822                return self.fail(Error::ReplayInterrupted);
823            }
824        }
825        while let Some(current) = self.sections.front_mut() {
826            let blob_size = current.reader.blob_size();
827
828            // Resume a recorded frame header or decode the next one
829            let (item_size, varint_len) = match current.pending {
830                Some(header) => header,
831                None => {
832                    // Ensure we have enough data for varint header.
833                    // ensure() returns Ok(false) if exhausted with fewer bytes,
834                    // but we still try to decode from remaining bytes.
835                    match current.reader.ensure(MAX_U32_VARINT_SIZE).await {
836                        Ok(true) => {}
837                        Ok(false) => {
838                            // Reader exhausted - check if buffer is empty
839                            if current.reader.remaining() == 0 {
840                                self.sections.pop_front();
841                                continue;
842                            }
843                            // Buffer still has data - continue to try decoding
844                        }
845                        Err(err) => {
846                            if let Err(err) = self.repair(err).await {
847                                return self.fail(err);
848                            }
849                            continue;
850                        }
851                    }
852
853                    // Skip bytes if needed (for start_offset)
854                    if current.skip_bytes > 0 {
855                        let to_skip =
856                            current.skip_bytes.min(current.reader.remaining() as u64) as usize;
857                        current.reader.advance(to_skip);
858                        current.skip_bytes -= to_skip as u64;
859                        current.offset += to_skip as u64;
860                        continue;
861                    }
862
863                    // Try to decode length prefix
864                    let before_remaining = current.reader.remaining();
865                    match decode_length_prefix(&mut current.reader) {
866                        Ok(header) => {
867                            // Record the header before awaiting the body so a dropped
868                            // next future resumes losslessly.
869                            current.pending = Some(header);
870                            header
871                        }
872                        Err(err) => {
873                            // Could be incomplete varint - check if reader exhausted
874                            if current.reader.is_exhausted()
875                                || before_remaining < MAX_U32_VARINT_SIZE
876                            {
877                                // Treat as trailing bytes
878                                if current.valid_offset < blob_size
879                                    && current.offset < blob_size
880                                    && let Err(err) = self
881                                        .repair_tail("trailing bytes detected: truncating")
882                                        .await
883                                {
884                                    self.sections.pop_front();
885                                    return self.fail(err);
886                                }
887                                self.sections.pop_front();
888                                continue;
889                            }
890                            self.sections.pop_front();
891                            return self.fail(err);
892                        }
893                    }
894                }
895            };
896
897            // Ensure we have enough data for item body
898            match current.reader.ensure(item_size).await {
899                Ok(true) => {}
900                Ok(false) => {
901                    // Incomplete item at end - truncate
902                    if let Err(err) = self.repair_tail("incomplete item at end: truncating").await {
903                        self.sections.pop_front();
904                        return self.fail(err);
905                    }
906                    self.sections.pop_front();
907                    continue;
908                }
909                Err(err) => {
910                    if let Err(err) = self.repair(err).await {
911                        return self.fail(err);
912                    }
913                    continue;
914                }
915            }
916
917            // Decode the item without reading past its frame
918            let item_offset = current.offset;
919            let next_offset = match current
920                .offset
921                .checked_add(varint_len as u64)
922                .and_then(|o| o.checked_add(item_size as u64))
923            {
924                Some(o) => o,
925                None => {
926                    self.sections.pop_front();
927                    return self.fail(Error::OffsetOverflow);
928                }
929            };
930            match decode_item::<V>(
931                Limited::new(&mut current.reader, item_size),
932                &self.journal.0.codec_config,
933                self.journal.0.compression.is_some(),
934            ) {
935                Ok(decoded) => {
936                    current.pending = None;
937                    current.valid_offset = next_offset;
938                    current.offset = next_offset;
939                    return Some(Ok((
940                        current.section,
941                        item_offset,
942                        item_size as u32,
943                        decoded,
944                    )));
945                }
946                Err(err) => {
947                    self.sections.pop_front();
948                    return self.fail(err);
949                }
950            }
951        }
952        self.finished = true;
953        None
954    }
955
956    /// Records a yielded error, which is fatal to the journal.
957    const fn fail(&mut self, err: Error) -> Option<Result<(u64, u64, u32, V), Error>> {
958        self.errored = true;
959        Some(Err(err))
960    }
961
962    /// Returns the journal.
963    ///
964    /// Fails when the reader was not fully drained or yielded an error: the journal is
965    /// destroyed and recovery is re-initialization.
966    pub fn finish(mut self) -> Result<Journal<E, V>, Error> {
967        if self.errored || !self.finished {
968            return Err(Error::ReplayFailed);
969        }
970        if let Some(start) = self.recovered_from {
971            self.journal
972                .0
973                .unrecovered
974                .retain(|section| *section < start);
975        }
976        Ok(self.journal)
977    }
978}
979
980#[cfg(test)]
981mod tests {
982    use super::*;
983    use crate::journal::frame::encode_frame_into;
984    use commonware_codec::{EncodeSize, Write as _, varint::UInt};
985    use commonware_macros::test_traced;
986    use commonware_runtime::{
987        Blob, BufMut, Runner, Storage, Supervisor as _, WriteOptions,
988        buffer::paged::corrupt_page,
989        deterministic,
990        mocks::{DelayedSyncContext, PendingSyncs, RecordingContext, release_pending_syncs},
991    };
992    use commonware_utils::{NZU16, NZUsize, probability};
993    use std::num::NonZeroU16;
994
995    impl<E: Storage + Metrics, V: CodecShared> Inner<E, V> {
996        /// Append raw bytes, which need not form a valid frame, to `section`.
997        async fn append_raw(&mut self, section: u64, buf: IoBuf) -> Result<u64, Error> {
998            let blob = self.manager.take(section).await?;
999            let (blob, offset) = blob.append_owned(buf).await?;
1000            self.manager.put(section, blob);
1001            Ok(offset)
1002        }
1003    }
1004
1005    impl<E: Storage + Metrics, V: CodecShared> Journal<E, V> {
1006        async fn test_reopen_at_most(self, section: u64, end: u64) -> Result<Self, Error> {
1007            let (context, partition, factory) = self.0.manager.test_configuration();
1008            let cfg = Config {
1009                partition,
1010                write_buffer: factory.write_buffer,
1011                page_cache: factory.page_cache_ref,
1012                compression: self.0.compression,
1013                codec_config: self.0.codec_config.clone(),
1014            };
1015            _ = self.sync_all().await?;
1016            Self::init_at_most(context, cfg, section, end).await
1017        }
1018
1019        async fn test_reopen_section(self, section: u64, end: u64) -> Result<Self, Error> {
1020            let (context, partition, factory) = self.0.manager.test_configuration();
1021            let replay_buffer = factory.write_buffer;
1022            let cfg = Config {
1023                partition,
1024                write_buffer: replay_buffer,
1025                page_cache: factory.page_cache_ref,
1026                compression: self.0.compression,
1027                codec_config: self.0.codec_config.clone(),
1028            };
1029            _ = self.sync_all().await?;
1030            let mut journal = Self::init(context, cfg).await?;
1031            journal.0.manager = journal
1032                .0
1033                .manager
1034                .truncate_pending_section(section, end)
1035                .await?;
1036            let mut replay = journal
1037                .replay(0, 0, replay_buffer, ReadOptions::default())
1038                .await?;
1039            while let Some(item) = replay.next().await {
1040                item?;
1041            }
1042            replay.finish()
1043        }
1044    }
1045
1046    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
1047    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
1048
1049    async fn journal_with_torn_interior_page(
1050        context: &deterministic::Context,
1051        partition: &str,
1052        later_section: bool,
1053    ) -> Journal<deterministic::Context, u64> {
1054        const LOGICAL_PAGE_SIZE: u64 = 64;
1055        const FIRST_SECTION: u64 = 0;
1056        const TORN_SECTION: u64 = 1;
1057
1058        let cfg = Config {
1059            partition: partition.into(),
1060            compression: None,
1061            codec_config: (),
1062            page_cache: CacheRef::new(NZU16!(LOGICAL_PAGE_SIZE as u16), NZUsize!(4)),
1063            write_buffer: NZUsize!(256),
1064        };
1065        let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
1066            .await
1067            .unwrap();
1068        (journal, _, _) = journal.append(FIRST_SECTION, &u64::MAX).await.unwrap();
1069        for value in 0..15u64 {
1070            let offset;
1071            (journal, offset, _) = journal.append(TORN_SECTION, &value).await.unwrap();
1072            assert_eq!(offset, value * 9);
1073        }
1074        if later_section {
1075            (journal, _, _) = journal.append(2, &u64::MIN).await.unwrap();
1076        }
1077        journal = journal.sync_all().await.unwrap();
1078        drop(journal);
1079
1080        corrupt_page(
1081            context,
1082            &cfg.partition,
1083            &TORN_SECTION.to_be_bytes(),
1084            1,
1085            LOGICAL_PAGE_SIZE,
1086        )
1087        .await;
1088
1089        Journal::<_, u64>::init(context.child("recover"), cfg)
1090            .await
1091            .unwrap()
1092    }
1093
1094    #[test]
1095    fn test_segmented_capped_init_bounds_torn_suffix_reads() {
1096        deterministic::Runner::default().start(|context| async move {
1097            for source_section in [0, 1] {
1098                for cap in [0, 9] {
1099                    // Compare a short and long discarded suffix behind the same retained cap.
1100                    let mut counts = Vec::new();
1101                    for pages in [16, 4096] {
1102                        let partition = format!("torn-cap-{source_section}-{cap}-{pages}");
1103                        let mut page = Vec::new();
1104                        for value in 0..8u64 {
1105                            encode_frame_into(None, &value, &mut page).unwrap();
1106                        }
1107                        assert_eq!(page.len(), 72);
1108                        super::super::manager::tests::seed_torn_suffix(
1109                            &context,
1110                            &partition,
1111                            source_section,
1112                            &page,
1113                            pages,
1114                        )
1115                        .await;
1116
1117                        // Bounded initialization must read independently of the discarded suffix.
1118                        let cfg = Config {
1119                            partition,
1120                            page_cache: CacheRef::new(NZU16!(72), NZUsize!(4)),
1121                            write_buffer: NZUsize!(144),
1122                            compression: None,
1123                            codec_config: (),
1124                        };
1125                        let (recorded, recordings) = RecordingContext::new(context.child("cap"));
1126                        let mut journal =
1127                            Journal::<_, u64>::init_at_most(recorded, cfg.clone(), 0, cap)
1128                                .await
1129                                .unwrap();
1130                        let retained = if source_section == 0 { cap } else { 0 };
1131                        assert_eq!(journal.size(0).unwrap(), retained);
1132                        counts.push(recordings.snapshot().reads.len());
1133
1134                        // Appending after the cap must not reconnect any discarded frames.
1135                        (journal, _, _) = journal.append(0, &999).await.unwrap();
1136                        drop(journal.sync_all().await.unwrap());
1137                        let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1138                            .await
1139                            .unwrap();
1140                        let mut replay = journal
1141                            .replay(0, 0, NZUsize!(144), ReadOptions::default())
1142                            .await
1143                            .unwrap();
1144                        let mut actual = Vec::new();
1145                        while let Some(item) = replay.next().await {
1146                            actual.push(item.unwrap().3);
1147                        }
1148                        replay.finish().unwrap();
1149                        let expected = if retained == 0 {
1150                            vec![999]
1151                        } else {
1152                            vec![0, 999]
1153                        };
1154                        assert_eq!(actual, expected);
1155                    }
1156
1157                    // Suffix length must not change the number of reads needed for the same cap.
1158                    assert_eq!(
1159                        counts[0], counts[1],
1160                        "discarded suffix reads grew: {counts:?}"
1161                    );
1162                }
1163            }
1164        });
1165    }
1166
1167    #[test_traced]
1168    fn test_bounded_initialization_bounds_scan() {
1169        deterministic::Runner::default().start(|context| async move {
1170            // Seed different journal lengths under the same page and buffer configuration.
1171            let mut counts = Vec::new();
1172            for count in [128, 65536] {
1173                let cfg = Config {
1174                    partition: format!("variable-cap-scan-{count}"),
1175                    page_cache: CacheRef::new(NZU16!(64), NZUsize!(4)),
1176                    write_buffer: NZUsize!(65536),
1177                    compression: None,
1178                    codec_config: (),
1179                };
1180                let mut journal = Journal::<_, u64>::init(
1181                    context.child(if count == 128 {
1182                        "seed_small"
1183                    } else {
1184                        "seed_large"
1185                    }),
1186                    cfg.clone(),
1187                )
1188                .await
1189                .unwrap();
1190                for value in 0..count {
1191                    (journal, _, _) = journal.append(0, &value).await.unwrap();
1192                }
1193                _ = journal.sync_all().await.unwrap();
1194
1195                // Record the reads required to reopen at the same one-item boundary.
1196                let (recorded, recordings) =
1197                    RecordingContext::new(context.child(if count == 128 {
1198                        "cap_small"
1199                    } else {
1200                        "cap_large"
1201                    }));
1202                let journal = Journal::<_, u64>::init_at_most(recorded, cfg, 0, 9)
1203                    .await
1204                    .unwrap();
1205                assert_eq!(journal.size(0).unwrap(), 9);
1206                counts.push(recordings.snapshot().reads.len());
1207            }
1208
1209            // Work below the cap must be independent of the discarded journal length.
1210            assert_eq!(
1211                counts[0], counts[1],
1212                "same one-item cap: small/large suffix read counts {counts:?}"
1213            );
1214        });
1215    }
1216
1217    #[test_traced]
1218    fn test_bounded_initialization_recovers_torn_page() {
1219        deterministic::Runner::default().start(|context| async move {
1220            for cap in [0, 9, 60, 63, 64, 65, 90, 128, 135, u64::MAX] {
1221                // Seed two sections so the bound must repair one and remove the later section.
1222                let cfg = Config {
1223                    partition: format!("capped-torn-{cap}"),
1224                    page_cache: CacheRef::new(NZU16!(64), NZUsize!(4)),
1225                    write_buffer: NZUsize!(256),
1226                    compression: None,
1227                    codec_config: (),
1228                };
1229                let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
1230                    .await
1231                    .unwrap();
1232                for value in 0..15 {
1233                    (journal, _, _) = journal.append(0, &value).await.unwrap();
1234                }
1235                (journal, _, _) = journal.append(1, &100).await.unwrap();
1236                _ = journal.sync_all().await.unwrap();
1237
1238                // A valid later page survives after the torn page in section zero.
1239                corrupt_page(&context, &cfg.partition, &0u64.to_be_bytes(), 1, 64).await;
1240                let journal =
1241                    Journal::<_, u64>::init_at_most(context.child("cap"), cfg.clone(), 0, cap)
1242                        .await
1243                        .unwrap();
1244                let expected_bytes = cap.min(63) / 9 * 9;
1245                assert_eq!(journal.size(0).unwrap(), expected_bytes, "cap {cap}");
1246                assert_eq!(journal.newest_section(), Some(0));
1247                drop(journal);
1248
1249                // A normal restart must retain the repaired prefix and append at its exact end.
1250                let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1251                    .await
1252                    .unwrap();
1253                let mut replay = journal
1254                    .replay(0, 0, NZUsize!(256), ReadOptions::default())
1255                    .await
1256                    .unwrap();
1257                let mut count = 0;
1258                while let Some(item) = replay.next().await {
1259                    let item = item.unwrap();
1260                    assert_eq!(item.0, 0);
1261                    assert_eq!(item.3, count);
1262                    count += 1;
1263                }
1264                assert_eq!(count, expected_bytes / 9);
1265                let mut journal = replay.finish().unwrap();
1266                let position;
1267                (journal, position, _) = journal.append(0, &99).await.unwrap();
1268                assert_eq!(position, expected_bytes);
1269                journal.destroy().await.unwrap();
1270            }
1271        });
1272    }
1273
1274    #[test_traced]
1275    #[should_panic(expected = "must be replayed before append")]
1276    fn test_segmented_variable_rejects_append_before_replay() {
1277        let executor = deterministic::Runner::default();
1278        executor.start(|context| async move {
1279            const PARTITION: &str = "segmented-variable-append-before-replay";
1280            const NEW_SECTION: u64 = 2;
1281            const TORN_SECTION: u64 = 1;
1282
1283            let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
1284            let (journal, offset, _) = journal.append(NEW_SECTION, &15).await.unwrap();
1285            assert_eq!(offset, 0);
1286            let (journal, offset, _) = journal.append(NEW_SECTION, &16).await.unwrap();
1287            assert_eq!(offset, 9);
1288            let _ = journal.append(TORN_SECTION, &15).await;
1289        });
1290    }
1291
1292    #[test_traced]
1293    #[should_panic(expected = "must be replayed before append")]
1294    fn test_segmented_variable_partial_replay_keeps_append_guard() {
1295        let executor = deterministic::Runner::default();
1296        executor.start(|context| async move {
1297            const PARTITION: &str = "segmented-variable-partial-replay-append";
1298            const SECTION: u64 = 1;
1299
1300            let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
1301            let mut replay = journal
1302                .replay(SECTION, 9, NZUsize!(1024), ReadOptions::default())
1303                .await
1304                .unwrap();
1305            while let Some(item) = replay.next().await {
1306                item.unwrap();
1307            }
1308            let journal = replay.finish().unwrap();
1309            let _ = journal.append(SECTION, &7).await;
1310        });
1311    }
1312
1313    #[test_traced]
1314    #[should_panic(expected = "must be replayed before append")]
1315    fn test_segmented_variable_gates_clean_older_section() {
1316        let executor = deterministic::Runner::default();
1317        executor.start(|context| async move {
1318            const PARTITION: &str = "segmented-variable-gate-clean-older-section";
1319
1320            // Sections 0 and 2 are intact and section 1 is torn. The oldest section is gated
1321            // even though it is neither torn nor the newest.
1322            let journal = journal_with_torn_interior_page(&context, PARTITION, true).await;
1323            let _ = journal.append(0, &7).await;
1324        });
1325    }
1326
1327    #[test_traced]
1328    fn test_segmented_variable_replay_propagates_read_options() {
1329        let executor = deterministic::Runner::default();
1330        executor.start(|context| async move {
1331            let (context, recordings) = RecordingContext::new(context);
1332            let cfg = Config {
1333                partition: "test-partition".into(),
1334                compression: None,
1335                codec_config: (),
1336                page_cache: CacheRef::new(NZU16!(8), PAGE_CACHE_SIZE),
1337                write_buffer: NZUsize!(1024),
1338            };
1339            let mut journal = Journal::init(context.child("storage"), cfg)
1340                .await
1341                .expect("failed to init");
1342
1343            for section in 1..=2 {
1344                (journal, _, _) = journal
1345                    .append(section, &section)
1346                    .await
1347                    .expect("failed to append");
1348            }
1349
1350            let mut replay = journal
1351                .replay(1, 0, NZUsize!(1036), ReadOptions::DONT_CACHE)
1352                .await
1353                .expect("failed to replay");
1354            recordings.clear();
1355
1356            // The first lazy refill must carry the caller's policy.
1357            let (section, offset, _, item) = replay
1358                .next()
1359                .await
1360                .expect("missing first replay item")
1361                .expect("failed to read first replay item");
1362            assert_eq!((section, offset, item), (1, 0, 1));
1363            let reads = recordings.snapshot().reads;
1364            assert!(!reads.is_empty());
1365            assert!(
1366                reads
1367                    .iter()
1368                    .all(|options| *options == ReadOptions::DONT_CACHE)
1369            );
1370
1371            // Crossing into the next section must preserve the same policy.
1372            recordings.clear();
1373            let (section, offset, _, item) = replay
1374                .next()
1375                .await
1376                .expect("missing second replay item")
1377                .expect("failed to read second replay item");
1378            assert_eq!((section, offset, item), (2, 0, 2));
1379            let reads = recordings.snapshot().reads;
1380            assert!(!reads.is_empty());
1381            assert!(
1382                reads
1383                    .iter()
1384                    .all(|options| *options == ReadOptions::DONT_CACHE)
1385            );
1386            assert!(replay.next().await.is_none());
1387
1388            let journal = replay.finish().expect("failed to finish replay");
1389            journal.destroy().await.expect("failed to destroy");
1390        });
1391    }
1392
1393    #[test_traced]
1394    fn test_journal_append_and_read() {
1395        // Initialize the deterministic context
1396        let executor = deterministic::Runner::default();
1397
1398        // Start the test within the executor
1399        executor.start(|context| async move {
1400            // Initialize the journal
1401            let cfg = Config {
1402                partition: "test-partition".into(),
1403                compression: None,
1404                codec_config: (),
1405                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1406                write_buffer: NZUsize!(1024),
1407            };
1408            let index = 1u64;
1409            let data = 10;
1410            let mut journal = Journal::init(context.child("first"), cfg.clone())
1411                .await
1412                .expect("Failed to initialize journal");
1413
1414            // Append an item to the journal
1415            (journal, _, _) = journal
1416                .append(index, &data)
1417                .await
1418                .expect("Failed to append data");
1419
1420            // Check metrics
1421            let buffer = context.encode();
1422            assert!(buffer.contains("first_tracked 1"));
1423
1424            // Drop and re-open the journal to simulate a restart
1425            journal = journal.sync(index).await.expect("Failed to sync journal");
1426            drop(journal);
1427            let journal = Journal::<_, i32>::init(context.child("second"), cfg)
1428                .await
1429                .expect("Failed to re-initialize journal");
1430
1431            // Replay the journal and collect items
1432            let mut items = Vec::new();
1433            let mut replay = journal
1434                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1435                .await
1436                .expect("unable to setup replay");
1437            while let Some(result) = replay.next().await {
1438                match result {
1439                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1440                    Err(err) => panic!("Failed to read item: {err}"),
1441                }
1442            }
1443
1444            // Verify that the item was replayed correctly
1445            assert_eq!(items.len(), 1);
1446            assert_eq!(items[0].0, index);
1447            assert_eq!(items[0].1, data);
1448
1449            // Check metrics
1450            let buffer = context.encode();
1451            assert!(buffer.contains("second_tracked 1"));
1452        });
1453    }
1454
1455    #[test_traced]
1456    fn test_journal_multiple_appends_and_reads() {
1457        // Initialize the deterministic context
1458        let executor = deterministic::Runner::default();
1459
1460        // Start the test within the executor
1461        executor.start(|context| async move {
1462            // Create a journal configuration
1463            let cfg = Config {
1464                partition: "test-partition".into(),
1465                compression: None,
1466                codec_config: (),
1467                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1468                write_buffer: NZUsize!(1024),
1469            };
1470
1471            // Initialize the journal
1472            let mut journal = Journal::init(context.child("first"), cfg.clone())
1473                .await
1474                .expect("Failed to initialize journal");
1475
1476            // Append multiple items to different blobs
1477            let data_items = vec![(1u64, 1), (1u64, 2), (2u64, 3), (3u64, 4)];
1478            for (index, data) in &data_items {
1479                (journal, _, _) = journal
1480                    .append(*index, data)
1481                    .await
1482                    .expect("Failed to append data");
1483                journal = journal.sync(*index).await.expect("Failed to sync blob");
1484            }
1485
1486            // Check metrics
1487            let buffer = context.encode();
1488            assert!(buffer.contains("first_tracked 3"));
1489            assert!(buffer.contains("first_synced_total 4"));
1490
1491            // Drop and re-open the journal to simulate a restart
1492            drop(journal);
1493            let mut journal = Journal::init(context.child("second"), cfg)
1494                .await
1495                .expect("Failed to re-initialize journal");
1496
1497            // Replay the journal and collect items
1498            let mut items = Vec::<(u64, u32)>::new();
1499            {
1500                let mut replay = journal
1501                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1502                    .await
1503                    .expect("unable to setup replay");
1504                while let Some(result) = replay.next().await {
1505                    match result {
1506                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1507                        Err(err) => panic!("Failed to read item: {err}"),
1508                    }
1509                }
1510                journal = replay.finish().expect("failed to finish replay");
1511            }
1512
1513            // Verify that all items were replayed correctly
1514            assert_eq!(items.len(), data_items.len());
1515            for ((expected_index, expected_data), (actual_index, actual_data)) in
1516                data_items.iter().zip(items.iter())
1517            {
1518                assert_eq!(actual_index, expected_index);
1519                assert_eq!(actual_data, expected_data);
1520            }
1521
1522            // Cleanup
1523            journal.destroy().await.expect("Failed to destroy journal");
1524        });
1525    }
1526
1527    #[test_traced]
1528    fn test_journal_prune_blobs() {
1529        // Initialize the deterministic context
1530        let executor = deterministic::Runner::default();
1531
1532        // Start the test within the executor
1533        executor.start(|context| async move {
1534            // Create a journal configuration
1535            let cfg = Config {
1536                partition: "test-partition".into(),
1537                compression: None,
1538                codec_config: (),
1539                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1540                write_buffer: NZUsize!(1024),
1541            };
1542
1543            // Initialize the journal
1544            let mut journal = Journal::init(context.child("first"), cfg.clone())
1545                .await
1546                .expect("Failed to initialize journal");
1547
1548            // Append items to multiple blobs
1549            for index in 1u64..=5u64 {
1550                (journal, _, _) = journal
1551                    .append(index, &index)
1552                    .await
1553                    .expect("Failed to append data");
1554                journal = journal.sync(index).await.expect("Failed to sync blob");
1555            }
1556
1557            // Add one item out-of-order
1558            let data = 99;
1559            (journal, _, _) = journal
1560                .append(2u64, &data)
1561                .await
1562                .expect("Failed to append data");
1563            journal = journal.sync(2u64).await.expect("Failed to sync blob");
1564
1565            // Prune blobs with indices less than 3
1566            (journal, _) = journal.prune(3).await.expect("Failed to prune blobs");
1567
1568            // Check metrics
1569            let buffer = context.encode();
1570            assert!(buffer.contains("first_pruned_total 2"));
1571
1572            // Prune again with a section less than the previous one, should be a no-op
1573            (journal, _) = journal.prune(2).await.expect("Failed to no-op prune");
1574            let buffer = context.encode();
1575            assert!(buffer.contains("first_pruned_total 2"));
1576
1577            // Drop and re-open the journal to simulate a restart
1578            drop(journal);
1579            let mut journal = Journal::init(context.child("second"), cfg.clone())
1580                .await
1581                .expect("Failed to re-initialize journal");
1582
1583            // Replay the journal and collect items
1584            let mut items = Vec::<(u64, u64)>::new();
1585            {
1586                let mut replay = journal
1587                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1588                    .await
1589                    .expect("unable to setup replay");
1590                while let Some(result) = replay.next().await {
1591                    match result {
1592                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1593                        Err(err) => panic!("Failed to read item: {err}"),
1594                    }
1595                }
1596                journal = replay.finish().expect("failed to finish replay");
1597            }
1598
1599            // Verify that items from blobs 1 and 2 are not present
1600            assert_eq!(items.len(), 3);
1601            let expected_indices = [3u64, 4u64, 5u64];
1602            for (item, expected_index) in items.iter().zip(expected_indices.iter()) {
1603                assert_eq!(item.0, *expected_index);
1604            }
1605
1606            // Prune all blobs
1607            (journal, _) = journal.prune(6).await.expect("Failed to prune blobs");
1608
1609            // Drop the journal
1610            drop(journal);
1611
1612            // Ensure no remaining blobs exist
1613            //
1614            // Note: We don't remove the partition, so this does not error
1615            // and instead returns an empty list of blobs.
1616            assert!(
1617                context
1618                    .scan(&cfg.partition)
1619                    .await
1620                    .expect("Failed to list blobs")
1621                    .is_empty()
1622            );
1623        });
1624    }
1625
1626    #[test_traced]
1627    fn test_journal_prune_guard() {
1628        let executor = deterministic::Runner::default();
1629        executor.start(|context| async move {
1630            let cfg = Config {
1631                partition: "test-partition".into(),
1632                compression: None,
1633                codec_config: (),
1634                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1635                write_buffer: NZUsize!(1024),
1636            };
1637
1638            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1639                .await
1640                .expect("Failed to initialize journal");
1641
1642            // Append items to sections 1-5
1643            for section in 1u64..=5u64 {
1644                (journal, _, _) = journal
1645                    .append(section, &(section as i32))
1646                    .await
1647                    .expect("Failed to append data");
1648                journal = journal.sync(section).await.expect("Failed to sync");
1649            }
1650
1651            // Prune sections < 3
1652            (journal, _) = journal.prune(3).await.expect("Failed to prune");
1653
1654            // The public accessor mirrors the guard
1655            assert!(journal.pruned(1));
1656            assert!(journal.pruned(2));
1657            assert!(!journal.pruned(3));
1658
1659            // Test that accessing pruned sections returns the correct error
1660
1661            // Test append on pruned section
1662            match journal.0.append(1, &100).await {
1663                Err(Error::AlreadyPrunedToSection(3)) => {}
1664                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1665            }
1666
1667            match journal.0.append(2, &100).await {
1668                Err(Error::AlreadyPrunedToSection(3)) => {}
1669                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1670            }
1671
1672            // Test get on pruned section
1673            match journal.get(1, 0).await {
1674                Err(Error::AlreadyPrunedToSection(3)) => {}
1675                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1676            }
1677
1678            // Test size on pruned section
1679            match journal.size(1) {
1680                Err(Error::AlreadyPrunedToSection(3)) => {}
1681                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
1682            }
1683
1684            // Test that accessing sections at or after the threshold works
1685            assert!(journal.get(3, 0).await.is_ok());
1686            assert!(journal.get(4, 0).await.is_ok());
1687            assert!(journal.get(5, 0).await.is_ok());
1688            assert!(journal.size(3).is_ok());
1689            journal = journal
1690                .sync(4)
1691                .await
1692                .expect("Should be able to sync section 4");
1693
1694            // Append to section at threshold should work
1695            (journal, _, _) = journal
1696                .append(3, &999)
1697                .await
1698                .expect("Should be able to append to section 3");
1699
1700            // Prune more sections
1701            (journal, _) = journal.prune(5).await.expect("Failed to prune");
1702
1703            // Verify sections 3 and 4 are now pruned
1704            assert!(journal.pruned(4));
1705            assert!(!journal.pruned(5));
1706            match journal.get(3, 0).await {
1707                Err(Error::AlreadyPrunedToSection(5)) => {}
1708                other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1709            }
1710
1711            match journal.get(4, 0).await {
1712                Err(Error::AlreadyPrunedToSection(5)) => {}
1713                other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1714            }
1715
1716            // Section 5 should still be accessible
1717            assert!(journal.get(5, 0).await.is_ok());
1718
1719            // Test sync on pruned section. The failure destroys the journal.
1720            match journal.sync(4).await {
1721                Err(Error::AlreadyPrunedToSection(5)) => {}
1722                other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
1723            }
1724        });
1725    }
1726
1727    #[test_traced]
1728    fn test_journal_prune_guard_across_restart() {
1729        let executor = deterministic::Runner::default();
1730        executor.start(|context| async move {
1731            let cfg = Config {
1732                partition: "test-partition".into(),
1733                compression: None,
1734                codec_config: (),
1735                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1736                write_buffer: NZUsize!(1024),
1737            };
1738
1739            // First session: create and prune
1740            {
1741                let mut journal = Journal::init(context.child("first"), cfg.clone())
1742                    .await
1743                    .expect("Failed to initialize journal");
1744
1745                for section in 1u64..=5u64 {
1746                    (journal, _, _) = journal
1747                        .append(section, &(section as i32))
1748                        .await
1749                        .expect("Failed to append data");
1750                    journal = journal.sync(section).await.expect("Failed to sync");
1751                }
1752
1753                journal.prune(3).await.expect("Failed to prune");
1754            }
1755
1756            // Second session: verify oldest_retained_section is reset
1757            {
1758                let journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
1759                    .await
1760                    .expect("Failed to re-initialize journal");
1761
1762                // The floor is execution-scoped, so pruned reports false after restart
1763                assert!(!journal.pruned(1));
1764                assert!(!journal.pruned(2));
1765
1766                // But the actual sections 1 and 2 should be gone from storage
1767                // so get should return SectionOutOfRange, not AlreadyPrunedToSection
1768                match journal.get(1, 0).await {
1769                    Err(Error::SectionOutOfRange(1)) => {}
1770                    other => panic!("Expected SectionOutOfRange(1), got {other:?}"),
1771                }
1772
1773                match journal.get(2, 0).await {
1774                    Err(Error::SectionOutOfRange(2)) => {}
1775                    other => panic!("Expected SectionOutOfRange(2), got {other:?}"),
1776                }
1777
1778                // Sections 3-5 should still be accessible
1779                assert!(journal.get(3, 0).await.is_ok());
1780                assert!(journal.get(4, 0).await.is_ok());
1781                assert!(journal.get(5, 0).await.is_ok());
1782            }
1783        });
1784    }
1785
1786    #[test_traced]
1787    fn test_journal_with_invalid_blob_name() {
1788        // Initialize the deterministic context
1789        let executor = deterministic::Runner::default();
1790
1791        // Start the test within the executor
1792        executor.start(|context| async move {
1793            // Create a journal configuration
1794            let cfg = Config {
1795                partition: "test-partition".into(),
1796                compression: None,
1797                codec_config: (),
1798                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1799                write_buffer: NZUsize!(1024),
1800            };
1801
1802            // Manually create a blob with an invalid name (not 8 bytes)
1803            let invalid_blob_name = b"invalid"; // Less than 8 bytes
1804            let (blob, _) = context
1805                .open(&cfg.partition, invalid_blob_name)
1806                .await
1807                .expect("Failed to create blob with invalid name");
1808            blob.sync().await.expect("Failed to sync blob");
1809
1810            // Attempt to initialize the journal
1811            drop(blob);
1812            let result = Journal::<_, u64>::init(context, cfg).await;
1813
1814            // Expect an error
1815            assert!(matches!(result, Err(Error::InvalidBlobName(_))));
1816        });
1817    }
1818
1819    #[test_traced]
1820    fn test_journal_read_size_missing() {
1821        // Initialize the deterministic context
1822        let executor = deterministic::Runner::default();
1823
1824        // Start the test within the executor
1825        executor.start(|context| async move {
1826            // Create a journal configuration
1827            let cfg = Config {
1828                partition: "test-partition".into(),
1829                compression: None,
1830                codec_config: (),
1831                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1832                write_buffer: NZUsize!(1024),
1833            };
1834
1835            // Manually create a blob with incomplete size data
1836            let section = 1u64;
1837            let blob_name = section.to_be_bytes();
1838            let (blob, _) = context
1839                .open(&cfg.partition, &blob_name)
1840                .await
1841                .expect("Failed to create blob");
1842
1843            // Write incomplete varint by encoding u32::MAX (5 bytes) and truncating to 1 byte
1844            let mut incomplete_data = Vec::new();
1845            UInt(u32::MAX).write(&mut incomplete_data);
1846            incomplete_data.truncate(1);
1847            blob.write_at(0, incomplete_data, WriteOptions::SYNC)
1848                .await
1849                .expect("Failed to write incomplete data");
1850
1851            // Initialize the journal
1852            drop(blob);
1853            let journal = Journal::init(context, cfg)
1854                .await
1855                .expect("Failed to initialize journal");
1856
1857            // Attempt to replay the journal
1858            let mut replay = journal
1859                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1860                .await
1861                .expect("unable to setup replay");
1862            let mut items = Vec::<(u64, u64)>::new();
1863            while let Some(result) = replay.next().await {
1864                match result {
1865                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1866                    Err(err) => panic!("Failed to read item: {err}"),
1867                }
1868            }
1869            assert!(items.is_empty());
1870        });
1871    }
1872
1873    #[test_traced]
1874    fn test_journal_replay_empty_finishes_immediately() {
1875        let executor = deterministic::Runner::default();
1876        executor.start(|context| async move {
1877            let cfg = Config {
1878                partition: "test-partition".into(),
1879                compression: None,
1880                codec_config: (),
1881                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1882                write_buffer: NZUsize!(1024),
1883            };
1884            let journal = Journal::<_, i32>::init(context.child("storage"), cfg)
1885                .await
1886                .expect("Failed to initialize journal");
1887
1888            // An empty journal's reader is exhausted from the start
1889            let replay = journal
1890                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1891                .await
1892                .expect("Failed to replay");
1893            let journal = replay.finish().expect("failed to finish replay");
1894            journal.destroy().await.expect("Failed to destroy");
1895        });
1896    }
1897
1898    #[test_traced]
1899    fn test_journal_replay_finish_before_drain_fails() {
1900        let executor = deterministic::Runner::default();
1901        executor.start(|context| async move {
1902            let cfg = Config {
1903                partition: "test-partition".into(),
1904                compression: None,
1905                codec_config: (),
1906                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1907                write_buffer: NZUsize!(1024),
1908            };
1909            let mut journal = Journal::init(context.child("storage"), cfg)
1910                .await
1911                .expect("Failed to initialize journal");
1912            (journal, _, _) = journal.append(1, &7i32).await.expect("Failed to append");
1913            journal = journal.sync(1).await.expect("Failed to sync");
1914
1915            let replay = journal
1916                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1917                .await
1918                .expect("Failed to replay");
1919            assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
1920        });
1921    }
1922
1923    #[test_traced]
1924    fn test_journal_replay_reports_resize_error_on_trailing_bytes() {
1925        let executor = deterministic::Runner::default();
1926        executor.start(|context| async move {
1927            let cfg = Config {
1928                partition: "test-partition".into(),
1929                compression: None,
1930                codec_config: (),
1931                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1932                write_buffer: NZUsize!(1024),
1933            };
1934
1935            // Leave one byte in the first page so the trailing bytes below cross the page
1936            // boundary and repair must issue a physical resize.
1937            let section = 1u64;
1938            let item = [10u8; 1021];
1939            let item_record_size =
1940                UInt(item.encode_size() as u32).encode_size() + item.encode_size();
1941            assert_eq!(item_record_size, PAGE_SIZE.get() as usize - 1);
1942
1943            let mut journal = Journal::init(context.child("first"), cfg.clone())
1944                .await
1945                .expect("Failed to initialize journal");
1946            (journal, _, _) = journal
1947                .append(section, &item)
1948                .await
1949                .expect("Failed to append item");
1950            journal
1951                .0
1952                .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
1953                .await
1954                .expect("Failed to append trailing bytes");
1955            journal = journal.sync(section).await.expect("Failed to sync journal");
1956            drop(journal);
1957
1958            let journal = Journal::init(context.child("second"), cfg)
1959                .await
1960                .expect("Failed to re-initialize journal");
1961            *context.storage_fault_config().write() = deterministic::FaultConfig {
1962                resize_rate: Some(deterministic::ResizeConfig {
1963                    failure_rate: probability!(1.0),
1964                    partial_rate: probability!(0.0),
1965                }),
1966                ..Default::default()
1967            };
1968
1969            let mut replay = journal
1970                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1971                .await
1972                .expect("unable to setup replay");
1973
1974            let first = replay
1975                .next()
1976                .await
1977                .expect("expected item before trailing bytes")
1978                .expect("failed to replay valid item");
1979            assert_eq!(first, (section, 0, item.encode_size() as u32, item));
1980
1981            // The trailing bytes cross the page boundary, so repair must issue a physical resize.
1982            match replay.next().await {
1983                Some(Err(_)) => {}
1984                other => {
1985                    panic!("expected resize error while repairing trailing bytes, got {other:?}")
1986                }
1987            }
1988            assert!(replay.next().await.is_none());
1989        });
1990    }
1991
1992    #[test_traced]
1993    fn test_journal_replay_finish_after_error_fails() {
1994        let executor = deterministic::Runner::default();
1995        executor.start(|context| async move {
1996            let cfg = Config {
1997                partition: "test-partition".into(),
1998                compression: None,
1999                codec_config: (),
2000                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2001                write_buffer: NZUsize!(1024),
2002            };
2003
2004            // Same layout as the resize-error test: trailing bytes cross the page
2005            // boundary so repair must issue a physical resize, which the fault fails.
2006            let section = 1u64;
2007            let item = [10u8; 1021];
2008            let mut journal = Journal::init(context.child("first"), cfg.clone())
2009                .await
2010                .expect("Failed to initialize journal");
2011            (journal, _, _) = journal
2012                .append(section, &item)
2013                .await
2014                .expect("Failed to append item");
2015            journal
2016                .0
2017                .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
2018                .await
2019                .expect("Failed to append trailing bytes");
2020            journal = journal.sync(section).await.expect("Failed to sync journal");
2021            drop(journal);
2022
2023            let journal = Journal::<_, [u8; 1021]>::init(context.child("second"), cfg)
2024                .await
2025                .expect("Failed to re-initialize journal");
2026            *context.storage_fault_config().write() = deterministic::FaultConfig {
2027                resize_rate: Some(deterministic::ResizeConfig {
2028                    failure_rate: probability!(1.0),
2029                    partial_rate: probability!(0.0),
2030                }),
2031                ..Default::default()
2032            };
2033
2034            let mut replay = journal
2035                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2036                .await
2037                .expect("unable to setup replay");
2038            let _ = replay
2039                .next()
2040                .await
2041                .expect("expected item before trailing bytes")
2042                .expect("failed to replay valid item");
2043            assert!(matches!(replay.next().await, Some(Err(_))));
2044            assert!(replay.next().await.is_none());
2045
2046            // The yielded error is fatal, so finish must refuse to return the journal
2047            assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2048        });
2049    }
2050
2051    #[test_traced]
2052    fn test_journal_replay_dropped_during_repair_fails_replay() {
2053        let executor = deterministic::Runner::default();
2054        executor.start(|context| async move {
2055            let cfg = Config {
2056                partition: "test-partition".into(),
2057                compression: None,
2058                codec_config: (),
2059                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2060                write_buffer: NZUsize!(1024),
2061            };
2062
2063            // Same layout as the resize-error test: trailing bytes cross the page
2064            // boundary so repair must issue a physical resize (and its sync).
2065            let section = 1u64;
2066            let item = [10u8; 1021];
2067            let mut journal = Journal::init(context.child("first"), cfg.clone())
2068                .await
2069                .expect("Failed to initialize journal");
2070            (journal, _, _) = journal
2071                .append(section, &item)
2072                .await
2073                .expect("Failed to append item");
2074            journal
2075                .0
2076                .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
2077                .await
2078                .expect("Failed to append trailing bytes");
2079            journal = journal.sync(section).await.expect("Failed to sync journal");
2080            drop(journal);
2081
2082            // Gate syncs so the repair suspends, then drop the in-flight next()
2083            let pending = PendingSyncs::default();
2084            let gated = DelayedSyncContext {
2085                inner: context.child("second"),
2086                pending: pending.clone(),
2087            };
2088            let journal = Journal::<_, [u8; 1021]>::init(gated, cfg.clone())
2089                .await
2090                .expect("Failed to re-initialize journal");
2091            let mut replay = journal
2092                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2093                .await
2094                .expect("unable to setup replay");
2095            let _ = replay
2096                .next()
2097                .await
2098                .expect("expected item before trailing bytes")
2099                .expect("failed to replay valid item");
2100            pending.arm();
2101            {
2102                let fut = replay.next();
2103                futures::pin_mut!(fut);
2104                assert!(
2105                    futures::poll!(fut.as_mut()).is_pending(),
2106                    "repair must suspend on the gated sync"
2107                );
2108            }
2109            release_pending_syncs(&pending);
2110
2111            // The interrupted repair fails the replay rather than resuming over it
2112            assert!(matches!(
2113                replay.next().await,
2114                Some(Err(Error::ReplayInterrupted))
2115            ));
2116            assert!(replay.next().await.is_none());
2117            drop(replay);
2118
2119            // Re-initialization repairs from durable state
2120            let journal = Journal::<_, [u8; 1021]>::init(context.child("third"), cfg)
2121                .await
2122                .expect("Failed to re-initialize journal");
2123            let mut replay = journal
2124                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2125                .await
2126                .expect("unable to setup replay");
2127            let first = replay
2128                .next()
2129                .await
2130                .expect("expected item after recovery")
2131                .expect("failed to replay valid item");
2132            assert_eq!(first, (section, 0, item.encode_size() as u32, item));
2133            assert!(replay.next().await.is_none());
2134            let journal = replay.finish().expect("failed to finish replay");
2135            journal.destroy().await.expect("Failed to destroy");
2136        });
2137    }
2138
2139    #[test_traced]
2140    fn test_journal_read_item_missing() {
2141        // Initialize the deterministic context
2142        let executor = deterministic::Runner::default();
2143
2144        // Start the test within the executor
2145        executor.start(|context| async move {
2146            // Create a journal configuration
2147            let cfg = Config {
2148                partition: "test-partition".into(),
2149                compression: None,
2150                codec_config: (),
2151                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2152                write_buffer: NZUsize!(1024),
2153            };
2154
2155            // Manually create a blob with missing item data
2156            let section = 1u64;
2157            let blob_name = section.to_be_bytes();
2158            let (blob, _) = context
2159                .open(&cfg.partition, &blob_name)
2160                .await
2161                .expect("Failed to create blob");
2162
2163            // Write size but incomplete item data
2164            let item_size: u32 = 10; // Size indicates 10 bytes of data
2165            let mut buf = Vec::new();
2166            UInt(item_size).write(&mut buf); // Varint encoding
2167            let data = [2u8; 5];
2168            BufMut::put_slice(&mut buf, &data);
2169            blob.write_at(0, buf, WriteOptions::SYNC)
2170                .await
2171                .expect("Failed to write incomplete item");
2172
2173            // Initialize the journal
2174            drop(blob);
2175            let journal = Journal::init(context, cfg)
2176                .await
2177                .expect("Failed to initialize journal");
2178
2179            // Attempt to replay the journal
2180            let mut replay = journal
2181                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2182                .await
2183                .expect("unable to setup replay");
2184            let mut items = Vec::<(u64, u64)>::new();
2185            while let Some(result) = replay.next().await {
2186                match result {
2187                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2188                    Err(err) => panic!("Failed to read item: {err}"),
2189                }
2190            }
2191            assert!(items.is_empty());
2192        });
2193    }
2194
2195    #[test_traced]
2196    fn test_journal_read_checksum_missing() {
2197        // Initialize the deterministic context
2198        let executor = deterministic::Runner::default();
2199
2200        // Start the test within the executor
2201        executor.start(|context| async move {
2202            // Create a journal configuration
2203            let cfg = Config {
2204                partition: "test-partition".into(),
2205                compression: None,
2206                codec_config: (),
2207                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2208                write_buffer: NZUsize!(1024),
2209            };
2210
2211            // Manually create a blob with missing checksum
2212            let section = 1u64;
2213            let blob_name = section.to_be_bytes();
2214            let (blob, _) = context
2215                .open(&cfg.partition, &blob_name)
2216                .await
2217                .expect("Failed to create blob");
2218
2219            // Prepare item data
2220            let item_data = b"Test data";
2221            let item_size = item_data.len() as u32;
2222
2223            // Write size (varint) and data, but no checksum
2224            let mut buf = Vec::new();
2225            UInt(item_size).write(&mut buf);
2226            BufMut::put_slice(&mut buf, item_data);
2227            blob.write_at(0, buf, WriteOptions::SYNC)
2228                .await
2229                .expect("Failed to write item without checksum");
2230
2231            // Initialize the journal
2232            drop(blob);
2233            let journal = Journal::init(context, cfg)
2234                .await
2235                .expect("Failed to initialize journal");
2236
2237            // Attempt to replay the journal
2238            //
2239            // This will truncate the leftover bytes from our manual write.
2240            let mut replay = journal
2241                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2242                .await
2243                .expect("unable to setup replay");
2244            let mut items = Vec::<(u64, u64)>::new();
2245            while let Some(result) = replay.next().await {
2246                match result {
2247                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2248                    Err(err) => panic!("Failed to read item: {err}"),
2249                }
2250            }
2251            assert!(items.is_empty());
2252        });
2253    }
2254
2255    #[test_traced]
2256    fn test_journal_read_checksum_mismatch() {
2257        // Initialize the deterministic context
2258        let executor = deterministic::Runner::default();
2259
2260        // Start the test within the executor
2261        executor.start(|context| async move {
2262            // Create a journal configuration
2263            let cfg = Config {
2264                partition: "test-partition".into(),
2265                compression: None,
2266                codec_config: (),
2267                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2268                write_buffer: NZUsize!(1024),
2269            };
2270
2271            // Manually create a blob with incorrect checksum
2272            let section = 1u64;
2273            let blob_name = section.to_be_bytes();
2274            let (blob, _) = context
2275                .open(&cfg.partition, &blob_name)
2276                .await
2277                .expect("Failed to create blob");
2278
2279            // Prepare item data
2280            let item_data = b"Test data";
2281            let item_size = item_data.len() as u32;
2282            let incorrect_checksum: u32 = 0xDEADBEEF;
2283
2284            // Write size (varint), data, and incorrect checksum
2285            let mut buf = Vec::new();
2286            UInt(item_size).write(&mut buf);
2287            BufMut::put_slice(&mut buf, item_data);
2288            buf.put_u32(incorrect_checksum);
2289            blob.write_at(0, buf, WriteOptions::SYNC)
2290                .await
2291                .expect("Failed to write item with bad checksum");
2292
2293            // Initialize the journal
2294            drop(blob);
2295            let mut journal = Journal::init(context.child("storage"), cfg.clone())
2296                .await
2297                .expect("Failed to initialize journal");
2298
2299            // Attempt to replay the journal
2300            {
2301                let mut replay = journal
2302                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2303                    .await
2304                    .expect("unable to setup replay");
2305                let mut items = Vec::<(u64, u64)>::new();
2306                while let Some(result) = replay.next().await {
2307                    match result {
2308                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2309                        Err(err) => panic!("Failed to read item: {err}"),
2310                    }
2311                }
2312                journal = replay.finish().expect("failed to finish replay");
2313                assert!(items.is_empty());
2314            }
2315            drop(journal);
2316
2317            // Confirm blob is expected length
2318            let (_, blob_size) = context
2319                .open(&cfg.partition, &section.to_be_bytes())
2320                .await
2321                .expect("Failed to open blob");
2322            assert_eq!(blob_size, 0);
2323        });
2324    }
2325
2326    #[test_traced]
2327    fn test_segmented_variable_replay_repairs_torn_interior_page_when_reached() {
2328        let executor = deterministic::Runner::default();
2329        executor.start(|context| async move {
2330            const FIRST_SECTION: u64 = 0;
2331            const PARTITION: &str = "segmented-variable-torn-interior";
2332            const TORN_SECTION: u64 = 1;
2333
2334            // Fifteen nine-byte frames occupy 135 bytes. Pages 0 and 2 remain valid while page 1
2335            // is torn, so backward sizing reports the full tail. Forward page validation must
2336            // first truncate to 64 bytes. Frame replay can then retain the seven complete frames
2337            // ending at byte 63 and safely discard the one-byte frame prefix at the page boundary.
2338            // The replay buffer spans all three pages, proving recovery does not lose page 0 when
2339            // a prefetched later page fails validation. FIRST_SECTION establishes the ordered
2340            // lifecycle boundary: replay setup and consumption of an earlier section must not
2341            // read or repair this later section.
2342            let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
2343            let original_size = context
2344                .logical_blob(PARTITION, &TORN_SECTION.to_be_bytes())
2345                .unwrap()
2346                .len();
2347            let mut replay = journal
2348                .replay(FIRST_SECTION, 0, NZUsize!(1024), ReadOptions::default())
2349                .await
2350                .unwrap();
2351
2352            let size = context
2353                .logical_blob(PARTITION, &TORN_SECTION.to_be_bytes())
2354                .unwrap()
2355                .len();
2356            assert_eq!(
2357                size, original_size,
2358                "replay setup must not repair a later section"
2359            );
2360
2361            let (section, offset, _, value) = replay.next().await.unwrap().unwrap();
2362            assert_eq!((section, offset, value), (FIRST_SECTION, 0, u64::MAX));
2363            let size = context
2364                .logical_blob(PARTITION, &TORN_SECTION.to_be_bytes())
2365                .unwrap()
2366                .len();
2367            assert_eq!(
2368                size, original_size,
2369                "consuming an earlier section must not repair a later section"
2370            );
2371
2372            let mut values = Vec::new();
2373            while let Some(result) = replay.next().await {
2374                let (section, offset, _, value) = result.unwrap();
2375                assert_eq!(section, TORN_SECTION);
2376                assert_eq!(offset, value * 9);
2377                values.push(value);
2378            }
2379            assert_eq!(values, (0..7).collect::<Vec<_>>());
2380
2381            let journal = replay.finish().unwrap();
2382            assert_eq!(journal.size(TORN_SECTION).unwrap(), 63);
2383            let (journal, offset, _) = journal.append(TORN_SECTION, &7).await.unwrap();
2384            assert_eq!(offset, 63);
2385            journal.destroy().await.unwrap();
2386        });
2387    }
2388
2389    #[test_traced]
2390    fn test_segmented_variable_replay_rejects_start_beyond_torn_prefix_without_repair() {
2391        let executor = deterministic::Runner::default();
2392        executor.start(|context| async move {
2393            const PARTITION: &str = "segmented-variable-torn-start";
2394            const SECTION: u64 = 1;
2395            const START_OFFSET: u64 = 72;
2396
2397            let journal = journal_with_torn_interior_page(&context, PARTITION, false).await;
2398            let original_size = context
2399                .logical_blob(PARTITION, &SECTION.to_be_bytes())
2400                .unwrap()
2401                .len();
2402            let mut replay = journal
2403                .replay(
2404                    SECTION,
2405                    START_OFFSET,
2406                    NZUsize!(1024),
2407                    ReadOptions::default(),
2408                )
2409                .await
2410                .expect("apparent tail still covers the requested start");
2411
2412            assert!(matches!(
2413                replay.next().await,
2414                Some(Err(Error::ItemOutOfRange(START_OFFSET)))
2415            ));
2416            let size = context
2417                .logical_blob(PARTITION, &SECTION.to_be_bytes())
2418                .unwrap()
2419                .len();
2420            assert_eq!(
2421                size, original_size,
2422                "an unvalidated start offset must not become a repair boundary"
2423            );
2424            assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2425        });
2426    }
2427
2428    #[test_traced]
2429    fn test_segmented_variable_replay_stops_after_failed_interior_repair() {
2430        let executor = deterministic::Runner::default();
2431        executor.start(|context| async move {
2432            let journal = journal_with_torn_interior_page(
2433                &context,
2434                "segmented-variable-failed-interior-repair",
2435                true,
2436            )
2437            .await;
2438            *context.storage_fault_config().write() = deterministic::FaultConfig {
2439                resize_rate: Some(deterministic::ResizeConfig {
2440                    failure_rate: probability!(1.0),
2441                    partial_rate: probability!(0.0),
2442                }),
2443                ..Default::default()
2444            };
2445
2446            // Section 0 delays validation of the torn section until iteration. Section 2 proves a
2447            // fatal repair error cannot be treated like an ordinary per-section decode error.
2448            let mut replay = journal
2449                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2450                .await
2451                .unwrap();
2452            assert!(matches!(replay.next().await, Some(Ok((0, 0, _, u64::MAX)))));
2453            assert!(matches!(replay.next().await, Some(Err(Error::Runtime(_)))));
2454            assert!(replay.next().await.is_none());
2455            assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2456        });
2457    }
2458
2459    #[test_traced]
2460    fn test_segmented_variable_replay_stops_after_failed_tail_repair() {
2461        let executor = deterministic::Runner::default();
2462        executor.start(|context| async move {
2463            let journal = journal_with_torn_interior_page(
2464                &context,
2465                "segmented-variable-failed-tail-repair",
2466                true,
2467            )
2468            .await;
2469            let mut replay = journal
2470                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2471                .await
2472                .unwrap();
2473
2474            assert!(matches!(replay.next().await, Some(Ok((0, 0, _, u64::MAX)))));
2475            for expected in 0..7 {
2476                let (section, offset, _, value) = replay.next().await.unwrap().unwrap();
2477                assert_eq!((section, offset, value), (1, expected * 9, expected));
2478            }
2479
2480            *context.storage_fault_config().write() = deterministic::FaultConfig {
2481                write_rate: Some(deterministic::WriteConfig {
2482                    failure_rate: probability!(1.0),
2483                    retention_rate: probability!(1.0),
2484                    mode: deterministic::PartialWriteMode::Prefix,
2485                }),
2486                ..Default::default()
2487            };
2488            assert!(matches!(replay.next().await, Some(Err(Error::Runtime(_)))));
2489            *context.storage_fault_config().write() = deterministic::FaultConfig::default();
2490
2491            assert!(replay.next().await.is_none());
2492            assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
2493        });
2494    }
2495
2496    #[test_traced]
2497    fn test_journal_truncation_recovery() {
2498        // Initialize the deterministic context
2499        let executor = deterministic::Runner::default();
2500
2501        // Start the test within the executor
2502        executor.start(|context| async move {
2503            // Create a journal configuration
2504            let cfg = Config {
2505                partition: "test-partition".into(),
2506                compression: None,
2507                codec_config: (),
2508                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2509                write_buffer: NZUsize!(1024),
2510            };
2511
2512            // Initialize the journal
2513            let mut journal = Journal::init(context.child("first"), cfg.clone())
2514                .await
2515                .expect("Failed to initialize journal");
2516
2517            // Append 1 item to the first index
2518            (journal, _, _) = journal.append(1, &1).await.expect("Failed to append data");
2519
2520            // Append multiple items to the second section
2521            let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
2522            for (index, data) in &data_items {
2523                (journal, _, _) = journal
2524                    .append(*index, data)
2525                    .await
2526                    .expect("Failed to append data");
2527                journal = journal.sync(*index).await.expect("Failed to sync blob");
2528            }
2529
2530            // Sync all sections and drop the journal
2531            journal = journal.sync_all().await.expect("Failed to sync");
2532            drop(journal);
2533
2534            // Manually corrupt the end of the second blob
2535            let (blob, blob_size) = context
2536                .open(&cfg.partition, &2u64.to_be_bytes())
2537                .await
2538                .expect("Failed to open blob");
2539            blob.resize(blob_size - 4)
2540                .await
2541                .expect("Failed to corrupt blob");
2542            blob.sync().await.expect("Failed to sync blob");
2543
2544            // Re-initialize the journal to simulate a restart
2545            drop(blob);
2546            let mut journal = Journal::init(context.child("second"), cfg.clone())
2547                .await
2548                .expect("Failed to re-initialize journal");
2549
2550            // Attempt to replay the journal
2551            let mut items = Vec::<(u64, u32)>::new();
2552            {
2553                let mut replay = journal
2554                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2555                    .await
2556                    .expect("unable to setup replay");
2557                while let Some(result) = replay.next().await {
2558                    match result {
2559                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2560                        Err(err) => panic!("Failed to read item: {err}"),
2561                    }
2562                }
2563                journal = replay.finish().expect("failed to finish replay");
2564            }
2565            drop(journal);
2566
2567            // Verify that replay stopped after corruption detected (the second blob).
2568            assert_eq!(items.len(), 1);
2569            assert_eq!(items[0].0, 1);
2570            assert_eq!(items[0].1, 1);
2571
2572            // Confirm second blob was truncated.
2573            let (_, blob_size) = context
2574                .open(&cfg.partition, &2u64.to_be_bytes())
2575                .await
2576                .expect("Failed to open blob");
2577            assert_eq!(blob_size, 0);
2578
2579            // Attempt to replay journal after truncation
2580            let mut journal = Journal::init(context.child("third"), cfg.clone())
2581                .await
2582                .expect("Failed to re-initialize journal");
2583
2584            // Attempt to replay the journal
2585            let mut items = Vec::<(u64, u32)>::new();
2586            {
2587                let mut replay = journal
2588                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2589                    .await
2590                    .expect("unable to setup replay");
2591                while let Some(result) = replay.next().await {
2592                    match result {
2593                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2594                        Err(err) => panic!("Failed to read item: {err}"),
2595                    }
2596                }
2597                journal = replay.finish().expect("failed to finish replay");
2598            }
2599
2600            // Verify that only non-corrupted items were replayed
2601            assert_eq!(items.len(), 1);
2602            assert_eq!(items[0].0, 1);
2603            assert_eq!(items[0].1, 1);
2604
2605            // Append a new item to truncated partition
2606            (journal, _, _) = journal.append(2, &5).await.expect("Failed to append data");
2607            journal = journal.sync(2).await.expect("Failed to sync blob");
2608
2609            // Get the new item (offset is 0 since blob was truncated)
2610            let item = journal.get(2, 0).await.expect("Failed to get item");
2611            assert_eq!(item, 5);
2612
2613            // Drop the journal (data already synced)
2614            drop(journal);
2615
2616            // Re-initialize the journal to simulate a restart
2617            let journal = Journal::init(context.child("storage"), cfg.clone())
2618                .await
2619                .expect("Failed to re-initialize journal");
2620
2621            // Attempt to replay the journal
2622            let mut items = Vec::<(u64, u32)>::new();
2623            {
2624                let mut replay = journal
2625                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2626                    .await
2627                    .expect("unable to setup replay");
2628                while let Some(result) = replay.next().await {
2629                    match result {
2630                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2631                        Err(err) => panic!("Failed to read item: {err}"),
2632                    }
2633                }
2634            }
2635
2636            // Verify that only non-corrupted items were replayed
2637            assert_eq!(items.len(), 2);
2638            assert_eq!(items[0].0, 1);
2639            assert_eq!(items[0].1, 1);
2640            assert_eq!(items[1].0, 2);
2641            assert_eq!(items[1].1, 5);
2642        });
2643    }
2644
2645    #[test_traced]
2646    fn test_journal_handling_extra_data() {
2647        // Initialize the deterministic context
2648        let executor = deterministic::Runner::default();
2649
2650        // Start the test within the executor
2651        executor.start(|context| async move {
2652            // Create a journal configuration
2653            let cfg = Config {
2654                partition: "test-partition".into(),
2655                compression: None,
2656                codec_config: (),
2657                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2658                write_buffer: NZUsize!(1024),
2659            };
2660
2661            // Initialize the journal
2662            let mut journal = Journal::init(context.child("first"), cfg.clone())
2663                .await
2664                .expect("Failed to initialize journal");
2665
2666            // Append 1 item to the first index
2667            (journal, _, _) = journal.append(1, &1).await.expect("Failed to append data");
2668
2669            // Append multiple items to the second index
2670            let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
2671            for (index, data) in &data_items {
2672                (journal, _, _) = journal
2673                    .append(*index, data)
2674                    .await
2675                    .expect("Failed to append data");
2676                journal = journal.sync(*index).await.expect("Failed to sync blob");
2677            }
2678
2679            // Sync all sections and drop the journal
2680            journal = journal.sync_all().await.expect("Failed to sync");
2681            drop(journal);
2682
2683            // Manually add extra data to the end of the second blob
2684            let (blob, blob_size) = context
2685                .open(&cfg.partition, &2u64.to_be_bytes())
2686                .await
2687                .expect("Failed to open blob");
2688            blob.write_at(blob_size, vec![0u8; 16], WriteOptions::SYNC)
2689                .await
2690                .expect("Failed to add extra data");
2691
2692            // Re-initialize the journal to simulate a restart
2693            drop(blob);
2694            let journal = Journal::init(context.child("second"), cfg)
2695                .await
2696                .expect("Failed to re-initialize journal");
2697
2698            // Attempt to replay the journal
2699            let mut items = Vec::<(u64, i32)>::new();
2700            let mut replay = journal
2701                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2702                .await
2703                .expect("unable to setup replay");
2704            while let Some(result) = replay.next().await {
2705                match result {
2706                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
2707                    Err(err) => panic!("Failed to read item: {err}"),
2708                }
2709            }
2710        });
2711    }
2712
2713    #[test_traced]
2714    fn test_journal_truncate() {
2715        // Initialize the deterministic context
2716        let executor = deterministic::Runner::default();
2717        executor.start(|context| async move {
2718            // Create journal
2719            let cfg = Config {
2720                partition: "test-partition".into(),
2721                compression: None,
2722                codec_config: (),
2723                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2724                write_buffer: NZUsize!(1024),
2725            };
2726            let mut journal = Journal::init(context, cfg).await.unwrap();
2727
2728            // Check size of non-existent section
2729            let size = journal.size(1).unwrap();
2730            assert_eq!(size, 0);
2731
2732            // Append data to section 1
2733            (journal, _, _) = journal.append(1, &42i32).await.unwrap();
2734
2735            // Check size of section 1 - should be greater than 0
2736            let size = journal.size(1).unwrap();
2737            assert!(size > 0);
2738
2739            // Append more data and verify size increases
2740            (journal, _, _) = journal.append(1, &43i32).await.unwrap();
2741            let new_size = journal.size(1).unwrap();
2742            assert!(new_size > size);
2743
2744            // Check size of different section - should still be 0
2745            let size = journal.size(2).unwrap();
2746            assert_eq!(size, 0);
2747
2748            // Append data to section 2
2749            (journal, _, _) = journal.append(2, &44i32).await.unwrap();
2750
2751            // Check size of section 2 - should be greater than 0
2752            let size = journal.size(2).unwrap();
2753            assert!(size > 0);
2754
2755            // Rollback everything in section 1 and 2
2756            journal = journal.test_reopen_at_most(1, 0).await.unwrap();
2757
2758            // Check size of section 1 - should be 0
2759            let size = journal.size(1).unwrap();
2760            assert_eq!(size, 0);
2761
2762            // Check size of section 2 - should be 0
2763            let size = journal.size(2).unwrap();
2764            assert_eq!(size, 0);
2765        });
2766    }
2767
2768    #[test_traced]
2769    fn test_journal_truncate_max_section() {
2770        let executor = deterministic::Runner::default();
2771        executor.start(|context| async move {
2772            let cfg = Config {
2773                partition: "test-partition".into(),
2774                compression: None,
2775                codec_config: (),
2776                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2777                write_buffer: NZUsize!(1024),
2778            };
2779            let mut journal = Journal::init(context, cfg).await.unwrap();
2780
2781            // Append to the maximal section. `section + 1` has no representable successor.
2782            let offset;
2783            (journal, offset, _) = journal.append(u64::MAX, &42i32).await.unwrap();
2784            let size = journal.size(u64::MAX).unwrap();
2785            assert!(size > 0);
2786
2787            // Truncating the maximal section removes no sections above it and must not panic.
2788            journal = journal.test_reopen_at_most(u64::MAX, size).await.unwrap();
2789
2790            // The section is intact and readable.
2791            assert_eq!(journal.size(u64::MAX).unwrap(), size);
2792            assert_eq!(journal.get(u64::MAX, offset).await.unwrap(), 42i32);
2793        });
2794    }
2795
2796    #[test_traced]
2797    fn test_journal_truncate_section() {
2798        // Initialize the deterministic context
2799        let executor = deterministic::Runner::default();
2800        executor.start(|context| async move {
2801            // Create journal
2802            let cfg = Config {
2803                partition: "test-partition".into(),
2804                compression: None,
2805                codec_config: (),
2806                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2807                write_buffer: NZUsize!(1024),
2808            };
2809            let mut journal = Journal::init(context, cfg).await.unwrap();
2810
2811            // Check size of non-existent section
2812            let size = journal.size(1).unwrap();
2813            assert_eq!(size, 0);
2814
2815            // Append data to section 1
2816            (journal, _, _) = journal.append(1, &42i32).await.unwrap();
2817
2818            // Check size of section 1 - should be greater than 0
2819            let size = journal.size(1).unwrap();
2820            assert!(size > 0);
2821
2822            // Append more data and verify size increases
2823            (journal, _, _) = journal.append(1, &43i32).await.unwrap();
2824            let new_size = journal.size(1).unwrap();
2825            assert!(new_size > size);
2826
2827            // Check size of different section - should still be 0
2828            let size = journal.size(2).unwrap();
2829            assert_eq!(size, 0);
2830
2831            // Append data to section 2
2832            (journal, _, _) = journal.append(2, &44i32).await.unwrap();
2833
2834            // Check size of section 2 - should be greater than 0
2835            let size = journal.size(2).unwrap();
2836            assert!(size > 0);
2837
2838            // Rollback everything in section 1
2839            journal = journal.test_reopen_section(1, 0).await.unwrap();
2840
2841            // Check size of section 1 - should be 0
2842            let size = journal.size(1).unwrap();
2843            assert_eq!(size, 0);
2844
2845            // Check size of section 2 - should be greater than 0
2846            let size = journal.size(2).unwrap();
2847            assert!(size > 0);
2848        });
2849    }
2850
2851    #[test_traced]
2852    fn test_journal_small_items() {
2853        let executor = deterministic::Runner::default();
2854        executor.start(|context| async move {
2855            let cfg = Config {
2856                partition: "test-partition".into(),
2857                compression: None,
2858                codec_config: (),
2859                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2860                write_buffer: NZUsize!(1024),
2861            };
2862
2863            let mut journal = Journal::init(context.child("first"), cfg.clone())
2864                .await
2865                .expect("Failed to initialize journal");
2866
2867            // Append many small (1-byte) items to the same section
2868            let num_items = 100;
2869            let mut offsets = Vec::new();
2870            for i in 0..num_items {
2871                let offset;
2872                let size;
2873                (journal, offset, size) = journal
2874                    .append(1, &(i as u8))
2875                    .await
2876                    .expect("Failed to append data");
2877                assert_eq!(size, 1, "u8 should encode to 1 byte");
2878                offsets.push(offset);
2879            }
2880            journal = journal.sync(1).await.expect("Failed to sync");
2881
2882            // Read each item back via random access
2883            for (i, &offset) in offsets.iter().enumerate() {
2884                let item: u8 = journal.get(1, offset).await.expect("Failed to get item");
2885                assert_eq!(item, i as u8, "Item mismatch at offset {offset}");
2886            }
2887
2888            // Drop and reopen to test replay
2889            drop(journal);
2890            let journal = Journal::<_, u8>::init(context.child("second"), cfg)
2891                .await
2892                .expect("Failed to re-initialize journal");
2893
2894            // Replay and verify all items
2895            let mut replay = journal
2896                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2897                .await
2898                .expect("Failed to setup replay");
2899
2900            let mut count = 0;
2901            while let Some(result) = replay.next().await {
2902                let (section, offset, size, item) = result.expect("Failed to replay item");
2903                assert_eq!(section, 1);
2904                assert_eq!(offset, offsets[count]);
2905                assert_eq!(size, 1);
2906                assert_eq!(item, count as u8);
2907                count += 1;
2908            }
2909            assert_eq!(count, num_items, "Should replay all items");
2910        });
2911    }
2912
2913    #[test_traced]
2914    fn test_journal_truncate_many_sections() {
2915        let executor = deterministic::Runner::default();
2916        executor.start(|context| async move {
2917            let cfg = Config {
2918                partition: "test-partition".into(),
2919                compression: None,
2920                codec_config: (),
2921                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2922                write_buffer: NZUsize!(1024),
2923            };
2924            let mut journal = Journal::init(context.child("storage"), cfg.clone())
2925                .await
2926                .unwrap();
2927
2928            // Create sections 1-10 with data
2929            for section in 1u64..=10 {
2930                (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
2931            }
2932            journal = journal.sync_all().await.unwrap();
2933
2934            // Verify all sections exist
2935            for section in 1u64..=10 {
2936                let size = journal.size(section).unwrap();
2937                assert!(size > 0, "section {section} should have data");
2938            }
2939
2940            // Truncate to section 5 (should remove sections 6-10)
2941            let size = journal.size(5).unwrap();
2942            journal = journal.test_reopen_at_most(5, size).await.unwrap();
2943
2944            // Verify sections 1-5 still exist with correct data
2945            for section in 1u64..=5 {
2946                let size = journal.size(section).unwrap();
2947                assert!(size > 0, "section {section} should still have data");
2948            }
2949
2950            // Verify sections 6-10 are removed (size should be 0)
2951            for section in 6u64..=10 {
2952                let size = journal.size(section).unwrap();
2953                assert_eq!(size, 0, "section {section} should be removed");
2954            }
2955
2956            // Verify data integrity via replay
2957            {
2958                let mut replay = journal
2959                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2960                    .await
2961                    .unwrap();
2962                let mut items = Vec::new();
2963                while let Some(result) = replay.next().await {
2964                    let (section, _, _, item) = result.unwrap();
2965                    items.push((section, item));
2966                }
2967                journal = replay.finish().expect("failed to finish replay");
2968                assert_eq!(items.len(), 5);
2969                for (i, (section, item)) in items.iter().enumerate() {
2970                    assert_eq!(*section, (i + 1) as u64);
2971                    assert_eq!(*item, (i + 1) as i32);
2972                }
2973            }
2974
2975            journal.destroy().await.unwrap();
2976        });
2977    }
2978
2979    #[test_traced]
2980    fn test_journal_truncate_partial_truncation() {
2981        let executor = deterministic::Runner::default();
2982        executor.start(|context| async move {
2983            let cfg = Config {
2984                partition: "test-partition".into(),
2985                compression: None,
2986                codec_config: (),
2987                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2988                write_buffer: NZUsize!(1024),
2989            };
2990            let mut journal = Journal::init(context.child("storage"), cfg.clone())
2991                .await
2992                .unwrap();
2993
2994            // Append 5 items and record sizes after each
2995            let mut sizes = Vec::new();
2996            for i in 0..5 {
2997                (journal, _, _) = journal.append(1, &i).await.unwrap();
2998                journal = journal.sync(1).await.unwrap();
2999                sizes.push(journal.size(1).unwrap());
3000            }
3001
3002            // Truncate to keep only first 3 items
3003            let target_size = sizes[2];
3004            journal = journal.test_reopen_at_most(1, target_size).await.unwrap();
3005
3006            // Verify size is correct
3007            let new_size = journal.size(1).unwrap();
3008            assert_eq!(new_size, target_size);
3009
3010            // Verify first 3 items via replay
3011            {
3012                let mut replay = journal
3013                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3014                    .await
3015                    .unwrap();
3016                let mut items = Vec::new();
3017                while let Some(result) = replay.next().await {
3018                    let (_, _, _, item) = result.unwrap();
3019                    items.push(item);
3020                }
3021                journal = replay.finish().expect("failed to finish replay");
3022                assert_eq!(items.len(), 3);
3023                for (i, item) in items.iter().enumerate() {
3024                    assert_eq!(*item, i as i32);
3025                }
3026            }
3027
3028            journal.destroy().await.unwrap();
3029        });
3030    }
3031
3032    #[test_traced]
3033    fn test_journal_truncate_nonexistent_target() {
3034        let executor = deterministic::Runner::default();
3035        executor.start(|context| async move {
3036            let cfg = Config {
3037                partition: "test-partition".into(),
3038                compression: None,
3039                codec_config: (),
3040                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3041                write_buffer: NZUsize!(1024),
3042            };
3043            let mut journal = Journal::init(context.child("storage"), cfg.clone())
3044                .await
3045                .unwrap();
3046
3047            // Create sections 5, 6, 7 (skip 1-4)
3048            for section in 5u64..=7 {
3049                (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
3050            }
3051            journal = journal.sync_all().await.unwrap();
3052
3053            // Truncate to section 3 (doesn't exist)
3054            journal = journal.test_reopen_at_most(3, 0).await.unwrap();
3055
3056            // Verify sections 5, 6, 7 are removed
3057            for section in 5u64..=7 {
3058                let size = journal.size(section).unwrap();
3059                assert_eq!(size, 0, "section {section} should be removed");
3060            }
3061
3062            // Verify replay returns nothing
3063            {
3064                let mut replay = journal
3065                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3066                    .await
3067                    .unwrap();
3068                assert!(replay.next().await.is_none());
3069                journal = replay.finish().expect("failed to finish replay");
3070            }
3071
3072            journal.destroy().await.unwrap();
3073        });
3074    }
3075
3076    #[test_traced]
3077    fn test_journal_truncate_persistence() {
3078        let executor = deterministic::Runner::default();
3079        executor.start(|context| async move {
3080            let cfg = Config {
3081                partition: "test-partition".into(),
3082                compression: None,
3083                codec_config: (),
3084                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3085                write_buffer: NZUsize!(1024),
3086            };
3087
3088            // Create sections 1-5 with data
3089            let mut journal = Journal::init(context.child("first"), cfg.clone())
3090                .await
3091                .unwrap();
3092            for section in 1u64..=5 {
3093                (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
3094            }
3095            journal = journal.sync_all().await.unwrap();
3096
3097            // Truncate to section 2
3098            let size = journal.size(2).unwrap();
3099            journal = journal.test_reopen_at_most(2, size).await.unwrap();
3100            journal = journal.sync_all().await.unwrap();
3101            drop(journal);
3102
3103            // Re-init and verify only sections 1-2 exist
3104            let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3105                .await
3106                .unwrap();
3107
3108            // Verify sections 1-2 have data
3109            for section in 1u64..=2 {
3110                let size = journal.size(section).unwrap();
3111                assert!(size > 0, "section {section} should have data after restart");
3112            }
3113
3114            // Verify sections 3-5 are gone
3115            for section in 3u64..=5 {
3116                let size = journal.size(section).unwrap();
3117                assert_eq!(size, 0, "section {section} should be gone after restart");
3118            }
3119
3120            // Verify data integrity via replay
3121            {
3122                let mut replay = journal
3123                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3124                    .await
3125                    .unwrap();
3126                let mut items = Vec::new();
3127                while let Some(result) = replay.next().await {
3128                    let (section, _, _, item) = result.unwrap();
3129                    items.push((section, item));
3130                }
3131                journal = replay.finish().expect("failed to finish replay");
3132                assert_eq!(items.len(), 2);
3133                assert_eq!(items[0], (1, 1));
3134                assert_eq!(items[1], (2, 2));
3135            }
3136
3137            journal.destroy().await.unwrap();
3138        });
3139    }
3140
3141    #[test_traced]
3142    fn test_journal_truncate_to_zero_removes_all_newer() {
3143        let executor = deterministic::Runner::default();
3144        executor.start(|context| async move {
3145            let cfg = Config {
3146                partition: "test-partition".into(),
3147                compression: None,
3148                codec_config: (),
3149                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3150                write_buffer: NZUsize!(1024),
3151            };
3152            let mut journal = Journal::init(context.child("storage"), cfg.clone())
3153                .await
3154                .unwrap();
3155
3156            // Create sections 1, 2, 3
3157            for section in 1u64..=3 {
3158                (journal, _, _) = journal.append(section, &(section as i32)).await.unwrap();
3159            }
3160            journal = journal.sync_all().await.unwrap();
3161
3162            // Truncate section 1 to size 0
3163            journal = journal.test_reopen_at_most(1, 0).await.unwrap();
3164
3165            // Verify section 1 exists but is empty
3166            let size = journal.size(1).unwrap();
3167            assert_eq!(size, 0, "section 1 should be empty");
3168
3169            // Verify sections 2, 3 are completely removed
3170            for section in 2u64..=3 {
3171                let size = journal.size(section).unwrap();
3172                assert_eq!(size, 0, "section {section} should be removed");
3173            }
3174
3175            // Verify replay returns nothing
3176            {
3177                let mut replay = journal
3178                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3179                    .await
3180                    .unwrap();
3181                assert!(replay.next().await.is_none());
3182                journal = replay.finish().expect("failed to finish replay");
3183            }
3184
3185            journal.destroy().await.unwrap();
3186        });
3187    }
3188
3189    #[test_traced]
3190    fn test_journal_replay_start_offset_with_trailing_bytes() {
3191        // Regression: valid_offset must be initialized to start_offset, not 0.
3192        let executor = deterministic::Runner::default();
3193        executor.start(|context| async move {
3194            let cfg = Config {
3195                partition: "test-partition".into(),
3196                compression: None,
3197                codec_config: (),
3198                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3199                write_buffer: NZUsize!(1024),
3200            };
3201            let mut journal = Journal::init(context.child("first"), cfg.clone())
3202                .await
3203                .expect("Failed to initialize journal");
3204
3205            // Append several items to build up valid data
3206            for i in 0..5i32 {
3207                (journal, _, _) = journal.append(1, &i).await.unwrap();
3208            }
3209            journal = journal.sync(1).await.unwrap();
3210            let valid_logical_size = journal.size(1).unwrap();
3211            drop(journal);
3212
3213            // Get the physical blob size before corruption
3214            let (blob, physical_size_before) = context
3215                .open(&cfg.partition, &1u64.to_be_bytes())
3216                .await
3217                .unwrap();
3218
3219            // Write incomplete varint: 0xFF has continuation bit set, needs more bytes
3220            // This creates 2 trailing bytes that cannot form a valid item
3221            blob.write_at(physical_size_before, vec![0xFF, 0xFF], WriteOptions::SYNC)
3222                .await
3223                .unwrap();
3224
3225            // Reopen journal and replay starting PAST all valid items
3226            // (start_offset = valid_logical_size means we skip all valid data)
3227            // The first thing encountered will be the trailing corrupt bytes
3228            let start_offset = valid_logical_size;
3229            {
3230                drop(blob);
3231                let journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3232                    .await
3233                    .unwrap();
3234
3235                let mut replay = journal
3236                    .replay(1, start_offset, NZUsize!(1024), ReadOptions::default())
3237                    .await
3238                    .unwrap();
3239
3240                // Consume the reader - should detect trailing bytes and truncate
3241                while let Some(_result) = replay.next().await {}
3242            }
3243
3244            // Verify that valid data before start_offset was NOT lost
3245            let physical_size_after = context
3246                .logical_blob(&cfg.partition, &1u64.to_be_bytes())
3247                .unwrap()
3248                .len() as u64;
3249
3250            // The blob should have been truncated back to the valid physical size
3251            // (removing the trailing corrupt bytes) but NOT to 0
3252            assert!(
3253                physical_size_after >= physical_size_before,
3254                "Valid data was lost! Physical blob truncated from {physical_size_before} to \
3255                 {physical_size_after}. Logical valid size was {valid_logical_size}. \
3256                 This indicates valid_offset was incorrectly initialized to 0 instead of start_offset."
3257            );
3258        });
3259    }
3260
3261    #[test_traced]
3262    fn test_journal_replay_rejects_mid_frame_offset() {
3263        deterministic::Runner::default().start(|context| async move {
3264            let cfg = Config {
3265                partition: "mid-frame".into(),
3266                compression: None,
3267                codec_config: (),
3268                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3269                write_buffer: NZUsize!(1024),
3270            };
3271            let journal = Journal::<_, [u8; 16]>::init(context.child("storage"), cfg.clone())
3272                .await
3273                .unwrap();
3274            let (journal, _, _) = journal.append(1, &[127; 16]).await.unwrap();
3275            let journal = journal.sync_all().await.unwrap();
3276
3277            // The first payload byte looks like a length larger than the remaining frame.
3278            let mut replay = journal
3279                .replay(1, 1, NZUsize!(1024), ReadOptions::default())
3280                .await
3281                .unwrap();
3282            assert!(matches!(
3283                replay.next().await,
3284                Some(Err(Error::ItemOutOfRange(1)))
3285            ));
3286            drop(replay);
3287
3288            let journal = Journal::<_, [u8; 16]>::init(context.child("reopen"), cfg)
3289                .await
3290                .unwrap();
3291            let mut replay = journal
3292                .replay(1, 0, NZUsize!(1024), ReadOptions::default())
3293                .await
3294                .unwrap();
3295            assert_eq!(replay.next().await.unwrap().unwrap().3, [127; 16]);
3296            assert!(replay.next().await.is_none());
3297            replay.finish().unwrap().destroy().await.unwrap();
3298        });
3299    }
3300
3301    #[test_traced]
3302    fn test_journal_replay_rejects_start_offset_past_section() {
3303        let executor = deterministic::Runner::default();
3304        executor.start(|context| async move {
3305            let cfg = Config {
3306                partition: "test-partition".into(),
3307                compression: None,
3308                codec_config: (),
3309                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3310                write_buffer: NZUsize!(1024),
3311            };
3312            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
3313            (journal, _, _) = journal.append(1, &7i32).await.unwrap();
3314
3315            // A failed replay consumes the journal
3316            let result = journal
3317                .replay(1, u64::MAX, NZUsize!(1024), ReadOptions::default())
3318                .await;
3319            assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
3320        });
3321    }
3322
3323    #[test_traced]
3324    fn test_journal_large_item_spanning_pages() {
3325        // 2048 bytes spans 2 full pages (PAGE_SIZE = 1024).
3326        const LARGE_SIZE: usize = 2048;
3327        type LargeItem = [u8; LARGE_SIZE];
3328
3329        let executor = deterministic::Runner::default();
3330        executor.start(|context| async move {
3331            let cfg = Config {
3332                partition: "test-partition".into(),
3333                compression: None,
3334                codec_config: (),
3335                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3336                write_buffer: NZUsize!(4096),
3337            };
3338            let mut journal = Journal::init(context.child("first"), cfg.clone())
3339                .await
3340                .expect("Failed to initialize journal");
3341
3342            // Create a large item that spans multiple pages.
3343            let mut large_data: LargeItem = [0u8; LARGE_SIZE];
3344            for (i, byte) in large_data.iter_mut().enumerate() {
3345                *byte = (i % 256) as u8;
3346            }
3347            assert!(
3348                LARGE_SIZE > PAGE_SIZE.get() as usize,
3349                "Item must be larger than page size"
3350            );
3351
3352            // Append the large item
3353            let offset;
3354            let size;
3355            (journal, offset, size) = journal
3356                .append(1, &large_data)
3357                .await
3358                .expect("Failed to append large item");
3359            assert_eq!(size as usize, LARGE_SIZE);
3360            journal = journal.sync(1).await.expect("Failed to sync");
3361
3362            // Read the item back via random access
3363            let retrieved: LargeItem = journal
3364                .get(1, offset)
3365                .await
3366                .expect("Failed to get large item");
3367            assert_eq!(retrieved, large_data, "Random access read mismatch");
3368
3369            // Drop and reopen to test replay
3370            drop(journal);
3371            let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
3372                .await
3373                .expect("Failed to re-initialize journal");
3374
3375            // Replay and verify the large item
3376            {
3377                let mut replay = journal
3378                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3379                    .await
3380                    .expect("Failed to setup replay");
3381
3382                let mut items = Vec::new();
3383                while let Some(result) = replay.next().await {
3384                    let (section, off, sz, item) = result.expect("Failed to replay item");
3385                    items.push((section, off, sz, item));
3386                }
3387                journal = replay.finish().expect("failed to finish replay");
3388
3389                assert_eq!(items.len(), 1, "Should have exactly one item");
3390                let (section, off, sz, item) = &items[0];
3391                assert_eq!(*section, 1);
3392                assert_eq!(*off, offset);
3393                assert_eq!(*sz as usize, LARGE_SIZE);
3394                assert_eq!(*item, large_data, "Replay read mismatch");
3395            }
3396
3397            journal.destroy().await.unwrap();
3398        });
3399    }
3400
3401    #[test_traced]
3402    fn test_journal_large_item_direct_path() {
3403        // Items larger than the write buffer are written directly to the blob. The first append
3404        // takes the direct path from an empty tip; the second takes it with a non-empty tip
3405        // (holding the first item's sub-page remainder), covering both top-up branches. The
3406        // returned offsets must remain correct since callers persist them for random access.
3407        const LARGE_SIZE: usize = 2048;
3408        type LargeItem = [u8; LARGE_SIZE];
3409
3410        let executor = deterministic::Runner::default();
3411        executor.start(|context| async move {
3412            let cfg = Config {
3413                partition: "test-partition".into(),
3414                compression: None,
3415                codec_config: (),
3416                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3417                write_buffer: NZUsize!(1024),
3418            };
3419            let mut journal = Journal::init(context.child("first"), cfg.clone())
3420                .await
3421                .expect("Failed to initialize journal");
3422
3423            let mut first: LargeItem = [0u8; LARGE_SIZE];
3424            for (i, byte) in first.iter_mut().enumerate() {
3425                *byte = (i % 256) as u8;
3426            }
3427            let mut second: LargeItem = [0u8; LARGE_SIZE];
3428            for (i, byte) in second.iter_mut().enumerate() {
3429                *byte = ((i + 7) % 251) as u8;
3430            }
3431
3432            let first_offset;
3433            (journal, first_offset, _) = journal
3434                .append(1, &first)
3435                .await
3436                .expect("Failed to append first item");
3437            let second_offset;
3438            (journal, second_offset, _) = journal
3439                .append(1, &second)
3440                .await
3441                .expect("Failed to append second item");
3442
3443            // Both items are readable at their returned offsets before any sync.
3444            let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
3445            assert_eq!(retrieved, first);
3446            let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
3447            assert_eq!(retrieved, second);
3448
3449            // Everything survives a sync and reopen.
3450            journal = journal.sync(1).await.expect("Failed to sync");
3451            drop(journal);
3452            let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
3453                .await
3454                .expect("Failed to re-initialize journal");
3455
3456            let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
3457            assert_eq!(retrieved, first);
3458            let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
3459            assert_eq!(retrieved, second);
3460
3461            {
3462                let mut replay = journal
3463                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3464                    .await
3465                    .expect("Failed to setup replay");
3466
3467                let mut items = Vec::new();
3468                while let Some(result) = replay.next().await {
3469                    let (section, off, _, item) = result.expect("Failed to replay item");
3470                    items.push((section, off, item));
3471                }
3472                journal = replay.finish().expect("failed to finish replay");
3473                assert_eq!(items.len(), 2);
3474                assert_eq!(items[0], (1, first_offset, first));
3475                assert_eq!(items[1], (1, second_offset, second));
3476            }
3477
3478            journal.destroy().await.unwrap();
3479        });
3480    }
3481
3482    #[test_traced]
3483    fn test_journal_non_contiguous_sections() {
3484        // Test that sections with gaps in numbering work correctly.
3485        // Sections 1, 5, 10 should all be independent and accessible.
3486        let executor = deterministic::Runner::default();
3487        executor.start(|context| async move {
3488            let cfg = Config {
3489                partition: "test-partition".into(),
3490                compression: None,
3491                codec_config: (),
3492                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3493                write_buffer: NZUsize!(1024),
3494            };
3495            let mut journal = Journal::init(context.child("first"), cfg.clone())
3496                .await
3497                .expect("Failed to initialize journal");
3498
3499            // Create sections with gaps: 1, 5, 10
3500            let sections_and_data = [(1u64, 100i32), (5u64, 500i32), (10u64, 1000i32)];
3501            let mut offsets = Vec::new();
3502
3503            for (section, data) in &sections_and_data {
3504                let offset;
3505                (journal, offset, _) = journal
3506                    .append(*section, data)
3507                    .await
3508                    .expect("Failed to append");
3509                offsets.push(offset);
3510            }
3511            journal = journal.sync_all().await.expect("Failed to sync");
3512
3513            // Verify random access to each section
3514            for (i, (section, expected_data)) in sections_and_data.iter().enumerate() {
3515                let retrieved: i32 = journal
3516                    .get(*section, offsets[i])
3517                    .await
3518                    .expect("Failed to get item");
3519                assert_eq!(retrieved, *expected_data);
3520            }
3521
3522            // Verify non-existent sections return appropriate errors
3523            for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
3524                let result = journal.get(missing_section, 0).await;
3525                assert!(
3526                    matches!(result, Err(Error::SectionOutOfRange(_))),
3527                    "Expected SectionOutOfRange for section {}, got {:?}",
3528                    missing_section,
3529                    result
3530                );
3531            }
3532
3533            // Drop and reopen to test replay
3534            drop(journal);
3535            let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3536                .await
3537                .expect("Failed to re-initialize journal");
3538
3539            // Replay and verify all items in order
3540            {
3541                let mut replay = journal
3542                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3543                    .await
3544                    .expect("Failed to setup replay");
3545
3546                let mut items = Vec::new();
3547                while let Some(result) = replay.next().await {
3548                    let (section, _, _, item) = result.expect("Failed to replay item");
3549                    items.push((section, item));
3550                }
3551                journal = replay.finish().expect("failed to finish replay");
3552
3553                assert_eq!(items.len(), 3, "Should have 3 items");
3554                assert_eq!(items[0], (1, 100));
3555                assert_eq!(items[1], (5, 500));
3556                assert_eq!(items[2], (10, 1000));
3557            }
3558
3559            // Test replay starting from middle section (5)
3560            {
3561                let mut replay = journal
3562                    .replay(5, 0, NZUsize!(1024), ReadOptions::default())
3563                    .await
3564                    .expect("Failed to setup replay from section 5");
3565
3566                let mut items = Vec::new();
3567                while let Some(result) = replay.next().await {
3568                    let (section, _, _, item) = result.expect("Failed to replay item");
3569                    items.push((section, item));
3570                }
3571                journal = replay.finish().expect("failed to finish replay");
3572
3573                assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
3574                assert_eq!(items[0], (5, 500));
3575                assert_eq!(items[1], (10, 1000));
3576            }
3577
3578            // Test replay starting from non-existent section (should skip to next)
3579            {
3580                let mut replay = journal
3581                    .replay(3, 0, NZUsize!(1024), ReadOptions::default())
3582                    .await
3583                    .expect("Failed to setup replay from section 3");
3584
3585                let mut items = Vec::new();
3586                while let Some(result) = replay.next().await {
3587                    let (section, _, _, item) = result.expect("Failed to replay item");
3588                    items.push((section, item));
3589                }
3590                journal = replay.finish().expect("failed to finish replay");
3591
3592                // Should get sections 5 and 10 (skipping non-existent 3, 4)
3593                assert_eq!(items.len(), 2);
3594                assert_eq!(items[0], (5, 500));
3595                assert_eq!(items[1], (10, 1000));
3596            }
3597
3598            journal.destroy().await.unwrap();
3599        });
3600    }
3601
3602    #[test_traced]
3603    fn test_journal_empty_section_in_middle() {
3604        // Test that replay correctly handles an empty section between sections with data.
3605        // Section 1 has data, section 2 is empty, section 3 has data.
3606        let executor = deterministic::Runner::default();
3607        executor.start(|context| async move {
3608            let cfg = Config {
3609                partition: "test-partition".into(),
3610                compression: None,
3611                codec_config: (),
3612                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3613                write_buffer: NZUsize!(1024),
3614            };
3615            let mut journal = Journal::init(context.child("first"), cfg.clone())
3616                .await
3617                .expect("Failed to initialize journal");
3618
3619            // Append to section 1
3620            (journal, _, _) = journal.append(1, &100i32).await.expect("Failed to append");
3621
3622            // Create section 2, then truncate it to empty.
3623            (journal, _, _) = journal.append(2, &200i32).await.expect("Failed to append");
3624            journal = journal.sync(2).await.expect("Failed to sync");
3625            journal = journal
3626                .test_reopen_section(2, 0)
3627                .await
3628                .expect("Failed to truncate");
3629
3630            // Append to section 3
3631            (journal, _, _) = journal.append(3, &300i32).await.expect("Failed to append");
3632
3633            journal = journal.sync_all().await.expect("Failed to sync");
3634
3635            // Verify section sizes
3636            assert!(journal.size(1).unwrap() > 0);
3637            assert_eq!(journal.size(2).unwrap(), 0);
3638            assert!(journal.size(3).unwrap() > 0);
3639
3640            // Drop and reopen to test replay
3641            drop(journal);
3642            let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
3643                .await
3644                .expect("Failed to re-initialize journal");
3645
3646            // Replay all - should get items from sections 1 and 3, skipping empty section 2
3647            {
3648                let mut replay = journal
3649                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3650                    .await
3651                    .expect("Failed to setup replay");
3652
3653                let mut items = Vec::new();
3654                while let Some(result) = replay.next().await {
3655                    let (section, _, _, item) = result.expect("Failed to replay item");
3656                    items.push((section, item));
3657                }
3658                journal = replay.finish().expect("failed to finish replay");
3659
3660                assert_eq!(
3661                    items.len(),
3662                    2,
3663                    "Should have 2 items (skipping empty section)"
3664                );
3665                assert_eq!(items[0], (1, 100));
3666                assert_eq!(items[1], (3, 300));
3667            }
3668
3669            // Replay starting from empty section 2 - should get only section 3
3670            {
3671                let mut replay = journal
3672                    .replay(2, 0, NZUsize!(1024), ReadOptions::default())
3673                    .await
3674                    .expect("Failed to setup replay from section 2");
3675
3676                let mut items = Vec::new();
3677                while let Some(result) = replay.next().await {
3678                    let (section, _, _, item) = result.expect("Failed to replay item");
3679                    items.push((section, item));
3680                }
3681                journal = replay.finish().expect("failed to finish replay");
3682
3683                assert_eq!(items.len(), 1, "Should have 1 item from section 3");
3684                assert_eq!(items[0], (3, 300));
3685            }
3686
3687            journal.destroy().await.unwrap();
3688        });
3689    }
3690
3691    #[test_traced]
3692    fn test_journal_item_exactly_page_size() {
3693        // Test that items exactly equal to PAGE_SIZE work correctly.
3694        // This is a boundary condition where item fills exactly one page.
3695        const ITEM_SIZE: usize = PAGE_SIZE.get() as usize;
3696        type ExactItem = [u8; ITEM_SIZE];
3697
3698        let executor = deterministic::Runner::default();
3699        executor.start(|context| async move {
3700            let cfg = Config {
3701                partition: "test-partition".into(),
3702                compression: None,
3703                codec_config: (),
3704                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3705                write_buffer: NZUsize!(4096),
3706            };
3707            let mut journal = Journal::init(context.child("first"), cfg.clone())
3708                .await
3709                .expect("Failed to initialize journal");
3710
3711            // Create an item exactly PAGE_SIZE bytes
3712            let mut exact_data: ExactItem = [0u8; ITEM_SIZE];
3713            for (i, byte) in exact_data.iter_mut().enumerate() {
3714                *byte = (i % 256) as u8;
3715            }
3716
3717            // Append the exact-size item
3718            let offset;
3719            let size;
3720            (journal, offset, size) = journal
3721                .append(1, &exact_data)
3722                .await
3723                .expect("Failed to append exact item");
3724            assert_eq!(size as usize, ITEM_SIZE);
3725            journal = journal.sync(1).await.expect("Failed to sync");
3726
3727            // Read the item back via random access
3728            let retrieved: ExactItem = journal
3729                .get(1, offset)
3730                .await
3731                .expect("Failed to get exact item");
3732            assert_eq!(retrieved, exact_data, "Random access read mismatch");
3733
3734            // Drop and reopen to test replay
3735            drop(journal);
3736            let mut journal = Journal::<_, ExactItem>::init(context.child("second"), cfg.clone())
3737                .await
3738                .expect("Failed to re-initialize journal");
3739
3740            // Replay and verify
3741            {
3742                let mut replay = journal
3743                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3744                    .await
3745                    .expect("Failed to setup replay");
3746
3747                let mut items = Vec::new();
3748                while let Some(result) = replay.next().await {
3749                    let (section, off, sz, item) = result.expect("Failed to replay item");
3750                    items.push((section, off, sz, item));
3751                }
3752                journal = replay.finish().expect("failed to finish replay");
3753
3754                assert_eq!(items.len(), 1, "Should have exactly one item");
3755                let (section, off, sz, item) = &items[0];
3756                assert_eq!(*section, 1);
3757                assert_eq!(*off, offset);
3758                assert_eq!(*sz as usize, ITEM_SIZE);
3759                assert_eq!(*item, exact_data, "Replay read mismatch");
3760            }
3761
3762            journal.destroy().await.unwrap();
3763        });
3764    }
3765
3766    #[test_traced]
3767    fn test_journal_uncompressed_frames_fill_write_buffer() {
3768        deterministic::Runner::default().start(|context| async move {
3769            let cfg = Config {
3770                partition: "test-partition".into(),
3771                compression: None,
3772                codec_config: ((..).into(), ()),
3773                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3774                write_buffer: NZUsize!(2048),
3775            };
3776            let direct = vec![7u8; 126];
3777            let fallback = vec![9u8; 127];
3778            assert_eq!(direct.encode_size(), 127);
3779            assert_eq!(fallback.encode_size(), 128);
3780
3781            let mut journal = Journal::init(context.child("first"), cfg.clone())
3782                .await
3783                .unwrap();
3784
3785            // Sixteen 128-byte frames exactly fill the write buffer, so the next 130-byte frame
3786            // must take the owned fallback.
3787            for i in 0..16 {
3788                let offset;
3789                let item_len;
3790                (journal, offset, item_len) = journal.append(1, &direct).await.unwrap();
3791                assert_eq!(offset, i * 128);
3792                assert_eq!(item_len, 127);
3793            }
3794            let fallback_offset;
3795            let fallback_len;
3796            (journal, fallback_offset, fallback_len) = journal.append(1, &fallback).await.unwrap();
3797            assert_eq!(fallback_offset, 2048);
3798            assert_eq!(fallback_len, 128);
3799
3800            // Reopen the section to verify both append paths on disk.
3801            journal = journal.sync(1).await.unwrap();
3802            drop(journal);
3803            let mut journal = Journal::<_, Vec<u8>>::init(context.child("second"), cfg)
3804                .await
3805                .unwrap();
3806            assert_eq!(journal.get(1, fallback_offset).await.unwrap(), fallback);
3807
3808            // Replay returns the direct frames followed by the fallback frame.
3809            let mut replay = journal
3810                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
3811                .await
3812                .unwrap();
3813            for i in 0..16 {
3814                let (section, offset, item_len, item) = replay.next().await.unwrap().unwrap();
3815                assert_eq!((section, offset, item_len), (1, i * 128, 127));
3816                assert_eq!(item, direct);
3817            }
3818            let (section, offset, item_len, item) = replay.next().await.unwrap().unwrap();
3819            assert_eq!((section, offset, item_len), (1, 2048, 128));
3820            assert_eq!(item, fallback);
3821            assert!(replay.next().await.is_none());
3822            journal = replay.finish().unwrap();
3823            journal.destroy().await.unwrap();
3824        });
3825    }
3826
3827    #[test_traced]
3828    fn test_journal_varint_spanning_page_boundary() {
3829        // Test that items with data spanning page boundaries work correctly
3830        // when using a small page size.
3831        //
3832        // With PAGE_SIZE=16:
3833        // - Physical page = 16 + 12 = 28 bytes
3834        // - Each [u8; 128] item = 2-byte varint + 128 bytes data = 130 bytes
3835        // - This spans multiple 16-byte pages, testing cross-page reading
3836        const SMALL_PAGE: NonZeroU16 = NZU16!(16);
3837
3838        let executor = deterministic::Runner::default();
3839        executor.start(|context| async move {
3840            let cfg = Config {
3841                partition: "test-partition".into(),
3842                compression: None,
3843                codec_config: (),
3844                page_cache: CacheRef::new(SMALL_PAGE, PAGE_CACHE_SIZE),
3845                write_buffer: NZUsize!(1024),
3846            };
3847            let mut journal: Journal<_, [u8; 128]> =
3848                Journal::init(context.child("first"), cfg.clone())
3849                    .await
3850                    .expect("Failed to initialize journal");
3851
3852            // Create items that will span many 16-byte pages
3853            let item1: [u8; 128] = [1u8; 128];
3854            let item2: [u8; 128] = [2u8; 128];
3855            let item3: [u8; 128] = [3u8; 128];
3856
3857            // Append items - each is 130 bytes (2-byte varint + 128 data)
3858            // spanning ceil(130/16) = 9 pages worth of logical data
3859            let offset1;
3860            (journal, offset1, _) = journal.append(1, &item1).await.expect("Failed to append");
3861            let offset2;
3862            (journal, offset2, _) = journal.append(1, &item2).await.expect("Failed to append");
3863            let offset3;
3864            (journal, offset3, _) = journal.append(1, &item3).await.expect("Failed to append");
3865
3866            journal = journal.sync(1).await.expect("Failed to sync");
3867
3868            // Read items back via random access
3869            let retrieved1: [u8; 128] = journal.get(1, offset1).await.expect("Failed to get");
3870            let retrieved2: [u8; 128] = journal.get(1, offset2).await.expect("Failed to get");
3871            let retrieved3: [u8; 128] = journal.get(1, offset3).await.expect("Failed to get");
3872            assert_eq!(retrieved1, item1);
3873            assert_eq!(retrieved2, item2);
3874            assert_eq!(retrieved3, item3);
3875
3876            // Drop and reopen to test replay
3877            drop(journal);
3878            let mut journal: Journal<_, [u8; 128]> =
3879                Journal::init(context.child("second"), cfg.clone())
3880                    .await
3881                    .expect("Failed to re-initialize journal");
3882
3883            // Replay and verify all items
3884            {
3885                let mut replay = journal
3886                    .replay(0, 0, NZUsize!(64), ReadOptions::default())
3887                    .await
3888                    .expect("Failed to setup replay");
3889
3890                let mut items = Vec::new();
3891                while let Some(result) = replay.next().await {
3892                    let (section, off, _, item) = result.expect("Failed to replay item");
3893                    items.push((section, off, item));
3894                }
3895                journal = replay.finish().expect("failed to finish replay");
3896
3897                assert_eq!(items.len(), 3, "Should have 3 items");
3898                assert_eq!(items[0], (1, offset1, item1));
3899                assert_eq!(items[1], (1, offset2, item2));
3900                assert_eq!(items[2], (1, offset3, item3));
3901            }
3902
3903            journal.destroy().await.unwrap();
3904        });
3905    }
3906
3907    #[test_traced]
3908    fn test_journal_clear() {
3909        let executor = deterministic::Runner::default();
3910        executor.start(|context| async move {
3911            let cfg = Config {
3912                partition: "clear-test".into(),
3913                compression: None,
3914                codec_config: (),
3915                page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3916                write_buffer: NZUsize!(1024),
3917            };
3918
3919            let mut journal: Journal<_, u64> = Journal::init(context.child("journal"), cfg.clone())
3920                .await
3921                .expect("Failed to initialize journal");
3922
3923            // Append items across multiple sections
3924            for section in 0..5u64 {
3925                for i in 0..10u64 {
3926                    (journal, _, _) = journal
3927                        .append(section, &(section * 1000 + i))
3928                        .await
3929                        .expect("Failed to append");
3930                }
3931                journal = journal.sync(section).await.expect("Failed to sync");
3932            }
3933
3934            // Verify we have data
3935            assert_eq!(journal.get(0, 0).await.unwrap(), 0);
3936            assert_eq!(journal.get(4, 0).await.unwrap(), 4000);
3937
3938            // Clear the journal
3939            journal = journal.clear().await.expect("Failed to clear");
3940
3941            // After clear, all reads should fail
3942            for section in 0..5u64 {
3943                assert!(matches!(
3944                    journal.get(section, 0).await,
3945                    Err(Error::SectionOutOfRange(s)) if s == section
3946                ));
3947            }
3948
3949            // Append new data after clear
3950            for i in 0..5u64 {
3951                (journal, _, _) = journal
3952                    .append(10, &(i * 100))
3953                    .await
3954                    .expect("Failed to append after clear");
3955            }
3956            journal = journal.sync(10).await.expect("Failed to sync after clear");
3957
3958            // New data should be readable
3959            assert_eq!(journal.get(10, 0).await.unwrap(), 0);
3960
3961            // Old sections should still be missing
3962            assert!(matches!(
3963                journal.get(0, 0).await,
3964                Err(Error::SectionOutOfRange(0))
3965            ));
3966
3967            journal.destroy().await.unwrap();
3968        });
3969    }
3970
3971    #[test_traced]
3972    fn test_journal_try_get_sync() {
3973        let executor = deterministic::Runner::default();
3974        executor.start(|context| async move {
3975            for (compression, partition) in [(None, "plain"), (Some(3), "compressed")] {
3976                let cfg = Config {
3977                    partition: partition.into(),
3978                    compression,
3979                    codec_config: (..).into(),
3980                    page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
3981                    write_buffer: NZUsize!(1024),
3982                };
3983                let journal = Journal::<_, Bytes>::init(context.child(partition), cfg)
3984                    .await
3985                    .expect("Failed to initialize journal");
3986
3987                // Uncompressed, the short frame fits the header read and is decoded from it.
3988                // The long frame is read in full from the write buffer.
3989                let short = Bytes::from(vec![1u8; 3]);
3990                let (journal, short_offset, _) =
3991                    journal.append(1, &short).await.expect("Failed to append");
3992                let long = Bytes::from(vec![7u8; 32]);
3993                let (journal, long_offset, _) =
3994                    journal.append(1, &long).await.expect("Failed to append");
3995                assert_eq!(journal.try_get_sync(1, short_offset), Some(short.clone()));
3996                assert_eq!(journal.try_get_sync(1, long_offset), Some(long.clone()));
3997
3998                // Once synced and read back, the item is served from the page cache.
3999                let journal = journal.sync(1).await.expect("Failed to sync");
4000                journal.get(1, long_offset).await.expect("Failed to get");
4001                assert_eq!(journal.try_get_sync(1, long_offset), Some(long.clone()));
4002
4003                // An offset at the end of the section is a miss.
4004                let size = journal.size(1).expect("Failed to size");
4005                assert!(journal.try_get_sync(1, size).is_none());
4006
4007                journal.destroy().await.expect("Failed to destroy");
4008            }
4009        });
4010    }
4011}