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