Skip to main content

commonware_storage/freezer/
storage.rs

1use super::{Config, Error, Identifier};
2use crate::{
3    Context,
4    journal::segmented::oversized::{
5        Config as OversizedConfig, Oversized, Record as OversizedRecord,
6    },
7};
8use commonware_codec::{CodecShared, FixedArray, FixedSize, Read, ReadExt, Write as CodecWrite};
9use commonware_cryptography::{Crc32, Hasher, crc32};
10use commonware_runtime::{
11    Blob, Buf, BufMut, BufferPooler, IoBuf, ReadOptions, WriteOptions, buffer,
12    iobuf::EncodeExt,
13    telemetry::metrics::{Counter, MetricsExt as _},
14};
15use commonware_utils::{Array, Span};
16use futures::future::try_join;
17use std::{cmp::Ordering, collections::BTreeSet, num::NonZeroUsize, ops::Deref};
18use tracing::debug;
19
20/// The percentage of table entries that must reach `table_resize_frequency`
21/// before a resize is triggered.
22const RESIZE_THRESHOLD: u64 = 50;
23
24/// Location of an item in the [Freezer].
25///
26/// This can be used to directly access the data for a given
27/// key-value pair (rather than walking the journal chain).
28#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, FixedArray)]
29#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
30#[repr(transparent)]
31pub struct Cursor([u8; u64::SIZE + u64::SIZE + u32::SIZE]);
32
33impl Cursor {
34    /// Create a new [Cursor].
35    fn new(section: u64, offset: u64, size: u32) -> Self {
36        let mut buf = [0u8; u64::SIZE + u64::SIZE + u32::SIZE];
37        buf[..u64::SIZE].copy_from_slice(&section.to_be_bytes());
38        buf[u64::SIZE..u64::SIZE + u64::SIZE].copy_from_slice(&offset.to_be_bytes());
39        buf[u64::SIZE + u64::SIZE..].copy_from_slice(&size.to_be_bytes());
40        Self(buf)
41    }
42
43    /// Get the section of the cursor.
44    fn section(&self) -> u64 {
45        u64::from_be_bytes(self.0[..u64::SIZE].try_into().unwrap())
46    }
47
48    /// Get the offset of the cursor.
49    fn offset(&self) -> u64 {
50        u64::from_be_bytes(self.0[u64::SIZE..u64::SIZE + u64::SIZE].try_into().unwrap())
51    }
52
53    /// Get the size of the value.
54    fn size(&self) -> u32 {
55        u32::from_be_bytes(self.0[u64::SIZE + u64::SIZE..].try_into().unwrap())
56    }
57}
58
59impl Read for Cursor {
60    type Cfg = ();
61
62    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
63        <[u8; u64::SIZE + u64::SIZE + u32::SIZE]>::read(buf).map(Self)
64    }
65}
66
67impl CodecWrite for Cursor {
68    fn write(&self, buf: &mut impl BufMut) {
69        self.0.write(buf);
70    }
71}
72
73impl FixedSize for Cursor {
74    const SIZE: usize = u64::SIZE + u64::SIZE + u32::SIZE;
75}
76
77impl Span for Cursor {}
78
79impl Array for Cursor {}
80
81impl Deref for Cursor {
82    type Target = [u8];
83    fn deref(&self) -> &Self::Target {
84        &self.0
85    }
86}
87
88impl AsRef<[u8]> for Cursor {
89    fn as_ref(&self) -> &[u8] {
90        &self.0
91    }
92}
93
94impl std::fmt::Debug for Cursor {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        write!(
97            f,
98            "Cursor(section={}, offset={}, size={})",
99            self.section(),
100            self.offset(),
101            self.size()
102        )
103    }
104}
105
106impl std::fmt::Display for Cursor {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(
109            f,
110            "Cursor(section={}, offset={}, size={})",
111            self.section(),
112            self.offset(),
113            self.size()
114        )
115    }
116}
117
118/// Marker of [Freezer] progress.
119///
120/// This can be used to restore the [Freezer] to a consistent
121/// state after shutdown.
122#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)]
123#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
124pub struct Checkpoint {
125    /// The epoch of the last committed operation.
126    epoch: u64,
127    /// The section of the last committed operation.
128    section: u64,
129    /// The size of the oversized index journal in the last committed section.
130    oversized_size: u64,
131    /// The size of the table.
132    table_size: u32,
133}
134
135impl Checkpoint {
136    /// Initialize a new [Checkpoint].
137    const fn init(table_size: u32) -> Self {
138        Self {
139            table_size,
140            epoch: 0,
141            section: 0,
142            oversized_size: 0,
143        }
144    }
145
146    /// Return true if this checkpoint represents a fresh [Freezer].
147    const fn is_empty(&self) -> bool {
148        self.epoch == 0 && self.section == 0 && self.oversized_size == 0 && self.table_size == 0
149    }
150}
151
152impl Read for Checkpoint {
153    type Cfg = ();
154    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, commonware_codec::Error> {
155        let epoch = u64::read(buf)?;
156        let section = u64::read(buf)?;
157        let oversized_size = u64::read(buf)?;
158        let table_size = u32::read(buf)?;
159        Ok(Self {
160            epoch,
161            section,
162            oversized_size,
163            table_size,
164        })
165    }
166}
167
168impl CodecWrite for Checkpoint {
169    fn write(&self, buf: &mut impl BufMut) {
170        self.epoch.write(buf);
171        self.section.write(buf);
172        self.oversized_size.write(buf);
173        self.table_size.write(buf);
174    }
175}
176
177impl FixedSize for Checkpoint {
178    const SIZE: usize = u64::SIZE + u64::SIZE + u64::SIZE + u32::SIZE;
179}
180
181/// Name of the table blob.
182const TABLE_BLOB_NAME: &[u8] = b"table";
183
184/// Single table entry stored in the table blob.
185#[derive(Debug, Clone, PartialEq)]
186#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
187struct Entry {
188    // Epoch in which this slot was written
189    epoch: u64,
190    // Section in which this slot was written
191    section: u64,
192    // Position in the key index for this section
193    position: u64,
194    // Number of items added to this entry since last resize
195    added: u8,
196    // CRC of (epoch | section | position | added)
197    crc: u32,
198}
199
200impl Entry {
201    /// The full size of a table entry (2 slots).
202    const FULL_SIZE: usize = Self::SIZE * 2;
203
204    /// Compute a checksum for [Entry].
205    fn compute_crc(epoch: u64, section: u64, position: u64, added: u8) -> u32 {
206        Crc32::hash(&[
207            &epoch.to_be_bytes(),
208            &section.to_be_bytes(),
209            &position.to_be_bytes(),
210            &added.to_be_bytes(),
211        ])
212        .as_u32()
213    }
214
215    /// Create a new [Entry].
216    fn new(epoch: u64, section: u64, position: u64, added: u8) -> Self {
217        Self {
218            epoch,
219            section,
220            position,
221            added,
222            crc: Self::compute_crc(epoch, section, position, added),
223        }
224    }
225
226    /// Create a new empty [Entry].
227    const fn new_empty() -> Self {
228        Self {
229            epoch: 0,
230            section: 0,
231            position: 0,
232            added: 0,
233            crc: 0,
234        }
235    }
236
237    /// Check if this entry is empty (all zeros).
238    const fn is_empty(&self) -> bool {
239        self.epoch == 0
240            && self.section == 0
241            && self.position == 0
242            && self.added == 0
243            && self.crc == 0
244    }
245
246    /// Check if this entry is valid.
247    ///
248    /// An empty entry does not have a valid checksum and is treated as invalid by this function.
249    fn is_valid(&self) -> bool {
250        Self::compute_crc(self.epoch, self.section, self.position, self.added) == self.crc
251    }
252}
253
254impl FixedSize for Entry {
255    const SIZE: usize = u64::SIZE + u64::SIZE + u64::SIZE + u8::SIZE + crc32::Digest::SIZE;
256}
257
258impl CodecWrite for Entry {
259    fn write(&self, buf: &mut impl BufMut) {
260        self.epoch.write(buf);
261        self.section.write(buf);
262        self.position.write(buf);
263        self.added.write(buf);
264        self.crc.write(buf);
265    }
266}
267
268impl Read for Entry {
269    type Cfg = ();
270    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
271        let epoch = u64::read(buf)?;
272        let section = u64::read(buf)?;
273        let position = u64::read(buf)?;
274        let added = u8::read(buf)?;
275        let crc = u32::read(buf)?;
276
277        Ok(Self {
278            epoch,
279            section,
280            position,
281            added,
282            crc,
283        })
284    }
285}
286
287/// Sentinel value indicating no next entry in the collision chain.
288const NO_NEXT_SECTION: u64 = u64::MAX;
289const NO_NEXT_POSITION: u64 = u64::MAX;
290
291/// Key entry stored in the segmented/fixed key index journal.
292///
293/// All fields are fixed size, enabling efficient collision chain traversal
294/// without reading large values.
295///
296/// The `next` pointer uses sentinel values (u64::MAX, u64::MAX) to indicate
297/// "no next entry" instead of Option, ensuring fixed-size encoding.
298#[derive(Debug, Clone, PartialEq)]
299struct Record<K: Array> {
300    /// The key for this entry.
301    key: K,
302    /// Pointer to next entry in collision chain (section, position in key index).
303    /// Uses (u64::MAX, u64::MAX) as sentinel for "no next".
304    next_section: u64,
305    next_position: u64,
306    /// Byte offset in value journal (same section).
307    value_offset: u64,
308    /// Size of value data in the value journal.
309    value_size: u32,
310}
311
312impl<K: Array> Record<K> {
313    /// Create a new [Record].
314    fn new(key: K, next: Option<(u64, u64)>, value_offset: u64, value_size: u32) -> Self {
315        let (next_section, next_position) = next.unwrap_or((NO_NEXT_SECTION, NO_NEXT_POSITION));
316        Self {
317            key,
318            next_section,
319            next_position,
320            value_offset,
321            value_size,
322        }
323    }
324
325    /// Get the next entry in the collision chain, if any.
326    const fn next(&self) -> Option<(u64, u64)> {
327        if self.next_section == NO_NEXT_SECTION && self.next_position == NO_NEXT_POSITION {
328            None
329        } else {
330            Some((self.next_section, self.next_position))
331        }
332    }
333}
334
335impl<K: Array> CodecWrite for Record<K> {
336    fn write(&self, buf: &mut impl BufMut) {
337        self.key.write(buf);
338        self.next_section.write(buf);
339        self.next_position.write(buf);
340        self.value_offset.write(buf);
341        self.value_size.write(buf);
342    }
343}
344
345impl<K: Array> Read for Record<K> {
346    type Cfg = ();
347    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
348        let key = K::read(buf)?;
349        let next_section = u64::read(buf)?;
350        let next_position = u64::read(buf)?;
351        let value_offset = u64::read(buf)?;
352        let value_size = u32::read(buf)?;
353
354        Ok(Self {
355            key,
356            next_section,
357            next_position,
358            value_offset,
359            value_size,
360        })
361    }
362}
363
364impl<K: Array> FixedSize for Record<K> {
365    // key + next_section + next_position + value_offset + value_size
366    const SIZE: usize = K::SIZE + u64::SIZE + u64::SIZE + u64::SIZE + u32::SIZE;
367}
368
369impl<K: Array> OversizedRecord for Record<K> {
370    fn value_location(&self) -> (u64, u32) {
371        (self.value_offset, self.value_size)
372    }
373
374    fn with_location(mut self, offset: u64, size: u32) -> Self {
375        self.value_offset = offset;
376        self.value_size = size;
377        self
378    }
379}
380
381#[cfg(feature = "arbitrary")]
382impl<K: Array> arbitrary::Arbitrary<'_> for Record<K>
383where
384    K: for<'a> arbitrary::Arbitrary<'a>,
385{
386    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
387        Ok(Self {
388            key: K::arbitrary(u)?,
389            next_section: u64::arbitrary(u)?,
390            next_position: u64::arbitrary(u)?,
391            value_offset: u64::arbitrary(u)?,
392            value_size: u32::arbitrary(u)?,
393        })
394    }
395}
396
397/// The freezer's state, boxed so the public [Freezer] handle stays pointer-sized.
398struct Inner<E: Context, K: Array, V: CodecShared> {
399    // Context for storage operations
400    context: E,
401
402    // Table configuration
403    table_partition: String,
404    table_size: u32,
405    table_resize_threshold: u64,
406    table_resize_frequency: u8,
407    table_resize_chunk_size: u32,
408
409    // Table blob that maps slots to key index chain heads
410    table: E::Blob,
411
412    // Combined key index + value storage with crash recovery
413    oversized: Oversized<E, Record<K>, V>,
414
415    // Target size for value blob sections
416    blob_target_size: u64,
417
418    // Current section for new writes
419    current_section: u64,
420    next_epoch: u64,
421
422    // Sections with pending table updates to be synced
423    modified_sections: BTreeSet<u64>,
424    resizable: u32,
425    resize_progress: Option<u32>,
426
427    // Metrics
428    puts: Counter,
429    gets: Counter,
430    has: Counter,
431    unnecessary_reads: Counter,
432    unnecessary_writes: Counter,
433    resizes: Counter,
434}
435
436impl<E: Context, K: Array, V: CodecShared> Inner<E, K, V> {
437    /// Calculate the byte offset for a table index.
438    #[inline]
439    const fn table_offset(table_index: u32) -> u64 {
440        table_index as u64 * Entry::FULL_SIZE as u64
441    }
442
443    /// Parse table entries from a buffer.
444    fn parse_entries(mut buf: impl Buf) -> Result<(Entry, Entry), Error> {
445        let entry1 = Entry::read(&mut buf)?;
446        let entry2 = Entry::read(&mut buf)?;
447        Ok((entry1, entry2))
448    }
449
450    /// Read entries from the table blob.
451    async fn read_table(blob: &E::Blob, table_index: u32) -> Result<(Entry, Entry), Error> {
452        let offset = Self::table_offset(table_index);
453        let read_buf = blob
454            .read_at(offset, Entry::FULL_SIZE, ReadOptions::default())
455            .await?;
456
457        Self::parse_entries(read_buf)
458    }
459
460    /// Recover a single table entry and update tracking.
461    async fn recover_entry(
462        blob: &E::Blob,
463        entry: &mut Entry,
464        entry_offset: u64,
465        max_valid_epoch: Option<u64>,
466        max_epoch: &mut u64,
467        max_section: &mut u64,
468    ) -> Result<bool, Error> {
469        if entry.is_empty() {
470            return Ok(false);
471        }
472
473        if !entry.is_valid()
474            || (max_valid_epoch.is_some() && entry.epoch > max_valid_epoch.unwrap())
475        {
476            debug!(
477                valid_epoch = max_valid_epoch,
478                entry_epoch = entry.epoch,
479                "found invalid table entry"
480            );
481            *entry = Entry::new_empty();
482            let zero_buf = IoBuf::from(&[0u8; Entry::SIZE]);
483            blob.write_at(entry_offset, zero_buf, WriteOptions::default())
484                .await?;
485            Ok(true)
486        } else if max_valid_epoch.is_none() && entry.epoch > *max_epoch {
487            // Only track max epoch if we're discovering it (not validating against a known epoch)
488            *max_epoch = entry.epoch;
489            *max_section = entry.section;
490            Ok(false)
491        } else {
492            Ok(false)
493        }
494    }
495
496    /// Validate and clean invalid table entries for a given epoch.
497    ///
498    /// Returns (modified, max_epoch, max_section, resizable) where:
499    /// - modified: whether any entries were cleaned
500    /// - max_epoch: the maximum valid epoch found
501    /// - max_section: the section corresponding to `max_epoch`
502    /// - resizable: the number of entries that can be resized
503    async fn recover_table(
504        pooler: &impl BufferPooler,
505        blob: &E::Blob,
506        table_size: u32,
507        table_resize_frequency: u8,
508        max_valid_epoch: Option<u64>,
509        table_replay_buffer: NonZeroUsize,
510    ) -> Result<(bool, u64, u64, u32), Error> {
511        // Create a buffered reader for efficient scanning
512        let blob_size = Self::table_offset(table_size);
513        let mut reader =
514            buffer::Read::from_pooler(pooler, blob.clone(), blob_size, table_replay_buffer);
515
516        // Iterate over all table entries and overwrite invalid ones
517        let mut modified = false;
518        let mut max_epoch = 0u64;
519        let mut max_section = 0u64;
520        let mut resizable = 0u32;
521        for table_index in 0..table_size {
522            let offset = Self::table_offset(table_index);
523
524            // Read both entries from the buffer.
525            let entry_buf = reader.read(Entry::FULL_SIZE).await?;
526            let (mut entry1, mut entry2) = Self::parse_entries(entry_buf)?;
527
528            // Check both entries
529            let entry1_cleared = Self::recover_entry(
530                blob,
531                &mut entry1,
532                offset,
533                max_valid_epoch,
534                &mut max_epoch,
535                &mut max_section,
536            )
537            .await?;
538            let entry2_cleared = Self::recover_entry(
539                blob,
540                &mut entry2,
541                offset + Entry::SIZE as u64,
542                max_valid_epoch,
543                &mut max_epoch,
544                &mut max_section,
545            )
546            .await?;
547            modified |= entry1_cleared || entry2_cleared;
548
549            // If the latest entry has reached the resize frequency, increment the resizable entries
550            if let Some((_, _, added)) = Self::read_latest_entry(&entry1, &entry2)
551                && added >= table_resize_frequency
552            {
553                resizable += 1;
554            }
555        }
556
557        Ok((modified, max_epoch, max_section, resizable))
558    }
559
560    /// Determine the write offset for a table entry based on current entries and epoch.
561    const fn compute_write_offset(entry1: &Entry, entry2: &Entry, epoch: u64) -> u64 {
562        // If either entry matches the current epoch, overwrite it
563        if !entry1.is_empty() && entry1.epoch == epoch {
564            return 0;
565        }
566        if !entry2.is_empty() && entry2.epoch == epoch {
567            return Entry::SIZE as u64;
568        }
569
570        // Otherwise, write to the older slot (or empty slot)
571        match (entry1.is_empty(), entry2.is_empty()) {
572            (true, _) => 0,                  // First slot is empty
573            (_, true) => Entry::SIZE as u64, // Second slot is empty
574            (false, false) => {
575                if entry1.epoch < entry2.epoch {
576                    0
577                } else {
578                    Entry::SIZE as u64
579                }
580            }
581        }
582    }
583
584    /// Read the latest valid entry from two table slots.
585    fn read_latest_entry(entry1: &Entry, entry2: &Entry) -> Option<(u64, u64, u8)> {
586        match (
587            !entry1.is_empty() && entry1.is_valid(),
588            !entry2.is_empty() && entry2.is_valid(),
589        ) {
590            (true, true) => match entry1.epoch.cmp(&entry2.epoch) {
591                Ordering::Greater => Some((entry1.section, entry1.position, entry1.added)),
592                Ordering::Less => Some((entry2.section, entry2.position, entry2.added)),
593                Ordering::Equal => {
594                    unreachable!("two valid entries with the same epoch")
595                }
596            },
597            (true, false) => Some((entry1.section, entry1.position, entry1.added)),
598            (false, true) => Some((entry2.section, entry2.position, entry2.added)),
599            (false, false) => None,
600        }
601    }
602
603    /// Write a table entry to the appropriate slot based on epoch.
604    async fn update_head(
605        pooler: &impl BufferPooler,
606        table: &E::Blob,
607        table_index: u32,
608        entry1: &Entry,
609        entry2: &Entry,
610        update: Entry,
611    ) -> Result<(), Error> {
612        // Calculate the base offset for this table index
613        let table_offset = Self::table_offset(table_index);
614
615        // Determine which slot to write to based on the provided entries
616        let start = Self::compute_write_offset(entry1, entry2, update.epoch);
617
618        // Write the new entry
619        table
620            .write_at(
621                table_offset + start,
622                update.encode_with_pool_mut(pooler.storage_buffer_pool()),
623                WriteOptions::default(),
624            )
625            .await
626            .map_err(Error::Runtime)
627    }
628
629    /// Initialize table with given size and sync.
630    async fn init_table(blob: &E::Blob, table_size: u32) -> Result<(), Error> {
631        let table_len = Self::table_offset(table_size);
632        blob.resize(table_len).await?;
633        blob.sync().await?;
634        Ok(())
635    }
636
637    /// See [Freezer::init].
638    async fn init(
639        context: E,
640        config: Config<V::Cfg>,
641        checkpoint: Option<Checkpoint>,
642    ) -> Result<Self, Error> {
643        // Validate that initial_table_size is a power of 2
644        assert!(
645            config.table_initial_size > 0 && config.table_initial_size.is_power_of_two(),
646            "table_initial_size must be a power of 2"
647        );
648
649        // A missing or empty checkpoint starts fresh: delete all existing freezer data
650        let reset = checkpoint.is_none_or(|checkpoint| checkpoint.is_empty());
651        if reset {
652            for partition in [
653                &config.key_partition,
654                &config.value_partition,
655                &config.table_partition,
656            ] {
657                match context.remove(partition, None).await {
658                    Ok(()) | Err(commonware_runtime::Error::PartitionMissing(_)) => {}
659                    Err(err) => return Err(Error::Runtime(err)),
660                }
661            }
662        }
663
664        // Initialize oversized journal. A checkpoint is only published after the
665        // oversized journal is durably synced (see Self::sync), so recovery restores
666        // exactly the checkpointed state: committed data it covers cannot be silently
667        // repaired away, and anything beyond it is discarded.
668        let oversized_cfg = OversizedConfig {
669            index_partition: config.key_partition.clone(),
670            value_partition: config.value_partition.clone(),
671            index_page_cache: config.key_page_cache.clone(),
672            index_write_buffer: config.key_write_buffer,
673            value_write_buffer: config.value_write_buffer,
674            replay_buffer: config.table_replay_buffer,
675            compression: config.value_compression,
676            codec_config: config.codec_config,
677        };
678        let oversized_context = context.child("oversized");
679        let oversized: Oversized<E, Record<K>, V> = match checkpoint
680            .filter(|checkpoint| !checkpoint.is_empty())
681            .map(|checkpoint| (checkpoint.section, checkpoint.oversized_size))
682        {
683            Some(checkpoint) => {
684                Oversized::init_with_checkpoint(oversized_context, oversized_cfg, checkpoint)
685                    .await?
686            }
687            None => Oversized::init(oversized_context, oversized_cfg).await?,
688        };
689
690        // Open table blob
691        let (table, table_len) = context
692            .open(&config.table_partition, TABLE_BLOB_NAME)
693            .await?;
694
695        // Determine checkpoint based on initialization scenario
696        let (checkpoint, resizable) = match checkpoint {
697            // Non-empty checkpoint: align existing data to it
698            Some(checkpoint) if !checkpoint.is_empty() => {
699                // A non-empty checkpoint against an empty table references data that does not exist
700                if table_len == 0 {
701                    return Err(Error::CheckpointMismatch);
702                }
703                assert!(
704                    checkpoint.table_size > 0 && checkpoint.table_size.is_power_of_two(),
705                    "table_size must be a power of 2"
706                );
707
708                // Resize the table if needed. Growing is never valid: the checkpoint publishes
709                // only after the table sync completes, so a shorter table cannot back the
710                // checkpointed entries, and zero-extending would fabricate empty heads.
711                let expected_table_len = Self::table_offset(checkpoint.table_size);
712                if table_len < expected_table_len {
713                    return Err(Error::CheckpointMismatch);
714                }
715                let mut modified = if table_len != expected_table_len {
716                    table.resize(expected_table_len).await?;
717                    true
718                } else {
719                    false
720                };
721
722                // Validate and clean invalid entries
723                let (table_modified, _, _, resizable) = Self::recover_table(
724                    &context,
725                    &table,
726                    checkpoint.table_size,
727                    config.table_resize_frequency,
728                    Some(checkpoint.epoch),
729                    config.table_replay_buffer,
730                )
731                .await?;
732                if table_modified {
733                    modified = true;
734                }
735
736                // Sync table if needed
737                if modified {
738                    table.sync().await?;
739                }
740
741                (checkpoint, resizable)
742            }
743
744            // Missing or empty checkpoint: reset wiped any existing data, so initialize a new table
745            _ => {
746                Self::init_table(&table, config.table_initial_size).await?;
747                (Checkpoint::init(config.table_initial_size), 0)
748            }
749        };
750
751        // Create metrics
752        let puts = context.counter("puts", "number of put operations");
753        let gets = context.counter("gets", "number of get operations");
754        let has = context.counter("has", "number of has operations");
755        let unnecessary_reads = context.counter(
756            "unnecessary_reads",
757            "number of unnecessary reads performed during key lookups",
758        );
759        let unnecessary_writes = context.counter(
760            "unnecessary_writes",
761            "number of unnecessary writes performed during resize",
762        );
763        let resizes = context.counter("resizes", "number of table resizing operations");
764
765        Ok(Self {
766            context,
767            table_partition: config.table_partition,
768            table_size: checkpoint.table_size,
769            table_resize_threshold: checkpoint.table_size as u64 * RESIZE_THRESHOLD / 100,
770            table_resize_frequency: config.table_resize_frequency,
771            table_resize_chunk_size: config.table_resize_chunk_size,
772            table,
773            oversized,
774            blob_target_size: config.value_target_size,
775            current_section: checkpoint.section,
776            next_epoch: checkpoint.epoch.checked_add(1).expect("epoch overflow"),
777            modified_sections: BTreeSet::new(),
778            resizable,
779            resize_progress: None,
780            puts,
781            gets,
782            has,
783            unnecessary_reads,
784            unnecessary_writes,
785            resizes,
786        })
787    }
788
789    /// Compute the table index for a given key.
790    ///
791    /// As the table doubles in size during a resize, each existing entry splits into two:
792    /// one at the original index and another at a new index (original index + previous table size).
793    ///
794    /// For example, with an initial table size of 4 (2^2):
795    /// - Initially: uses 2 bits of the hash, mapping to entries 0, 1, 2, 3.
796    /// - After resizing to 8: uses 3 bits, entry 0 splits into indices 0 and 4.
797    /// - After resizing to 16: uses 4 bits, entry 0 splits into indices 0 and 8, and so on.
798    ///
799    /// To determine the appropriate entry, we AND the key's hash with the current table size.
800    fn table_index(&self, key: &K) -> u32 {
801        let hash = Crc32::checksum(key.as_ref());
802        hash & (self.table_size - 1)
803    }
804
805    /// Determine if the table should be resized.
806    const fn should_resize(&self) -> bool {
807        self.resizable as u64 >= self.table_resize_threshold
808    }
809
810    /// Determine which blob section to write to based on current blob size.
811    async fn update_section(&mut self) -> Result<(), Error> {
812        // Get the current value blob section size
813        let value_size = self.oversized.value_size(self.current_section).await?;
814
815        // If the current section has reached the target size, create a new section
816        if value_size >= self.blob_target_size {
817            self.current_section += 1;
818            debug!(
819                size = value_size,
820                section = self.current_section,
821                "updated section"
822            );
823        }
824
825        Ok(())
826    }
827
828    /// See [Freezer::put].
829    async fn put(mut self: Box<Self>, key: K, value: V) -> Result<(Box<Self>, Cursor), Error> {
830        self.puts.inc();
831
832        // Update the section if needed
833        self.update_section().await?;
834
835        // Get head of the chain from table
836        let table_index = self.table_index(&key);
837        let (entry1, entry2) = Self::read_table(&self.table, table_index).await?;
838        let head = Self::read_latest_entry(&entry1, &entry2);
839
840        // Create key entry with pointer to previous head (value location set by oversized.append)
841        let key_entry = Record::new(
842            key,
843            head.map(|(section, position, _)| (section, position)),
844            0,
845            0,
846        );
847
848        // Write value and key entry (glob first, then index)
849        let (position, value_offset, value_size);
850        (self.oversized, position, value_offset, value_size) = self
851            .oversized
852            .append(self.current_section, key_entry, &value)
853            .await?;
854
855        // Update the number of items added to the entry.
856        //
857        // We use `saturating_add` to handle overflow (when the table is at max size) gracefully.
858        let mut added = head.map(|(_, _, added)| added).unwrap_or(0);
859        added = added.saturating_add(1);
860
861        // If we've reached the threshold for resizing, increment the resizable entries
862        if added == self.table_resize_frequency {
863            self.resizable += 1;
864        }
865
866        // Update the old position
867        self.modified_sections.insert(self.current_section);
868        let new_entry = Entry::new(self.next_epoch, self.current_section, position, added);
869        Self::update_head(
870            &self.context,
871            &self.table,
872            table_index,
873            &entry1,
874            &entry2,
875            new_entry,
876        )
877        .await?;
878
879        // If we're mid-resize and this entry has already been processed, update the new position too
880        if let Some(resize_progress) = self.resize_progress
881            && table_index < resize_progress
882        {
883            self.unnecessary_writes.inc();
884
885            // If the previous entry crossed the threshold, so did this one
886            if added == self.table_resize_frequency {
887                self.resizable += 1;
888            }
889
890            // This entry has been processed, so we need to update the new position as well.
891            //
892            // The entries are still identical to the old ones, so we don't need to read them again.
893            let new_table_index = self.table_size + table_index;
894            let new_entry = Entry::new(self.next_epoch, self.current_section, position, added);
895            Self::update_head(
896                &self.context,
897                &self.table,
898                new_table_index,
899                &entry1,
900                &entry2,
901                new_entry,
902            )
903            .await?;
904        }
905
906        let cursor = Cursor::new(self.current_section, value_offset, value_size);
907        Ok((self, cursor))
908    }
909
910    /// Get the value for a given [Cursor].
911    async fn get_cursor(&self, cursor: Cursor) -> Result<V, Error> {
912        let value = self
913            .oversized
914            .get_value(cursor.section(), cursor.offset(), cursor.size())
915            .await?;
916
917        Ok(value)
918    }
919
920    /// Find the first key entry matching `key`, returning it with its section.
921    ///
922    /// Reads key entries only, never values.
923    async fn find_key(&self, key: &K) -> Result<Option<(u64, Record<K>)>, Error> {
924        // Get head of the chain from table
925        let table_index = self.table_index(key);
926        let (entry1, entry2) = Self::read_table(&self.table, table_index).await?;
927        let Some((mut section, mut position, _)) = Self::read_latest_entry(&entry1, &entry2) else {
928            return Ok(None);
929        };
930
931        // Follow the linked list chain to find the first matching key
932        loop {
933            // Get the key entry from the fixed key index (efficient, good cache locality)
934            let key_entry = self.oversized.get(section, position).await?;
935
936            // Check if this key matches
937            if key_entry.key.as_ref() == key.as_ref() {
938                return Ok(Some((section, key_entry)));
939            }
940
941            // Increment unnecessary reads
942            self.unnecessary_reads.inc();
943
944            // Follow the chain
945            let Some(next) = key_entry.next() else {
946                break; // End of chain
947            };
948            section = next.0;
949            position = next.1;
950        }
951
952        Ok(None)
953    }
954
955    /// Get the first value for a given key.
956    async fn get_key(&self, key: &K) -> Result<Option<V>, Error> {
957        self.gets.inc();
958
959        let Some((section, key_entry)) = self.find_key(key).await? else {
960            return Ok(None);
961        };
962        let value = self
963            .oversized
964            .get_value(section, key_entry.value_offset, key_entry.value_size)
965            .await?;
966        Ok(Some(value))
967    }
968
969    /// See [Freezer::get].
970    async fn get<'a>(&'a self, identifier: Identifier<'a, K>) -> Result<Option<V>, Error> {
971        match identifier {
972            Identifier::Cursor(cursor) => self.get_cursor(cursor).await.map(Some),
973            Identifier::Key(key) => self.get_key(key).await,
974        }
975    }
976
977    /// See [Freezer::has].
978    async fn has(&self, key: &K) -> Result<bool, Error> {
979        self.has.inc();
980
981        Ok(self.find_key(key).await?.is_some())
982    }
983
984    /// Resize the table by doubling its size and split each entry into two.
985    async fn start_resize(&mut self) -> Result<(), Error> {
986        self.resizes.inc();
987
988        // Double the table size (if not already at the max size)
989        let old_size = self.table_size;
990        let Some(new_size) = old_size.checked_mul(2) else {
991            return Ok(());
992        };
993        self.table.resize(Self::table_offset(new_size)).await?;
994
995        // Start the resize
996        self.resize_progress = Some(0);
997        debug!(old = old_size, new = new_size, "table resize started");
998
999        Ok(())
1000    }
1001
1002    /// Write a pair of entries to a buffer, replacing one slot with the new entry.
1003    fn rewrite_entries(buf: &mut impl BufMut, entry1: &Entry, entry2: &Entry, new_entry: &Entry) {
1004        if Self::compute_write_offset(entry1, entry2, new_entry.epoch) == 0 {
1005            new_entry.write(buf);
1006            entry2.write(buf);
1007        } else {
1008            entry1.write(buf);
1009            new_entry.write(buf);
1010        }
1011    }
1012
1013    /// Continue a resize operation by processing the next chunk of entries.
1014    ///
1015    /// This function processes `table_resize_chunk_size` entries at a time, allowing the resize to
1016    /// be spread across multiple sync operations to avoid latency spikes.
1017    async fn advance_resize(&mut self) -> Result<(), Error> {
1018        // Compute the range to update
1019        let current_index = self.resize_progress.unwrap();
1020        let old_size = self.table_size;
1021        let chunk_end = (current_index + self.table_resize_chunk_size).min(old_size);
1022        let chunk_size = chunk_end - current_index;
1023
1024        // Read the entire chunk
1025        let chunk_bytes = chunk_size as usize * Entry::FULL_SIZE;
1026        let read_offset = Self::table_offset(current_index);
1027        let mut read_buf = self
1028            .table
1029            .read_at(read_offset, chunk_bytes, ReadOptions::default())
1030            .await?;
1031
1032        // Process each entry in the chunk
1033        let mut writes = self.context.storage_buffer_pool().alloc(chunk_bytes);
1034        for _ in 0..chunk_size {
1035            // Parse the next two slots directly from the read stream.
1036            let (entry1, entry2) = Self::parse_entries(&mut read_buf)?;
1037
1038            // Get the current head
1039            let head = Self::read_latest_entry(&entry1, &entry2);
1040
1041            // Get the reset entry (may be empty)
1042            let reset_entry = match head {
1043                Some((section, position, added)) => {
1044                    // If the entry was at or over the threshold, decrement the resizable entries.
1045                    if added >= self.table_resize_frequency {
1046                        self.resizable -= 1;
1047                    }
1048                    Entry::new(self.next_epoch, section, position, 0)
1049                }
1050                None => Entry::new_empty(),
1051            };
1052
1053            // Rewrite the entries
1054            Self::rewrite_entries(&mut writes, &entry1, &entry2, &reset_entry);
1055        }
1056
1057        // Put the writes into the table.
1058        let writes = writes.freeze();
1059        let old_write = self
1060            .table
1061            .write_at(read_offset, writes.clone(), WriteOptions::default());
1062        let new_offset = (old_size as usize * Entry::FULL_SIZE) as u64 + read_offset;
1063        let new_write = self
1064            .table
1065            .write_at(new_offset, writes, WriteOptions::default());
1066        try_join(old_write, new_write).await?;
1067
1068        // Update progress
1069        if chunk_end >= old_size {
1070            // Resize complete
1071            self.table_size = old_size * 2;
1072            self.table_resize_threshold = self.table_size as u64 * RESIZE_THRESHOLD / 100;
1073            self.resize_progress = None;
1074            debug!(
1075                old = old_size,
1076                new = self.table_size,
1077                "table resize completed"
1078            );
1079        } else {
1080            // More chunks to process
1081            self.resize_progress = Some(chunk_end);
1082            debug!(current = current_index, chunk_end, "table resize progress");
1083        }
1084
1085        Ok(())
1086    }
1087
1088    /// See [Freezer::sync].
1089    async fn sync(mut self: Box<Self>) -> Result<(Box<Self>, Checkpoint), Error> {
1090        // Sync all modified sections for oversized journal
1091        self.oversized = self.oversized.sync(&self.modified_sections).await?;
1092        self.modified_sections.clear();
1093
1094        // Start a resize (if needed)
1095        if self.should_resize() && self.resize_progress.is_none() {
1096            self.start_resize().await?;
1097        }
1098
1099        // Continue a resize (if ongoing)
1100        if self.resize_progress.is_some() {
1101            self.advance_resize().await?;
1102        }
1103
1104        // Sync updated table entries
1105        self.table.sync().await?;
1106        let stored_epoch = self.next_epoch;
1107        self.next_epoch = self.next_epoch.checked_add(1).expect("epoch overflow");
1108
1109        // Get size from oversized
1110        let oversized_size = self.oversized.size(self.current_section)?;
1111
1112        let checkpoint = Checkpoint {
1113            epoch: stored_epoch,
1114            section: self.current_section,
1115            oversized_size,
1116            table_size: self.table_size,
1117        };
1118        Ok((self, checkpoint))
1119    }
1120
1121    /// See [Freezer::close].
1122    async fn close(mut self: Box<Self>) -> Result<Checkpoint, Error> {
1123        // If we're mid-resize, complete it
1124        while self.resize_progress.is_some() {
1125            self.advance_resize().await?;
1126        }
1127
1128        // Sync any pending updates before closing
1129        let (_, checkpoint) = self.sync().await?;
1130
1131        Ok(checkpoint)
1132    }
1133
1134    /// See [Freezer::destroy].
1135    async fn destroy(self) -> Result<(), Error> {
1136        // Destroy oversized journal
1137        self.oversized.destroy().await?;
1138
1139        // Destroy the table
1140        drop(self.table);
1141        self.context
1142            .remove(&self.table_partition, Some(TABLE_BLOB_NAME))
1143            .await?;
1144        self.context.remove(&self.table_partition, None).await?;
1145
1146        Ok(())
1147    }
1148}
1149
1150/// Implementation of [Freezer].
1151///
1152/// Mutating functions consume the freezer and return it only on success: an error (or a dropped
1153/// future) destroys the handle.
1154pub struct Freezer<E: Context, K: Array, V: CodecShared>(Box<Inner<E, K, V>>);
1155
1156impl<E: Context, K: Array, V: CodecShared> std::fmt::Debug for Freezer<E, K, V> {
1157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1158        f.debug_struct("Freezer")
1159            .field("current_section", &self.0.current_section)
1160            .field("next_epoch", &self.0.next_epoch)
1161            .finish_non_exhaustive()
1162    }
1163}
1164
1165impl<E: Context, K: Array, V: CodecShared> Freezer<E, K, V> {
1166    /// Initialize a [Freezer] instance, aligning existing data to a [Checkpoint] when provided.
1167    ///
1168    /// Passing `None` or an empty [Checkpoint] deletes any existing freezer data and starts empty.
1169    pub async fn init(
1170        context: E,
1171        config: Config<V::Cfg>,
1172        checkpoint: Option<Checkpoint>,
1173    ) -> Result<Self, Error> {
1174        Ok(Self(Box::new(
1175            Inner::init(context, config, checkpoint).await?,
1176        )))
1177    }
1178
1179    /// Put a key-value pair into the [Freezer].
1180    /// If the key already exists, the value is updated.
1181    pub async fn put(mut self, key: K, value: V) -> Result<(Self, Cursor), Error> {
1182        let cursor;
1183        (self.0, cursor) = self.0.put(key, value).await?;
1184        Ok((self, cursor))
1185    }
1186
1187    /// Get the value for a given [Identifier].
1188    ///
1189    /// If a [Cursor] is known for the required key, it
1190    /// is much faster to use it than searching for a `key`.
1191    pub async fn get<'a>(&'a self, identifier: Identifier<'a, K>) -> Result<Option<V>, Error> {
1192        self.0.get(identifier).await
1193    }
1194
1195    /// Check whether a value exists for a given key.
1196    ///
1197    /// Walks the same key index chain as [`Self::get`] with [`Identifier::Key`]
1198    /// but never reads values.
1199    pub async fn has(&self, key: &K) -> Result<bool, Error> {
1200        self.0.has(key).await
1201    }
1202
1203    /// Sync all pending data in [Freezer].
1204    ///
1205    /// If the table needs to be resized, the resize will begin during this sync.
1206    /// The resize operation is performed incrementally across multiple sync calls
1207    /// to avoid a large latency spike (or unexpected long latency for [Freezer::put]).
1208    /// Each sync will process up to `table_resize_chunk_size` entries until the resize
1209    /// is complete.
1210    pub async fn sync(mut self) -> Result<(Self, Checkpoint), Error> {
1211        let checkpoint;
1212        (self.0, checkpoint) = self.0.sync().await?;
1213        Ok((self, checkpoint))
1214    }
1215
1216    /// Close the [Freezer] and return a [Checkpoint] for recovery.
1217    pub async fn close(self) -> Result<Checkpoint, Error> {
1218        self.0.close().await
1219    }
1220
1221    /// Close and remove any underlying blobs created by the [Freezer].
1222    pub async fn destroy(self) -> Result<(), Error> {
1223        self.0.destroy().await
1224    }
1225
1226    /// Get the current progress of the resize operation.
1227    ///
1228    /// Returns `None` if the [Freezer] is not resizing.
1229    #[cfg(test)]
1230    pub fn resizing(&self) -> Option<u32> {
1231        self.0.resize_progress
1232    }
1233
1234    /// Get the number of resizable entries.
1235    #[cfg(test)]
1236    pub fn resizable(&self) -> u32 {
1237        self.0.resizable
1238    }
1239
1240    /// Get the current size of the table.
1241    #[cfg(test)]
1242    pub fn table_size(&self) -> u32 {
1243        self.0.table_size
1244    }
1245}
1246
1247#[cfg(all(test, feature = "arbitrary"))]
1248mod conformance {
1249    use super::*;
1250    use commonware_codec::conformance::CodecConformance;
1251    use commonware_utils::sequence::U64;
1252
1253    commonware_conformance::conformance_tests! {
1254        CodecConformance<Cursor>,
1255        CodecConformance<Checkpoint>,
1256        CodecConformance<Entry>,
1257        CodecConformance<Record<U64>>
1258    }
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263    use super::*;
1264    use commonware_codec::DecodeExt;
1265    use commonware_macros::test_traced;
1266    use commonware_runtime::{
1267        Runner, Storage, Supervisor as _, WriteOptions, buffer::paged::CacheRef, deterministic,
1268        deterministic::Context,
1269    };
1270    use commonware_utils::{
1271        NZU16, NZUsize,
1272        sequence::{FixedBytes, U64},
1273    };
1274
1275    fn test_key(key: &str) -> FixedBytes<64> {
1276        let mut buf = [0u8; 64];
1277        let key = key.as_bytes();
1278        assert!(key.len() <= buf.len());
1279        buf[..key.len()].copy_from_slice(key);
1280        FixedBytes::decode(buf.as_ref()).unwrap()
1281    }
1282
1283    fn test_key_at_index(table_size: u32, table_index: u32) -> FixedBytes<64> {
1284        assert!(table_size.is_power_of_two());
1285        assert!(table_index < table_size);
1286
1287        for value in 0u64.. {
1288            let mut buf = [0u8; 64];
1289            let bytes = value.to_be_bytes();
1290            buf[..bytes.len()].copy_from_slice(&bytes);
1291            let key = FixedBytes::new(buf);
1292            if Crc32::checksum(key.as_ref()) & (table_size - 1) == table_index {
1293                return key;
1294            }
1295        }
1296
1297        unreachable!("u64 key space exhausted");
1298    }
1299
1300    type TestFreezer = Freezer<Context, U64, u64>;
1301
1302    fn is_send<T: Send>(_: T) {}
1303
1304    #[allow(dead_code)]
1305    fn assert_freezer_futures_are_send(freezer: TestFreezer, key: U64) {
1306        is_send(freezer.get(Identifier::Key(&key)));
1307        is_send(freezer.put(key, 0u64));
1308    }
1309
1310    #[allow(dead_code)]
1311    fn assert_freezer_destroy_is_send(freezer: TestFreezer) {
1312        is_send(freezer.destroy());
1313    }
1314
1315    #[test_traced]
1316    fn issue_2966_regression() {
1317        let executor = deterministic::Runner::default();
1318        executor.start(|context| async move {
1319            let cfg = super::super::Config {
1320                key_partition: "test-key-index".into(),
1321                key_write_buffer: NZUsize!(1024),
1322                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1323                value_partition: "test-value-journal".into(),
1324                value_compression: None,
1325                value_write_buffer: NZUsize!(1024),
1326                value_target_size: 10 * 1024 * 1024,
1327                table_partition: "test-table".into(),
1328                // Use 4 entries but only insert to 2, leaving 2 empty
1329                table_initial_size: 4,
1330                table_resize_frequency: 1,
1331                table_resize_chunk_size: 4,
1332                table_replay_buffer: NZUsize!(64 * 1024),
1333                codec_config: (),
1334            };
1335            let freezer =
1336                Freezer::<_, FixedBytes<64>, i32>::init(context.child("first"), cfg.clone(), None)
1337                    .await
1338                    .unwrap();
1339
1340            // Insert only 2 keys to different entries. With table_size=4, entries 2 and 3
1341            // should remain empty.
1342            let (freezer, _) = freezer.put(test_key("key0"), 0).await.unwrap();
1343            let (freezer, _) = freezer.put(test_key("key2"), 1).await.unwrap();
1344            freezer.close().await.unwrap();
1345
1346            let (blob, size) = context.open(&cfg.table_partition, b"table").await.unwrap();
1347            let table_data = blob
1348                .read_at(0, size as usize, ReadOptions::default())
1349                .await
1350                .unwrap()
1351                .coalesce();
1352
1353            // Verify resize happened (table doubled from 4 to 8)
1354            let num_entries = size as usize / Entry::FULL_SIZE;
1355            assert_eq!(num_entries, 8);
1356
1357            // Count entries where both slots are truly empty. The bug would cause empty
1358            // entries to have one slot with epoch != 0 and valid CRC.
1359            let mut both_empty_count = 0;
1360            for entry_idx in 0..num_entries {
1361                let offset = entry_idx * Entry::FULL_SIZE;
1362                let buf = &table_data.as_ref()[offset..offset + Entry::FULL_SIZE];
1363                let (slot0, slot1) =
1364                    Inner::<Context, FixedBytes<64>, i32>::parse_entries(buf).unwrap();
1365                if slot0.is_empty() && slot1.is_empty() {
1366                    both_empty_count += 1;
1367                }
1368            }
1369            // 2 keys in 4 entries = 2 empty. After resize to 8, those become 4 empty.
1370            assert_eq!(both_empty_count, 4);
1371        });
1372    }
1373
1374    #[test_traced]
1375    fn issue_2955_regression() {
1376        let executor = deterministic::Runner::default();
1377        executor.start(|context| async move {
1378            let cfg = super::super::Config {
1379                key_partition: "test-key-index".into(),
1380                key_write_buffer: NZUsize!(1024),
1381                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1382                value_partition: "test-value-journal".into(),
1383                value_compression: None,
1384                value_write_buffer: NZUsize!(1024),
1385                value_target_size: 10 * 1024 * 1024,
1386                table_partition: "test-table".into(),
1387                table_initial_size: 4,
1388                table_resize_frequency: 1,
1389                table_resize_chunk_size: 4,
1390                table_replay_buffer: NZUsize!(64 * 1024),
1391                codec_config: (),
1392            };
1393
1394            // Create freezer with data
1395            let checkpoint = {
1396                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1397                    context.child("first"),
1398                    cfg.clone(),
1399                    None,
1400                )
1401                .await
1402                .unwrap();
1403                let (freezer, _) = freezer.put(test_key("key0"), 42).await.unwrap();
1404                let (freezer, _) = freezer.sync().await.unwrap();
1405                freezer.close().await.unwrap()
1406            };
1407
1408            // Corrupt the CRC in both slots of the table entry
1409            {
1410                let (blob, _) = context.open(&cfg.table_partition, b"table").await.unwrap();
1411                let entry_data = blob
1412                    .read_at(0, Entry::FULL_SIZE, ReadOptions::default())
1413                    .await
1414                    .unwrap();
1415                let mut corrupted = entry_data.coalesce();
1416                // Corrupt CRC of first slot (last 4 bytes of first slot)
1417                corrupted.as_mut()[Entry::SIZE - 4] ^= 0xFF;
1418                // Corrupt CRC of second slot (last 4 bytes of second slot)
1419                corrupted.as_mut()[Entry::FULL_SIZE - 4] ^= 0xFF;
1420                blob.write_at(0, corrupted, WriteOptions::SYNC)
1421                    .await
1422                    .unwrap();
1423            }
1424
1425            // Reopen to trigger recovery. The bug would set both cleared entries to
1426            // Entry::new(0,0,0,0) which has is_empty()=false and is_valid()=true.
1427            // read_latest_entry would then see two "valid" entries with epoch=0 and
1428            // panic on unreachable!().
1429            let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1430                context.child("second"),
1431                cfg.clone(),
1432                Some(checkpoint),
1433            )
1434            .await
1435            .unwrap();
1436            drop(freezer);
1437        });
1438    }
1439
1440    #[test_traced]
1441    fn no_checkpoint_deletes_partial_sync_resize() {
1442        let executor = deterministic::Runner::default();
1443        executor.start(|context| async move {
1444            let cfg = super::super::Config {
1445                key_partition: "test-key-index".into(),
1446                key_write_buffer: NZUsize!(1024),
1447                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1448                value_partition: "test-value-journal".into(),
1449                value_compression: None,
1450                value_write_buffer: NZUsize!(1024),
1451                value_target_size: 10 * 1024 * 1024,
1452                table_partition: "test-table".into(),
1453                table_initial_size: 2,
1454                table_resize_frequency: 1,
1455                table_resize_chunk_size: 1,
1456                table_replay_buffer: NZUsize!(64 * 1024),
1457                codec_config: (),
1458            };
1459            let key = test_key_at_index(4, 3);
1460
1461            {
1462                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1463                    context.child("first"),
1464                    cfg.clone(),
1465                    None,
1466                )
1467                .await
1468                .unwrap();
1469                let (freezer, _) = freezer.put(key.clone(), 42).await.unwrap();
1470                let (freezer, _) = freezer.sync().await.unwrap();
1471
1472                assert_eq!(freezer.resizing(), Some(1));
1473                assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1474            }
1475
1476            let freezer =
1477                Freezer::<_, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone(), None)
1478                    .await
1479                    .unwrap();
1480            assert_eq!(freezer.table_size(), 2);
1481            assert_eq!(freezer.resizing(), None);
1482            assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1483        });
1484    }
1485
1486    #[test_traced]
1487    fn empty_checkpoint_deletes_existing_data() {
1488        let executor = deterministic::Runner::default();
1489        executor.start(|context| async move {
1490            let cfg = super::super::Config {
1491                key_partition: "test-key-index".into(),
1492                key_write_buffer: NZUsize!(1024),
1493                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1494                value_partition: "test-value-journal".into(),
1495                value_compression: None,
1496                value_write_buffer: NZUsize!(1024),
1497                value_target_size: 10 * 1024 * 1024,
1498                table_partition: "test-table".into(),
1499                table_initial_size: 2,
1500                table_resize_frequency: 1,
1501                table_resize_chunk_size: 1,
1502                table_replay_buffer: NZUsize!(64 * 1024),
1503                codec_config: (),
1504            };
1505            let key = test_key_at_index(4, 3);
1506
1507            {
1508                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1509                    context.child("first"),
1510                    cfg.clone(),
1511                    None,
1512                )
1513                .await
1514                .unwrap();
1515                let (freezer, _) = freezer.put(key.clone(), 42).await.unwrap();
1516                let (freezer, _) = freezer.sync().await.unwrap();
1517                assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1518            }
1519
1520            let checkpoint = Checkpoint {
1521                epoch: 0,
1522                section: 0,
1523                oversized_size: 0,
1524                table_size: 0,
1525            };
1526            let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1527                context.child("second"),
1528                cfg.clone(),
1529                Some(checkpoint),
1530            )
1531            .await
1532            .unwrap();
1533            assert_eq!(freezer.table_size(), 2);
1534            assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1535        });
1536    }
1537
1538    #[test_traced]
1539    fn no_checkpoint_deletes_close_started_partial_resize() {
1540        let executor = deterministic::Runner::default();
1541        executor.start(|context| async move {
1542            let cfg = super::super::Config {
1543                key_partition: "test-key-index".into(),
1544                key_write_buffer: NZUsize!(1024),
1545                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1546                value_partition: "test-value-journal".into(),
1547                value_compression: None,
1548                value_write_buffer: NZUsize!(1024),
1549                value_target_size: 10 * 1024 * 1024,
1550                table_partition: "test-table".into(),
1551                table_initial_size: 2,
1552                table_resize_frequency: 1,
1553                table_resize_chunk_size: 1,
1554                table_replay_buffer: NZUsize!(64 * 1024),
1555                codec_config: (),
1556            };
1557            let key = test_key_at_index(4, 3);
1558
1559            {
1560                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1561                    context.child("first"),
1562                    cfg.clone(),
1563                    None,
1564                )
1565                .await
1566                .unwrap();
1567                let (freezer, _) = freezer.put(key.clone(), 42).await.unwrap();
1568                let checkpoint = freezer.close().await.unwrap();
1569                assert_eq!(checkpoint.table_size, 2);
1570            }
1571
1572            let freezer =
1573                Freezer::<_, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone(), None)
1574                    .await
1575                    .unwrap();
1576            assert_eq!(freezer.table_size(), 2);
1577            assert_eq!(freezer.resizing(), None);
1578            assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1579        });
1580    }
1581
1582    #[test_traced]
1583    fn no_checkpoint_deletes_completed_resize() {
1584        let executor = deterministic::Runner::default();
1585        executor.start(|context| async move {
1586            let cfg = super::super::Config {
1587                key_partition: "test-key-index".into(),
1588                key_write_buffer: NZUsize!(1024),
1589                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1590                value_partition: "test-value-journal".into(),
1591                value_compression: None,
1592                value_write_buffer: NZUsize!(1024),
1593                value_target_size: 10 * 1024 * 1024,
1594                table_partition: "test-table".into(),
1595                table_initial_size: 2,
1596                table_resize_frequency: 1,
1597                table_resize_chunk_size: 2,
1598                table_replay_buffer: NZUsize!(64 * 1024),
1599                codec_config: (),
1600            };
1601            let key = test_key_at_index(4, 3);
1602
1603            {
1604                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1605                    context.child("first"),
1606                    cfg.clone(),
1607                    None,
1608                )
1609                .await
1610                .unwrap();
1611                let (freezer, _) = freezer.put(key.clone(), 42).await.unwrap();
1612                let (freezer, checkpoint) = freezer.sync().await.unwrap();
1613
1614                assert_eq!(checkpoint.table_size, 4);
1615                assert_eq!(freezer.resizing(), None);
1616                assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1617            }
1618
1619            let freezer =
1620                Freezer::<_, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone(), None)
1621                    .await
1622                    .unwrap();
1623            assert_eq!(freezer.table_size(), 2);
1624            assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1625        });
1626    }
1627
1628    #[test_traced]
1629    fn checkpoint_rewinds_completed_resize() {
1630        let executor = deterministic::Runner::default();
1631        executor.start(|context| async move {
1632            let cfg = super::super::Config {
1633                key_partition: "test-key-index".into(),
1634                key_write_buffer: NZUsize!(1024),
1635                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1636                value_partition: "test-value-journal".into(),
1637                value_compression: None,
1638                value_write_buffer: NZUsize!(1024),
1639                value_target_size: 10 * 1024 * 1024,
1640                table_partition: "test-table".into(),
1641                table_initial_size: 2,
1642                table_resize_frequency: 1,
1643                table_resize_chunk_size: 2,
1644                table_replay_buffer: NZUsize!(64 * 1024),
1645                codec_config: (),
1646            };
1647            let key = test_key_at_index(4, 3);
1648
1649            let stale_checkpoint = {
1650                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1651                    context.child("first"),
1652                    cfg.clone(),
1653                    None,
1654                )
1655                .await
1656                .unwrap();
1657                let (freezer, stale_checkpoint) = freezer.sync().await.unwrap();
1658                assert_eq!(stale_checkpoint.table_size, 2);
1659
1660                let (freezer, _) = freezer.put(key.clone(), 42).await.unwrap();
1661                let (freezer, checkpoint) = freezer.sync().await.unwrap();
1662                assert_eq!(checkpoint.table_size, 4);
1663                assert_eq!(freezer.resizing(), None);
1664                assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), Some(42));
1665
1666                stale_checkpoint
1667            };
1668
1669            let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1670                context.child("second"),
1671                cfg.clone(),
1672                Some(stale_checkpoint),
1673            )
1674            .await
1675            .unwrap();
1676            assert_eq!(freezer.table_size(), 2);
1677            assert_eq!(freezer.get(Identifier::Key(&key)).await.unwrap(), None);
1678        });
1679    }
1680
1681    #[test_traced]
1682    fn non_empty_checkpoint_against_empty_table_errors() {
1683        let executor = deterministic::Runner::default();
1684        executor.start(|context| async move {
1685            let cfg = super::super::Config {
1686                key_partition: "test-key-index".into(),
1687                key_write_buffer: NZUsize!(1024),
1688                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1689                value_partition: "test-value-journal".into(),
1690                value_compression: None,
1691                value_write_buffer: NZUsize!(1024),
1692                value_target_size: 10 * 1024 * 1024,
1693                table_partition: "test-table".into(),
1694                table_initial_size: 2,
1695                table_resize_frequency: 1,
1696                table_resize_chunk_size: 1,
1697                table_replay_buffer: NZUsize!(64 * 1024),
1698                codec_config: (),
1699            };
1700
1701            let checkpoint = Checkpoint {
1702                epoch: 1,
1703                section: 0,
1704                oversized_size: 0,
1705                table_size: 2,
1706            };
1707            let result = Freezer::<_, FixedBytes<64>, i32>::init(
1708                context.child("storage"),
1709                cfg.clone(),
1710                Some(checkpoint),
1711            )
1712            .await;
1713            assert!(matches!(result, Err(Error::CheckpointMismatch)));
1714        });
1715    }
1716
1717    /// A durable checkpoint's table fsync completed at the checkpointed size, so a shorter
1718    /// (but non-empty) table is corruption. Initialization must reject it rather than grow the
1719    /// table with fabricated empty entries, and retries must fail identically.
1720    #[test_traced]
1721    fn non_empty_checkpoint_against_short_table_errors() {
1722        let executor = deterministic::Runner::default();
1723        executor.start(|context| async move {
1724            let cfg = super::super::Config {
1725                key_partition: "test-key-index".into(),
1726                key_write_buffer: NZUsize!(1024),
1727                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1728                value_partition: "test-value-journal".into(),
1729                value_compression: None,
1730                value_write_buffer: NZUsize!(1024),
1731                value_target_size: 10 * 1024 * 1024,
1732                table_partition: "test-table".into(),
1733                table_initial_size: 2,
1734                table_resize_frequency: 1,
1735                table_resize_chunk_size: 1,
1736                table_replay_buffer: NZUsize!(64 * 1024),
1737                codec_config: (),
1738            };
1739
1740            let short_len = Entry::FULL_SIZE as u64;
1741            let (table, _) = context
1742                .open(&cfg.table_partition, TABLE_BLOB_NAME)
1743                .await
1744                .unwrap();
1745            table.resize(short_len).await.unwrap();
1746            table.sync().await.unwrap();
1747            drop(table);
1748
1749            let checkpoint = Checkpoint {
1750                epoch: 1,
1751                section: 0,
1752                oversized_size: 0,
1753                table_size: 2,
1754            };
1755            for child in ["first", "retry"] {
1756                let result = Freezer::<_, FixedBytes<64>, i32>::init(
1757                    context.child(child),
1758                    cfg.clone(),
1759                    Some(checkpoint),
1760                )
1761                .await;
1762                assert!(matches!(result, Err(Error::CheckpointMismatch)));
1763            }
1764
1765            // The rejection must not resize the table.
1766            let (_, size) = context
1767                .open(&cfg.table_partition, TABLE_BLOB_NAME)
1768                .await
1769                .unwrap();
1770            assert_eq!(size, short_len);
1771        });
1772    }
1773
1774    #[test_traced]
1775    fn corrupted_committed_value_surfaces_at_read() {
1776        let executor = deterministic::Runner::default();
1777        executor.start(|context| async move {
1778            let cfg = super::super::Config {
1779                key_partition: "test-key-index".into(),
1780                key_write_buffer: NZUsize!(1024),
1781                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1782                value_partition: "test-value-journal".into(),
1783                value_compression: None,
1784                value_write_buffer: NZUsize!(1024),
1785                value_target_size: 10 * 1024 * 1024,
1786                table_partition: "test-table".into(),
1787                table_initial_size: 4,
1788                table_resize_frequency: 1,
1789                table_resize_chunk_size: 4,
1790                table_replay_buffer: NZUsize!(64 * 1024),
1791                codec_config: (),
1792            };
1793
1794            // Create freezer with committed data
1795            let checkpoint = {
1796                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1797                    context.child("first"),
1798                    cfg.clone(),
1799                    None,
1800                )
1801                .await
1802                .unwrap();
1803                let (freezer, _) = freezer.put(test_key("key0"), 42).await.unwrap();
1804                let (freezer, _) = freezer.put(test_key("key1"), 43).await.unwrap();
1805                let (freezer, _) = freezer.sync().await.unwrap();
1806                freezer.close().await.unwrap()
1807            };
1808            assert!(checkpoint.oversized_size > 0);
1809
1810            // Corrupt the last committed value's checksum in the value journal
1811            {
1812                let (blob, len) = context
1813                    .open(&cfg.value_partition, &checkpoint.section.to_be_bytes())
1814                    .await
1815                    .unwrap();
1816                let byte = blob
1817                    .read_at(len - 1, 1, ReadOptions::default())
1818                    .await
1819                    .unwrap();
1820                let mut corrupted = byte.coalesce();
1821                corrupted.as_mut()[0] ^= 0xFF;
1822                blob.write_at(len - 1, corrupted, WriteOptions::SYNC)
1823                    .await
1824                    .unwrap();
1825            }
1826
1827            // Recovery restores the checkpointed state without probing committed
1828            // values, so init succeeds and the corruption surfaces at read on
1829            // exactly the affected key.
1830            let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1831                context.child("second"),
1832                cfg.clone(),
1833                Some(checkpoint),
1834            )
1835            .await
1836            .unwrap();
1837            assert!(matches!(
1838                freezer.get(Identifier::Key(&test_key("key1"))).await,
1839                Err(Error::Journal(crate::journal::Error::ChecksumMismatch(
1840                    _,
1841                    _
1842                )))
1843            ));
1844            assert_eq!(
1845                freezer
1846                    .get(Identifier::Key(&test_key("key0")))
1847                    .await
1848                    .unwrap(),
1849                Some(42)
1850            );
1851
1852            // The freezer remains usable
1853            let (freezer, _) = freezer.put(test_key("key2"), 44).await.unwrap();
1854            let (freezer, _) = freezer.sync().await.unwrap();
1855            assert_eq!(
1856                freezer
1857                    .get(Identifier::Key(&test_key("key2")))
1858                    .await
1859                    .unwrap(),
1860                Some(44)
1861            );
1862        });
1863    }
1864
1865    #[test_traced]
1866    fn incomplete_committed_section_fails_init() {
1867        let executor = deterministic::Runner::default();
1868        executor.start(|context| async move {
1869            // A tiny value target so every put seals a section
1870            let cfg = super::super::Config {
1871                key_partition: "test-key-index".into(),
1872                key_write_buffer: NZUsize!(1024),
1873                key_page_cache: CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10)),
1874                value_partition: "test-value-journal".into(),
1875                value_compression: None,
1876                value_write_buffer: NZUsize!(1024),
1877                value_target_size: 8,
1878                table_partition: "test-table".into(),
1879                table_initial_size: 4,
1880                table_resize_frequency: 1,
1881                table_resize_chunk_size: 4,
1882                table_replay_buffer: NZUsize!(64 * 1024),
1883                codec_config: (),
1884            };
1885
1886            // Create freezer with committed data across multiple sections
1887            let checkpoint = {
1888                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1889                    context.child("first"),
1890                    cfg.clone(),
1891                    None,
1892                )
1893                .await
1894                .unwrap();
1895                let (freezer, _) = freezer.put(test_key("key0"), 42).await.unwrap();
1896                let (freezer, _) = freezer.put(test_key("key1"), 43).await.unwrap();
1897                let (freezer, _) = freezer.sync().await.unwrap();
1898                freezer.close().await.unwrap()
1899            };
1900            assert!(checkpoint.section > 0);
1901
1902            // Truncate the first committed section's values, simulating lost durable
1903            // state below the checkpoint
1904            {
1905                let (blob, len) = context
1906                    .open(&cfg.value_partition, &0u64.to_be_bytes())
1907                    .await
1908                    .unwrap();
1909                assert!(len > 0);
1910                blob.resize(len - 1).await.unwrap();
1911                blob.sync().await.unwrap();
1912            }
1913
1914            // The checkpoint covers the damaged section, so init must fail rather than
1915            // silently absorb the loss. Nothing is repaired, so the failure persists
1916            // across restarts.
1917            for instance in ["second", "third"] {
1918                let result = Freezer::<_, FixedBytes<64>, i32>::init(
1919                    context.child(instance),
1920                    cfg.clone(),
1921                    Some(checkpoint),
1922                )
1923                .await;
1924                assert!(matches!(
1925                    result,
1926                    Err(Error::Journal(crate::journal::Error::Corruption(_)))
1927                ));
1928            }
1929        });
1930    }
1931}