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