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