Skip to main content

commonware_storage/journal/segmented/
oversized.rs

1//! Segmented journal for oversized values.
2//!
3//! This module combines [super::fixed::Journal] with [super::glob::Glob] to handle
4//! entries that reference variable-length "oversized" values. It provides coordinated
5//! operations and built-in crash recovery.
6//!
7//! # Architecture
8//!
9//! ```text
10//! +-------------------+     +-------------------+
11//! | Fixed Journal     |     | Glob (Values)     |
12//! | (Index Entries)   |     |                   |
13//! +-------------------+     +-------------------+
14//! | entry_0           | --> | value_0           |
15//! | entry_1           | --> | value_1           |
16//! | ...               |     | ...               |
17//! +-------------------+     +-------------------+
18//! ```
19//!
20//! Each index entry contains `(value_offset, value_size)` pointing to its value in glob.
21//!
22//! # Crash Recovery
23//!
24//! On unclean shutdown, the index journal and glob may have different lengths:
25//! - Index entry pointing to non-existent glob data (dangerous)
26//! - Glob value without index entry (orphan - acceptable but cleaned up)
27//! - Glob sections without corresponding index sections (orphan sections - removed)
28//!
29//! During initialization, crash recovery is performed:
30//! 1. Each index entry's glob reference is validated (`value_offset + value_size <= glob_size`)
31//! 2. Invalid entries are skipped and the index journal is rewound
32//! 3. Orphan value sections (sections in glob but not in index) are removed
33//!
34//! This allows async writes (glob first, then index) while ensuring consistency
35//! after recovery.
36//!
37//! _Recovery only validates that index entries point to valid byte ranges
38//! within the glob. It does **not** verify value checksums during recovery (this would
39//! require reading all values). Value checksums are verified lazily when values are
40//! read via `get_value()`. If the underlying storage is corrupted, `get_value()` will
41//! return a checksum error even though the index entry exists._
42
43use super::{
44    fixed::{Config as FixedConfig, Journal as FixedJournal},
45    glob::{Config as GlobConfig, Glob},
46};
47use crate::journal::Error;
48use commonware_codec::{Codec, CodecFixed, CodecShared};
49use commonware_runtime::{BufferPooler, Handle, Metrics, Storage};
50use futures::{future::try_join, stream::Stream};
51use std::{collections::HashSet, num::NonZeroUsize};
52use tracing::{debug, warn};
53
54/// Trait for index entries that reference oversized values in glob storage.
55///
56/// Implementations must provide access to the value location for crash recovery validation,
57/// and a way to set the location when appending.
58pub trait Record: CodecFixed<Cfg = ()> + Clone {
59    /// Returns `(value_offset, value_size)` for crash recovery validation.
60    fn value_location(&self) -> (u64, u32);
61
62    /// Returns a new entry with the value location set.
63    ///
64    /// Called during `append` after the value is written to glob storage.
65    fn with_location(self, offset: u64, size: u32) -> Self;
66}
67
68/// Configuration for oversized journal.
69#[derive(Clone)]
70pub struct Config<C> {
71    /// Partition for the fixed index journal.
72    pub index_partition: String,
73
74    /// Partition for the glob value storage.
75    pub value_partition: String,
76
77    /// Page cache for index journal caching.
78    pub index_page_cache: commonware_runtime::buffer::paged::CacheRef,
79
80    /// Write buffer size for the index journal.
81    pub index_write_buffer: NonZeroUsize,
82
83    /// Write buffer size for the value journal.
84    pub value_write_buffer: NonZeroUsize,
85
86    /// Optional compression level for values (using zstd).
87    pub compression: Option<u8>,
88
89    /// Codec configuration for values.
90    pub codec_config: C,
91}
92
93/// Segmented journal for entries with oversized values.
94///
95/// Combines a fixed-size index journal with glob storage for variable-length values.
96/// Provides coordinated operations and crash recovery.
97pub struct Oversized<E: BufferPooler + Storage + Metrics, I: Record, V: Codec> {
98    index: FixedJournal<E, I>,
99    values: Glob<E, V>,
100}
101
102impl<E: BufferPooler + Storage + Metrics, I: Record + Send + Sync, V: CodecShared>
103    Oversized<E, I, V>
104{
105    /// Initialize with crash recovery validation.
106    ///
107    /// Validates each index entry's glob reference during replay. Invalid entries
108    /// (pointing beyond glob size) are skipped, and the index journal is rewound
109    /// to exclude trailing invalid entries.
110    pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
111        // Initialize both journals
112        let index_cfg = FixedConfig {
113            partition: cfg.index_partition,
114            page_cache: cfg.index_page_cache,
115            write_buffer: cfg.index_write_buffer,
116        };
117        let index = FixedJournal::init(context.child("index"), index_cfg).await?;
118
119        let value_cfg = GlobConfig {
120            partition: cfg.value_partition,
121            compression: cfg.compression,
122            codec_config: cfg.codec_config,
123            write_buffer: cfg.value_write_buffer,
124        };
125        let values = Glob::init(context.child("values"), value_cfg).await?;
126
127        let mut oversized = Self { index, values };
128
129        // Perform crash recovery validation
130        oversized.recover().await?;
131
132        Ok(oversized)
133    }
134
135    /// Perform crash recovery by validating index entries against glob sizes.
136    ///
137    /// Only checks the last entry in each section. Since entries are appended sequentially
138    /// and value offsets are monotonically increasing within a section, if the last entry
139    /// is valid then all earlier entries must be valid too.
140    async fn recover(&mut self) -> Result<(), Error> {
141        let chunk_size = FixedJournal::<E, I>::CHUNK_SIZE as u64;
142        let sections: Vec<u64> = self.index.sections().collect();
143
144        for section in sections {
145            let index_size = self.index.size(section)?;
146            if index_size == 0 {
147                continue;
148            }
149
150            let glob_size = match self.values.size(section) {
151                Ok(size) => size,
152                Err(Error::AlreadyPrunedToSection(oldest)) => {
153                    // This shouldn't happen in normal operation: prune() prunes the index
154                    // first, then the glob. A crash between these would leave the glob
155                    // NOT pruned (opposite of this case). We handle this defensively in
156                    // case of external manipulation or future changes.
157                    warn!(
158                        section,
159                        oldest, "index has section that glob already pruned"
160                    );
161                    0
162                }
163                Err(e) => return Err(e),
164            };
165
166            // Truncate any trailing partial entry
167            let entry_count = index_size / chunk_size;
168            let aligned_size = entry_count * chunk_size;
169            if aligned_size < index_size {
170                warn!(
171                    section,
172                    index_size, aligned_size, "trailing bytes detected: truncating"
173                );
174                self.index.rewind_section(section, aligned_size).await?;
175            }
176
177            // If there is nothing, we can exit early and rewind values to 0
178            if entry_count == 0 {
179                warn!(
180                    section,
181                    index_size, "trailing bytes detected: truncating to 0"
182                );
183                self.values.rewind_section(section, 0).await?;
184                continue;
185            }
186
187            // Find last valid entry and target glob size
188            let (valid_count, glob_target) = self
189                .find_last_valid_entry(section, entry_count, glob_size)
190                .await;
191
192            // Rewind index if any entries are invalid
193            if valid_count < entry_count {
194                let valid_size = valid_count * chunk_size;
195                debug!(section, entry_count, valid_count, "rewinding index");
196                self.index.rewind_section(section, valid_size).await?;
197            }
198
199            // Truncate glob trailing garbage (can occur when value was written but
200            // index entry wasn't, or when index was truncated but glob wasn't)
201            if glob_size > glob_target {
202                debug!(
203                    section,
204                    glob_size, glob_target, "truncating glob trailing garbage"
205                );
206                self.values.rewind_section(section, glob_target).await?;
207            }
208        }
209
210        // Clean up orphan value sections that don't exist in index
211        self.cleanup_orphan_value_sections().await?;
212
213        Ok(())
214    }
215
216    /// Remove any value sections that don't have corresponding index sections.
217    ///
218    /// This can happen if a crash occurs after writing to values but before
219    /// writing to index for a new section. Since sections don't have to be
220    /// contiguous, we compare the actual sets of sections rather than just
221    /// comparing the newest section numbers.
222    async fn cleanup_orphan_value_sections(&mut self) -> Result<(), Error> {
223        // Collect index sections into a set for O(1) lookup
224        let index_sections: HashSet<u64> = self.index.sections().collect();
225
226        // Find value sections that don't exist in index
227        let orphan_sections: Vec<u64> = self
228            .values
229            .sections()
230            .filter(|s| !index_sections.contains(s))
231            .collect();
232
233        // Remove each orphan section
234        for section in orphan_sections {
235            warn!(section, "removing orphan value section");
236            self.values.remove_section(section).await?;
237        }
238
239        Ok(())
240    }
241
242    /// Find the number of valid entries and the corresponding glob target size.
243    ///
244    /// Scans backwards from the last entry until a valid one is found.
245    /// Returns `(valid_count, glob_target)` where `glob_target` is the end offset
246    /// of the last valid entry's value.
247    async fn find_last_valid_entry(
248        &self,
249        section: u64,
250        entry_count: u64,
251        glob_size: u64,
252    ) -> (u64, u64) {
253        for pos in (0..entry_count).rev() {
254            match self.index.get(section, pos).await {
255                Ok(entry) => {
256                    let (offset, size) = entry.value_location();
257                    let entry_end = offset.saturating_add(u64::from(size));
258                    if entry_end <= glob_size {
259                        return (pos + 1, entry_end);
260                    }
261                    if pos == entry_count - 1 {
262                        warn!(
263                            section,
264                            pos, glob_size, entry_end, "invalid entry: glob truncated"
265                        );
266                    }
267                }
268                Err(_) => {
269                    if pos == entry_count - 1 {
270                        warn!(section, pos, "corrupted last entry, scanning backwards");
271                    }
272                }
273            }
274        }
275        (0, 0)
276    }
277
278    /// Append entry + value.
279    ///
280    /// Writes value to glob first, then writes index entry with the value location.
281    ///
282    /// Returns `(position, offset, size)` where:
283    /// - `position`: Position in the index journal
284    /// - `offset`: Byte offset in glob
285    /// - `size`: Size of value in glob (including checksum)
286    pub async fn append(
287        &mut self,
288        section: u64,
289        entry: I,
290        value: &V,
291    ) -> Result<(u64, u64, u32), Error> {
292        // Write value first (glob). This will typically write to an in-memory
293        // buffer and return quickly (only blocks when the buffer is full).
294        let (offset, size) = self.values.append(section, value).await?;
295
296        // Update entry with actual location and write to index
297        let entry_with_location = entry.with_location(offset, size);
298        let position = self.index.append(section, &entry_with_location).await?;
299
300        Ok((position, offset, size))
301    }
302
303    /// Get entry at position (index entry only, not value).
304    pub async fn get(&self, section: u64, position: u64) -> Result<I, Error> {
305        self.index.get(section, position).await
306    }
307
308    /// Get the last entry for a section, if any.
309    ///
310    /// Returns `Ok(None)` if the section is empty.
311    ///
312    /// # Errors
313    ///
314    /// - [Error::AlreadyPrunedToSection] if the section has been pruned.
315    /// - [Error::SectionOutOfRange] if the section doesn't exist.
316    pub async fn last(&self, section: u64) -> Result<Option<I>, Error> {
317        self.index.last(section).await
318    }
319
320    /// Get value using offset/size from entry.
321    ///
322    /// The offset should be the byte offset from `append()` or from the entry's `value_location()`.
323    pub async fn get_value(&self, section: u64, offset: u64, size: u32) -> Result<V, Error> {
324        self.values.get(section, offset, size).await
325    }
326
327    /// Replay index entries starting from given section.
328    ///
329    /// Returns a stream of `(section, position, entry)` tuples.
330    pub async fn replay(
331        &mut self,
332        start_section: u64,
333        start_position: u64,
334        buffer: NonZeroUsize,
335    ) -> Result<impl Stream<Item = Result<(u64, u64, I), Error>> + Send + '_, Error> {
336        self.index
337            .replay(start_section, start_position, buffer)
338            .await
339    }
340
341    /// Sync both journals for the given `sections`.
342    pub async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
343        let sections = sections.sections().collect::<Vec<_>>();
344        try_join(self.index.sync(&sections), self.values.sync(&sections))
345            .await
346            .map(|_| ())
347    }
348
349    /// Start syncing both journals for the given `sections`.
350    ///
351    /// The returned handle completes once both journals' syncs complete, failing with the first
352    /// error encountered.
353    pub async fn start_sync(
354        &mut self,
355        sections: impl crate::Sections,
356    ) -> Result<Handle<()>, Error> {
357        let sections = sections.sections().collect::<Vec<_>>();
358        let (index, values) = try_join(
359            self.index.start_sync(&sections),
360            self.values.start_sync(&sections),
361        )
362        .await?;
363        Ok(Handle::from_future(async move {
364            try_join(index, values).await.map(|_| ())
365        }))
366    }
367
368    /// Sync all sections.
369    pub async fn sync_all(&mut self) -> Result<(), Error> {
370        try_join(self.index.sync_all(), self.values.sync_all())
371            .await
372            .map(|_| ())
373    }
374
375    /// Prune both journals. Returns true if any sections were pruned.
376    ///
377    /// Prunes index first, then glob. This order ensures crash safety:
378    /// - If crash after index prune but before glob: orphan data in glob (acceptable)
379    /// - If crash before index prune: no change, retry works
380    pub async fn prune(&mut self, min: u64) -> Result<bool, Error> {
381        let index_pruned = self.index.prune(min).await?;
382        let value_pruned = self.values.prune(min).await?;
383        Ok(index_pruned || value_pruned)
384    }
385
386    /// Rewind both journals to a specific section and index size.
387    ///
388    /// This rewinds the section to the given index size and removes all sections
389    /// after the given section. The value size is derived from the last entry.
390    pub async fn rewind(&mut self, section: u64, index_size: u64) -> Result<(), Error> {
391        // Rewind index first (this also removes sections after `section`)
392        self.index.rewind(section, index_size).await?;
393
394        // Derive value size from last entry (section may not exist if empty)
395        let value_size = match self.index.last(section).await {
396            Ok(Some(entry)) => {
397                let (offset, size) = entry.value_location();
398                offset
399                    .checked_add(u64::from(size))
400                    .ok_or(Error::OffsetOverflow)?
401            }
402            Ok(None) => 0,
403            Err(Error::SectionOutOfRange(_)) if index_size == 0 => 0,
404            Err(e) => return Err(e),
405        };
406
407        // Rewind values (this also removes sections after `section`)
408        self.values.rewind(section, value_size).await
409    }
410
411    /// Rewind only the given section to a specific index size.
412    ///
413    /// Unlike `rewind`, this does not affect other sections.
414    /// The value size is derived from the last entry after rewinding the index.
415    pub async fn rewind_section(&mut self, section: u64, index_size: u64) -> Result<(), Error> {
416        // Rewind index first
417        self.index.rewind_section(section, index_size).await?;
418
419        // Derive value size from last entry (section may not exist if empty)
420        let value_size = match self.index.last(section).await {
421            Ok(Some(entry)) => {
422                let (offset, size) = entry.value_location();
423                offset
424                    .checked_add(u64::from(size))
425                    .ok_or(Error::OffsetOverflow)?
426            }
427            Ok(None) => 0,
428            Err(Error::SectionOutOfRange(_)) if index_size == 0 => 0,
429            Err(e) => return Err(e),
430        };
431
432        // Rewind values
433        self.values.rewind_section(section, value_size).await
434    }
435
436    /// Get index size for checkpoint.
437    ///
438    /// The value size can be derived from the last entry's location when needed.
439    pub fn size(&self, section: u64) -> Result<u64, Error> {
440        self.index.size(section)
441    }
442
443    /// Get the value size for a section, derived from the last entry's location.
444    pub async fn value_size(&self, section: u64) -> Result<u64, Error> {
445        match self.index.last(section).await {
446            Ok(Some(entry)) => {
447                let (offset, size) = entry.value_location();
448                offset
449                    .checked_add(u64::from(size))
450                    .ok_or(Error::OffsetOverflow)
451            }
452            Ok(None) | Err(Error::SectionOutOfRange(_)) => Ok(0),
453            Err(e) => Err(e),
454        }
455    }
456
457    /// Returns the oldest section number, if any exist.
458    pub fn oldest_section(&self) -> Option<u64> {
459        self.index.oldest_section()
460    }
461
462    /// Returns the newest section number, if any exist.
463    pub fn newest_section(&self) -> Option<u64> {
464        self.index.newest_section()
465    }
466
467    /// Destroy all underlying storage.
468    pub async fn destroy(self) -> Result<(), Error> {
469        try_join(self.index.destroy(), self.values.destroy())
470            .await
471            .map(|_| ())
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use commonware_codec::{FixedSize, Read, ReadExt, Write};
479    use commonware_cryptography::Crc32;
480    use commonware_macros::test_traced;
481    use commonware_runtime::{
482        buffer::paged::CacheRef, deterministic, Blob as _, Buf, BufMut, BufferPooler, Runner,
483        Supervisor as _,
484    };
485    use commonware_utils::{NZUsize, NZU16};
486
487    /// Convert offset + size to byte end position (for truncation tests).
488    fn byte_end(offset: u64, size: u32) -> u64 {
489        offset + u64::from(size)
490    }
491
492    /// Test index entry that stores a u64 id and references a value.
493    #[derive(Debug, Clone, PartialEq)]
494    struct TestEntry {
495        id: u64,
496        value_offset: u64,
497        value_size: u32,
498    }
499
500    impl TestEntry {
501        fn new(id: u64, value_offset: u64, value_size: u32) -> Self {
502            Self {
503                id,
504                value_offset,
505                value_size,
506            }
507        }
508    }
509
510    impl Write for TestEntry {
511        fn write(&self, buf: &mut impl BufMut) {
512            self.id.write(buf);
513            self.value_offset.write(buf);
514            self.value_size.write(buf);
515        }
516    }
517
518    impl Read for TestEntry {
519        type Cfg = ();
520
521        fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
522            let id = u64::read(buf)?;
523            let value_offset = u64::read(buf)?;
524            let value_size = u32::read(buf)?;
525            Ok(Self {
526                id,
527                value_offset,
528                value_size,
529            })
530        }
531    }
532
533    impl FixedSize for TestEntry {
534        const SIZE: usize = u64::SIZE + u64::SIZE + u32::SIZE;
535    }
536
537    impl Record for TestEntry {
538        fn value_location(&self) -> (u64, u32) {
539            (self.value_offset, self.value_size)
540        }
541
542        fn with_location(mut self, offset: u64, size: u32) -> Self {
543            self.value_offset = offset;
544            self.value_size = size;
545            self
546        }
547    }
548
549    fn test_cfg(pooler: &impl BufferPooler) -> Config<()> {
550        Config {
551            index_partition: "test-index".into(),
552            value_partition: "test-values".into(),
553            index_page_cache: CacheRef::from_pooler(pooler, NZU16!(64), NZUsize!(8)),
554            index_write_buffer: NZUsize!(1024),
555            value_write_buffer: NZUsize!(1024),
556            compression: None,
557            codec_config: (),
558        }
559    }
560
561    /// Simple test value type with unit config.
562    type TestValue = [u8; 16];
563
564    #[test_traced]
565    fn test_oversized_append_and_get() {
566        let executor = deterministic::Runner::default();
567        executor.start(|context| async move {
568            let cfg = test_cfg(&context);
569            let mut oversized: Oversized<_, TestEntry, TestValue> =
570                Oversized::init(context, cfg).await.expect("Failed to init");
571
572            // Append entry with value
573            let value: TestValue = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
574            let entry = TestEntry::new(42, 0, 0);
575            let (position, offset, size) = oversized
576                .append(1, entry, &value)
577                .await
578                .expect("Failed to append");
579
580            assert_eq!(position, 0);
581
582            // Get entry
583            let retrieved_entry = oversized.get(1, position).await.expect("Failed to get");
584            assert_eq!(retrieved_entry.id, 42);
585
586            // Get value
587            let retrieved_value = oversized
588                .get_value(1, offset, size)
589                .await
590                .expect("Failed to get value");
591            assert_eq!(retrieved_value, value);
592
593            oversized.destroy().await.expect("Failed to destroy");
594        });
595    }
596
597    #[test_traced]
598    fn test_oversized_crash_recovery() {
599        let executor = deterministic::Runner::default();
600        executor.start(|context| async move {
601            let cfg = test_cfg(&context);
602
603            // Create and populate oversized journal
604            let mut oversized: Oversized<_, TestEntry, TestValue> =
605                Oversized::init(context.child("first"), cfg.clone())
606                    .await
607                    .expect("Failed to init");
608
609            // Append multiple entries
610            let mut locations = Vec::new();
611            for i in 0..5u8 {
612                let value: TestValue = [i; 16];
613                let entry = TestEntry::new(i as u64, 0, 0);
614                let (position, offset, size) = oversized
615                    .append(1, entry, &value)
616                    .await
617                    .expect("Failed to append");
618                locations.push((position, offset, size));
619            }
620            oversized.sync(1).await.expect("Failed to sync");
621            drop(oversized);
622
623            // Simulate crash: truncate glob to lose last 2 values
624            let (blob, _) = context
625                .open(&cfg.value_partition, &1u64.to_be_bytes())
626                .await
627                .expect("Failed to open blob");
628
629            // Calculate size to keep first 3 entries
630            let keep_size = byte_end(locations[2].1, locations[2].2);
631            blob.resize(keep_size).await.expect("Failed to truncate");
632            blob.sync().await.expect("Failed to sync");
633            drop(blob);
634
635            // Reinitialize - should recover and rewind index
636            let oversized: Oversized<_, TestEntry, TestValue> =
637                Oversized::init(context.child("second"), cfg.clone())
638                    .await
639                    .expect("Failed to reinit");
640
641            // First 3 entries should still be valid
642            for i in 0..3u8 {
643                let (position, offset, size) = locations[i as usize];
644                let entry = oversized.get(1, position).await.expect("Failed to get");
645                assert_eq!(entry.id, i as u64);
646
647                let value = oversized
648                    .get_value(1, offset, size)
649                    .await
650                    .expect("Failed to get value");
651                assert_eq!(value, [i; 16]);
652            }
653
654            // Entry at position 3 should fail (index was rewound)
655            let result = oversized.get(1, 3).await;
656            assert!(result.is_err());
657
658            oversized.destroy().await.expect("Failed to destroy");
659        });
660    }
661
662    #[test_traced]
663    fn test_oversized_persistence() {
664        let executor = deterministic::Runner::default();
665        executor.start(|context| async move {
666            let cfg = test_cfg(&context);
667
668            // Create and populate
669            let mut oversized: Oversized<_, TestEntry, TestValue> =
670                Oversized::init(context.child("first"), cfg.clone())
671                    .await
672                    .expect("Failed to init");
673
674            let value: TestValue = [42; 16];
675            let entry = TestEntry::new(123, 0, 0);
676            let (position, offset, size) = oversized
677                .append(1, entry, &value)
678                .await
679                .expect("Failed to append");
680            oversized.sync(1).await.expect("Failed to sync");
681            drop(oversized);
682
683            // Reopen and verify
684            let oversized: Oversized<_, TestEntry, TestValue> =
685                Oversized::init(context.child("second"), cfg)
686                    .await
687                    .expect("Failed to reinit");
688
689            let retrieved_entry = oversized.get(1, position).await.expect("Failed to get");
690            assert_eq!(retrieved_entry.id, 123);
691
692            let retrieved_value = oversized
693                .get_value(1, offset, size)
694                .await
695                .expect("Failed to get value");
696            assert_eq!(retrieved_value, value);
697
698            oversized.destroy().await.expect("Failed to destroy");
699        });
700    }
701
702    #[test_traced]
703    fn test_oversized_sync() {
704        let executor = deterministic::Runner::default();
705        executor.start(|context| async move {
706            let cfg = test_cfg(&context);
707
708            let mut oversized: Oversized<_, TestEntry, TestValue> =
709                Oversized::init(context.child("first"), cfg.clone())
710                    .await
711                    .expect("Failed to init");
712
713            // One sub-page entry/value per section stays buffered until synced.
714            let mut located = Vec::new();
715            for section in 1u64..=3 {
716                let value: TestValue = [section as u8; 16];
717                let entry = TestEntry::new(section, 0, 0);
718                let (position, offset, size) = oversized
719                    .append(section, entry, &value)
720                    .await
721                    .expect("Failed to append");
722                located.push((section, position, offset, size, value));
723            }
724
725            // Sync sections 1 and 3 (both index and values); a nonexistent section (99) is
726            // skipped, not an error.
727            oversized
728                .sync(&[1, 3, 99])
729                .await
730                .expect("Failed to sync sections");
731            drop(oversized);
732
733            // Only the synced sections survive the unclean drop, with both index and value durable.
734            let oversized: Oversized<_, TestEntry, TestValue> =
735                Oversized::init(context.child("second"), cfg)
736                    .await
737                    .expect("Failed to reinit");
738            for &(section, position, offset, size, value) in &located {
739                let result = oversized.get(section, position).await;
740                if section == 2 {
741                    assert!(result.is_err(), "unsynced section 2 must not be durable");
742                    continue;
743                }
744                assert_eq!(result.expect("synced entry durable").id, section);
745                let retrieved = oversized
746                    .get_value(section, offset, size)
747                    .await
748                    .expect("synced value durable");
749                assert_eq!(retrieved, value);
750            }
751
752            oversized.destroy().await.expect("Failed to destroy");
753        });
754    }
755
756    #[test_traced]
757    fn test_oversized_prune() {
758        let executor = deterministic::Runner::default();
759        executor.start(|context| async move {
760            let cfg = test_cfg(&context);
761            let mut oversized: Oversized<_, TestEntry, TestValue> =
762                Oversized::init(context, cfg).await.expect("Failed to init");
763
764            // Append to multiple sections
765            for section in 1u64..=5 {
766                let value: TestValue = [section as u8; 16];
767                let entry = TestEntry::new(section, 0, 0);
768                oversized
769                    .append(section, entry, &value)
770                    .await
771                    .expect("Failed to append");
772                oversized.sync(section).await.expect("Failed to sync");
773            }
774
775            // Prune sections < 3
776            oversized.prune(3).await.expect("Failed to prune");
777
778            // Sections 1, 2 should be gone
779            assert!(oversized.get(1, 0).await.is_err());
780            assert!(oversized.get(2, 0).await.is_err());
781
782            // Sections 3, 4, 5 should exist
783            assert!(oversized.get(3, 0).await.is_ok());
784            assert!(oversized.get(4, 0).await.is_ok());
785            assert!(oversized.get(5, 0).await.is_ok());
786
787            oversized.destroy().await.expect("Failed to destroy");
788        });
789    }
790
791    #[test_traced]
792    fn test_recovery_empty_section() {
793        let executor = deterministic::Runner::default();
794        executor.start(|context| async move {
795            let cfg = test_cfg(&context);
796
797            // Create oversized journal
798            let mut oversized: Oversized<_, TestEntry, TestValue> =
799                Oversized::init(context.child("first"), cfg.clone())
800                    .await
801                    .expect("Failed to init");
802
803            // Append to section 2 only (section 1 remains empty after being opened)
804            let value: TestValue = [42; 16];
805            let entry = TestEntry::new(1, 0, 0);
806            oversized
807                .append(2, entry, &value)
808                .await
809                .expect("Failed to append");
810            oversized.sync(2).await.expect("Failed to sync");
811            drop(oversized);
812
813            // Reinitialize - recovery should handle the empty/non-existent section 1
814            let oversized: Oversized<_, TestEntry, TestValue> =
815                Oversized::init(context.child("second"), cfg)
816                    .await
817                    .expect("Failed to reinit");
818
819            // Section 2 entry should be valid
820            let entry = oversized.get(2, 0).await.expect("Failed to get");
821            assert_eq!(entry.id, 1);
822
823            oversized.destroy().await.expect("Failed to destroy");
824        });
825    }
826
827    #[test_traced]
828    fn test_recovery_all_entries_invalid() {
829        let executor = deterministic::Runner::default();
830        executor.start(|context| async move {
831            let cfg = test_cfg(&context);
832
833            // Create and populate
834            let mut oversized: Oversized<_, TestEntry, TestValue> =
835                Oversized::init(context.child("first"), cfg.clone())
836                    .await
837                    .expect("Failed to init");
838
839            // Append 5 entries
840            for i in 0..5u8 {
841                let value: TestValue = [i; 16];
842                let entry = TestEntry::new(i as u64, 0, 0);
843                oversized
844                    .append(1, entry, &value)
845                    .await
846                    .expect("Failed to append");
847            }
848            oversized.sync(1).await.expect("Failed to sync");
849            drop(oversized);
850
851            // Truncate glob to 0 bytes - ALL entries become invalid
852            let (blob, _) = context
853                .open(&cfg.value_partition, &1u64.to_be_bytes())
854                .await
855                .expect("Failed to open blob");
856            blob.resize(0).await.expect("Failed to truncate");
857            blob.sync().await.expect("Failed to sync");
858            drop(blob);
859
860            // Reinitialize - should recover and rewind index to 0
861            let mut oversized: Oversized<_, TestEntry, TestValue> =
862                Oversized::init(context.child("second"), cfg)
863                    .await
864                    .expect("Failed to reinit");
865
866            // No entries should be accessible
867            let result = oversized.get(1, 0).await;
868            assert!(result.is_err());
869
870            // Should be able to append after recovery
871            let value: TestValue = [99; 16];
872            let entry = TestEntry::new(100, 0, 0);
873            let (pos, offset, size) = oversized
874                .append(1, entry, &value)
875                .await
876                .expect("Failed to append after recovery");
877            assert_eq!(pos, 0);
878
879            let retrieved = oversized.get(1, 0).await.expect("Failed to get");
880            assert_eq!(retrieved.id, 100);
881            let retrieved_value = oversized
882                .get_value(1, offset, size)
883                .await
884                .expect("Failed to get value");
885            assert_eq!(retrieved_value, value);
886
887            oversized.destroy().await.expect("Failed to destroy");
888        });
889    }
890
891    #[test_traced]
892    fn test_recovery_multiple_sections_mixed_validity() {
893        let executor = deterministic::Runner::default();
894        executor.start(|context| async move {
895            let cfg = test_cfg(&context);
896
897            // Create and populate multiple sections
898            let mut oversized: Oversized<_, TestEntry, TestValue> =
899                Oversized::init(context.child("first"), cfg.clone())
900                    .await
901                    .expect("Failed to init");
902
903            // Section 1: 3 entries
904            let mut section1_locations = Vec::new();
905            for i in 0..3u8 {
906                let value: TestValue = [i; 16];
907                let entry = TestEntry::new(i as u64, 0, 0);
908                let loc = oversized
909                    .append(1, entry, &value)
910                    .await
911                    .expect("Failed to append");
912                section1_locations.push(loc);
913            }
914            oversized.sync(1).await.expect("Failed to sync");
915
916            // Section 2: 5 entries
917            let mut section2_locations = Vec::new();
918            for i in 0..5u8 {
919                let value: TestValue = [10 + i; 16];
920                let entry = TestEntry::new(10 + i as u64, 0, 0);
921                let loc = oversized
922                    .append(2, entry, &value)
923                    .await
924                    .expect("Failed to append");
925                section2_locations.push(loc);
926            }
927            oversized.sync(2).await.expect("Failed to sync");
928
929            // Section 3: 2 entries
930            for i in 0..2u8 {
931                let value: TestValue = [20 + i; 16];
932                let entry = TestEntry::new(20 + i as u64, 0, 0);
933                oversized
934                    .append(3, entry, &value)
935                    .await
936                    .expect("Failed to append");
937            }
938            oversized.sync(3).await.expect("Failed to sync");
939            drop(oversized);
940
941            // Truncate section 1 glob to keep only first entry
942            let (blob, _) = context
943                .open(&cfg.value_partition, &1u64.to_be_bytes())
944                .await
945                .expect("Failed to open blob");
946            let keep_size = byte_end(section1_locations[0].1, section1_locations[0].2);
947            blob.resize(keep_size).await.expect("Failed to truncate");
948            blob.sync().await.expect("Failed to sync");
949            drop(blob);
950
951            // Truncate section 2 glob to keep first 3 entries
952            let (blob, _) = context
953                .open(&cfg.value_partition, &2u64.to_be_bytes())
954                .await
955                .expect("Failed to open blob");
956            let keep_size = byte_end(section2_locations[2].1, section2_locations[2].2);
957            blob.resize(keep_size).await.expect("Failed to truncate");
958            blob.sync().await.expect("Failed to sync");
959            drop(blob);
960
961            // Section 3 remains intact
962
963            // Reinitialize
964            let oversized: Oversized<_, TestEntry, TestValue> =
965                Oversized::init(context.child("second"), cfg)
966                    .await
967                    .expect("Failed to reinit");
968
969            // Section 1: only position 0 valid
970            assert!(oversized.get(1, 0).await.is_ok());
971            assert!(oversized.get(1, 1).await.is_err());
972            assert!(oversized.get(1, 2).await.is_err());
973
974            // Section 2: positions 0,1,2 valid
975            assert!(oversized.get(2, 0).await.is_ok());
976            assert!(oversized.get(2, 1).await.is_ok());
977            assert!(oversized.get(2, 2).await.is_ok());
978            assert!(oversized.get(2, 3).await.is_err());
979            assert!(oversized.get(2, 4).await.is_err());
980
981            // Section 3: both positions valid
982            assert!(oversized.get(3, 0).await.is_ok());
983            assert!(oversized.get(3, 1).await.is_ok());
984
985            oversized.destroy().await.expect("Failed to destroy");
986        });
987    }
988
989    #[test_traced]
990    fn test_recovery_corrupted_last_index_entry() {
991        let executor = deterministic::Runner::default();
992        executor.start(|context| async move {
993            // Use page size = entry size so each entry is on its own page.
994            // This allows corrupting just the last entry's page without affecting others.
995            // Physical page size = TestEntry::SIZE (20) + 12 (CRC record) = 32 bytes.
996            let cfg = Config {
997                index_partition: "test-index".into(),
998                value_partition: "test-values".into(),
999                index_page_cache: CacheRef::from_pooler(
1000                    &context,
1001                    NZU16!(TestEntry::SIZE as u16),
1002                    NZUsize!(8),
1003                ),
1004                index_write_buffer: NZUsize!(1024),
1005                value_write_buffer: NZUsize!(1024),
1006                compression: None,
1007                codec_config: (),
1008            };
1009
1010            // Create and populate
1011            let mut oversized: Oversized<_, TestEntry, TestValue> =
1012                Oversized::init(context.child("first"), cfg.clone())
1013                    .await
1014                    .expect("Failed to init");
1015
1016            // Append 5 entries (each on its own page)
1017            for i in 0..5u8 {
1018                let value: TestValue = [i; 16];
1019                let entry = TestEntry::new(i as u64, 0, 0);
1020                oversized
1021                    .append(1, entry, &value)
1022                    .await
1023                    .expect("Failed to append");
1024            }
1025            oversized.sync(1).await.expect("Failed to sync");
1026            drop(oversized);
1027
1028            // Corrupt the last page's CRC to trigger page-level integrity failure
1029            let (blob, size) = context
1030                .open(&cfg.index_partition, &1u64.to_be_bytes())
1031                .await
1032                .expect("Failed to open blob");
1033
1034            // Physical page size = 20 + 12 = 32 bytes
1035            // 5 entries = 5 pages = 160 bytes total
1036            // Last page CRC starts at offset 160 - 12 = 148
1037            assert_eq!(size, 160);
1038            let last_page_crc_offset = size - 12;
1039            blob.write_at_sync(last_page_crc_offset, vec![0xFF; 12])
1040                .await
1041                .expect("Failed to corrupt");
1042            drop(blob);
1043
1044            // Reinitialize - should detect page corruption and truncate
1045            let mut oversized: Oversized<_, TestEntry, TestValue> =
1046                Oversized::init(context.child("second"), cfg)
1047                    .await
1048                    .expect("Failed to reinit");
1049
1050            // First 4 entries should be valid (on pages 0-3)
1051            for i in 0..4u8 {
1052                let entry = oversized.get(1, i as u64).await.expect("Failed to get");
1053                assert_eq!(entry.id, i as u64);
1054            }
1055
1056            // Entry 4 should be gone (its page was corrupted)
1057            assert!(oversized.get(1, 4).await.is_err());
1058
1059            // Should be able to append after recovery
1060            let value: TestValue = [99; 16];
1061            let entry = TestEntry::new(100, 0, 0);
1062            let (pos, offset, size) = oversized
1063                .append(1, entry, &value)
1064                .await
1065                .expect("Failed to append after recovery");
1066            assert_eq!(pos, 4);
1067
1068            let retrieved = oversized.get(1, 4).await.expect("Failed to get");
1069            assert_eq!(retrieved.id, 100);
1070            let retrieved_value = oversized
1071                .get_value(1, offset, size)
1072                .await
1073                .expect("Failed to get value");
1074            assert_eq!(retrieved_value, value);
1075
1076            oversized.destroy().await.expect("Failed to destroy");
1077        });
1078    }
1079
1080    #[test_traced]
1081    fn test_recovery_all_entries_valid() {
1082        let executor = deterministic::Runner::default();
1083        executor.start(|context| async move {
1084            let cfg = test_cfg(&context);
1085
1086            // Create and populate
1087            let mut oversized: Oversized<_, TestEntry, TestValue> =
1088                Oversized::init(context.child("first"), cfg.clone())
1089                    .await
1090                    .expect("Failed to init");
1091
1092            // Append entries to multiple sections
1093            for section in 1u64..=3 {
1094                for i in 0..10u8 {
1095                    let value: TestValue = [(section as u8) * 10 + i; 16];
1096                    let entry = TestEntry::new(section * 100 + i as u64, 0, 0);
1097                    oversized
1098                        .append(section, entry, &value)
1099                        .await
1100                        .expect("Failed to append");
1101                }
1102                oversized.sync(section).await.expect("Failed to sync");
1103            }
1104            drop(oversized);
1105
1106            // Reinitialize with no corruption - should be fast
1107            let oversized: Oversized<_, TestEntry, TestValue> =
1108                Oversized::init(context.child("second"), cfg)
1109                    .await
1110                    .expect("Failed to reinit");
1111
1112            // All entries should be valid
1113            for section in 1u64..=3 {
1114                for i in 0..10u8 {
1115                    let entry = oversized
1116                        .get(section, i as u64)
1117                        .await
1118                        .expect("Failed to get");
1119                    assert_eq!(entry.id, section * 100 + i as u64);
1120                }
1121            }
1122
1123            oversized.destroy().await.expect("Failed to destroy");
1124        });
1125    }
1126
1127    #[test_traced]
1128    fn test_recovery_single_entry_invalid() {
1129        let executor = deterministic::Runner::default();
1130        executor.start(|context| async move {
1131            let cfg = test_cfg(&context);
1132
1133            // Create and populate with single entry
1134            let mut oversized: Oversized<_, TestEntry, TestValue> =
1135                Oversized::init(context.child("first"), cfg.clone())
1136                    .await
1137                    .expect("Failed to init");
1138
1139            let value: TestValue = [42; 16];
1140            let entry = TestEntry::new(1, 0, 0);
1141            oversized
1142                .append(1, entry, &value)
1143                .await
1144                .expect("Failed to append");
1145            oversized.sync(1).await.expect("Failed to sync");
1146            drop(oversized);
1147
1148            // Truncate glob to 0 - single entry becomes invalid
1149            let (blob, _) = context
1150                .open(&cfg.value_partition, &1u64.to_be_bytes())
1151                .await
1152                .expect("Failed to open blob");
1153            blob.resize(0).await.expect("Failed to truncate");
1154            blob.sync().await.expect("Failed to sync");
1155            drop(blob);
1156
1157            // Reinitialize
1158            let oversized: Oversized<_, TestEntry, TestValue> =
1159                Oversized::init(context.child("second"), cfg)
1160                    .await
1161                    .expect("Failed to reinit");
1162
1163            // Entry should be gone
1164            assert!(oversized.get(1, 0).await.is_err());
1165
1166            oversized.destroy().await.expect("Failed to destroy");
1167        });
1168    }
1169
1170    #[test_traced]
1171    fn test_recovery_last_entry_off_by_one() {
1172        let executor = deterministic::Runner::default();
1173        executor.start(|context| async move {
1174            let cfg = test_cfg(&context);
1175
1176            // Create and populate
1177            let mut oversized: Oversized<_, TestEntry, TestValue> =
1178                Oversized::init(context.child("first"), cfg.clone())
1179                    .await
1180                    .expect("Failed to init");
1181
1182            let mut locations = Vec::new();
1183            for i in 0..3u8 {
1184                let value: TestValue = [i; 16];
1185                let entry = TestEntry::new(i as u64, 0, 0);
1186                let loc = oversized
1187                    .append(1, entry, &value)
1188                    .await
1189                    .expect("Failed to append");
1190                locations.push(loc);
1191            }
1192            oversized.sync(1).await.expect("Failed to sync");
1193            drop(oversized);
1194
1195            // Truncate glob to be off by 1 byte from last entry
1196            let (blob, _) = context
1197                .open(&cfg.value_partition, &1u64.to_be_bytes())
1198                .await
1199                .expect("Failed to open blob");
1200
1201            // Last entry needs: offset + size bytes
1202            // Truncate to offset + size - 1 (missing 1 byte)
1203            let last = &locations[2];
1204            let truncate_to = byte_end(last.1, last.2) - 1;
1205            blob.resize(truncate_to).await.expect("Failed to truncate");
1206            blob.sync().await.expect("Failed to sync");
1207            drop(blob);
1208
1209            // Reinitialize
1210            let mut oversized: Oversized<_, TestEntry, TestValue> =
1211                Oversized::init(context.child("second"), cfg)
1212                    .await
1213                    .expect("Failed to reinit");
1214
1215            // First 2 entries should be valid
1216            assert!(oversized.get(1, 0).await.is_ok());
1217            assert!(oversized.get(1, 1).await.is_ok());
1218
1219            // Entry 2 should be gone (truncated)
1220            assert!(oversized.get(1, 2).await.is_err());
1221
1222            // Should be able to append after recovery
1223            let value: TestValue = [99; 16];
1224            let entry = TestEntry::new(100, 0, 0);
1225            let (pos, offset, size) = oversized
1226                .append(1, entry, &value)
1227                .await
1228                .expect("Failed to append after recovery");
1229            assert_eq!(pos, 2);
1230
1231            let retrieved = oversized.get(1, 2).await.expect("Failed to get");
1232            assert_eq!(retrieved.id, 100);
1233            let retrieved_value = oversized
1234                .get_value(1, offset, size)
1235                .await
1236                .expect("Failed to get value");
1237            assert_eq!(retrieved_value, value);
1238
1239            oversized.destroy().await.expect("Failed to destroy");
1240        });
1241    }
1242
1243    #[test_traced]
1244    fn test_recovery_glob_missing_entirely() {
1245        let executor = deterministic::Runner::default();
1246        executor.start(|context| async move {
1247            let cfg = test_cfg(&context);
1248
1249            // Create and populate
1250            let mut oversized: Oversized<_, TestEntry, TestValue> =
1251                Oversized::init(context.child("first"), cfg.clone())
1252                    .await
1253                    .expect("Failed to init");
1254
1255            for i in 0..3u8 {
1256                let value: TestValue = [i; 16];
1257                let entry = TestEntry::new(i as u64, 0, 0);
1258                oversized
1259                    .append(1, entry, &value)
1260                    .await
1261                    .expect("Failed to append");
1262            }
1263            oversized.sync(1).await.expect("Failed to sync");
1264            drop(oversized);
1265
1266            // Delete the glob file entirely
1267            context
1268                .remove(&cfg.value_partition, Some(&1u64.to_be_bytes()))
1269                .await
1270                .expect("Failed to remove");
1271
1272            // Reinitialize - glob size will be 0, all entries invalid
1273            let oversized: Oversized<_, TestEntry, TestValue> =
1274                Oversized::init(context.child("second"), cfg)
1275                    .await
1276                    .expect("Failed to reinit");
1277
1278            // All entries should be gone
1279            assert!(oversized.get(1, 0).await.is_err());
1280            assert!(oversized.get(1, 1).await.is_err());
1281            assert!(oversized.get(1, 2).await.is_err());
1282
1283            oversized.destroy().await.expect("Failed to destroy");
1284        });
1285    }
1286
1287    #[test_traced]
1288    fn test_recovery_can_append_after_recovery() {
1289        let executor = deterministic::Runner::default();
1290        executor.start(|context| async move {
1291            let cfg = test_cfg(&context);
1292
1293            // Create and populate
1294            let mut oversized: Oversized<_, TestEntry, TestValue> =
1295                Oversized::init(context.child("first"), cfg.clone())
1296                    .await
1297                    .expect("Failed to init");
1298
1299            let mut locations = Vec::new();
1300            for i in 0..5u8 {
1301                let value: TestValue = [i; 16];
1302                let entry = TestEntry::new(i as u64, 0, 0);
1303                let loc = oversized
1304                    .append(1, entry, &value)
1305                    .await
1306                    .expect("Failed to append");
1307                locations.push(loc);
1308            }
1309            oversized.sync(1).await.expect("Failed to sync");
1310            drop(oversized);
1311
1312            // Truncate glob to keep only first 2 entries
1313            let (blob, _) = context
1314                .open(&cfg.value_partition, &1u64.to_be_bytes())
1315                .await
1316                .expect("Failed to open blob");
1317            let keep_size = byte_end(locations[1].1, locations[1].2);
1318            blob.resize(keep_size).await.expect("Failed to truncate");
1319            blob.sync().await.expect("Failed to sync");
1320            drop(blob);
1321
1322            // Reinitialize
1323            let mut oversized: Oversized<_, TestEntry, TestValue> =
1324                Oversized::init(context.child("second"), cfg.clone())
1325                    .await
1326                    .expect("Failed to reinit");
1327
1328            // Verify first 2 entries exist
1329            assert!(oversized.get(1, 0).await.is_ok());
1330            assert!(oversized.get(1, 1).await.is_ok());
1331            assert!(oversized.get(1, 2).await.is_err());
1332
1333            // Append new entries after recovery
1334            for i in 10..15u8 {
1335                let value: TestValue = [i; 16];
1336                let entry = TestEntry::new(i as u64, 0, 0);
1337                oversized
1338                    .append(1, entry, &value)
1339                    .await
1340                    .expect("Failed to append after recovery");
1341            }
1342            oversized.sync(1).await.expect("Failed to sync");
1343
1344            // Verify new entries at positions 2, 3, 4, 5, 6
1345            for i in 0..5u8 {
1346                let entry = oversized
1347                    .get(1, 2 + i as u64)
1348                    .await
1349                    .expect("Failed to get new entry");
1350                assert_eq!(entry.id, (10 + i) as u64);
1351            }
1352
1353            oversized.destroy().await.expect("Failed to destroy");
1354        });
1355    }
1356
1357    #[test_traced]
1358    fn test_recovery_glob_pruned_but_index_not() {
1359        let executor = deterministic::Runner::default();
1360        executor.start(|context| async move {
1361            let cfg = test_cfg(&context);
1362
1363            // Create and populate multiple sections
1364            let mut oversized: Oversized<_, TestEntry, TestValue> =
1365                Oversized::init(context.child("first"), cfg.clone())
1366                    .await
1367                    .expect("Failed to init");
1368
1369            for section in 1u64..=3 {
1370                let value: TestValue = [section as u8; 16];
1371                let entry = TestEntry::new(section, 0, 0);
1372                oversized
1373                    .append(section, entry, &value)
1374                    .await
1375                    .expect("Failed to append");
1376                oversized.sync(section).await.expect("Failed to sync");
1377            }
1378            drop(oversized);
1379
1380            // Simulate crash during prune: prune ONLY the glob, not the index
1381            // This creates the "glob pruned but index not" scenario
1382            use crate::journal::segmented::glob::{Config as GlobConfig, Glob};
1383            let glob_cfg = GlobConfig {
1384                partition: cfg.value_partition.clone(),
1385                compression: cfg.compression,
1386                codec_config: (),
1387                write_buffer: cfg.value_write_buffer,
1388            };
1389            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
1390                .await
1391                .expect("Failed to init glob");
1392            glob.prune(2).await.expect("Failed to prune glob");
1393            glob.sync_all().await.expect("Failed to sync glob");
1394            drop(glob);
1395
1396            // Reinitialize - should recover gracefully with warning
1397            // Index section 1 will be rewound to 0 entries
1398            let oversized: Oversized<_, TestEntry, TestValue> =
1399                Oversized::init(context.child("second"), cfg.clone())
1400                    .await
1401                    .expect("Failed to reinit");
1402
1403            // Section 1 entries should be gone (index rewound due to glob pruned)
1404            assert!(oversized.get(1, 0).await.is_err());
1405
1406            // Sections 2 and 3 should still be valid
1407            assert!(oversized.get(2, 0).await.is_ok());
1408            assert!(oversized.get(3, 0).await.is_ok());
1409
1410            oversized.destroy().await.expect("Failed to destroy");
1411        });
1412    }
1413
1414    #[test_traced]
1415    fn test_recovery_index_partition_deleted() {
1416        let executor = deterministic::Runner::default();
1417        executor.start(|context| async move {
1418            let cfg = test_cfg(&context);
1419
1420            // Create and populate multiple sections
1421            let mut oversized: Oversized<_, TestEntry, TestValue> =
1422                Oversized::init(context.child("first"), cfg.clone())
1423                    .await
1424                    .expect("Failed to init");
1425
1426            for section in 1u64..=3 {
1427                let value: TestValue = [section as u8; 16];
1428                let entry = TestEntry::new(section, 0, 0);
1429                oversized
1430                    .append(section, entry, &value)
1431                    .await
1432                    .expect("Failed to append");
1433                oversized.sync(section).await.expect("Failed to sync");
1434            }
1435            drop(oversized);
1436
1437            // Delete index blob for section 2 (simulate corruption/loss)
1438            context
1439                .remove(&cfg.index_partition, Some(&2u64.to_be_bytes()))
1440                .await
1441                .expect("Failed to remove index");
1442
1443            // Reinitialize - should handle gracefully
1444            // Section 2 is gone from index, orphan data in glob is acceptable
1445            let oversized: Oversized<_, TestEntry, TestValue> =
1446                Oversized::init(context.child("second"), cfg.clone())
1447                    .await
1448                    .expect("Failed to reinit");
1449
1450            // Section 1 and 3 should still be valid
1451            assert!(oversized.get(1, 0).await.is_ok());
1452            assert!(oversized.get(3, 0).await.is_ok());
1453
1454            // Section 2 should be gone (index file deleted)
1455            assert!(oversized.get(2, 0).await.is_err());
1456
1457            oversized.destroy().await.expect("Failed to destroy");
1458        });
1459    }
1460
1461    #[test_traced]
1462    fn test_recovery_index_synced_but_glob_not() {
1463        let executor = deterministic::Runner::default();
1464        executor.start(|context| async move {
1465            let cfg = test_cfg(&context);
1466
1467            // Create and populate
1468            let mut oversized: Oversized<_, TestEntry, TestValue> =
1469                Oversized::init(context.child("first"), cfg.clone())
1470                    .await
1471                    .expect("Failed to init");
1472
1473            // Append entries and sync
1474            let mut locations = Vec::new();
1475            for i in 0..3u8 {
1476                let value: TestValue = [i; 16];
1477                let entry = TestEntry::new(i as u64, 0, 0);
1478                let loc = oversized
1479                    .append(1, entry, &value)
1480                    .await
1481                    .expect("Failed to append");
1482                locations.push(loc);
1483            }
1484            oversized.sync(1).await.expect("Failed to sync");
1485
1486            // Add more entries WITHOUT syncing (simulates unsynced writes)
1487            for i in 10..15u8 {
1488                let value: TestValue = [i; 16];
1489                let entry = TestEntry::new(i as u64, 0, 0);
1490                oversized
1491                    .append(1, entry, &value)
1492                    .await
1493                    .expect("Failed to append");
1494            }
1495            // Note: NOT calling sync() here
1496            drop(oversized);
1497
1498            // Simulate crash where index was synced but glob wasn't:
1499            // Truncate glob back to the synced size (3 entries)
1500            let (blob, _) = context
1501                .open(&cfg.value_partition, &1u64.to_be_bytes())
1502                .await
1503                .expect("Failed to open blob");
1504            let synced_size = byte_end(locations[2].1, locations[2].2);
1505            blob.resize(synced_size).await.expect("Failed to truncate");
1506            blob.sync().await.expect("Failed to sync");
1507            drop(blob);
1508
1509            // Reinitialize - should rewind index to match glob
1510            let oversized: Oversized<_, TestEntry, TestValue> =
1511                Oversized::init(context.child("second"), cfg)
1512                    .await
1513                    .expect("Failed to reinit");
1514
1515            // First 3 entries should be valid
1516            for i in 0..3u8 {
1517                let entry = oversized.get(1, i as u64).await.expect("Failed to get");
1518                assert_eq!(entry.id, i as u64);
1519            }
1520
1521            // Entries 3-7 should be gone (unsynced, index rewound)
1522            assert!(oversized.get(1, 3).await.is_err());
1523
1524            oversized.destroy().await.expect("Failed to destroy");
1525        });
1526    }
1527
1528    #[test_traced]
1529    fn test_recovery_glob_synced_but_index_not() {
1530        let executor = deterministic::Runner::default();
1531        executor.start(|context| async move {
1532            // Use page size = entry size so each entry is exactly one page.
1533            // This allows truncating by entry count to equal truncating by full pages,
1534            // maintaining page-level integrity.
1535            let cfg = Config {
1536                index_partition: "test-index".into(),
1537                value_partition: "test-values".into(),
1538                index_page_cache: CacheRef::from_pooler(
1539                    &context,
1540                    NZU16!(TestEntry::SIZE as u16),
1541                    NZUsize!(8),
1542                ),
1543                index_write_buffer: NZUsize!(1024),
1544                value_write_buffer: NZUsize!(1024),
1545                compression: None,
1546                codec_config: (),
1547            };
1548
1549            // Create and populate
1550            let mut oversized: Oversized<_, TestEntry, TestValue> =
1551                Oversized::init(context.child("first"), cfg.clone())
1552                    .await
1553                    .expect("Failed to init");
1554
1555            // Append entries and sync
1556            let mut locations = Vec::new();
1557            for i in 0..3u8 {
1558                let value: TestValue = [i; 16];
1559                let entry = TestEntry::new(i as u64, 0, 0);
1560                let loc = oversized
1561                    .append(1, entry, &value)
1562                    .await
1563                    .expect("Failed to append");
1564                locations.push(loc);
1565            }
1566            oversized.sync(1).await.expect("Failed to sync");
1567            drop(oversized);
1568
1569            // Simulate crash: truncate INDEX but leave GLOB intact
1570            // This creates orphan data in glob (glob ahead of index)
1571            let (blob, _size) = context
1572                .open(&cfg.index_partition, &1u64.to_be_bytes())
1573                .await
1574                .expect("Failed to open blob");
1575
1576            // Keep only first 2 index entries (2 full pages)
1577            // Physical page size = logical (20) + CRC record (12) = 32 bytes
1578            let physical_page_size = (TestEntry::SIZE + 12) as u64;
1579            blob.resize(2 * physical_page_size)
1580                .await
1581                .expect("Failed to truncate");
1582            blob.sync().await.expect("Failed to sync");
1583            drop(blob);
1584
1585            // Reinitialize - glob has orphan data from entry 3
1586            let mut oversized: Oversized<_, TestEntry, TestValue> =
1587                Oversized::init(context.child("second"), cfg.clone())
1588                    .await
1589                    .expect("Failed to reinit");
1590
1591            // First 2 entries should be valid
1592            for i in 0..2u8 {
1593                let (position, offset, size) = locations[i as usize];
1594                let entry = oversized.get(1, position).await.expect("Failed to get");
1595                assert_eq!(entry.id, i as u64);
1596
1597                let value = oversized
1598                    .get_value(1, offset, size)
1599                    .await
1600                    .expect("Failed to get value");
1601                assert_eq!(value, [i; 16]);
1602            }
1603
1604            // Entry at position 2 should fail (index was truncated)
1605            assert!(oversized.get(1, 2).await.is_err());
1606
1607            // Append new entries - should work despite orphan data in glob
1608            let mut new_locations = Vec::new();
1609            for i in 10..13u8 {
1610                let value: TestValue = [i; 16];
1611                let entry = TestEntry::new(i as u64, 0, 0);
1612                let (position, offset, size) = oversized
1613                    .append(1, entry, &value)
1614                    .await
1615                    .expect("Failed to append after recovery");
1616
1617                // New entries start at position 2 (after the 2 valid entries)
1618                assert_eq!(position, (i - 10 + 2) as u64);
1619                new_locations.push((position, offset, size, i));
1620
1621                // Verify we can read the new entry
1622                let retrieved = oversized.get(1, position).await.expect("Failed to get");
1623                assert_eq!(retrieved.id, i as u64);
1624
1625                let retrieved_value = oversized
1626                    .get_value(1, offset, size)
1627                    .await
1628                    .expect("Failed to get value");
1629                assert_eq!(retrieved_value, value);
1630            }
1631
1632            // Sync and restart again to verify persistence with orphan data
1633            oversized.sync(1).await.expect("Failed to sync");
1634            drop(oversized);
1635
1636            // Reinitialize after adding data on top of orphan glob data
1637            let oversized: Oversized<_, TestEntry, TestValue> =
1638                Oversized::init(context.child("third"), cfg)
1639                    .await
1640                    .expect("Failed to reinit after append");
1641
1642            // Read all valid entries in the index
1643            // First 2 entries from original data
1644            for i in 0..2u8 {
1645                let (position, offset, size) = locations[i as usize];
1646                let entry = oversized.get(1, position).await.expect("Failed to get");
1647                assert_eq!(entry.id, i as u64);
1648
1649                let value = oversized
1650                    .get_value(1, offset, size)
1651                    .await
1652                    .expect("Failed to get value");
1653                assert_eq!(value, [i; 16]);
1654            }
1655
1656            // New entries added after recovery
1657            for (position, offset, size, expected_id) in &new_locations {
1658                let entry = oversized
1659                    .get(1, *position)
1660                    .await
1661                    .expect("Failed to get new entry after restart");
1662                assert_eq!(entry.id, *expected_id as u64);
1663
1664                let value = oversized
1665                    .get_value(1, *offset, *size)
1666                    .await
1667                    .expect("Failed to get new value after restart");
1668                assert_eq!(value, [*expected_id; 16]);
1669            }
1670
1671            // Verify total entry count: 2 original + 3 new = 5
1672            assert!(oversized.get(1, 4).await.is_ok());
1673            assert!(oversized.get(1, 5).await.is_err());
1674
1675            oversized.destroy().await.expect("Failed to destroy");
1676        });
1677    }
1678
1679    #[test_traced]
1680    fn test_recovery_partial_index_entry() {
1681        let executor = deterministic::Runner::default();
1682        executor.start(|context| async move {
1683            let cfg = test_cfg(&context);
1684
1685            // Create and populate
1686            let mut oversized: Oversized<_, TestEntry, TestValue> =
1687                Oversized::init(context.child("first"), cfg.clone())
1688                    .await
1689                    .expect("Failed to init");
1690
1691            // Append 3 entries
1692            for i in 0..3u8 {
1693                let value: TestValue = [i; 16];
1694                let entry = TestEntry::new(i as u64, 0, 0);
1695                oversized
1696                    .append(1, entry, &value)
1697                    .await
1698                    .expect("Failed to append");
1699            }
1700            oversized.sync(1).await.expect("Failed to sync");
1701            drop(oversized);
1702
1703            // Simulate crash during write: truncate index to partial entry
1704            // Each entry is TestEntry::SIZE (20) + 4 (CRC32) = 24 bytes
1705            // Truncate to 3 full entries + 10 bytes of partial entry
1706            let (blob, _) = context
1707                .open(&cfg.index_partition, &1u64.to_be_bytes())
1708                .await
1709                .expect("Failed to open blob");
1710            let partial_size = 3 * 24 + 10; // 3 full entries + partial
1711            blob.resize(partial_size).await.expect("Failed to resize");
1712            blob.sync().await.expect("Failed to sync");
1713            drop(blob);
1714
1715            // Reinitialize - should handle partial entry gracefully
1716            let mut oversized: Oversized<_, TestEntry, TestValue> =
1717                Oversized::init(context.child("second"), cfg.clone())
1718                    .await
1719                    .expect("Failed to reinit");
1720
1721            // First 3 entries should still be valid
1722            for i in 0..3u8 {
1723                let entry = oversized.get(1, i as u64).await.expect("Failed to get");
1724                assert_eq!(entry.id, i as u64);
1725            }
1726
1727            // Entry 3 should not exist (partial entry was removed)
1728            assert!(oversized.get(1, 3).await.is_err());
1729
1730            // Append new entry after recovery
1731            let value: TestValue = [42; 16];
1732            let entry = TestEntry::new(100, 0, 0);
1733            let (pos, offset, size) = oversized
1734                .append(1, entry, &value)
1735                .await
1736                .expect("Failed to append after recovery");
1737            assert_eq!(pos, 3);
1738
1739            // Verify we can read the new entry
1740            let retrieved = oversized.get(1, 3).await.expect("Failed to get new entry");
1741            assert_eq!(retrieved.id, 100);
1742            let retrieved_value = oversized
1743                .get_value(1, offset, size)
1744                .await
1745                .expect("Failed to get new value");
1746            assert_eq!(retrieved_value, value);
1747
1748            oversized.destroy().await.expect("Failed to destroy");
1749        });
1750    }
1751
1752    #[test_traced]
1753    fn test_recovery_only_partial_entry() {
1754        let executor = deterministic::Runner::default();
1755        executor.start(|context| async move {
1756            let cfg = test_cfg(&context);
1757
1758            // Create and populate with single entry
1759            let mut oversized: Oversized<_, TestEntry, TestValue> =
1760                Oversized::init(context.child("first"), cfg.clone())
1761                    .await
1762                    .expect("Failed to init");
1763
1764            let value: TestValue = [42; 16];
1765            let entry = TestEntry::new(1, 0, 0);
1766            oversized
1767                .append(1, entry, &value)
1768                .await
1769                .expect("Failed to append");
1770            oversized.sync(1).await.expect("Failed to sync");
1771            drop(oversized);
1772
1773            // Truncate index to only partial data (less than one full entry)
1774            let (blob, _) = context
1775                .open(&cfg.index_partition, &1u64.to_be_bytes())
1776                .await
1777                .expect("Failed to open blob");
1778            blob.resize(10).await.expect("Failed to resize"); // Less than chunk size
1779            blob.sync().await.expect("Failed to sync");
1780            drop(blob);
1781
1782            // Reinitialize - should handle gracefully (rewind to 0)
1783            let mut oversized: Oversized<_, TestEntry, TestValue> =
1784                Oversized::init(context.child("second"), cfg.clone())
1785                    .await
1786                    .expect("Failed to reinit");
1787
1788            // No entries should exist
1789            assert!(oversized.get(1, 0).await.is_err());
1790
1791            // Should be able to append after recovery
1792            let value: TestValue = [99; 16];
1793            let entry = TestEntry::new(100, 0, 0);
1794            let (pos, offset, size) = oversized
1795                .append(1, entry, &value)
1796                .await
1797                .expect("Failed to append after recovery");
1798            assert_eq!(pos, 0);
1799
1800            let retrieved = oversized.get(1, 0).await.expect("Failed to get");
1801            assert_eq!(retrieved.id, 100);
1802            let retrieved_value = oversized
1803                .get_value(1, offset, size)
1804                .await
1805                .expect("Failed to get value");
1806            assert_eq!(retrieved_value, value);
1807
1808            oversized.destroy().await.expect("Failed to destroy");
1809        });
1810    }
1811
1812    #[test_traced]
1813    fn test_recovery_crash_during_rewind_index_ahead() {
1814        // Simulates crash where index was rewound but glob wasn't
1815        let executor = deterministic::Runner::default();
1816        executor.start(|context| async move {
1817            // Use page size = entry size so each entry is exactly one page.
1818            // This allows truncating by entry count to equal truncating by full pages,
1819            // maintaining page-level integrity.
1820            let cfg = Config {
1821                index_partition: "test-index".into(),
1822                value_partition: "test-values".into(),
1823                index_page_cache: CacheRef::from_pooler(
1824                    &context,
1825                    NZU16!(TestEntry::SIZE as u16),
1826                    NZUsize!(8),
1827                ),
1828                index_write_buffer: NZUsize!(1024),
1829                value_write_buffer: NZUsize!(1024),
1830                compression: None,
1831                codec_config: (),
1832            };
1833
1834            // Create and populate
1835            let mut oversized: Oversized<_, TestEntry, TestValue> =
1836                Oversized::init(context.child("first"), cfg.clone())
1837                    .await
1838                    .expect("Failed to init");
1839
1840            let mut locations = Vec::new();
1841            for i in 0..5u8 {
1842                let value: TestValue = [i; 16];
1843                let entry = TestEntry::new(i as u64, 0, 0);
1844                let loc = oversized
1845                    .append(1, entry, &value)
1846                    .await
1847                    .expect("Failed to append");
1848                locations.push(loc);
1849            }
1850            oversized.sync(1).await.expect("Failed to sync");
1851            drop(oversized);
1852
1853            // Simulate crash during rewind: truncate index to 2 entries but leave glob intact
1854            // This simulates: rewind(index) succeeded, crash before rewind(glob)
1855            let (blob, _) = context
1856                .open(&cfg.index_partition, &1u64.to_be_bytes())
1857                .await
1858                .expect("Failed to open blob");
1859            // Physical page size = logical (20) + CRC record (12) = 32 bytes
1860            let physical_page_size = (TestEntry::SIZE + 12) as u64;
1861            blob.resize(2 * physical_page_size)
1862                .await
1863                .expect("Failed to truncate");
1864            blob.sync().await.expect("Failed to sync");
1865            drop(blob);
1866
1867            // Reinitialize - recovery should succeed (glob has orphan data)
1868            let mut oversized: Oversized<_, TestEntry, TestValue> =
1869                Oversized::init(context.child("second"), cfg.clone())
1870                    .await
1871                    .expect("Failed to reinit");
1872
1873            // First 2 entries should be valid
1874            for i in 0..2u8 {
1875                let entry = oversized.get(1, i as u64).await.expect("Failed to get");
1876                assert_eq!(entry.id, i as u64);
1877            }
1878
1879            // Entries 2-4 should be gone (index was truncated)
1880            assert!(oversized.get(1, 2).await.is_err());
1881
1882            // Should be able to append new entries
1883            let (pos, _, _) = oversized
1884                .append(1, TestEntry::new(100, 0, 0), &[100u8; 16])
1885                .await
1886                .expect("Failed to append");
1887            assert_eq!(pos, 2);
1888
1889            oversized.destroy().await.expect("Failed to destroy");
1890        });
1891    }
1892
1893    #[test_traced]
1894    fn test_recovery_crash_during_rewind_glob_ahead() {
1895        // Simulates crash where glob was rewound but index wasn't
1896        let executor = deterministic::Runner::default();
1897        executor.start(|context| async move {
1898            let cfg = test_cfg(&context);
1899
1900            // Create and populate
1901            let mut oversized: Oversized<_, TestEntry, TestValue> =
1902                Oversized::init(context.child("first"), cfg.clone())
1903                    .await
1904                    .expect("Failed to init");
1905
1906            let mut locations = Vec::new();
1907            for i in 0..5u8 {
1908                let value: TestValue = [i; 16];
1909                let entry = TestEntry::new(i as u64, 0, 0);
1910                let loc = oversized
1911                    .append(1, entry, &value)
1912                    .await
1913                    .expect("Failed to append");
1914                locations.push(loc);
1915            }
1916            oversized.sync(1).await.expect("Failed to sync");
1917            drop(oversized);
1918
1919            // Simulate crash during rewind: truncate glob to 2 entries but leave index intact
1920            // This simulates: rewind(glob) succeeded, crash before rewind(index)
1921            let (blob, _) = context
1922                .open(&cfg.value_partition, &1u64.to_be_bytes())
1923                .await
1924                .expect("Failed to open blob");
1925            let keep_size = byte_end(locations[1].1, locations[1].2);
1926            blob.resize(keep_size).await.expect("Failed to truncate");
1927            blob.sync().await.expect("Failed to sync");
1928            drop(blob);
1929
1930            // Reinitialize - recovery should detect index entries pointing beyond glob
1931            let mut oversized: Oversized<_, TestEntry, TestValue> =
1932                Oversized::init(context.child("second"), cfg.clone())
1933                    .await
1934                    .expect("Failed to reinit");
1935
1936            // First 2 entries should be valid (index rewound to match glob)
1937            for i in 0..2u8 {
1938                let entry = oversized.get(1, i as u64).await.expect("Failed to get");
1939                assert_eq!(entry.id, i as u64);
1940            }
1941
1942            // Entries 2-4 should be gone (index rewound during recovery)
1943            assert!(oversized.get(1, 2).await.is_err());
1944
1945            // Should be able to append after recovery
1946            let value: TestValue = [99; 16];
1947            let entry = TestEntry::new(100, 0, 0);
1948            let (pos, offset, size) = oversized
1949                .append(1, entry, &value)
1950                .await
1951                .expect("Failed to append after recovery");
1952            assert_eq!(pos, 2);
1953
1954            let retrieved = oversized.get(1, 2).await.expect("Failed to get");
1955            assert_eq!(retrieved.id, 100);
1956            let retrieved_value = oversized
1957                .get_value(1, offset, size)
1958                .await
1959                .expect("Failed to get value");
1960            assert_eq!(retrieved_value, value);
1961
1962            oversized.destroy().await.expect("Failed to destroy");
1963        });
1964    }
1965
1966    #[test_traced]
1967    fn test_oversized_get_value_invalid_size() {
1968        let executor = deterministic::Runner::default();
1969        executor.start(|context| async move {
1970            let cfg = test_cfg(&context);
1971            let mut oversized: Oversized<_, TestEntry, TestValue> =
1972                Oversized::init(context, cfg).await.expect("Failed to init");
1973
1974            let value: TestValue = [42; 16];
1975            let entry = TestEntry::new(1, 0, 0);
1976            let (_, offset, _size) = oversized
1977                .append(1, entry, &value)
1978                .await
1979                .expect("Failed to append");
1980            oversized.sync(1).await.expect("Failed to sync");
1981
1982            // Size 0 - should fail
1983            assert!(oversized.get_value(1, offset, 0).await.is_err());
1984
1985            // Size < value size - should fail with codec error, checksum mismatch, or
1986            // insufficient length (if size < 4 bytes for checksum)
1987            for size in 1..4u32 {
1988                let result = oversized.get_value(1, offset, size).await;
1989                assert!(
1990                    matches!(
1991                        result,
1992                        Err(Error::Codec(_))
1993                            | Err(Error::ChecksumMismatch(_, _))
1994                            | Err(Error::Runtime(_))
1995                    ),
1996                    "expected error, got: {:?}",
1997                    result
1998                );
1999            }
2000
2001            oversized.destroy().await.expect("Failed to destroy");
2002        });
2003    }
2004
2005    #[test_traced]
2006    fn test_oversized_get_value_wrong_size() {
2007        let executor = deterministic::Runner::default();
2008        executor.start(|context| async move {
2009            let cfg = test_cfg(&context);
2010            let mut oversized: Oversized<_, TestEntry, TestValue> =
2011                Oversized::init(context, cfg).await.expect("Failed to init");
2012
2013            let value: TestValue = [42; 16];
2014            let entry = TestEntry::new(1, 0, 0);
2015            let (_, offset, correct_size) = oversized
2016                .append(1, entry, &value)
2017                .await
2018                .expect("Failed to append");
2019            oversized.sync(1).await.expect("Failed to sync");
2020
2021            // Size too small - will fail to decode or checksum mismatch
2022            // (checksum mismatch can occur because we read wrong bytes as the checksum)
2023            let result = oversized.get_value(1, offset, correct_size - 1).await;
2024            assert!(
2025                matches!(
2026                    result,
2027                    Err(Error::Codec(_)) | Err(Error::ChecksumMismatch(_, _))
2028                ),
2029                "expected Codec or ChecksumMismatch error, got: {:?}",
2030                result
2031            );
2032
2033            oversized.destroy().await.expect("Failed to destroy");
2034        });
2035    }
2036
2037    #[test_traced]
2038    fn test_recovery_values_has_orphan_section() {
2039        let executor = deterministic::Runner::default();
2040        executor.start(|context| async move {
2041            let cfg = test_cfg(&context);
2042
2043            // Create and populate with sections 1 and 2
2044            let mut oversized: Oversized<_, TestEntry, TestValue> =
2045                Oversized::init(context.child("first"), cfg.clone())
2046                    .await
2047                    .expect("Failed to init");
2048
2049            for section in 1u64..=2 {
2050                let value: TestValue = [section as u8; 16];
2051                let entry = TestEntry::new(section, 0, 0);
2052                oversized
2053                    .append(section, entry, &value)
2054                    .await
2055                    .expect("Failed to append");
2056                oversized.sync(section).await.expect("Failed to sync");
2057            }
2058            drop(oversized);
2059
2060            // Manually create an orphan value section (section 3) without corresponding index
2061            let glob_cfg = GlobConfig {
2062                partition: cfg.value_partition.clone(),
2063                compression: cfg.compression,
2064                codec_config: (),
2065                write_buffer: cfg.value_write_buffer,
2066            };
2067            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
2068                .await
2069                .expect("Failed to init glob");
2070            let orphan_value: TestValue = [99; 16];
2071            glob.append(3, &orphan_value)
2072                .await
2073                .expect("Failed to append orphan");
2074            glob.sync(3).await.expect("Failed to sync glob");
2075            drop(glob);
2076
2077            // Reinitialize - should detect and remove the orphan section
2078            let oversized: Oversized<_, TestEntry, TestValue> =
2079                Oversized::init(context.child("second"), cfg.clone())
2080                    .await
2081                    .expect("Failed to reinit");
2082
2083            // Sections 1 and 2 should still be valid
2084            assert!(oversized.get(1, 0).await.is_ok());
2085            assert!(oversized.get(2, 0).await.is_ok());
2086
2087            // Newest section should be 2 (orphan was removed)
2088            assert_eq!(oversized.newest_section(), Some(2));
2089
2090            oversized.destroy().await.expect("Failed to destroy");
2091        });
2092    }
2093
2094    #[test_traced]
2095    fn test_recovery_values_has_multiple_orphan_sections() {
2096        let executor = deterministic::Runner::default();
2097        executor.start(|context| async move {
2098            let cfg = test_cfg(&context);
2099
2100            // Create and populate with only section 1
2101            let mut oversized: Oversized<_, TestEntry, TestValue> =
2102                Oversized::init(context.child("first"), cfg.clone())
2103                    .await
2104                    .expect("Failed to init");
2105
2106            let value: TestValue = [1; 16];
2107            let entry = TestEntry::new(1, 0, 0);
2108            oversized
2109                .append(1, entry, &value)
2110                .await
2111                .expect("Failed to append");
2112            oversized.sync(1).await.expect("Failed to sync");
2113            drop(oversized);
2114
2115            // Manually create multiple orphan value sections (2, 3, 4)
2116            let glob_cfg = GlobConfig {
2117                partition: cfg.value_partition.clone(),
2118                compression: cfg.compression,
2119                codec_config: (),
2120                write_buffer: cfg.value_write_buffer,
2121            };
2122            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
2123                .await
2124                .expect("Failed to init glob");
2125
2126            for section in 2u64..=4 {
2127                let orphan_value: TestValue = [section as u8; 16];
2128                glob.append(section, &orphan_value)
2129                    .await
2130                    .expect("Failed to append orphan");
2131                glob.sync(section).await.expect("Failed to sync glob");
2132            }
2133            drop(glob);
2134
2135            // Reinitialize - should detect and remove all orphan sections
2136            let oversized: Oversized<_, TestEntry, TestValue> =
2137                Oversized::init(context.child("second"), cfg.clone())
2138                    .await
2139                    .expect("Failed to reinit");
2140
2141            // Section 1 should still be valid
2142            assert!(oversized.get(1, 0).await.is_ok());
2143
2144            // Newest section should be 1 (orphans removed)
2145            assert_eq!(oversized.newest_section(), Some(1));
2146
2147            oversized.destroy().await.expect("Failed to destroy");
2148        });
2149    }
2150
2151    #[test_traced]
2152    fn test_recovery_index_empty_but_values_exist() {
2153        let executor = deterministic::Runner::default();
2154        executor.start(|context| async move {
2155            let cfg = test_cfg(&context);
2156
2157            // Manually create value sections without any index entries
2158            let glob_cfg = GlobConfig {
2159                partition: cfg.value_partition.clone(),
2160                compression: cfg.compression,
2161                codec_config: (),
2162                write_buffer: cfg.value_write_buffer,
2163            };
2164            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
2165                .await
2166                .expect("Failed to init glob");
2167
2168            for section in 1u64..=3 {
2169                let orphan_value: TestValue = [section as u8; 16];
2170                glob.append(section, &orphan_value)
2171                    .await
2172                    .expect("Failed to append orphan");
2173                glob.sync(section).await.expect("Failed to sync glob");
2174            }
2175            drop(glob);
2176
2177            // Initialize oversized - should remove all orphan value sections
2178            let oversized: Oversized<_, TestEntry, TestValue> =
2179                Oversized::init(context.child("first"), cfg.clone())
2180                    .await
2181                    .expect("Failed to init");
2182
2183            // No sections should exist
2184            assert_eq!(oversized.newest_section(), None);
2185            assert_eq!(oversized.oldest_section(), None);
2186
2187            oversized.destroy().await.expect("Failed to destroy");
2188        });
2189    }
2190
2191    #[test_traced]
2192    fn test_recovery_orphan_section_append_after() {
2193        let executor = deterministic::Runner::default();
2194        executor.start(|context| async move {
2195            let cfg = test_cfg(&context);
2196
2197            // Create and populate with section 1
2198            let mut oversized: Oversized<_, TestEntry, TestValue> =
2199                Oversized::init(context.child("first"), cfg.clone())
2200                    .await
2201                    .expect("Failed to init");
2202
2203            let value: TestValue = [1; 16];
2204            let entry = TestEntry::new(1, 0, 0);
2205            let (_, offset1, size1) = oversized
2206                .append(1, entry, &value)
2207                .await
2208                .expect("Failed to append");
2209            oversized.sync(1).await.expect("Failed to sync");
2210            drop(oversized);
2211
2212            // Manually create orphan value sections (2, 3)
2213            let glob_cfg = GlobConfig {
2214                partition: cfg.value_partition.clone(),
2215                compression: cfg.compression,
2216                codec_config: (),
2217                write_buffer: cfg.value_write_buffer,
2218            };
2219            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
2220                .await
2221                .expect("Failed to init glob");
2222
2223            for section in 2u64..=3 {
2224                let orphan_value: TestValue = [section as u8; 16];
2225                glob.append(section, &orphan_value)
2226                    .await
2227                    .expect("Failed to append orphan");
2228                glob.sync(section).await.expect("Failed to sync glob");
2229            }
2230            drop(glob);
2231
2232            // Reinitialize - should remove orphan sections
2233            let mut oversized: Oversized<_, TestEntry, TestValue> =
2234                Oversized::init(context.child("second"), cfg.clone())
2235                    .await
2236                    .expect("Failed to reinit");
2237
2238            // Section 1 should still be valid
2239            let entry = oversized.get(1, 0).await.expect("Failed to get");
2240            assert_eq!(entry.id, 1);
2241            let value = oversized
2242                .get_value(1, offset1, size1)
2243                .await
2244                .expect("Failed to get value");
2245            assert_eq!(value, [1; 16]);
2246
2247            // Should be able to append to section 2 after recovery
2248            let new_value: TestValue = [42; 16];
2249            let new_entry = TestEntry::new(42, 0, 0);
2250            let (pos, offset, size) = oversized
2251                .append(2, new_entry, &new_value)
2252                .await
2253                .expect("Failed to append after recovery");
2254            assert_eq!(pos, 0);
2255
2256            // Verify the new entry
2257            let retrieved = oversized.get(2, 0).await.expect("Failed to get");
2258            assert_eq!(retrieved.id, 42);
2259            let retrieved_value = oversized
2260                .get_value(2, offset, size)
2261                .await
2262                .expect("Failed to get value");
2263            assert_eq!(retrieved_value, new_value);
2264
2265            // Sync and restart to verify persistence
2266            oversized.sync(2).await.expect("Failed to sync");
2267            drop(oversized);
2268
2269            let oversized: Oversized<_, TestEntry, TestValue> =
2270                Oversized::init(context.child("third"), cfg)
2271                    .await
2272                    .expect("Failed to reinit after append");
2273
2274            // Both sections should be valid
2275            assert!(oversized.get(1, 0).await.is_ok());
2276            assert!(oversized.get(2, 0).await.is_ok());
2277            assert_eq!(oversized.newest_section(), Some(2));
2278
2279            oversized.destroy().await.expect("Failed to destroy");
2280        });
2281    }
2282
2283    #[test_traced]
2284    fn test_recovery_no_orphan_sections() {
2285        let executor = deterministic::Runner::default();
2286        executor.start(|context| async move {
2287            let cfg = test_cfg(&context);
2288
2289            // Create and populate with sections 1, 2, 3 (no orphans)
2290            let mut oversized: Oversized<_, TestEntry, TestValue> =
2291                Oversized::init(context.child("first"), cfg.clone())
2292                    .await
2293                    .expect("Failed to init");
2294
2295            for section in 1u64..=3 {
2296                let value: TestValue = [section as u8; 16];
2297                let entry = TestEntry::new(section, 0, 0);
2298                oversized
2299                    .append(section, entry, &value)
2300                    .await
2301                    .expect("Failed to append");
2302                oversized.sync(section).await.expect("Failed to sync");
2303            }
2304            drop(oversized);
2305
2306            // Reinitialize - no orphan cleanup needed
2307            let oversized: Oversized<_, TestEntry, TestValue> =
2308                Oversized::init(context.child("second"), cfg)
2309                    .await
2310                    .expect("Failed to reinit");
2311
2312            // All sections should be valid
2313            for section in 1u64..=3 {
2314                let entry = oversized.get(section, 0).await.expect("Failed to get");
2315                assert_eq!(entry.id, section);
2316            }
2317            assert_eq!(oversized.newest_section(), Some(3));
2318
2319            oversized.destroy().await.expect("Failed to destroy");
2320        });
2321    }
2322
2323    #[test_traced]
2324    fn test_recovery_orphan_with_empty_index_section() {
2325        let executor = deterministic::Runner::default();
2326        executor.start(|context| async move {
2327            let cfg = test_cfg(&context);
2328
2329            // Create and populate section 1 with entries
2330            let mut oversized: Oversized<_, TestEntry, TestValue> =
2331                Oversized::init(context.child("first"), cfg.clone())
2332                    .await
2333                    .expect("Failed to init");
2334
2335            let value: TestValue = [1; 16];
2336            let entry = TestEntry::new(1, 0, 0);
2337            oversized
2338                .append(1, entry, &value)
2339                .await
2340                .expect("Failed to append");
2341            oversized.sync(1).await.expect("Failed to sync");
2342            drop(oversized);
2343
2344            // Manually create orphan value section 2
2345            let glob_cfg = GlobConfig {
2346                partition: cfg.value_partition.clone(),
2347                compression: cfg.compression,
2348                codec_config: (),
2349                write_buffer: cfg.value_write_buffer,
2350            };
2351            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
2352                .await
2353                .expect("Failed to init glob");
2354            let orphan_value: TestValue = [2; 16];
2355            glob.append(2, &orphan_value)
2356                .await
2357                .expect("Failed to append orphan");
2358            glob.sync(2).await.expect("Failed to sync glob");
2359            drop(glob);
2360
2361            // Now truncate index section 1 to 0 (making it empty but still tracked)
2362            let (blob, _) = context
2363                .open(&cfg.index_partition, &1u64.to_be_bytes())
2364                .await
2365                .expect("Failed to open blob");
2366            blob.resize(0).await.expect("Failed to truncate");
2367            blob.sync().await.expect("Failed to sync");
2368            drop(blob);
2369
2370            // Reinitialize - should handle empty index section and remove orphan value section
2371            let oversized: Oversized<_, TestEntry, TestValue> =
2372                Oversized::init(context.child("second"), cfg)
2373                    .await
2374                    .expect("Failed to reinit");
2375
2376            // Section 1 should exist but have no entries (empty after truncation)
2377            assert!(oversized.get(1, 0).await.is_err());
2378
2379            // Orphan section 2 should be removed
2380            assert_eq!(oversized.newest_section(), Some(1));
2381
2382            oversized.destroy().await.expect("Failed to destroy");
2383        });
2384    }
2385
2386    #[test_traced]
2387    fn test_recovery_orphan_sections_with_gaps() {
2388        // Test non-contiguous sections: index has [1, 3, 5], values has [1, 2, 3, 4, 5, 6]
2389        // Orphan sections 2, 4, 6 should be removed
2390        let executor = deterministic::Runner::default();
2391        executor.start(|context| async move {
2392            let cfg = test_cfg(&context);
2393
2394            // Create index with sections 1, 3, 5 (gaps)
2395            let mut oversized: Oversized<_, TestEntry, TestValue> =
2396                Oversized::init(context.child("first"), cfg.clone())
2397                    .await
2398                    .expect("Failed to init");
2399
2400            for section in [1u64, 3, 5] {
2401                let value: TestValue = [section as u8; 16];
2402                let entry = TestEntry::new(section, 0, 0);
2403                oversized
2404                    .append(section, entry, &value)
2405                    .await
2406                    .expect("Failed to append");
2407                oversized.sync(section).await.expect("Failed to sync");
2408            }
2409            drop(oversized);
2410
2411            // Manually create orphan value sections 2, 4, 6 (filling gaps and beyond)
2412            let glob_cfg = GlobConfig {
2413                partition: cfg.value_partition.clone(),
2414                compression: cfg.compression,
2415                codec_config: (),
2416                write_buffer: cfg.value_write_buffer,
2417            };
2418            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
2419                .await
2420                .expect("Failed to init glob");
2421
2422            for section in [2u64, 4, 6] {
2423                let orphan_value: TestValue = [section as u8; 16];
2424                glob.append(section, &orphan_value)
2425                    .await
2426                    .expect("Failed to append orphan");
2427                glob.sync(section).await.expect("Failed to sync glob");
2428            }
2429            drop(glob);
2430
2431            // Reinitialize - should remove orphan sections 2, 4, 6
2432            let oversized: Oversized<_, TestEntry, TestValue> =
2433                Oversized::init(context.child("second"), cfg)
2434                    .await
2435                    .expect("Failed to reinit");
2436
2437            // Sections 1, 3, 5 should still be valid
2438            for section in [1u64, 3, 5] {
2439                let entry = oversized.get(section, 0).await.expect("Failed to get");
2440                assert_eq!(entry.id, section);
2441            }
2442
2443            // Verify only sections 1, 3, 5 exist (orphans removed)
2444            assert_eq!(oversized.oldest_section(), Some(1));
2445            assert_eq!(oversized.newest_section(), Some(5));
2446
2447            oversized.destroy().await.expect("Failed to destroy");
2448        });
2449    }
2450
2451    #[test_traced]
2452    fn test_recovery_glob_trailing_garbage_truncated() {
2453        // Tests the bug fix: when value is written to glob but index entry isn't
2454        // (crash after value write, before index write), recovery should truncate
2455        // the glob trailing garbage so subsequent appends start at correct offset.
2456        let executor = deterministic::Runner::default();
2457        executor.start(|context| async move {
2458            let cfg = test_cfg(&context);
2459
2460            // Create and populate
2461            let mut oversized: Oversized<_, TestEntry, TestValue> =
2462                Oversized::init(context.child("first"), cfg.clone())
2463                    .await
2464                    .expect("Failed to init");
2465
2466            // Append 2 entries
2467            let mut locations = Vec::new();
2468            for i in 0..2u8 {
2469                let value: TestValue = [i; 16];
2470                let entry = TestEntry::new(i as u64, 0, 0);
2471                let loc = oversized
2472                    .append(1, entry, &value)
2473                    .await
2474                    .expect("Failed to append");
2475                locations.push(loc);
2476            }
2477            oversized.sync(1).await.expect("Failed to sync");
2478
2479            // Record where next entry SHOULD start (end of entry 1)
2480            let expected_next_offset = byte_end(locations[1].1, locations[1].2);
2481            drop(oversized);
2482
2483            // Simulate crash: write garbage to glob (simulating partial value write)
2484            let (blob, size) = context
2485                .open(&cfg.value_partition, &1u64.to_be_bytes())
2486                .await
2487                .expect("Failed to open blob");
2488            assert_eq!(size, expected_next_offset);
2489
2490            // Write 100 bytes of garbage (simulating partial/failed value write)
2491            let garbage = vec![0xDE; 100];
2492            blob.write_at_sync(size, garbage)
2493                .await
2494                .expect("Failed to write garbage");
2495            drop(blob);
2496
2497            // Verify glob now has trailing garbage
2498            let (blob, new_size) = context
2499                .open(&cfg.value_partition, &1u64.to_be_bytes())
2500                .await
2501                .expect("Failed to open blob");
2502            assert_eq!(new_size, expected_next_offset + 100);
2503            drop(blob);
2504
2505            // Reinitialize - should truncate the trailing garbage
2506            let mut oversized: Oversized<_, TestEntry, TestValue> =
2507                Oversized::init(context.child("second"), cfg.clone())
2508                    .await
2509                    .expect("Failed to reinit");
2510
2511            // First 2 entries should still be valid
2512            for i in 0..2u8 {
2513                let entry = oversized.get(1, i as u64).await.expect("Failed to get");
2514                assert_eq!(entry.id, i as u64);
2515            }
2516
2517            // Append new entry - should start at expected_next_offset, NOT at garbage end
2518            let new_value: TestValue = [99; 16];
2519            let new_entry = TestEntry::new(99, 0, 0);
2520            let (pos, offset, _size) = oversized
2521                .append(1, new_entry, &new_value)
2522                .await
2523                .expect("Failed to append after recovery");
2524
2525            // Verify position is 2 (after the 2 existing entries)
2526            assert_eq!(pos, 2);
2527
2528            // Verify offset is at expected_next_offset (garbage was truncated)
2529            assert_eq!(offset, expected_next_offset);
2530
2531            // Verify we can read the new entry
2532            let retrieved = oversized.get(1, 2).await.expect("Failed to get new entry");
2533            assert_eq!(retrieved.id, 99);
2534
2535            oversized.destroy().await.expect("Failed to destroy");
2536        });
2537    }
2538
2539    #[test_traced]
2540    fn test_recovery_entry_with_overflow_offset() {
2541        // Tests that an entry with offset near u64::MAX that would overflow
2542        // when added to size is detected as invalid during recovery.
2543        let executor = deterministic::Runner::default();
2544        executor.start(|context| async move {
2545            // Use page size = entry size so one entry per page
2546            let cfg = Config {
2547                index_partition: "test-index".into(),
2548                value_partition: "test-values".into(),
2549                index_page_cache: CacheRef::from_pooler(
2550                    &context,
2551                    NZU16!(TestEntry::SIZE as u16),
2552                    NZUsize!(8),
2553                ),
2554                index_write_buffer: NZUsize!(1024),
2555                value_write_buffer: NZUsize!(1024),
2556                compression: None,
2557                codec_config: (),
2558            };
2559
2560            // Create and populate with valid entry
2561            let mut oversized: Oversized<_, TestEntry, TestValue> =
2562                Oversized::init(context.child("first"), cfg.clone())
2563                    .await
2564                    .expect("Failed to init");
2565
2566            let value: TestValue = [1; 16];
2567            let entry = TestEntry::new(1, 0, 0);
2568            oversized
2569                .append(1, entry, &value)
2570                .await
2571                .expect("Failed to append");
2572            oversized.sync(1).await.expect("Failed to sync");
2573            drop(oversized);
2574
2575            // Build a corrupted entry with offset near u64::MAX that would overflow.
2576            // We need to write a valid page (with correct page-level CRC) containing
2577            // the semantically-invalid entry data.
2578            let (blob, _) = context
2579                .open(&cfg.index_partition, &1u64.to_be_bytes())
2580                .await
2581                .expect("Failed to open blob");
2582
2583            // Build entry data: id (8) + value_offset (8) + value_size (4) = 20 bytes
2584            let mut entry_data = Vec::new();
2585            1u64.write(&mut entry_data); // id
2586            (u64::MAX - 10).write(&mut entry_data); // value_offset (near max)
2587            100u32.write(&mut entry_data); // value_size (offset + size overflows)
2588            assert_eq!(entry_data.len(), TestEntry::SIZE);
2589
2590            // Build page-level CRC record (12 bytes):
2591            // len1 (2) + crc1 (4) + len2 (2) + crc2 (4)
2592            let crc = Crc32::checksum(&entry_data);
2593            let len1 = TestEntry::SIZE as u16;
2594            let mut crc_record = Vec::new();
2595            crc_record.extend_from_slice(&len1.to_be_bytes()); // len1
2596            crc_record.extend_from_slice(&crc.to_be_bytes()); // crc1
2597            crc_record.extend_from_slice(&0u16.to_be_bytes()); // len2 (unused)
2598            crc_record.extend_from_slice(&0u32.to_be_bytes()); // crc2 (unused)
2599            assert_eq!(crc_record.len(), 12);
2600
2601            // Write the complete physical page: entry_data + crc_record
2602            let mut page = entry_data;
2603            page.extend_from_slice(&crc_record);
2604            blob.write_at_sync(0, page)
2605                .await
2606                .expect("Failed to write corrupted page");
2607            drop(blob);
2608
2609            // Reinitialize - recovery should detect the invalid entry
2610            // (offset + size would overflow, and even with saturating_add it exceeds glob_size)
2611            let mut oversized: Oversized<_, TestEntry, TestValue> =
2612                Oversized::init(context.child("second"), cfg.clone())
2613                    .await
2614                    .expect("Failed to reinit");
2615
2616            // The corrupted entry should have been rewound (invalid)
2617            assert!(oversized.get(1, 0).await.is_err());
2618
2619            // Should be able to append after recovery
2620            let new_value: TestValue = [99; 16];
2621            let new_entry = TestEntry::new(99, 0, 0);
2622            let (pos, new_offset, _) = oversized
2623                .append(1, new_entry, &new_value)
2624                .await
2625                .expect("Failed to append after recovery");
2626
2627            // Position should be 0 (corrupted entry was removed)
2628            assert_eq!(pos, 0);
2629            // Offset should be 0 (glob was truncated to 0)
2630            assert_eq!(new_offset, 0);
2631
2632            oversized.destroy().await.expect("Failed to destroy");
2633        });
2634    }
2635
2636    #[test_traced]
2637    fn test_empty_section_persistence() {
2638        // Tests that sections that become empty (all entries removed/rewound)
2639        // are handled correctly across restart cycles.
2640        let executor = deterministic::Runner::default();
2641        executor.start(|context| async move {
2642            let cfg = test_cfg(&context);
2643
2644            // Create and populate section 1 with entries
2645            let mut oversized: Oversized<_, TestEntry, TestValue> =
2646                Oversized::init(context.child("first"), cfg.clone())
2647                    .await
2648                    .expect("Failed to init");
2649
2650            for i in 0..3u8 {
2651                let value: TestValue = [i; 16];
2652                let entry = TestEntry::new(i as u64, 0, 0);
2653                oversized
2654                    .append(1, entry, &value)
2655                    .await
2656                    .expect("Failed to append");
2657            }
2658            oversized.sync(1).await.expect("Failed to sync");
2659
2660            // Also create section 2 to ensure it survives
2661            let value2: TestValue = [10; 16];
2662            let entry2 = TestEntry::new(10, 0, 0);
2663            oversized
2664                .append(2, entry2, &value2)
2665                .await
2666                .expect("Failed to append to section 2");
2667            oversized.sync(2).await.expect("Failed to sync section 2");
2668            drop(oversized);
2669
2670            // Truncate section 1's index to 0 (making it empty)
2671            let (blob, _) = context
2672                .open(&cfg.index_partition, &1u64.to_be_bytes())
2673                .await
2674                .expect("Failed to open blob");
2675            blob.resize(0).await.expect("Failed to truncate");
2676            blob.sync().await.expect("Failed to sync");
2677            drop(blob);
2678
2679            // First restart - recovery should handle empty section 1
2680            let mut oversized: Oversized<_, TestEntry, TestValue> =
2681                Oversized::init(context.child("second"), cfg.clone())
2682                    .await
2683                    .expect("Failed to reinit");
2684
2685            // Section 1 should exist but have no entries
2686            assert!(oversized.get(1, 0).await.is_err());
2687
2688            // Section 2 should still be valid
2689            let entry = oversized.get(2, 0).await.expect("Failed to get section 2");
2690            assert_eq!(entry.id, 10);
2691
2692            // Section 1 should still be tracked (blob exists but is empty)
2693            assert_eq!(oversized.oldest_section(), Some(1));
2694
2695            // Append to empty section 1
2696            // Note: When index is truncated to 0 but the index blob still exists,
2697            // the glob is NOT truncated (the section isn't considered an orphan).
2698            // The glob still has orphan DATA from the old entries, but this doesn't
2699            // affect correctness - new entries simply append after the orphan data.
2700            let new_value: TestValue = [99; 16];
2701            let new_entry = TestEntry::new(99, 0, 0);
2702            let (pos, offset, size) = oversized
2703                .append(1, new_entry, &new_value)
2704                .await
2705                .expect("Failed to append to empty section");
2706            assert_eq!(pos, 0);
2707            // Glob offset is non-zero because orphan data wasn't truncated
2708            assert!(offset > 0);
2709            oversized.sync(1).await.expect("Failed to sync");
2710
2711            // Verify the new entry is readable despite orphan data before it
2712            let entry = oversized.get(1, 0).await.expect("Failed to get");
2713            assert_eq!(entry.id, 99);
2714            let value = oversized
2715                .get_value(1, offset, size)
2716                .await
2717                .expect("Failed to get value");
2718            assert_eq!(value, new_value);
2719
2720            drop(oversized);
2721
2722            // Second restart - verify persistence
2723            let oversized: Oversized<_, TestEntry, TestValue> =
2724                Oversized::init(context.child("third"), cfg.clone())
2725                    .await
2726                    .expect("Failed to reinit again");
2727
2728            // Section 1's new entry should be valid
2729            let entry = oversized.get(1, 0).await.expect("Failed to get");
2730            assert_eq!(entry.id, 99);
2731
2732            // Section 2 should still be valid
2733            let entry = oversized.get(2, 0).await.expect("Failed to get section 2");
2734            assert_eq!(entry.id, 10);
2735
2736            oversized.destroy().await.expect("Failed to destroy");
2737        });
2738    }
2739
2740    #[test_traced]
2741    fn test_get_value_size_equals_crc_size() {
2742        // Tests the boundary condition where size = 4 (just CRC, no data).
2743        // This should fail because there's no actual data to decode.
2744        let executor = deterministic::Runner::default();
2745        executor.start(|context| async move {
2746            let cfg = test_cfg(&context);
2747            let mut oversized: Oversized<_, TestEntry, TestValue> =
2748                Oversized::init(context, cfg).await.expect("Failed to init");
2749
2750            let value: TestValue = [42; 16];
2751            let entry = TestEntry::new(1, 0, 0);
2752            let (_, offset, _) = oversized
2753                .append(1, entry, &value)
2754                .await
2755                .expect("Failed to append");
2756            oversized.sync(1).await.expect("Failed to sync");
2757
2758            // Size = 4 (exactly CRC_SIZE) means 0 bytes of actual data
2759            // This should fail with ChecksumMismatch or decode error
2760            let result = oversized.get_value(1, offset, 4).await;
2761            assert!(result.is_err());
2762
2763            oversized.destroy().await.expect("Failed to destroy");
2764        });
2765    }
2766
2767    #[test_traced]
2768    fn test_get_value_size_just_over_crc() {
2769        // Tests size = 5 (CRC + 1 byte of data).
2770        // This should fail because the data is too short to decode.
2771        let executor = deterministic::Runner::default();
2772        executor.start(|context| async move {
2773            let cfg = test_cfg(&context);
2774            let mut oversized: Oversized<_, TestEntry, TestValue> =
2775                Oversized::init(context, cfg).await.expect("Failed to init");
2776
2777            let value: TestValue = [42; 16];
2778            let entry = TestEntry::new(1, 0, 0);
2779            let (_, offset, _) = oversized
2780                .append(1, entry, &value)
2781                .await
2782                .expect("Failed to append");
2783            oversized.sync(1).await.expect("Failed to sync");
2784
2785            // Size = 5 means 1 byte of actual data (after stripping CRC)
2786            // This should fail with checksum mismatch since we're reading wrong bytes
2787            let result = oversized.get_value(1, offset, 5).await;
2788            assert!(result.is_err());
2789
2790            oversized.destroy().await.expect("Failed to destroy");
2791        });
2792    }
2793
2794    #[test_traced]
2795    fn test_recovery_maximum_section_numbers() {
2796        // Test recovery with very large section numbers near u64::MAX to check
2797        // for overflow edge cases in section arithmetic.
2798        let executor = deterministic::Runner::default();
2799        executor.start(|context| async move {
2800            let cfg = test_cfg(&context);
2801
2802            // Use section numbers near u64::MAX
2803            let large_sections = [u64::MAX - 3, u64::MAX - 2, u64::MAX - 1];
2804
2805            // Create and populate with large section numbers
2806            let mut oversized: Oversized<_, TestEntry, TestValue> =
2807                Oversized::init(context.child("first"), cfg.clone())
2808                    .await
2809                    .expect("Failed to init");
2810
2811            let mut locations = Vec::new();
2812            for &section in &large_sections {
2813                let value: TestValue = [(section & 0xFF) as u8; 16];
2814                let entry = TestEntry::new(section, 0, 0);
2815                let loc = oversized
2816                    .append(section, entry, &value)
2817                    .await
2818                    .expect("Failed to append");
2819                locations.push((section, loc));
2820                oversized.sync(section).await.expect("Failed to sync");
2821            }
2822            drop(oversized);
2823
2824            // Simulate crash: truncate glob for middle section
2825            let middle_section = large_sections[1];
2826            let (blob, size) = context
2827                .open(&cfg.value_partition, &middle_section.to_be_bytes())
2828                .await
2829                .expect("Failed to open blob");
2830            blob.resize(size / 2).await.expect("Failed to truncate");
2831            blob.sync().await.expect("Failed to sync");
2832            drop(blob);
2833
2834            // Reinitialize - should recover without overflow panics
2835            let mut oversized: Oversized<_, TestEntry, TestValue> =
2836                Oversized::init(context.child("second"), cfg.clone())
2837                    .await
2838                    .expect("Failed to reinit");
2839
2840            // First and last sections should still be valid
2841            let entry = oversized
2842                .get(large_sections[0], 0)
2843                .await
2844                .expect("Failed to get first section");
2845            assert_eq!(entry.id, large_sections[0]);
2846
2847            let entry = oversized
2848                .get(large_sections[2], 0)
2849                .await
2850                .expect("Failed to get last section");
2851            assert_eq!(entry.id, large_sections[2]);
2852
2853            // Middle section should have been rewound (no entries)
2854            assert!(oversized.get(middle_section, 0).await.is_err());
2855
2856            // Verify we can still append to these large sections
2857            let new_value: TestValue = [0xAB; 16];
2858            let new_entry = TestEntry::new(999, 0, 0);
2859            oversized
2860                .append(middle_section, new_entry, &new_value)
2861                .await
2862                .expect("Failed to append after recovery");
2863
2864            oversized.destroy().await.expect("Failed to destroy");
2865        });
2866    }
2867
2868    #[test_traced]
2869    fn test_recovery_crash_during_recovery_rewind() {
2870        // Tests a nested crash scenario: initial crash leaves inconsistent state,
2871        // then a second crash occurs during recovery's rewind operation.
2872        // This simulates the worst-case where recovery itself is interrupted.
2873        let executor = deterministic::Runner::default();
2874        executor.start(|context| async move {
2875            let cfg = test_cfg(&context);
2876
2877            // Phase 1: Create valid data with 5 entries
2878            let mut oversized: Oversized<_, TestEntry, TestValue> =
2879                Oversized::init(context.child("first"), cfg.clone())
2880                    .await
2881                    .expect("Failed to init");
2882
2883            let mut locations = Vec::new();
2884            for i in 0..5u8 {
2885                let value: TestValue = [i; 16];
2886                let entry = TestEntry::new(i as u64, 0, 0);
2887                let loc = oversized
2888                    .append(1, entry, &value)
2889                    .await
2890                    .expect("Failed to append");
2891                locations.push(loc);
2892            }
2893            oversized.sync(1).await.expect("Failed to sync");
2894            drop(oversized);
2895
2896            // Phase 2: Simulate first crash - truncate glob to lose last 2 entries
2897            let (blob, _) = context
2898                .open(&cfg.value_partition, &1u64.to_be_bytes())
2899                .await
2900                .expect("Failed to open blob");
2901            let keep_size = byte_end(locations[2].1, locations[2].2);
2902            blob.resize(keep_size).await.expect("Failed to truncate");
2903            blob.sync().await.expect("Failed to sync");
2904            drop(blob);
2905
2906            // Phase 3: Simulate crash during recovery's rewind
2907            // Recovery would try to rewind index from 5 entries to 3 entries.
2908            // Simulate partial rewind by manually truncating index to 4 entries
2909            // (as if crash occurred mid-rewind).
2910            let chunk_size = FixedJournal::<deterministic::Context, TestEntry>::CHUNK_SIZE as u64;
2911            let (index_blob, _) = context
2912                .open(&cfg.index_partition, &1u64.to_be_bytes())
2913                .await
2914                .expect("Failed to open index blob");
2915            let partial_rewind_size = 4 * chunk_size; // 4 entries instead of 3
2916            index_blob
2917                .resize(partial_rewind_size)
2918                .await
2919                .expect("Failed to resize");
2920            index_blob.sync().await.expect("Failed to sync");
2921            drop(index_blob);
2922
2923            // Phase 4: Second recovery attempt should handle the inconsistent state
2924            // Index has 4 entries, but glob only supports 3.
2925            let mut oversized: Oversized<_, TestEntry, TestValue> =
2926                Oversized::init(context.child("second"), cfg.clone())
2927                    .await
2928                    .expect("Failed to reinit after nested crash");
2929
2930            // Only first 3 entries should be valid (recovery should rewind again)
2931            for i in 0..3u8 {
2932                let entry = oversized.get(1, i as u64).await.expect("Failed to get");
2933                assert_eq!(entry.id, i as u64);
2934
2935                let (_, offset, size) = locations[i as usize];
2936                let value = oversized
2937                    .get_value(1, offset, size)
2938                    .await
2939                    .expect("Failed to get value");
2940                assert_eq!(value, [i; 16]);
2941            }
2942
2943            // Entry 3 should not exist (index was rewound to match glob)
2944            assert!(oversized.get(1, 3).await.is_err());
2945
2946            // Verify append works after nested crash recovery
2947            let new_value: TestValue = [0xFF; 16];
2948            let new_entry = TestEntry::new(100, 0, 0);
2949            let (pos, offset, _size) = oversized
2950                .append(1, new_entry, &new_value)
2951                .await
2952                .expect("Failed to append");
2953            assert_eq!(pos, 3); // Should be position 3 (after the 3 valid entries)
2954
2955            // Verify the offset starts where entry 2 ended (no gaps)
2956            assert_eq!(offset, byte_end(locations[2].1, locations[2].2));
2957
2958            oversized.destroy().await.expect("Failed to destroy");
2959        });
2960    }
2961
2962    #[test_traced]
2963    fn test_recovery_crash_during_orphan_cleanup() {
2964        // Tests crash during orphan section cleanup: recovery starts removing
2965        // orphan value sections, but crashes mid-cleanup.
2966        let executor = deterministic::Runner::default();
2967        executor.start(|context| async move {
2968            let cfg = test_cfg(&context);
2969
2970            // Phase 1: Create valid data in section 1
2971            let mut oversized: Oversized<_, TestEntry, TestValue> =
2972                Oversized::init(context.child("first"), cfg.clone())
2973                    .await
2974                    .expect("Failed to init");
2975
2976            let value: TestValue = [1; 16];
2977            let entry = TestEntry::new(1, 0, 0);
2978            let (_, offset1, size1) = oversized
2979                .append(1, entry, &value)
2980                .await
2981                .expect("Failed to append");
2982            oversized.sync(1).await.expect("Failed to sync");
2983            drop(oversized);
2984
2985            // Phase 2: Create orphan value sections 2, 3, 4 (no index entries)
2986            let glob_cfg = GlobConfig {
2987                partition: cfg.value_partition.clone(),
2988                compression: cfg.compression,
2989                codec_config: (),
2990                write_buffer: cfg.value_write_buffer,
2991            };
2992            let mut glob: Glob<_, TestValue> = Glob::init(context.child("glob"), glob_cfg)
2993                .await
2994                .expect("Failed to init glob");
2995
2996            for section in 2u64..=4 {
2997                let orphan_value: TestValue = [section as u8; 16];
2998                glob.append(section, &orphan_value)
2999                    .await
3000                    .expect("Failed to append orphan");
3001                glob.sync(section).await.expect("Failed to sync glob");
3002            }
3003            drop(glob);
3004
3005            // Phase 3: Simulate partial orphan cleanup (section 2 removed, 3 and 4 remain)
3006            // This simulates a crash during cleanup_orphan_value_sections()
3007            context
3008                .remove(&cfg.value_partition, Some(&2u64.to_be_bytes()))
3009                .await
3010                .expect("Failed to remove section 2");
3011
3012            // Phase 4: Recovery should complete the cleanup
3013            let mut oversized: Oversized<_, TestEntry, TestValue> =
3014                Oversized::init(context.child("second"), cfg.clone())
3015                    .await
3016                    .expect("Failed to reinit");
3017
3018            // Section 1 should still be valid
3019            let entry = oversized.get(1, 0).await.expect("Failed to get");
3020            assert_eq!(entry.id, 1);
3021            let value = oversized
3022                .get_value(1, offset1, size1)
3023                .await
3024                .expect("Failed to get value");
3025            assert_eq!(value, [1; 16]);
3026
3027            // No orphan sections should remain
3028            assert_eq!(oversized.oldest_section(), Some(1));
3029            assert_eq!(oversized.newest_section(), Some(1));
3030
3031            // Should be able to append to section 2 (now clean)
3032            let new_value: TestValue = [42; 16];
3033            let new_entry = TestEntry::new(42, 0, 0);
3034            let (pos, _, _) = oversized
3035                .append(2, new_entry, &new_value)
3036                .await
3037                .expect("Failed to append to section 2");
3038            assert_eq!(pos, 0); // First entry in new section
3039
3040            oversized.destroy().await.expect("Failed to destroy");
3041        });
3042    }
3043
3044    #[test_traced]
3045    fn test_rewind_to_zero_index_size() {
3046        let executor = deterministic::Runner::default();
3047        executor.start(|context| async move {
3048            let cfg = test_cfg(&context);
3049            let mut oversized: Oversized<_, TestEntry, TestValue> =
3050                Oversized::init(context, cfg).await.expect("Failed to init");
3051
3052            let value: TestValue = [1; 16];
3053            let entry = TestEntry::new(1, 0, 0);
3054            oversized
3055                .append(0, entry, &value)
3056                .await
3057                .expect("Failed to append");
3058            oversized.sync(0).await.expect("Failed to sync");
3059
3060            oversized
3061                .rewind(0, 0)
3062                .await
3063                .expect("rewind to zero index_size must not fail");
3064
3065            assert_eq!(oversized.last(0).await.unwrap(), None);
3066            assert_eq!(oversized.size(0).unwrap(), 0);
3067            assert_eq!(oversized.value_size(0).await.unwrap(), 0);
3068
3069            oversized.destroy().await.expect("Failed to destroy");
3070        });
3071    }
3072
3073    #[test_traced]
3074    fn test_rewind_to_zero_on_missing_section() {
3075        let executor = deterministic::Runner::default();
3076        executor.start(|context| async move {
3077            let cfg = test_cfg(&context);
3078            let mut oversized: Oversized<_, TestEntry, TestValue> =
3079                Oversized::init(context, cfg).await.expect("Failed to init");
3080
3081            oversized
3082                .rewind(0, 0)
3083                .await
3084                .expect("rewind on missing section must not fail");
3085
3086            assert!(matches!(
3087                oversized.last(0).await,
3088                Err(Error::SectionOutOfRange(0))
3089            ));
3090            assert_eq!(oversized.value_size(0).await.unwrap(), 0);
3091
3092            oversized.destroy().await.expect("Failed to destroy");
3093        });
3094    }
3095
3096    #[test_traced]
3097    fn test_rewind_nonzero_on_missing_section_errors() {
3098        let executor = deterministic::Runner::default();
3099        executor.start(|context| async move {
3100            let cfg = test_cfg(&context);
3101            let mut oversized: Oversized<_, TestEntry, TestValue> =
3102                Oversized::init(context, cfg).await.expect("Failed to init");
3103
3104            let result = oversized.rewind(0, 1).await;
3105            assert!(
3106                matches!(result, Err(Error::SectionOutOfRange(0))),
3107                "nonzero index_size on missing section must fail, got: {result:?}"
3108            );
3109
3110            oversized.destroy().await.expect("Failed to destroy");
3111        });
3112    }
3113
3114    #[test_traced]
3115    fn test_rewind_section_nonzero_on_missing_section_errors() {
3116        let executor = deterministic::Runner::default();
3117        executor.start(|context| async move {
3118            let cfg = test_cfg(&context);
3119            let mut oversized: Oversized<_, TestEntry, TestValue> =
3120                Oversized::init(context, cfg).await.expect("Failed to init");
3121
3122            let result = oversized.rewind_section(0, 1).await;
3123            assert!(
3124                matches!(result, Err(Error::SectionOutOfRange(0))),
3125                "nonzero index_size on missing section must fail, got: {result:?}"
3126            );
3127
3128            oversized.destroy().await.expect("Failed to destroy");
3129        });
3130    }
3131
3132    #[test_traced]
3133    fn test_last_pruned_section_returns_error() {
3134        let executor = deterministic::Runner::default();
3135        executor.start(|context| async move {
3136            let cfg = test_cfg(&context);
3137            let mut oversized: Oversized<_, TestEntry, TestValue> =
3138                Oversized::init(context, cfg).await.expect("Failed to init");
3139
3140            let value: TestValue = [1; 16];
3141            oversized
3142                .append(0, TestEntry::new(1, 0, 0), &value)
3143                .await
3144                .expect("Failed to append");
3145            oversized
3146                .append(1, TestEntry::new(2, 0, 0), &value)
3147                .await
3148                .expect("Failed to append");
3149            oversized.sync_all().await.expect("Failed to sync");
3150
3151            oversized.prune(1).await.expect("Failed to prune");
3152
3153            assert!(matches!(
3154                oversized.last(0).await,
3155                Err(Error::AlreadyPrunedToSection(1))
3156            ));
3157            assert!(oversized.last(1).await.unwrap().is_some());
3158
3159            oversized.destroy().await.expect("Failed to destroy");
3160        });
3161    }
3162}