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