Skip to main content

commonware_storage/freezer/
mod.rs

1//! An immutable key-value store optimized for minimal memory usage and write amplification.
2//!
3//! [Freezer] is a key-value store designed for permanent storage where data is written once and never
4//! modified. Meant for resource-constrained environments, [Freezer] exclusively employs disk-resident
5//! data structures to serve queries and avoids ever rewriting (i.e. compacting) inserted data.
6//!
7//! As a byproduct of the mechanisms used to satisfy these constraints, [Freezer] consistently provides
8//! low latency access to recently added data (regardless of how much data has been stored) at the expense
9//! of a logarithmic increase in latency for old data (increasing with the number of items stored).
10//!
11//! # Format
12//!
13//! The [Freezer] uses a three-level architecture:
14//! 1. An extendible hash table (written in a single [commonware_runtime::Blob]) that maps keys to locations
15//! 2. A key index journal ([crate::journal::segmented::fixed]) that stores keys and collision chain pointers
16//! 3. A value journal ([crate::journal::segmented::glob]) that stores the actual values
17//!
18//! These journals are combined via [crate::journal::segmented::oversized], which coordinates
19//! crash recovery between them.
20//!
21//! ```text
22//! +-----------------------------------------------------------------+
23//! |                           Hash Table                            |
24//! |  +---------+---------+---------+---------+---------+---------+  |
25//! |  | Entry 0 | Entry 1 | Entry 2 | Entry 3 | Entry 4 |   ...   |  |
26//! |  +----+----+----+----+----+----+----+----+----+----+---------+  |
27//! +-------|---------|---------|---------|---------|---------|-------+
28//!         |         |         |         |         |         |
29//!         v         v         v         v         v         v
30//! +-----------------------------------------------------------------+
31//! |                      Key Index Journal                          |
32//! |  Section 0: [Entry 0][Entry 1][Entry 2]...                      |
33//! |  Section 1: [Entry 10][Entry 11][Entry 12]...                   |
34//! |  Section N: [Entry 100][Entry 101][Entry 102]...                |
35//! +-------|---------|---------|---------|---------|---------|-------+
36//!         |         |         |         |         |         |
37//!         v         v         v         v         v         v
38//! +-----------------------------------------------------------------+
39//! |                        Value Journal                            |
40//! |  Section 0: [Value 0][Value 1][Value 2]...                      |
41//! |  Section 1: [Value 10][Value 11][Value 12]...                   |
42//! |  Section N: [Value 100][Value 101][Value 102]...                |
43//! +-----------------------------------------------------------------+
44//! ```
45//!
46//! The table uses two fixed-size slots per entry to ensure consistency during updates. Each slot
47//! contains an epoch number that monotonically increases with each sync operation. During reads,
48//! the slot with the higher epoch is selected (provided it's not greater than the last committed
49//! epoch), ensuring consistency even if the system crashed during a write.
50//!
51//! ```text
52//! +-------------------------------------+
53//! |          Hash Table Entry           |
54//! +-------------------------------------+
55//! |     Slot 0      |      Slot 1       |
56//! +-----------------+-------------------+
57//! | epoch:    u64   | epoch:    u64     |
58//! | section:  u64   | section:  u64     |
59//! | offset:   u32   | offset:   u32     |
60//! | added:    u8    | added:    u8      |
61//! +-----------------+-------------------+
62//! | CRC32:    u32   | CRC32:    u32     |
63//! +-----------------+-------------------+
64//! ```
65//!
66//! The key index journal stores fixed-size entries containing a key, a pointer to the value in the
67//! value journal, and an optional pointer to the next entry in the collision chain (for keys that
68//! hash to the same table index).
69//!
70//! ```text
71//! +-------------------------------------+
72//! |        Key Index Entry              |
73//! +-------------------------------------+
74//! | Key:           Array                |
75//! | Value Offset:  u64                  |
76//! | Value Size:    u32                  |
77//! | Next:          Option<(u64, u32)>   |
78//! +-------------------------------------+
79//! ```
80//!
81//! The value journal stores the actual encoded values at the offsets referenced by the key index entries.
82//!
83//! # Traversing Conflicts
84//!
85//! When multiple keys hash to the same table index, they form a linked list within the key index
86//! journal. Each key index entry points to its value in the value journal:
87//!
88//! ```text
89//! Hash Table:
90//! [Index 42]         +-------------------+
91//!                    | section: 2        |
92//!                    | offset: 768       |
93//!                    +---------+---------+
94//!                              |
95//! Key Index Journal:           v
96//! [Section 2]        +-----------------------+
97//!                    | Key: "foo"            |
98//!                    | ValOff: 100           |
99//!                    | ValSize: 20           |
100//!                    | Next: (1, 512) -------+---+
101//!                    +-----------------------+   |
102//!                                                v
103//! [Section 1]        +-----------------------+
104//!                    | Key: "bar"            |
105//!                    | ValOff: 50            |
106//!                    | ValSize: 20           |
107//!                    | Next: (0, 256) -------+---+
108//!                    +-----------------------+   |
109//!                                                v
110//! [Section 0]        +-----------------------+
111//!                    | Key: "baz"            |
112//!                    | ValOff: 0             |
113//!                    | ValSize: 20           |
114//!                    | Next: None            |
115//!                    +-----------------------+
116//!
117//! Value Journal:
118//! [Section 0]        [Value: 126 @ offset 0 ]
119//! [Section 1]        [Value: 84  @ offset 50]
120//! [Section 2]        [Value: 42  @ offset 100]
121//! ```
122//!
123//! New entries are prepended to the chain, becoming the new head. During lookup, the chain
124//! is traversed until a matching key is found. The `added` field in the table entry tracks
125//! insertions since the last resize, triggering table growth when 50% of entries have had
126//! `table_resize_frequency` items added (since the last resize).
127//!
128//! # Extendible Hashing
129//!
130//! The [Freezer] uses bit-based indexing to grow the on-disk hash table without rehashing existing entries:
131//!
132//! ```text
133//! Initial state (table_size=4, using 2 bits of hash):
134//! Hash: 0b...00 -> Index 0
135//! Hash: 0b...01 -> Index 1
136//! Hash: 0b...10 -> Index 2
137//! Hash: 0b...11 -> Index 3
138//!
139//! After resize (table_size=8, using 3 bits of hash):
140//! Hash: 0b...000 -> Index 0 -+
141//! ...                        |
142//! Hash: 0b...100 -> Index 4 -+- Both map to old Index 0
143//! Hash: 0b...001 -> Index 1 -+
144//! ...                        |
145//! Hash: 0b...101 -> Index 5 -+- Both map to old Index 1
146//! ```
147//!
148//! When the table doubles in size:
149//! 1. Each entry at index `i` splits into two entries: `i` and `i + old_size`
150//! 2. The existing chain head is copied to both locations with `added=0`
151//! 3. Future insertions will naturally distribute between the two entries based on their hash
152//!
153//! This approach ensures that entries inserted before a resize remain discoverable after the resize,
154//! as the lookup algorithm checks the appropriate entry based on the current table size. As more and more
155//! items are added (and resizes occur), the latency for fetching old data will increase logarithmically
156//! (with the number of items stored).
157//!
158//! To prevent a "stall" during a single resize, the table is resized incrementally across multiple sync calls.
159//! Each sync will process up to `table_resize_chunk_size` entries until the resize is complete. If there is
160//! an ongoing resize when closing the [Freezer], the resize will be completed before closing.
161//!
162//! # Recovery
163//!
164//! [Freezer::sync] and [Freezer::close] return a [Checkpoint] for recovering existing data.
165//! When a checkpoint is provided, [Freezer::init] rewinds the journals to the checkpoint, truncates
166//! a longer table to the checkpointed table size, and clears invalid or newer table entries. A
167//! table shorter than the checkpointed size cannot back the checkpointed entries and fails
168//! initialization. Passing `None` or an empty checkpoint to [Freezer::init] deletes any existing
169//! freezer data and starts empty.
170//!
171//! # Example
172//!
173//! ```rust
174//! use commonware_runtime::{Spawner, Runner, deterministic, buffer::paged::CacheRef};
175//! use commonware_storage::freezer::{Freezer, Config, Identifier};
176//! use commonware_utils::{sequence::FixedBytes, NZUsize, NZU16};
177//!
178//! let executor = deterministic::Runner::default();
179//! executor.start(|context| async move {
180//!     // Create a freezer
181//!     let cfg = Config {
182//!         key_partition: "freezer-key-index".into(),
183//!         key_write_buffer: NZUsize!(1024 * 1024), // 1MB
184//!         key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
185//!         value_partition: "freezer-value-journal".into(),
186//!         value_compression: Some(3),
187//!         value_write_buffer: NZUsize!(1024 * 1024), // 1MB
188//!         value_target_size: 100 * 1024 * 1024, // 100MB
189//!         table_partition: "freezer-table".into(),
190//!         table_initial_size: 65_536, // ~3MB initial table size
191//!         table_resize_frequency: 4, // Force resize once 4 writes to the same entry occur
192//!         table_resize_chunk_size: 16_384, // ~1MB of table entries rewritten per sync
193//!         table_replay_buffer: NZUsize!(1024 * 1024), // 1MB
194//!         codec_config: (),
195//!     };
196//!     let freezer = Freezer::<_, FixedBytes<32>, i32>::init(context, cfg, None).await.unwrap();
197//!
198//!     // Put a key-value pair
199//!     let key = FixedBytes::new([1u8; 32]);
200//!     let (freezer, _cursor) = freezer.put(key.clone(), &42).await.unwrap();
201//!
202//!     // Sync to disk
203//!     let (freezer, _checkpoint) = freezer.sync().await.unwrap();
204//!
205//!     // Get the value
206//!     let value = freezer.get(Identifier::Key(&key)).await.unwrap().unwrap();
207//!     assert_eq!(value, 42);
208//!
209//!     // Close the freezer
210//!     freezer.close().await.unwrap();
211//! });
212//! ```
213
214#[cfg(all(test, feature = "arbitrary"))]
215mod conformance;
216mod storage;
217
218use commonware_runtime::buffer::paged::CacheRef;
219use commonware_utils::Array;
220use std::num::NonZeroUsize;
221pub use storage::{Checkpoint, Cursor, Freezer};
222use thiserror::Error;
223
224/// Subject of a [Freezer::get] operation.
225pub enum Identifier<'a, K: Array> {
226    Cursor(Cursor),
227    Key(&'a K),
228}
229
230/// Errors that can occur when interacting with the [Freezer].
231#[derive(Debug, Error)]
232pub enum Error {
233    #[error("runtime error: {0}")]
234    Runtime(#[from] commonware_runtime::Error),
235    #[error("journal error: {0}")]
236    Journal(#[from] crate::journal::Error),
237    #[error("codec error: {0}")]
238    Codec(#[from] commonware_codec::Error),
239    #[error("checkpoint does not match stored data")]
240    CheckpointMismatch,
241}
242
243/// Configuration for [Freezer].
244#[derive(Clone)]
245pub struct Config<C> {
246    /// The [commonware_runtime::Storage] partition for the key index journal.
247    pub key_partition: String,
248
249    /// The size of the write buffer for the key index journal.
250    pub key_write_buffer: NonZeroUsize,
251
252    /// The page cache for the key index journal.
253    pub key_page_cache: CacheRef,
254
255    /// The [commonware_runtime::Storage] partition for the value journal.
256    pub value_partition: String,
257
258    /// Optional zstd compression level for the value journal.
259    ///
260    /// Keep the choice between `None` and `Some(_)` fixed while stored values are retained.
261    /// Only the compression level may change between initializations when compression is enabled.
262    pub value_compression: Option<u8>,
263
264    /// The size of the write buffer for the value journal.
265    pub value_write_buffer: NonZeroUsize,
266
267    /// The target size of each value journal section before creating a new one.
268    pub value_target_size: u64,
269
270    /// The [commonware_runtime::Storage] partition to use for storing the table.
271    pub table_partition: String,
272
273    /// The initial number of items in the table.
274    pub table_initial_size: u32,
275
276    /// The number of items that must be added to 50% of table entries since the last resize before
277    /// the table is resized again.
278    pub table_resize_frequency: u8,
279
280    /// The number of items to move during each resize operation (many may be required to complete a resize).
281    pub table_resize_chunk_size: u32,
282
283    /// The size of the read buffer to use when scanning the table (e.g., during recovery or resize).
284    pub table_replay_buffer: NonZeroUsize,
285
286    /// The codec configuration to use for the value stored in the freezer.
287    pub codec_config: C,
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use commonware_formatting::hex;
294    use commonware_macros::{test_group, test_traced};
295    use commonware_runtime::{
296        Blob, Metrics as _, ReadOptions, Runner, Storage, Supervisor as _, WriteOptions,
297        deterministic,
298    };
299    use commonware_utils::{NZU16, NZUsize, sequence::FixedBytes};
300    use rand::{Rng, RngExt as _};
301    use std::num::NonZeroU16;
302
303    fn test_key(key: &str) -> FixedBytes<64> {
304        let mut buf = [0u8; 64];
305        let key = key.as_bytes();
306        assert!(key.len() <= buf.len());
307        buf[..key.len()].copy_from_slice(key);
308        FixedBytes::new(buf)
309    }
310
311    const DEFAULT_WRITE_BUFFER: usize = 1024;
312    const DEFAULT_VALUE_TARGET_SIZE: u64 = 10 * 1024 * 1024;
313    const DEFAULT_TABLE_INITIAL_SIZE: u32 = 256;
314    const DEFAULT_TABLE_RESIZE_FREQUENCY: u8 = 4;
315    const DEFAULT_TABLE_RESIZE_CHUNK_SIZE: u32 = 128; // force multiple chunks
316    const DEFAULT_TABLE_REPLAY_BUFFER: usize = 64 * 1024; // 64KB
317    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
318    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
319
320    fn test_put_get(compression: Option<u8>) {
321        // Initialize the deterministic context
322        let executor = deterministic::Runner::default();
323        executor.start(|context| async move {
324            // Initialize the freezer
325            let cfg = Config {
326                key_partition: "test-key-index".into(),
327                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
328                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
329                value_partition: "test-value-journal".into(),
330                value_compression: compression,
331                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
332                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
333                table_partition: "test-table".into(),
334                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
335                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
336                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
337                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
338                codec_config: (),
339            };
340            let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
341                context.child("storage"),
342                cfg.clone(),
343                None,
344            )
345            .await
346            .expect("Failed to initialize freezer");
347
348            let key = test_key("testkey");
349            let data = 42;
350
351            // Check key doesn't exist
352            let value = freezer
353                .get(Identifier::Key(&key))
354                .await
355                .expect("Failed to check key");
356            assert!(value.is_none());
357
358            // Put the key-data pair
359            let (freezer, _) = freezer
360                .put(key.clone(), &data)
361                .await
362                .expect("Failed to put data");
363
364            // Get the data back
365            let value = freezer
366                .get(Identifier::Key(&key))
367                .await
368                .expect("Failed to get data")
369                .expect("Data not found");
370            assert_eq!(value, data);
371
372            // Check metrics
373            let buffer = context.encode();
374            assert!(buffer.contains("gets_total 2"), "{}", buffer);
375            assert!(buffer.contains("puts_total 1"), "{}", buffer);
376            assert!(buffer.contains("unnecessary_reads_total 0"), "{}", buffer);
377
378            // Force a sync
379            freezer.sync().await.expect("Failed to sync data");
380        });
381    }
382
383    #[test_traced]
384    fn test_put_get_no_compression() {
385        test_put_get(None);
386    }
387
388    #[test_traced]
389    fn test_put_get_compression() {
390        test_put_get(Some(3));
391    }
392
393    #[test_traced]
394    fn test_has() {
395        // Initialize the deterministic context
396        let executor = deterministic::Runner::default();
397        executor.start(|context| async move {
398            // Initialize the freezer
399            let cfg = Config {
400                key_partition: "test-key-index".into(),
401                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
402                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
403                value_partition: "test-value-journal".into(),
404                value_compression: None,
405                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
406                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
407                table_partition: "test-table".into(),
408                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
409                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
410                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
411                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
412                codec_config: (),
413            };
414            let freezer =
415                Freezer::<_, FixedBytes<64>, i32>::init(context.child("storage"), cfg, None)
416                    .await
417                    .expect("Failed to initialize freezer");
418
419            // Absent key
420            let key = test_key("testkey");
421            assert!(!freezer.has(&key).await.expect("Failed to check key"));
422
423            // Present key
424            let (freezer, _) = freezer
425                .put(key.clone(), &42)
426                .await
427                .expect("Failed to put data");
428            assert!(freezer.has(&key).await.expect("Failed to check key"));
429
430            // A different key remains absent
431            assert!(
432                !freezer
433                    .has(&test_key("otherkey"))
434                    .await
435                    .expect("Failed to check key")
436            );
437
438            // Existence checks are counted as has, never as gets
439            let buffer = context.encode();
440            assert!(buffer.contains("has_total 3"), "{}", buffer);
441            assert!(buffer.contains("gets_total 0"), "{}", buffer);
442        });
443    }
444
445    #[test_traced]
446    fn test_multiple_keys() {
447        // Initialize the deterministic context
448        let executor = deterministic::Runner::default();
449        executor.start(|context| async move {
450            // Initialize the freezer
451            let cfg = Config {
452                key_partition: "test-key-index".into(),
453                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
454                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
455                value_partition: "test-value-journal".into(),
456                value_compression: None,
457                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
458                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
459                table_partition: "test-table".into(),
460                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
461                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
462                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
463                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
464                codec_config: (),
465            };
466            let mut freezer = Freezer::<_, FixedBytes<64>, i32>::init(
467                context.child("storage"),
468                cfg.clone(),
469                None,
470            )
471            .await
472            .expect("Failed to initialize freezer");
473
474            // Insert multiple keys
475            let keys = vec![
476                (test_key("key1"), 1),
477                (test_key("key2"), 2),
478                (test_key("key3"), 3),
479                (test_key("key4"), 4),
480                (test_key("key5"), 5),
481            ];
482
483            for (key, data) in &keys {
484                (freezer, _) = freezer
485                    .put(key.clone(), data)
486                    .await
487                    .expect("Failed to put data");
488            }
489
490            // Retrieve all keys and verify
491            for (key, data) in &keys {
492                let retrieved = freezer
493                    .get(Identifier::Key(key))
494                    .await
495                    .expect("Failed to get data")
496                    .expect("Data not found");
497                assert_eq!(retrieved, *data);
498            }
499        });
500    }
501
502    #[test_traced]
503    fn test_collision_handling() {
504        // Initialize the deterministic context
505        let executor = deterministic::Runner::default();
506        executor.start(|context| async move {
507            // Initialize the freezer with a very small table to force collisions
508            let cfg = Config {
509                key_partition: "test-key-index".into(),
510                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
511                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
512                value_partition: "test-value-journal".into(),
513                value_compression: None,
514                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
515                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
516                table_partition: "test-table".into(),
517                table_initial_size: 4, // Very small to force collisions
518                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
519                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
520                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
521                codec_config: (),
522            };
523            let mut freezer = Freezer::<_, FixedBytes<64>, i32>::init(
524                context.child("storage"),
525                cfg.clone(),
526                None,
527            )
528            .await
529            .expect("Failed to initialize freezer");
530
531            // Insert multiple keys that will likely collide
532            let keys = vec![
533                (test_key("key1"), 1),
534                (test_key("key2"), 2),
535                (test_key("key3"), 3),
536                (test_key("key4"), 4),
537                (test_key("key5"), 5),
538                (test_key("key6"), 6),
539                (test_key("key7"), 7),
540                (test_key("key8"), 8),
541            ];
542
543            for (key, data) in &keys {
544                (freezer, _) = freezer
545                    .put(key.clone(), data)
546                    .await
547                    .expect("Failed to put data");
548            }
549
550            // Sync to disk
551            let (freezer, _) = freezer.sync().await.expect("Failed to sync");
552
553            // Retrieve all keys and verify they can still be found
554            for (key, data) in &keys {
555                let retrieved = freezer
556                    .get(Identifier::Key(key))
557                    .await
558                    .expect("Failed to get data")
559                    .expect("Data not found");
560                assert_eq!(retrieved, *data);
561            }
562
563            // Check metrics
564            let buffer = context.encode();
565            assert!(buffer.contains("gets_total 8"), "{}", buffer);
566            assert!(buffer.contains("unnecessary_reads_total 5"), "{}", buffer);
567        });
568    }
569
570    #[test_traced]
571    fn test_restart() {
572        // Initialize the deterministic context
573        let executor = deterministic::Runner::default();
574        executor.start(|context| async move {
575            let cfg = Config {
576                key_partition: "test-key-index".into(),
577                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
578                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
579                value_partition: "test-value-journal".into(),
580                value_compression: None,
581                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
582                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
583                table_partition: "test-table".into(),
584                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
585                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
586                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
587                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
588                codec_config: (),
589            };
590
591            // Insert data and close the freezer
592            let checkpoint = {
593                let mut freezer = Freezer::<_, FixedBytes<64>, i32>::init(
594                    context.child("first"),
595                    cfg.clone(),
596                    None,
597                )
598                .await
599                .expect("Failed to initialize freezer");
600
601                let keys = vec![
602                    (test_key("persist1"), 100),
603                    (test_key("persist2"), 200),
604                    (test_key("persist3"), 300),
605                ];
606
607                for (key, data) in &keys {
608                    (freezer, _) = freezer
609                        .put(key.clone(), data)
610                        .await
611                        .expect("Failed to put data");
612                }
613
614                freezer.close().await.expect("Failed to close freezer")
615            };
616
617            // Reopen and verify data persisted
618            {
619                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
620                    context.child("second"),
621                    cfg.clone(),
622                    Some(checkpoint),
623                )
624                .await
625                .expect("Failed to initialize freezer");
626
627                let keys = vec![
628                    (test_key("persist1"), 100),
629                    (test_key("persist2"), 200),
630                    (test_key("persist3"), 300),
631                ];
632
633                for (key, data) in &keys {
634                    let retrieved = freezer
635                        .get(Identifier::Key(key))
636                        .await
637                        .expect("Failed to get data")
638                        .expect("Data not found");
639                    assert_eq!(retrieved, *data);
640                }
641            }
642        });
643    }
644
645    #[test_traced]
646    fn test_crash_consistency() {
647        // Initialize the deterministic context
648        let executor = deterministic::Runner::default();
649        executor.start(|context| async move {
650            let cfg = Config {
651                key_partition: "test-key-index".into(),
652                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
653                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
654                value_partition: "test-value-journal".into(),
655                value_compression: None,
656                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
657                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
658                table_partition: "test-table".into(),
659                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
660                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
661                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
662                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
663                codec_config: (),
664            };
665
666            // First, create some committed data and close the freezer
667            let checkpoint = {
668                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
669                    context.child("first"),
670                    cfg.clone(),
671                    None,
672                )
673                .await
674                .expect("Failed to initialize freezer");
675
676                let (freezer, _) = freezer
677                    .put(test_key("committed1"), &1)
678                    .await
679                    .expect("Failed to put data");
680                let (freezer, _) = freezer
681                    .put(test_key("committed2"), &2)
682                    .await
683                    .expect("Failed to put data");
684
685                // Sync to ensure data is committed
686                let (freezer, _) = freezer.sync().await.expect("Failed to sync");
687
688                // Add more data but don't sync (simulating crash)
689                let (freezer, _) = freezer
690                    .put(test_key("uncommitted1"), &3)
691                    .await
692                    .expect("Failed to put data");
693                let (freezer, _) = freezer
694                    .put(test_key("uncommitted2"), &4)
695                    .await
696                    .expect("Failed to put data");
697
698                // Close without syncing to simulate crash
699                freezer.close().await.expect("Failed to close")
700            };
701
702            // Reopen and verify only committed data is present
703            {
704                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
705                    context.child("second"),
706                    cfg.clone(),
707                    Some(checkpoint),
708                )
709                .await
710                .expect("Failed to initialize freezer");
711
712                // Committed data should be present
713                assert_eq!(
714                    freezer
715                        .get(Identifier::Key(&test_key("committed1")))
716                        .await
717                        .unwrap(),
718                    Some(1)
719                );
720                assert_eq!(
721                    freezer
722                        .get(Identifier::Key(&test_key("committed2")))
723                        .await
724                        .unwrap(),
725                    Some(2)
726                );
727
728                // Uncommitted data might or might not be present depending on implementation
729                // But if present, it should be correct
730                if let Some(val) = freezer
731                    .get(Identifier::Key(&test_key("uncommitted1")))
732                    .await
733                    .unwrap()
734                {
735                    assert_eq!(val, 3);
736                }
737                if let Some(val) = freezer
738                    .get(Identifier::Key(&test_key("uncommitted2")))
739                    .await
740                    .unwrap()
741                {
742                    assert_eq!(val, 4);
743                }
744            }
745        });
746    }
747
748    #[test_traced]
749    fn test_destroy() {
750        // Initialize the deterministic context
751        let executor = deterministic::Runner::default();
752        executor.start(|context| async move {
753            // Initialize the freezer
754            let cfg = Config {
755                key_partition: "test-key-index".into(),
756                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
757                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
758                value_partition: "test-value-journal".into(),
759                value_compression: None,
760                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
761                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
762                table_partition: "test-table".into(),
763                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
764                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
765                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
766                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
767                codec_config: (),
768            };
769            {
770                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
771                    context.child("first"),
772                    cfg.clone(),
773                    None,
774                )
775                .await
776                .expect("Failed to initialize freezer");
777
778                let (freezer, _) = freezer
779                    .put(test_key("destroy1"), &1)
780                    .await
781                    .expect("Failed to put data");
782                let (freezer, _) = freezer
783                    .put(test_key("destroy2"), &2)
784                    .await
785                    .expect("Failed to put data");
786
787                // Destroy the freezer
788                freezer.destroy().await.expect("Failed to destroy freezer");
789            }
790
791            // Try to create a new freezer - it should be empty
792            {
793                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
794                    context.child("second"),
795                    cfg.clone(),
796                    None,
797                )
798                .await
799                .expect("Failed to initialize freezer");
800
801                // Should not find any data
802                assert!(
803                    freezer
804                        .get(Identifier::Key(&test_key("destroy1")))
805                        .await
806                        .unwrap()
807                        .is_none()
808                );
809                assert!(
810                    freezer
811                        .get(Identifier::Key(&test_key("destroy2")))
812                        .await
813                        .unwrap()
814                        .is_none()
815                );
816            }
817        });
818    }
819
820    #[test_traced]
821    fn test_partial_table_entry_write() {
822        // Initialize the deterministic context
823        let executor = deterministic::Runner::default();
824        executor.start(|context| async move {
825            // Initialize the freezer
826            let cfg = Config {
827                key_partition: "test-key-index".into(),
828                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
829                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
830                value_partition: "test-value-journal".into(),
831                value_compression: None,
832                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
833                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
834                table_partition: "test-table".into(),
835                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
836                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
837                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
838                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
839                codec_config: (),
840            };
841            let checkpoint = {
842                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
843                    context.child("first"),
844                    cfg.clone(),
845                    None,
846                )
847                .await
848                .expect("Failed to initialize freezer");
849
850                let (freezer, _) = freezer.put(test_key("key1"), &42).await.unwrap();
851                let (freezer, _) = freezer.sync().await.unwrap();
852                freezer.close().await.unwrap()
853            };
854
855            // Corrupt the table by writing partial entry
856            {
857                let (blob, _) = context.open(&cfg.table_partition, b"table").await.unwrap();
858                // Write incomplete table entry (only 10 bytes instead of 24)
859                blob.write_at(0, vec![0xFF; 10], WriteOptions::SYNC)
860                    .await
861                    .unwrap();
862            }
863
864            // Reopen and verify it handles the corruption
865            {
866                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
867                    context.child("second"),
868                    cfg.clone(),
869                    Some(checkpoint),
870                )
871                .await
872                .expect("Failed to initialize freezer");
873
874                // The key should still be retrievable from journal if table is corrupted
875                // but the table entry is zeroed out
876                let result = freezer
877                    .get(Identifier::Key(&test_key("key1")))
878                    .await
879                    .unwrap();
880                assert!(result.is_none() || result == Some(42));
881            }
882        });
883    }
884
885    #[test_traced]
886    fn test_table_entry_invalid_crc() {
887        // Initialize the deterministic context
888        let executor = deterministic::Runner::default();
889        executor.start(|context| async move {
890            let cfg = Config {
891                key_partition: "test-key-index".into(),
892                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
893                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
894                value_partition: "test-value-journal".into(),
895                value_compression: None,
896                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
897                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
898                table_partition: "test-table".into(),
899                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
900                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
901                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
902                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
903                codec_config: (),
904            };
905
906            // Create freezer with data
907            let checkpoint = {
908                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
909                    context.child("first"),
910                    cfg.clone(),
911                    None,
912                )
913                .await
914                .expect("Failed to initialize freezer");
915
916                let (freezer, _) = freezer.put(test_key("key1"), &42).await.unwrap();
917                let (freezer, _) = freezer.sync().await.unwrap();
918                freezer.close().await.unwrap()
919            };
920
921            // Corrupt the CRC in the index entry
922            {
923                let (blob, _) = context.open(&cfg.table_partition, b"table").await.unwrap();
924                // Read the first entry
925                let entry_data = blob.read_at(0, 24, ReadOptions::default()).await.unwrap();
926                let mut corrupted = entry_data.coalesce();
927                // Corrupt the CRC (last 4 bytes of the entry)
928                corrupted.as_mut()[20] ^= 0xFF;
929                blob.write_at(0, corrupted, WriteOptions::SYNC)
930                    .await
931                    .unwrap();
932            }
933
934            // Reopen and verify it handles invalid CRC
935            {
936                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
937                    context.child("second"),
938                    cfg.clone(),
939                    Some(checkpoint),
940                )
941                .await
942                .expect("Failed to initialize freezer");
943
944                // With invalid CRC, the entry should be treated as invalid
945                let result = freezer
946                    .get(Identifier::Key(&test_key("key1")))
947                    .await
948                    .unwrap();
949                // The freezer should still work but may not find the key due to invalid table entry
950                assert!(result.is_none() || result == Some(42));
951            }
952        });
953    }
954
955    #[test_traced]
956    fn test_table_extra_bytes() {
957        // Initialize the deterministic context
958        let executor = deterministic::Runner::default();
959        executor.start(|context| async move {
960            let cfg = Config {
961                key_partition: "test-key-index".into(),
962                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
963                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
964                value_partition: "test-value-journal".into(),
965                value_compression: None,
966                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
967                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
968                table_partition: "test-table".into(),
969                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
970                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
971                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
972                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
973                codec_config: (),
974            };
975
976            // Create freezer with data
977            let checkpoint = {
978                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
979                    context.child("first"),
980                    cfg.clone(),
981                    None,
982                )
983                .await
984                .expect("Failed to initialize freezer");
985
986                let (freezer, _) = freezer.put(test_key("key1"), &42).await.unwrap();
987                let (freezer, _) = freezer.sync().await.unwrap();
988                freezer.close().await.unwrap()
989            };
990
991            // Add extra bytes to the table blob
992            {
993                let (blob, size) = context.open(&cfg.table_partition, b"table").await.unwrap();
994                // Append garbage data
995                blob.write_at(size, hex!("0xdeadbeef").to_vec(), WriteOptions::SYNC)
996                    .await
997                    .unwrap();
998            }
999
1000            // Reopen and verify it handles extra bytes gracefully
1001            {
1002                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1003                    context.child("second"),
1004                    cfg.clone(),
1005                    Some(checkpoint),
1006                )
1007                .await
1008                .expect("Failed to initialize freezer");
1009
1010                // Should still be able to read the key
1011                assert_eq!(
1012                    freezer
1013                        .get(Identifier::Key(&test_key("key1")))
1014                        .await
1015                        .unwrap(),
1016                    Some(42)
1017                );
1018
1019                // And write new data
1020                let (freezer, _) = freezer.put(test_key("key2"), &43).await.unwrap();
1021                assert_eq!(
1022                    freezer
1023                        .get(Identifier::Key(&test_key("key2")))
1024                        .await
1025                        .unwrap(),
1026                    Some(43)
1027                );
1028            }
1029        });
1030    }
1031
1032    #[test_traced]
1033    fn test_indexing_across_resizes() {
1034        // Initialize the deterministic context
1035        let executor = deterministic::Runner::default();
1036        executor.start(|context| async move {
1037            // Initialize the freezer
1038            let cfg = Config {
1039                key_partition: "test-key-index".into(),
1040                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1041                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1042                value_partition: "test-value-journal".into(),
1043                value_compression: None,
1044                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1045                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
1046                table_partition: "test-table".into(),
1047                table_initial_size: 2, // Very small initial size to force multiple resizes
1048                table_resize_frequency: 2, // Resize after 2 items per entry
1049                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
1050                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
1051                codec_config: (),
1052            };
1053            let mut freezer =
1054                Freezer::<_, FixedBytes<64>, i32>::init(context.child("first"), cfg.clone(), None)
1055                    .await
1056                    .expect("Failed to initialize freezer");
1057
1058            // Insert many keys to force multiple table resizes
1059            // Table will grow from 2 -> 4 -> 8 -> 16 -> 32 -> 64 -> 128 -> 256 -> 512 -> 1024
1060            let mut keys = Vec::new();
1061            for i in 0..1000 {
1062                let key = test_key(&format!("key{i}"));
1063                keys.push((key.clone(), i));
1064
1065                // Force sync to ensure resize occurs ASAP
1066                (freezer, _) = freezer.put(key, &i).await.expect("Failed to put data");
1067                (freezer, _) = freezer.sync().await.expect("Failed to sync");
1068            }
1069
1070            // Verify all keys can still be found after multiple resizes
1071            for (key, value) in &keys {
1072                let retrieved = freezer
1073                    .get(Identifier::Key(key))
1074                    .await
1075                    .expect("Failed to get data")
1076                    .expect("Data not found");
1077                assert_eq!(retrieved, *value, "Value mismatch for key after resizes");
1078            }
1079
1080            // Verify metrics show resize operations occurred. Must be checked
1081            // before closing: dropping the freezer drops its Registered metric
1082            // handles, which unregisters the metrics.
1083            let buffer = context.encode();
1084            assert!(buffer.contains("first_resizes_total 8"), "{}", buffer);
1085
1086            // Close and reopen to verify persistence
1087            let checkpoint = freezer.close().await.expect("Failed to close");
1088            let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1089                context.child("second"),
1090                cfg.clone(),
1091                Some(checkpoint),
1092            )
1093            .await
1094            .expect("Failed to reinitialize freezer");
1095
1096            // Verify all keys can still be found after restart
1097            for (key, value) in &keys {
1098                let retrieved = freezer
1099                    .get(Identifier::Key(key))
1100                    .await
1101                    .expect("Failed to get data")
1102                    .expect("Data not found");
1103                assert_eq!(retrieved, *value, "Value mismatch for key after restart");
1104            }
1105        });
1106    }
1107
1108    #[test_traced]
1109    fn test_insert_during_resize() {
1110        let executor = deterministic::Runner::default();
1111        executor.start(|context| async move {
1112            let cfg = Config {
1113                key_partition: "test-key-index".into(),
1114                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1115                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1116                value_partition: "test-value-journal".into(),
1117                value_compression: None,
1118                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1119                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
1120                table_partition: "test-table".into(),
1121                table_initial_size: 2,
1122                table_resize_frequency: 1,
1123                table_resize_chunk_size: 1, // Process one at a time
1124                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
1125                codec_config: (),
1126            };
1127            let mut freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1128                context.child("storage"),
1129                cfg.clone(),
1130                None,
1131            )
1132            .await
1133            .unwrap();
1134
1135            // Insert keys to trigger resize
1136            // key0 -> entry 0, key2 -> entry 1
1137            (freezer, _) = freezer.put(test_key("key0"), &0).await.unwrap();
1138            (freezer, _) = freezer.put(test_key("key2"), &1).await.unwrap();
1139            (freezer, _) = freezer.sync().await.unwrap(); // should start resize
1140
1141            // Verify resize started
1142            assert!(freezer.resizing().is_some());
1143
1144            // Insert during resize (to first entry)
1145            // key6 -> entry 0
1146            (freezer, _) = freezer.put(test_key("key6"), &2).await.unwrap();
1147            assert!(context.encode().contains("unnecessary_writes_total 1"));
1148            assert_eq!(freezer.resizable(), 3);
1149
1150            // Insert another key (to unmodified entry)
1151            // key3 -> entry 1
1152            (freezer, _) = freezer.put(test_key("key3"), &3).await.unwrap();
1153            assert!(context.encode().contains("unnecessary_writes_total 1"));
1154            assert_eq!(freezer.resizable(), 3);
1155
1156            // Verify resize completed
1157            (freezer, _) = freezer.sync().await.unwrap();
1158            assert!(freezer.resizing().is_none());
1159            assert_eq!(freezer.resizable(), 2);
1160
1161            // More inserts
1162            // key4 -> entry 1, key7 -> entry 0
1163            (freezer, _) = freezer.put(test_key("key4"), &4).await.unwrap();
1164            (freezer, _) = freezer.put(test_key("key7"), &5).await.unwrap();
1165            (freezer, _) = freezer.sync().await.unwrap();
1166
1167            // Another resize should've started
1168            assert!(freezer.resizing().is_some());
1169
1170            // Verify all can be retrieved during resize
1171            let keys = ["key0", "key2", "key6", "key3", "key4", "key7"];
1172            for (i, k) in keys.iter().enumerate() {
1173                assert_eq!(
1174                    freezer.get(Identifier::Key(&test_key(k))).await.unwrap(),
1175                    Some(i as i32)
1176                );
1177            }
1178
1179            // Sync until resize completes
1180            while freezer.resizing().is_some() {
1181                (freezer, _) = freezer.sync().await.unwrap();
1182            }
1183
1184            // Ensure no entries are considered resizable
1185            assert_eq!(freezer.resizable(), 0);
1186        });
1187    }
1188
1189    #[test_traced]
1190    fn test_resize_after_startup() {
1191        let executor = deterministic::Runner::default();
1192        executor.start(|context| async move {
1193            let cfg = Config {
1194                key_partition: "test-key-index".into(),
1195                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1196                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1197                value_partition: "test-value-journal".into(),
1198                value_compression: None,
1199                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1200                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
1201                table_partition: "test-table".into(),
1202                table_initial_size: 2,
1203                table_resize_frequency: 1,
1204                table_resize_chunk_size: 1, // Process one at a time
1205                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
1206                codec_config: (),
1207            };
1208
1209            // Create freezer and then shutdown uncleanly
1210            let checkpoint = {
1211                let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1212                    context.child("first"),
1213                    cfg.clone(),
1214                    None,
1215                )
1216                .await
1217                .unwrap();
1218
1219                // Insert keys to trigger resize
1220                // key0 -> entry 0, key2 -> entry 1
1221                let (freezer, _) = freezer.put(test_key("key0"), &0).await.unwrap();
1222                let (freezer, _) = freezer.put(test_key("key2"), &1).await.unwrap();
1223                let (freezer, checkpoint) = freezer.sync().await.unwrap();
1224
1225                // Verify resize started
1226                assert!(freezer.resizing().is_some());
1227
1228                checkpoint
1229            };
1230
1231            // Reopen freezer
1232            let mut freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1233                context.child("second"),
1234                cfg.clone(),
1235                Some(checkpoint),
1236            )
1237            .await
1238            .unwrap();
1239            assert_eq!(freezer.resizable(), 1);
1240            assert_eq!(freezer.resizing(), None);
1241
1242            // Verify resize restarts from the checkpointed table.
1243            (freezer, _) = freezer.sync().await.unwrap();
1244            assert_eq!(freezer.resizing(), Some(1));
1245
1246            // Run until resize completes
1247            while freezer.resizing().is_some() {
1248                (freezer, _) = freezer.sync().await.unwrap();
1249            }
1250
1251            // Ensure no entries are considered resizable
1252            assert_eq!(freezer.resizable(), 0);
1253        });
1254    }
1255
1256    fn test_operations_and_restart(num_keys: usize) -> String {
1257        let executor = deterministic::Runner::default();
1258        executor.start(|mut context| async move {
1259            let cfg = Config {
1260                key_partition: "test-key-index".into(),
1261                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1262                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1263                value_partition: "test-value-journal".into(),
1264                value_compression: None,
1265                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1266                value_target_size: 128, // Force multiple journal sections
1267                table_partition: "test-table".into(),
1268                table_initial_size: 8,     // Small table to force collisions
1269                table_resize_frequency: 2, // Force resize frequently
1270                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
1271                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
1272                codec_config: (),
1273            };
1274            let mut freezer = Freezer::<_, FixedBytes<96>, FixedBytes<256>>::init(
1275                context.child("init").with_attribute("index", 1),
1276                cfg.clone(),
1277                None,
1278            )
1279            .await
1280            .expect("Failed to initialize freezer");
1281
1282            // Generate and insert random key-value pairs
1283            let mut pairs = Vec::new();
1284
1285            for _ in 0..num_keys {
1286                // Generate random key
1287                let mut key = [0u8; 96];
1288                context.fill_bytes(&mut key);
1289                let key = FixedBytes::<96>::new(key);
1290
1291                // Generate random value
1292                let mut value = [0u8; 256];
1293                context.fill_bytes(&mut value);
1294                let value = FixedBytes::<256>::new(value);
1295
1296                // Store the key-value pair
1297                (freezer, _) = freezer
1298                    .put(key.clone(), &value)
1299                    .await
1300                    .expect("Failed to put data");
1301                pairs.push((key, value));
1302
1303                // Randomly sync to test resizing
1304                if context.random_bool(0.1) {
1305                    (freezer, _) = freezer.sync().await.expect("Failed to sync");
1306                }
1307            }
1308
1309            // Sync data
1310            (freezer, _) = freezer.sync().await.expect("Failed to sync");
1311
1312            // Verify all pairs can be retrieved
1313            for (key, value) in &pairs {
1314                let retrieved = freezer
1315                    .get(Identifier::Key(key))
1316                    .await
1317                    .expect("Failed to get data")
1318                    .expect("Data not found");
1319                assert_eq!(&retrieved, value);
1320            }
1321
1322            // Test get() on all keys
1323            for (key, _) in &pairs {
1324                assert!(
1325                    freezer
1326                        .get(Identifier::Key(key))
1327                        .await
1328                        .expect("Failed to check key")
1329                        .is_some()
1330                );
1331            }
1332
1333            // Check some non-existent keys
1334            for _ in 0..10 {
1335                let mut key = [0u8; 96];
1336                context.fill_bytes(&mut key);
1337                let key = FixedBytes::<96>::new(key);
1338                assert!(
1339                    freezer
1340                        .get(Identifier::Key(&key))
1341                        .await
1342                        .expect("Failed to check key")
1343                        .is_none()
1344                );
1345            }
1346
1347            // Close the freezer
1348            let checkpoint = freezer.close().await.expect("Failed to close freezer");
1349
1350            // Reopen the freezer
1351            let mut freezer = Freezer::<_, FixedBytes<96>, FixedBytes<256>>::init(
1352                context.child("init").with_attribute("index", 2),
1353                cfg.clone(),
1354                Some(checkpoint),
1355            )
1356            .await
1357            .expect("Failed to initialize freezer");
1358
1359            // Verify all pairs are still there after restart
1360            for (key, value) in &pairs {
1361                let retrieved = freezer
1362                    .get(Identifier::Key(key))
1363                    .await
1364                    .expect("Failed to get data")
1365                    .expect("Data not found");
1366                assert_eq!(&retrieved, value);
1367            }
1368
1369            // Add more pairs after restart to test collision handling
1370            for _ in 0..20 {
1371                let mut key = [0u8; 96];
1372                context.fill_bytes(&mut key);
1373                let key = FixedBytes::<96>::new(key);
1374
1375                let mut value = [0u8; 256];
1376                context.fill_bytes(&mut value);
1377                let value = FixedBytes::<256>::new(value);
1378
1379                (freezer, _) = freezer.put(key, &value).await.expect("Failed to put data");
1380            }
1381
1382            // Multiple syncs to test epoch progression
1383            for _ in 0..3 {
1384                (freezer, _) = freezer.sync().await.expect("Failed to sync");
1385
1386                // Add a few more entries between syncs
1387                for _ in 0..5 {
1388                    let mut key = [0u8; 96];
1389                    context.fill_bytes(&mut key);
1390                    let key = FixedBytes::<96>::new(key);
1391
1392                    let mut value = [0u8; 256];
1393                    context.fill_bytes(&mut value);
1394                    let value = FixedBytes::<256>::new(value);
1395
1396                    (freezer, _) = freezer.put(key, &value).await.expect("Failed to put data");
1397                }
1398            }
1399
1400            // Final sync
1401            freezer.sync().await.expect("Failed to sync");
1402
1403            // Return the auditor state for comparison
1404            context.auditor().state()
1405        })
1406    }
1407
1408    #[test_group("slow")]
1409    #[test_traced]
1410    fn test_determinism() {
1411        let state1 = test_operations_and_restart(1_000);
1412        let state2 = test_operations_and_restart(1_000);
1413        assert_eq!(state1, state2);
1414    }
1415
1416    #[test_traced]
1417    fn test_put_multiple_updates() {
1418        // Initialize the deterministic context
1419        let executor = deterministic::Runner::default();
1420        executor.start(|context| async move {
1421            // Initialize the freezer
1422            let cfg = Config {
1423                key_partition: "test-key-index".into(),
1424                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1425                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
1426                value_partition: "test-value-journal".into(),
1427                value_compression: None,
1428                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1429                value_target_size: DEFAULT_VALUE_TARGET_SIZE,
1430                table_partition: "test-table".into(),
1431                table_initial_size: DEFAULT_TABLE_INITIAL_SIZE,
1432                table_resize_frequency: DEFAULT_TABLE_RESIZE_FREQUENCY,
1433                table_resize_chunk_size: DEFAULT_TABLE_RESIZE_CHUNK_SIZE,
1434                table_replay_buffer: NZUsize!(DEFAULT_TABLE_REPLAY_BUFFER),
1435                codec_config: (),
1436            };
1437            let freezer = Freezer::<_, FixedBytes<64>, i32>::init(
1438                context.child("storage"),
1439                cfg.clone(),
1440                None,
1441            )
1442            .await
1443            .expect("Failed to initialize freezer");
1444
1445            let key = test_key("key1");
1446
1447            let (freezer, _) = freezer
1448                .put(key.clone(), &1)
1449                .await
1450                .expect("Failed to put data");
1451            let (freezer, _) = freezer
1452                .put(key.clone(), &2)
1453                .await
1454                .expect("Failed to put data");
1455            let (freezer, _) = freezer.sync().await.expect("Failed to sync");
1456            assert_eq!(
1457                freezer
1458                    .get(Identifier::Key(&key))
1459                    .await
1460                    .expect("Failed to get data")
1461                    .unwrap(),
1462                2
1463            );
1464        });
1465    }
1466}