Skip to main content

commonware_storage/journal/segmented/
fixed.rs

1//! Segmented journal for fixed-size items.
2//!
3//! # Format
4//!
5//! Data is stored in one blob per section. Items are stored sequentially:
6//!
7//! ```text
8//! +--------+--------+--------+----------+
9//! | item_0 | item_1 |   ...  | item_n-1 |
10//! +--------+--------+--------+----------+
11//! ```
12//!
13//! # Sync
14//!
15//! Data written to `Journal` may not be immediately persisted to `Storage`. Use the
16//! `sync` method to force pending data to be written.
17//!
18//! # Pruning
19//!
20//! All data must be assigned to a `section`. This allows pruning entire sections
21//! (and their corresponding blobs) independently.
22
23use super::manager::{
24    AppendFactory, Config as ManagerConfig, Manager, section_from_name, stored_names,
25};
26use crate::journal::Error;
27use commonware_codec::{CodecFixed, CodecFixedShared, DecodeExt as _, ReadExt as _};
28use commonware_runtime::{
29    Blob, Error as RError, Handle, Metrics, ReadOptions, Storage,
30    buffer::paged::{CacheRef, Replay as BlobReplay, Writer},
31};
32use commonware_utils::NZUsize;
33use std::{
34    collections::{BTreeMap, BTreeSet, VecDeque},
35    marker::PhantomData,
36    num::{NonZeroU16, NonZeroUsize},
37};
38use tracing::{trace, warn};
39
40/// State for replaying a single section's blob.
41struct SectionReplay<B: Blob> {
42    section: u64,
43    reader: BlobReplay<B>,
44    position: u64,
45}
46
47/// Configuration for the fixed segmented journal.
48#[derive(Clone)]
49pub struct Config {
50    /// The partition to use for storing blobs.
51    pub partition: String,
52
53    /// The page cache to use for caching data.
54    pub page_cache: CacheRef,
55
56    /// The size of the write buffer to use for each blob.
57    pub write_buffer: NonZeroUsize,
58}
59
60/// The journal's state, boxed so the public [Journal] handle stays pointer-sized.
61struct Inner<E: Storage + Metrics, A: CodecFixed> {
62    manager: Manager<E, AppendFactory>,
63
64    /// Nonempty sections opened at initialization that have not been replayed from position zero.
65    unrecovered: BTreeSet<u64>,
66
67    /// Logical byte prefixes protected by durable validation markers.
68    floors: BTreeMap<u64, u64>,
69
70    _array: PhantomData<A>,
71}
72
73/// Recovery mutations authorized by a completed non-mutating preflight.
74enum PreflightMode {
75    /// Preserve each durable validation floor while replay repairs its suffix.
76    Floors(BTreeMap<u64, u64>),
77    /// Restore one checkpoint section and discard every later section.
78    Restore {
79        /// The checkpoint section remains explicit because an empty checkpoint has no floor.
80        section: u64,
81        floors: BTreeMap<u64, u64>,
82    },
83}
84
85/// Non-mutating recovery evidence and authorized work bound to the storage that produced it.
86pub(crate) struct RecoveryPreflight<E: Storage + Metrics, A: CodecFixed> {
87    context: E,
88    cfg: Config,
89
90    /// Terminal entry at each validated logical boundary.
91    boundaries: BTreeMap<u64, Option<A>>,
92
93    /// Mutations permitted after sibling storage validates these boundaries.
94    mode: PreflightMode,
95}
96
97impl<E: Storage + Metrics, A: CodecFixedShared> RecoveryPreflight<E, A> {
98    /// Terminal entries captured at each validated boundary.
99    pub(crate) const fn boundaries(&self) -> &BTreeMap<u64, Option<A>> {
100        &self.boundaries
101    }
102
103    /// Apply the preflighted recovery work and open the journal writer.
104    pub(crate) async fn finish(self) -> Result<Journal<E, A>, Error> {
105        Ok(Journal(Box::new(
106            Inner::init(self.context, self.cfg, Some(self.mode)).await?,
107        )))
108    }
109}
110
111impl<E: Storage + Metrics, A: CodecFixed> Inner<E, A> {
112    /// The section's writer. A replayed section cannot be removed while the replay owns the journal.
113    fn writer(&mut self, section: u64) -> &mut Writer<E::Blob> {
114        self.manager
115            .get_mut(section)
116            .expect("replayed section is present")
117    }
118}
119
120impl<E: Storage + Metrics, A: CodecFixedShared> Inner<E, A> {
121    /// Size of each entry.
122    const CHUNK_SIZE: usize = A::SIZE;
123    const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE as u64;
124
125    /// Return canonical section names in numeric order without opening writers.
126    async fn stored(context: &E, cfg: &Config) -> Result<BTreeMap<u64, Vec<u8>>, Error> {
127        // Recovery relies on numeric section order for checkpoint coverage and reverse deletion.
128        let mut stored = BTreeMap::new();
129        for name in stored_names(context, &cfg.partition).await? {
130            let section = section_from_name(&name)?;
131            stored.insert(section, name);
132        }
133        Ok(stored)
134    }
135
136    /// Prove each validation floor and capture its terminal entry without mutating storage.
137    async fn preflight_floors(
138        context: E,
139        cfg: Config,
140        minimum_items: &BTreeMap<u64, u64>,
141    ) -> Result<RecoveryPreflight<E, A>, Error> {
142        let stored = Self::stored(&context, &cfg).await?;
143        let page_size = cfg.page_cache.page_size();
144        let mut floor_sizes = BTreeMap::new();
145        let mut boundaries = BTreeMap::new();
146
147        for (&section, &items) in minimum_items {
148            // Every advertised floor must name a retained, representable byte prefix.
149            let Some(name) = stored.get(&section) else {
150                return Err(Error::Corruption(format!(
151                    "section {section} has a validation floor but no blob"
152                )));
153            };
154            let required = items.checked_mul(Self::CHUNK_SIZE_U64).ok_or_else(|| {
155                Error::Corruption(format!(
156                    "section {section} validation floor {items} overflows its byte size"
157                ))
158            })?;
159            if required == 0 {
160                boundaries.insert(section, None);
161                continue;
162            }
163
164            // A published marker follows a completed index/value sync, so its earlier pages are
165            // already durable. Validate the terminal index page that binds the two journals.
166            let (blob, _) = context.open(&cfg.partition, name).await?;
167            let entry = Self::boundary(&blob, page_size, section, required).await?;
168            floor_sizes.insert(section, required);
169            boundaries.insert(section, Some(entry));
170        }
171
172        Ok(RecoveryPreflight {
173            context,
174            cfg,
175            boundaries,
176            mode: PreflightMode::Floors(floor_sizes),
177        })
178    }
179
180    /// Prove every checkpoint-covered index boundary without mutating damaged storage.
181    async fn preflight_restore(
182        context: E,
183        cfg: Config,
184        section: u64,
185        size: u64,
186    ) -> Result<RecoveryPreflight<E, A>, Error> {
187        // The checkpoint can only identify a boundary between fixed-size items.
188        if !size.is_multiple_of(Self::CHUNK_SIZE_U64) {
189            return Err(Error::Corruption(format!(
190                "section {section} checkpoint size {size} is not item-aligned"
191            )));
192        }
193
194        // A non-empty current checkpoint requires its section blob to exist.
195        let stored = Self::stored(&context, &cfg).await?;
196        if size > 0 && !stored.contains_key(&section) {
197            return Err(Error::Corruption(format!(
198                "section {section} has a checkpoint but no blob"
199            )));
200        }
201
202        let page_size = cfg.page_cache.page_size();
203        let mut boundaries = BTreeMap::new();
204        let mut floors = BTreeMap::new();
205
206        for (&candidate, name) in stored.range(..=section) {
207            // Earlier sections end at their retained terminal page. The checkpoint supplies the
208            // current section's exact logical boundary, which may precede an uncommitted suffix.
209            let (blob, physical_size) = context.open(&cfg.partition, name).await?;
210            let entry = if candidate == section {
211                if size == 0 {
212                    None
213                } else {
214                    let entry = Self::boundary(&blob, page_size, section, size).await?;
215                    floors.insert(section, size);
216                    Some(entry)
217                }
218            } else if physical_size == 0 {
219                None
220            } else {
221                let (logical_size, entry) = Writer::<E::Blob>::read_tail(
222                    &blob,
223                    physical_size,
224                    page_size,
225                    Self::CHUNK_SIZE,
226                    ReadOptions::default(),
227                )
228                .await
229                .map_err(|err| Self::boundary_error(candidate, physical_size, err))?;
230                if !logical_size.is_multiple_of(Self::CHUNK_SIZE_U64) {
231                    return Err(Error::Corruption(format!(
232                        "section {candidate} is not a complete checkpoint-covered index"
233                    )));
234                }
235                floors.insert(candidate, logical_size);
236                Some(A::decode(entry.coalesce()).map_err(Error::Codec)?)
237            };
238            boundaries.insert(candidate, entry);
239        }
240        boundaries.entry(section).or_insert(None);
241        Ok(RecoveryPreflight {
242            context,
243            cfg,
244            boundaries,
245            mode: PreflightMode::Restore { section, floors },
246        })
247    }
248
249    /// Read and decode the terminal entry of a validated `size`-byte prefix.
250    async fn boundary(
251        blob: &E::Blob,
252        page_size: NonZeroU16,
253        section: u64,
254        size: u64,
255    ) -> Result<A, Error> {
256        let entry = Writer::<E::Blob>::read_range(
257            blob,
258            page_size,
259            size - Self::CHUNK_SIZE_U64,
260            Self::CHUNK_SIZE,
261            ReadOptions::default(),
262        )
263        .await
264        .map_err(|err| Self::boundary_error(section, size, err))?;
265        A::decode(entry.coalesce()).map_err(Error::Codec)
266    }
267
268    /// Classify an invalid or missing boundary as committed corruption without hiding I/O errors.
269    ///
270    /// `searched` is the extent the boundary read covered: the current section's logical
271    /// boundary size, or an earlier section's physical blob size.
272    fn boundary_error(section: u64, searched: u64, err: RError) -> Error {
273        match err {
274            RError::InvalidChecksum | RError::BlobInsufficientLength => Error::Corruption(format!(
275                "section {section} does not retain a valid durable boundary within {searched} bytes"
276            )),
277            err => err.into(),
278        }
279    }
280
281    /// See [Journal::init].
282    async fn init(context: E, cfg: Config, mode: Option<PreflightMode>) -> Result<Self, Error> {
283        let (floors, restore) = match mode {
284            None => (BTreeMap::new(), None),
285            Some(PreflightMode::Floors(floors)) => (floors, None),
286            Some(PreflightMode::Restore { section, floors }) => {
287                let size = floors.get(&section).copied().unwrap_or(0);
288                (floors, Some((section, size)))
289            }
290        };
291
292        let manager_cfg = ManagerConfig {
293            partition: cfg.partition,
294            factory: AppendFactory {
295                write_buffer: cfg.write_buffer,
296                page_cache_ref: cfg.page_cache,
297            },
298        };
299        let mut manager = Manager::init(context, manager_cfg).await?;
300        if let Some((section, size)) = restore {
301            // The checkpoint preflight authorized this exact truncation. Make it durable before
302            // the paired value journal can release any corresponding bytes.
303            manager.rewind(section, size).await?;
304            manager.sync(section).await?;
305            return Ok(Self {
306                manager,
307                unrecovered: BTreeSet::new(),
308                floors,
309                _array: PhantomData,
310            });
311        }
312
313        let mut unrecovered = BTreeSet::new();
314        for section in manager.sections() {
315            let size = manager.size(section)?;
316            let floor = floors.get(&section).copied().unwrap_or(0);
317            if size < floor {
318                return Err(Error::Corruption(format!(
319                    "section {section} retains {size} of its {floor}-byte validation floor"
320                )));
321            }
322            if size > floor {
323                unrecovered.insert(section);
324            }
325        }
326
327        Ok(Self {
328            manager,
329            unrecovered,
330            floors,
331            _array: PhantomData,
332        })
333    }
334
335    /// See [Journal::append].
336    async fn append(&mut self, section: u64, item: &A) -> Result<u64, Error> {
337        assert!(
338            !self.unrecovered.contains(&section),
339            "section {section} must be replayed before append"
340        );
341        let blob = self.manager.get_or_create(section).await?;
342
343        // Encode the item
344        let buf = item.encode_mut();
345        let offset = blob.append(&buf).await?;
346        if !offset.is_multiple_of(Self::CHUNK_SIZE_U64) {
347            return Err(Error::InvalidBlobSize(section, offset));
348        }
349        let position = offset / Self::CHUNK_SIZE_U64;
350        trace!(section, position, "appended item");
351
352        Ok(position)
353    }
354
355    /// See [Journal::get].
356    async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
357        let blob = self
358            .manager
359            .get(section)?
360            .ok_or(Error::SectionOutOfRange(section))?;
361
362        let offset = position
363            .checked_mul(Self::CHUNK_SIZE_U64)
364            .ok_or(Error::ItemOutOfRange(position))?;
365
366        // The read validates bounds against the blob's logical size.
367        let buf = blob
368            .read_at(offset, Self::CHUNK_SIZE)
369            .await
370            .map_err(|err| match err {
371                commonware_runtime::Error::BlobInsufficientLength
372                | commonware_runtime::Error::OffsetOverflow => Error::ItemOutOfRange(position),
373                err => Error::Runtime(err),
374            })?;
375        A::decode(buf.coalesce()).map_err(Error::Codec)
376    }
377
378    /// See [Journal::get_many].
379    async fn get_many(
380        &self,
381        section: u64,
382        positions: &[u64],
383        buf: &mut [u8],
384    ) -> Result<(Vec<A>, usize), Error> {
385        assert!(
386            positions.is_sorted_by(|a, b| a < b),
387            "positions must be strictly increasing"
388        );
389        if positions.is_empty() {
390            return Ok((Vec::new(), 0));
391        }
392        assert!(
393            buf.len() >= positions.len() * Self::CHUNK_SIZE,
394            "get_many requires buf.len() >= positions.len() * CHUNK_SIZE"
395        );
396        let buf = &mut buf[..positions.len() * Self::CHUNK_SIZE];
397        let blob = self
398            .manager
399            .get(section)?
400            .ok_or(Error::SectionOutOfRange(section))?;
401
402        let offsets: Vec<u64> = positions
403            .iter()
404            .map(|&p| {
405                p.checked_mul(Self::CHUNK_SIZE_U64)
406                    .ok_or(Error::ItemOutOfRange(p))
407            })
408            .collect::<Result<_, _>>()?;
409
410        let hits = blob
411            .read_many_into(buf, &offsets, NZUsize!(Self::CHUNK_SIZE))
412            .await?;
413
414        let mut items = Vec::with_capacity(positions.len());
415        for i in 0..positions.len() {
416            let slice = &buf[i * Self::CHUNK_SIZE..(i + 1) * Self::CHUNK_SIZE];
417            items.push(A::decode(slice).map_err(Error::Codec)?);
418        }
419        Ok((items, hits))
420    }
421
422    /// See [Journal::try_get_sync].
423    fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
424        let blob = self.manager.get(section).ok()??;
425        let offset = position.checked_mul(Self::CHUNK_SIZE_U64)?;
426        let remaining = blob.size().checked_sub(offset)?;
427        if remaining < Self::CHUNK_SIZE_U64 {
428            return None;
429        }
430        let mut buf = vec![0u8; Self::CHUNK_SIZE];
431        if !blob.try_read_sync_into(&mut buf, offset) {
432            return None;
433        }
434        A::decode(&buf[..]).ok()
435    }
436
437    /// See [Journal::last].
438    async fn last(&self, section: u64) -> Result<Option<A>, Error> {
439        let blob = self
440            .manager
441            .get(section)?
442            .ok_or(Error::SectionOutOfRange(section))?;
443
444        let size = blob.size();
445        if size < Self::CHUNK_SIZE_U64 {
446            return Ok(None);
447        }
448
449        let last_position = (size / Self::CHUNK_SIZE_U64) - 1;
450        let offset = last_position * Self::CHUNK_SIZE_U64;
451        let buf = blob.read_at(offset, Self::CHUNK_SIZE).await?;
452        A::decode(buf.coalesce()).map_err(Error::Codec).map(Some)
453    }
454
455    /// See [Journal::sync].
456    async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
457        self.manager.sync(sections).await
458    }
459
460    /// See [Journal::start_sync].
461    async fn start_sync(&mut self, sections: impl crate::Sections) -> Result<Handle<()>, Error> {
462        self.manager.start_sync(sections).await
463    }
464
465    /// See [Journal::sync_all].
466    async fn sync_all(&mut self) -> Result<(), Error> {
467        self.manager.sync_all().await
468    }
469
470    /// See [Journal::prune].
471    async fn prune(&mut self, min: u64) -> Result<bool, Error> {
472        let pruned = self.manager.prune(min).await?;
473        if pruned {
474            self.unrecovered.retain(|section| *section >= min);
475            self.floors.retain(|section, _| *section >= min);
476        }
477        Ok(pruned)
478    }
479
480    /// See [Journal::pruned].
481    const fn pruned(&self, section: u64) -> bool {
482        self.manager.pruned(section)
483    }
484
485    /// See [Journal::oldest_section].
486    fn oldest_section(&self) -> Option<u64> {
487        self.manager.oldest_section()
488    }
489
490    /// See [Journal::newest_section].
491    fn newest_section(&self) -> Option<u64> {
492        self.manager.newest_section()
493    }
494
495    /// See [Journal::sections].
496    fn sections(&self) -> impl Iterator<Item = u64> + '_ {
497        self.manager.sections()
498    }
499
500    /// See [Journal::section_len].
501    fn section_len(&self, section: u64) -> Result<u64, Error> {
502        let size = self.manager.size(section)?;
503        Ok(size / Self::CHUNK_SIZE_U64)
504    }
505
506    /// See [Journal::size].
507    fn size(&self, section: u64) -> Result<u64, Error> {
508        self.manager.size(section)
509    }
510
511    /// See [Journal::rewind].
512    async fn rewind(&mut self, section: u64, offset: u64) -> Result<(), Error> {
513        self.manager.rewind(section, offset).await?;
514        self.unrecovered.retain(|candidate| *candidate <= section);
515        self.floors.retain(|candidate, _| *candidate <= section);
516        if offset == 0 {
517            self.unrecovered.remove(&section);
518        }
519        if let Some(floor) = self.floors.get_mut(&section) {
520            *floor = (*floor).min(offset);
521        }
522        Ok(())
523    }
524
525    /// See [Journal::rewind_section].
526    async fn rewind_section(&mut self, section: u64, size: u64) -> Result<(), Error> {
527        self.manager.rewind_section(section, size).await?;
528        if size == 0 {
529            self.unrecovered.remove(&section);
530        }
531        if let Some(floor) = self.floors.get_mut(&section) {
532            *floor = (*floor).min(size);
533        }
534        Ok(())
535    }
536
537    /// See [Journal::destroy].
538    async fn destroy(self) -> Result<(), Error> {
539        self.manager.destroy().await
540    }
541
542    /// See [Journal::clear].
543    async fn clear(&mut self) -> Result<(), Error> {
544        self.manager.clear().await?;
545        self.unrecovered.clear();
546        self.floors.clear();
547        Ok(())
548    }
549}
550
551/// A segmented journal with fixed-size entries.
552///
553/// Each section is stored in a separate blob. Within each blob, items are fixed-size.
554///
555/// # Repair
556///
557/// Like
558/// [sqlite](https://github.com/sqlite/sqlite/blob/8658a8df59f00ec8fcfea336a2a6a4b5ef79d2ee/src/wal.c#L1504-L1505)
559/// and
560/// [rocksdb](https://github.com/facebook/rocksdb/blob/0c533e61bc6d89fdf1295e8e0bcee4edb3aef401/include/rocksdb/options.h#L441-L445),
561/// the first invalid data read will be considered the new end of the journal (and the
562/// underlying [Blob] will be truncated to the last valid item). Repair occurs during
563/// replay so clean initialization reads only each blob's terminal page. A nonempty section opened
564/// during initialization must be replayed from position zero before it accepts new appends.
565///
566/// Mutating functions consume the journal and return it only on success: an error (or a dropped
567/// future) destroys the handle. [Journal::replay] consumes the journal into an owned [Replay]
568/// reader, which returns it via [Replay::finish] once exhausted. Mutations on pruned sections
569/// fail with [Error::AlreadyPrunedToSection] without mutating. Check [Journal::pruned] first to
570/// keep the handle.
571pub struct Journal<E: Storage + Metrics, A: CodecFixed>(Box<Inner<E, A>>);
572
573impl<E: Storage + Metrics, A: CodecFixedShared> std::fmt::Debug for Journal<E, A> {
574    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575        f.debug_struct("Journal")
576            .field("oldest_section", &self.oldest_section())
577            .field("newest_section", &self.newest_section())
578            .finish_non_exhaustive()
579    }
580}
581
582impl<E: Storage + Metrics, A: CodecFixedShared> Journal<E, A> {
583    /// Size of each entry.
584    pub const CHUNK_SIZE: usize = Inner::<E, A>::CHUNK_SIZE;
585
586    /// Initialize a new `Journal` instance.
587    ///
588    /// Backing blobs are opened without scanning their full page prefixes. Use `replay` to validate
589    /// and iterate over all items before appending to a retained section.
590    pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
591        Ok(Self(Box::new(Inner::init(context, cfg, None).await?)))
592    }
593
594    /// Prove validation floors without opening a writer or mutating their blobs.
595    pub(crate) async fn preflight_floors(
596        context: E,
597        cfg: Config,
598        minimum_items: &BTreeMap<u64, u64>,
599    ) -> Result<RecoveryPreflight<E, A>, Error> {
600        Inner::preflight_floors(context, cfg, minimum_items).await
601    }
602
603    /// Prove every checkpoint-covered index byte without mutating damage.
604    pub(crate) async fn preflight_restore(
605        context: E,
606        cfg: Config,
607        section: u64,
608        size: u64,
609    ) -> Result<RecoveryPreflight<E, A>, Error> {
610        Inner::preflight_restore(context, cfg, section, size).await
611    }
612
613    /// Append a new item to the journal in the given section.
614    ///
615    /// Returns the position of the item within the section (0-indexed).
616    ///
617    /// # Panics
618    ///
619    /// Panics when `section` contained an unvalidated suffix at initialization and has not
620    /// completed a replay from position zero.
621    pub async fn append(mut self, section: u64, item: &A) -> Result<(Self, u64), Error> {
622        let position = self.0.append(section, item).await?;
623        Ok((self, position))
624    }
625
626    /// Read the item at the given section and position.
627    ///
628    /// # Errors
629    ///
630    /// - [Error::AlreadyPrunedToSection] if the section has been pruned.
631    /// - [Error::SectionOutOfRange] if the section doesn't exist.
632    /// - [Error::ItemOutOfRange] if the position is beyond the blob size.
633    pub async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
634        self.0.get(section, position).await
635    }
636
637    /// Read multiple items from the same section into a caller buffer.
638    ///
639    /// `buf` must be at least `positions.len() * CHUNK_SIZE` bytes. All positions must be
640    /// strictly increasing and within the section's bounds.
641    ///
642    /// Returns the decoded items and the number served without a blob read (page cache or tip
643    /// buffer hits).
644    pub async fn get_many(
645        &self,
646        section: u64,
647        positions: &[u64],
648        buf: &mut [u8],
649    ) -> Result<(Vec<A>, usize), Error> {
650        self.0.get_many(section, positions, buf).await
651    }
652
653    /// Get an item if it can be done synchronously (e.g. without I/O), returning `None` otherwise.
654    pub fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
655        self.0.try_get_sync(section, position)
656    }
657
658    /// Read the last item in a section, if any.
659    ///
660    /// Returns `Ok(None)` if the section is empty.
661    ///
662    /// # Errors
663    ///
664    /// - [Error::AlreadyPrunedToSection] if the section has been pruned.
665    /// - [Error::SectionOutOfRange] if the section doesn't exist.
666    pub async fn last(&self, section: u64) -> Result<Option<A>, Error> {
667        self.0.last(section).await
668    }
669
670    /// Consumes the journal and returns an owned [Replay] reader over all items starting
671    /// from `start_position` in `start_section`.
672    ///
673    /// Setup flushes buffered pages so the reader observes every accepted write. It
674    /// validates replay setup but does not allocate `buffer` bytes per blob. Page buffers
675    /// are allocated lazily as the reader advances. Every backing blob read performed by
676    /// the returned replay uses `read_options`, including reads after advancing to
677    /// another section.
678    ///
679    /// A nonzero start must be a boundary already validated by a prior replay or a durable
680    /// marker: torn-page repair treats everything below it as proven.
681    pub async fn replay(
682        mut self,
683        start_section: u64,
684        start_position: u64,
685        buffer: NonZeroUsize,
686        read_options: ReadOptions,
687    ) -> Result<Replay<E, A>, Error> {
688        let mut sections = VecDeque::new();
689        for (&section, blob) in self.0.manager.sections_from(start_section) {
690            let blob_size = blob.size();
691            let mut reader = blob.replay(buffer, read_options).await?;
692            // For the first section, seek to the start position
693            let position = if section == start_section {
694                let start = start_position
695                    .checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
696                    .ok_or(Error::ItemOutOfRange(start_position))?;
697                if start > blob_size {
698                    return Err(Error::ItemOutOfRange(start_position));
699                }
700                reader.seek_to(start)?;
701                start_position
702            } else {
703                0
704            };
705            sections.push_back(SectionReplay {
706                section,
707                reader,
708                position,
709            });
710        }
711        let finished = sections.is_empty();
712        Ok(Replay {
713            journal: self,
714            sections,
715            recovered_from: if start_position == 0 {
716                Some(start_section)
717            } else {
718                start_section.checked_add(1)
719            },
720            buffer,
721            read_options,
722            finished,
723            errored: false,
724            repairing: false,
725        })
726    }
727
728    /// Sync the given `sections` to storage.
729    pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
730        self.0.sync(sections).await?;
731        Ok(self)
732    }
733
734    /// Start syncing the given `sections` to storage.
735    ///
736    /// An error reported by the returned [Handle] is fatal to the journal: the caller
737    /// must stop using the returned journal.
738    pub async fn start_sync(
739        mut self,
740        sections: impl crate::Sections,
741    ) -> Result<(Self, Handle<()>), Error> {
742        let handle = self.0.start_sync(sections).await?;
743        Ok((self, handle))
744    }
745
746    /// Sync all sections to storage.
747    pub async fn sync_all(mut self) -> Result<Self, Error> {
748        self.0.sync_all().await?;
749        Ok(self)
750    }
751
752    /// Prune all sections less than `min`. Returns true if any were pruned.
753    pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
754        let pruned = self.0.prune(min).await?;
755        Ok((self, pruned))
756    }
757
758    /// Returns true when `section` is below the prune floor.
759    ///
760    /// The floor only tracks prunes from the current execution and resets at init, so a
761    /// section pruned in a previous execution reports false.
762    pub fn pruned(&self, section: u64) -> bool {
763        self.0.pruned(section)
764    }
765
766    /// Returns the oldest section number, if any blobs exist.
767    pub fn oldest_section(&self) -> Option<u64> {
768        self.0.oldest_section()
769    }
770
771    /// Returns the newest section number, if any blobs exist.
772    pub fn newest_section(&self) -> Option<u64> {
773        self.0.newest_section()
774    }
775
776    /// Returns an iterator over all section numbers.
777    pub fn sections(&self) -> impl Iterator<Item = u64> + '_ {
778        self.0.sections()
779    }
780
781    /// Returns the number of items in the given section.
782    pub fn section_len(&self, section: u64) -> Result<u64, Error> {
783        self.0.section_len(section)
784    }
785
786    /// Returns the byte size of the given section.
787    pub fn size(&self, section: u64) -> Result<u64, Error> {
788        self.0.size(section)
789    }
790
791    /// Rewind the journal to a specific section and byte size.
792    ///
793    /// This truncates the section to the given size. All sections
794    /// after `section` are removed.
795    pub async fn rewind(mut self, section: u64, size: u64) -> Result<Self, Error> {
796        self.0.rewind(section, size).await?;
797        Ok(self)
798    }
799
800    /// Rewind only the given section to a specific byte offset.
801    ///
802    /// Unlike `rewind`, this does not affect other sections.
803    pub async fn rewind_section(mut self, section: u64, size: u64) -> Result<Self, Error> {
804        self.0.rewind_section(section, size).await?;
805        Ok(self)
806    }
807
808    /// Remove all underlying blobs.
809    pub async fn destroy(self) -> Result<(), Error> {
810        self.0.destroy().await
811    }
812
813    /// Clear all data, resetting the journal to an empty state.
814    ///
815    /// Unlike `destroy`, this keeps the journal alive so it can be reused.
816    pub async fn clear(mut self) -> Result<Self, Error> {
817        self.0.clear().await?;
818        Ok(self)
819    }
820}
821
822/// Owned replay reader over a [Journal]'s items.
823///
824/// Yields `(section, position, item)` in order. Dropping the reader before it is exhausted
825/// destroys the journal: recovery is re-initialization. Call [Replay::finish] on an
826/// exhausted reader to get the journal back.
827pub struct Replay<E: Storage + Metrics, A: CodecFixed> {
828    journal: Journal<E, A>,
829    sections: VecDeque<SectionReplay<E::Blob>>,
830    /// The first section this replay fully covers: [Replay::finish] marks it and every
831    /// later section recovered.
832    recovered_from: Option<u64>,
833    buffer: NonZeroUsize,
834    read_options: ReadOptions,
835    finished: bool,
836    errored: bool,
837    repairing: bool,
838}
839
840impl<E: Storage + Metrics, A: CodecFixedShared> Replay<E, A> {
841    /// Validate that the front section's checksum failure is a repairable torn page, returning
842    /// the item-aligned truncation target and the validated replay prefix.
843    async fn plan_repair(&mut self, source: RError) -> Result<(u64, u64), Error> {
844        // Only a checksum failure is repairable: it marks a torn write, while any other error
845        // is an I/O failure this repair must not mask.
846        if !matches!(source, RError::InvalidChecksum) {
847            return Err(source.into());
848        }
849
850        // The bytes already replayed are validated: they bound the truncation from below.
851        let current = self.sections.front().expect("replayed section is present");
852        let section = current.section;
853        let position = current.position;
854        let size = current.reader.blob_size();
855        let valid_size = position
856            .checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
857            .ok_or(Error::OffsetOverflow)?;
858
859        // Forward-validate from the replayed prefix to find where well-formed pages end.
860        let recoverable = self
861            .journal
862            .0
863            .writer(section)
864            .recoverable_prefix_len(valid_size, self.buffer, self.read_options)
865            .await?;
866
867        // A whole-blob recoverable prefix means the checksum failure did not come from a torn
868        // page: surface the original error instead of truncating valid data.
869        if recoverable >= size {
870            return Err(source.into());
871        }
872
873        // Truncate to whole items, never below the validated replay prefix or the durability
874        // floor: a cut inside either lost acknowledged data.
875        let target = recoverable - recoverable % Inner::<E, A>::CHUNK_SIZE_U64;
876        if target < valid_size {
877            return Err(Error::ItemOutOfRange(position));
878        }
879        self.ensure_above_floor(section, target)?;
880
881        Ok((valid_size, target))
882    }
883
884    /// Repair a torn page discovered by ordered replay and resume at the last complete item.
885    async fn repair(&mut self, source: RError) -> Result<(), Error> {
886        // A rejected plan mutates nothing: drop the damaged section and surface its error.
887        let (valid_size, target) = match self.plan_repair(source).await {
888            Ok(plan) => plan,
889            Err(err) => {
890                self.sections.pop_front();
891                return Err(err);
892            }
893        };
894
895        let current = self.sections.front().expect("replayed section is present");
896        let (section, position) = (current.section, current.position);
897        warn!(
898            section,
899            invalid_size = current.reader.blob_size(),
900            new_size = target,
901            "torn page detected: truncating"
902        );
903
904        // Keep the interruption guard set until a new reader has replaced the stale view.
905        self.repairing = true;
906        let current = self
907            .sections
908            .pop_front()
909            .expect("repaired section is present");
910        drop(current.reader);
911        repair_blob(&mut self.journal, section, target).await?;
912        let mut reader = self
913            .journal
914            .0
915            .writer(section)
916            .replay(self.buffer, self.read_options)
917            .await?;
918        reader.seek_to(valid_size)?;
919        self.sections.push_front(SectionReplay {
920            section,
921            reader,
922            position,
923        });
924        self.repairing = false;
925        Ok(())
926    }
927
928    /// Reject a repair that would remove any marker-protected item.
929    fn ensure_above_floor(&self, section: u64, target: u64) -> Result<(), Error> {
930        let floor = self.journal.0.floors.get(&section).copied().unwrap_or(0);
931        if target < floor {
932            return Err(Error::Corruption(format!(
933                "section {section} recovery target {target} is below its {floor}-byte validation floor"
934            )));
935        }
936        Ok(())
937    }
938
939    /// Returns the next `(section, position, item)`, or `None` once every section is
940    /// exhausted.
941    ///
942    /// An error ends the section that produced it, and iteration continues with the
943    /// next section. Errors while mutating storage to repair a section, and
944    /// [Error::ReplayInterrupted], end the replay.
945    pub async fn next(&mut self) -> Option<Result<(u64, u64, A), Error>> {
946        // A cancelled repair leaves the section's writer unusable.
947        if self.repairing {
948            self.repairing = false;
949            self.sections.clear();
950            if !self.errored {
951                return self.fail(Error::ReplayInterrupted);
952            }
953        }
954        while let Some(current) = self.sections.front_mut() {
955            // Ensure we have enough data for one item
956            match current.reader.ensure(Inner::<E, A>::CHUNK_SIZE).await {
957                Ok(true) => {}
958                Ok(false) => {
959                    let valid_size =
960                        match current.position.checked_mul(Inner::<E, A>::CHUNK_SIZE_U64) {
961                            Some(size) => size,
962                            None => return self.fail(Error::OffsetOverflow),
963                        };
964                    let blob_size = current.reader.blob_size();
965                    if valid_size < blob_size {
966                        let section = current.section;
967                        if let Err(err) = self.ensure_above_floor(section, valid_size) {
968                            self.sections.pop_front();
969                            return self.fail(err);
970                        }
971                        warn!(
972                            section,
973                            invalid_size = blob_size,
974                            new_size = valid_size,
975                            "incomplete item detected: truncating"
976                        );
977                        self.repairing = true;
978                        if let Err(err) = repair_blob(&mut self.journal, section, valid_size).await
979                        {
980                            self.sections.pop_front();
981                            return self.fail(err);
982                        }
983                        self.repairing = false;
984                    }
985                    self.sections.pop_front();
986                    continue;
987                }
988                Err(err) => {
989                    if let Err(err) = self.repair(err).await {
990                        return self.fail(err);
991                    }
992                    continue;
993                }
994            }
995
996            // Decode the item at the current position
997            match A::read(&mut current.reader) {
998                Ok(item) => {
999                    let yielded = (current.section, current.position, item);
1000                    current.position += 1;
1001                    return Some(Ok(yielded));
1002                }
1003                Err(err) => {
1004                    self.sections.pop_front();
1005                    return self.fail(Error::Codec(err));
1006                }
1007            }
1008        }
1009        self.finished = true;
1010        None
1011    }
1012
1013    /// Records a yielded error, which is fatal to the journal.
1014    const fn fail(&mut self, err: Error) -> Option<Result<(u64, u64, A), Error>> {
1015        self.errored = true;
1016        Some(Err(err))
1017    }
1018
1019    /// Returns the journal.
1020    ///
1021    /// Fails when the reader was not fully drained or yielded an error: the journal is
1022    /// destroyed and recovery is re-initialization.
1023    pub fn finish(mut self) -> Result<Journal<E, A>, Error> {
1024        if self.errored || !self.finished {
1025            return Err(Error::ReplayFailed);
1026        }
1027        if let Some(start) = self.recovered_from {
1028            self.journal
1029                .0
1030                .unrecovered
1031                .retain(|section| *section < start);
1032        }
1033        Ok(self.journal)
1034    }
1035}
1036
1037/// Truncate a replayed section and make the repair durable before allowing new appends.
1038async fn repair_blob<E: Storage + Metrics, A: CodecFixed>(
1039    journal: &mut Journal<E, A>,
1040    section: u64,
1041    size: u64,
1042) -> Result<(), Error> {
1043    let blob = journal.0.writer(section);
1044    blob.resize(size).await?;
1045    blob.sync().await?;
1046    Ok(())
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051    use super::*;
1052    use commonware_codec::FixedSize;
1053    use commonware_cryptography::{Hasher as _, Sha256, sha256::Digest};
1054    use commonware_macros::test_traced;
1055    use commonware_runtime::{
1056        BufferPooler, Error as RError, Runner, Spawner as _, Supervisor as _,
1057        buffer::paged::{CacheRef, Writer, corrupt_page},
1058        deterministic,
1059        mocks::{
1060            DelayedSyncContext, PendingSyncs, RecordingContext, fail_pending_syncs,
1061            release_pending_syncs,
1062        },
1063    };
1064    use commonware_utils::{NZU16, NZUsize};
1065    use core::num::NonZeroU16;
1066    use std::{
1067        ops::RangeInclusive,
1068        sync::{
1069            Arc,
1070            atomic::{AtomicUsize, Ordering},
1071        },
1072    };
1073
1074    const PAGE_SIZE: NonZeroU16 = NZU16!(44);
1075    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
1076
1077    fn test_digest(value: u64) -> Digest {
1078        Sha256::hash(&[&value.to_be_bytes()])
1079    }
1080
1081    fn test_cfg(pooler: &impl BufferPooler) -> Config {
1082        Config {
1083            partition: "test-partition".into(),
1084            page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
1085            write_buffer: NZUsize!(2048),
1086        }
1087    }
1088
1089    fn aligned_cfg(pooler: &impl BufferPooler) -> Config {
1090        Config {
1091            partition: "segmented-fixed-aligned".into(),
1092            page_cache: CacheRef::from_pooler(pooler, NZU16!(16), NZUsize!(4)),
1093            write_buffer: NZUsize!(128),
1094        }
1095    }
1096
1097    fn lazy_recovery_cfg(pooler: &impl BufferPooler, partition: &str) -> Config {
1098        Config {
1099            partition: partition.into(),
1100            page_cache: CacheRef::from_pooler(pooler, NZU16!(16), NZUsize!(4)),
1101            write_buffer: NZUsize!(1),
1102        }
1103    }
1104
1105    async fn replay_all<E, A>(journal: Journal<E, A>) -> Journal<E, A>
1106    where
1107        E: Storage + Metrics,
1108        A: CodecFixedShared,
1109    {
1110        let mut replay = journal
1111            .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1112            .await
1113            .expect("failed to start recovery replay");
1114        while let Some(item) = replay.next().await {
1115            item.expect("failed to recover journal");
1116        }
1117        replay.finish().expect("failed to finish recovery replay")
1118    }
1119
1120    /// Seed each section with sixteen durable u64 items.
1121    async fn seed<E: Storage + Metrics>(context: &E, cfg: &Config, sections: RangeInclusive<u64>) {
1122        let mut journal = Journal::init(context.child("seed"), cfg.clone())
1123            .await
1124            .expect("failed to init");
1125        for section in sections {
1126            for value in 0..16u64 {
1127                (journal, _) = journal
1128                    .append(section, &value)
1129                    .await
1130                    .expect("failed to append");
1131            }
1132        }
1133        journal.sync_all().await.expect("failed to sync");
1134    }
1135
1136    #[test_traced]
1137    fn test_segmented_fixed_append_and_get() {
1138        let executor = deterministic::Runner::default();
1139        executor.start(|context| async move {
1140            let cfg = test_cfg(&context);
1141            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1142                .await
1143                .expect("failed to init");
1144
1145            let pos0;
1146            (journal, pos0) = journal
1147                .append(1, &test_digest(0))
1148                .await
1149                .expect("failed to append");
1150            assert_eq!(pos0, 0);
1151
1152            let pos1;
1153            (journal, pos1) = journal
1154                .append(1, &test_digest(1))
1155                .await
1156                .expect("failed to append");
1157            assert_eq!(pos1, 1);
1158
1159            let pos2;
1160            (journal, pos2) = journal
1161                .append(2, &test_digest(2))
1162                .await
1163                .expect("failed to append");
1164            assert_eq!(pos2, 0);
1165
1166            let item0 = journal.get(1, 0).await.expect("failed to get");
1167            assert_eq!(item0, test_digest(0));
1168
1169            let item1 = journal.get(1, 1).await.expect("failed to get");
1170            assert_eq!(item1, test_digest(1));
1171
1172            let item2 = journal.get(2, 0).await.expect("failed to get");
1173            assert_eq!(item2, test_digest(2));
1174
1175            let err = journal.get(1, 2).await;
1176            assert!(matches!(err, Err(Error::ItemOutOfRange(2))));
1177
1178            let err = journal.get(3, 0).await;
1179            assert!(matches!(err, Err(Error::SectionOutOfRange(3))));
1180
1181            journal.destroy().await.expect("failed to destroy");
1182        });
1183    }
1184
1185    #[test_traced]
1186    fn test_segmented_fixed_replay_empty_finishes_immediately() {
1187        let executor = deterministic::Runner::default();
1188        executor.start(|context| async move {
1189            let cfg = test_cfg(&context);
1190            let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
1191                .await
1192                .expect("failed to init");
1193
1194            // An empty journal's reader is exhausted from the start
1195            let replay = journal
1196                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1197                .await
1198                .expect("failed to replay");
1199            let journal = replay.finish().expect("failed to finish replay");
1200            journal.destroy().await.expect("failed to destroy");
1201        });
1202    }
1203
1204    #[test_traced]
1205    fn test_segmented_fixed_replay_propagates_read_options() {
1206        let executor = deterministic::Runner::default();
1207        executor.start(|context| async move {
1208            let (context, recordings) = RecordingContext::new(context);
1209            let cfg = test_cfg(&context);
1210            let mut journal = Journal::init(context.child("storage"), cfg)
1211                .await
1212                .expect("failed to init");
1213
1214            for section in 1..=2 {
1215                (journal, _) = journal
1216                    .append(section, &test_digest(section))
1217                    .await
1218                    .expect("failed to append");
1219            }
1220
1221            let mut replay = journal
1222                .replay(1, 0, NZUsize!(56), ReadOptions::DONT_CACHE)
1223                .await
1224                .expect("failed to replay");
1225            recordings.clear();
1226
1227            // The first lazy refill must carry the caller's policy.
1228            let (section, position, item) = replay
1229                .next()
1230                .await
1231                .expect("missing first replay item")
1232                .expect("failed to read first replay item");
1233            assert_eq!((section, position, item), (1, 0, test_digest(1)));
1234            let reads = recordings.snapshot().reads;
1235            assert!(!reads.is_empty());
1236            assert!(
1237                reads
1238                    .iter()
1239                    .all(|options| *options == ReadOptions::DONT_CACHE)
1240            );
1241
1242            // Crossing into the next section must preserve the same policy.
1243            recordings.clear();
1244            let (section, position, item) = replay
1245                .next()
1246                .await
1247                .expect("missing second replay item")
1248                .expect("failed to read second replay item");
1249            assert_eq!((section, position, item), (2, 0, test_digest(2)));
1250            let reads = recordings.snapshot().reads;
1251            assert!(!reads.is_empty());
1252            assert!(
1253                reads
1254                    .iter()
1255                    .all(|options| *options == ReadOptions::DONT_CACHE)
1256            );
1257            assert!(replay.next().await.is_none());
1258
1259            let journal = replay.finish().expect("failed to finish replay");
1260            journal.destroy().await.expect("failed to destroy");
1261        });
1262    }
1263
1264    #[test_traced]
1265    fn test_segmented_fixed_clean_recovery_uses_replay_buffer() {
1266        let executor = deterministic::Runner::default();
1267        executor.start(|context| async move {
1268            let (context, recordings) = RecordingContext::new(context);
1269            let cfg = lazy_recovery_cfg(&context, "segmented-fixed-lazy-recovery");
1270
1271            // Sixteen u64s occupy eight physical pages. Persist a clean journal whose tiny write
1272            // buffer would force an eager recovery scan to issue one read per page.
1273            seed(&context, &cfg, 1..=1).await;
1274
1275            // Initialization needs only the terminal page used to open Writer. Recovery belongs to
1276            // replay, whose 112-byte budget batches four 28-byte physical pages per read.
1277            recordings.clear();
1278            let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1279                .await
1280                .expect("failed to reopen");
1281            assert_eq!(recordings.snapshot().reads.len(), 1);
1282
1283            let mut replay = journal
1284                .replay(0, 0, NZUsize!(112), ReadOptions::default())
1285                .await
1286                .expect("failed to replay");
1287            while let Some(item) = replay.next().await {
1288                item.expect("failed to read replay item");
1289            }
1290            assert_eq!(recordings.snapshot().reads.len(), 3);
1291            replay
1292                .finish()
1293                .expect("failed to finish replay")
1294                .destroy()
1295                .await
1296                .expect("failed to destroy");
1297        });
1298    }
1299
1300    #[test_traced]
1301    #[should_panic(expected = "must be replayed before append")]
1302    fn test_segmented_fixed_append_requires_replay_after_reopen() {
1303        let executor = deterministic::Runner::default();
1304        executor.start(|context| async move {
1305            let cfg = test_cfg(&context);
1306            let mut journal = Journal::init(context.child("seed"), cfg.clone())
1307                .await
1308                .expect("failed to init");
1309            (journal, _) = journal
1310                .append(1, &test_digest(0))
1311                .await
1312                .expect("failed to append");
1313            journal = journal.sync_all().await.expect("failed to sync");
1314            drop(journal);
1315
1316            // A tail page can hide an earlier torn page. Reopened sections remain append-locked
1317            // until ordered replay has validated their full retained prefix.
1318            let journal = Journal::init(context.child("reopen"), cfg)
1319                .await
1320                .expect("failed to reopen");
1321            journal.append(1, &test_digest(1)).await.unwrap();
1322        });
1323    }
1324
1325    #[test_traced]
1326    #[should_panic(expected = "must be replayed before append")]
1327    fn test_segmented_fixed_gates_older_section_after_reopen() {
1328        let executor = deterministic::Runner::default();
1329        executor.start(|context| async move {
1330            let cfg = test_cfg(&context);
1331            seed(&context, &cfg, 1..=3).await;
1332
1333            // Every nonempty retained section is append-locked, not only the oldest or the
1334            // newest.
1335            let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
1336                .await
1337                .expect("failed to reopen");
1338            journal.append(2, &2).await.unwrap();
1339        });
1340    }
1341
1342    #[test_traced]
1343    fn test_segmented_fixed_floor_preflight_reads_boundary_only() {
1344        let executor = deterministic::Runner::default();
1345        executor.start(|context| async move {
1346            let (context, recordings) = RecordingContext::new(context);
1347            let cfg = lazy_recovery_cfg(&context, "segmented-fixed-floor-boundary");
1348            seed(&context, &cfg, 1..=1).await;
1349
1350            // A durable floor proves its prefix. Preflight reads only the page containing the
1351            // terminal entry and leaves the ordinary replay pass to inspect the suffix.
1352            recordings.clear();
1353            let floors = BTreeMap::from([(1, 16)]);
1354            let preflight =
1355                Journal::<_, u64>::preflight_floors(context.child("preflight"), cfg, &floors)
1356                    .await
1357                    .expect("failed to preflight");
1358            assert_eq!(recordings.snapshot().reads.len(), 1);
1359            preflight
1360                .finish()
1361                .await
1362                .expect("failed to finish preflight")
1363                .destroy()
1364                .await
1365                .expect("failed to destroy");
1366        });
1367    }
1368
1369    #[test_traced]
1370    fn test_segmented_fixed_restore_reads_boundaries_only() {
1371        let executor = deterministic::Runner::default();
1372        executor.start(|context| async move {
1373            let (context, recordings) = RecordingContext::new(context);
1374            let cfg = lazy_recovery_cfg(&context, "segmented-fixed-restore-boundaries");
1375            seed(&context, &cfg, 0..=1).await;
1376
1377            // The checkpoint makes both retained sections durable. Restore needs one terminal
1378            // boundary read per section, independent of the eight pages stored in each.
1379            recordings.clear();
1380            let preflight = Journal::<_, u64>::preflight_restore(
1381                context.child("preflight"),
1382                cfg,
1383                1,
1384                16 * u64::SIZE as u64,
1385            )
1386            .await
1387            .expect("failed to preflight");
1388            assert_eq!(recordings.snapshot().reads.len(), 2);
1389            preflight
1390                .finish()
1391                .await
1392                .expect("failed to finish preflight")
1393                .destroy()
1394                .await
1395                .expect("failed to destroy");
1396        });
1397    }
1398
1399    #[test_traced]
1400    fn test_segmented_fixed_restore_retains_empty_checkpoint_section() {
1401        let executor = deterministic::Runner::default();
1402        executor.start(|context| async move {
1403            let cfg = lazy_recovery_cfg(&context, "segmented-fixed-empty-restore");
1404            let mut journal = Journal::init(context.child("seed"), cfg.clone())
1405                .await
1406                .expect("failed to init");
1407            for section in 0..=2 {
1408                (journal, _) = journal
1409                    .append(section, &section)
1410                    .await
1411                    .expect("failed to append");
1412            }
1413            journal = journal.sync_all().await.expect("failed to sync");
1414            journal = journal
1415                .rewind_section(1, 0)
1416                .await
1417                .expect("failed to empty checkpoint section");
1418            journal = journal.sync(1).await.expect("failed to sync empty section");
1419            drop(journal);
1420
1421            let journal = Journal::<_, u64>::preflight_restore(context.child("restore"), cfg, 1, 0)
1422                .await
1423                .expect("failed to preflight")
1424                .finish()
1425                .await
1426                .expect("failed to finish preflight");
1427
1428            assert_eq!(journal.sections().collect::<Vec<_>>(), vec![0, 1]);
1429            assert_eq!(journal.size(1).expect("missing checkpoint section"), 0);
1430            journal.destroy().await.expect("failed to destroy");
1431        });
1432    }
1433
1434    #[test_traced]
1435    fn test_segmented_fixed_restore_floors_block_below_checkpoint_repair() {
1436        let executor = deterministic::Runner::default();
1437        executor.start(|context| async move {
1438            let cfg = lazy_recovery_cfg(&context, "segmented-fixed-restore-floors");
1439            seed(&context, &cfg, 0..=1).await;
1440
1441            // Tear an interior page below the checkpoint boundary. The preflight reads only the
1442            // terminal boundary page, so restore succeeds and must arm the proven floor.
1443            corrupt_page(&context, &cfg.partition, &1u64.to_be_bytes(), 2, 16).await;
1444            let size = 16 * u64::SIZE as u64;
1445            let journal = Journal::<_, u64>::preflight_restore(
1446                context.child("restore"),
1447                cfg.clone(),
1448                1,
1449                size,
1450            )
1451            .await
1452            .expect("failed to preflight")
1453            .finish()
1454            .await
1455            .expect("failed to finish preflight");
1456
1457            // Replay repair may not truncate checkpoint-proven bytes: the torn page sits below
1458            // the restore floor, so recovery fails loud instead of mutating.
1459            let mut replay = journal
1460                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1461                .await
1462                .expect("failed to start replay");
1463            let mut outcome = None;
1464            while let Some(item) = replay.next().await {
1465                if let Err(err) = item {
1466                    outcome = Some(err);
1467                    break;
1468                }
1469            }
1470            assert!(matches!(
1471                outcome,
1472                Some(Error::Corruption(ref message))
1473                    if message.contains("below its 128-byte validation floor")
1474            ));
1475            drop(replay);
1476
1477            // The checkpoint-proven bytes stay untouched: eight 28-byte physical pages.
1478            let (_, blob_size) = context
1479                .open(&cfg.partition, &1u64.to_be_bytes())
1480                .await
1481                .expect("failed to open");
1482            assert_eq!(blob_size, 8 * 28);
1483        });
1484    }
1485
1486    #[test_traced]
1487    fn test_segmented_fixed_floor_preflight_reads_terminal_entry_only() {
1488        let executor = deterministic::Runner::default();
1489        executor.start(|context| async move {
1490            let (context, recordings) = RecordingContext::new(context);
1491            let cfg = aligned_cfg(&context);
1492            let mut journal = Journal::init(context.child("seed"), cfg.clone())
1493                .await
1494                .expect("failed to init");
1495            for value in 0..2 {
1496                (journal, _) = journal
1497                    .append(1, &test_digest(value))
1498                    .await
1499                    .expect("failed to append");
1500            }
1501            journal = journal.sync(1).await.expect("failed to sync");
1502            drop(journal);
1503
1504            recordings.clear();
1505            let floors = BTreeMap::from([(1, 2)]);
1506            let journal =
1507                Journal::<_, Digest>::preflight_floors(context.child("reopen"), cfg, &floors)
1508                    .await
1509                    .expect("failed to preflight")
1510                    .finish()
1511                    .await
1512                    .expect("failed to reopen");
1513
1514            // Two 32-byte items occupy four 16-byte pages. The terminal entry spans two pages, and
1515            // Writer reads the tail once when opening. No earlier entry page is scanned.
1516            assert_eq!(recordings.snapshot().reads.len(), 3);
1517            journal.destroy().await.expect("failed to destroy");
1518        });
1519    }
1520
1521    #[test_traced]
1522    fn test_segmented_fixed_replay_finish_before_drain_fails() {
1523        let executor = deterministic::Runner::default();
1524        executor.start(|context| async move {
1525            let cfg = test_cfg(&context);
1526            let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
1527                .await
1528                .expect("failed to init");
1529            (journal, _) = journal
1530                .append(1, &test_digest(0))
1531                .await
1532                .expect("failed to append");
1533            journal = journal.sync_all().await.expect("failed to sync");
1534
1535            let replay = journal
1536                .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1537                .await
1538                .expect("failed to replay");
1539            assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
1540        });
1541    }
1542
1543    #[test_traced]
1544    fn test_segmented_fixed_replay() {
1545        let executor = deterministic::Runner::default();
1546        executor.start(|context| async move {
1547            let cfg = test_cfg(&context);
1548            let mut journal = Journal::init(context.child("first"), cfg.clone())
1549                .await
1550                .expect("failed to init");
1551
1552            for i in 0u64..10 {
1553                (journal, _) = journal
1554                    .append(1, &test_digest(i))
1555                    .await
1556                    .expect("failed to append");
1557            }
1558            for i in 10u64..20 {
1559                (journal, _) = journal
1560                    .append(2, &test_digest(i))
1561                    .await
1562                    .expect("failed to append");
1563            }
1564
1565            journal = journal.sync_all().await.expect("failed to sync");
1566            drop(journal);
1567
1568            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1569                .await
1570                .expect("failed to re-init");
1571
1572            let items = {
1573                let mut replay = journal
1574                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1575                    .await
1576                    .expect("failed to replay");
1577
1578                let mut items = Vec::new();
1579                while let Some(result) = replay.next().await {
1580                    match result {
1581                        Ok((section, pos, item)) => items.push((section, pos, item)),
1582                        Err(err) => panic!("replay error: {err}"),
1583                    }
1584                }
1585                journal = replay.finish().expect("failed to finish replay");
1586                items
1587            };
1588
1589            assert_eq!(items.len(), 20);
1590            for (i, item) in items.iter().enumerate().take(10) {
1591                assert_eq!(item.0, 1);
1592                assert_eq!(item.1, i as u64);
1593                assert_eq!(item.2, test_digest(i as u64));
1594            }
1595            for (i, item) in items.iter().enumerate().skip(10).take(10) {
1596                assert_eq!(item.0, 2);
1597                assert_eq!(item.1, (i - 10) as u64);
1598                assert_eq!(item.2, test_digest(i as u64));
1599            }
1600
1601            journal.destroy().await.expect("failed to destroy");
1602        });
1603    }
1604
1605    #[test_traced]
1606    fn test_segmented_fixed_replay_with_start_offset() {
1607        // Test that replay with a non-zero start_position correctly skips items.
1608        let executor = deterministic::Runner::default();
1609        executor.start(|context| async move {
1610            let cfg = test_cfg(&context);
1611            let mut journal = Journal::init(context.child("first"), cfg.clone())
1612                .await
1613                .expect("failed to init");
1614
1615            // Append 10 items to section 1
1616            for i in 0u64..10 {
1617                (journal, _) = journal
1618                    .append(1, &test_digest(i))
1619                    .await
1620                    .expect("failed to append");
1621            }
1622            // Append 5 items to section 2
1623            for i in 10u64..15 {
1624                (journal, _) = journal
1625                    .append(2, &test_digest(i))
1626                    .await
1627                    .expect("failed to append");
1628            }
1629            journal = journal.sync_all().await.expect("failed to sync");
1630            drop(journal);
1631
1632            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1633                .await
1634                .expect("failed to re-init");
1635
1636            // Replay from section 1, position 5 - should get items 5-9 from section 1 and all of section 2
1637            {
1638                let mut replay = journal
1639                    .replay(1, 5, NZUsize!(1024), ReadOptions::default())
1640                    .await
1641                    .expect("failed to replay");
1642
1643                let mut items = Vec::new();
1644                while let Some(result) = replay.next().await {
1645                    let (section, pos, item) = result.expect("replay error");
1646                    items.push((section, pos, item));
1647                }
1648                journal = replay.finish().expect("failed to finish replay");
1649
1650                assert_eq!(
1651                    items.len(),
1652                    10,
1653                    "Should have 5 items from section 1 + 5 from section 2"
1654                );
1655
1656                // Check section 1 items (positions 5-9)
1657                for (i, (section, pos, item)) in items.iter().enumerate().take(5) {
1658                    assert_eq!(*section, 1);
1659                    assert_eq!(*pos, (i + 5) as u64);
1660                    assert_eq!(*item, test_digest((i + 5) as u64));
1661                }
1662
1663                // Check section 2 items (positions 0-4)
1664                for (i, (section, pos, item)) in items.iter().enumerate().skip(5) {
1665                    assert_eq!(*section, 2);
1666                    assert_eq!(*pos, (i - 5) as u64);
1667                    assert_eq!(*item, test_digest((i + 5) as u64));
1668                }
1669            }
1670
1671            // Replay from section 1, position 9 - should get only item 9 from section 1 and all of section 2
1672            {
1673                let mut replay = journal
1674                    .replay(1, 9, NZUsize!(1024), ReadOptions::default())
1675                    .await
1676                    .expect("failed to replay");
1677
1678                let mut items = Vec::new();
1679                while let Some(result) = replay.next().await {
1680                    let (section, pos, item) = result.expect("replay error");
1681                    items.push((section, pos, item));
1682                }
1683                journal = replay.finish().expect("failed to finish replay");
1684
1685                assert_eq!(
1686                    items.len(),
1687                    6,
1688                    "Should have 1 item from section 1 + 5 from section 2"
1689                );
1690                assert_eq!(items[0], (1, 9, test_digest(9)));
1691                for (i, (section, pos, item)) in items.iter().enumerate().skip(1) {
1692                    assert_eq!(*section, 2);
1693                    assert_eq!(*pos, (i - 1) as u64);
1694                    assert_eq!(*item, test_digest((i + 9) as u64));
1695                }
1696            }
1697
1698            // Replay from section 2, position 3 - should get only items 3-4 from section 2
1699            {
1700                let mut replay = journal
1701                    .replay(2, 3, NZUsize!(1024), ReadOptions::default())
1702                    .await
1703                    .expect("failed to replay");
1704
1705                let mut items = Vec::new();
1706                while let Some(result) = replay.next().await {
1707                    let (section, pos, item) = result.expect("replay error");
1708                    items.push((section, pos, item));
1709                }
1710                journal = replay.finish().expect("failed to finish replay");
1711
1712                assert_eq!(items.len(), 2, "Should have 2 items from section 2");
1713                assert_eq!(items[0], (2, 3, test_digest(13)));
1714                assert_eq!(items[1], (2, 4, test_digest(14)));
1715            }
1716
1717            // Replay from position past the end should return ItemOutOfRange error.
1718            // A failed replay consumes the journal, so re-initialize between attempts.
1719            let result = journal
1720                .replay(1, 100, NZUsize!(1024), ReadOptions::default())
1721                .await;
1722            assert!(matches!(result, Err(Error::ItemOutOfRange(100))));
1723
1724            let journal = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
1725                .await
1726                .expect("failed to re-init");
1727            let result = journal
1728                .replay(1, u64::MAX, NZUsize!(1024), ReadOptions::default())
1729                .await;
1730            assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
1731
1732            let journal = Journal::<_, Digest>::init(context.child("fourth"), cfg.clone())
1733                .await
1734                .expect("failed to re-init");
1735            journal.destroy().await.expect("failed to destroy");
1736        });
1737    }
1738
1739    #[test_traced]
1740    fn test_segmented_fixed_prune() {
1741        let executor = deterministic::Runner::default();
1742        executor.start(|context| async move {
1743            let cfg = test_cfg(&context);
1744            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1745                .await
1746                .expect("failed to init");
1747
1748            for section in 1u64..=5 {
1749                (journal, _) = journal
1750                    .append(section, &test_digest(section))
1751                    .await
1752                    .expect("failed to append");
1753            }
1754            journal = journal.sync_all().await.expect("failed to sync");
1755
1756            (journal, _) = journal.prune(3).await.expect("failed to prune");
1757
1758            let err = journal.get(1, 0).await;
1759            assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
1760
1761            let err = journal.get(2, 0).await;
1762            assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
1763
1764            let item = journal.get(3, 0).await.expect("should exist");
1765            assert_eq!(item, test_digest(3));
1766
1767            journal.destroy().await.expect("failed to destroy");
1768        });
1769    }
1770
1771    #[test_traced]
1772    fn test_segmented_fixed_pruned_after_full_prune() {
1773        // `pruned` must keep reporting the floor after every blob is removed, when
1774        // `oldest_section` returns None (indistinguishable from a fresh journal).
1775        let executor = deterministic::Runner::default();
1776        executor.start(|context| async move {
1777            let cfg = test_cfg(&context);
1778            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1779                .await
1780                .expect("failed to init");
1781
1782            for section in 1u64..=3 {
1783                (journal, _) = journal
1784                    .append(section, &test_digest(section))
1785                    .await
1786                    .expect("failed to append");
1787            }
1788            journal = journal.sync_all().await.expect("failed to sync");
1789
1790            (journal, _) = journal.prune(10).await.expect("failed to prune");
1791            assert_eq!(journal.oldest_section(), None);
1792            assert!(journal.pruned(3));
1793            assert!(journal.pruned(9));
1794            assert!(!journal.pruned(10));
1795
1796            journal.destroy().await.expect("failed to destroy");
1797        });
1798    }
1799
1800    #[test_traced]
1801    fn test_segmented_fixed_rewind() {
1802        let executor = deterministic::Runner::default();
1803        executor.start(|context| async move {
1804            let cfg = test_cfg(&context);
1805            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1806                .await
1807                .expect("failed to init");
1808
1809            // Create sections 1, 2, 3
1810            for section in 1u64..=3 {
1811                (journal, _) = journal
1812                    .append(section, &test_digest(section))
1813                    .await
1814                    .expect("failed to append");
1815            }
1816            journal = journal.sync_all().await.expect("failed to sync");
1817
1818            // Verify all sections exist
1819            for section in 1u64..=3 {
1820                let size = journal.size(section).expect("failed to get size");
1821                assert!(size > 0, "section {section} should have data");
1822            }
1823
1824            // Rewind to section 1 (should remove sections 2, 3)
1825            let size = journal.size(1).expect("failed to get size");
1826            journal = journal.rewind(1, size).await.expect("failed to rewind");
1827
1828            // Verify section 1 still has data
1829            let size = journal.size(1).expect("failed to get size");
1830            assert!(size > 0, "section 1 should still have data");
1831
1832            // Verify sections 2, 3 are removed
1833            for section in 2u64..=3 {
1834                let size = journal.size(section).expect("failed to get size");
1835                assert_eq!(size, 0, "section {section} should be removed");
1836            }
1837
1838            // Verify data in section 1 is still readable
1839            let item = journal.get(1, 0).await.expect("failed to get");
1840            assert_eq!(item, test_digest(1));
1841
1842            journal.destroy().await.expect("failed to destroy");
1843        });
1844    }
1845
1846    #[test_traced]
1847    fn test_segmented_fixed_rewind_max_section() {
1848        let executor = deterministic::Runner::default();
1849        executor.start(|context| async move {
1850            let cfg = test_cfg(&context);
1851            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1852                .await
1853                .expect("failed to init");
1854
1855            // Append to the maximal section. `section + 1` has no representable successor.
1856            (journal, _) = journal
1857                .append(u64::MAX, &test_digest(0))
1858                .await
1859                .expect("failed to append");
1860            journal = journal.sync_all().await.expect("failed to sync");
1861
1862            // Rewinding the maximal section removes no sections above it and must not panic.
1863            let size = journal.size(u64::MAX).expect("failed to get size");
1864            journal = journal
1865                .rewind(u64::MAX, size)
1866                .await
1867                .expect("failed to rewind");
1868
1869            // The section is intact and readable.
1870            assert_eq!(journal.size(u64::MAX).expect("failed to get size"), size);
1871            assert_eq!(journal.get(u64::MAX, 0).await.unwrap(), test_digest(0));
1872
1873            journal.destroy().await.expect("failed to destroy");
1874        });
1875    }
1876
1877    #[test_traced]
1878    fn test_segmented_fixed_rewind_many_sections() {
1879        let executor = deterministic::Runner::default();
1880        executor.start(|context| async move {
1881            let cfg = test_cfg(&context);
1882            let mut journal = Journal::init(context.child("storage"), cfg.clone())
1883                .await
1884                .expect("failed to init");
1885
1886            // Create sections 1-10
1887            for section in 1u64..=10 {
1888                (journal, _) = journal
1889                    .append(section, &test_digest(section))
1890                    .await
1891                    .expect("failed to append");
1892            }
1893            journal = journal.sync_all().await.expect("failed to sync");
1894
1895            // Rewind to section 5 (should remove sections 6-10)
1896            let size = journal.size(5).expect("failed to get size");
1897            journal = journal.rewind(5, size).await.expect("failed to rewind");
1898
1899            // Verify sections 1-5 still have data
1900            for section in 1u64..=5 {
1901                let size = journal.size(section).expect("failed to get size");
1902                assert!(size > 0, "section {section} should still have data");
1903            }
1904
1905            // Verify sections 6-10 are removed
1906            for section in 6u64..=10 {
1907                let size = journal.size(section).expect("failed to get size");
1908                assert_eq!(size, 0, "section {section} should be removed");
1909            }
1910
1911            // Verify data integrity via replay
1912            {
1913                let mut replay = journal
1914                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
1915                    .await
1916                    .expect("failed to replay");
1917                let mut items = Vec::new();
1918                while let Some(result) = replay.next().await {
1919                    let (section, _, item) = result.expect("failed to read");
1920                    items.push((section, item));
1921                }
1922                journal = replay.finish().expect("failed to finish replay");
1923                assert_eq!(items.len(), 5);
1924                for (i, (section, item)) in items.iter().enumerate() {
1925                    assert_eq!(*section, (i + 1) as u64);
1926                    assert_eq!(*item, test_digest((i + 1) as u64));
1927                }
1928            }
1929
1930            journal.destroy().await.expect("failed to destroy");
1931        });
1932    }
1933
1934    #[test_traced]
1935    fn test_segmented_fixed_rewind_persistence() {
1936        let executor = deterministic::Runner::default();
1937        executor.start(|context| async move {
1938            let cfg = test_cfg(&context);
1939
1940            // Create sections 1-5
1941            let mut journal = Journal::init(context.child("first"), cfg.clone())
1942                .await
1943                .expect("failed to init");
1944            for section in 1u64..=5 {
1945                (journal, _) = journal
1946                    .append(section, &test_digest(section))
1947                    .await
1948                    .expect("failed to append");
1949            }
1950            journal = journal.sync_all().await.expect("failed to sync");
1951
1952            // Rewind to section 2
1953            let size = journal.size(2).expect("failed to get size");
1954            journal = journal.rewind(2, size).await.expect("failed to rewind");
1955            journal = journal.sync_all().await.expect("failed to sync");
1956            drop(journal);
1957
1958            // Re-init and verify only sections 1-2 exist
1959            let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
1960                .await
1961                .expect("failed to re-init");
1962
1963            // Verify sections 1-2 have data
1964            for section in 1u64..=2 {
1965                let size = journal.size(section).expect("failed to get size");
1966                assert!(size > 0, "section {section} should have data after restart");
1967            }
1968
1969            // Verify sections 3-5 are gone
1970            for section in 3u64..=5 {
1971                let size = journal.size(section).expect("failed to get size");
1972                assert_eq!(size, 0, "section {section} should be gone after restart");
1973            }
1974
1975            // Verify data integrity
1976            let item1 = journal.get(1, 0).await.expect("failed to get");
1977            assert_eq!(item1, test_digest(1));
1978            let item2 = journal.get(2, 0).await.expect("failed to get");
1979            assert_eq!(item2, test_digest(2));
1980
1981            journal.destroy().await.expect("failed to destroy");
1982        });
1983    }
1984
1985    #[test_traced]
1986    fn test_segmented_fixed_corruption_recovery() {
1987        let executor = deterministic::Runner::default();
1988        executor.start(|context| async move {
1989            let cfg = test_cfg(&context);
1990            let mut journal = Journal::init(context.child("first"), cfg.clone())
1991                .await
1992                .expect("failed to init");
1993
1994            for i in 0u64..5 {
1995                (journal, _) = journal
1996                    .append(1, &test_digest(i))
1997                    .await
1998                    .expect("failed to append");
1999            }
2000            journal = journal.sync_all().await.expect("failed to sync");
2001            drop(journal);
2002
2003            let (blob, size) = context
2004                .open(&cfg.partition, &1u64.to_be_bytes())
2005                .await
2006                .expect("failed to open blob");
2007            blob.resize(size - 1).await.expect("failed to truncate");
2008            blob.sync().await.expect("failed to sync");
2009
2010            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2011                .await
2012                .expect("failed to re-init");
2013
2014            let count = {
2015                let mut replay = journal
2016                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2017                    .await
2018                    .expect("failed to replay");
2019
2020                let mut count = 0;
2021                while let Some(result) = replay.next().await {
2022                    result.expect("should be ok");
2023                    count += 1;
2024                }
2025                journal = replay.finish().expect("failed to finish replay");
2026                count
2027            };
2028            assert_eq!(count, 4);
2029
2030            journal.destroy().await.expect("failed to destroy");
2031        });
2032    }
2033
2034    #[test_traced]
2035    fn test_segmented_fixed_persistence() {
2036        let executor = deterministic::Runner::default();
2037        executor.start(|context| async move {
2038            let cfg = test_cfg(&context);
2039
2040            // Create and populate journal
2041            let mut journal = Journal::init(context.child("first"), cfg.clone())
2042                .await
2043                .expect("failed to init");
2044
2045            for i in 0u64..5 {
2046                (journal, _) = journal
2047                    .append(1, &test_digest(i))
2048                    .await
2049                    .expect("failed to append");
2050            }
2051            journal = journal.sync_all().await.expect("failed to sync");
2052            drop(journal);
2053
2054            // Reopen and verify data persisted
2055            let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
2056                .await
2057                .expect("failed to re-init");
2058
2059            for i in 0u64..5 {
2060                let item = journal.get(1, i).await.expect("failed to get");
2061                assert_eq!(item, test_digest(i));
2062            }
2063
2064            journal.destroy().await.expect("failed to destroy");
2065        });
2066    }
2067
2068    #[test_traced]
2069    fn test_segmented_fixed_sync() {
2070        let executor = deterministic::Runner::default();
2071        executor.start(|context| async move {
2072            let cfg = test_cfg(&context);
2073            let mut journal = Journal::init(context.child("first"), cfg.clone())
2074                .await
2075                .expect("failed to init");
2076
2077            // One sub-page item per section stays buffered until synced.
2078            for section in 1u64..=3 {
2079                (journal, _) = journal
2080                    .append(section, &test_digest(section))
2081                    .await
2082                    .expect("failed to append");
2083            }
2084
2085            // Sync sections 1 and 3; a nonexistent section (99) is skipped, not an error.
2086            journal
2087                .sync(&[1, 3, 99])
2088                .await
2089                .expect("failed to sync sections");
2090
2091            // Only the synced sections survive the unclean drop.
2092            let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
2093                .await
2094                .expect("failed to re-init");
2095            assert_eq!(
2096                journal.get(1, 0).await.expect("section 1 durable"),
2097                test_digest(1)
2098            );
2099            assert_eq!(
2100                journal.get(3, 0).await.expect("section 3 durable"),
2101                test_digest(3)
2102            );
2103            assert!(matches!(
2104                journal.get(2, 0).await,
2105                Err(Error::ItemOutOfRange(0)) | Err(Error::SectionOutOfRange(2))
2106            ));
2107
2108            journal.destroy().await.expect("failed to destroy");
2109        });
2110    }
2111
2112    #[test_traced]
2113    fn test_segmented_fixed_section_len() {
2114        let executor = deterministic::Runner::default();
2115        executor.start(|context| async move {
2116            let cfg = test_cfg(&context);
2117            let mut journal = Journal::init(context.child("storage"), cfg.clone())
2118                .await
2119                .expect("failed to init");
2120
2121            assert_eq!(journal.section_len(1).unwrap(), 0);
2122
2123            for i in 0u64..5 {
2124                (journal, _) = journal
2125                    .append(1, &test_digest(i))
2126                    .await
2127                    .expect("failed to append");
2128            }
2129
2130            assert_eq!(journal.section_len(1).unwrap(), 5);
2131            assert_eq!(journal.section_len(2).unwrap(), 0);
2132
2133            journal.destroy().await.expect("failed to destroy");
2134        });
2135    }
2136
2137    #[test_traced]
2138    fn test_segmented_fixed_non_contiguous_sections() {
2139        // Test that sections with gaps in numbering work correctly.
2140        // Sections 1, 5, 10 should all be independent and accessible.
2141        let executor = deterministic::Runner::default();
2142        executor.start(|context| async move {
2143            let cfg = test_cfg(&context);
2144            let mut journal = Journal::init(context.child("first"), cfg.clone())
2145                .await
2146                .expect("failed to init");
2147
2148            // Create sections with gaps: 1, 5, 10
2149            (journal, _) = journal
2150                .append(1, &test_digest(100))
2151                .await
2152                .expect("failed to append");
2153            (journal, _) = journal
2154                .append(5, &test_digest(500))
2155                .await
2156                .expect("failed to append");
2157            (journal, _) = journal
2158                .append(10, &test_digest(1000))
2159                .await
2160                .expect("failed to append");
2161            journal = journal.sync_all().await.expect("failed to sync");
2162
2163            // Verify random access to each section
2164            assert_eq!(journal.get(1, 0).await.unwrap(), test_digest(100));
2165            assert_eq!(journal.get(5, 0).await.unwrap(), test_digest(500));
2166            assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(1000));
2167
2168            // Verify non-existent sections return appropriate errors
2169            for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
2170                let result = journal.get(missing_section, 0).await;
2171                assert!(
2172                    matches!(result, Err(Error::SectionOutOfRange(_))),
2173                    "Expected SectionOutOfRange for section {}, got {:?}",
2174                    missing_section,
2175                    result
2176                );
2177            }
2178
2179            // Drop and reopen to test replay
2180            drop(journal);
2181            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2182                .await
2183                .expect("failed to re-init");
2184
2185            // Replay and verify all items in order
2186            {
2187                let mut replay = journal
2188                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2189                    .await
2190                    .expect("failed to replay");
2191
2192                let mut items = Vec::new();
2193                while let Some(result) = replay.next().await {
2194                    let (section, _, item) = result.expect("replay error");
2195                    items.push((section, item));
2196                }
2197                journal = replay.finish().expect("failed to finish replay");
2198
2199                assert_eq!(items.len(), 3, "Should have 3 items");
2200                assert_eq!(items[0], (1, test_digest(100)));
2201                assert_eq!(items[1], (5, test_digest(500)));
2202                assert_eq!(items[2], (10, test_digest(1000)));
2203            }
2204
2205            // Test replay starting from middle section (5)
2206            {
2207                let mut replay = journal
2208                    .replay(5, 0, NZUsize!(1024), ReadOptions::default())
2209                    .await
2210                    .expect("failed to replay from section 5");
2211
2212                let mut items = Vec::new();
2213                while let Some(result) = replay.next().await {
2214                    let (section, _, item) = result.expect("replay error");
2215                    items.push((section, item));
2216                }
2217                journal = replay.finish().expect("failed to finish replay");
2218
2219                assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
2220                assert_eq!(items[0], (5, test_digest(500)));
2221                assert_eq!(items[1], (10, test_digest(1000)));
2222            }
2223
2224            journal.destroy().await.expect("failed to destroy");
2225        });
2226    }
2227
2228    #[test_traced]
2229    fn test_segmented_fixed_empty_section_in_middle() {
2230        // Test that replay correctly handles an empty section between sections with data.
2231        // Section 1 has data, section 2 is empty, section 3 has data.
2232        let executor = deterministic::Runner::default();
2233        executor.start(|context| async move {
2234            let cfg = test_cfg(&context);
2235            let mut journal = Journal::init(context.child("first"), cfg.clone())
2236                .await
2237                .expect("failed to init");
2238
2239            // Append to section 1
2240            (journal, _) = journal
2241                .append(1, &test_digest(100))
2242                .await
2243                .expect("failed to append");
2244
2245            // Create section 2 but make it empty via rewind
2246            (journal, _) = journal
2247                .append(2, &test_digest(200))
2248                .await
2249                .expect("failed to append");
2250            journal = journal.sync(2).await.expect("failed to sync");
2251            journal = journal
2252                .rewind_section(2, 0)
2253                .await
2254                .expect("failed to rewind");
2255
2256            // Append to section 3
2257            (journal, _) = journal
2258                .append(3, &test_digest(300))
2259                .await
2260                .expect("failed to append");
2261
2262            journal = journal.sync_all().await.expect("failed to sync");
2263
2264            // Verify section lengths
2265            assert_eq!(journal.section_len(1).unwrap(), 1);
2266            assert_eq!(journal.section_len(2).unwrap(), 0);
2267            assert_eq!(journal.section_len(3).unwrap(), 1);
2268
2269            // Drop and reopen to test replay
2270            drop(journal);
2271            let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2272                .await
2273                .expect("failed to re-init");
2274
2275            // Replay all - should get items from sections 1 and 3, skipping empty section 2
2276            {
2277                let mut replay = journal
2278                    .replay(0, 0, NZUsize!(1024), ReadOptions::default())
2279                    .await
2280                    .expect("failed to replay");
2281
2282                let mut items = Vec::new();
2283                while let Some(result) = replay.next().await {
2284                    let (section, _, item) = result.expect("replay error");
2285                    items.push((section, item));
2286                }
2287                journal = replay.finish().expect("failed to finish replay");
2288
2289                assert_eq!(
2290                    items.len(),
2291                    2,
2292                    "Should have 2 items (skipping empty section)"
2293                );
2294                assert_eq!(items[0], (1, test_digest(100)));
2295                assert_eq!(items[1], (3, test_digest(300)));
2296            }
2297
2298            // Replay starting from empty section 2 - should get only section 3
2299            {
2300                let mut replay = journal
2301                    .replay(2, 0, NZUsize!(1024), ReadOptions::default())
2302                    .await
2303                    .expect("failed to replay from section 2");
2304
2305                let mut items = Vec::new();
2306                while let Some(result) = replay.next().await {
2307                    let (section, _, item) = result.expect("replay error");
2308                    items.push((section, item));
2309                }
2310                journal = replay.finish().expect("failed to finish replay");
2311
2312                assert_eq!(items.len(), 1, "Should have 1 item from section 3");
2313                assert_eq!(items[0], (3, test_digest(300)));
2314            }
2315
2316            journal.destroy().await.expect("failed to destroy");
2317        });
2318    }
2319
2320    #[test_traced]
2321    fn test_segmented_fixed_validates_pages_before_trailing_bytes() {
2322        let executor = deterministic::Runner::default();
2323        executor.start(|context| async move {
2324            const LOGICAL_PAGE_SIZE: u64 = 5;
2325            const SECTION: u64 = 0;
2326
2327            let cfg = Config {
2328                partition: "segmented-fixed-validate-before-tail-trim".into(),
2329                page_cache: CacheRef::from_pooler(
2330                    &context,
2331                    NZU16!(LOGICAL_PAGE_SIZE as u16),
2332                    NZUsize!(4),
2333                ),
2334                write_buffer: NZUsize!(128),
2335            };
2336            let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2337                .await
2338                .unwrap();
2339            for value in [11u64, 22, 33, 44] {
2340                (journal, _) = journal.append(SECTION, &value).await.unwrap();
2341            }
2342            journal = journal.sync_all().await.unwrap();
2343            drop(journal);
2344
2345            let (blob, size) = context
2346                .open(&cfg.partition, &SECTION.to_be_bytes())
2347                .await
2348                .unwrap();
2349            let mut writer = Writer::new(blob, size, 128, cfg.page_cache.clone())
2350                .await
2351                .unwrap();
2352            writer.resize(30).await.unwrap();
2353            writer.sync().await.unwrap();
2354            drop(writer);
2355
2356            // Five-byte integrity pages crossed by eight-byte journal items:
2357            //
2358            // pages: [0..5) [5..10) [10..15) [15..20) [20..25) [25..30)
2359            // state:    ok      ok       ok       ok       torn      ok
2360            // items: [0......8) [8.......16) [16......24) [24..30 tail)
2361            //
2362            // Backward sizing stops at valid page 5 and reports 30 logical bytes. Item alignment
2363            // alone selects 24, inside torn page 4, which `Writer::resize` cannot preserve.
2364            // Forward page validation finds 20 contiguous bytes and selects safe item boundary 16.
2365            corrupt_page(
2366                &context,
2367                &cfg.partition,
2368                &SECTION.to_be_bytes(),
2369                4,
2370                LOGICAL_PAGE_SIZE,
2371            )
2372            .await;
2373
2374            let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
2375                .await
2376                .unwrap();
2377            let journal = replay_all(journal).await;
2378            assert_eq!(journal.section_len(SECTION).unwrap(), 2);
2379            assert_eq!(journal.get(SECTION, 0).await.unwrap(), 11);
2380            assert_eq!(journal.get(SECTION, 1).await.unwrap(), 22);
2381            journal.destroy().await.unwrap();
2382        });
2383    }
2384
2385    #[test_traced]
2386    fn test_segmented_fixed_repairs_torn_interior_page() {
2387        const SECTION: u64 = 0;
2388
2389        let executor = deterministic::Runner::default();
2390        executor.start(|context| async move {
2391            let cfg = aligned_cfg(&context);
2392            let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
2393                .await
2394                .unwrap();
2395            for value in 0..6 {
2396                (journal, _) = journal.append(SECTION, &value).await.unwrap();
2397            }
2398            journal = journal.sync_all().await.unwrap();
2399            drop(journal);
2400
2401            // The three 16-byte logical pages each contain two complete u64 items. Tear the
2402            // interior page while its neighbors stay valid:
2403            //
2404            // pages: [0........16) [16.......32) [32.......48)
2405            // state:     valid          torn          valid
2406            // items: [0.......1]    [2.......3]    [4.......5]
2407            //
2408            // Backward sizing sees valid page 2 and reports the item-aligned length 48. Recovery
2409            // must still scan forward, truncate to page 0's 16-byte prefix, and discard items 2-5.
2410            corrupt_page(&context, &cfg.partition, &SECTION.to_be_bytes(), 1, 16).await;
2411
2412            let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
2413                .await
2414                .unwrap();
2415            journal = replay_all(journal).await;
2416            assert_eq!(journal.section_len(SECTION).unwrap(), 2);
2417            assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
2418            assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
2419            assert!(matches!(
2420                journal.get(SECTION, 2).await,
2421                Err(Error::ItemOutOfRange(2))
2422            ));
2423
2424            // The repair truncation is durable: appends resume at the repaired boundary and
2425            // survive reopen.
2426            let position;
2427            (journal, position) = journal.append(SECTION, &99).await.unwrap();
2428            assert_eq!(position, 2);
2429            journal.sync_all().await.unwrap();
2430
2431            let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
2432                .await
2433                .unwrap();
2434            assert_eq!(journal.section_len(SECTION).unwrap(), 3);
2435            assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
2436            assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
2437            assert_eq!(journal.get(SECTION, 2).await.unwrap(), 99);
2438            journal.destroy().await.unwrap();
2439        });
2440    }
2441
2442    #[test_traced]
2443    fn test_segmented_fixed_truncation_recovery_across_page_boundary() {
2444        // Test that truncating a single byte from a blob that has items straddling a page boundary
2445        // correctly recovers by removing the incomplete item.
2446        //
2447        // With PAGE_SIZE=44 and ITEM_SIZE=32:
2448        // - Item 0: bytes 0-31
2449        // - Item 1: bytes 32-63 (straddles page boundary at 44)
2450        // - Item 2: bytes 64-95 (straddles page boundary at 88)
2451        //
2452        // After 3 items we have 96 bytes = 2 full pages + 8 bytes. Truncating 1 byte leaves 95
2453        // bytes, which is not a multiple of 32. Recovery should truncate to 64 bytes (2 complete
2454        // items).
2455        let executor = deterministic::Runner::default();
2456        executor.start(|context| async move {
2457            let cfg = test_cfg(&context);
2458            let mut journal = Journal::init(context.child("first"), cfg.clone())
2459                .await
2460                .expect("failed to init");
2461
2462            // Append 3 items (just over 2 pages worth)
2463            for i in 0u64..3 {
2464                (journal, _) = journal
2465                    .append(1, &test_digest(i))
2466                    .await
2467                    .expect("failed to append");
2468            }
2469            journal = journal.sync_all().await.expect("failed to sync");
2470
2471            // Verify all 3 items are readable
2472            for i in 0u64..3 {
2473                let item = journal.get(1, i).await.expect("failed to get");
2474                assert_eq!(item, test_digest(i));
2475            }
2476            drop(journal);
2477
2478            // Truncate the blob by exactly 1 byte to simulate partial write
2479            let (blob, size) = context
2480                .open(&cfg.partition, &1u64.to_be_bytes())
2481                .await
2482                .expect("failed to open blob");
2483            blob.resize(size - 1).await.expect("failed to truncate");
2484            blob.sync().await.expect("failed to sync");
2485            drop(blob);
2486
2487            // Reopen and drain recovery. Writer removes the torn physical tail while replay rounds
2488            // the remaining logical prefix down to complete items.
2489            let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
2490                .await
2491                .expect("failed to re-init");
2492            let journal = replay_all(journal).await;
2493
2494            // Verify section now has only 2 items
2495            assert_eq!(journal.section_len(1).unwrap(), 2);
2496
2497            // Verify size is the expected multiple of ITEM_SIZE (this would fail if we didn't trim
2498            // items and just relied on page-level checksum recovery).
2499            assert_eq!(journal.size(1).unwrap(), 64);
2500
2501            // Items 0 and 1 should still be readable
2502            let item0 = journal.get(1, 0).await.expect("failed to get item 0");
2503            assert_eq!(item0, test_digest(0));
2504            let item1 = journal.get(1, 1).await.expect("failed to get item 1");
2505            assert_eq!(item1, test_digest(1));
2506
2507            // Item 2 should return ItemOutOfRange
2508            let err = journal.get(1, 2).await;
2509            assert!(
2510                matches!(err, Err(Error::ItemOutOfRange(2))),
2511                "expected ItemOutOfRange(2), got {:?}",
2512                err
2513            );
2514
2515            journal.destroy().await.expect("failed to destroy");
2516        });
2517    }
2518
2519    #[test_traced]
2520    fn test_journal_clear() {
2521        let executor = deterministic::Runner::default();
2522        executor.start(|context| async move {
2523            let cfg = Config {
2524                partition: "clear-test".into(),
2525                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2526                write_buffer: NZUsize!(1024),
2527            };
2528
2529            let mut journal: Journal<_, Digest> =
2530                Journal::init(context.child("journal"), cfg.clone())
2531                    .await
2532                    .expect("Failed to initialize journal");
2533
2534            // Append items across multiple sections
2535            for section in 0..5u64 {
2536                for i in 0..10u64 {
2537                    (journal, _) = journal
2538                        .append(section, &test_digest(section * 1000 + i))
2539                        .await
2540                        .expect("Failed to append");
2541                }
2542                journal = journal.sync(section).await.expect("Failed to sync");
2543            }
2544
2545            // Verify we have data
2546            assert_eq!(journal.get(0, 0).await.unwrap(), test_digest(0));
2547            assert_eq!(journal.get(4, 0).await.unwrap(), test_digest(4000));
2548
2549            // Clear the journal
2550            journal = journal.clear().await.expect("Failed to clear");
2551
2552            // After clear, all reads should fail
2553            for section in 0..5u64 {
2554                assert!(matches!(
2555                    journal.get(section, 0).await,
2556                    Err(Error::SectionOutOfRange(s)) if s == section
2557                ));
2558            }
2559
2560            // Append new data after clear
2561            for i in 0..5u64 {
2562                (journal, _) = journal
2563                    .append(10, &test_digest(i * 100))
2564                    .await
2565                    .expect("Failed to append after clear");
2566            }
2567            journal = journal.sync(10).await.expect("Failed to sync after clear");
2568
2569            // New data should be readable
2570            assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(0));
2571
2572            // Old sections should still be missing
2573            assert!(matches!(
2574                journal.get(0, 0).await,
2575                Err(Error::SectionOutOfRange(0))
2576            ));
2577
2578            journal.destroy().await.unwrap();
2579        });
2580    }
2581
2582    #[test_traced]
2583    fn test_last_missing_section_returns_error() {
2584        let executor = deterministic::Runner::default();
2585        executor.start(|context| async move {
2586            let cfg = test_cfg(&context);
2587            let journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
2588                .await
2589                .expect("failed to init");
2590
2591            assert!(matches!(
2592                journal.last(0).await,
2593                Err(Error::SectionOutOfRange(0))
2594            ));
2595            assert!(matches!(
2596                journal.last(99).await,
2597                Err(Error::SectionOutOfRange(99))
2598            ));
2599
2600            journal.destroy().await.unwrap();
2601        });
2602    }
2603
2604    #[test_traced]
2605    fn test_last_after_rewind_to_zero() {
2606        let executor = deterministic::Runner::default();
2607        executor.start(|context| async move {
2608            let cfg = test_cfg(&context);
2609            let mut journal = Journal::init(context.child("storage"), cfg.clone())
2610                .await
2611                .expect("failed to init");
2612
2613            (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
2614            (journal, _) = journal.append(0, &test_digest(1)).await.unwrap();
2615            journal = journal.sync(0).await.unwrap();
2616
2617            assert!(journal.last(0).await.unwrap().is_some());
2618
2619            journal = journal.rewind(0, 0).await.unwrap();
2620            assert_eq!(journal.last(0).await.unwrap(), None);
2621
2622            journal.destroy().await.unwrap();
2623        });
2624    }
2625
2626    #[test_traced]
2627    fn test_last_pruned_section_returns_error() {
2628        let executor = deterministic::Runner::default();
2629        executor.start(|context| async move {
2630            let cfg = test_cfg(&context);
2631            let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
2632                .await
2633                .expect("failed to init");
2634
2635            (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
2636            (journal, _) = journal.append(1, &test_digest(1)).await.unwrap();
2637            journal = journal.sync_all().await.unwrap();
2638
2639            (journal, _) = journal.prune(1).await.unwrap();
2640
2641            assert!(matches!(
2642                journal.last(0).await,
2643                Err(Error::AlreadyPrunedToSection(1))
2644            ));
2645            assert!(journal.last(1).await.unwrap().is_some());
2646
2647            journal.destroy().await.unwrap();
2648        });
2649    }
2650
2651    #[test_traced]
2652    fn test_get_many_empty() {
2653        let executor = deterministic::Runner::default();
2654        executor.start(|context| async move {
2655            let cfg = test_cfg(&context);
2656            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2657            (journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
2658            assert_eq!(journal.section_len(0).unwrap(), 1);
2659
2660            let mut buf = [];
2661            let (items, hits) = journal.get_many(0, &[], &mut buf).await.unwrap();
2662            assert!(items.is_empty());
2663            assert_eq!(hits, 0);
2664
2665            journal.destroy().await.unwrap();
2666        });
2667    }
2668
2669    #[test_traced]
2670    fn test_get_many_single_section() {
2671        let executor = deterministic::Runner::default();
2672        executor.start(|context| async move {
2673            let cfg = test_cfg(&context);
2674            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2675
2676            for i in 0..5 {
2677                (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
2678            }
2679            assert_eq!(journal.section_len(0).unwrap(), 5);
2680
2681            // Read all 5 items in one call. The reusable buffer is intentionally oversized:
2682            // get_many slices it to the exact length the batch needs.
2683            let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
2684            let mut buf = vec![0u8; 6 * chunk];
2685            let (items, _) = journal
2686                .get_many(0, &[0, 1, 2, 3, 4], &mut buf)
2687                .await
2688                .unwrap();
2689
2690            for (i, item) in items.iter().enumerate() {
2691                assert_eq!(*item, test_digest(i as u64));
2692            }
2693
2694            journal.destroy().await.unwrap();
2695        });
2696    }
2697
2698    #[test_traced]
2699    fn test_get_many_subset() {
2700        // Read a sparse subset of positions.
2701        let executor = deterministic::Runner::default();
2702        executor.start(|context| async move {
2703            let cfg = test_cfg(&context);
2704            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2705
2706            for i in 0..10 {
2707                (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
2708            }
2709            assert_eq!(journal.section_len(0).unwrap(), 10);
2710
2711            let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
2712            let positions = [1, 4, 7, 9];
2713            let mut buf = vec![0u8; positions.len() * chunk];
2714            let (items, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
2715
2716            for (i, &pos) in positions.iter().enumerate() {
2717                assert_eq!(items[i], test_digest(pos));
2718            }
2719
2720            journal.destroy().await.unwrap();
2721        });
2722    }
2723
2724    #[test_traced]
2725    fn test_get_many_bad_section() {
2726        let executor = deterministic::Runner::default();
2727        executor.start(|context| async move {
2728            let cfg = test_cfg(&context);
2729            let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
2730                .await
2731                .unwrap();
2732
2733            let mut buf = vec![0u8; 64];
2734            let err = journal.get_many(99, &[0], &mut buf).await.unwrap_err();
2735            assert!(matches!(err, Error::SectionOutOfRange(99)));
2736
2737            journal.destroy().await.unwrap();
2738        });
2739    }
2740
2741    #[test_traced]
2742    fn test_get_many_matches_get() {
2743        // Verify batch read matches individual reads.
2744        let executor = deterministic::Runner::default();
2745        executor.start(|context| async move {
2746            let cfg = test_cfg(&context);
2747            let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
2748
2749            for i in 0..8 {
2750                (journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
2751            }
2752            assert_eq!(journal.section_len(0).unwrap(), 8);
2753            journal = journal.sync_all().await.unwrap();
2754
2755            let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
2756            let positions: Vec<u64> = (0..8).collect();
2757            let mut buf = vec![0u8; positions.len() * chunk];
2758            let (batch, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
2759
2760            for pos in &positions {
2761                let single = journal.get(0, *pos).await.unwrap();
2762                assert_eq!(batch[*pos as usize], single);
2763            }
2764
2765            journal.destroy().await.unwrap();
2766        });
2767    }
2768
2769    #[test_traced]
2770    fn test_segmented_fixed_prune_waits_for_in_flight_start_sync() {
2771        let executor = deterministic::Runner::default();
2772        executor.start(|context| async move {
2773            let pending = PendingSyncs::default();
2774            let context = DelayedSyncContext {
2775                inner: context,
2776                pending: pending.clone(),
2777            };
2778            let cfg = test_cfg(&context);
2779            let mut journal = Journal::init(context.child("storage"), cfg)
2780                .await
2781                .expect("failed to init");
2782
2783            (journal, _) = journal
2784                .append(1, &test_digest(0))
2785                .await
2786                .expect("failed to append");
2787            let handle;
2788            (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
2789            assert!(!pending.lock().is_empty());
2790
2791            let started = Arc::new(AtomicUsize::new(0));
2792            let completed = Arc::new(AtomicUsize::new(0));
2793            let started_clone = started.clone();
2794            let completed_clone = completed.clone();
2795            let waiter = context.inner.child("prune").spawn(|_| async move {
2796                started_clone.fetch_add(1, Ordering::Relaxed);
2797                let (journal, pruned) = journal.prune(2).await.expect("failed to prune");
2798                assert!(pruned);
2799                completed_clone.fetch_add(1, Ordering::Relaxed);
2800                journal
2801            });
2802
2803            while started.load(Ordering::Relaxed) == 0 {
2804                commonware_runtime::reschedule().await;
2805            }
2806            commonware_runtime::reschedule().await;
2807            assert_eq!(
2808                completed.load(Ordering::Relaxed),
2809                0,
2810                "prune must wait for in-flight syncs on pruned sections"
2811            );
2812
2813            release_pending_syncs(&pending);
2814            handle
2815                .await
2816                .expect("sync handle should complete despite pruning");
2817            while completed.load(Ordering::Relaxed) == 0 {
2818                commonware_runtime::reschedule().await;
2819            }
2820            let journal = waiter.await.expect("prune task failed");
2821            assert_eq!(journal.oldest_section(), None);
2822        });
2823    }
2824
2825    #[test_traced]
2826    fn test_segmented_fixed_destroy_waits_for_in_flight_start_sync() {
2827        let executor = deterministic::Runner::default();
2828        executor.start(|context| async move {
2829            let pending = PendingSyncs::default();
2830            let context = DelayedSyncContext {
2831                inner: context,
2832                pending: pending.clone(),
2833            };
2834            let cfg = test_cfg(&context);
2835            let mut journal = Journal::init(context.child("storage"), cfg)
2836                .await
2837                .expect("failed to init");
2838
2839            (journal, _) = journal
2840                .append(1, &test_digest(0))
2841                .await
2842                .expect("failed to append");
2843            let handle;
2844            (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
2845            assert!(!pending.lock().is_empty());
2846
2847            let started = Arc::new(AtomicUsize::new(0));
2848            let completed = Arc::new(AtomicUsize::new(0));
2849            let started_clone = started.clone();
2850            let completed_clone = completed.clone();
2851            let waiter = context.inner.child("destroy").spawn(|_| async move {
2852                started_clone.fetch_add(1, Ordering::Relaxed);
2853                journal.destroy().await.expect("failed to destroy");
2854                completed_clone.fetch_add(1, Ordering::Relaxed);
2855            });
2856
2857            while started.load(Ordering::Relaxed) == 0 {
2858                commonware_runtime::reschedule().await;
2859            }
2860            commonware_runtime::reschedule().await;
2861            assert_eq!(
2862                completed.load(Ordering::Relaxed),
2863                0,
2864                "destroy must wait for in-flight syncs"
2865            );
2866
2867            release_pending_syncs(&pending);
2868            handle
2869                .await
2870                .expect("sync handle should complete despite destruction");
2871            while completed.load(Ordering::Relaxed) == 0 {
2872                commonware_runtime::reschedule().await;
2873            }
2874            waiter.await.expect("destroy task failed");
2875        });
2876    }
2877
2878    #[test_traced]
2879    fn test_segmented_fixed_clear_waits_for_in_flight_start_sync() {
2880        let executor = deterministic::Runner::default();
2881        executor.start(|context| async move {
2882            let pending = PendingSyncs::default();
2883            let context = DelayedSyncContext {
2884                inner: context,
2885                pending: pending.clone(),
2886            };
2887            let cfg = test_cfg(&context);
2888            let mut journal = Journal::init(context.child("storage"), cfg)
2889                .await
2890                .expect("failed to init");
2891
2892            (journal, _) = journal
2893                .append(1, &test_digest(0))
2894                .await
2895                .expect("failed to append");
2896            let handle;
2897            (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
2898            assert!(!pending.lock().is_empty());
2899
2900            let started = Arc::new(AtomicUsize::new(0));
2901            let completed = Arc::new(AtomicUsize::new(0));
2902            let started_clone = started.clone();
2903            let completed_clone = completed.clone();
2904            let waiter = context.inner.child("clear").spawn(|_| async move {
2905                started_clone.fetch_add(1, Ordering::Relaxed);
2906                journal = journal.clear().await.expect("failed to clear");
2907                completed_clone.fetch_add(1, Ordering::Relaxed);
2908                journal
2909            });
2910
2911            while started.load(Ordering::Relaxed) == 0 {
2912                commonware_runtime::reschedule().await;
2913            }
2914            commonware_runtime::reschedule().await;
2915            assert_eq!(
2916                completed.load(Ordering::Relaxed),
2917                0,
2918                "clear must wait for in-flight syncs"
2919            );
2920
2921            release_pending_syncs(&pending);
2922            handle
2923                .await
2924                .expect("sync handle should complete despite clearing");
2925            while completed.load(Ordering::Relaxed) == 0 {
2926                commonware_runtime::reschedule().await;
2927            }
2928            let mut journal = waiter.await.expect("clear task failed");
2929
2930            // The journal must remain usable after clear.
2931            assert_eq!(journal.oldest_section(), None);
2932            let position;
2933            (journal, position) = journal
2934                .append(1, &test_digest(1))
2935                .await
2936                .expect("failed to append after clear");
2937            assert_eq!(position, 0);
2938            journal.destroy().await.expect("failed to destroy");
2939        });
2940    }
2941
2942    #[test_traced]
2943    fn test_segmented_fixed_rewind_waits_for_in_flight_start_sync() {
2944        let executor = deterministic::Runner::default();
2945        executor.start(|context| async move {
2946            let pending = PendingSyncs::default();
2947            let context = DelayedSyncContext {
2948                inner: context,
2949                pending: pending.clone(),
2950            };
2951            let cfg = test_cfg(&context);
2952            let mut journal = Journal::init(context.child("storage"), cfg)
2953                .await
2954                .expect("failed to init");
2955
2956            (journal, _) = journal
2957                .append(1, &test_digest(0))
2958                .await
2959                .expect("failed to append");
2960            (journal, _) = journal
2961                .append(2, &test_digest(1))
2962                .await
2963                .expect("failed to append");
2964            let handle;
2965            (journal, handle) = journal.start_sync(2).await.expect("failed to start sync");
2966            assert!(!pending.lock().is_empty());
2967
2968            let size = journal.size(1).expect("failed to get size");
2969            let started = Arc::new(AtomicUsize::new(0));
2970            let completed = Arc::new(AtomicUsize::new(0));
2971            let started_clone = started.clone();
2972            let completed_clone = completed.clone();
2973            let waiter = context.inner.child("rewind").spawn(move |_| async move {
2974                started_clone.fetch_add(1, Ordering::Relaxed);
2975                journal = journal.rewind(1, size).await.expect("failed to rewind");
2976                completed_clone.fetch_add(1, Ordering::Relaxed);
2977                journal
2978            });
2979
2980            while started.load(Ordering::Relaxed) == 0 {
2981                commonware_runtime::reschedule().await;
2982            }
2983            commonware_runtime::reschedule().await;
2984            assert_eq!(
2985                completed.load(Ordering::Relaxed),
2986                0,
2987                "rewind must wait for in-flight syncs on removed sections"
2988            );
2989
2990            release_pending_syncs(&pending);
2991            handle
2992                .await
2993                .expect("sync handle should complete despite rewind");
2994            while completed.load(Ordering::Relaxed) == 0 {
2995                commonware_runtime::reschedule().await;
2996            }
2997            let journal = waiter.await.expect("rewind task failed");
2998            assert_eq!(journal.size(2).expect("failed to get size"), 0);
2999            journal.destroy().await.expect("failed to destroy");
3000        });
3001    }
3002
3003    #[test_traced]
3004    fn test_segmented_fixed_prune_surfaces_failed_in_flight_start_sync() {
3005        let executor = deterministic::Runner::default();
3006        executor.start(|context| async move {
3007            let pending = PendingSyncs::default();
3008            let context = DelayedSyncContext {
3009                inner: context,
3010                pending: pending.clone(),
3011            };
3012            let cfg = test_cfg(&context);
3013            let mut journal = Journal::init(context.child("storage"), cfg)
3014                .await
3015                .expect("failed to init");
3016
3017            (journal, _) = journal
3018                .append(1, &test_digest(0))
3019                .await
3020                .expect("failed to append");
3021            let handle;
3022            (journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
3023            fail_pending_syncs(&pending);
3024
3025            let err = journal
3026                .prune(2)
3027                .await
3028                .expect_err("prune must surface a failed in-flight sync");
3029            assert!(matches!(err, Error::Runtime(RError::Io(_))));
3030
3031            let err = handle.await.expect_err("sync handle should fail");
3032            assert!(matches!(err, RError::Io(_)));
3033        });
3034    }
3035}