Skip to content

Commit 3425d43

Browse files
duplicate key
1 parent 08fa5dc commit 3425d43

3 files changed

Lines changed: 81 additions & 14 deletions

File tree

storage/src/archive/immutable/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
//!
66
//! # Uniqueness
77
//!
8-
//! [Archive] assumes all stored indexes and keys are unique. If the same key is associated with
9-
//! multiple `indices`, there is no guarantee which value will be returned. If the key is written to
10-
//! an existing `index`, [Archive] will return an error.
8+
//! Indices are unique: writing to an occupied index is a no-op. Keys may be stored at multiple
9+
//! indices, and a lookup by [crate::archive::Identifier::Key] may return any of the associated
10+
//! values.
1111
//!
1212
//! # Compression
1313
//!

storage/src/archive/mod.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! A write-once key-value store for ordered data.
22
//!
3-
//! [Archive] is a key-value store designed for workloads where all data is written only once and is
4-
//! uniquely associated with both an `index` and a `key`.
3+
//! [Archive] is a key-value store designed for workloads where data is written only once and each
4+
//! item is addressed by both an `index` and a `key`. Indices are unique for [Archive]; duplicate
5+
//! indices can be stored via [MultiArchive]. The same key may be stored at multiple indices in
6+
//! either case, and a key lookup may return any of the associated values.
57
68
use commonware_codec::Codec;
79
use commonware_utils::Array;
@@ -39,18 +41,20 @@ pub enum Error {
3941
RecordTooLarge,
4042
}
4143

42-
/// A write-once key-value store where each key is associated with a unique index.
44+
/// A write-once key-value store addressed by both an index and a key.
4345
pub trait Archive: Send {
4446
/// The type of the key.
4547
type Key: Array;
4648

4749
/// The type of the value.
4850
type Value: Codec + Send;
4951

50-
/// Store an item in [Archive]. Both indices and keys are assumed to both be globally unique.
52+
/// Store an item in [Archive].
5153
///
52-
/// If the index already exists, put does nothing and returns. If the same key is stored multiple times
53-
/// at different indices (not recommended), any value associated with the key may be returned.
54+
/// Indices are unique: if the index already exists, put does nothing and returns. Duplicate
55+
/// indices can be stored via [MultiArchive::put_multi]. Keys need not be unique — the same key
56+
/// may be stored at multiple indices, and a subsequent [Archive::get] or [Archive::has] call
57+
/// with an [Identifier::Key] identifier may return any of the values associated with that key.
5458
fn put(
5559
&mut self,
5660
index: u64,
@@ -122,8 +126,7 @@ pub trait Archive: Send {
122126
///
123127
/// Unlike [Archive::put], which is a no-op when the index already exists,
124128
/// [MultiArchive::put_multi] allows storing additional `(key, value)` pairs
125-
/// at an existing index. As with [Archive::put], keys are assumed to be globally
126-
/// unique, but duplicate keys are not rejected.
129+
/// at an existing index.
127130
pub trait MultiArchive: Archive {
128131
/// Retrieve all values stored at the given index.
129132
///

storage/src/archive/prunable/mod.rs

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
//!
3131
//! # Uniqueness
3232
//!
33-
//! [Archive] assumes all stored indexes and keys are unique. If the same key is associated with
34-
//! multiple `indices`, there is no guarantee which value will be returned. If the key is written to
35-
//! an existing `index`, [Archive] will return an error.
33+
//! Indices are unique for [Archive] — writing to an occupied index is a no-op. Duplicate indices
34+
//! can be stored via [MultiArchive::put_multi]. Keys may be stored at multiple indices with either
35+
//! put variant: a lookup by [Identifier::Key] may return any of the values at that key, with one
36+
//! refinement over the base trait contract — entries whose index has been pruned are never
37+
//! returned or reported as present, so a key that matches both a pruned and a non-pruned entry
38+
//! resolves to the non-pruned entry.
3639
//!
3740
//! ## Conflicts
3841
//!
@@ -681,6 +684,67 @@ mod tests {
681684
assert_eq!(state1, state2);
682685
}
683686

687+
/// Regression: when the same key is stored at multiple indices and the
688+
/// earlier index is pruned, a subsequent `get`/`has` by key must resolve
689+
/// to the surviving, non-pruned entry rather than report the pruned one.
690+
/// Callers such as consensus's marshal cache rely on this to retain a
691+
/// reproposal of the same block at a later index even after the
692+
/// earlier index's retention window closes.
693+
#[test_traced]
694+
fn test_archive_key_lookup_skips_pruned_duplicates() {
695+
let executor = deterministic::Runner::default();
696+
executor.start(|context| async move {
697+
let cfg = Config {
698+
translator: FourCap,
699+
key_partition: "test-index".into(),
700+
key_page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
701+
value_partition: "test-value".into(),
702+
codec_config: (),
703+
compression: None,
704+
key_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
705+
value_write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
706+
replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
707+
items_per_section: NZU64!(1),
708+
};
709+
let mut archive = Archive::init(context.clone(), cfg)
710+
.await
711+
.expect("Failed to initialize archive");
712+
713+
// Same key stored at two different indices. Distinct values only
714+
// to make it observable which entry wins; a real caller would
715+
// store the same value (e.g. the same block) at both indices.
716+
let key = test_key("dupe-key");
717+
archive.put(2, key.clone(), 20).await.unwrap();
718+
archive.put(5, key.clone(), 50).await.unwrap();
719+
720+
// Before pruning, either entry is a permitted answer per the
721+
// trait contract. The implementation happens to return the
722+
// earlier index, but we only assert a value is present.
723+
assert!(archive
724+
.get(Identifier::Key(&key))
725+
.await
726+
.unwrap()
727+
.is_some());
728+
assert!(archive.has(Identifier::Key(&key)).await.unwrap());
729+
730+
// Prune the earlier index (section 2). The later index must be
731+
// the sole surviving answer.
732+
archive.prune(3).await.unwrap();
733+
let got = archive.get(Identifier::Key(&key)).await.unwrap();
734+
assert_eq!(
735+
got,
736+
Some(50),
737+
"key lookup must skip the pruned entry and return the surviving one"
738+
);
739+
assert!(archive.has(Identifier::Key(&key)).await.unwrap());
740+
741+
// Prune past the later index too — now nothing survives.
742+
archive.prune(6).await.unwrap();
743+
assert_eq!(archive.get(Identifier::Key(&key)).await.unwrap(), None);
744+
assert!(!archive.has(Identifier::Key(&key)).await.unwrap());
745+
});
746+
}
747+
684748
#[test_traced]
685749
fn test_get_all_after_prune() {
686750
let executor = deterministic::Runner::default();

0 commit comments

Comments
 (0)