Skip to main content

commonware_storage/archive/prunable/
mod.rs

1//! A prunable key-value store for ordered data.
2//!
3//! Data is stored across two backends, [crate::journal::segmented::fixed] for fixed-size index
4//! entries and [crate::journal::segmented::glob::Glob] for values (managed by
5//! [crate::journal::segmented::oversized]). The location of written data is stored in-memory by
6//! both index and key (see [Indexing](#indexing)) to enable efficient lookups (on average).
7//!
8//! _Notably, [Archive] does not make use of compaction nor on-disk indexes (and thus has no read
9//! nor write amplification during normal operation).
10//!
11//! # Format
12//!
13//! [Archive] uses a two-journal structure for efficient page cache usage:
14//!
15//! **Index Journal (segmented/fixed)** - Fixed-size entries for fast startup replay:
16//! ```text
17//! +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
18//! | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 |12 |13 |14 |15 |16 |17 |18 |19 |20 |21 |22 |23 |
19//! +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
20//! |          Index(u64)           |Key(Fixed Size)|        val_offset(u64)        | val_size(u32) |
21//! +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
22//! ```
23//!
24//! **Value Blob** - Raw values with CRC32 checksums (direct reads, no page cache):
25//! ```text
26//! +---+---+---+---+---+---+---+---+---+---+---+---+
27//! |     Compressed Data (variable)    |   CRC32   |
28//! +---+---+---+---+---+---+---+---+---+---+---+---+
29//! ```
30//!
31//! # Uniqueness
32//!
33//! Indices are unique for [Archive] and writing to an occupied index is a no-op. Duplicate
34//! indices can be stored via [`crate::archive::MultiArchive::put_multi`].
35//!
36//! Keys may be stored at multiple indices with either put variant. A lookup by
37//! [`crate::archive::Identifier::Key`] may return any of the values at that key. Entries
38//! whose index has been pruned are never returned or reported as present, so a key matching
39//! both a pruned and a non-pruned entry resolves to the non-pruned entry.
40//!
41//! ## Indexing
42//!
43//! Each retained `section` (selected by `section = index / items_per_section * items_per_section`
44//! to minimize the number of open blobs) holds the translated key of every record it stores, by
45//! journal position, and the first journal position of every index it stores, in a vector by
46//! offset from the section's start. The vector spans every offset up to the highest one stored,
47//! so a section costs `8` bytes per index from its start to its highest stored index whether or
48//! not the indices between are stored. Contiguous records cost `8` bytes each, while records
49//! spread sparsely across a large `items_per_section` cost up to `8 * items_per_section` bytes
50//! per section.
51//!
52//! Lookups by key go through a [commonware_utils::handles::Table] from the hash of a translated
53//! key to a handle naming the record's section and position. The table stores no keys, so a lookup
54//! resolves each candidate handle to the translated key its section holds and reads the index
55//! journal only for records whose translated key matches.
56//!
57//! ## Conflicts
58//!
59//! Because a translated representation of a key is only ever stored in memory, it is possible (and
60//! expected) that two keys will eventually be represented by the same translated key. To handle
61//! this case, [Archive] must check the persisted form of all conflicting keys to ensure data from
62//! the correct key is returned. Every record whose translated key matches is a candidate, and its
63//! stored key is compared before a value is read. Records that share a translated key, whether
64//! through conflicts or one key stored at many indices, occupy one run of the key table, so
65//! storing, pruning, and looking up such records each cost time linear in their number, and storing
66//! or pruning `k` copies of one key costs `O(k^2)` in total.
67//!
68//! _If the [Translator] provided by the caller does not uniformly distribute keys across the key
69//! space or uses a translated representation that means keys on average have many conflicts,
70//! performance will degrade._
71//!
72//! ## Memory Overhead
73//!
74//! Per retained record, [Archive] keeps `translated(key).len()` bytes and `8` bytes of position
75//! in its section (see [Indexing](#indexing) for sparse sections) and one key-table entry of
76//! `12` bytes per slot, where the table holds between three eighths and three quarters of a
77//! power-of-two number of slots. With a [Translator] that uses the first `8` bytes of a key,
78//! this is between `~32` and `~48` bytes per record depending on how full the key table is. The
79//! table never shrinks, so after a large prune its allocation reflects the most records retained
80//! at once rather than the current count.
81//!
82//! ### MultiArchive Overhead
83//!
84//! Additional positions for indices written via [crate::archive::MultiArchive::put_multi] are
85//! tracked in `extra_indices: BTreeMap<u64, Vec<u64>>`, so the overhead above is unchanged for
86//! the first item at an index. For indices with duplicates, the additional in-memory payload is:
87//! - one `Vec<u64>` header (`24` bytes), and
88//! - `n * 8` bytes for `n` additional positions.
89//!
90//! Equivalently, this is `24 + (n * 8)` bytes per duplicated index, excluding `BTreeMap` node
91//! overhead for `extra_indices`.
92//!
93//! # Pruning
94//!
95//! [Archive] supports pruning up to a minimum `index` using the `prune` method. After `prune` is
96//! called on a `section`, entries below the pruned `section` are gone: `get` returns `None`,
97//! and a `put` below the floor is satisfied without storing. The floor is not persisted. After a
98//! restart, a `put` below a previously pruned `section` is stored and readable until a later
99//! `prune` covers it.
100//!
101//! ## Index Cleanup
102//!
103//! Pruning a `section` drops its in-memory state and removes its records' handles from the key
104//! table by the translated keys the section holds, so the index journal is never read and the
105//! table is never scanned. Removal shifts the following entries back, leaving no tombstones, so
106//! pruning never forces a rebuild and the table's allocation reflects the most records retained
107//! at once.
108//!
109//! # Read Path
110//!
111//! All reads (by index or key) first read the index entry from the index journal to get the
112//! value location (offset and size), then read the value from the value blob. The index journal
113//! uses a page cache for caching, so hot entries are served from memory. Values are read directly
114//! from disk without caching to avoid polluting the page cache with large values.
115//!
116//! # Compression
117//!
118//! [Archive] supports optional zstd compression through [Config::compression]. Keep the choice
119//! between `None` and `Some(_)` fixed while stored values are retained. Only the compression level
120//! may change between initializations when compression is enabled.
121//!
122//! # Querying for Gaps
123//!
124//! [Archive] tracks gaps in the index space to enable the caller to efficiently fetch unknown keys
125//! using `next_gap`. This is a very common pattern when syncing blocks in a blockchain.
126//!
127//! # Example
128//!
129//! ```rust
130//! use commonware_runtime::{Spawner, Runner, deterministic, buffer::paged::CacheRef};
131//! use commonware_cryptography::{Hasher as _, Sha256};
132//! use commonware_storage::{
133//!     translator::FourCap,
134//!     archive::{
135//!         Archive as _,
136//!         prunable::{Archive, Config},
137//!     },
138//! };
139//! use commonware_utils::{NZUsize, NZU16, NZU64};
140//!
141//! let executor = deterministic::Runner::default();
142//! executor.start(|context| async move {
143//!     // Create an archive
144//!     let cfg = Config {
145//!         translator: FourCap,
146//!         metadata_partition: "demo-metadata".into(),
147//!         key_partition: "demo-index".into(),
148//!         key_page_cache: CacheRef::new(NZU16!(1024), NZUsize!(10)),
149//!         value_partition: "demo-value".into(),
150//!         compression: Some(3),
151//!         codec_config: (),
152//!         items_per_section: NZU64!(1024),
153//!         key_write_buffer: NZUsize!(1024 * 1024),
154//!         value_write_buffer: NZUsize!(1024 * 1024),
155//!         replay_buffer: NZUsize!(4096),
156//!     };
157//!     let mut archive = Archive::init(context, cfg).await.unwrap();
158//!
159//!     // Put a key
160//!     archive = archive.put(1, Sha256::hash(&[b"data"]), &10).await.unwrap();
161//!
162//!     // Sync the archive
163//!     archive.sync().await.unwrap();
164//! });
165//! ```
166
167use crate::translator::Translator;
168use commonware_runtime::buffer::paged::CacheRef;
169use std::num::{NonZeroU64, NonZeroUsize};
170
171mod storage;
172pub use storage::Archive;
173
174/// Configuration for [Archive] storage.
175#[derive(Clone)]
176pub struct Config<T: Translator, C> {
177    /// Logic to transform keys into their index representation.
178    ///
179    /// [Archive] assumes that all internal keys are spread uniformly across the key space.
180    /// If that is not the case, lookups may be O(n) instead of O(1).
181    pub translator: T,
182
183    /// The partition to use for per-section validation markers. Recovery adopts entries
184    /// below a section's marker without re-validating their values.
185    pub metadata_partition: String,
186
187    /// The partition to use for the key journal (stores index+key metadata).
188    pub key_partition: String,
189
190    /// The page cache to use for the key journal.
191    pub key_page_cache: CacheRef,
192
193    /// The partition to use for the value blob (stores values).
194    pub value_partition: String,
195
196    /// The compression level to use for the value blob.
197    pub compression: Option<u8>,
198
199    /// The [commonware_codec::Codec] configuration to use for the value stored in the archive.
200    pub codec_config: C,
201
202    /// The number of items per section (the granularity of pruning).
203    ///
204    /// Fixed for the life of the archive's data. Records live in the section their index
205    /// selects, and [Archive::init] fails with [crate::archive::Error::RecordCorrupted] when a
206    /// stored record's section no longer matches. Each retained section tracks its indices in a
207    /// vector spanning every offset up to its highest stored index (`8` bytes each), so sparse
208    /// indices in a large section cost up to `8 * items_per_section` bytes (see
209    /// [Indexing](self#indexing)).
210    pub items_per_section: NonZeroU64,
211
212    /// The amount of bytes that can be buffered for the key journal before being written to a
213    /// [commonware_runtime::Blob].
214    pub key_write_buffer: NonZeroUsize,
215
216    /// The amount of bytes that can be buffered for the value journal before being written to a
217    /// [commonware_runtime::Blob].
218    pub value_write_buffer: NonZeroUsize,
219
220    /// The buffer size to use when replaying a [commonware_runtime::Blob].
221    pub replay_buffer: NonZeroUsize,
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use crate::{
228        archive::{Archive as _, Error, Identifier, MultiArchive as _},
229        journal::{Error as JournalError, segmented::glob::corrupt_frame},
230        translator::{EightCap, FourCap, TwoCap},
231    };
232    use commonware_codec::{Error as CodecError, FixedSize};
233    use commonware_cryptography::Crc32;
234    use commonware_macros::{test_group, test_traced};
235    use commonware_runtime::{
236        Blob as _, Error as RError, Metrics as _, ReadOptions, Runner, Spawner as _, Storage as _,
237        Supervisor as _, WriteOptions, deterministic,
238        mocks::{
239            DelayedSyncContext, PendingSyncs, drive_pending_syncs, fail_pending_syncs,
240            release_next_pending_syncs, release_pending_syncs,
241        },
242        telemetry::metrics::has_metric_value,
243    };
244    use commonware_utils::{NZU16, NZU64, NZUsize, sequence::FixedBytes};
245    use rand::RngExt as _;
246    use std::{
247        collections::BTreeMap,
248        num::{NonZeroU16, NonZeroU64},
249        sync::{
250            Arc,
251            atomic::{AtomicUsize, Ordering},
252        },
253    };
254
255    fn test_key(key: &str) -> FixedBytes<64> {
256        let mut buf = [0u8; 64];
257        let key = key.as_bytes();
258        assert!(key.len() <= buf.len());
259        buf[..key.len()].copy_from_slice(key);
260        FixedBytes::new(buf)
261    }
262
263    const DEFAULT_ITEMS_PER_SECTION: u64 = 65536;
264    const DEFAULT_WRITE_BUFFER: usize = 1024;
265    const DEFAULT_REPLAY_BUFFER: usize = 4096;
266    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
267    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
268
269    fn test_config<T: Translator>(
270        partition_prefix: &str,
271        items_per_section: NonZeroU64,
272        translator: T,
273    ) -> Config<T, ()> {
274        Config {
275            translator,
276            metadata_partition: format!("{partition_prefix}-metadata"),
277            key_partition: format!("{partition_prefix}-index"),
278            key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
279            value_partition: format!("{partition_prefix}-value"),
280            codec_config: (),
281            compression: None,
282            key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
283            value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
284            replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
285            items_per_section,
286        }
287    }
288
289    /// Physical size of one uncompressed i32 value frame in the value journal, per the
290    /// glob's frame layout (encoded value followed by its CRC32).
291    const I32_VALUE_FRAME_SIZE: u64 =
292        (i32::SIZE + crate::journal::segmented::glob::CHECKSUM_SIZE) as u64;
293
294    #[test_traced]
295    fn test_put_after_start_sync_is_accepted_before_handle_completes() {
296        let executor = deterministic::Runner::default();
297        let (_, checkpoint) = executor.start_and_recover(|context| async move {
298            let pending = PendingSyncs::default();
299            let context = DelayedSyncContext {
300                inner: context,
301                pending: pending.clone(),
302            };
303            let cfg = test_config("test", NZU64!(DEFAULT_ITEMS_PER_SECTION), FourCap);
304            let archive = Archive::init(context.child("storage"), cfg)
305                .await
306                .expect("Failed to initialize archive");
307
308            let (mut archive, handle) = archive
309                .put_start_sync(1, test_key("aaa"), &10)
310                .await
311                .expect("Failed to start sync");
312            let pending_after_start = pending.lock().len();
313            assert!(
314                pending_after_start > 0,
315                "put_start_sync should return while the sync handle is still pending"
316            );
317
318            archive = archive
319                .put(2, test_key("bbb"), &20)
320                .await
321                .expect("archive should remain usable before sync completion");
322            assert_eq!(
323                pending.lock().len(),
324                pending_after_start,
325                "put should not issue a new storage sync while accepting later data"
326            );
327            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
328
329            release_pending_syncs(&pending);
330            handle.await.expect("sync handle should complete");
331
332            let (_archive, follow_up) = archive
333                .start_sync()
334                .await
335                .expect("Failed to start next sync");
336            assert!(
337                !pending.lock().is_empty(),
338                "the later put must remain pending for a future sync"
339            );
340            release_pending_syncs(&pending);
341            follow_up.await.expect("follow-up sync should complete");
342        });
343
344        deterministic::Runner::from(checkpoint).start(|context| async move {
345            let cfg = test_config("test", NZU64!(DEFAULT_ITEMS_PER_SECTION), FourCap);
346            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
347                .await
348                .expect("Failed to reopen archive");
349
350            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
351            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
352        });
353    }
354
355    #[test_traced]
356    fn test_duplicate_put_start_sync_observes_in_flight_sync() {
357        let executor = deterministic::Runner::default();
358        executor.start(|context| async move {
359            let pending = PendingSyncs::default();
360            let context = DelayedSyncContext {
361                inner: context,
362                pending: pending.clone(),
363            };
364            let cfg = test_config("test", NZU64!(DEFAULT_ITEMS_PER_SECTION), FourCap);
365            let archive = Archive::init(context.child("storage"), cfg)
366                .await
367                .expect("Failed to initialize archive");
368
369            let (archive, first) = archive
370                .put_start_sync(1, test_key("aaa"), &10)
371                .await
372                .expect("Failed to start sync");
373            assert_eq!(pending.lock().len(), 2);
374
375            let (archive, second) = archive
376                .put_start_sync(1, test_key("duplicate"), &99)
377                .await
378                .expect("Failed to start duplicate sync");
379            assert_eq!(
380                pending.lock().len(),
381                2,
382                "duplicate put_start_sync must not issue a new storage sync"
383            );
384
385            let started = Arc::new(AtomicUsize::new(0));
386            let completed = Arc::new(AtomicUsize::new(0));
387            let started_clone = started.clone();
388            let completed_clone = completed.clone();
389            let waiter = context.inner.child("duplicate").spawn(|_| async move {
390                started_clone.fetch_add(1, Ordering::Relaxed);
391                second.await.expect("duplicate sync handle should complete");
392                completed_clone.fetch_add(1, Ordering::Relaxed);
393            });
394
395            while started.load(Ordering::Relaxed) == 0 {
396                commonware_runtime::reschedule().await;
397            }
398            commonware_runtime::reschedule().await;
399            assert_eq!(
400                completed.load(Ordering::Relaxed),
401                0,
402                "duplicate put_start_sync must observe the original in-flight sync"
403            );
404
405            release_pending_syncs(&pending);
406            first.await.expect("first sync handle should complete");
407            while completed.load(Ordering::Relaxed) == 0 {
408                commonware_runtime::reschedule().await;
409            }
410            waiter.await.expect("duplicate waiter failed");
411
412            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
413        });
414    }
415
416    #[test_traced]
417    fn test_below_floor_put_start_sync_covers_prior_pending_write() {
418        let executor = deterministic::Runner::default();
419        executor.start(|context| async move {
420            let pending = PendingSyncs::default();
421            let context = DelayedSyncContext {
422                inner: context,
423                pending: pending.clone(),
424            };
425            let cfg = test_config("test", NZU64!(1), FourCap);
426            let archive = Archive::init(context.child("storage"), cfg)
427                .await
428                .expect("Failed to initialize archive");
429
430            // Raise the prune floor above index 0, then buffer an unsynced write to retained
431            // section 2.
432            let archive = archive.prune(1).await.expect("Failed to set prune floor");
433            let archive = archive
434                .put(2, test_key("pending"), &20)
435                .await
436                .expect("Failed to buffer retained write");
437
438            // The below-floor put stores nothing, yet the returned handle must cover every
439            // previously accepted write. The two parked operations are section 2's index and
440            // value syncs.
441            assert!(pending.lock().is_empty());
442            let (archive, handle) = archive
443                .put_start_sync(0, test_key("pruned"), &0)
444                .await
445                .expect("Failed to request sync through below-floor put");
446            assert_eq!(
447                pending.lock().len(),
448                2,
449                "the sync combinator must cover writes accepted before its below-floor put"
450            );
451
452            // Releasing the parked syncs completes the handle, proving the covered write
453            // durable while the below-floor index stays absent.
454            release_pending_syncs(&pending);
455            handle.await.expect("covering sync should complete");
456            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
457            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), None);
458        });
459    }
460
461    #[test_traced]
462    fn test_below_floor_put_multi_sync_covers_prior_pending_write() {
463        let executor = deterministic::Runner::default();
464        executor.start(|context| async move {
465            let pending = PendingSyncs::default();
466            let context = DelayedSyncContext {
467                inner: context,
468                pending: pending.clone(),
469            };
470            let cfg = test_config("test", NZU64!(1), FourCap);
471            let archive = Archive::init(context.child("storage"), cfg)
472                .await
473                .expect("Failed to initialize archive");
474
475            // Raise the prune floor above index 0, then buffer an unsynced write to retained
476            // section 2.
477            let archive = archive.prune(1).await.expect("Failed to set prune floor");
478            let archive = archive
479                .put_multi(2, test_key("pending"), &20)
480                .await
481                .expect("Failed to buffer retained write");
482
483            // Run the blocking combinator in a spawned task so the test can observe whether it
484            // returns while the covering sync is still parked.
485            pending.arm();
486            let completed = Arc::new(AtomicUsize::new(0));
487            let completed_clone = completed.clone();
488            let task = context.inner.child("put_multi_sync").spawn(|_| async move {
489                let result = archive.put_multi_sync(0, test_key("pruned"), &0).await;
490                completed_clone.store(1, Ordering::Relaxed);
491                result
492            });
493            while pending.calls() == 0 && completed.load(Ordering::Relaxed) == 0 {
494                commonware_runtime::reschedule().await;
495            }
496
497            // The below-floor put stores nothing, yet the blocking call must not return before
498            // the previously buffered write is durable.
499            assert_eq!(
500                completed.load(Ordering::Relaxed),
501                0,
502                "put_multi_sync must wait for writes accepted before its below-floor put"
503            );
504            assert!(pending.calls() > 0);
505            release_pending_syncs(&pending);
506            let archive = task
507                .await
508                .expect("put_multi_sync task failed")
509                .expect("put_multi_sync failed");
510
511            // The covered write survives while the below-floor index stays absent.
512            assert_eq!(archive.get_all(2).await.unwrap(), Some(vec![20]));
513            assert_eq!(archive.get_all(0).await.unwrap(), None);
514        });
515    }
516
517    #[test_traced]
518    fn test_overlapping_put_start_sync_waits_for_in_flight_sync() {
519        let executor = deterministic::Runner::default();
520        executor.start(|context| async move {
521            let pending = PendingSyncs::default();
522            let context = DelayedSyncContext {
523                inner: context,
524                pending: pending.clone(),
525            };
526            let cfg = test_config("test", NZU64!(DEFAULT_ITEMS_PER_SECTION), FourCap);
527            let archive = Archive::init(context.child("storage"), cfg)
528                .await
529                .expect("Failed to initialize archive");
530
531            let (archive, first) = archive
532                .put_start_sync(1, test_key("aaa"), &10)
533                .await
534                .expect("Failed to start sync");
535            let pending_after_first = pending.lock().len();
536            assert!(pending_after_first > 0);
537
538            let started = Arc::new(AtomicUsize::new(0));
539            let completed = Arc::new(AtomicUsize::new(0));
540            let started_clone = started.clone();
541            let completed_clone = completed.clone();
542            let waiter = context.inner.child("second").spawn(|_| async move {
543                started_clone.fetch_add(1, Ordering::Relaxed);
544                let (archive, second) = archive
545                    .put_start_sync(2, test_key("bbb"), &20)
546                    .await
547                    .expect("Failed to start second sync");
548                completed_clone.fetch_add(1, Ordering::Relaxed);
549                (archive, second)
550            });
551
552            while started.load(Ordering::Relaxed) == 0 {
553                commonware_runtime::reschedule().await;
554            }
555            commonware_runtime::reschedule().await;
556            assert_eq!(completed.load(Ordering::Relaxed), 0);
557            assert_eq!(
558                pending.lock().len(),
559                pending_after_first,
560                "second put_start_sync must not start new syncs before the first completes"
561            );
562
563            release_pending_syncs(&pending);
564            first.await.expect("first sync handle should complete");
565            while completed.load(Ordering::Relaxed) == 0 {
566                commonware_runtime::reschedule().await;
567            }
568            let (archive, second) = waiter.await.expect("second put task failed");
569            assert!(!pending.lock().is_empty());
570            release_pending_syncs(&pending);
571            second.await.expect("second sync handle should complete");
572
573            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
574            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
575        });
576    }
577
578    #[test_traced]
579    fn test_sync_after_put_start_sync_waits_for_in_flight_sync() {
580        let executor = deterministic::Runner::default();
581        executor.start(|context| async move {
582            let pending = PendingSyncs::default();
583            let context = DelayedSyncContext {
584                inner: context,
585                pending: pending.clone(),
586            };
587            let cfg = test_config("test", NZU64!(DEFAULT_ITEMS_PER_SECTION), FourCap);
588            let archive = Archive::init(context.child("storage"), cfg)
589                .await
590                .expect("Failed to initialize archive");
591
592            let (archive, first) = archive
593                .put_start_sync(1, test_key("aaa"), &10)
594                .await
595                .expect("Failed to start sync");
596            assert!(!pending.lock().is_empty());
597
598            let started = Arc::new(AtomicUsize::new(0));
599            let completed = Arc::new(AtomicUsize::new(0));
600            let started_clone = started.clone();
601            let completed_clone = completed.clone();
602            let waiter = context.inner.child("sync").spawn(|_| async move {
603                started_clone.fetch_add(1, Ordering::Relaxed);
604                let archive = archive.sync().await.expect("sync should complete");
605                completed_clone.fetch_add(1, Ordering::Relaxed);
606                archive
607            });
608
609            while started.load(Ordering::Relaxed) == 0 {
610                commonware_runtime::reschedule().await;
611            }
612            commonware_runtime::reschedule().await;
613            assert_eq!(
614                completed.load(Ordering::Relaxed),
615                0,
616                "shutdown sync must wait for the in-flight put_start_sync handle"
617            );
618
619            release_pending_syncs(&pending);
620            first.await.expect("first sync handle should complete");
621            while completed.load(Ordering::Relaxed) == 0 {
622                commonware_runtime::reschedule().await;
623            }
624            let archive = waiter.await.expect("sync task failed");
625            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
626        });
627    }
628
629    #[test_traced]
630    fn test_destroy_after_put_start_sync_waits_for_in_flight_sync() {
631        let executor = deterministic::Runner::default();
632        executor.start(|context| async move {
633            let pending = PendingSyncs::default();
634            let context = DelayedSyncContext {
635                inner: context,
636                pending: pending.clone(),
637            };
638            let cfg = test_config("test", NZU64!(DEFAULT_ITEMS_PER_SECTION), FourCap);
639            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
640                .await
641                .expect("Failed to initialize archive");
642
643            let (archive, first) = archive
644                .put_start_sync(1, test_key("aaa"), &10)
645                .await
646                .expect("Failed to start sync");
647            assert!(!pending.lock().is_empty());
648
649            let started = Arc::new(AtomicUsize::new(0));
650            let completed = Arc::new(AtomicUsize::new(0));
651            let started_clone = started.clone();
652            let completed_clone = completed.clone();
653            let waiter = context.inner.child("destroy").spawn(|_| async move {
654                started_clone.fetch_add(1, Ordering::Relaxed);
655                archive.destroy().await.expect("destroy should complete");
656                completed_clone.fetch_add(1, Ordering::Relaxed);
657            });
658
659            while started.load(Ordering::Relaxed) == 0 {
660                commonware_runtime::reschedule().await;
661            }
662            commonware_runtime::reschedule().await;
663            assert_eq!(
664                completed.load(Ordering::Relaxed),
665                0,
666                "destroy must wait for the in-flight put_start_sync handle"
667            );
668
669            release_pending_syncs(&pending);
670            first.await.expect("first sync handle should complete");
671            while completed.load(Ordering::Relaxed) == 0 {
672                commonware_runtime::reschedule().await;
673            }
674            waiter.await.expect("destroy task failed");
675        });
676    }
677
678    #[test_traced]
679    fn test_prune_after_put_start_sync_waits_for_in_flight_sync() {
680        let executor = deterministic::Runner::default();
681        executor.start(|context| async move {
682            let pending = PendingSyncs::default();
683            let context = DelayedSyncContext {
684                inner: context,
685                pending: pending.clone(),
686            };
687            let cfg = test_config("test", NZU64!(1), FourCap);
688            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
689                .await
690                .expect("Failed to initialize archive");
691
692            let (archive, first) = archive
693                .put_start_sync(1, test_key("aaa"), &10)
694                .await
695                .expect("Failed to start sync");
696            assert!(!pending.lock().is_empty());
697
698            let started = Arc::new(AtomicUsize::new(0));
699            let completed = Arc::new(AtomicUsize::new(0));
700            let started_clone = started.clone();
701            let completed_clone = completed.clone();
702            let waiter = context.inner.child("prune").spawn(|_| async move {
703                started_clone.fetch_add(1, Ordering::Relaxed);
704                let archive = archive.prune(2).await.expect("prune should complete");
705                completed_clone.fetch_add(1, Ordering::Relaxed);
706                archive
707            });
708
709            while started.load(Ordering::Relaxed) == 0 {
710                commonware_runtime::reschedule().await;
711            }
712            commonware_runtime::reschedule().await;
713            assert_eq!(
714                completed.load(Ordering::Relaxed),
715                0,
716                "prune must wait for in-flight syncs on pruned sections"
717            );
718
719            release_pending_syncs(&pending);
720            first
721                .await
722                .expect("sync handle should complete despite pruning");
723            while completed.load(Ordering::Relaxed) == 0 {
724                commonware_runtime::reschedule().await;
725            }
726            let archive = waiter.await.expect("prune task failed");
727            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
728        });
729    }
730
731    #[test_traced]
732    fn test_prune_surfaces_failed_in_flight_sync() {
733        let executor = deterministic::Runner::default();
734        executor.start(|context| async move {
735            let pending = PendingSyncs::default();
736            let context = DelayedSyncContext {
737                inner: context,
738                pending: pending.clone(),
739            };
740            let cfg = test_config("test", NZU64!(1), FourCap);
741            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
742                .await
743                .expect("Failed to initialize archive");
744
745            let (archive, first) = archive
746                .put_start_sync(1, test_key("aaa"), &10)
747                .await
748                .expect("Failed to start sync");
749            fail_pending_syncs(&pending);
750
751            let err = archive
752                .prune(2)
753                .await
754                .expect_err("prune must surface a failed in-flight sync");
755            assert!(matches!(
756                err,
757                Error::Journal(JournalError::Runtime(RError::Io(_)))
758            ));
759
760            let err = first.await.expect_err("first sync handle should fail");
761            assert!(matches!(err, RError::Io(_)));
762        });
763    }
764
765    #[test_traced]
766    fn test_put_start_sync_after_prune_drops_pruned_sync_requests() {
767        let executor = deterministic::Runner::default();
768        executor.start(|context| async move {
769            let pending = PendingSyncs::default();
770            let context = DelayedSyncContext {
771                inner: context,
772                pending: pending.clone(),
773            };
774            let cfg = test_config("test", NZU64!(1), FourCap);
775            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
776                .await
777                .expect("Failed to initialize archive");
778
779            let (archive, first) = archive
780                .put_start_sync(1, test_key("aaa"), &10)
781                .await
782                .expect("Failed to start sync");
783            release_pending_syncs(&pending);
784            first.await.expect("first sync handle should complete");
785
786            let archive = archive.prune(2).await.expect("Failed to prune");
787
788            // If pruning left section 1 in the retained sync-request set, these calls would trip
789            // the journal's prune guard.
790            let (archive, second) = archive
791                .put_start_sync(2, test_key("bbb"), &20)
792                .await
793                .expect("put_start_sync after prune should succeed");
794            release_pending_syncs(&pending);
795            second.await.expect("second sync handle should complete");
796            let archive = drive_pending_syncs(&pending, archive.sync())
797                .await
798                .expect("sync after prune should succeed");
799
800            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
801            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
802        });
803    }
804
805    #[test_traced]
806    fn test_overlapping_put_start_sync_restarts_after_all_handles_complete() {
807        let executor = deterministic::Runner::default();
808        let (_, checkpoint) = executor.start_and_recover(|context| async move {
809            let pending = PendingSyncs::default();
810            let context = DelayedSyncContext {
811                inner: context,
812                pending: pending.clone(),
813            };
814            let cfg = test_config("test", NZU64!(1), FourCap);
815            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
816                .await
817                .expect("Failed to initialize archive");
818
819            let (archive, first) = archive
820                .put_start_sync(1, test_key("aaa"), &10)
821                .await
822                .expect("Failed to start first sync");
823            assert_eq!(pending.lock().len(), 2);
824
825            let (_archive, second) = archive
826                .put_start_sync(2, test_key("bbb"), &20)
827                .await
828                .expect("Failed to start second sync");
829            assert_eq!(
830                pending.lock().len(),
831                4,
832                "different sections should be able to have independent in-flight syncs"
833            );
834
835            release_pending_syncs(&pending);
836            first.await.expect("first sync handle should complete");
837            second.await.expect("second sync handle should complete");
838        });
839
840        deterministic::Runner::from(checkpoint).start(|context| async move {
841            let cfg = test_config("test", NZU64!(1), FourCap);
842            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
843                .await
844                .expect("Failed to reopen archive");
845
846            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
847            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(20));
848        });
849    }
850
851    #[test_traced]
852    fn test_overlapping_put_start_sync_restarts_only_completed_handles() {
853        let executor = deterministic::Runner::default();
854        let (_, checkpoint) = executor.start_and_recover(|context| async move {
855            let pending = PendingSyncs::default();
856            let context = DelayedSyncContext {
857                inner: context,
858                pending: pending.clone(),
859            };
860            let cfg = test_config("test", NZU64!(1), FourCap);
861            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
862                .await
863                .expect("Failed to initialize archive");
864
865            let (archive, first) = archive
866                .put_start_sync(1, test_key("aaa"), &10)
867                .await
868                .expect("Failed to start first sync");
869            let (archive, second) = archive
870                .put_start_sync(2, test_key("bbb"), &20)
871                .await
872                .expect("Failed to start second sync");
873            assert_eq!(pending.lock().len(), 4);
874
875            release_next_pending_syncs(&pending, 2);
876            first.await.expect("first sync handle should complete");
877
878            drop(second);
879            drop(archive);
880        });
881
882        deterministic::Runner::from(checkpoint).start(|context| async move {
883            let cfg = test_config("test", NZU64!(1), FourCap);
884            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
885                .await
886                .expect("Failed to reopen archive");
887
888            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(10));
889            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), None);
890        });
891    }
892
893    #[test_traced]
894    fn test_failed_start_sync_is_returned_by_next_start_sync() {
895        let executor = deterministic::Runner::default();
896        executor.start(|context| async move {
897            let pending = PendingSyncs::default();
898            let context = DelayedSyncContext {
899                inner: context,
900                pending: pending.clone(),
901            };
902            let cfg = test_config("test", NZU64!(DEFAULT_ITEMS_PER_SECTION), FourCap);
903            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("storage"), cfg)
904                .await
905                .expect("Failed to initialize archive");
906
907            let (archive, first) = archive
908                .put_start_sync(1, test_key("aaa"), &10)
909                .await
910                .expect("Failed to start sync");
911            assert_eq!(pending.lock().len(), 2);
912            fail_pending_syncs(&pending);
913
914            let archive = archive
915                .put(2, test_key("bbb"), &20)
916                .await
917                .expect("write should be accepted before observing the failed sync");
918
919            // The next start_sync flushes the buffered write, so it first observes the failed
920            // in-flight sync.
921            let Err(err) = archive.start_sync().await else {
922                panic!("next start_sync should return the failed in-flight sync");
923            };
924            assert!(matches!(
925                err,
926                Error::Journal(JournalError::Runtime(RError::Io(_)))
927            ));
928
929            let err = first.await.expect_err("first sync handle should fail");
930            assert!(matches!(err, RError::Io(_)));
931        });
932    }
933
934    #[test_traced]
935    fn test_archive_truncates_at_first_invalid_value() {
936        deterministic::Runner::default().start(|context| async move {
937            for (name, bad_position, retained) in [("first", 0, 0), ("middle", 1, 1)] {
938                let cfg = test_config(&format!("invalid-{name}"), NZU64!(4), FourCap);
939
940                // Seed three values in one section. The sync leaves them durable with no marker
941                // (the active section's boundary stays debt), so reopen must CRC-validate every
942                // frame from position 0.
943                let mut archive = Archive::init(context.child(name), cfg.clone())
944                    .await
945                    .unwrap();
946                for (index, value) in [10, 20, 30].into_iter().enumerate() {
947                    archive = archive
948                        .put(index as u64, test_key(&format!("key-{index}")), &value)
949                        .await
950                        .unwrap();
951                }
952                archive = archive.sync().await.unwrap();
953                drop(archive);
954
955                corrupt_frame(
956                    &context,
957                    &cfg.value_partition,
958                    &0u64.to_be_bytes(),
959                    bad_position,
960                    I32_VALUE_FRAME_SIZE,
961                )
962                .await;
963
964                // Every frame after the first invalid one belongs to the same uncommitted suffix,
965                // even if its own CRC is valid:
966                //
967                // first:  [bad]              [valid] [valid] -> []
968                // middle: [valid, retained]  [bad]   [valid] -> [valid]
969                let archive =
970                    Archive::<_, _, FixedBytes<64>, i32>::init(context.child(name), cfg.clone())
971                        .await
972                        .unwrap();
973                assert_eq!(
974                    archive.ranges().collect::<Vec<_>>(),
975                    if retained == 0 {
976                        Vec::new()
977                    } else {
978                        vec![(0, retained - 1)]
979                    }
980                );
981                for (index, value) in [10, 20, 30].into_iter().enumerate() {
982                    let expected = (index < retained as usize).then_some(value);
983                    assert_eq!(
984                        archive.get(Identifier::Index(index as u64)).await.unwrap(),
985                        expected
986                    );
987                }
988                drop(archive);
989
990                // The truncation was made durable before the first reopen returned: a second
991                // reopen observes the same retained prefix.
992                let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child(name), cfg)
993                    .await
994                    .unwrap();
995                assert_eq!(archive.last_index(), retained.checked_sub(1));
996                archive.destroy().await.unwrap();
997            }
998        });
999    }
1000
1001    #[test_traced]
1002    fn test_archive_completes_interrupted_rewind_to_empty_section() {
1003        deterministic::Runner::default().start(|context| async move {
1004            let cfg = test_config("empty-rewind", NZU64!(4), FourCap);
1005
1006            // Seed one durable value, then drop the sidecar so startup cannot consult markers
1007            // and must reconcile the journals alone.
1008            let archive = Archive::init(context.child("seed"), cfg.clone())
1009                .await
1010                .unwrap();
1011            let archive = archive.put(0, test_key("zero"), &10).await.unwrap();
1012            let archive = archive.sync().await.unwrap();
1013            drop(archive);
1014            context.remove(&cfg.metadata_partition, None).await.unwrap();
1015
1016            // Model a crash between Archive's two durable section truncations:
1017            //
1018            //     before: index [A] -> values [A]
1019            //     crash:  index [ ]    values [A]
1020            //
1021            // Archive owns both journals, so startup must finish removing the unindexed value.
1022            let (index, _) = context
1023                .open(&cfg.key_partition, &0u64.to_be_bytes())
1024                .await
1025                .unwrap();
1026            index.resize(0).await.unwrap();
1027            index.sync().await.unwrap();
1028            drop(index);
1029            let (_, value_size) = context
1030                .open(&cfg.value_partition, &0u64.to_be_bytes())
1031                .await
1032                .unwrap();
1033            assert_eq!(value_size, I32_VALUE_FRAME_SIZE);
1034
1035            let archive =
1036                Archive::<_, _, FixedBytes<64>, i32>::init(context.child("repair"), cfg.clone())
1037                    .await
1038                    .unwrap();
1039            assert_eq!(archive.last_index(), None);
1040            drop(archive);
1041            let (_, value_size) = context
1042                .open(&cfg.value_partition, &0u64.to_be_bytes())
1043                .await
1044                .unwrap();
1045            assert_eq!(value_size, 0, "startup must finish the value truncation");
1046            context.remove(&cfg.metadata_partition, None).await.unwrap();
1047
1048            // Once both halves of the rewind are empty, missing derived metadata does not require
1049            // durability work:
1050            //
1051            //     index [ ]    values [ ]    metadata [ ] -> no rewind, no sync
1052            //
1053            // This keeps the recovery write bounded to the restart that actually finds the
1054            // orphaned value bytes.
1055            let pending = PendingSyncs::default();
1056            let delayed = DelayedSyncContext {
1057                inner: context.child("clean_restart"),
1058                pending: pending.clone(),
1059            };
1060            pending.arm();
1061            let completed = Arc::new(AtomicUsize::new(0));
1062            let completed_clone = completed.clone();
1063            let cfg_clone = cfg.clone();
1064            let task = context.child("clean_restart_task").spawn(|_| async move {
1065                let result = Archive::init(delayed.child("archive"), cfg_clone).await;
1066                completed_clone.store(1, Ordering::Relaxed);
1067                result
1068            });
1069            while pending.calls() == 0 && completed.load(Ordering::Relaxed) == 0 {
1070                commonware_runtime::reschedule().await;
1071            }
1072            if pending.calls() != 0 {
1073                pending.unblock();
1074                let _ = task.await;
1075                panic!("clean empty-section restart must not issue durability operations");
1076            }
1077            pending.unblock();
1078            let archive = task.await.unwrap().unwrap();
1079
1080            // The repaired empty section accepts a fresh write at the reclaimed offsets, and
1081            // the write survives reopen.
1082            let archive = archive.put(0, test_key("new"), &20).await.unwrap();
1083            let archive = archive.sync().await.unwrap();
1084            drop(archive);
1085            let (_, value_size) = context
1086                .open(&cfg.value_partition, &0u64.to_be_bytes())
1087                .await
1088                .unwrap();
1089            assert_eq!(value_size, I32_VALUE_FRAME_SIZE);
1090
1091            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
1092                .await
1093                .unwrap();
1094            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(20));
1095            archive.destroy().await.unwrap();
1096        });
1097    }
1098
1099    #[test_traced]
1100    fn test_validation_marker_skips_previously_validated_values() {
1101        let executor = deterministic::Runner::default();
1102        executor.start(|context| async move {
1103            let cfg = test_config("marker-skip", NZU64!(4), FourCap);
1104
1105            // Seed two durable values with no published marker.
1106            let mut archive = Archive::init(context.child("seed"), cfg.clone())
1107                .await
1108                .unwrap();
1109            archive = archive.put(0, test_key("zero"), &10).await.unwrap();
1110            archive = archive.put(1, test_key("one"), &20).await.unwrap();
1111            archive = archive.sync().await.unwrap();
1112            drop(archive);
1113
1114            // Simulate an archive created before validation markers existed. The first open
1115            // scans every retained value and writes the additive sidecar:
1116            //
1117            // first open:  [value 0] [value 1] -> validate 2
1118            // second open: [marker covers both] -> validate 0
1119            context.remove(&cfg.metadata_partition, None).await.unwrap();
1120            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1121                context.child("first_open"),
1122                cfg.clone(),
1123            )
1124            .await
1125            .unwrap();
1126            assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 1)]);
1127            drop(archive);
1128
1129            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1130                context.child("second_open"),
1131                cfg.clone(),
1132            )
1133            .await
1134            .unwrap();
1135            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1136            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1137
1138            // Append a third value above the marker. Its boundary stays debt, so the third open
1139            // must validate the suffix and adopt the covered pair into one contiguous range.
1140            let archive = archive.put(2, test_key("two"), &30).await.unwrap();
1141            let archive = archive.sync().await.unwrap();
1142            drop(archive);
1143
1144            let archive =
1145                Archive::<_, _, FixedBytes<64>, i32>::init(context.child("third_open"), cfg)
1146                    .await
1147                    .unwrap();
1148            assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 2)]);
1149            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1150            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1151            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(30));
1152        });
1153    }
1154
1155    #[test_traced]
1156    fn test_validation_marker_skips_covered_interior_values() {
1157        deterministic::Runner::default().start(|context| async move {
1158            let cfg = test_config("marker-covered-interior", NZU64!(4), FourCap);
1159
1160            // Publish a boundary covering both values. The terminal value remains the floor's
1161            // cross-journal proof. The earlier value must not be revisited during startup.
1162            let archive = Archive::init(context.child("seed"), cfg.clone())
1163                .await
1164                .unwrap();
1165            let archive = archive.put(0, test_key("zero"), &10).await.unwrap();
1166            let archive = archive.put(1, test_key("one"), &20).await.unwrap();
1167
1168            // The first sync proves the data durable. The second publishes the resulting marker.
1169            let archive = archive.sync().await.unwrap();
1170            let archive = archive.sync().await.unwrap();
1171            drop(archive);
1172
1173            // Damage only the covered interior frame. Initialization succeeds because the marker
1174            // skips it, while a direct read still exposes its invalid checksum.
1175            corrupt_frame(
1176                &context,
1177                &cfg.value_partition,
1178                &0u64.to_be_bytes(),
1179                0,
1180                I32_VALUE_FRAME_SIZE,
1181            )
1182            .await;
1183            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
1184                .await
1185                .unwrap();
1186            assert!(archive.get(Identifier::Index(0)).await.is_err());
1187            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1188        });
1189    }
1190
1191    #[test_traced]
1192    fn test_validation_marker_damage_never_mutates() {
1193        #[derive(Clone, Copy)]
1194        enum Damage {
1195            MissingIndex,
1196            MissingValues,
1197            TruncatedIndex,
1198            TruncatedValues,
1199            CorruptIndex,
1200            CorruptValues,
1201        }
1202
1203        deterministic::Runner::default().start(|context| async move {
1204            // Every shape damages data at or below a published marker. Marked bytes were proven
1205            // durable before publication, so startup must treat their loss as corruption (or
1206            // leave it to lazy reads) without mutating either journal.
1207            for (name, damage) in [
1208                ("missing_index", Damage::MissingIndex),
1209                ("missing_values", Damage::MissingValues),
1210                ("truncated_index", Damage::TruncatedIndex),
1211                ("truncated_values", Damage::TruncatedValues),
1212                ("corrupt_index", Damage::CorruptIndex),
1213                ("corrupt_values", Damage::CorruptValues),
1214            ] {
1215                let case = context.child(name);
1216                let cfg = test_config(name, NZU64!(4), FourCap);
1217
1218                // Seed one marked item: the blocking put proves it durable and the trailing
1219                // sync publishes its marker.
1220                let archive = Archive::init(case.child("seed"), cfg.clone())
1221                    .await
1222                    .unwrap();
1223                let archive = archive.put_sync(0, test_key("zero"), &10).await.unwrap();
1224                let archive = archive.sync().await.unwrap();
1225                drop(archive);
1226
1227                // The marker proves that both journals contained one durable validated item:
1228                //
1229                // metadata: section 0 -> 1
1230                // index:    section 0 -> [record]
1231                // values:   section 0 -> [frame]
1232                //
1233                // Prune removes the marker durably before either journal, so missing or truncated
1234                // marked data is corruption, not an interrupted prune.
1235                let (_, index_size) = context
1236                    .open(&cfg.key_partition, &0u64.to_be_bytes())
1237                    .await
1238                    .unwrap();
1239                let (_, value_size) = context
1240                    .open(&cfg.value_partition, &0u64.to_be_bytes())
1241                    .await
1242                    .unwrap();
1243                let damage_index = matches!(
1244                    damage,
1245                    Damage::MissingIndex | Damage::TruncatedIndex | Damage::CorruptIndex
1246                );
1247                let damaged_partition = if damage_index {
1248                    &cfg.key_partition
1249                } else {
1250                    &cfg.value_partition
1251                };
1252                let damaged_size = match damage {
1253                    Damage::MissingIndex | Damage::MissingValues => {
1254                        context
1255                            .remove(damaged_partition, Some(&0u64.to_be_bytes()))
1256                            .await
1257                            .unwrap();
1258                        None
1259                    }
1260                    Damage::TruncatedIndex | Damage::TruncatedValues => {
1261                        let (blob, size) = context
1262                            .open(damaged_partition, &0u64.to_be_bytes())
1263                            .await
1264                            .unwrap();
1265                        let size = if damage_index { size - 1 } else { 0 };
1266                        blob.resize(size).await.unwrap();
1267                        blob.sync().await.unwrap();
1268                        Some(size)
1269                    }
1270                    Damage::CorruptIndex | Damage::CorruptValues => {
1271                        let (blob, size) = context
1272                            .open(damaged_partition, &0u64.to_be_bytes())
1273                            .await
1274                            .unwrap();
1275                        let byte = blob
1276                            .read_at(0, 1, ReadOptions::default())
1277                            .await
1278                            .unwrap()
1279                            .coalesce();
1280                        let byte = byte.as_ref()[0];
1281                        blob.write_at(0, vec![byte ^ 0xFF], WriteOptions::SYNC)
1282                            .await
1283                            .unwrap();
1284                        Some(size)
1285                    }
1286                };
1287
1288                // Marked values are never re-read at startup: value damage that preserves the
1289                // floor's byte range surfaces lazily at get, like covered interior values.
1290                if matches!(damage, Damage::CorruptValues) {
1291                    for child in ["first", "second"] {
1292                        let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1293                            case.child(child),
1294                            cfg.clone(),
1295                        )
1296                        .await
1297                        .expect("marked value damage must not fail startup");
1298                        assert!(archive.get(Identifier::Index(0)).await.is_err());
1299                        drop(archive);
1300                        let (_, size) = context
1301                            .open(&cfg.value_partition, &0u64.to_be_bytes())
1302                            .await
1303                            .unwrap();
1304                        assert_eq!(size, value_size, "adoption must preserve the damaged frame");
1305                    }
1306                    continue;
1307                }
1308
1309                // Failed startups must be byte-stable across repeated opens: rejection happens
1310                // before any repair could mutate the marked section.
1311                for child in ["first", "second"] {
1312                    let result =
1313                        Archive::<_, _, FixedBytes<64>, i32>::init(case.child(child), cfg.clone())
1314                            .await;
1315                    assert!(
1316                        matches!(result, Err(Error::Journal(JournalError::Corruption(_)))),
1317                        "damaged marked section must remain visible as corruption"
1318                    );
1319
1320                    let (surviving_partition, surviving_size) = if damage_index {
1321                        (&cfg.value_partition, value_size)
1322                    } else {
1323                        (&cfg.key_partition, index_size)
1324                    };
1325                    let (_, size) = context
1326                        .open(surviving_partition, &0u64.to_be_bytes())
1327                        .await
1328                        .unwrap();
1329                    assert_eq!(
1330                        size, surviving_size,
1331                        "failed startup must preserve the surviving journal section"
1332                    );
1333                    if let Some(damaged_size) = damaged_size {
1334                        let (_, size) = context
1335                            .open(damaged_partition, &0u64.to_be_bytes())
1336                            .await
1337                            .unwrap();
1338                        assert_eq!(
1339                            size, damaged_size,
1340                            "failed startup must not normalize the damaged journal section"
1341                        );
1342                    }
1343                }
1344            }
1345        });
1346    }
1347
1348    #[test_traced]
1349    fn test_validation_floor_rejection_precedes_index_suffix_repair() {
1350        deterministic::Runner::default().start(|context| async move {
1351            let cfg = test_config("floor-order", NZU64!(4), FourCap);
1352
1353            // Seed one marked item: the blocking put proves it durable and the trailing sync
1354            // publishes its marker.
1355            let archive =
1356                Archive::<_, _, FixedBytes<64>, i32>::init(context.child("seed"), cfg.clone())
1357                    .await
1358                    .unwrap();
1359            let archive = archive.put_sync(0, test_key("zero"), &10).await.unwrap();
1360            let archive = archive.sync().await.unwrap();
1361            drop(archive);
1362
1363            // Append trailing junk past the index tail. Alone, this is repairable damage that
1364            // suffix repair would truncate on the next open.
1365            let (index, index_size) = context
1366                .open(&cfg.key_partition, &0u64.to_be_bytes())
1367                .await
1368                .unwrap();
1369            index
1370                .write_at(index_size, vec![0xA5; 7], WriteOptions::SYNC)
1371                .await
1372                .unwrap();
1373            let expected_size = index_size + 7;
1374
1375            // Break the floor's terminal index page so preflight rejects the section before
1376            // the repairable trailing junk can be truncated.
1377            let byte = index
1378                .read_at(0, 1, ReadOptions::default())
1379                .await
1380                .unwrap()
1381                .coalesce();
1382            index
1383                .write_at(0, vec![byte.as_ref()[0] ^ 0xFF], WriteOptions::SYNC)
1384                .await
1385                .unwrap();
1386            let expected = index
1387                .read_at(0, expected_size as usize, ReadOptions::default())
1388                .await
1389                .unwrap()
1390                .coalesce();
1391            drop(index);
1392
1393            // Repeated failed opens must leave the section byte-identical, junk included: the
1394            // floor check rejects the section before suffix repair can truncate anything.
1395            for child in ["first", "second"] {
1396                let result =
1397                    Archive::<_, _, FixedBytes<64>, i32>::init(context.child(child), cfg.clone())
1398                        .await;
1399                assert!(matches!(
1400                    result,
1401                    Err(Error::Journal(JournalError::Corruption(_)))
1402                ));
1403
1404                let (index, actual_size) = context
1405                    .open(&cfg.key_partition, &0u64.to_be_bytes())
1406                    .await
1407                    .unwrap();
1408                assert_eq!(actual_size, expected_size);
1409                let actual = index
1410                    .read_at(0, actual_size as usize, ReadOptions::default())
1411                    .await
1412                    .unwrap()
1413                    .coalesce();
1414                assert_eq!(actual.as_ref(), expected.as_ref());
1415            }
1416        });
1417    }
1418
1419    #[test_traced]
1420    fn test_validation_marker_survives_torn_index_tail_rewrite() {
1421        let executor = deterministic::Runner::default();
1422        let (_, checkpoint) = executor.start_and_recover(|context| async move {
1423            let cfg = test_config("marker-torn-tail", NZU64!(4), FourCap);
1424            let archive = Archive::init(context.child("seed"), cfg.clone())
1425                .await
1426                .unwrap();
1427
1428            // Publish a marker for one record while its index occupies only part of the first page.
1429            let archive = archive.put_sync(0, test_key("zero"), &10).await.unwrap();
1430            let archive = archive.sync().await.unwrap();
1431
1432            // A physical page is the logical page plus a two-slot trailer, where each slot is a
1433            // two-byte length and a four-byte checksum over that length's prefix of the page:
1434            //
1435            //   [ records ... pad ......][len0 crc0][len1 crc1]
1436            //   0               page_size         +6        +12
1437            //
1438            // The first write filled slot 0 for one record and left slot 1 zeroed.
1439            let page_size = usize::from(PAGE_SIZE.get());
1440            let physical_page_size = page_size + 12;
1441            let record_size = u64::SIZE + FixedBytes::<64>::SIZE + u64::SIZE + u32::SIZE;
1442            assert!(record_size < page_size);
1443            let old_page = context
1444                .logical_blob(&cfg.key_partition, &0u64.to_be_bytes())
1445                .unwrap();
1446            assert_eq!(old_page.len(), physical_page_size);
1447            let old_len =
1448                u16::from_be_bytes(old_page[page_size..page_size + 2].try_into().unwrap()) as usize;
1449            let old_crc =
1450                u32::from_be_bytes(old_page[page_size + 2..page_size + 6].try_into().unwrap());
1451            assert_eq!(old_len, record_size);
1452            assert_eq!(old_crc, Crc32::checksum(&old_page[..old_len]));
1453
1454            // Capture Archive's same-page extension, then persist only the prefix through the new
1455            // slot's length. This is the exact Prefix fault cut: the old slot remains valid while
1456            // the new slot's checksum retains its prior zero bytes.
1457            let archive = archive.put_sync(1, test_key("one"), &20).await.unwrap();
1458            drop(archive);
1459            let (index, size) = context
1460                .open(&cfg.key_partition, &0u64.to_be_bytes())
1461                .await
1462                .unwrap();
1463            assert_eq!(size, physical_page_size as u64);
1464            let new_page = index
1465                .read_at(0, physical_page_size, ReadOptions::default())
1466                .await
1467                .unwrap()
1468                .coalesce();
1469            let new_page = new_page.as_ref().to_vec();
1470            assert_eq!(
1471                &new_page[page_size..page_size + 6],
1472                &old_page[page_size..page_size + 6],
1473            );
1474            let new_len =
1475                u16::from_be_bytes(new_page[page_size + 6..page_size + 8].try_into().unwrap())
1476                    as usize;
1477            let new_crc =
1478                u32::from_be_bytes(new_page[page_size + 8..page_size + 12].try_into().unwrap());
1479            assert_eq!(new_len, 2 * record_size);
1480            assert_eq!(new_crc, Crc32::checksum(&new_page[..new_len]));
1481
1482            // Restore the one-record image, then replay the extended image only through slot
1483            // 1's length. The stored page now holds both records' data, a valid slot 0 covering
1484            // one record, and a slot 1 that advertises two records with a stale zero checksum:
1485            //
1486            //   [ record 0 | record 1 | pad ][len0 crc0][len1 crc1]
1487            //     persisted prefix ends after len1 --------------^
1488            index
1489                .write_at(0, old_page.clone(), WriteOptions::SYNC)
1490                .await
1491                .unwrap();
1492            let torn_prefix = page_size + 6 + 2;
1493            index
1494                .write_at(0, new_page[..torn_prefix].to_vec(), WriteOptions::SYNC)
1495                .await
1496                .unwrap();
1497            let torn_page = index
1498                .read_at(0, physical_page_size, ReadOptions::default())
1499                .await
1500                .unwrap()
1501                .coalesce();
1502            let torn_page = torn_page.as_ref();
1503            assert_eq!(
1504                &torn_page[page_size..page_size + 6],
1505                &old_page[page_size..page_size + 6],
1506            );
1507            assert_eq!(
1508                u16::from_be_bytes(torn_page[page_size + 6..page_size + 8].try_into().unwrap(),)
1509                    as usize,
1510                new_len,
1511            );
1512            let torn_crc =
1513                u32::from_be_bytes(torn_page[page_size + 8..page_size + 12].try_into().unwrap());
1514            assert_ne!(torn_crc, Crc32::checksum(&torn_page[..new_len]));
1515            drop(index);
1516
1517            // Both value frames survive the torn index write, leaving recovery to decide which
1518            // bytes are orphaned.
1519            let (_, value_size) = context
1520                .open(&cfg.value_partition, &0u64.to_be_bytes())
1521                .await
1522                .unwrap();
1523            assert_eq!(value_size, 2 * I32_VALUE_FRAME_SIZE);
1524        });
1525
1526        deterministic::Runner::from(checkpoint).start(|context| async move {
1527            let pending = PendingSyncs::default();
1528            let delayed = DelayedSyncContext {
1529                inner: context.child("delayed"),
1530                pending: pending.clone(),
1531            };
1532            pending.arm();
1533            let cfg = test_config("marker-torn-tail", NZU64!(4), FourCap);
1534
1535            // Recovery must trust the marker-covered record, select the older checksum slot, and
1536            // durably remove only the orphaned value suffix.
1537            let archive = drive_pending_syncs(
1538                &pending,
1539                Archive::<_, _, FixedBytes<64>, i32>::init(delayed.child("reopen"), cfg.clone()),
1540            )
1541            .await
1542            .unwrap();
1543            assert_eq!(pending.calls(), 1);
1544            assert_eq!(archive.last_index(), Some(0));
1545            assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 0)]);
1546            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1547            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
1548            drop(archive);
1549
1550            let (_, value_size) = context
1551                .open(&cfg.value_partition, &0u64.to_be_bytes())
1552                .await
1553                .unwrap();
1554            assert_eq!(value_size, I32_VALUE_FRAME_SIZE);
1555        });
1556    }
1557
1558    #[test_traced]
1559    fn test_startup_publishes_validated_marker_without_data_resync() {
1560        deterministic::Runner::default().start(|context| async move {
1561            let cfg = test_config("startup-order", NZU64!(4), FourCap);
1562
1563            // Seed one durable value with no published marker, so the next startup must
1564            // validate it and derive the marker itself.
1565            let archive = Archive::init(context.child("seed"), cfg.clone())
1566                .await
1567                .unwrap();
1568            let archive = archive.put(0, test_key("zero"), &10).await.unwrap();
1569            let archive = archive.sync().await.unwrap();
1570            drop(archive);
1571
1572            // Reopen through the armed wrapper to count every durability operation startup
1573            // issues.
1574            let pending = PendingSyncs::default();
1575            let delayed = DelayedSyncContext {
1576                inner: context.child("delayed"),
1577                pending: pending.clone(),
1578            };
1579            pending.arm();
1580            let completed = Arc::new(AtomicUsize::new(0));
1581            let completed_clone = completed.clone();
1582            let reopen_cfg = cfg.clone();
1583            let task = context.child("startup").spawn(|_| async move {
1584                let result =
1585                    Archive::<_, _, FixedBytes<64>, i32>::init(delayed.child("reopen"), reopen_cfg)
1586                        .await;
1587                completed_clone.store(1, Ordering::Relaxed);
1588                result
1589            });
1590
1591            while pending.calls() == 0 && completed.load(Ordering::Relaxed) == 0 {
1592                commonware_runtime::reschedule().await;
1593            }
1594            commonware_runtime::reschedule().await;
1595
1596            // Startup-readable bytes are already durable. A clean reopen should start only the
1597            // derived marker and return while that metadata sync drives itself in the background.
1598            let calls = pending.calls();
1599            let finished = completed.load(Ordering::Relaxed);
1600            if calls != 1 || finished != 1 {
1601                pending.unblock();
1602                let _ = task.await;
1603                panic!(
1604                    "clean startup must return after starting one marker sync, calls={calls}, \
1605                     finished={finished}"
1606                );
1607            }
1608
1609            pending.unblock();
1610            let archive = task.await.unwrap().unwrap().sync().await.unwrap();
1611            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1612            drop(archive);
1613
1614            // The published marker must leave a reopenable archive.
1615            Archive::<_, _, FixedBytes<64>, i32>::init(context.child("marker_reopen"), cfg)
1616                .await
1617                .unwrap()
1618                .destroy()
1619                .await
1620                .unwrap();
1621        });
1622    }
1623
1624    #[test_traced]
1625    fn test_startup_marker_failure_fails_next_sync() {
1626        deterministic::Runner::default().start(|context| async move {
1627            let cfg = test_config("startup-marker-failure", NZU64!(4), FourCap);
1628
1629            // Seed one durable value with no published marker, so reopen must derive one.
1630            let archive = Archive::init(context.child("seed"), cfg.clone())
1631                .await
1632                .unwrap();
1633            let archive = archive.put(0, test_key("zero"), &10).await.unwrap();
1634            let archive = archive.sync().await.unwrap();
1635            drop(archive);
1636
1637            // Reopen with the armed wrapper. The only durability operation startup issues is
1638            // the derived marker sync, left parked.
1639            let pending = PendingSyncs::default();
1640            let delayed = DelayedSyncContext {
1641                inner: context.child("delayed"),
1642                pending: pending.clone(),
1643            };
1644            pending.arm();
1645            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(delayed.child("reopen"), cfg)
1646                .await
1647                .unwrap();
1648            assert_eq!(pending.lock().len(), 1);
1649
1650            // A marker sync failure must not vanish: the next sync call observes the completed
1651            // generation and surfaces the failure as a metadata error.
1652            fail_pending_syncs(&pending);
1653            assert!(matches!(archive.sync().await, Err(Error::Metadata(_))));
1654        });
1655    }
1656
1657    #[test_traced]
1658    fn test_start_sync_publishes_closed_section_boundary() {
1659        let executor = deterministic::Runner::default();
1660        executor.start(|context| async move {
1661            let cfg = test_config("marker-lag", NZU64!(4), FourCap);
1662
1663            let pending = PendingSyncs::default();
1664            let delayed = DelayedSyncContext {
1665                inner: context.child("delayed"),
1666                pending: pending.clone(),
1667            };
1668            let archive = Archive::init(delayed.child("archive"), cfg.clone())
1669                .await
1670                .unwrap();
1671
1672            // The first call has no prior durability proof, so it starts only the index and value
1673            // syncs. Moving to section 4 closes section 0 and publishes its completed boundary
1674            // alongside the new section's data syncs:
1675            //
1676            // call 1: section 0 data durable, marker absent
1677            // call 2: section 4 data pending, section 0 marker pending
1678            let (archive, first) = archive
1679                .put_start_sync(0, test_key("zero"), &10)
1680                .await
1681                .unwrap();
1682            assert_eq!(pending.lock().len(), 2);
1683            release_pending_syncs(&pending);
1684            first.await.unwrap();
1685
1686            let archive = archive.put(4, test_key("four"), &40).await.unwrap();
1687            let (archive, second) = archive.start_sync().await.unwrap();
1688            assert_eq!(pending.lock().len(), 3);
1689            release_pending_syncs(&pending);
1690            second.await.unwrap();
1691            drop(archive);
1692
1693            // Reopen adopts section 0 through its published marker and validates section 4
1694            // above its absent one. Both items must survive.
1695            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1696                context.child("first_reopen"),
1697                cfg.clone(),
1698            )
1699            .await
1700            .unwrap();
1701            assert_eq!(archive.ranges().collect::<Vec<_>>(), vec![(0, 0), (4, 4)]);
1702            drop(archive);
1703
1704            Archive::<_, _, FixedBytes<64>, i32>::init(context.child("second_reopen"), cfg)
1705                .await
1706                .unwrap()
1707                .destroy()
1708                .await
1709                .unwrap();
1710        });
1711    }
1712
1713    #[test_traced]
1714    fn test_sync_publishes_closed_section_boundary() {
1715        let executor = deterministic::Runner::default();
1716        executor.start(|context| async move {
1717            let cfg = test_config("blocking-marker-lag", NZU64!(4), FourCap);
1718
1719            let pending = PendingSyncs::default();
1720            let delayed = DelayedSyncContext {
1721                inner: context.child("delayed"),
1722                pending: pending.clone(),
1723            };
1724            let archive = Archive::init(delayed.child("archive"), cfg.clone())
1725                .await
1726                .unwrap();
1727
1728            // Prove section 0's single item durable so its boundary becomes publishable debt.
1729            let (archive, first) = archive
1730                .put_start_sync(0, test_key("zero"), &10)
1731                .await
1732                .unwrap();
1733            assert_eq!(pending.lock().len(), 2);
1734            release_pending_syncs(&pending);
1735            first.await.unwrap();
1736
1737            // Buffer section 4 and park its blocking sync behind the armed gate.
1738            let archive = archive.put(4, test_key("four"), &40).await.unwrap();
1739            pending.arm();
1740            let completed = Arc::new(AtomicUsize::new(0));
1741            let completed_clone = completed.clone();
1742            let task = delayed.inner.child("sync").spawn(|_| async move {
1743                let archive = archive.sync().await.unwrap();
1744                completed_clone.store(1, Ordering::Relaxed);
1745                archive
1746            });
1747
1748            while pending.calls() < 2 {
1749                commonware_runtime::reschedule().await;
1750            }
1751            commonware_runtime::reschedule().await;
1752
1753            // The marker for closed section 0 must start alongside section 4's data sync, rather
1754            // than after it:
1755            //
1756            // data:   section 4 [0 -------- 1)  <- current sync, two journals
1757            // marker: section 0 [0 -------- 1)  <- previous durable boundary
1758            let parked_syncs = pending.lock().len();
1759            if parked_syncs != 3 {
1760                // Let the spawned operation unwind before reporting the regression. Otherwise the
1761                // deterministic runner would correctly keep waiting for its parked durability work.
1762                pending.unblock();
1763                let _ = task.await;
1764                panic!(
1765                    "blocking sync must not serialize the derived marker behind data: \
1766                     parked {parked_syncs} durability operations"
1767                );
1768            }
1769
1770            // The two data journals enqueue first. Release only the metadata operation to prove
1771            // that publishing the older boundary cannot satisfy the blocking data contract. The
1772            // marker future is polled only when a later request observes it, so this test
1773            // cannot wait for it to park before releasing it.
1774            let metadata = pending.lock().remove(2);
1775            metadata.release.send(Ok(())).unwrap();
1776            commonware_runtime::reschedule().await;
1777            assert_eq!(
1778                completed.load(Ordering::Relaxed),
1779                0,
1780                "publishing the previous marker must not complete the current data sync"
1781            );
1782
1783            release_pending_syncs(&pending);
1784            let archive = task.await.unwrap();
1785            drop(archive);
1786
1787            // Both items survive reopen: the published marker adopts section 0 and validation
1788            // covers section 4.
1789            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1790                delayed.inner.child("first_reopen"),
1791                cfg.clone(),
1792            )
1793            .await
1794            .unwrap();
1795            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1796            assert_eq!(archive.get(Identifier::Index(4)).await.unwrap(), Some(40));
1797            drop(archive);
1798
1799            Archive::<_, _, FixedBytes<64>, i32>::init(delayed.inner.child("second_reopen"), cfg)
1800                .await
1801                .unwrap()
1802                .destroy()
1803                .await
1804                .unwrap();
1805        });
1806    }
1807
1808    #[test_traced]
1809    fn test_sync_delays_immediately_ready_durable_boundary() {
1810        let executor = deterministic::Runner::default();
1811        executor.start(|context| async move {
1812            // Started durability operations complete immediately and are only counted.
1813            let pending = PendingSyncs::default();
1814            pending.unblock();
1815            let immediate = DelayedSyncContext {
1816                inner: context,
1817                pending: pending.clone(),
1818            };
1819            let cfg = test_config("ready-marker-lag", NZU64!(4), FourCap);
1820
1821            let archive = Archive::init(immediate.child("archive"), cfg)
1822                .await
1823                .unwrap();
1824            let initial_starts = pending.starts();
1825            let archive = archive.put_sync(0, test_key("zero"), &10).await.unwrap();
1826
1827            // The unblocked wrapper returns immediately-ready data-sync handles while retaining
1828            // the number of durability operations. The current boundary must still become
1829            // publication debt for the next sync, rather than making this call synchronously
1830            // persist a marker after its data:
1831            //
1832            // call 1: two data syncs, marker absent
1833            // call 2: no new data, one marker sync
1834            assert_eq!(pending.starts() - initial_starts, 2);
1835
1836            let archive = archive.sync().await.unwrap();
1837            assert_eq!(pending.starts() - initial_starts, 3);
1838            archive.destroy().await.unwrap();
1839        });
1840    }
1841
1842    #[test_traced]
1843    fn test_sync_batches_markers_by_active_section() {
1844        let executor = deterministic::Runner::default();
1845        executor.start(|context| async move {
1846            // Started durability operations complete immediately and are only counted.
1847            let pending = PendingSyncs::default();
1848            pending.unblock();
1849            let immediate = DelayedSyncContext {
1850                inner: context,
1851                pending: pending.clone(),
1852            };
1853            let cfg = test_config("section-marker-batch", NZU64!(4), FourCap);
1854            let archive = Archive::init(immediate.child("archive"), cfg)
1855                .await
1856                .unwrap();
1857            let initial_starts = pending.starts();
1858
1859            // Repeated writes in one active section start only the index and value durability
1860            // operations. Its marker remains debt until writes move to another section.
1861            let archive = archive.put_sync(0, test_key("zero"), &10).await.unwrap();
1862            let archive = archive.put_sync(1, test_key("one"), &20).await.unwrap();
1863            assert_eq!(pending.starts() - initial_starts, 4);
1864
1865            // Moving to section 4 publishes section 0's completed boundary alongside the two data
1866            // operations for section 4. An explicit empty sync then flushes the final partial
1867            // section once. Another empty sync has no durability work.
1868            let archive = archive.put_sync(4, test_key("four"), &40).await.unwrap();
1869            assert_eq!(pending.starts() - initial_starts, 7);
1870            let archive = archive.sync().await.unwrap();
1871            assert_eq!(pending.starts() - initial_starts, 8);
1872            let archive = archive.sync().await.unwrap();
1873            assert_eq!(pending.starts() - initial_starts, 8);
1874            archive.destroy().await.unwrap();
1875        });
1876    }
1877
1878    #[test_traced]
1879    fn test_start_sync_withholds_marker_for_unproven_boundary() {
1880        let executor = deterministic::Runner::default();
1881        executor.start(|context| async move {
1882            let cfg = test_config("marker-unproven-boundary", NZU64!(4), FourCap);
1883
1884            let pending = PendingSyncs::default();
1885            let delayed = DelayedSyncContext {
1886                inner: context.child("delayed"),
1887                pending: pending.clone(),
1888            };
1889            let archive = Archive::init(delayed.child("archive"), cfg.clone())
1890                .await
1891                .unwrap();
1892
1893            // Prove section 0's single item so it owns publishable marker debt.
1894            let (archive, first) = archive
1895                .put_start_sync(0, test_key("zero"), &10)
1896                .await
1897                .unwrap();
1898            release_pending_syncs(&pending);
1899            first.await.unwrap();
1900
1901            // Moving to section 4 starts its data syncs and publishes section 0's boundary.
1902            let archive = archive.put(4, test_key("four"), &40).await.unwrap();
1903            let (archive, second) = archive.start_sync().await.unwrap();
1904            assert_eq!(pending.lock().len(), 3);
1905
1906            // Complete only the marker generation, which parked last. Section 4's data syncs
1907            // stay in flight, so nothing beyond its published floor is proven. Sync futures
1908            // are lazy, so the released marker resolves when the next request polls it.
1909            let marker = pending.lock().pop().expect("marker sync parked");
1910            marker
1911                .release
1912                .send(Ok(()))
1913                .expect("marker sync receiver dropped");
1914
1915            // The next request observes the completed marker generation and retires caught-up
1916            // barriers. It must not manufacture a durability proof for section 4: publishing
1917            // its unproven length as a marker would let a crash that keeps the marker but
1918            // loses the in-flight items make every reopen fail. The two operations started
1919            // here are section 8's index and value syncs.
1920            let archive = archive.put(8, test_key("eight"), &80).await.unwrap();
1921            let before = pending.starts();
1922            let (archive, third) = archive.start_sync().await.unwrap();
1923            assert_eq!(pending.starts() - before, 2);
1924
1925            release_pending_syncs(&pending);
1926            second.await.unwrap();
1927            third.await.unwrap();
1928
1929            // Every boundary publishes once proven. A blocking sync flushes the remaining
1930            // debt and a reopen sees all three sections.
1931            let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
1932            drop(archive);
1933            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
1934                .await
1935                .unwrap();
1936            assert_eq!(
1937                archive.ranges().collect::<Vec<_>>(),
1938                vec![(0, 0), (4, 4), (8, 8)]
1939            );
1940            archive.destroy().await.unwrap();
1941        });
1942    }
1943
1944    #[test_traced]
1945    fn test_sync_publishes_previous_durable_sections_across_section_changes() {
1946        let executor = deterministic::Runner::default();
1947        executor.start(|context| async move {
1948            let cfg = test_config("cross-section-marker-lag", NZU64!(1), FourCap);
1949
1950            let pending = PendingSyncs::default();
1951            let delayed = DelayedSyncContext {
1952                inner: context.child("delayed"),
1953                pending: pending.clone(),
1954            };
1955            let archive = Archive::init(delayed.child("archive"), cfg.clone())
1956                .await
1957                .unwrap();
1958
1959            // Three items land in three single-item sections, each through its own blocking
1960            // sync.
1961            let archive = drive_pending_syncs(&pending, archive.put_sync(0, test_key("zero"), &10))
1962                .await
1963                .unwrap();
1964            let archive = drive_pending_syncs(&pending, archive.put_sync(1, test_key("one"), &20))
1965                .await
1966                .unwrap();
1967            let archive = drive_pending_syncs(&pending, archive.put_sync(2, test_key("two"), &30))
1968                .await
1969                .unwrap();
1970            drop(archive);
1971
1972            // Every item occupies its own section. Each blocking sync publishes the preceding
1973            // call's completed section while synchronizing the current section:
1974            //
1975            // call 1: data section 0, marker absent
1976            // call 2: data section 1, marker section 0
1977            // call 3: data section 2, marker section 1
1978            //
1979            // Markers may trail durable data, but changing sections must not strand every earlier
1980            // proof and turn startup validation into an unbounded full-archive scan.
1981            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(
1982                context.child("first_reopen"),
1983                cfg.clone(),
1984            )
1985            .await
1986            .unwrap();
1987            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
1988            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
1989            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(30));
1990            drop(archive);
1991
1992            Archive::<_, _, FixedBytes<64>, i32>::init(context.child("second_reopen"), cfg)
1993                .await
1994                .unwrap()
1995                .destroy()
1996                .await
1997                .unwrap();
1998        });
1999    }
2000
2001    #[test_traced]
2002    fn test_empty_sync_publishes_final_durable_boundary() {
2003        let executor = deterministic::Runner::default();
2004        executor.start(|context| async move {
2005            let cfg = test_config("empty-sync-marker", NZU64!(1), FourCap);
2006
2007            let pending = PendingSyncs::default();
2008            let delayed = DelayedSyncContext {
2009                inner: context.child("delayed"),
2010                pending: pending.clone(),
2011            };
2012            let archive = Archive::init(delayed.child("archive"), cfg.clone())
2013                .await
2014                .unwrap();
2015
2016            // Seed one durable item. The two starts are section 0's index and value syncs.
2017            let archive = drive_pending_syncs(&pending, archive.put_sync(0, test_key("zero"), &10))
2018                .await
2019                .unwrap();
2020            assert_eq!(pending.starts(), 2);
2021
2022            // A blocking sync with no new data flushes the final lagging marker without
2023            // re-synchronizing the already durable section:
2024            //
2025            // call 1: data section 0, marker absent
2026            // call 2: data absent,    marker section 0
2027            let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2028            assert_eq!(pending.starts(), 3);
2029
2030            // Once the marker completes, another empty sync has no durability work.
2031            let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2032            assert_eq!(pending.starts(), 3);
2033            drop(archive);
2034
2035            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2036                .await
2037                .unwrap();
2038            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
2039        });
2040    }
2041
2042    #[test_traced]
2043    fn test_sync_recreates_settled_section_barrier() {
2044        let executor = deterministic::Runner::default();
2045        executor.start(|context| async move {
2046            let cfg = test_config("marker-recreate", NZU64!(2), FourCap);
2047
2048            let pending = PendingSyncs::default();
2049            let delayed = DelayedSyncContext {
2050                inner: context.child("delayed"),
2051                pending: pending.clone(),
2052            };
2053            let archive = Archive::init(delayed.child("archive"), cfg.clone())
2054                .await
2055                .unwrap();
2056
2057            // Each successful call publishes the preceding section and retains the current one as
2058            // marker debt. Returning to section 0 must start its new barrier at the retained
2059            // one-item prefix rather than at zero.
2060            let archive = drive_pending_syncs(&pending, archive.put_sync(0, test_key("zero"), &10))
2061                .await
2062                .unwrap();
2063            assert_eq!(pending.starts(), 2);
2064            let archive = drive_pending_syncs(&pending, archive.put_sync(2, test_key("two"), &30))
2065                .await
2066                .unwrap();
2067            assert_eq!(pending.starts(), 5);
2068            let archive = drive_pending_syncs(&pending, archive.put_sync(1, test_key("one"), &20))
2069                .await
2070                .unwrap();
2071            assert_eq!(pending.starts(), 8);
2072
2073            // Flush the final marker, then prove another empty sync has no retained work.
2074            let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2075            assert_eq!(pending.starts(), 9);
2076            let archive = drive_pending_syncs(&pending, archive.sync()).await.unwrap();
2077            assert_eq!(pending.starts(), 9);
2078            drop(archive);
2079
2080            // The recreated barrier's final marker covers section 0's full two-item prefix, so
2081            // a reopen sees every value.
2082            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2083                .await
2084                .unwrap();
2085            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(10));
2086            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), Some(20));
2087            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), Some(30));
2088        });
2089    }
2090
2091    #[test_traced]
2092    fn test_prune_clears_validation_marker_before_section_reuse() {
2093        let executor = deterministic::Runner::default();
2094        executor.start(|context| async move {
2095            let cfg = test_config("marker-reuse", NZU64!(2), FourCap);
2096
2097            // Seed one marked item in section 0 (the trailing sync publishes its marker), then
2098            // prune the section away. Prune must remove the durable marker along with the data:
2099            // a surviving marker would make the next init fail by claiming a marked section
2100            // with no journals.
2101            let archive = Archive::init(context.child("seed"), cfg.clone())
2102                .await
2103                .unwrap();
2104            let archive = archive.put_sync(0, test_key("old"), &10).await.unwrap();
2105            let archive = archive.sync().await.unwrap();
2106            let archive = archive.prune(2).await.unwrap();
2107            drop(archive);
2108
2109            // Reinitialization resets the in-memory prune floor, so section 0 can be created again
2110            // if the application does not reapply its durable floor. Make the replacement bytes
2111            // durable without a second sync call that could publish their new marker:
2112            //
2113            // old section 0: [position 0 -> 10] --prune--> absent
2114            // new section 0: [position 0 -> 20] --sync data only--> validate on reopen
2115            //
2116            // A stale marker from the old incarnation would incorrectly skip that validation.
2117            let pending = PendingSyncs::default();
2118            let delayed = DelayedSyncContext {
2119                inner: context.child("delayed"),
2120                pending: pending.clone(),
2121            };
2122            let archive = Archive::init(delayed.child("reuse"), cfg.clone())
2123                .await
2124                .unwrap();
2125            let archive = archive.put(0, test_key("new"), &20).await.unwrap();
2126            let (archive, handle) = archive.start_sync().await.unwrap();
2127            assert_eq!(pending.lock().len(), 2);
2128            release_pending_syncs(&pending);
2129            handle.await.unwrap();
2130            drop(archive);
2131
2132            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2133                .await
2134                .unwrap();
2135            assert_eq!(archive.get(Identifier::Index(0)).await.unwrap(), Some(20));
2136        });
2137    }
2138
2139    #[test_traced]
2140    fn test_archive_compression_then_none() {
2141        // Initialize the deterministic context
2142        let executor = deterministic::Runner::default();
2143        executor.start(|context| async move {
2144            // Initialize the archive
2145            let cfg = Config {
2146                translator: FourCap,
2147                metadata_partition: "test-metadata".into(),
2148                key_partition: "test-index".into(),
2149                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2150                value_partition: "test-value".into(),
2151                codec_config: (),
2152                compression: Some(3),
2153                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2154                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2155                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2156                items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2157            };
2158            let mut archive = Archive::init(context.child("first"), cfg.clone())
2159                .await
2160                .expect("Failed to initialize archive");
2161
2162            // Put the key-data pair
2163            let index = 1u64;
2164            let key = test_key("testkey");
2165            let data = 1;
2166            archive = archive
2167                .put(index, key.clone(), &data)
2168                .await
2169                .expect("Failed to put data");
2170
2171            // Sync and drop the archive
2172            let archive = archive.sync().await.expect("Failed to sync archive");
2173            drop(archive);
2174
2175            // Initialize the archive again without compression.
2176            // Index journal replay succeeds (no compression), but value reads will fail.
2177            let cfg = Config {
2178                translator: FourCap,
2179                metadata_partition: "test-metadata".into(),
2180                key_partition: "test-index".into(),
2181                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2182                value_partition: "test-value".into(),
2183                codec_config: (),
2184                compression: None,
2185                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2186                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2187                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2188                items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2189            };
2190            let archive =
2191                Archive::<_, _, FixedBytes<64>, i32>::init(context.child("second"), cfg.clone())
2192                    .await
2193                    .unwrap();
2194
2195            // Getting the value should fail because compression settings mismatch.
2196            // Without compression, the codec sees extra bytes after decoding the value
2197            // (because the compressed data doesn't match the expected format).
2198            let result: Result<Option<i32>, _> = archive.get(Identifier::Index(index)).await;
2199            assert!(matches!(
2200                result,
2201                Err(Error::Journal(JournalError::Codec(CodecError::ExtraData(
2202                    _
2203                ))))
2204            ));
2205        });
2206    }
2207
2208    #[test_traced]
2209    fn test_archive_overlapping_key_basic() {
2210        // Initialize the deterministic context
2211        let executor = deterministic::Runner::default();
2212        executor.start(|context| async move {
2213            // Initialize the archive
2214            let cfg = Config {
2215                translator: FourCap,
2216                metadata_partition: "test-metadata".into(),
2217                key_partition: "test-index".into(),
2218                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2219                value_partition: "test-value".into(),
2220                codec_config: (),
2221                compression: None,
2222                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2223                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2224                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2225                items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2226            };
2227            let mut archive = Archive::init(context.child("storage"), cfg.clone())
2228                .await
2229                .expect("Failed to initialize archive");
2230
2231            let index1 = 1u64;
2232            let key1 = test_key("keys1");
2233            let data1 = 1;
2234            let index2 = 2u64;
2235            let key2 = test_key("keys2");
2236            let data2 = 2;
2237
2238            // Put the key-data pair
2239            archive = archive
2240                .put(index1, key1.clone(), &data1)
2241                .await
2242                .expect("Failed to put data");
2243
2244            // Put the key-data pair
2245            archive = archive
2246                .put(index2, key2.clone(), &data2)
2247                .await
2248                .expect("Failed to put data");
2249
2250            // Get the data back
2251            let retrieved = archive
2252                .get(Identifier::Key(&key1))
2253                .await
2254                .expect("Failed to get data")
2255                .expect("Data not found");
2256            assert_eq!(retrieved, data1);
2257
2258            // Get the data back
2259            let retrieved = archive
2260                .get(Identifier::Key(&key2))
2261                .await
2262                .expect("Failed to get data")
2263                .expect("Data not found");
2264            assert_eq!(retrieved, data2);
2265
2266            // Check metrics
2267            let buffer = context.encode();
2268            assert!(has_metric_value(&buffer, "items_tracked", 2));
2269            assert!(buffer.contains("unnecessary_reads_total 1"));
2270            assert!(buffer.contains("gets_total 2"));
2271        });
2272    }
2273
2274    #[test_traced]
2275    fn test_archive_overlapping_key_multiple_sections() {
2276        // Initialize the deterministic context
2277        let executor = deterministic::Runner::default();
2278        executor.start(|context| async move {
2279            // Initialize the archive
2280            let cfg = Config {
2281                translator: FourCap,
2282                metadata_partition: "test-metadata".into(),
2283                key_partition: "test-index".into(),
2284                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2285                value_partition: "test-value".into(),
2286                codec_config: (),
2287                compression: None,
2288                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2289                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2290                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2291                items_per_section: NZU64!(DEFAULT_ITEMS_PER_SECTION),
2292            };
2293            let mut archive = Archive::init(context.child("storage"), cfg.clone())
2294                .await
2295                .expect("Failed to initialize archive");
2296
2297            let index1 = 1u64;
2298            let key1 = test_key("keys1");
2299            let data1 = 1;
2300            let index2 = 2_000_000u64;
2301            let key2 = test_key("keys2");
2302            let data2 = 2;
2303
2304            // Put the key-data pair
2305            archive = archive
2306                .put(index1, key1.clone(), &data1)
2307                .await
2308                .expect("Failed to put data");
2309
2310            // Put the key-data pair
2311            archive = archive
2312                .put(index2, key2.clone(), &data2)
2313                .await
2314                .expect("Failed to put data");
2315
2316            // Get the data back
2317            let retrieved = archive
2318                .get(Identifier::Key(&key1))
2319                .await
2320                .expect("Failed to get data")
2321                .expect("Data not found");
2322            assert_eq!(retrieved, data1);
2323
2324            // Get the data back
2325            let retrieved = archive
2326                .get(Identifier::Key(&key2))
2327                .await
2328                .expect("Failed to get data")
2329                .expect("Data not found");
2330            assert_eq!(retrieved, data2);
2331        });
2332    }
2333
2334    #[test_traced]
2335    fn test_archive_prune_keys() {
2336        // Initialize the deterministic context
2337        let executor = deterministic::Runner::default();
2338        executor.start(|context| async move {
2339            // Initialize the archive
2340            let cfg = Config {
2341                translator: FourCap,
2342                metadata_partition: "test-metadata".into(),
2343                key_partition: "test-index".into(),
2344                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2345                value_partition: "test-value".into(),
2346                codec_config: (),
2347                compression: None,
2348                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2349                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2350                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2351                items_per_section: NZU64!(1), // no mask - each item is its own section
2352            };
2353            let mut archive = Archive::init(context.child("storage"), cfg.clone())
2354                .await
2355                .expect("Failed to initialize archive");
2356
2357            // Insert multiple keys across different sections
2358            let keys = vec![
2359                (1u64, test_key("key1-blah"), 1),
2360                (2u64, test_key("key2-blah"), 2),
2361                (3u64, test_key("key3-blah"), 3),
2362                (4u64, test_key("key3-bleh"), 3),
2363                (5u64, test_key("key4-blah"), 4),
2364            ];
2365
2366            for (index, key, data) in &keys {
2367                archive = archive
2368                    .put(*index, key.clone(), data)
2369                    .await
2370                    .expect("Failed to put data");
2371            }
2372
2373            // Check metrics
2374            let buffer = context.encode();
2375            assert!(has_metric_value(&buffer, "items_tracked", 5));
2376
2377            // Prune sections less than 3
2378            archive = archive.prune(3).await.expect("Failed to prune");
2379
2380            // Ensure keys 1 and 2 are no longer present
2381            for (index, key, data) in keys {
2382                let retrieved = archive
2383                    .get(Identifier::Key(&key))
2384                    .await
2385                    .expect("Failed to get data");
2386                if index < 3 {
2387                    assert!(retrieved.is_none());
2388                } else {
2389                    assert_eq!(retrieved.expect("Data not found"), data);
2390                }
2391            }
2392
2393            // Check metrics
2394            let buffer = context.encode();
2395            assert!(has_metric_value(&buffer, "items_tracked", 3));
2396            assert!(has_metric_value(&buffer, "indices_pruned_total", 2));
2397            assert!(has_metric_value(&buffer, "keys_tracked", 3));
2398
2399            // Try to prune older section
2400            archive = archive.prune(2).await.expect("Failed to prune");
2401
2402            // Try to prune current section again
2403            archive = archive.prune(3).await.expect("Failed to prune");
2404
2405            // Storing a key that shares a pruned key's translated key adds one tracked key
2406            archive = archive
2407                .put(6, test_key("key2-blfh"), &5)
2408                .await
2409                .expect("Failed to put data");
2410
2411            // Check metrics
2412            let buffer = context.encode();
2413            assert!(has_metric_value(&buffer, "items_tracked", 4));
2414            assert!(has_metric_value(&buffer, "indices_pruned_total", 2));
2415            assert!(has_metric_value(&buffer, "keys_tracked", 4));
2416
2417            // A put below the prune floor is satisfied without storing
2418            let archive = archive
2419                .put(1, test_key("key1-blah"), &1)
2420                .await
2421                .expect("Failed to put below floor");
2422            assert_eq!(
2423                archive
2424                    .get(Identifier::Key(&test_key("key1-blah")))
2425                    .await
2426                    .expect("Failed to get data"),
2427                None
2428            );
2429
2430            // With no earlier pending writes, the below-floor sync combinators complete without
2431            // storing the pruned item.
2432            let (archive, handle) = archive
2433                .put_start_sync(1, test_key("key1-blah"), &1)
2434                .await
2435                .expect("Failed to put_start_sync below floor");
2436            handle.await.expect("handle must resolve");
2437            let archive = archive
2438                .put_sync(2, test_key("key2-blfh"), &2)
2439                .await
2440                .expect("Failed to put_sync below floor");
2441            assert_eq!(archive.get(Identifier::Index(1)).await.unwrap(), None);
2442            assert_eq!(archive.get(Identifier::Index(2)).await.unwrap(), None);
2443        });
2444    }
2445
2446    fn test_archive_keys_and_restart(num_keys: usize) -> String {
2447        // Initialize the deterministic context
2448        let executor = deterministic::Runner::default();
2449        executor.start(|mut context| async move {
2450            // Initialize the archive
2451            let items_per_section = 256u64;
2452            let cfg = Config {
2453                translator: TwoCap,
2454                metadata_partition: "test-metadata".into(),
2455                key_partition: "test-index".into(),
2456                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2457                value_partition: "test-value".into(),
2458                codec_config: (),
2459                compression: None,
2460                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2461                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2462                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2463                items_per_section: NZU64!(items_per_section),
2464            };
2465            let mut archive = Archive::init(
2466                context.child("init").with_attribute("index", 1),
2467                cfg.clone(),
2468            )
2469            .await
2470            .expect("Failed to initialize archive");
2471
2472            // Insert multiple keys across different sections
2473            let mut keys = BTreeMap::new();
2474            while keys.len() < num_keys {
2475                let index = keys.len() as u64;
2476                let mut key = [0u8; 64];
2477                context.fill(&mut key);
2478                let key = FixedBytes::new(key);
2479                let mut data = [0u8; 1024];
2480                context.fill(&mut data);
2481                let data = FixedBytes::new(data);
2482
2483                archive = archive
2484                    .put(index, key.clone(), &data)
2485                    .await
2486                    .expect("Failed to put data");
2487                keys.insert(key, (index, data));
2488            }
2489
2490            // Ensure all keys can be retrieved
2491            for (key, (index, data)) in &keys {
2492                let retrieved = archive
2493                    .get(Identifier::Index(*index))
2494                    .await
2495                    .expect("Failed to get data")
2496                    .expect("Data not found");
2497                assert_eq!(&retrieved, data);
2498                let retrieved = archive
2499                    .get(Identifier::Key(key))
2500                    .await
2501                    .expect("Failed to get data")
2502                    .expect("Data not found");
2503                assert_eq!(&retrieved, data);
2504            }
2505
2506            // Check metrics
2507            let buffer = context.encode();
2508            assert!(has_metric_value(&buffer, "items_tracked", num_keys));
2509            assert!(has_metric_value(&buffer, "keys_tracked", num_keys));
2510
2511            // Sync and drop the archive
2512            let archive = archive.sync().await.expect("Failed to sync archive");
2513            drop(archive);
2514
2515            // Reinitialize the archive
2516            let cfg = Config {
2517                translator: TwoCap,
2518                metadata_partition: "test-metadata".into(),
2519                key_partition: "test-index".into(),
2520                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2521                value_partition: "test-value".into(),
2522                codec_config: (),
2523                compression: None,
2524                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2525                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2526                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2527                items_per_section: NZU64!(items_per_section),
2528            };
2529            let mut archive = Archive::<_, _, _, FixedBytes<1024>>::init(
2530                context.child("init").with_attribute("index", 2),
2531                cfg.clone(),
2532            )
2533            .await
2534            .expect("Failed to initialize archive");
2535
2536            // Ensure all keys can be retrieved
2537            for (key, (index, data)) in &keys {
2538                let retrieved = archive
2539                    .get(Identifier::Index(*index))
2540                    .await
2541                    .expect("Failed to get data")
2542                    .expect("Data not found");
2543                assert_eq!(&retrieved, data);
2544                let retrieved = archive
2545                    .get(Identifier::Key(key))
2546                    .await
2547                    .expect("Failed to get data")
2548                    .expect("Data not found");
2549                assert_eq!(&retrieved, data);
2550            }
2551
2552            // Prune first half
2553            let min = (keys.len() / 2) as u64;
2554            archive = archive.prune(min).await.expect("Failed to prune");
2555
2556            // Ensure all keys can be retrieved that haven't been pruned
2557            let min = (min / items_per_section) * items_per_section;
2558            let mut removed = 0;
2559            for (key, (index, data)) in keys {
2560                if index >= min {
2561                    let retrieved = archive
2562                        .get(Identifier::Key(&key))
2563                        .await
2564                        .expect("Failed to get data")
2565                        .expect("Data not found");
2566                    assert_eq!(retrieved, data);
2567
2568                    // Check range
2569                    let (current_end, start_next) = archive.next_gap(index);
2570                    assert_eq!(current_end.unwrap(), num_keys as u64 - 1);
2571                    assert!(start_next.is_none());
2572                } else {
2573                    let retrieved = archive
2574                        .get(Identifier::Key(&key))
2575                        .await
2576                        .expect("Failed to get data");
2577                    assert!(retrieved.is_none());
2578                    removed += 1;
2579
2580                    // Check range
2581                    let (current_end, start_next) = archive.next_gap(index);
2582                    assert!(current_end.is_none());
2583                    assert_eq!(start_next.unwrap(), min);
2584                }
2585            }
2586
2587            // Check metrics
2588            let buffer = context.encode();
2589            assert!(has_metric_value(
2590                &buffer,
2591                "items_tracked",
2592                num_keys - removed
2593            ));
2594            assert!(has_metric_value(&buffer, "indices_pruned_total", removed));
2595            assert!(has_metric_value(
2596                &buffer,
2597                "keys_tracked",
2598                num_keys - removed
2599            ));
2600
2601            context.auditor().state()
2602        })
2603    }
2604
2605    #[test_group("slow")]
2606    #[test_traced]
2607    fn test_archive_many_keys_and_restart() {
2608        test_archive_keys_and_restart(100_000);
2609    }
2610
2611    #[test_group("slow")]
2612    #[test_traced]
2613    fn test_determinism() {
2614        let state1 = test_archive_keys_and_restart(5_000);
2615        let state2 = test_archive_keys_and_restart(5_000);
2616        assert_eq!(state1, state2);
2617    }
2618
2619    /// Regression: when the same key is stored at multiple indices and the
2620    /// earlier index is pruned, a subsequent `get`/`has` by key must resolve
2621    /// to the surviving, non-pruned entry rather than report the pruned one.
2622    /// Callers such as consensus's marshal cache rely on this to retain a
2623    /// reproposal of the same block at a later index even after the
2624    /// earlier index's retention window closes.
2625    #[test_traced]
2626    fn test_archive_key_lookup_skips_pruned_duplicates() {
2627        let executor = deterministic::Runner::default();
2628        executor.start(|context| async move {
2629            let cfg = Config {
2630                translator: FourCap,
2631                metadata_partition: "test-metadata".into(),
2632                key_partition: "test-index".into(),
2633                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2634                value_partition: "test-value".into(),
2635                codec_config: (),
2636                compression: None,
2637                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2638                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2639                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2640                items_per_section: NZU64!(1),
2641            };
2642            let mut archive = Archive::init(context.child("storage"), cfg)
2643                .await
2644                .expect("Failed to initialize archive");
2645
2646            // Same key stored at two different indices. Distinct values only
2647            // to make it observable which entry wins; a real caller would
2648            // store the same value (e.g. the same block) at both indices.
2649            let key = test_key("dupe-key");
2650            archive = archive.put(2, key.clone(), &20).await.unwrap();
2651            archive = archive.put(5, key.clone(), &50).await.unwrap();
2652
2653            // Before pruning, either entry is a permitted answer per the
2654            // trait contract. The implementation happens to return the
2655            // earlier index, but we only assert a value is present.
2656            assert!(archive.get(Identifier::Key(&key)).await.unwrap().is_some());
2657            assert!(archive.has(Identifier::Key(&key)).await.unwrap());
2658
2659            // Prune the earlier index (section 2). The later index must be
2660            // the sole surviving answer.
2661            archive = archive.prune(3).await.unwrap();
2662            let got = archive.get(Identifier::Key(&key)).await.unwrap();
2663            assert_eq!(
2664                got,
2665                Some(50),
2666                "key lookup must skip the pruned entry and return the surviving one"
2667            );
2668            assert!(archive.has(Identifier::Key(&key)).await.unwrap());
2669
2670            // Prune past the later index too, so nothing survives.
2671            let archive = archive.prune(6).await.unwrap();
2672            assert_eq!(archive.get(Identifier::Key(&key)).await.unwrap(), None);
2673            assert!(!archive.has(Identifier::Key(&key)).await.unwrap());
2674        });
2675    }
2676
2677    #[test_traced]
2678    fn test_get_all_after_prune() {
2679        let executor = deterministic::Runner::default();
2680        executor.start(|context| async move {
2681            let cfg = Config {
2682                translator: FourCap,
2683                metadata_partition: "test-metadata".into(),
2684                key_partition: "test-index".into(),
2685                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2686                value_partition: "test-value".into(),
2687                codec_config: (),
2688                compression: None,
2689                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2690                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2691                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2692                items_per_section: NZU64!(1),
2693            };
2694            let mut archive = Archive::init(context.child("storage"), cfg)
2695                .await
2696                .expect("Failed to initialize archive");
2697
2698            archive = archive.put_multi(1, test_key("aaa"), &10).await.unwrap();
2699            archive = archive.put_multi(1, test_key("bbb"), &20).await.unwrap();
2700            archive = archive.put_multi(3, test_key("ccc"), &30).await.unwrap();
2701
2702            // Prune below index 3
2703            let archive = archive.prune(3).await.unwrap();
2704
2705            // Pruned index returns None
2706            let all = archive.get_all(1).await.unwrap();
2707            assert_eq!(all, None);
2708
2709            // Surviving index still works
2710            let all = archive.get_all(3).await.unwrap();
2711            assert_eq!(all, Some(vec![30]));
2712        });
2713    }
2714
2715    #[test_traced]
2716    fn test_has_at() {
2717        let executor = deterministic::Runner::default();
2718        let (_, checkpoint) = executor.start_and_recover(|context| async move {
2719            let cfg = test_config("test", NZU64!(2), FourCap);
2720            let mut archive = Archive::init(context.child("storage"), cfg)
2721                .await
2722                .expect("Failed to initialize archive");
2723
2724            // Vacant index
2725            assert!(!archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2726
2727            // Exact key at the index
2728            archive = archive.put_multi(1, test_key("aaaa1"), &10).await.unwrap();
2729            assert!(archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2730
2731            // Same key is not reported at other indices
2732            assert!(!archive.has_at(2, &test_key("aaaa1")).await.unwrap());
2733
2734            // A translated-key collision (FourCap shares the "aaaa" prefix)
2735            // must not produce a false positive
2736            assert!(!archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2737
2738            // A second entry at the same index is visible alongside the first
2739            archive = archive.put_multi(1, test_key("aaaa2"), &20).await.unwrap();
2740            assert!(archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2741            assert!(archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2742
2743            // A different key at an occupied index is absent
2744            assert!(!archive.has_at(1, &test_key("bbbb")).await.unwrap());
2745
2746            archive = archive.put_multi(3, test_key("cccc"), &30).await.unwrap();
2747            archive.sync().await.unwrap();
2748        });
2749
2750        deterministic::Runner::from(checkpoint).start(|context| async move {
2751            let cfg = test_config("test", NZU64!(2), FourCap);
2752            let archive = Archive::<_, _, FixedBytes<64>, i32>::init(context.child("reopen"), cfg)
2753                .await
2754                .expect("Failed to reopen archive");
2755
2756            // Replay rebuilds both entries at the shared index
2757            assert!(archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2758            assert!(archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2759            assert!(!archive.has_at(1, &test_key("bbbb")).await.unwrap());
2760
2761            // Pruned indices report absent
2762            let archive = archive.prune(2).await.unwrap();
2763            assert!(!archive.has_at(1, &test_key("aaaa1")).await.unwrap());
2764            assert!(!archive.has_at(1, &test_key("aaaa2")).await.unwrap());
2765            assert!(archive.has_at(3, &test_key("cccc")).await.unwrap());
2766
2767            archive.destroy().await.unwrap();
2768        });
2769    }
2770
2771    #[test_traced]
2772    fn test_has_key() {
2773        let executor = deterministic::Runner::default();
2774        executor.start(|context| async move {
2775            let cfg = test_config("test", NZU64!(2), FourCap);
2776            let mut archive = Archive::init(context.child("storage"), cfg)
2777                .await
2778                .expect("Failed to initialize archive");
2779
2780            // Absent key
2781            let key = test_key("aaaa1");
2782            assert!(!archive.has(Identifier::Key(&key)).await.unwrap());
2783
2784            // Exact key
2785            archive = archive.put(1, key.clone(), &10).await.unwrap();
2786            assert!(archive.has(Identifier::Key(&key)).await.unwrap());
2787
2788            // A translated-key collision (FourCap shares the "aaaa" prefix)
2789            // must not produce a false positive
2790            let collision = test_key("aaaa2");
2791            assert!(!archive.has(Identifier::Key(&collision)).await.unwrap());
2792            archive = archive.put(2, collision.clone(), &20).await.unwrap();
2793            assert!(archive.has(Identifier::Key(&collision)).await.unwrap());
2794
2795            // Pruned keys report absent. Pruning is section-granular
2796            // (items_per_section = 2), so prune at a section boundary that
2797            // drops indices 1 and 2 while retaining index 4.
2798            archive = archive.put(4, test_key("cccc"), &30).await.unwrap();
2799            let archive = archive.prune(4).await.unwrap();
2800            assert!(!archive.has(Identifier::Key(&key)).await.unwrap());
2801            assert!(!archive.has(Identifier::Key(&collision)).await.unwrap());
2802            assert!(
2803                archive
2804                    .has(Identifier::Key(&test_key("cccc")))
2805                    .await
2806                    .unwrap()
2807            );
2808
2809            archive.destroy().await.unwrap();
2810        });
2811    }
2812
2813    #[test_traced]
2814    fn test_put_multi_prune() {
2815        let executor = deterministic::Runner::default();
2816        executor.start(|context| async move {
2817            let cfg = Config {
2818                translator: FourCap,
2819                metadata_partition: "test-metadata".into(),
2820                key_partition: "test-index".into(),
2821                key_page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
2822                value_partition: "test-value".into(),
2823                codec_config: (),
2824                compression: None,
2825                key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2826                value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2827                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2828                items_per_section: NZU64!(1),
2829            };
2830            let mut archive = Archive::init(context.child("storage"), cfg)
2831                .await
2832                .expect("Failed to initialize archive");
2833
2834            // Two items at index 1, one at index 3
2835            archive = archive.put_multi(1, test_key("aaa"), &10).await.unwrap();
2836            archive = archive.put_multi(1, test_key("bbb"), &20).await.unwrap();
2837            archive = archive.put_multi(3, test_key("ccc"), &30).await.unwrap();
2838
2839            let buffer = context.encode();
2840            assert!(has_metric_value(&buffer, "items_tracked", 2));
2841
2842            // Prune below index 3
2843            let archive = archive.prune(3).await.unwrap();
2844
2845            // Both items at index 1 are gone
2846            assert_eq!(
2847                archive
2848                    .get(Identifier::Key(&test_key("aaa")))
2849                    .await
2850                    .unwrap(),
2851                None
2852            );
2853            assert_eq!(
2854                archive
2855                    .get(Identifier::Key(&test_key("bbb")))
2856                    .await
2857                    .unwrap(),
2858                None
2859            );
2860
2861            // Item at index 3 survives
2862            assert_eq!(
2863                archive
2864                    .get(Identifier::Key(&test_key("ccc")))
2865                    .await
2866                    .unwrap(),
2867                Some(30)
2868            );
2869
2870            let buffer = context.encode();
2871            assert!(has_metric_value(&buffer, "items_tracked", 1));
2872            assert!(has_metric_value(&buffer, "indices_pruned_total", 1));
2873
2874            // put_multi below the prune floor is satisfied without storing
2875            let archive = archive
2876                .put_multi(2, test_key("ddd"), &40)
2877                .await
2878                .expect("Failed to put below floor");
2879            assert_eq!(
2880                archive
2881                    .get(Identifier::Key(&test_key("ddd")))
2882                    .await
2883                    .expect("Failed to get data"),
2884                None
2885            );
2886
2887            // With no earlier pending writes, put_multi_start_sync below the prune floor returns
2888            // a ready handle without storing the pruned item.
2889            let (archive, handle) = archive
2890                .put_multi_start_sync(2, test_key("ddd"), &41)
2891                .await
2892                .expect("Failed to put_multi_start_sync below floor");
2893            handle.await.expect("handle must resolve");
2894            assert_eq!(archive.get_all(2).await.expect("Failed to get data"), None);
2895
2896            // put_multi_sync below the prune floor stores nothing.
2897            let archive = archive
2898                .put_multi_sync(2, test_key("ddd"), &42)
2899                .await
2900                .expect("Failed to put_multi_sync below floor");
2901            assert_eq!(archive.get_all(2).await.expect("Failed to get data"), None);
2902        });
2903    }
2904
2905    #[test_traced]
2906    fn test_prune_retires_keys() {
2907        let executor = deterministic::Runner::default();
2908        executor.start(|context| async move {
2909            // Each key has its own EightCap translation, so `keys_tracked` equals the retained
2910            // records only if prune removed every pruned record's handle.
2911            let cfg = test_config("test", NZU64!(4), EightCap);
2912            let mut archive = Archive::init(context.child("storage"), cfg.clone())
2913                .await
2914                .expect("Failed to initialize archive");
2915            let key = |index: u64| test_key(&format!("{index:04x}"));
2916            for batch in 0..32u64 {
2917                let first = batch * 16;
2918                for index in first..first + 16 {
2919                    archive = archive
2920                        .put(index, key(index), &(index as i32))
2921                        .await
2922                        .expect("Failed to put data");
2923                }
2924                archive = archive.prune(first).await.expect("Failed to prune");
2925
2926                // The key index holds exactly the retained window.
2927                let buffer = context.encode();
2928                assert!(has_metric_value(&buffer, "items_tracked", 16));
2929                assert!(has_metric_value(&buffer, "keys_tracked", 16));
2930                assert_eq!(
2931                    archive.get(Identifier::Key(&key(first))).await.unwrap(),
2932                    Some(first as i32)
2933                );
2934                if first > 0 {
2935                    assert_eq!(
2936                        archive.get(Identifier::Key(&key(first - 1))).await.unwrap(),
2937                        None
2938                    );
2939                }
2940
2941                // Pruning within the retained section changes nothing.
2942                archive = archive.prune(first + 1).await.expect("Failed to prune");
2943                assert!(has_metric_value(&context.encode(), "keys_tracked", 16));
2944            }
2945
2946            // Restart rebuilds the retained window, and pruning everything empties it.
2947            archive.sync().await.expect("Failed to sync archive");
2948            let archive = Archive::init(context.child("reopened"), cfg)
2949                .await
2950                .expect("Failed to initialize archive");
2951            let buffer = context.encode();
2952            assert!(has_metric_value(&buffer, "reopened_keys_tracked", 16));
2953            assert_eq!(
2954                archive.get(Identifier::Key(&key(511))).await.unwrap(),
2955                Some(511)
2956            );
2957            assert_eq!(archive.get(Identifier::Key(&key(495))).await.unwrap(), None);
2958            let archive = archive.prune(512).await.expect("Failed to prune");
2959            let buffer = context.encode();
2960            assert!(has_metric_value(&buffer, "reopened_items_tracked", 0));
2961            assert!(has_metric_value(&buffer, "reopened_keys_tracked", 0));
2962            assert_eq!(archive.get(Identifier::Key(&key(511))).await.unwrap(), None);
2963        });
2964    }
2965
2966    #[test_traced]
2967    fn test_prune_keeps_colliding_retained_keys() {
2968        let executor = deterministic::Runner::default();
2969        executor.start(|context| async move {
2970            // Each pruned key shares its translated key with a retained record, whether another
2971            // key, a put_multi value, or the same key stored on both sides of the floor.
2972            let cfg = test_config("test", NZU64!(4), FourCap);
2973            let mut archive = Archive::init(context.child("storage"), cfg.clone())
2974                .await
2975                .expect("Failed to initialize archive");
2976            archive = archive.put(1, test_key("head-pruned"), &10).await.unwrap();
2977            archive = archive.put(5, test_key("head-kept"), &50).await.unwrap();
2978            archive = archive
2979                .put_multi(5, test_key("tail-kept"), &51)
2980                .await
2981                .unwrap();
2982            archive = archive.put(2, test_key("tail-pruned"), &20).await.unwrap();
2983            archive = archive.put_multi(1, test_key("same"), &60).await.unwrap();
2984            archive = archive.put_multi(5, test_key("same"), &61).await.unwrap();
2985            archive = archive.prune(7).await.expect("Failed to prune");
2986
2987            let expected = [
2988                ("head-kept", Some(50)),
2989                ("tail-kept", Some(51)),
2990                ("same", Some(61)),
2991                ("head-pruned", None),
2992                ("tail-pruned", None),
2993            ];
2994            assert_eq!(archive.get_all(5).await.unwrap(), Some(vec![50, 51, 61]));
2995            for (key, value) in expected {
2996                assert_eq!(
2997                    archive.get(Identifier::Key(&test_key(key))).await.unwrap(),
2998                    value,
2999                    "{key}"
3000                );
3001            }
3002            let buffer = context.encode();
3003            assert!(has_metric_value(&buffer, "items_tracked", 1));
3004            assert!(has_metric_value(&buffer, "keys_tracked", 3));
3005            assert!(has_metric_value(&buffer, "indices_pruned_total", 2));
3006
3007            // The retained entries survive a restart.
3008            archive.sync().await.expect("Failed to sync archive");
3009            let archive = Archive::init(context.child("reopened"), cfg)
3010                .await
3011                .expect("Failed to initialize archive");
3012            assert_eq!(archive.get_all(5).await.unwrap(), Some(vec![50, 51, 61]));
3013            for (key, value) in expected {
3014                assert_eq!(
3015                    archive.get(Identifier::Key(&test_key(key))).await.unwrap(),
3016                    value,
3017                    "{key}"
3018                );
3019            }
3020            assert!(has_metric_value(
3021                &context.encode(),
3022                "reopened_keys_tracked",
3023                3
3024            ));
3025        });
3026    }
3027
3028    #[test_traced]
3029    fn test_positions_match_model() {
3030        let executor = deterministic::Runner::default();
3031        executor.start(|context| async move {
3032            // Offsets arrive in order at first, then spread out, then fill in. Positions count
3033            // every step, so repeats of an offset keep its first position.
3034            let mut rng = context;
3035            let mut positions = storage::Positions::new();
3036            let mut model = BTreeMap::new();
3037            for step in 0..4_000u64 {
3038                let offset = match step {
3039                    0..=99 => step,
3040                    100..=199 => rng.random_range(0..100_000),
3041                    _ => rng.random_range(0..2_000),
3042                };
3043                let inserted = positions.insert(offset, step);
3044                assert_eq!(inserted, !model.contains_key(&offset));
3045                if inserted {
3046                    model.insert(offset, step);
3047                }
3048                assert_eq!(positions.len(), model.len() as u64);
3049                for probe in [offset, offset + 1, rng.random_range(0..100_000)] {
3050                    assert_eq!(positions.get(probe), model.get(&probe).copied(), "{probe}");
3051                }
3052            }
3053        });
3054    }
3055
3056    #[test_traced]
3057    fn test_out_of_order_section() {
3058        let executor = deterministic::Runner::default();
3059        executor.start(|context| async move {
3060            // One wide section whose puts arrive in order, then with gaps, out of order, far
3061            // apart, and as duplicates.
3062            let cfg = test_config("test", NZU64!(1 << 20), FourCap);
3063            let mut archive = Archive::init(context.child("storage"), cfg.clone())
3064                .await
3065                .expect("Failed to initialize archive");
3066            let indices = [0u64, 1, 2, 10, 5_000, 3, 7, 4];
3067            for index in indices {
3068                archive = archive
3069                    .put(index, test_key(&format!("k{index}")), &(index as i32))
3070                    .await
3071                    .expect("Failed to put data");
3072            }
3073            archive = archive
3074                .put_multi(7, test_key("k7-dup"), &70)
3075                .await
3076                .expect("Failed to put_multi");
3077            archive = archive
3078                .put(6, test_key("k6"), &6)
3079                .await
3080                .expect("Failed to put data");
3081
3082            let check = |archive: Archive<_, _, _, i32>| async move {
3083                for index in indices.into_iter().chain([6]) {
3084                    assert_eq!(
3085                        archive.get(Identifier::Index(index)).await.unwrap(),
3086                        Some(index as i32),
3087                        "{index}"
3088                    );
3089                    assert_eq!(
3090                        archive
3091                            .get(Identifier::Key(&test_key(&format!("k{index}"))))
3092                            .await
3093                            .unwrap(),
3094                        Some(index as i32),
3095                        "{index}"
3096                    );
3097                }
3098                for index in [5, 8, 9, 11, 4_999, 5_001] {
3099                    assert_eq!(archive.get(Identifier::Index(index)).await.unwrap(), None);
3100                    assert!(!archive.has(Identifier::Index(index)).await.unwrap());
3101                }
3102                assert_eq!(archive.get_all(7).await.unwrap(), Some(vec![7, 70]));
3103                assert!(archive.has_at(7, &test_key("k7-dup")).await.unwrap());
3104                assert!(!archive.has_at(7, &test_key("k6")).await.unwrap());
3105                assert!(!archive.has_at(8, &test_key("k7")).await.unwrap());
3106                assert_eq!(archive.next_gap(0), (Some(4), Some(6)));
3107                assert_eq!(archive.next_gap(6), (Some(7), Some(10)));
3108                archive
3109            };
3110            let archive = check(archive).await;
3111            let buffer = context.encode();
3112            assert!(has_metric_value(&buffer, "items_tracked", 9));
3113            assert!(has_metric_value(&buffer, "keys_tracked", 10));
3114
3115            // The same view is rebuilt from the journal.
3116            archive.sync().await.expect("Failed to sync archive");
3117            let archive = Archive::init(context.child("reopened"), cfg)
3118                .await
3119                .expect("Failed to initialize archive");
3120            let buffer = context.encode();
3121            assert!(has_metric_value(&buffer, "reopened_items_tracked", 9));
3122            assert!(has_metric_value(&buffer, "reopened_keys_tracked", 10));
3123            check(archive).await;
3124        });
3125    }
3126
3127    #[test_traced]
3128    fn test_init_rejects_changed_items_per_section() {
3129        let executor = deterministic::Runner::default();
3130        executor.start(|context| async move {
3131            // Records written with one section size land in sections another size never selects.
3132            let mut archive = Archive::init(
3133                context.child("storage"),
3134                test_config("test", NZU64!(16), FourCap),
3135            )
3136            .await
3137            .expect("Failed to initialize archive");
3138            for index in 0..40u64 {
3139                archive = archive
3140                    .put(index, test_key(&format!("k{index}")), &(index as i32))
3141                    .await
3142                    .expect("Failed to put data");
3143            }
3144            archive.sync().await.expect("Failed to sync archive");
3145
3146            let result = Archive::<_, _, FixedBytes<64>, i32>::init(
3147                context.child("reopened"),
3148                test_config("test", NZU64!(32), FourCap),
3149            )
3150            .await;
3151            assert!(matches!(result, Err(Error::RecordCorrupted)));
3152        });
3153    }
3154}