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` method. When
32//! calling `close`, all pending data is automatically synced and any open blobs are dropped.
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 to produce a stream of all items in the `Journal` in order of their `section` and
45//! `offset`.
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 mut 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//!     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    frame::{
86        decode_item, decode_length_prefix, encode_frame_into, find_frame, read_frame_at, FrameInfo,
87    },
88    Error,
89};
90use commonware_codec::{varint::MAX_U32_VARINT_SIZE, Codec, CodecShared};
91use commonware_runtime::{
92    buffer::paged::{CacheRef, Replay, Writer},
93    Blob, Buf, Handle, IoBuf, Metrics, Storage,
94};
95use futures::stream::{self, Stream, StreamExt};
96use std::{io::Cursor, num::NonZeroUsize};
97use tracing::{trace, warn};
98
99/// Configuration for `Journal` storage.
100#[derive(Clone)]
101pub struct Config<C> {
102    /// The `commonware-runtime::Storage` partition to use
103    /// for storing journal blobs.
104    pub partition: String,
105
106    /// Optional compression level (using `zstd`) to apply to data before storing.
107    pub compression: Option<u8>,
108
109    /// The codec configuration to use for encoding and decoding items.
110    pub codec_config: C,
111
112    /// The page cache to use for caching data.
113    pub page_cache: CacheRef,
114
115    /// The size of the write buffer to use for each blob.
116    pub write_buffer: NonZeroUsize,
117}
118
119/// State for replaying a single section's blob.
120struct ReplayState<'a, B: Blob, C> {
121    section: u64,
122    // Need a [Writer] because replay may repair the blob by rewinding invalid trailing data.
123    blob: &'a mut Writer<B>,
124    replay: Replay<B>,
125    skip_bytes: u64,
126    offset: u64,
127    valid_offset: u64,
128    codec_config: C,
129    compressed: bool,
130    done: bool,
131}
132
133/// A segmented journal with variable-size entries.
134///
135/// Each section is stored in a separate blob. Items are length-prefixed with a varint.
136///
137/// # Repair
138///
139/// Like
140/// [sqlite](https://github.com/sqlite/sqlite/blob/8658a8df59f00ec8fcfea336a2a6a4b5ef79d2ee/src/wal.c#L1504-L1505)
141/// and
142/// [rocksdb](https://github.com/facebook/rocksdb/blob/0c533e61bc6d89fdf1295e8e0bcee4edb3aef401/include/rocksdb/options.h#L441-L445),
143/// the first invalid data read will be considered the new end of the journal (and the
144/// underlying [Blob] will be truncated to the last valid item). Repair occurs during
145/// replay (not init) because any blob could have trailing bytes.
146pub struct Journal<E: Storage + Metrics, V: Codec> {
147    manager: Manager<E, AppendFactory>,
148
149    /// Compression level (if enabled).
150    compression: Option<u8>,
151
152    /// Codec configuration.
153    codec_config: V::Cfg,
154}
155
156impl<E: Storage + Metrics, V: CodecShared> Journal<E, V> {
157    /// Initialize a new `Journal` instance.
158    ///
159    /// All backing blobs are opened but not read during
160    /// initialization. The `replay` method can be used
161    /// to iterate over all items in the `Journal`.
162    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
163        let manager_cfg = ManagerConfig {
164            partition: cfg.partition,
165            factory: AppendFactory {
166                write_buffer: cfg.write_buffer,
167                page_cache_ref: cfg.page_cache,
168            },
169        };
170        let manager = Manager::init(context, manager_cfg).await?;
171
172        Ok(Self {
173            manager,
174            compression: cfg.compression,
175            codec_config: cfg.codec_config,
176        })
177    }
178
179    /// Reads an item from the blob at the given offset.
180    async fn read(
181        compressed: bool,
182        cfg: &V::Cfg,
183        blob: &Writer<E::Blob>,
184        offset: u64,
185    ) -> Result<(u64, u32, V), Error> {
186        read_frame_at(blob, offset, cfg, compressed).await
187    }
188
189    /// Returns an ordered stream of all items in the journal starting with the item at the given
190    /// `start_section` and `offset` into that section. Each item is returned as a tuple of
191    /// (section, offset, size, item).
192    pub async fn replay(
193        &mut self,
194        start_section: u64,
195        mut start_offset: u64,
196        buffer: NonZeroUsize,
197    ) -> Result<impl Stream<Item = Result<(u64, u64, u32, V), Error>> + Send + '_, Error> {
198        // Collect all blobs to replay. This validates replay setup but does not allocate
199        // `buffer` bytes per blob; page buffers are allocated later by `Replay::ensure`.
200        let codec_config = self.codec_config.clone();
201        let compressed = self.compression.is_some();
202        let mut blobs = Vec::new();
203        for (&section, blob) in self.manager.sections_from(start_section) {
204            if section == start_section && start_offset > blob.size() {
205                return Err(Error::ItemOutOfRange(start_offset));
206            }
207            let replay = blob.replay(buffer).await?;
208            blobs.push((section, blob, replay, codec_config.clone(), compressed));
209        }
210
211        // Stream items as they are read to avoid occupying too much memory
212        Ok(stream::iter(blobs).flat_map(
213            move |(section, blob, replay, codec_config, compressed)| {
214                // Calculate initial skip bytes for first blob
215                let skip_bytes = if section == start_section {
216                    start_offset
217                } else {
218                    start_offset = 0;
219                    0
220                };
221
222                stream::unfold(
223                    ReplayState {
224                        section,
225                        blob,
226                        replay,
227                        skip_bytes,
228                        offset: 0,
229                        valid_offset: skip_bytes,
230                        codec_config,
231                        compressed,
232                        done: false,
233                    },
234                    move |mut state| async move {
235                        if state.done {
236                            return None;
237                        }
238
239                        let blob_size = state.replay.blob_size();
240                        let mut batch: Vec<Result<(u64, u64, u32, V), Error>> = Vec::new();
241                        loop {
242                            // Ensure we have enough data for varint header.
243                            // ensure() returns Ok(false) if exhausted with fewer bytes,
244                            // but we still try to decode from remaining bytes.
245                            match state.replay.ensure(MAX_U32_VARINT_SIZE).await {
246                                Ok(true) => {}
247                                Ok(false) => {
248                                    // Reader exhausted - check if buffer is empty
249                                    if state.replay.remaining() == 0 {
250                                        state.done = true;
251                                        return if batch.is_empty() {
252                                            None
253                                        } else {
254                                            Some((batch, state))
255                                        };
256                                    }
257                                    // Buffer still has data - continue to try decoding
258                                }
259                                Err(err) => {
260                                    batch.push(Err(err.into()));
261                                    state.done = true;
262                                    return Some((batch, state));
263                                }
264                            }
265
266                            // Skip bytes if needed (for start_offset)
267                            if state.skip_bytes > 0 {
268                                let to_skip =
269                                    state.skip_bytes.min(state.replay.remaining() as u64) as usize;
270                                state.replay.advance(to_skip);
271                                state.skip_bytes -= to_skip as u64;
272                                state.offset += to_skip as u64;
273                                continue;
274                            }
275
276                            // Try to decode length prefix
277                            let before_remaining = state.replay.remaining();
278                            let (item_size, varint_len) =
279                                match decode_length_prefix(&mut state.replay) {
280                                    Ok(result) => result,
281                                    Err(err) => {
282                                        // Could be incomplete varint - check if reader exhausted
283                                        if state.replay.is_exhausted()
284                                            || before_remaining < MAX_U32_VARINT_SIZE
285                                        {
286                                            // Treat as trailing bytes
287                                            if state.valid_offset < blob_size
288                                                && state.offset < blob_size
289                                            {
290                                                warn!(
291                                                    blob = state.section,
292                                                    bad_offset = state.offset,
293                                                    new_size = state.valid_offset,
294                                                    "trailing bytes detected: truncating"
295                                                );
296                                                // Tail repair is exceptional; make it durable
297                                                // immediately so callers do not need to track
298                                                // replay-time repaired sections separately.
299                                                if let Err(err) =
300                                                    state.blob.resize(state.valid_offset).await
301                                                {
302                                                    batch.push(Err(err.into()));
303                                                    state.done = true;
304                                                    return Some((batch, state));
305                                                }
306                                                if let Err(err) = state.blob.sync().await {
307                                                    batch.push(Err(err.into()));
308                                                    state.done = true;
309                                                    return Some((batch, state));
310                                                }
311                                            }
312                                            state.done = true;
313                                            return if batch.is_empty() {
314                                                None
315                                            } else {
316                                                Some((batch, state))
317                                            };
318                                        }
319                                        batch.push(Err(err));
320                                        state.done = true;
321                                        return Some((batch, state));
322                                    }
323                                };
324
325                            // Ensure we have enough data for item body
326                            match state.replay.ensure(item_size).await {
327                                Ok(true) => {}
328                                Ok(false) => {
329                                    // Incomplete item at end - truncate
330                                    warn!(
331                                        blob = state.section,
332                                        bad_offset = state.offset,
333                                        new_size = state.valid_offset,
334                                        "incomplete item at end: truncating"
335                                    );
336                                    if let Err(err) = state.blob.resize(state.valid_offset).await {
337                                        batch.push(Err(err.into()));
338                                        state.done = true;
339                                        return Some((batch, state));
340                                    }
341                                    if let Err(err) = state.blob.sync().await {
342                                        batch.push(Err(err.into()));
343                                        state.done = true;
344                                        return Some((batch, state));
345                                    }
346                                    state.done = true;
347                                    return if batch.is_empty() {
348                                        None
349                                    } else {
350                                        Some((batch, state))
351                                    };
352                                }
353                                Err(err) => {
354                                    batch.push(Err(err.into()));
355                                    state.done = true;
356                                    return Some((batch, state));
357                                }
358                            }
359
360                            // Decode item - use take() to limit bytes read
361                            let item_offset = state.offset;
362                            let next_offset = match state
363                                .offset
364                                .checked_add(varint_len as u64)
365                                .and_then(|o| o.checked_add(item_size as u64))
366                            {
367                                Some(o) => o,
368                                None => {
369                                    batch.push(Err(Error::OffsetOverflow));
370                                    state.done = true;
371                                    return Some((batch, state));
372                                }
373                            };
374                            match decode_item::<V>(
375                                (&mut state.replay).take(item_size),
376                                &state.codec_config,
377                                state.compressed,
378                            ) {
379                                Ok(decoded) => {
380                                    batch.push(Ok((
381                                        state.section,
382                                        item_offset,
383                                        item_size as u32,
384                                        decoded,
385                                    )));
386                                    state.valid_offset = next_offset;
387                                    state.offset = next_offset;
388                                }
389                                Err(err) => {
390                                    batch.push(Err(err));
391                                    state.done = true;
392                                    return Some((batch, state));
393                                }
394                            }
395
396                            // Return batch if we have items and buffer is low
397                            if !batch.is_empty() && state.replay.remaining() < MAX_U32_VARINT_SIZE {
398                                return Some((batch, state));
399                            }
400                        }
401                    },
402                )
403                .flat_map(stream::iter)
404            },
405        ))
406    }
407
408    /// Encode an item.
409    ///
410    /// Returns `(buf, item_len)` where `item_len` is the length of the encoded (and
411    /// possibly compressed) payload, excluding the size prefix.
412    fn encode_item(compression: Option<u8>, item: &V) -> Result<(Vec<u8>, u32), Error> {
413        let mut buf = Vec::new();
414        let item_len = encode_frame_into(compression, item, &mut buf)?;
415        Ok((buf, item_len))
416    }
417
418    /// Appends an item to `Journal` in a given `section`, returning the offset
419    /// where the item was written and the size of the item (which may differ
420    /// from the raw encoded size if compression is enabled).
421    pub async fn append(&mut self, section: u64, item: &V) -> Result<(u64, u32), Error> {
422        let (buf, item_len) = Self::encode_item(self.compression, item)?;
423        self.append_raw(section, IoBuf::from(buf))
424            .await
425            .map(|offset| (offset, item_len))
426    }
427
428    /// Append pre-encoded bytes to the given section, returning the byte offset
429    /// where the data was written.
430    ///
431    /// The buffer must be in the on-disk format produced by [Self::encode_item].
432    async fn append_raw(&mut self, section: u64, buf: IoBuf) -> Result<u64, Error> {
433        let blob = self.manager.get_or_create(section).await?;
434        let offset = blob.append_owned(buf).await?;
435        trace!(blob = section, offset, "appended item");
436        Ok(offset)
437    }
438
439    /// Retrieves an item from `Journal` at a given `section` and `offset`.
440    ///
441    /// # Errors
442    ///  - [Error::AlreadyPrunedToSection] if the requested `section` has been pruned during the
443    ///    current execution.
444    ///  - [Error::SectionOutOfRange] if the requested `section` is empty (i.e. has never had any
445    ///    data appended to it, or has been pruned in a previous execution).
446    ///  - An invalid `offset` for a given section (that is, an offset that doesn't correspond to a
447    ///    previously appended item) will result in an error, with the specific type being
448    ///    undefined.
449    pub async fn get(&self, section: u64, offset: u64) -> Result<V, Error> {
450        let blob = self
451            .manager
452            .get(section)?
453            .ok_or(Error::SectionOutOfRange(section))?;
454
455        // Perform a multi-op read.
456        let (_, _, item) =
457            Self::read(self.compression.is_some(), &self.codec_config, blob, offset).await?;
458        Ok(item)
459    }
460
461    /// Read multiple items from the same section.
462    ///
463    /// Offsets should be sorted in ascending order.
464    pub async fn get_many(&self, section: u64, offsets: &[u64]) -> Result<Vec<V>, Error> {
465        if offsets.is_empty() {
466            return Ok(Vec::new());
467        }
468        let blob = self
469            .manager
470            .get(section)?
471            .ok_or(Error::SectionOutOfRange(section))?;
472
473        let compressed = self.compression.is_some();
474        let cfg = &self.codec_config;
475        let mut items = Vec::with_capacity(offsets.len());
476        for &offset in offsets {
477            let (_, _, item) = Self::read(compressed, cfg, blob, offset).await?;
478            items.push(item);
479        }
480        Ok(items)
481    }
482
483    /// Get an item if it can be done synchronously (e.g. without I/O), returning `None` otherwise.
484    pub fn try_get_sync(&self, section: u64, offset: u64) -> Option<V> {
485        let blob = self.manager.get(section).ok()??;
486        let remaining = blob.size().checked_sub(offset)?;
487        let header_len = usize::try_from(remaining.min(MAX_U32_VARINT_SIZE as u64)).ok()?;
488        if header_len == 0 {
489            return None;
490        }
491
492        // Read the varint header to determine item size.
493        let mut header = [0u8; MAX_U32_VARINT_SIZE];
494        if !blob.try_read_sync_into(&mut header[..header_len], offset) {
495            return None;
496        }
497        let mut cursor = Cursor::new(&header[..header_len]);
498        let (_, frame_info) = find_frame(&mut cursor, offset).ok()?;
499        let (varint_len, data_len) = match frame_info {
500            FrameInfo::Complete {
501                varint_len,
502                data_len,
503            } => (varint_len, data_len),
504            FrameInfo::Incomplete {
505                varint_len,
506                total_len,
507                ..
508            } => (varint_len, total_len),
509        };
510        let item_len = varint_len.checked_add(data_len)?;
511        if item_len > usize::try_from(remaining).ok()? {
512            return None;
513        }
514
515        // If the full item fits in the header read, decode directly.
516        let compressed = self.compression.is_some();
517        if item_len <= header_len {
518            return decode_item::<V>(
519                &header[varint_len..varint_len + data_len],
520                &self.codec_config,
521                compressed,
522            )
523            .ok();
524        }
525
526        // Otherwise try reading the full item from cache.
527        let mut buf = vec![0u8; item_len];
528        if !blob.try_read_sync_into(&mut buf, offset) {
529            return None;
530        }
531        decode_item::<V>(
532            &buf[varint_len..varint_len + data_len],
533            &self.codec_config,
534            compressed,
535        )
536        .ok()
537    }
538
539    /// Gets the size of the journal for a specific section.
540    ///
541    /// Returns 0 if the section does not exist.
542    pub fn size(&self, section: u64) -> Result<u64, Error> {
543        self.manager.size(section)
544    }
545
546    /// Rewinds the journal to the given `section` and `size`.
547    ///
548    /// This removes any data beyond the specified `section` and `size`.
549    ///
550    /// # Warnings
551    ///
552    /// * This operation is not guaranteed to survive restarts until sync is called.
553    /// * This operation is not atomic, but it will always leave the journal in a consistent state
554    ///   in the event of failure since blobs are always removed in reverse order of section.
555    pub async fn rewind(&mut self, section: u64, size: u64) -> Result<(), Error> {
556        self.manager.rewind(section, size).await
557    }
558
559    /// Rewinds the `section` to the given `size`.
560    ///
561    /// Unlike [Self::rewind], this method does not modify anything other than the given `section`.
562    ///
563    /// # Warning
564    ///
565    /// This operation is not guaranteed to survive restarts until sync is called.
566    pub async fn rewind_section(&mut self, section: u64, size: u64) -> Result<(), Error> {
567        self.manager.rewind_section(section, size).await
568    }
569
570    /// Ensures the given `sections` are synced to the underlying store.
571    ///
572    /// If a selected section does not exist, no error will be returned.
573    pub async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
574        self.manager.sync(sections).await
575    }
576
577    /// Start syncing the given `sections` to storage.
578    pub async fn start_sync(
579        &mut self,
580        sections: impl crate::Sections,
581    ) -> Result<Handle<()>, Error> {
582        self.manager.start_sync(sections).await
583    }
584
585    /// Syncs all open sections.
586    pub async fn sync_all(&mut self) -> Result<(), Error> {
587        self.manager.sync_all().await
588    }
589
590    /// Prunes all `sections` less than `min`. Returns true if any sections were pruned.
591    pub async fn prune(&mut self, min: u64) -> Result<bool, Error> {
592        self.manager.prune(min).await
593    }
594
595    /// Returns the number of the oldest section in the journal.
596    pub fn oldest_section(&self) -> Option<u64> {
597        self.manager.oldest_section()
598    }
599
600    /// Returns the number of the newest section in the journal.
601    pub fn newest_section(&self) -> Option<u64> {
602        self.manager.newest_section()
603    }
604
605    /// Returns true if no sections exist.
606    pub fn is_empty(&self) -> bool {
607        self.manager.is_empty()
608    }
609
610    /// Returns the number of sections.
611    pub fn num_sections(&self) -> usize {
612        self.manager.num_sections()
613    }
614
615    /// Removes any underlying blobs created by the journal.
616    pub async fn destroy(self) -> Result<(), Error> {
617        self.manager.destroy().await
618    }
619
620    /// Clear all data, resetting the journal to an empty state.
621    ///
622    /// Unlike `destroy`, this keeps the journal alive so it can be reused.
623    pub async fn clear(&mut self) -> Result<(), Error> {
624        self.manager.clear().await
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631    use commonware_codec::{varint::UInt, EncodeSize, Write as _};
632    use commonware_macros::test_traced;
633    use commonware_runtime::{deterministic, Blob, BufMut, Runner, Storage, Supervisor as _};
634    use commonware_utils::{NZUsize, NZU16};
635    use futures::{pin_mut, StreamExt};
636    use std::num::NonZeroU16;
637
638    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
639    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
640
641    #[test_traced]
642    fn test_journal_append_and_read() {
643        // Initialize the deterministic context
644        let executor = deterministic::Runner::default();
645
646        // Start the test within the executor
647        executor.start(|context| async move {
648            // Initialize the journal
649            let cfg = Config {
650                partition: "test-partition".into(),
651                compression: None,
652                codec_config: (),
653                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
654                write_buffer: NZUsize!(1024),
655            };
656            let index = 1u64;
657            let data = 10;
658            let mut journal = Journal::init(context.child("first"), cfg.clone())
659                .await
660                .expect("Failed to initialize journal");
661
662            // Append an item to the journal
663            journal
664                .append(index, &data)
665                .await
666                .expect("Failed to append data");
667
668            // Check metrics
669            let buffer = context.encode();
670            assert!(buffer.contains("first_tracked 1"));
671
672            // Drop and re-open the journal to simulate a restart
673            journal.sync(index).await.expect("Failed to sync journal");
674            drop(journal);
675            let mut journal = Journal::<_, i32>::init(context.child("second"), cfg)
676                .await
677                .expect("Failed to re-initialize journal");
678
679            // Replay the journal and collect items
680            let mut items = Vec::new();
681            let stream = journal
682                .replay(0, 0, NZUsize!(1024))
683                .await
684                .expect("unable to setup replay");
685            pin_mut!(stream);
686            while let Some(result) = stream.next().await {
687                match result {
688                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
689                    Err(err) => panic!("Failed to read item: {err}"),
690                }
691            }
692
693            // Verify that the item was replayed correctly
694            assert_eq!(items.len(), 1);
695            assert_eq!(items[0].0, index);
696            assert_eq!(items[0].1, data);
697
698            // Check metrics
699            let buffer = context.encode();
700            assert!(buffer.contains("second_tracked 1"));
701        });
702    }
703
704    #[test_traced]
705    fn test_journal_multiple_appends_and_reads() {
706        // Initialize the deterministic context
707        let executor = deterministic::Runner::default();
708
709        // Start the test within the executor
710        executor.start(|context| async move {
711            // Create a journal configuration
712            let cfg = Config {
713                partition: "test-partition".into(),
714                compression: None,
715                codec_config: (),
716                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
717                write_buffer: NZUsize!(1024),
718            };
719
720            // Initialize the journal
721            let mut journal = Journal::init(context.child("first"), cfg.clone())
722                .await
723                .expect("Failed to initialize journal");
724
725            // Append multiple items to different blobs
726            let data_items = vec![(1u64, 1), (1u64, 2), (2u64, 3), (3u64, 4)];
727            for (index, data) in &data_items {
728                journal
729                    .append(*index, data)
730                    .await
731                    .expect("Failed to append data");
732                journal.sync(*index).await.expect("Failed to sync blob");
733            }
734
735            // Check metrics
736            let buffer = context.encode();
737            assert!(buffer.contains("first_tracked 3"));
738            assert!(buffer.contains("first_synced_total 4"));
739
740            // Drop and re-open the journal to simulate a restart
741            drop(journal);
742            let mut journal = Journal::init(context.child("second"), cfg)
743                .await
744                .expect("Failed to re-initialize journal");
745
746            // Replay the journal and collect items
747            let mut items = Vec::<(u64, u32)>::new();
748            {
749                let stream = journal
750                    .replay(0, 0, NZUsize!(1024))
751                    .await
752                    .expect("unable to setup replay");
753                pin_mut!(stream);
754                while let Some(result) = stream.next().await {
755                    match result {
756                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
757                        Err(err) => panic!("Failed to read item: {err}"),
758                    }
759                }
760            }
761
762            // Verify that all items were replayed correctly
763            assert_eq!(items.len(), data_items.len());
764            for ((expected_index, expected_data), (actual_index, actual_data)) in
765                data_items.iter().zip(items.iter())
766            {
767                assert_eq!(actual_index, expected_index);
768                assert_eq!(actual_data, expected_data);
769            }
770
771            // Cleanup
772            journal.destroy().await.expect("Failed to destroy journal");
773        });
774    }
775
776    #[test_traced]
777    fn test_journal_prune_blobs() {
778        // Initialize the deterministic context
779        let executor = deterministic::Runner::default();
780
781        // Start the test within the executor
782        executor.start(|context| async move {
783            // Create a journal configuration
784            let cfg = Config {
785                partition: "test-partition".into(),
786                compression: None,
787                codec_config: (),
788                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
789                write_buffer: NZUsize!(1024),
790            };
791
792            // Initialize the journal
793            let mut journal = Journal::init(context.child("first"), cfg.clone())
794                .await
795                .expect("Failed to initialize journal");
796
797            // Append items to multiple blobs
798            for index in 1u64..=5u64 {
799                journal
800                    .append(index, &index)
801                    .await
802                    .expect("Failed to append data");
803                journal.sync(index).await.expect("Failed to sync blob");
804            }
805
806            // Add one item out-of-order
807            let data = 99;
808            journal
809                .append(2u64, &data)
810                .await
811                .expect("Failed to append data");
812            journal.sync(2u64).await.expect("Failed to sync blob");
813
814            // Prune blobs with indices less than 3
815            journal.prune(3).await.expect("Failed to prune blobs");
816
817            // Check metrics
818            let buffer = context.encode();
819            assert!(buffer.contains("first_pruned_total 2"));
820
821            // Prune again with a section less than the previous one, should be a no-op
822            journal.prune(2).await.expect("Failed to no-op prune");
823            let buffer = context.encode();
824            assert!(buffer.contains("first_pruned_total 2"));
825
826            // Drop and re-open the journal to simulate a restart
827            drop(journal);
828            let mut journal = Journal::init(context.child("second"), cfg.clone())
829                .await
830                .expect("Failed to re-initialize journal");
831
832            // Replay the journal and collect items
833            let mut items = Vec::<(u64, u64)>::new();
834            {
835                let stream = journal
836                    .replay(0, 0, NZUsize!(1024))
837                    .await
838                    .expect("unable to setup replay");
839                pin_mut!(stream);
840                while let Some(result) = stream.next().await {
841                    match result {
842                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
843                        Err(err) => panic!("Failed to read item: {err}"),
844                    }
845                }
846            }
847
848            // Verify that items from blobs 1 and 2 are not present
849            assert_eq!(items.len(), 3);
850            let expected_indices = [3u64, 4u64, 5u64];
851            for (item, expected_index) in items.iter().zip(expected_indices.iter()) {
852                assert_eq!(item.0, *expected_index);
853            }
854
855            // Prune all blobs
856            journal.prune(6).await.expect("Failed to prune blobs");
857
858            // Drop the journal
859            drop(journal);
860
861            // Ensure no remaining blobs exist
862            //
863            // Note: We don't remove the partition, so this does not error
864            // and instead returns an empty list of blobs.
865            assert!(context
866                .scan(&cfg.partition)
867                .await
868                .expect("Failed to list blobs")
869                .is_empty());
870        });
871    }
872
873    #[test_traced]
874    fn test_journal_prune_guard() {
875        let executor = deterministic::Runner::default();
876
877        executor.start(|context| async move {
878            let cfg = Config {
879                partition: "test-partition".into(),
880                compression: None,
881                codec_config: (),
882                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
883                write_buffer: NZUsize!(1024),
884            };
885
886            let mut journal = Journal::init(context.child("storage"), cfg.clone())
887                .await
888                .expect("Failed to initialize journal");
889
890            // Append items to sections 1-5
891            for section in 1u64..=5u64 {
892                journal
893                    .append(section, &(section as i32))
894                    .await
895                    .expect("Failed to append data");
896                journal.sync(section).await.expect("Failed to sync");
897            }
898
899            // Prune sections < 3
900            journal.prune(3).await.expect("Failed to prune");
901
902            // Test that accessing pruned sections returns the correct error
903
904            // Test append on pruned section
905            match journal.append(1, &100).await {
906                Err(Error::AlreadyPrunedToSection(3)) => {}
907                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
908            }
909
910            match journal.append(2, &100).await {
911                Err(Error::AlreadyPrunedToSection(3)) => {}
912                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
913            }
914
915            // Test get on pruned section
916            match journal.get(1, 0).await {
917                Err(Error::AlreadyPrunedToSection(3)) => {}
918                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
919            }
920
921            // Test size on pruned section
922            match journal.size(1) {
923                Err(Error::AlreadyPrunedToSection(3)) => {}
924                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
925            }
926
927            // Test rewind on pruned section
928            match journal.rewind(2, 0).await {
929                Err(Error::AlreadyPrunedToSection(3)) => {}
930                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
931            }
932
933            // Test rewind_section on pruned section
934            match journal.rewind_section(1, 0).await {
935                Err(Error::AlreadyPrunedToSection(3)) => {}
936                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
937            }
938
939            // Test sync on pruned section
940            match journal.sync(2).await {
941                Err(Error::AlreadyPrunedToSection(3)) => {}
942                other => panic!("Expected AlreadyPrunedToSection(3), got {other:?}"),
943            }
944
945            // Test that accessing sections at or after the threshold works
946            assert!(journal.get(3, 0).await.is_ok());
947            assert!(journal.get(4, 0).await.is_ok());
948            assert!(journal.get(5, 0).await.is_ok());
949            assert!(journal.size(3).is_ok());
950            assert!(journal.sync(4).await.is_ok());
951
952            // Append to section at threshold should work
953            journal
954                .append(3, &999)
955                .await
956                .expect("Should be able to append to section 3");
957
958            // Prune more sections
959            journal.prune(5).await.expect("Failed to prune");
960
961            // Verify sections 3 and 4 are now pruned
962            match journal.get(3, 0).await {
963                Err(Error::AlreadyPrunedToSection(5)) => {}
964                other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
965            }
966
967            match journal.get(4, 0).await {
968                Err(Error::AlreadyPrunedToSection(5)) => {}
969                other => panic!("Expected AlreadyPrunedToSection(5), got {other:?}"),
970            }
971
972            // Section 5 should still be accessible
973            assert!(journal.get(5, 0).await.is_ok());
974        });
975    }
976
977    #[test_traced]
978    fn test_journal_prune_guard_across_restart() {
979        let executor = deterministic::Runner::default();
980
981        executor.start(|context| async move {
982            let cfg = Config {
983                partition: "test-partition".into(),
984                compression: None,
985                codec_config: (),
986                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
987                write_buffer: NZUsize!(1024),
988            };
989
990            // First session: create and prune
991            {
992                let mut journal = Journal::init(context.child("first"), cfg.clone())
993                    .await
994                    .expect("Failed to initialize journal");
995
996                for section in 1u64..=5u64 {
997                    journal
998                        .append(section, &(section as i32))
999                        .await
1000                        .expect("Failed to append data");
1001                    journal.sync(section).await.expect("Failed to sync");
1002                }
1003
1004                journal.prune(3).await.expect("Failed to prune");
1005            }
1006
1007            // Second session: verify oldest_retained_section is reset
1008            {
1009                let journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
1010                    .await
1011                    .expect("Failed to re-initialize journal");
1012
1013                // But the actual sections 1 and 2 should be gone from storage
1014                // so get should return SectionOutOfRange, not AlreadyPrunedToSection
1015                match journal.get(1, 0).await {
1016                    Err(Error::SectionOutOfRange(1)) => {}
1017                    other => panic!("Expected SectionOutOfRange(1), got {other:?}"),
1018                }
1019
1020                match journal.get(2, 0).await {
1021                    Err(Error::SectionOutOfRange(2)) => {}
1022                    other => panic!("Expected SectionOutOfRange(2), got {other:?}"),
1023                }
1024
1025                // Sections 3-5 should still be accessible
1026                assert!(journal.get(3, 0).await.is_ok());
1027                assert!(journal.get(4, 0).await.is_ok());
1028                assert!(journal.get(5, 0).await.is_ok());
1029            }
1030        });
1031    }
1032
1033    #[test_traced]
1034    fn test_journal_with_invalid_blob_name() {
1035        // Initialize the deterministic context
1036        let executor = deterministic::Runner::default();
1037
1038        // Start the test within the executor
1039        executor.start(|context| async move {
1040            // Create a journal configuration
1041            let cfg = Config {
1042                partition: "test-partition".into(),
1043                compression: None,
1044                codec_config: (),
1045                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1046                write_buffer: NZUsize!(1024),
1047            };
1048
1049            // Manually create a blob with an invalid name (not 8 bytes)
1050            let invalid_blob_name = b"invalid"; // Less than 8 bytes
1051            let (blob, _) = context
1052                .open(&cfg.partition, invalid_blob_name)
1053                .await
1054                .expect("Failed to create blob with invalid name");
1055            blob.sync().await.expect("Failed to sync blob");
1056
1057            // Attempt to initialize the journal
1058            let result = Journal::<_, u64>::init(context, cfg).await;
1059
1060            // Expect an error
1061            assert!(matches!(result, Err(Error::InvalidBlobName(_))));
1062        });
1063    }
1064
1065    #[test_traced]
1066    fn test_journal_read_size_missing() {
1067        // Initialize the deterministic context
1068        let executor = deterministic::Runner::default();
1069
1070        // Start the test within the executor
1071        executor.start(|context| async move {
1072            // Create a journal configuration
1073            let cfg = Config {
1074                partition: "test-partition".into(),
1075                compression: None,
1076                codec_config: (),
1077                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1078                write_buffer: NZUsize!(1024),
1079            };
1080
1081            // Manually create a blob with incomplete size data
1082            let section = 1u64;
1083            let blob_name = section.to_be_bytes();
1084            let (blob, _) = context
1085                .open(&cfg.partition, &blob_name)
1086                .await
1087                .expect("Failed to create blob");
1088
1089            // Write incomplete varint by encoding u32::MAX (5 bytes) and truncating to 1 byte
1090            let mut incomplete_data = Vec::new();
1091            UInt(u32::MAX).write(&mut incomplete_data);
1092            incomplete_data.truncate(1);
1093            blob.write_at_sync(0, incomplete_data)
1094                .await
1095                .expect("Failed to write incomplete data");
1096
1097            // Initialize the journal
1098            let mut journal = Journal::init(context, cfg)
1099                .await
1100                .expect("Failed to initialize journal");
1101
1102            // Attempt to replay the journal
1103            let stream = journal
1104                .replay(0, 0, NZUsize!(1024))
1105                .await
1106                .expect("unable to setup replay");
1107            pin_mut!(stream);
1108            let mut items = Vec::<(u64, u64)>::new();
1109            while let Some(result) = stream.next().await {
1110                match result {
1111                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1112                    Err(err) => panic!("Failed to read item: {err}"),
1113                }
1114            }
1115            assert!(items.is_empty());
1116        });
1117    }
1118
1119    #[test_traced]
1120    fn test_journal_replay_reports_resize_error_on_trailing_bytes() {
1121        let executor = deterministic::Runner::default();
1122        executor.start(|context| async move {
1123            let cfg = Config {
1124                partition: "test-partition".into(),
1125                compression: None,
1126                codec_config: (),
1127                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1128                write_buffer: NZUsize!(1024),
1129            };
1130
1131            // Leave one byte in the first page so the trailing bytes below cross the page
1132            // boundary and repair must issue a physical resize.
1133            let section = 1u64;
1134            let item = [10u8; 1021];
1135            let item_record_size =
1136                UInt(item.encode_size() as u32).encode_size() + item.encode_size();
1137            assert_eq!(item_record_size, PAGE_SIZE.get() as usize - 1);
1138
1139            let mut journal = Journal::init(context.child("first"), cfg.clone())
1140                .await
1141                .expect("Failed to initialize journal");
1142            journal
1143                .append(section, &item)
1144                .await
1145                .expect("Failed to append item");
1146            journal
1147                .append_raw(section, IoBuf::copy_from_slice(&[0xFF, 0xFF]))
1148                .await
1149                .expect("Failed to append trailing bytes");
1150            journal.sync(section).await.expect("Failed to sync journal");
1151            drop(journal);
1152
1153            let mut journal = Journal::init(context.child("second"), cfg)
1154                .await
1155                .expect("Failed to re-initialize journal");
1156            *context.storage_fault_config().write() = deterministic::FaultConfig {
1157                resize_rate: Some(1.0),
1158                ..Default::default()
1159            };
1160
1161            let stream = journal
1162                .replay(0, 0, NZUsize!(1024))
1163                .await
1164                .expect("unable to setup replay");
1165            pin_mut!(stream);
1166
1167            let first = stream
1168                .next()
1169                .await
1170                .expect("expected item before trailing bytes")
1171                .expect("failed to replay valid item");
1172            assert_eq!(first, (section, 0, item.encode_size() as u32, item));
1173
1174            // The trailing bytes cross the page boundary, so repair must issue a physical resize.
1175            match stream.next().await {
1176                Some(Err(_)) => {}
1177                other => {
1178                    panic!("expected resize error while repairing trailing bytes, got {other:?}")
1179                }
1180            }
1181            assert!(stream.next().await.is_none());
1182        });
1183    }
1184
1185    #[test_traced]
1186    fn test_journal_read_item_missing() {
1187        // Initialize the deterministic context
1188        let executor = deterministic::Runner::default();
1189
1190        // Start the test within the executor
1191        executor.start(|context| async move {
1192            // Create a journal configuration
1193            let cfg = Config {
1194                partition: "test-partition".into(),
1195                compression: None,
1196                codec_config: (),
1197                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1198                write_buffer: NZUsize!(1024),
1199            };
1200
1201            // Manually create a blob with missing item data
1202            let section = 1u64;
1203            let blob_name = section.to_be_bytes();
1204            let (blob, _) = context
1205                .open(&cfg.partition, &blob_name)
1206                .await
1207                .expect("Failed to create blob");
1208
1209            // Write size but incomplete item data
1210            let item_size: u32 = 10; // Size indicates 10 bytes of data
1211            let mut buf = Vec::new();
1212            UInt(item_size).write(&mut buf); // Varint encoding
1213            let data = [2u8; 5];
1214            BufMut::put_slice(&mut buf, &data);
1215            blob.write_at_sync(0, buf)
1216                .await
1217                .expect("Failed to write incomplete item");
1218
1219            // Initialize the journal
1220            let mut journal = Journal::init(context, cfg)
1221                .await
1222                .expect("Failed to initialize journal");
1223
1224            // Attempt to replay the journal
1225            let stream = journal
1226                .replay(0, 0, NZUsize!(1024))
1227                .await
1228                .expect("unable to setup replay");
1229            pin_mut!(stream);
1230            let mut items = Vec::<(u64, u64)>::new();
1231            while let Some(result) = stream.next().await {
1232                match result {
1233                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1234                    Err(err) => panic!("Failed to read item: {err}"),
1235                }
1236            }
1237            assert!(items.is_empty());
1238        });
1239    }
1240
1241    #[test_traced]
1242    fn test_journal_read_checksum_missing() {
1243        // Initialize the deterministic context
1244        let executor = deterministic::Runner::default();
1245
1246        // Start the test within the executor
1247        executor.start(|context| async move {
1248            // Create a journal configuration
1249            let cfg = Config {
1250                partition: "test-partition".into(),
1251                compression: None,
1252                codec_config: (),
1253                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1254                write_buffer: NZUsize!(1024),
1255            };
1256
1257            // Manually create a blob with missing checksum
1258            let section = 1u64;
1259            let blob_name = section.to_be_bytes();
1260            let (blob, _) = context
1261                .open(&cfg.partition, &blob_name)
1262                .await
1263                .expect("Failed to create blob");
1264
1265            // Prepare item data
1266            let item_data = b"Test data";
1267            let item_size = item_data.len() as u32;
1268
1269            // Write size (varint) and data, but no checksum
1270            let mut buf = Vec::new();
1271            UInt(item_size).write(&mut buf);
1272            BufMut::put_slice(&mut buf, item_data);
1273            blob.write_at_sync(0, buf)
1274                .await
1275                .expect("Failed to write item without checksum");
1276
1277            // Initialize the journal
1278            let mut journal = Journal::init(context, cfg)
1279                .await
1280                .expect("Failed to initialize journal");
1281
1282            // Attempt to replay the journal
1283            //
1284            // This will truncate the leftover bytes from our manual write.
1285            let stream = journal
1286                .replay(0, 0, NZUsize!(1024))
1287                .await
1288                .expect("unable to setup replay");
1289            pin_mut!(stream);
1290            let mut items = Vec::<(u64, u64)>::new();
1291            while let Some(result) = stream.next().await {
1292                match result {
1293                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1294                    Err(err) => panic!("Failed to read item: {err}"),
1295                }
1296            }
1297            assert!(items.is_empty());
1298        });
1299    }
1300
1301    #[test_traced]
1302    fn test_journal_read_checksum_mismatch() {
1303        // Initialize the deterministic context
1304        let executor = deterministic::Runner::default();
1305
1306        // Start the test within the executor
1307        executor.start(|context| async move {
1308            // Create a journal configuration
1309            let cfg = Config {
1310                partition: "test-partition".into(),
1311                compression: None,
1312                codec_config: (),
1313                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1314                write_buffer: NZUsize!(1024),
1315            };
1316
1317            // Manually create a blob with incorrect checksum
1318            let section = 1u64;
1319            let blob_name = section.to_be_bytes();
1320            let (blob, _) = context
1321                .open(&cfg.partition, &blob_name)
1322                .await
1323                .expect("Failed to create blob");
1324
1325            // Prepare item data
1326            let item_data = b"Test data";
1327            let item_size = item_data.len() as u32;
1328            let incorrect_checksum: u32 = 0xDEADBEEF;
1329
1330            // Write size (varint), data, and incorrect checksum
1331            let mut buf = Vec::new();
1332            UInt(item_size).write(&mut buf);
1333            BufMut::put_slice(&mut buf, item_data);
1334            buf.put_u32(incorrect_checksum);
1335            blob.write_at_sync(0, buf)
1336                .await
1337                .expect("Failed to write item with bad checksum");
1338
1339            // Initialize the journal
1340            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1341                .await
1342                .expect("Failed to initialize journal");
1343
1344            // Attempt to replay the journal
1345            {
1346                let stream = journal
1347                    .replay(0, 0, NZUsize!(1024))
1348                    .await
1349                    .expect("unable to setup replay");
1350                pin_mut!(stream);
1351                let mut items = Vec::<(u64, u64)>::new();
1352                while let Some(result) = stream.next().await {
1353                    match result {
1354                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1355                        Err(err) => panic!("Failed to read item: {err}"),
1356                    }
1357                }
1358                assert!(items.is_empty());
1359            }
1360            drop(journal);
1361
1362            // Confirm blob is expected length
1363            let (_, blob_size) = context
1364                .open(&cfg.partition, &section.to_be_bytes())
1365                .await
1366                .expect("Failed to open blob");
1367            assert_eq!(blob_size, 0);
1368        });
1369    }
1370
1371    #[test_traced]
1372    fn test_journal_truncation_recovery() {
1373        // Initialize the deterministic context
1374        let executor = deterministic::Runner::default();
1375
1376        // Start the test within the executor
1377        executor.start(|context| async move {
1378            // Create a journal configuration
1379            let cfg = Config {
1380                partition: "test-partition".into(),
1381                compression: None,
1382                codec_config: (),
1383                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1384                write_buffer: NZUsize!(1024),
1385            };
1386
1387            // Initialize the journal
1388            let mut journal = Journal::init(context.child("first"), cfg.clone())
1389                .await
1390                .expect("Failed to initialize journal");
1391
1392            // Append 1 item to the first index
1393            journal.append(1, &1).await.expect("Failed to append data");
1394
1395            // Append multiple items to the second section
1396            let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
1397            for (index, data) in &data_items {
1398                journal
1399                    .append(*index, data)
1400                    .await
1401                    .expect("Failed to append data");
1402                journal.sync(*index).await.expect("Failed to sync blob");
1403            }
1404
1405            // Sync all sections and drop the journal
1406            journal.sync_all().await.expect("Failed to sync");
1407            drop(journal);
1408
1409            // Manually corrupt the end of the second blob
1410            let (blob, blob_size) = context
1411                .open(&cfg.partition, &2u64.to_be_bytes())
1412                .await
1413                .expect("Failed to open blob");
1414            blob.resize(blob_size - 4)
1415                .await
1416                .expect("Failed to corrupt blob");
1417            blob.sync().await.expect("Failed to sync blob");
1418
1419            // Re-initialize the journal to simulate a restart
1420            let mut journal = Journal::init(context.child("second"), cfg.clone())
1421                .await
1422                .expect("Failed to re-initialize journal");
1423
1424            // Attempt to replay the journal
1425            let mut items = Vec::<(u64, u32)>::new();
1426            {
1427                let stream = journal
1428                    .replay(0, 0, NZUsize!(1024))
1429                    .await
1430                    .expect("unable to setup replay");
1431                pin_mut!(stream);
1432                while let Some(result) = stream.next().await {
1433                    match result {
1434                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1435                        Err(err) => panic!("Failed to read item: {err}"),
1436                    }
1437                }
1438            }
1439            drop(journal);
1440
1441            // Verify that replay stopped after corruption detected (the second blob).
1442            assert_eq!(items.len(), 1);
1443            assert_eq!(items[0].0, 1);
1444            assert_eq!(items[0].1, 1);
1445
1446            // Confirm second blob was truncated.
1447            let (_, blob_size) = context
1448                .open(&cfg.partition, &2u64.to_be_bytes())
1449                .await
1450                .expect("Failed to open blob");
1451            assert_eq!(blob_size, 0);
1452
1453            // Attempt to replay journal after truncation
1454            let mut journal = Journal::init(context.child("third"), cfg.clone())
1455                .await
1456                .expect("Failed to re-initialize journal");
1457
1458            // Attempt to replay the journal
1459            let mut items = Vec::<(u64, u32)>::new();
1460            {
1461                let stream = journal
1462                    .replay(0, 0, NZUsize!(1024))
1463                    .await
1464                    .expect("unable to setup replay");
1465                pin_mut!(stream);
1466                while let Some(result) = stream.next().await {
1467                    match result {
1468                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1469                        Err(err) => panic!("Failed to read item: {err}"),
1470                    }
1471                }
1472            }
1473
1474            // Verify that only non-corrupted items were replayed
1475            assert_eq!(items.len(), 1);
1476            assert_eq!(items[0].0, 1);
1477            assert_eq!(items[0].1, 1);
1478
1479            // Append a new item to truncated partition
1480            let (_offset, _) = journal.append(2, &5).await.expect("Failed to append data");
1481            journal.sync(2).await.expect("Failed to sync blob");
1482
1483            // Get the new item (offset is 0 since blob was truncated)
1484            let item = journal.get(2, 0).await.expect("Failed to get item");
1485            assert_eq!(item, 5);
1486
1487            // Drop the journal (data already synced)
1488            drop(journal);
1489
1490            // Re-initialize the journal to simulate a restart
1491            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1492                .await
1493                .expect("Failed to re-initialize journal");
1494
1495            // Attempt to replay the journal
1496            let mut items = Vec::<(u64, u32)>::new();
1497            {
1498                let stream = journal
1499                    .replay(0, 0, NZUsize!(1024))
1500                    .await
1501                    .expect("unable to setup replay");
1502                pin_mut!(stream);
1503                while let Some(result) = stream.next().await {
1504                    match result {
1505                        Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1506                        Err(err) => panic!("Failed to read item: {err}"),
1507                    }
1508                }
1509            }
1510
1511            // Verify that only non-corrupted items were replayed
1512            assert_eq!(items.len(), 2);
1513            assert_eq!(items[0].0, 1);
1514            assert_eq!(items[0].1, 1);
1515            assert_eq!(items[1].0, 2);
1516            assert_eq!(items[1].1, 5);
1517        });
1518    }
1519
1520    #[test_traced]
1521    fn test_journal_handling_extra_data() {
1522        // Initialize the deterministic context
1523        let executor = deterministic::Runner::default();
1524
1525        // Start the test within the executor
1526        executor.start(|context| async move {
1527            // Create a journal configuration
1528            let cfg = Config {
1529                partition: "test-partition".into(),
1530                compression: None,
1531                codec_config: (),
1532                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1533                write_buffer: NZUsize!(1024),
1534            };
1535
1536            // Initialize the journal
1537            let mut journal = Journal::init(context.child("first"), cfg.clone())
1538                .await
1539                .expect("Failed to initialize journal");
1540
1541            // Append 1 item to the first index
1542            journal.append(1, &1).await.expect("Failed to append data");
1543
1544            // Append multiple items to the second index
1545            let data_items = vec![(2u64, 2), (2u64, 3), (2u64, 4)];
1546            for (index, data) in &data_items {
1547                journal
1548                    .append(*index, data)
1549                    .await
1550                    .expect("Failed to append data");
1551                journal.sync(*index).await.expect("Failed to sync blob");
1552            }
1553
1554            // Sync all sections and drop the journal
1555            journal.sync_all().await.expect("Failed to sync");
1556            drop(journal);
1557
1558            // Manually add extra data to the end of the second blob
1559            let (blob, blob_size) = context
1560                .open(&cfg.partition, &2u64.to_be_bytes())
1561                .await
1562                .expect("Failed to open blob");
1563            blob.write_at_sync(blob_size, vec![0u8; 16])
1564                .await
1565                .expect("Failed to add extra data");
1566
1567            // Re-initialize the journal to simulate a restart
1568            let mut journal = Journal::init(context.child("second"), cfg)
1569                .await
1570                .expect("Failed to re-initialize journal");
1571
1572            // Attempt to replay the journal
1573            let mut items = Vec::<(u64, i32)>::new();
1574            let stream = journal
1575                .replay(0, 0, NZUsize!(1024))
1576                .await
1577                .expect("unable to setup replay");
1578            pin_mut!(stream);
1579            while let Some(result) = stream.next().await {
1580                match result {
1581                    Ok((blob_index, _, _, item)) => items.push((blob_index, item)),
1582                    Err(err) => panic!("Failed to read item: {err}"),
1583                }
1584            }
1585        });
1586    }
1587
1588    #[test_traced]
1589    fn test_journal_rewind() {
1590        // Initialize the deterministic context
1591        let executor = deterministic::Runner::default();
1592        executor.start(|context| async move {
1593            // Create journal
1594            let cfg = Config {
1595                partition: "test-partition".into(),
1596                compression: None,
1597                codec_config: (),
1598                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1599                write_buffer: NZUsize!(1024),
1600            };
1601            let mut journal = Journal::init(context, cfg).await.unwrap();
1602
1603            // Check size of non-existent section
1604            let size = journal.size(1).unwrap();
1605            assert_eq!(size, 0);
1606
1607            // Append data to section 1
1608            journal.append(1, &42i32).await.unwrap();
1609
1610            // Check size of section 1 - should be greater than 0
1611            let size = journal.size(1).unwrap();
1612            assert!(size > 0);
1613
1614            // Append more data and verify size increases
1615            journal.append(1, &43i32).await.unwrap();
1616            let new_size = journal.size(1).unwrap();
1617            assert!(new_size > size);
1618
1619            // Check size of different section - should still be 0
1620            let size = journal.size(2).unwrap();
1621            assert_eq!(size, 0);
1622
1623            // Append data to section 2
1624            journal.append(2, &44i32).await.unwrap();
1625
1626            // Check size of section 2 - should be greater than 0
1627            let size = journal.size(2).unwrap();
1628            assert!(size > 0);
1629
1630            // Rollback everything in section 1 and 2
1631            journal.rewind(1, 0).await.unwrap();
1632
1633            // Check size of section 1 - should be 0
1634            let size = journal.size(1).unwrap();
1635            assert_eq!(size, 0);
1636
1637            // Check size of section 2 - should be 0
1638            let size = journal.size(2).unwrap();
1639            assert_eq!(size, 0);
1640        });
1641    }
1642
1643    #[test_traced]
1644    fn test_journal_rewind_max_section() {
1645        let executor = deterministic::Runner::default();
1646        executor.start(|context| async move {
1647            let cfg = Config {
1648                partition: "test-partition".into(),
1649                compression: None,
1650                codec_config: (),
1651                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1652                write_buffer: NZUsize!(1024),
1653            };
1654            let mut journal = Journal::init(context, cfg).await.unwrap();
1655
1656            // Append to the maximal section. `section + 1` has no representable successor.
1657            let (offset, _) = journal.append(u64::MAX, &42i32).await.unwrap();
1658            let size = journal.size(u64::MAX).unwrap();
1659            assert!(size > 0);
1660
1661            // Rewinding the maximal section removes no sections above it and must not panic.
1662            journal.rewind(u64::MAX, size).await.unwrap();
1663
1664            // The section is intact and readable.
1665            assert_eq!(journal.size(u64::MAX).unwrap(), size);
1666            assert_eq!(journal.get(u64::MAX, offset).await.unwrap(), 42i32);
1667        });
1668    }
1669
1670    #[test_traced]
1671    fn test_journal_rewind_section() {
1672        // Initialize the deterministic context
1673        let executor = deterministic::Runner::default();
1674        executor.start(|context| async move {
1675            // Create journal
1676            let cfg = Config {
1677                partition: "test-partition".into(),
1678                compression: None,
1679                codec_config: (),
1680                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1681                write_buffer: NZUsize!(1024),
1682            };
1683            let mut journal = Journal::init(context, cfg).await.unwrap();
1684
1685            // Check size of non-existent section
1686            let size = journal.size(1).unwrap();
1687            assert_eq!(size, 0);
1688
1689            // Append data to section 1
1690            journal.append(1, &42i32).await.unwrap();
1691
1692            // Check size of section 1 - should be greater than 0
1693            let size = journal.size(1).unwrap();
1694            assert!(size > 0);
1695
1696            // Append more data and verify size increases
1697            journal.append(1, &43i32).await.unwrap();
1698            let new_size = journal.size(1).unwrap();
1699            assert!(new_size > size);
1700
1701            // Check size of different section - should still be 0
1702            let size = journal.size(2).unwrap();
1703            assert_eq!(size, 0);
1704
1705            // Append data to section 2
1706            journal.append(2, &44i32).await.unwrap();
1707
1708            // Check size of section 2 - should be greater than 0
1709            let size = journal.size(2).unwrap();
1710            assert!(size > 0);
1711
1712            // Rollback everything in section 1
1713            journal.rewind_section(1, 0).await.unwrap();
1714
1715            // Check size of section 1 - should be 0
1716            let size = journal.size(1).unwrap();
1717            assert_eq!(size, 0);
1718
1719            // Check size of section 2 - should be greater than 0
1720            let size = journal.size(2).unwrap();
1721            assert!(size > 0);
1722        });
1723    }
1724
1725    #[test_traced]
1726    fn test_journal_small_items() {
1727        let executor = deterministic::Runner::default();
1728        executor.start(|context| async move {
1729            let cfg = Config {
1730                partition: "test-partition".into(),
1731                compression: None,
1732                codec_config: (),
1733                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1734                write_buffer: NZUsize!(1024),
1735            };
1736
1737            let mut journal = Journal::init(context.child("first"), cfg.clone())
1738                .await
1739                .expect("Failed to initialize journal");
1740
1741            // Append many small (1-byte) items to the same section
1742            let num_items = 100;
1743            let mut offsets = Vec::new();
1744            for i in 0..num_items {
1745                let (offset, size) = journal
1746                    .append(1, &(i as u8))
1747                    .await
1748                    .expect("Failed to append data");
1749                assert_eq!(size, 1, "u8 should encode to 1 byte");
1750                offsets.push(offset);
1751            }
1752            journal.sync(1).await.expect("Failed to sync");
1753
1754            // Read each item back via random access
1755            for (i, &offset) in offsets.iter().enumerate() {
1756                let item: u8 = journal.get(1, offset).await.expect("Failed to get item");
1757                assert_eq!(item, i as u8, "Item mismatch at offset {offset}");
1758            }
1759
1760            // Drop and reopen to test replay
1761            drop(journal);
1762            let mut journal = Journal::<_, u8>::init(context.child("second"), cfg)
1763                .await
1764                .expect("Failed to re-initialize journal");
1765
1766            // Replay and verify all items
1767            let stream = journal
1768                .replay(0, 0, NZUsize!(1024))
1769                .await
1770                .expect("Failed to setup replay");
1771            pin_mut!(stream);
1772
1773            let mut count = 0;
1774            while let Some(result) = stream.next().await {
1775                let (section, offset, size, item) = result.expect("Failed to replay item");
1776                assert_eq!(section, 1);
1777                assert_eq!(offset, offsets[count]);
1778                assert_eq!(size, 1);
1779                assert_eq!(item, count as u8);
1780                count += 1;
1781            }
1782            assert_eq!(count, num_items, "Should replay all items");
1783        });
1784    }
1785
1786    #[test_traced]
1787    fn test_journal_rewind_many_sections() {
1788        let executor = deterministic::Runner::default();
1789        executor.start(|context| async move {
1790            let cfg = Config {
1791                partition: "test-partition".into(),
1792                compression: None,
1793                codec_config: (),
1794                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1795                write_buffer: NZUsize!(1024),
1796            };
1797            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1798                .await
1799                .unwrap();
1800
1801            // Create sections 1-10 with data
1802            for section in 1u64..=10 {
1803                journal.append(section, &(section as i32)).await.unwrap();
1804            }
1805            journal.sync_all().await.unwrap();
1806
1807            // Verify all sections exist
1808            for section in 1u64..=10 {
1809                let size = journal.size(section).unwrap();
1810                assert!(size > 0, "section {section} should have data");
1811            }
1812
1813            // Rewind to section 5 (should remove sections 6-10)
1814            journal.rewind(5, journal.size(5).unwrap()).await.unwrap();
1815
1816            // Verify sections 1-5 still exist with correct data
1817            for section in 1u64..=5 {
1818                let size = journal.size(section).unwrap();
1819                assert!(size > 0, "section {section} should still have data");
1820            }
1821
1822            // Verify sections 6-10 are removed (size should be 0)
1823            for section in 6u64..=10 {
1824                let size = journal.size(section).unwrap();
1825                assert_eq!(size, 0, "section {section} should be removed");
1826            }
1827
1828            // Verify data integrity via replay
1829            {
1830                let stream = journal.replay(0, 0, NZUsize!(1024)).await.unwrap();
1831                pin_mut!(stream);
1832                let mut items = Vec::new();
1833                while let Some(result) = stream.next().await {
1834                    let (section, _, _, item) = result.unwrap();
1835                    items.push((section, item));
1836                }
1837                assert_eq!(items.len(), 5);
1838                for (i, (section, item)) in items.iter().enumerate() {
1839                    assert_eq!(*section, (i + 1) as u64);
1840                    assert_eq!(*item, (i + 1) as i32);
1841                }
1842            }
1843
1844            journal.destroy().await.unwrap();
1845        });
1846    }
1847
1848    #[test_traced]
1849    fn test_journal_rewind_partial_truncation() {
1850        let executor = deterministic::Runner::default();
1851        executor.start(|context| async move {
1852            let cfg = Config {
1853                partition: "test-partition".into(),
1854                compression: None,
1855                codec_config: (),
1856                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1857                write_buffer: NZUsize!(1024),
1858            };
1859            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1860                .await
1861                .unwrap();
1862
1863            // Append 5 items and record sizes after each
1864            let mut sizes = Vec::new();
1865            for i in 0..5 {
1866                journal.append(1, &i).await.unwrap();
1867                journal.sync(1).await.unwrap();
1868                sizes.push(journal.size(1).unwrap());
1869            }
1870
1871            // Rewind to keep only first 3 items
1872            let target_size = sizes[2];
1873            journal.rewind(1, target_size).await.unwrap();
1874
1875            // Verify size is correct
1876            let new_size = journal.size(1).unwrap();
1877            assert_eq!(new_size, target_size);
1878
1879            // Verify first 3 items via replay
1880            {
1881                let stream = journal.replay(0, 0, NZUsize!(1024)).await.unwrap();
1882                pin_mut!(stream);
1883                let mut items = Vec::new();
1884                while let Some(result) = stream.next().await {
1885                    let (_, _, _, item) = result.unwrap();
1886                    items.push(item);
1887                }
1888                assert_eq!(items.len(), 3);
1889                for (i, item) in items.iter().enumerate() {
1890                    assert_eq!(*item, i as i32);
1891                }
1892            }
1893
1894            journal.destroy().await.unwrap();
1895        });
1896    }
1897
1898    #[test_traced]
1899    fn test_journal_rewind_nonexistent_target() {
1900        let executor = deterministic::Runner::default();
1901        executor.start(|context| async move {
1902            let cfg = Config {
1903                partition: "test-partition".into(),
1904                compression: None,
1905                codec_config: (),
1906                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1907                write_buffer: NZUsize!(1024),
1908            };
1909            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1910                .await
1911                .unwrap();
1912
1913            // Create sections 5, 6, 7 (skip 1-4)
1914            for section in 5u64..=7 {
1915                journal.append(section, &(section as i32)).await.unwrap();
1916            }
1917            journal.sync_all().await.unwrap();
1918
1919            // Rewind to section 3 (doesn't exist)
1920            journal.rewind(3, 0).await.unwrap();
1921
1922            // Verify sections 5, 6, 7 are removed
1923            for section in 5u64..=7 {
1924                let size = journal.size(section).unwrap();
1925                assert_eq!(size, 0, "section {section} should be removed");
1926            }
1927
1928            // Verify replay returns nothing
1929            {
1930                let stream = journal.replay(0, 0, NZUsize!(1024)).await.unwrap();
1931                pin_mut!(stream);
1932                let items: Vec<_> = stream.collect().await;
1933                assert!(items.is_empty());
1934            }
1935
1936            journal.destroy().await.unwrap();
1937        });
1938    }
1939
1940    #[test_traced]
1941    fn test_journal_rewind_persistence() {
1942        let executor = deterministic::Runner::default();
1943        executor.start(|context| async move {
1944            let cfg = Config {
1945                partition: "test-partition".into(),
1946                compression: None,
1947                codec_config: (),
1948                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1949                write_buffer: NZUsize!(1024),
1950            };
1951
1952            // Create sections 1-5 with data
1953            let mut journal = Journal::init(context.child("first"), cfg.clone())
1954                .await
1955                .unwrap();
1956            for section in 1u64..=5 {
1957                journal.append(section, &(section as i32)).await.unwrap();
1958            }
1959            journal.sync_all().await.unwrap();
1960
1961            // Rewind to section 2
1962            let size = journal.size(2).unwrap();
1963            journal.rewind(2, size).await.unwrap();
1964            journal.sync_all().await.unwrap();
1965            drop(journal);
1966
1967            // Re-init and verify only sections 1-2 exist
1968            let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
1969                .await
1970                .unwrap();
1971
1972            // Verify sections 1-2 have data
1973            for section in 1u64..=2 {
1974                let size = journal.size(section).unwrap();
1975                assert!(size > 0, "section {section} should have data after restart");
1976            }
1977
1978            // Verify sections 3-5 are gone
1979            for section in 3u64..=5 {
1980                let size = journal.size(section).unwrap();
1981                assert_eq!(size, 0, "section {section} should be gone after restart");
1982            }
1983
1984            // Verify data integrity via replay
1985            {
1986                let stream = journal.replay(0, 0, NZUsize!(1024)).await.unwrap();
1987                pin_mut!(stream);
1988                let mut items = Vec::new();
1989                while let Some(result) = stream.next().await {
1990                    let (section, _, _, item) = result.unwrap();
1991                    items.push((section, item));
1992                }
1993                assert_eq!(items.len(), 2);
1994                assert_eq!(items[0], (1, 1));
1995                assert_eq!(items[1], (2, 2));
1996            }
1997
1998            journal.destroy().await.unwrap();
1999        });
2000    }
2001
2002    #[test_traced]
2003    fn test_journal_rewind_to_zero_removes_all_newer() {
2004        let executor = deterministic::Runner::default();
2005        executor.start(|context| async move {
2006            let cfg = Config {
2007                partition: "test-partition".into(),
2008                compression: None,
2009                codec_config: (),
2010                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2011                write_buffer: NZUsize!(1024),
2012            };
2013            let mut journal = Journal::init(context.child("storage"), cfg.clone())
2014                .await
2015                .unwrap();
2016
2017            // Create sections 1, 2, 3
2018            for section in 1u64..=3 {
2019                journal.append(section, &(section as i32)).await.unwrap();
2020            }
2021            journal.sync_all().await.unwrap();
2022
2023            // Rewind section 1 to size 0
2024            journal.rewind(1, 0).await.unwrap();
2025
2026            // Verify section 1 exists but is empty
2027            let size = journal.size(1).unwrap();
2028            assert_eq!(size, 0, "section 1 should be empty");
2029
2030            // Verify sections 2, 3 are completely removed
2031            for section in 2u64..=3 {
2032                let size = journal.size(section).unwrap();
2033                assert_eq!(size, 0, "section {section} should be removed");
2034            }
2035
2036            // Verify replay returns nothing
2037            {
2038                let stream = journal.replay(0, 0, NZUsize!(1024)).await.unwrap();
2039                pin_mut!(stream);
2040                let items: Vec<_> = stream.collect().await;
2041                assert!(items.is_empty());
2042            }
2043
2044            journal.destroy().await.unwrap();
2045        });
2046    }
2047
2048    #[test_traced]
2049    fn test_journal_replay_start_offset_with_trailing_bytes() {
2050        // Regression: valid_offset must be initialized to start_offset, not 0.
2051        let executor = deterministic::Runner::default();
2052        executor.start(|context| async move {
2053            let cfg = Config {
2054                partition: "test-partition".into(),
2055                compression: None,
2056                codec_config: (),
2057                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2058                write_buffer: NZUsize!(1024),
2059            };
2060            let mut journal = Journal::init(context.child("first"), cfg.clone())
2061                .await
2062                .expect("Failed to initialize journal");
2063
2064            // Append several items to build up valid data
2065            for i in 0..5i32 {
2066                journal.append(1, &i).await.unwrap();
2067            }
2068            journal.sync(1).await.unwrap();
2069            let valid_logical_size = journal.size(1).unwrap();
2070            drop(journal);
2071
2072            // Get the physical blob size before corruption
2073            let (blob, physical_size_before) = context
2074                .open(&cfg.partition, &1u64.to_be_bytes())
2075                .await
2076                .unwrap();
2077
2078            // Write incomplete varint: 0xFF has continuation bit set, needs more bytes
2079            // This creates 2 trailing bytes that cannot form a valid item
2080            blob.write_at_sync(physical_size_before, vec![0xFF, 0xFF])
2081                .await
2082                .unwrap();
2083
2084            // Reopen journal and replay starting PAST all valid items
2085            // (start_offset = valid_logical_size means we skip all valid data)
2086            // The first thing encountered will be the trailing corrupt bytes
2087            let start_offset = valid_logical_size;
2088            {
2089                let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
2090                    .await
2091                    .unwrap();
2092
2093                let stream = journal
2094                    .replay(1, start_offset, NZUsize!(1024))
2095                    .await
2096                    .unwrap();
2097                pin_mut!(stream);
2098
2099                // Consume the stream - should detect trailing bytes and truncate
2100                while let Some(_result) = stream.next().await {}
2101            }
2102
2103            // Verify that valid data before start_offset was NOT lost
2104            let (_, physical_size_after) = context
2105                .open(&cfg.partition, &1u64.to_be_bytes())
2106                .await
2107                .unwrap();
2108
2109            // The blob should have been truncated back to the valid physical size
2110            // (removing the trailing corrupt bytes) but NOT to 0
2111            assert!(
2112                physical_size_after >= physical_size_before,
2113                "Valid data was lost! Physical blob truncated from {physical_size_before} to \
2114                 {physical_size_after}. Logical valid size was {valid_logical_size}. \
2115                 This indicates valid_offset was incorrectly initialized to 0 instead of start_offset."
2116            );
2117        });
2118    }
2119
2120    #[test_traced]
2121    fn test_journal_replay_rejects_start_offset_past_section() {
2122        let executor = deterministic::Runner::default();
2123        executor.start(|context| async move {
2124            let cfg = Config {
2125                partition: "test-partition".into(),
2126                compression: None,
2127                codec_config: (),
2128                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2129                write_buffer: NZUsize!(1024),
2130            };
2131            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2132            journal.append(1, &7i32).await.unwrap();
2133
2134            let result = journal.replay(1, u64::MAX, NZUsize!(1024)).await;
2135            assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
2136            drop(result);
2137
2138            journal.destroy().await.unwrap();
2139        });
2140    }
2141
2142    #[test_traced]
2143    fn test_journal_large_item_spanning_pages() {
2144        // 2048 bytes spans 2 full pages (PAGE_SIZE = 1024).
2145        const LARGE_SIZE: usize = 2048;
2146        type LargeItem = [u8; LARGE_SIZE];
2147
2148        let executor = deterministic::Runner::default();
2149        executor.start(|context| async move {
2150            let cfg = Config {
2151                partition: "test-partition".into(),
2152                compression: None,
2153                codec_config: (),
2154                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2155                write_buffer: NZUsize!(4096),
2156            };
2157            let mut journal = Journal::init(context.child("first"), cfg.clone())
2158                .await
2159                .expect("Failed to initialize journal");
2160
2161            // Create a large item that spans multiple pages.
2162            let mut large_data: LargeItem = [0u8; LARGE_SIZE];
2163            for (i, byte) in large_data.iter_mut().enumerate() {
2164                *byte = (i % 256) as u8;
2165            }
2166            assert!(
2167                LARGE_SIZE > PAGE_SIZE.get() as usize,
2168                "Item must be larger than page size"
2169            );
2170
2171            // Append the large item
2172            let (offset, size) = journal
2173                .append(1, &large_data)
2174                .await
2175                .expect("Failed to append large item");
2176            assert_eq!(size as usize, LARGE_SIZE);
2177            journal.sync(1).await.expect("Failed to sync");
2178
2179            // Read the item back via random access
2180            let retrieved: LargeItem = journal
2181                .get(1, offset)
2182                .await
2183                .expect("Failed to get large item");
2184            assert_eq!(retrieved, large_data, "Random access read mismatch");
2185
2186            // Drop and reopen to test replay
2187            drop(journal);
2188            let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
2189                .await
2190                .expect("Failed to re-initialize journal");
2191
2192            // Replay and verify the large item
2193            {
2194                let stream = journal
2195                    .replay(0, 0, NZUsize!(1024))
2196                    .await
2197                    .expect("Failed to setup replay");
2198                pin_mut!(stream);
2199
2200                let mut items = Vec::new();
2201                while let Some(result) = stream.next().await {
2202                    let (section, off, sz, item) = result.expect("Failed to replay item");
2203                    items.push((section, off, sz, item));
2204                }
2205
2206                assert_eq!(items.len(), 1, "Should have exactly one item");
2207                let (section, off, sz, item) = &items[0];
2208                assert_eq!(*section, 1);
2209                assert_eq!(*off, offset);
2210                assert_eq!(*sz as usize, LARGE_SIZE);
2211                assert_eq!(*item, large_data, "Replay read mismatch");
2212            }
2213
2214            journal.destroy().await.unwrap();
2215        });
2216    }
2217
2218    #[test_traced]
2219    fn test_journal_large_item_direct_path() {
2220        // Items larger than the write buffer are written directly to the blob. The first append
2221        // takes the direct path from an empty tip; the second takes it with a non-empty tip
2222        // (holding the first item's sub-page remainder), covering both top-up branches. The
2223        // returned offsets must remain correct since callers persist them for random access.
2224        const LARGE_SIZE: usize = 2048;
2225        type LargeItem = [u8; LARGE_SIZE];
2226
2227        let executor = deterministic::Runner::default();
2228        executor.start(|context| async move {
2229            let cfg = Config {
2230                partition: "test-partition".into(),
2231                compression: None,
2232                codec_config: (),
2233                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2234                write_buffer: NZUsize!(1024),
2235            };
2236            let mut journal = Journal::init(context.child("first"), cfg.clone())
2237                .await
2238                .expect("Failed to initialize journal");
2239
2240            let mut first: LargeItem = [0u8; LARGE_SIZE];
2241            for (i, byte) in first.iter_mut().enumerate() {
2242                *byte = (i % 256) as u8;
2243            }
2244            let mut second: LargeItem = [0u8; LARGE_SIZE];
2245            for (i, byte) in second.iter_mut().enumerate() {
2246                *byte = ((i + 7) % 251) as u8;
2247            }
2248
2249            let (first_offset, _) = journal
2250                .append(1, &first)
2251                .await
2252                .expect("Failed to append first item");
2253            let (second_offset, _) = journal
2254                .append(1, &second)
2255                .await
2256                .expect("Failed to append second item");
2257
2258            // Both items are readable at their returned offsets before any sync.
2259            let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
2260            assert_eq!(retrieved, first);
2261            let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
2262            assert_eq!(retrieved, second);
2263
2264            // Everything survives a sync and reopen.
2265            journal.sync(1).await.expect("Failed to sync");
2266            drop(journal);
2267            let mut journal = Journal::<_, LargeItem>::init(context.child("second"), cfg.clone())
2268                .await
2269                .expect("Failed to re-initialize journal");
2270
2271            let retrieved: LargeItem = journal.get(1, first_offset).await.unwrap();
2272            assert_eq!(retrieved, first);
2273            let retrieved: LargeItem = journal.get(1, second_offset).await.unwrap();
2274            assert_eq!(retrieved, second);
2275
2276            {
2277                let stream = journal
2278                    .replay(0, 0, NZUsize!(1024))
2279                    .await
2280                    .expect("Failed to setup replay");
2281                pin_mut!(stream);
2282
2283                let mut items = Vec::new();
2284                while let Some(result) = stream.next().await {
2285                    let (section, off, _, item) = result.expect("Failed to replay item");
2286                    items.push((section, off, item));
2287                }
2288                assert_eq!(items.len(), 2);
2289                assert_eq!(items[0], (1, first_offset, first));
2290                assert_eq!(items[1], (1, second_offset, second));
2291            }
2292
2293            journal.destroy().await.unwrap();
2294        });
2295    }
2296
2297    #[test_traced]
2298    fn test_journal_non_contiguous_sections() {
2299        // Test that sections with gaps in numbering work correctly.
2300        // Sections 1, 5, 10 should all be independent and accessible.
2301        let executor = deterministic::Runner::default();
2302        executor.start(|context| async move {
2303            let cfg = Config {
2304                partition: "test-partition".into(),
2305                compression: None,
2306                codec_config: (),
2307                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2308                write_buffer: NZUsize!(1024),
2309            };
2310            let mut journal = Journal::init(context.child("first"), cfg.clone())
2311                .await
2312                .expect("Failed to initialize journal");
2313
2314            // Create sections with gaps: 1, 5, 10
2315            let sections_and_data = [(1u64, 100i32), (5u64, 500i32), (10u64, 1000i32)];
2316            let mut offsets = Vec::new();
2317
2318            for (section, data) in &sections_and_data {
2319                let (offset, _) = journal
2320                    .append(*section, data)
2321                    .await
2322                    .expect("Failed to append");
2323                offsets.push(offset);
2324            }
2325            journal.sync_all().await.expect("Failed to sync");
2326
2327            // Verify random access to each section
2328            for (i, (section, expected_data)) in sections_and_data.iter().enumerate() {
2329                let retrieved: i32 = journal
2330                    .get(*section, offsets[i])
2331                    .await
2332                    .expect("Failed to get item");
2333                assert_eq!(retrieved, *expected_data);
2334            }
2335
2336            // Verify non-existent sections return appropriate errors
2337            for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
2338                let result = journal.get(missing_section, 0).await;
2339                assert!(
2340                    matches!(result, Err(Error::SectionOutOfRange(_))),
2341                    "Expected SectionOutOfRange for section {}, got {:?}",
2342                    missing_section,
2343                    result
2344                );
2345            }
2346
2347            // Drop and reopen to test replay
2348            drop(journal);
2349            let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
2350                .await
2351                .expect("Failed to re-initialize journal");
2352
2353            // Replay and verify all items in order
2354            {
2355                let stream = journal
2356                    .replay(0, 0, NZUsize!(1024))
2357                    .await
2358                    .expect("Failed to setup replay");
2359                pin_mut!(stream);
2360
2361                let mut items = Vec::new();
2362                while let Some(result) = stream.next().await {
2363                    let (section, _, _, item) = result.expect("Failed to replay item");
2364                    items.push((section, item));
2365                }
2366
2367                assert_eq!(items.len(), 3, "Should have 3 items");
2368                assert_eq!(items[0], (1, 100));
2369                assert_eq!(items[1], (5, 500));
2370                assert_eq!(items[2], (10, 1000));
2371            }
2372
2373            // Test replay starting from middle section (5)
2374            {
2375                let stream = journal
2376                    .replay(5, 0, NZUsize!(1024))
2377                    .await
2378                    .expect("Failed to setup replay from section 5");
2379                pin_mut!(stream);
2380
2381                let mut items = Vec::new();
2382                while let Some(result) = stream.next().await {
2383                    let (section, _, _, item) = result.expect("Failed to replay item");
2384                    items.push((section, item));
2385                }
2386
2387                assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
2388                assert_eq!(items[0], (5, 500));
2389                assert_eq!(items[1], (10, 1000));
2390            }
2391
2392            // Test replay starting from non-existent section (should skip to next)
2393            {
2394                let stream = journal
2395                    .replay(3, 0, NZUsize!(1024))
2396                    .await
2397                    .expect("Failed to setup replay from section 3");
2398                pin_mut!(stream);
2399
2400                let mut items = Vec::new();
2401                while let Some(result) = stream.next().await {
2402                    let (section, _, _, item) = result.expect("Failed to replay item");
2403                    items.push((section, item));
2404                }
2405
2406                // Should get sections 5 and 10 (skipping non-existent 3, 4)
2407                assert_eq!(items.len(), 2);
2408                assert_eq!(items[0], (5, 500));
2409                assert_eq!(items[1], (10, 1000));
2410            }
2411
2412            journal.destroy().await.unwrap();
2413        });
2414    }
2415
2416    #[test_traced]
2417    fn test_journal_empty_section_in_middle() {
2418        // Test that replay correctly handles an empty section between sections with data.
2419        // Section 1 has data, section 2 is empty, section 3 has data.
2420        let executor = deterministic::Runner::default();
2421        executor.start(|context| async move {
2422            let cfg = Config {
2423                partition: "test-partition".into(),
2424                compression: None,
2425                codec_config: (),
2426                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2427                write_buffer: NZUsize!(1024),
2428            };
2429            let mut journal = Journal::init(context.child("first"), cfg.clone())
2430                .await
2431                .expect("Failed to initialize journal");
2432
2433            // Append to section 1
2434            journal.append(1, &100i32).await.expect("Failed to append");
2435
2436            // Create section 2 but don't append anything - just sync to create the blob
2437            // Actually, we need to append something and then rewind to make it empty
2438            journal.append(2, &200i32).await.expect("Failed to append");
2439            journal.sync(2).await.expect("Failed to sync");
2440            journal
2441                .rewind_section(2, 0)
2442                .await
2443                .expect("Failed to rewind");
2444
2445            // Append to section 3
2446            journal.append(3, &300i32).await.expect("Failed to append");
2447
2448            journal.sync_all().await.expect("Failed to sync");
2449
2450            // Verify section sizes
2451            assert!(journal.size(1).unwrap() > 0);
2452            assert_eq!(journal.size(2).unwrap(), 0);
2453            assert!(journal.size(3).unwrap() > 0);
2454
2455            // Drop and reopen to test replay
2456            drop(journal);
2457            let mut journal = Journal::<_, i32>::init(context.child("second"), cfg.clone())
2458                .await
2459                .expect("Failed to re-initialize journal");
2460
2461            // Replay all - should get items from sections 1 and 3, skipping empty section 2
2462            {
2463                let stream = journal
2464                    .replay(0, 0, NZUsize!(1024))
2465                    .await
2466                    .expect("Failed to setup replay");
2467                pin_mut!(stream);
2468
2469                let mut items = Vec::new();
2470                while let Some(result) = stream.next().await {
2471                    let (section, _, _, item) = result.expect("Failed to replay item");
2472                    items.push((section, item));
2473                }
2474
2475                assert_eq!(
2476                    items.len(),
2477                    2,
2478                    "Should have 2 items (skipping empty section)"
2479                );
2480                assert_eq!(items[0], (1, 100));
2481                assert_eq!(items[1], (3, 300));
2482            }
2483
2484            // Replay starting from empty section 2 - should get only section 3
2485            {
2486                let stream = journal
2487                    .replay(2, 0, NZUsize!(1024))
2488                    .await
2489                    .expect("Failed to setup replay from section 2");
2490                pin_mut!(stream);
2491
2492                let mut items = Vec::new();
2493                while let Some(result) = stream.next().await {
2494                    let (section, _, _, item) = result.expect("Failed to replay item");
2495                    items.push((section, item));
2496                }
2497
2498                assert_eq!(items.len(), 1, "Should have 1 item from section 3");
2499                assert_eq!(items[0], (3, 300));
2500            }
2501
2502            journal.destroy().await.unwrap();
2503        });
2504    }
2505
2506    #[test_traced]
2507    fn test_journal_item_exactly_page_size() {
2508        // Test that items exactly equal to PAGE_SIZE work correctly.
2509        // This is a boundary condition where item fills exactly one page.
2510        const ITEM_SIZE: usize = PAGE_SIZE.get() as usize;
2511        type ExactItem = [u8; ITEM_SIZE];
2512
2513        let executor = deterministic::Runner::default();
2514        executor.start(|context| async move {
2515            let cfg = Config {
2516                partition: "test-partition".into(),
2517                compression: None,
2518                codec_config: (),
2519                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2520                write_buffer: NZUsize!(4096),
2521            };
2522            let mut journal = Journal::init(context.child("first"), cfg.clone())
2523                .await
2524                .expect("Failed to initialize journal");
2525
2526            // Create an item exactly PAGE_SIZE bytes
2527            let mut exact_data: ExactItem = [0u8; ITEM_SIZE];
2528            for (i, byte) in exact_data.iter_mut().enumerate() {
2529                *byte = (i % 256) as u8;
2530            }
2531
2532            // Append the exact-size item
2533            let (offset, size) = journal
2534                .append(1, &exact_data)
2535                .await
2536                .expect("Failed to append exact item");
2537            assert_eq!(size as usize, ITEM_SIZE);
2538            journal.sync(1).await.expect("Failed to sync");
2539
2540            // Read the item back via random access
2541            let retrieved: ExactItem = journal
2542                .get(1, offset)
2543                .await
2544                .expect("Failed to get exact item");
2545            assert_eq!(retrieved, exact_data, "Random access read mismatch");
2546
2547            // Drop and reopen to test replay
2548            drop(journal);
2549            let mut journal = Journal::<_, ExactItem>::init(context.child("second"), cfg.clone())
2550                .await
2551                .expect("Failed to re-initialize journal");
2552
2553            // Replay and verify
2554            {
2555                let stream = journal
2556                    .replay(0, 0, NZUsize!(1024))
2557                    .await
2558                    .expect("Failed to setup replay");
2559                pin_mut!(stream);
2560
2561                let mut items = Vec::new();
2562                while let Some(result) = stream.next().await {
2563                    let (section, off, sz, item) = result.expect("Failed to replay item");
2564                    items.push((section, off, sz, item));
2565                }
2566
2567                assert_eq!(items.len(), 1, "Should have exactly one item");
2568                let (section, off, sz, item) = &items[0];
2569                assert_eq!(*section, 1);
2570                assert_eq!(*off, offset);
2571                assert_eq!(*sz as usize, ITEM_SIZE);
2572                assert_eq!(*item, exact_data, "Replay read mismatch");
2573            }
2574
2575            journal.destroy().await.unwrap();
2576        });
2577    }
2578
2579    #[test_traced]
2580    fn test_journal_varint_spanning_page_boundary() {
2581        // Test that items with data spanning page boundaries work correctly
2582        // when using a small page size.
2583        //
2584        // With PAGE_SIZE=16:
2585        // - Physical page = 16 + 12 = 28 bytes
2586        // - Each [u8; 128] item = 2-byte varint + 128 bytes data = 130 bytes
2587        // - This spans multiple 16-byte pages, testing cross-page reading
2588        const SMALL_PAGE: NonZeroU16 = NZU16!(16);
2589
2590        let executor = deterministic::Runner::default();
2591        executor.start(|context| async move {
2592            let cfg = Config {
2593                partition: "test-partition".into(),
2594                compression: None,
2595                codec_config: (),
2596                page_cache: CacheRef::from_pooler(&context, SMALL_PAGE, PAGE_CACHE_SIZE),
2597                write_buffer: NZUsize!(1024),
2598            };
2599            let mut journal: Journal<_, [u8; 128]> =
2600                Journal::init(context.child("first"), cfg.clone())
2601                    .await
2602                    .expect("Failed to initialize journal");
2603
2604            // Create items that will span many 16-byte pages
2605            let item1: [u8; 128] = [1u8; 128];
2606            let item2: [u8; 128] = [2u8; 128];
2607            let item3: [u8; 128] = [3u8; 128];
2608
2609            // Append items - each is 130 bytes (2-byte varint + 128 data)
2610            // spanning ceil(130/16) = 9 pages worth of logical data
2611            let (offset1, _) = journal.append(1, &item1).await.expect("Failed to append");
2612            let (offset2, _) = journal.append(1, &item2).await.expect("Failed to append");
2613            let (offset3, _) = journal.append(1, &item3).await.expect("Failed to append");
2614
2615            journal.sync(1).await.expect("Failed to sync");
2616
2617            // Read items back via random access
2618            let retrieved1: [u8; 128] = journal.get(1, offset1).await.expect("Failed to get");
2619            let retrieved2: [u8; 128] = journal.get(1, offset2).await.expect("Failed to get");
2620            let retrieved3: [u8; 128] = journal.get(1, offset3).await.expect("Failed to get");
2621            assert_eq!(retrieved1, item1);
2622            assert_eq!(retrieved2, item2);
2623            assert_eq!(retrieved3, item3);
2624
2625            // Drop and reopen to test replay
2626            drop(journal);
2627            let mut journal: Journal<_, [u8; 128]> =
2628                Journal::init(context.child("second"), cfg.clone())
2629                    .await
2630                    .expect("Failed to re-initialize journal");
2631
2632            // Replay and verify all items
2633            {
2634                let stream = journal
2635                    .replay(0, 0, NZUsize!(64))
2636                    .await
2637                    .expect("Failed to setup replay");
2638                pin_mut!(stream);
2639
2640                let mut items = Vec::new();
2641                while let Some(result) = stream.next().await {
2642                    let (section, off, _, item) = result.expect("Failed to replay item");
2643                    items.push((section, off, item));
2644                }
2645
2646                assert_eq!(items.len(), 3, "Should have 3 items");
2647                assert_eq!(items[0], (1, offset1, item1));
2648                assert_eq!(items[1], (1, offset2, item2));
2649                assert_eq!(items[2], (1, offset3, item3));
2650            }
2651
2652            journal.destroy().await.unwrap();
2653        });
2654    }
2655
2656    #[test_traced]
2657    fn test_journal_clear() {
2658        let executor = deterministic::Runner::default();
2659        executor.start(|context| async move {
2660            let cfg = Config {
2661                partition: "clear-test".into(),
2662                compression: None,
2663                codec_config: (),
2664                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2665                write_buffer: NZUsize!(1024),
2666            };
2667
2668            let mut journal: Journal<_, u64> = Journal::init(context.child("journal"), cfg.clone())
2669                .await
2670                .expect("Failed to initialize journal");
2671
2672            // Append items across multiple sections
2673            for section in 0..5u64 {
2674                for i in 0..10u64 {
2675                    journal
2676                        .append(section, &(section * 1000 + i))
2677                        .await
2678                        .expect("Failed to append");
2679                }
2680                journal.sync(section).await.expect("Failed to sync");
2681            }
2682
2683            // Verify we have data
2684            assert_eq!(journal.get(0, 0).await.unwrap(), 0);
2685            assert_eq!(journal.get(4, 0).await.unwrap(), 4000);
2686
2687            // Clear the journal
2688            journal.clear().await.expect("Failed to clear");
2689
2690            // After clear, all reads should fail
2691            for section in 0..5u64 {
2692                assert!(matches!(
2693                    journal.get(section, 0).await,
2694                    Err(Error::SectionOutOfRange(s)) if s == section
2695                ));
2696            }
2697
2698            // Append new data after clear
2699            for i in 0..5u64 {
2700                journal
2701                    .append(10, &(i * 100))
2702                    .await
2703                    .expect("Failed to append after clear");
2704            }
2705            journal.sync(10).await.expect("Failed to sync after clear");
2706
2707            // New data should be readable
2708            assert_eq!(journal.get(10, 0).await.unwrap(), 0);
2709
2710            // Old sections should still be missing
2711            assert!(matches!(
2712                journal.get(0, 0).await,
2713                Err(Error::SectionOutOfRange(0))
2714            ));
2715
2716            journal.destroy().await.unwrap();
2717        });
2718    }
2719}