Skip to main content

commonware_storage/archive/
mod.rs

1//! A write-once key-value store for ordered data.
2//!
3//! [Archive] is a key-value store designed for workloads where data is written only once and each
4//! item is addressed by both an `index` and a `key`. Workloads with unique indices should use [Archive]
5//! and workloads with overlapping indices should use [MultiArchive] (allows all items with the same index
6//! to be retrieved). The same key may be stored at multiple indices in either case, and a key lookup may
7//! return any of the associated values.
8
9use commonware_codec::CodecShared;
10use commonware_runtime::Handle;
11use commonware_utils::Array;
12use std::future::Future;
13use thiserror::Error;
14
15pub mod immutable;
16pub mod prunable;
17
18#[cfg(all(test, feature = "arbitrary"))]
19mod conformance;
20
21/// Subject of a `get` or `has` operation.
22pub enum Identifier<'a, K: Array> {
23    Index(u64),
24    Key(&'a K),
25}
26
27/// Errors that can occur when interacting with the archive.
28#[derive(Debug, Error)]
29pub enum Error {
30    #[error("journal error: {0}")]
31    Journal(crate::journal::Error),
32    #[error("ordinal error: {0}")]
33    Ordinal(#[from] crate::ordinal::Error),
34    #[error("metadata error: {0}")]
35    Metadata(#[from] crate::metadata::Error),
36    #[error("freezer error: {0}")]
37    Freezer(#[from] crate::freezer::Error),
38    #[error("record corrupted")]
39    RecordCorrupted,
40}
41
42// Preserve the archive's error classification for journal-owned metadata operations.
43impl From<crate::journal::Error> for Error {
44    fn from(error: crate::journal::Error) -> Self {
45        match error {
46            crate::journal::Error::Metadata(error) => Self::Metadata(error),
47            error => Self::Journal(error),
48        }
49    }
50}
51
52/// A write-once key-value store addressed by both an index and a key.
53///
54/// Mutating functions consume the archive and return it only on success: an error (or a
55/// dropped future) destroys the handle.
56pub trait Archive: Send + Sized {
57    /// The type of the key.
58    type Key: Array;
59
60    /// The type of the value.
61    type Value: CodecShared;
62
63    /// Store an item in [Archive].
64    ///
65    /// Indices are unique: if the index already exists, put does nothing and returns. Duplicate
66    /// indices can be stored via [MultiArchive::put_multi]. Keys need not be unique: the same key
67    /// may be stored at multiple indices, and a subsequent [Archive::get] or [Archive::has] call
68    /// with an [Identifier::Key] identifier may return any of the values associated with that key.
69    fn put(
70        self,
71        index: u64,
72        key: Self::Key,
73        value: &Self::Value,
74    ) -> impl Future<Output = Result<Self, Error>> + Send;
75
76    /// Perform a [Archive::put] and [Archive::sync] in a single operation.
77    fn put_sync(
78        self,
79        index: u64,
80        key: Self::Key,
81        value: &Self::Value,
82    ) -> impl Future<Output = Result<Self, Error>> + Send {
83        async move { self.put(index, key, value).await?.sync().await }
84    }
85
86    /// Perform a [Archive::put] and [Archive::start_sync] in a single operation.
87    ///
88    /// If the index already exists (making the put a no-op), the returned handle still reports
89    /// the durability of all previously accepted writes, including the original write for this
90    /// index if its sync is still in flight.
91    fn put_start_sync(
92        self,
93        index: u64,
94        key: Self::Key,
95        value: &Self::Value,
96    ) -> impl Future<Output = Result<(Self, Handle<()>), Error>> + Send {
97        async move { self.put(index, key, value).await?.start_sync().await }
98    }
99
100    /// Retrieve an item from [Archive].
101    ///
102    /// Note that if the [Archive] is a [MultiArchive], there may be multiple values associated with the
103    /// same [Identifier::Index]. If there are multiple values, the first stored will be returned. Use
104    /// [MultiArchive::get_all] to retrieve all values at an index.
105    fn get<'a>(
106        &'a self,
107        identifier: Identifier<'a, Self::Key>,
108    ) -> impl Future<Output = Result<Option<Self::Value>, Error>> + Send + use<'a, Self>;
109
110    /// Check if an item exists in [Archive].
111    fn has<'a>(
112        &'a self,
113        identifier: Identifier<'a, Self::Key>,
114    ) -> impl Future<Output = Result<bool, Error>> + Send + use<'a, Self>;
115
116    /// Retrieve the end of the current range including `index` (inclusive) and
117    /// the start of the next range after `index` (if it exists).
118    ///
119    /// This is useful for driving backfill operations over the archive.
120    fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>);
121
122    /// Returns up to `max` missing items starting from `start`.
123    ///
124    /// This method iterates through gaps between existing ranges, collecting missing indices
125    /// until either `max` items are found or there are no more gaps to fill.
126    fn missing_items(&self, index: u64, max: usize) -> Vec<u64>;
127
128    /// Retrieve an iterator over all populated ranges (inclusive) within the [Archive].
129    fn ranges(&self) -> impl Iterator<Item = (u64, u64)>;
130
131    /// Retrieve an iterator over ranges that overlap or follow `from`.
132    fn ranges_from(&self, from: u64) -> impl Iterator<Item = (u64, u64)>;
133
134    /// Retrieve the first index in the [Archive].
135    fn first_index(&self) -> Option<u64>;
136
137    /// Retrieve the last index in the [Archive].
138    fn last_index(&self) -> Option<u64>;
139
140    /// Sync all pending writes.
141    fn sync(self) -> impl Future<Output = Result<Self, Error>> + Send;
142
143    /// Request that all pending writes are synced.
144    ///
145    /// The returned handle completes once every write accepted before this call is durable,
146    /// including writes covered by a sync that is still in flight. Implementations without a
147    /// non-blocking sync path may complete the sync before returning an already-finished handle.
148    ///
149    /// An error reported by the returned [Handle] is fatal to the archive: the caller must
150    /// stop using the returned archive.
151    fn start_sync(self) -> impl Future<Output = Result<(Self, Handle<()>), Error>> + Send {
152        async move {
153            let archive = self.sync().await?;
154            Ok((archive, Handle::ready(Ok(()))))
155        }
156    }
157
158    /// Remove all persistent data created by this [Archive].
159    fn destroy(self) -> impl Future<Output = Result<(), Error>> + Send;
160}
161
162/// Extension of [Archive] that supports multiple items at the same index.
163///
164/// Unlike [Archive::put], which is a no-op when the index already exists,
165/// [MultiArchive::put_multi] allows storing additional `(key, value)` pairs
166/// at an existing index.
167pub trait MultiArchive: Archive {
168    /// Retrieve all values stored at the given index.
169    ///
170    /// Returns `None` if the index does not exist or has been pruned.
171    fn get_all(
172        &self,
173        index: u64,
174    ) -> impl Future<Output = Result<Option<Vec<Self::Value>>, Error>> + Send + use<'_, Self>;
175
176    /// Check whether `key` is stored at `index`.
177    ///
178    /// Unlike [Archive::has] with [Identifier::Key], the check is scoped to a
179    /// single index. Unlike [MultiArchive::get_all], no values are fetched.
180    fn has_at<'a>(
181        &'a self,
182        index: u64,
183        key: &'a Self::Key,
184    ) -> impl Future<Output = Result<bool, Error>> + Send + use<'a, Self>;
185
186    /// Store an item, allowing multiple items at the same index.
187    ///
188    /// Multiple items may share the same `index`. If the same key is stored at
189    /// multiple indices, any associated value may be returned when queried with
190    /// [Identifier::Key].
191    fn put_multi(
192        self,
193        index: u64,
194        key: Self::Key,
195        value: &Self::Value,
196    ) -> impl Future<Output = Result<Self, Error>> + Send;
197
198    /// Perform a [MultiArchive::put_multi] and [Archive::sync] in a single operation.
199    fn put_multi_sync(
200        self,
201        index: u64,
202        key: Self::Key,
203        value: &Self::Value,
204    ) -> impl Future<Output = Result<Self, Error>> + Send {
205        async move { self.put_multi(index, key, value).await?.sync().await }
206    }
207
208    /// Perform a [MultiArchive::put_multi] and [Archive::start_sync] in a single operation.
209    fn put_multi_start_sync(
210        self,
211        index: u64,
212        key: Self::Key,
213        value: &Self::Value,
214    ) -> impl Future<Output = Result<(Self, Handle<()>), Error>> + Send {
215        async move { self.put_multi(index, key, value).await?.start_sync().await }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::translator::TwoCap;
223    use commonware_macros::{test_group, test_traced};
224    use commonware_runtime::{
225        Metrics as _, Runner, Supervisor as _,
226        buffer::paged::CacheRef,
227        deterministic::{self, Context},
228        telemetry::metrics::has_metric_value,
229    };
230    use commonware_utils::{NZU16, NZU64, NZUsize, sequence::FixedBytes};
231    use rand::RngExt as _;
232    use std::{
233        collections::BTreeMap,
234        num::{NonZeroU16, NonZeroUsize},
235    };
236
237    fn test_key(key: &str) -> FixedBytes<64> {
238        let mut buf = [0u8; 64];
239        let key = key.as_bytes();
240        assert!(key.len() <= buf.len());
241        buf[..key.len()].copy_from_slice(key);
242        FixedBytes::new(buf)
243    }
244
245    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
246    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
247
248    async fn create_prunable(
249        context: Context,
250        compression: Option<u8>,
251    ) -> impl MultiArchive<Key = FixedBytes<64>, Value = i32> {
252        let cfg = prunable::Config {
253            translator: TwoCap,
254            metadata_partition: "test-metadata".into(),
255            key_partition: "test-key".into(),
256            key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
257            value_partition: "test-value".into(),
258            compression,
259            codec_config: (),
260            items_per_section: NZU64!(1024),
261            key_write_buffer: NZUsize!(1024),
262            value_write_buffer: NZUsize!(1024),
263            replay_buffer: NZUsize!(1024),
264        };
265        prunable::Archive::init(context, cfg).await.unwrap()
266    }
267
268    async fn create_immutable(
269        context: Context,
270        compression: Option<u8>,
271    ) -> impl Archive<Key = FixedBytes<64>, Value = i32> {
272        let cfg = immutable::Config {
273            metadata_partition: "test-metadata".into(),
274            freezer_table_partition: "test-freezer-table".into(),
275            freezer_table_initial_size: 64,
276            freezer_table_resize_frequency: 2,
277            freezer_table_resize_chunk_size: 32,
278            freezer_key_partition: "test-freezer-key".into(),
279            freezer_key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
280            freezer_value_partition: "test-freezer-value".into(),
281            freezer_value_target_size: 1024 * 1024,
282            freezer_value_compression: compression,
283            ordinal_partition: "test-ordinal".into(),
284            items_per_section: NZU64!(1024),
285            freezer_key_write_buffer: NZUsize!(1024 * 1024),
286            freezer_value_write_buffer: NZUsize!(1024 * 1024),
287            ordinal_write_buffer: NZUsize!(1024 * 1024),
288            replay_buffer: NZUsize!(1024 * 1024),
289            codec_config: (),
290        };
291        immutable::Archive::init(context, cfg).await.unwrap()
292    }
293
294    async fn test_put_get_impl(mut archive: impl Archive<Key = FixedBytes<64>, Value = i32>) {
295        let index = 1u64;
296        let key = test_key("testkey");
297        let data = 1;
298
299        // Has the key before put
300        let has = archive
301            .has(Identifier::Index(index))
302            .await
303            .expect("Failed to check key");
304        assert!(!has);
305        let has = archive
306            .has(Identifier::Key(&key))
307            .await
308            .expect("Failed to check key");
309        assert!(!has);
310
311        // Put the key-data pair
312        archive = archive
313            .put(index, key.clone(), &data)
314            .await
315            .expect("Failed to put data");
316
317        // Has the key after put
318        let has = archive
319            .has(Identifier::Index(index))
320            .await
321            .expect("Failed to check key");
322        assert!(has);
323        let has = archive
324            .has(Identifier::Key(&key))
325            .await
326            .expect("Failed to check key");
327        assert!(has);
328
329        // Get the data by key
330        let retrieved = archive
331            .get(Identifier::Key(&key))
332            .await
333            .expect("Failed to get data");
334        assert_eq!(retrieved, Some(data));
335
336        // Get the data by index
337        let retrieved = archive
338            .get(Identifier::Index(index))
339            .await
340            .expect("Failed to get data");
341        assert_eq!(retrieved, Some(data));
342
343        // Force a sync
344        archive.sync().await.expect("Failed to sync data");
345    }
346
347    #[test_traced]
348    fn test_put_get_prunable_no_compression() {
349        let executor = deterministic::Runner::default();
350        executor.start(|context| async move {
351            let archive = create_prunable(context, None).await;
352            test_put_get_impl(archive).await;
353        });
354    }
355
356    #[test_traced]
357    fn test_put_get_prunable_compression() {
358        let executor = deterministic::Runner::default();
359        executor.start(|context| async move {
360            let archive = create_prunable(context, Some(3)).await;
361            test_put_get_impl(archive).await;
362        });
363    }
364
365    #[test_traced]
366    fn test_put_get_immutable_no_compression() {
367        let executor = deterministic::Runner::default();
368        executor.start(|context| async move {
369            let archive = create_immutable(context, None).await;
370            test_put_get_impl(archive).await;
371        });
372    }
373
374    #[test_traced]
375    fn test_put_get_immutable_compression() {
376        let executor = deterministic::Runner::default();
377        executor.start(|context| async move {
378            let archive = create_immutable(context, Some(3)).await;
379            test_put_get_impl(archive).await;
380        });
381    }
382
383    async fn test_duplicate_key_impl(mut archive: impl Archive<Key = FixedBytes<64>, Value = i32>) {
384        let index = 1u64;
385        let key = test_key("duplicate");
386        let data1 = 1;
387        let data2 = 2;
388
389        // Put the key-data pair
390        archive = archive
391            .put(index, key.clone(), &data1)
392            .await
393            .expect("Failed to put data");
394
395        // Put the key-data pair again (should be idempotent)
396        archive = archive
397            .put(index, key.clone(), &data2)
398            .await
399            .expect("Duplicate put should not fail");
400
401        // Get the data back - should still be the first value
402        let retrieved = archive
403            .get(Identifier::Index(index))
404            .await
405            .expect("Failed to get data")
406            .expect("Data not found");
407        assert_eq!(retrieved, data1);
408
409        let retrieved = archive
410            .get(Identifier::Key(&key))
411            .await
412            .expect("Failed to get data")
413            .expect("Data not found");
414        assert_eq!(retrieved, data1);
415    }
416
417    #[test_traced]
418    fn test_duplicate_key_prunable_no_compression() {
419        let executor = deterministic::Runner::default();
420        executor.start(|context| async move {
421            let archive = create_prunable(context, None).await;
422            test_duplicate_key_impl(archive).await;
423        });
424    }
425
426    #[test_traced]
427    fn test_duplicate_key_prunable_compression() {
428        let executor = deterministic::Runner::default();
429        executor.start(|context| async move {
430            let archive = create_prunable(context, Some(3)).await;
431            test_duplicate_key_impl(archive).await;
432        });
433    }
434
435    #[test_traced]
436    fn test_duplicate_key_immutable_no_compression() {
437        let executor = deterministic::Runner::default();
438        executor.start(|context| async move {
439            let archive = create_immutable(context, None).await;
440            test_duplicate_key_impl(archive).await;
441        });
442    }
443
444    async fn test_duplicate_key_cross_index_impl(
445        mut archive: impl Archive<Key = FixedBytes<64>, Value = i32>,
446    ) {
447        // Store the same key at two different indices; distinct values only so
448        // the test can observe which entry wins a key lookup.
449        let key = test_key("dupe-xindex");
450        archive = archive.put(2, key.clone(), &20).await.expect("put(2)");
451        archive = archive.put(5, key.clone(), &50).await.expect("put(5)");
452
453        // Both indices must resolve individually.
454        assert_eq!(
455            archive.get(Identifier::Index(2)).await.unwrap(),
456            Some(20),
457            "Index(2) must resolve to the value stored at 2"
458        );
459        assert_eq!(
460            archive.get(Identifier::Index(5)).await.unwrap(),
461            Some(50),
462            "Index(5) must resolve to the value stored at 5"
463        );
464
465        // Key lookup may return either value per the contract; just assert it
466        // returns one of them and that `has` reports presence.
467        let got = archive
468            .get(Identifier::Key(&key))
469            .await
470            .unwrap()
471            .expect("key lookup must find at least one entry");
472        assert!(got == 20 || got == 50, "unexpected value: {got}");
473        assert!(archive.has(Identifier::Key(&key)).await.unwrap());
474    }
475
476    #[test_traced]
477    fn test_duplicate_key_cross_index_prunable_no_compression() {
478        let executor = deterministic::Runner::default();
479        executor.start(|context| async move {
480            let archive = create_prunable(context, None).await;
481            test_duplicate_key_cross_index_impl(archive).await;
482        });
483    }
484
485    #[test_traced]
486    fn test_duplicate_key_cross_index_prunable_compression() {
487        let executor = deterministic::Runner::default();
488        executor.start(|context| async move {
489            let archive = create_prunable(context, Some(3)).await;
490            test_duplicate_key_cross_index_impl(archive).await;
491        });
492    }
493
494    #[test_traced]
495    fn test_duplicate_key_cross_index_immutable_no_compression() {
496        let executor = deterministic::Runner::default();
497        executor.start(|context| async move {
498            let archive = create_immutable(context, None).await;
499            test_duplicate_key_cross_index_impl(archive).await;
500        });
501    }
502
503    #[test_traced]
504    fn test_duplicate_key_cross_index_immutable_compression() {
505        let executor = deterministic::Runner::default();
506        executor.start(|context| async move {
507            let archive = create_immutable(context, Some(3)).await;
508            test_duplicate_key_cross_index_impl(archive).await;
509        });
510    }
511
512    #[test_traced]
513    fn test_duplicate_key_immutable_compression() {
514        let executor = deterministic::Runner::default();
515        executor.start(|context| async move {
516            let archive = create_immutable(context, Some(3)).await;
517            test_duplicate_key_impl(archive).await;
518        });
519    }
520
521    async fn test_get_nonexistent_impl(archive: impl Archive<Key = FixedBytes<64>, Value = i32>) {
522        // Attempt to get an index that doesn't exist
523        let index = 1u64;
524        let retrieved: Option<i32> = archive
525            .get(Identifier::Index(index))
526            .await
527            .expect("Failed to get data");
528        assert!(retrieved.is_none());
529
530        // Attempt to get a key that doesn't exist
531        let key = test_key("nonexistent");
532        let retrieved = archive
533            .get(Identifier::Key(&key))
534            .await
535            .expect("Failed to get data");
536        assert!(retrieved.is_none());
537    }
538
539    #[test_traced]
540    fn test_get_nonexistent_prunable_no_compression() {
541        let executor = deterministic::Runner::default();
542        executor.start(|context| async move {
543            let archive = create_prunable(context, None).await;
544            test_get_nonexistent_impl(archive).await;
545        });
546    }
547
548    #[test_traced]
549    fn test_get_nonexistent_prunable_compression() {
550        let executor = deterministic::Runner::default();
551        executor.start(|context| async move {
552            let archive = create_prunable(context, Some(3)).await;
553            test_get_nonexistent_impl(archive).await;
554        });
555    }
556
557    #[test_traced]
558    fn test_get_nonexistent_immutable_no_compression() {
559        let executor = deterministic::Runner::default();
560        executor.start(|context| async move {
561            let archive = create_immutable(context, None).await;
562            test_get_nonexistent_impl(archive).await;
563        });
564    }
565
566    #[test_traced]
567    fn test_get_nonexistent_immutable_compression() {
568        let executor = deterministic::Runner::default();
569        executor.start(|context| async move {
570            let archive = create_immutable(context, Some(3)).await;
571            test_get_nonexistent_impl(archive).await;
572        });
573    }
574
575    async fn test_persistence_impl<A, F, Fut>(context: Context, creator: F, compression: Option<u8>)
576    where
577        A: Archive<Key = FixedBytes<64>, Value = i32>,
578        F: Fn(Context, Option<u8>) -> Fut,
579        Fut: Future<Output = A>,
580    {
581        // Create and populate archive
582        {
583            let mut archive = creator(context.child("first"), compression).await;
584
585            // Insert multiple keys
586            let keys = vec![
587                (1u64, test_key("key1"), 1),
588                (2u64, test_key("key2"), 2),
589                (3u64, test_key("key3"), 3),
590            ];
591
592            for (index, key, data) in &keys {
593                archive = archive
594                    .put(*index, key.clone(), data)
595                    .await
596                    .expect("Failed to put data");
597            }
598
599            // Sync and drop the archive
600            archive.sync().await.expect("Failed to sync archive");
601        }
602
603        // Reopen and verify data
604        {
605            let archive = creator(context.child("second"), compression).await;
606
607            // Verify all keys are still present
608            let keys = vec![
609                (1u64, test_key("key1"), 1),
610                (2u64, test_key("key2"), 2),
611                (3u64, test_key("key3"), 3),
612            ];
613
614            for (index, key, expected_data) in &keys {
615                let retrieved = archive
616                    .get(Identifier::Index(*index))
617                    .await
618                    .expect("Failed to get data")
619                    .expect("Data not found");
620                assert_eq!(retrieved, *expected_data);
621
622                let retrieved = archive
623                    .get(Identifier::Key(key))
624                    .await
625                    .expect("Failed to get data")
626                    .expect("Data not found");
627                assert_eq!(retrieved, *expected_data);
628            }
629        }
630    }
631
632    #[test_traced]
633    fn test_persistence_prunable_no_compression() {
634        let executor = deterministic::Runner::default();
635        executor.start(|context| async move {
636            test_persistence_impl(context, create_prunable, None).await;
637        });
638    }
639
640    #[test_traced]
641    fn test_persistence_prunable_compression() {
642        let executor = deterministic::Runner::default();
643        executor.start(|context| async move {
644            test_persistence_impl(context, create_prunable, Some(3)).await;
645        });
646    }
647
648    #[test_traced]
649    fn test_persistence_immutable_no_compression() {
650        let executor = deterministic::Runner::default();
651        executor.start(|context| async move {
652            test_persistence_impl(context, create_immutable, None).await;
653        });
654    }
655
656    #[test_traced]
657    fn test_persistence_immutable_compression() {
658        let executor = deterministic::Runner::default();
659        executor.start(|context| async move {
660            test_persistence_impl(context, create_immutable, Some(3)).await;
661        });
662    }
663
664    async fn test_ranges_impl<A, F, Fut>(mut context: Context, creator: F, compression: Option<u8>)
665    where
666        A: Archive<Key = FixedBytes<64>, Value = i32>,
667        F: Fn(Context, Option<u8>) -> Fut,
668        Fut: Future<Output = A>,
669    {
670        let mut keys = BTreeMap::new();
671        {
672            let mut archive = creator(context.child("first"), compression).await;
673
674            // Insert 100 keys with gaps
675            let mut last_index = 0u64;
676            while keys.len() < 100 {
677                let gap: u64 = context.random_range(1..=10);
678                let index = last_index + gap;
679                last_index = index;
680
681                let mut key_bytes = [0u8; 64];
682                context.fill(&mut key_bytes);
683                let key = FixedBytes::new(key_bytes);
684                let data: i32 = context.random();
685
686                if keys.contains_key(&index) {
687                    continue;
688                }
689                keys.insert(index, (key.clone(), data));
690
691                archive = archive
692                    .put(index, key, &data)
693                    .await
694                    .expect("Failed to put data");
695            }
696
697            archive.sync().await.expect("Failed to sync archive");
698        }
699
700        {
701            let archive = creator(context.child("second"), compression).await;
702            let sorted_indices: Vec<u64> = keys.keys().cloned().collect();
703
704            // Check gap before the first element
705            let (current_end, start_next) = archive.next_gap(0);
706            assert!(current_end.is_none());
707            assert_eq!(start_next, Some(sorted_indices[0]));
708
709            // Check gaps between elements
710            let mut i = 0;
711            while i < sorted_indices.len() {
712                let current_index = sorted_indices[i];
713
714                // Find the end of the current contiguous block
715                let mut j = i;
716                while j + 1 < sorted_indices.len() && sorted_indices[j + 1] == sorted_indices[j] + 1
717                {
718                    j += 1;
719                }
720                let block_end_index = sorted_indices[j];
721                let next_actual_index = if j + 1 < sorted_indices.len() {
722                    Some(sorted_indices[j + 1])
723                } else {
724                    None
725                };
726
727                let (current_end, start_next) = archive.next_gap(current_index);
728                assert_eq!(current_end, Some(block_end_index));
729                assert_eq!(start_next, next_actual_index);
730
731                // If there's a gap, check an index within the gap
732                if let Some(next_index) = next_actual_index
733                    && next_index > block_end_index + 1
734                {
735                    let in_gap_index = block_end_index + 1;
736                    let (current_end, start_next) = archive.next_gap(in_gap_index);
737                    assert!(current_end.is_none());
738                    assert_eq!(start_next, Some(next_index));
739                }
740                i = j + 1;
741            }
742
743            // Check the last element
744            let last_index = *sorted_indices.last().unwrap();
745            let (current_end, start_next) = archive.next_gap(last_index);
746            assert!(current_end.is_some());
747            assert!(start_next.is_none());
748        }
749    }
750
751    #[test_traced]
752    fn test_ranges_prunable_no_compression() {
753        let executor = deterministic::Runner::default();
754        executor.start(|context| async move {
755            test_ranges_impl(context, create_prunable, None).await;
756        });
757    }
758
759    #[test_traced]
760    fn test_ranges_prunable_compression() {
761        let executor = deterministic::Runner::default();
762        executor.start(|context| async move {
763            test_ranges_impl(context, create_prunable, Some(3)).await;
764        });
765    }
766
767    #[test_traced]
768    fn test_ranges_immutable_no_compression() {
769        let executor = deterministic::Runner::default();
770        executor.start(|context| async move {
771            test_ranges_impl(context, create_immutable, None).await;
772        });
773    }
774
775    #[test_traced]
776    fn test_ranges_immutable_compression() {
777        let executor = deterministic::Runner::default();
778        executor.start(|context| async move {
779            test_ranges_impl(context, create_immutable, Some(3)).await;
780        });
781    }
782
783    async fn test_many_keys_impl<A, F, Fut>(
784        mut context: Context,
785        creator: F,
786        compression: Option<u8>,
787        num: usize,
788    ) where
789        A: Archive<Key = FixedBytes<64>, Value = i32>,
790        F: Fn(Context, Option<u8>) -> Fut,
791        Fut: Future<Output = A>,
792    {
793        // Insert many keys
794        let mut keys = BTreeMap::new();
795        {
796            let mut archive = creator(context.child("first"), compression).await;
797            while keys.len() < num {
798                let index = keys.len() as u64;
799                let mut key = [0u8; 64];
800                context.fill(&mut key);
801                let key = FixedBytes::new(key);
802                let data: i32 = context.random();
803
804                archive = archive
805                    .put(index, key.clone(), &data)
806                    .await
807                    .expect("Failed to put data");
808                keys.insert(key, (index, data));
809
810                // Randomly sync the archive
811                if context.random_bool(0.1) {
812                    archive = archive.sync().await.expect("Failed to sync archive");
813                }
814            }
815            archive = archive.sync().await.expect("Failed to sync archive");
816
817            // Ensure all keys can be retrieved
818            for (key, (index, data)) in &keys {
819                let retrieved = archive
820                    .get(Identifier::Index(*index))
821                    .await
822                    .expect("Failed to get data")
823                    .expect("Data not found");
824                assert_eq!(&retrieved, data);
825                let retrieved = archive
826                    .get(Identifier::Key(key))
827                    .await
828                    .expect("Failed to get data")
829                    .expect("Data not found");
830                assert_eq!(&retrieved, data);
831            }
832        }
833
834        // Reinitialize and verify
835        {
836            let archive = creator(context.child("second"), compression).await;
837
838            // Ensure all keys can be retrieved
839            for (key, (index, data)) in &keys {
840                let retrieved = archive
841                    .get(Identifier::Index(*index))
842                    .await
843                    .expect("Failed to get data")
844                    .expect("Data not found");
845                assert_eq!(&retrieved, data);
846                let retrieved = archive
847                    .get(Identifier::Key(key))
848                    .await
849                    .expect("Failed to get data")
850                    .expect("Data not found");
851                assert_eq!(&retrieved, data);
852            }
853        }
854    }
855
856    fn test_many_keys_determinism<F, Fut, A>(creator: F, compression: Option<u8>, num: usize)
857    where
858        A: Archive<Key = FixedBytes<64>, Value = i32>,
859        F: Fn(Context, Option<u8>) -> Fut + Copy + Send + 'static,
860        Fut: Future<Output = A> + Send,
861    {
862        let executor = deterministic::Runner::default();
863        let state1 = executor.start(|context| async move {
864            test_many_keys_impl(context.child("storage"), creator, compression, num).await;
865            context.auditor().state()
866        });
867        let executor = deterministic::Runner::default();
868        let state2 = executor.start(|context| async move {
869            test_many_keys_impl(context.child("storage"), creator, compression, num).await;
870            context.auditor().state()
871        });
872        assert_eq!(state1, state2);
873    }
874
875    #[test_traced]
876    fn test_many_keys_prunable_no_compression() {
877        test_many_keys_determinism(create_prunable, None, 1_000);
878    }
879
880    #[test_traced]
881    fn test_many_keys_prunable_compression() {
882        test_many_keys_determinism(create_prunable, Some(3), 1_000);
883    }
884
885    #[test_traced]
886    fn test_many_keys_immutable_no_compression() {
887        test_many_keys_determinism(create_immutable, None, 1_000);
888    }
889
890    #[test_traced]
891    fn test_many_keys_immutable_compression() {
892        test_many_keys_determinism(create_immutable, Some(3), 1_000);
893    }
894
895    #[test_group("slow")]
896    #[test_traced]
897    fn test_many_keys_prunable_large() {
898        test_many_keys_determinism(create_prunable, None, 50_000);
899    }
900
901    #[test_group("slow")]
902    #[test_traced]
903    fn test_many_keys_immutable_large() {
904        test_many_keys_determinism(create_immutable, None, 50_000);
905    }
906
907    async fn test_put_multi_and_get_impl(
908        context: Context,
909        mut archive: impl MultiArchive<Key = FixedBytes<64>, Value = i32>,
910    ) {
911        // Put three items at the same index with different keys
912        let index = 5u64;
913        let key_a = test_key("aaa");
914        let key_b = test_key("bbb");
915        let key_c = test_key("ccc");
916
917        archive = archive
918            .put_multi(index, key_a.clone(), &10)
919            .await
920            .expect("put_multi a");
921        archive = archive
922            .put_multi(index, key_b.clone(), &20)
923            .await
924            .expect("put_multi b");
925        archive = archive
926            .put_multi(index, key_c.clone(), &30)
927            .await
928            .expect("put_multi c");
929
930        // Retrieve each by key
931        assert_eq!(
932            archive.get(Identifier::Key(&key_a)).await.unwrap(),
933            Some(10)
934        );
935        assert_eq!(
936            archive.get(Identifier::Key(&key_b)).await.unwrap(),
937            Some(20)
938        );
939        assert_eq!(
940            archive.get(Identifier::Key(&key_c)).await.unwrap(),
941            Some(30)
942        );
943
944        // Missing key returns None
945        let missing = test_key("zzz");
946        assert_eq!(archive.get(Identifier::Key(&missing)).await.unwrap(), None);
947
948        // items_tracked reflects unique indices, not total items
949        let buffer = context.encode();
950        assert!(has_metric_value(&buffer, "items_tracked", 1));
951    }
952
953    #[test_traced]
954    fn test_put_multi_and_get_prunable() {
955        let executor = deterministic::Runner::default();
956        executor.start(|context| async move {
957            let archive = create_prunable(context.child("storage"), None).await;
958            test_put_multi_and_get_impl(context, archive).await;
959        });
960    }
961
962    async fn test_put_multi_duplicate_key_impl(
963        context: Context,
964        mut archive: impl MultiArchive<Key = FixedBytes<64>, Value = i32>,
965    ) {
966        let key = test_key("dup");
967        archive = archive.put_multi(5, key.clone(), &10).await.unwrap();
968        archive = archive.put_multi(7, key.clone(), &20).await.unwrap();
969
970        // Duplicate key is allowed across indices.
971        assert_eq!(archive.get(Identifier::Index(5)).await.unwrap(), Some(10));
972        assert_eq!(archive.get(Identifier::Index(7)).await.unwrap(), Some(20));
973        assert_eq!(archive.get_all(5).await.unwrap(), Some(vec![10]));
974        assert_eq!(archive.get_all(7).await.unwrap(), Some(vec![20]));
975
976        // Like Archive::put, duplicate keys may return any associated value.
977        assert!(matches!(
978            archive.get(Identifier::Key(&key)).await.unwrap(),
979            Some(10 | 20)
980        ));
981
982        let buffer = context.encode();
983        assert!(has_metric_value(&buffer, "items_tracked", 2));
984    }
985
986    #[test_traced]
987    fn test_put_multi_duplicate_key_prunable() {
988        let executor = deterministic::Runner::default();
989        executor.start(|context| async move {
990            let archive = create_prunable(context.child("storage"), None).await;
991            test_put_multi_duplicate_key_impl(context, archive).await;
992        });
993    }
994
995    async fn test_get_all_impl(mut archive: impl MultiArchive<Key = FixedBytes<64>, Value = i32>) {
996        // Three items at the same index
997        archive = archive.put_multi(5, test_key("aaa"), &10).await.unwrap();
998        archive = archive.put_multi(5, test_key("bbb"), &20).await.unwrap();
999        archive = archive.put_multi(5, test_key("ccc"), &30).await.unwrap();
1000
1001        // One item at a different index
1002        archive = archive.put_multi(7, test_key("ddd"), &40).await.unwrap();
1003
1004        // get_all returns all values at the index in insertion order
1005        let all = archive.get_all(5).await.unwrap();
1006        assert_eq!(all, Some(vec![10, 20, 30]));
1007
1008        // Single-item index returns one element
1009        let all = archive.get_all(7).await.unwrap();
1010        assert_eq!(all, Some(vec![40]));
1011
1012        // Missing index returns None
1013        let all = archive.get_all(99).await.unwrap();
1014        assert_eq!(all, None);
1015
1016        // Archive::get(Index) still returns only the first
1017        assert_eq!(archive.get(Identifier::Index(5)).await.unwrap(), Some(10));
1018    }
1019
1020    #[test_traced]
1021    fn test_get_all_prunable() {
1022        let executor = deterministic::Runner::default();
1023        executor.start(|context| async move {
1024            let archive = create_prunable(context, None).await;
1025            test_get_all_impl(archive).await;
1026        });
1027    }
1028
1029    async fn test_put_multi_preserves_archive_put_semantics_impl(
1030        mut archive: impl MultiArchive<Key = FixedBytes<64>, Value = i32>,
1031    ) {
1032        // put_multi two items at the same index
1033        archive = archive
1034            .put_multi(1, test_key("aaa"), &10)
1035            .await
1036            .expect("put_multi");
1037        archive = archive
1038            .put_multi(1, test_key("bbb"), &20)
1039            .await
1040            .expect("put_multi");
1041
1042        // Archive::put is a no-op when index already exists
1043        archive = archive
1044            .put(1, test_key("ccc"), &30)
1045            .await
1046            .expect("Archive::put should no-op");
1047
1048        // Only two items exist (Archive::put did not add a third)
1049        assert_eq!(
1050            archive
1051                .get(Identifier::Key(&test_key("aaa")))
1052                .await
1053                .unwrap(),
1054            Some(10)
1055        );
1056        assert_eq!(
1057            archive
1058                .get(Identifier::Key(&test_key("bbb")))
1059                .await
1060                .unwrap(),
1061            Some(20)
1062        );
1063        assert_eq!(
1064            archive
1065                .get(Identifier::Key(&test_key("ccc")))
1066                .await
1067                .unwrap(),
1068            None
1069        );
1070
1071        // Archive::get(Index) returns the first item inserted
1072        let first = archive
1073            .get(Identifier::Index(1))
1074            .await
1075            .unwrap()
1076            .expect("should find first");
1077        assert_eq!(first, 10);
1078    }
1079
1080    #[test_traced]
1081    fn test_put_multi_preserves_archive_put_semantics_prunable() {
1082        let executor = deterministic::Runner::default();
1083        executor.start(|context| async move {
1084            let archive = create_prunable(context, None).await;
1085            test_put_multi_preserves_archive_put_semantics_impl(archive).await;
1086        });
1087    }
1088
1089    async fn test_put_multi_restart_impl<A, F, Fut>(
1090        context: Context,
1091        creator: F,
1092        compression: Option<u8>,
1093    ) where
1094        A: MultiArchive<Key = FixedBytes<64>, Value = i32>,
1095        F: Fn(Context, Option<u8>) -> Fut,
1096        Fut: Future<Output = A>,
1097    {
1098        // Write multi-items, sync, and drop
1099        {
1100            let mut archive = creator(
1101                context.child("init").with_attribute("index", 1),
1102                compression,
1103            )
1104            .await;
1105            archive = archive.put_multi(5, test_key("aaa"), &10).await.unwrap();
1106            archive = archive.put_multi(5, test_key("bbb"), &20).await.unwrap();
1107            archive = archive.put_multi(7, test_key("ccc"), &30).await.unwrap();
1108            archive.sync().await.unwrap();
1109        }
1110
1111        // Reinitialize and verify
1112        let archive = creator(
1113            context.child("init").with_attribute("index", 2),
1114            compression,
1115        )
1116        .await;
1117
1118        assert_eq!(
1119            archive
1120                .get(Identifier::Key(&test_key("aaa")))
1121                .await
1122                .unwrap(),
1123            Some(10)
1124        );
1125        assert_eq!(
1126            archive
1127                .get(Identifier::Key(&test_key("bbb")))
1128                .await
1129                .unwrap(),
1130            Some(20)
1131        );
1132        assert_eq!(
1133            archive
1134                .get(Identifier::Key(&test_key("ccc")))
1135                .await
1136                .unwrap(),
1137            Some(30)
1138        );
1139
1140        // items_tracked reflects two unique indices after restart
1141        let buffer = context.encode();
1142        assert!(has_metric_value(&buffer, "items_tracked", 2));
1143    }
1144
1145    #[test_traced]
1146    fn test_put_multi_restart_prunable() {
1147        let executor = deterministic::Runner::default();
1148        executor.start(|context| async move {
1149            test_put_multi_restart_impl(context, create_prunable, None).await;
1150        });
1151    }
1152
1153    async fn test_put_multi_mixed_indices_impl(
1154        context: Context,
1155        mut archive: impl MultiArchive<Key = FixedBytes<64>, Value = i32>,
1156    ) {
1157        // Mix Archive::put (single-item) and MultiArchive::put_multi
1158        archive = archive.put(1, test_key("single"), &100).await.unwrap();
1159        archive = archive
1160            .put_multi(2, test_key("multi-a"), &200)
1161            .await
1162            .unwrap();
1163        archive = archive
1164            .put_multi(2, test_key("multi-b"), &201)
1165            .await
1166            .unwrap();
1167        archive = archive
1168            .put_multi(3, test_key("multi-c"), &300)
1169            .await
1170            .unwrap();
1171
1172        // All retrievable by key
1173        assert_eq!(
1174            archive
1175                .get(Identifier::Key(&test_key("single")))
1176                .await
1177                .unwrap(),
1178            Some(100)
1179        );
1180        assert_eq!(
1181            archive
1182                .get(Identifier::Key(&test_key("multi-a")))
1183                .await
1184                .unwrap(),
1185            Some(200)
1186        );
1187        assert_eq!(
1188            archive
1189                .get(Identifier::Key(&test_key("multi-b")))
1190                .await
1191                .unwrap(),
1192            Some(201)
1193        );
1194        assert_eq!(
1195            archive
1196                .get(Identifier::Key(&test_key("multi-c")))
1197                .await
1198                .unwrap(),
1199            Some(300)
1200        );
1201
1202        // Archive::get(Index) returns first item at that index
1203        assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(200));
1204
1205        // Gap tracking works across mixed usage
1206        let (end, next) = archive.next_gap(1);
1207        assert_eq!(end, Some(3));
1208        assert!(next.is_none());
1209
1210        let buffer = context.encode();
1211        assert!(has_metric_value(&buffer, "items_tracked", 3));
1212    }
1213
1214    #[test_traced]
1215    fn test_put_multi_mixed_indices_prunable() {
1216        let executor = deterministic::Runner::default();
1217        executor.start(|context| async move {
1218            let archive = create_prunable(context.child("storage"), None).await;
1219            test_put_multi_mixed_indices_impl(context, archive).await;
1220        });
1221    }
1222
1223    fn assert_send<T: Send>(_: T) {}
1224
1225    // Mutators consume the archive, so each consuming future is constructed in
1226    // its own match arm (only one arm ever runs, but all are type-checked). Every arm
1227    // but the last needs its own key, so clippy's per-path analysis flags
1228    // the clones as redundant.
1229    #[allow(dead_code)]
1230    fn assert_archive_futures_are_send<T: super::Archive>(
1231        archive: T,
1232        key: T::Key,
1233        value: T::Value,
1234    ) {
1235        assert_send(archive.get(Identifier::Index(1)));
1236        assert_send(archive.get(Identifier::Key(&key)));
1237        assert_send(archive.has(Identifier::Index(1)));
1238        assert_send(archive.has(Identifier::Key(&key)));
1239        #[allow(clippy::redundant_clone)]
1240        match 0u8 {
1241            0 => assert_send(archive.put(1, key.clone(), &value)),
1242            1 => assert_send(archive.put_sync(2, key.clone(), &value)),
1243            2 => assert_send(archive.put_start_sync(3, key, &value)),
1244            3 => assert_send(archive.sync()),
1245            4 => assert_send(archive.start_sync()),
1246            _ => assert_send(archive.destroy()),
1247        }
1248    }
1249
1250    #[allow(dead_code)]
1251    fn assert_multi_archive_futures_are_send<T: super::MultiArchive>(
1252        archive: T,
1253        key: T::Key,
1254        value: T::Value,
1255    ) {
1256        assert_send(archive.get_all(1));
1257        #[allow(clippy::redundant_clone)]
1258        match 0u8 {
1259            0 => assert_send(archive.put_multi(1, key.clone(), &value)),
1260            1 => assert_send(archive.put_multi_sync(2, key.clone(), &value)),
1261            2 => assert_send(archive.put_multi_start_sync(3, key.clone(), &value)),
1262            _ => assert_archive_futures_are_send(archive, key, value),
1263        }
1264    }
1265
1266    #[allow(dead_code)]
1267    fn assert_prunable_archive_futures_are_send(
1268        archive: prunable::Archive<TwoCap, Context, FixedBytes<64>, i32>,
1269        key: FixedBytes<64>,
1270        value: i32,
1271    ) {
1272        assert_archive_futures_are_send(archive, key, value);
1273    }
1274
1275    #[allow(dead_code)]
1276    fn assert_prunable_multi_archive_futures_are_send(
1277        archive: prunable::Archive<TwoCap, Context, FixedBytes<64>, i32>,
1278        key: FixedBytes<64>,
1279        value: i32,
1280    ) {
1281        assert_multi_archive_futures_are_send(archive, key, value);
1282    }
1283
1284    #[allow(dead_code)]
1285    fn assert_immutable_archive_futures_are_send(
1286        archive: immutable::Archive<Context, FixedBytes<64>, i32>,
1287        key: FixedBytes<64>,
1288        value: i32,
1289    ) {
1290        assert_archive_futures_are_send(archive, key, value);
1291    }
1292}