|
30 | 30 | //! |
31 | 31 | //! # Uniqueness |
32 | 32 | //! |
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. |
36 | 39 | //! |
37 | 40 | //! ## Conflicts |
38 | 41 | //! |
@@ -681,6 +684,67 @@ mod tests { |
681 | 684 | assert_eq!(state1, state2); |
682 | 685 | } |
683 | 686 |
|
| 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 | + |
684 | 748 | #[test_traced] |
685 | 749 | fn test_get_all_after_prune() { |
686 | 750 | let executor = deterministic::Runner::default(); |
|
0 commit comments