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