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