Skip to content

Commit c43bde5

Browse files
ma2bdclaude
andcommitted
Fix invalidate_*/record_*_unchecked interaction and add tests
- Revert delete_key/delete_prefix to use invalidate + record_unchecked so existing tests catch regressions in record_*_unchecked. - Fix invalidate_key/invalidate_prefix: only remove containing FindKeyValues entries (which affect query_read_value correctness). Keep containing FindKeys entries so record_*_unchecked can surgically update them. - Add unit tests verifying: - invalidate_* does not cache negatively (returns None, not Some(None)) - invalidate_* removes containing FindKeyValues entries - invalidate without followup is safe (cancellation scenario) - invalidate + record round-trip works correctly - delete_prefix records negative results via FindKeyValues - delete_prefix updates containing FindKeys entries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a9aa8b5 commit c43bde5

1 file changed

Lines changed: 221 additions & 25 deletions

File tree

linera-views/src/lru_prefix_cache.rs

Lines changed: 221 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -557,20 +557,16 @@ impl LruPrefixCache {
557557

558558
/// Removes a key's entry from the cache without recording a negative result.
559559
/// Used for invalidation before a write, where we don't yet know the outcome.
560-
/// Also removes any containing FindKeys/FindKeyValues entries that could
561-
/// return stale data for this key.
560+
///
561+
/// Removes containing `FindKeyValues` entries to prevent stale reads via
562+
/// `query_read_value`. Containing `FindKeys` entries are kept — they are
563+
/// only used for key listing (not value reads) and are needed by
564+
/// `record_key_deletion_unchecked` for surgical updates.
562565
pub(crate) fn invalidate_key(&mut self, key: &[u8]) {
563566
let cache_key = CacheKey::Value(key.to_vec());
564567
self.remove_cache_key_if_exists(&cache_key);
565568
if self.has_exclusive_access {
566-
let find_keys_to_remove = self
567-
.get_existing_find_keys_entry(key)
568-
.map(|(lb, _)| lb.to_vec());
569-
if let Some(lower_bound) = find_keys_to_remove {
570-
let cache_key = CacheKey::FindKeys(lower_bound.clone());
571-
self.remove_cache_key_if_exists(&cache_key);
572-
self.find_keys_map.remove(&lower_bound);
573-
}
569+
// Remove containing FindKeyValues entries to prevent stale value reads.
574570
let find_kv_to_remove = self
575571
.get_existing_find_key_values_entry(key)
576572
.map(|(lb, _)| lb.to_vec());
@@ -619,7 +615,6 @@ impl LruPrefixCache {
619615
}
620616

621617
/// Safely record the deletion of a key from the cache.
622-
#[cfg(test)]
623618
pub(crate) fn delete_key(&mut self, key: &[u8]) {
624619
self.invalidate_key(key);
625620
self.record_key_deletion_unchecked(key);
@@ -808,18 +803,10 @@ impl LruPrefixCache {
808803
let cache_key = CacheKey::FindKeyValues(prefix);
809804
self.remove_cache_key(&cache_key);
810805
}
811-
// Remove any containing FindKeys entry. We cannot surgically
812-
// delete sub-keys because that would implicitly assert those keys
813-
// don't exist — which we don't know yet.
814-
let find_keys_to_remove = self
815-
.get_existing_find_keys_entry(key_prefix)
816-
.map(|(lb, _)| lb.to_vec());
817-
if let Some(lower_bound) = find_keys_to_remove {
818-
let cache_key = CacheKey::FindKeys(lower_bound.clone());
819-
self.remove_cache_key_if_exists(&cache_key);
820-
self.find_keys_map.remove(&lower_bound);
821-
}
822-
// Same for containing FindKeyValues entries.
806+
// Remove containing FindKeyValues entries to prevent stale value
807+
// reads. Containing FindKeys entries are kept — they are only used
808+
// for key listing and are needed by record_prefix_deletion_unchecked
809+
// for surgical updates.
823810
let find_kv_to_remove = self
824811
.get_existing_find_key_values_entry(key_prefix)
825812
.map(|(lb, _)| lb.to_vec());
@@ -870,8 +857,7 @@ impl LruPrefixCache {
870857
}
871858
}
872859

873-
/// Safely record the deletion of a key prefix in the cache.
874-
#[cfg(test)]
860+
/// Safely record the deletion of a key prefix from the cache.
875861
pub(crate) fn delete_prefix(&mut self, key_prefix: &[u8]) {
876862
self.invalidate_prefix(key_prefix);
877863
self.record_prefix_deletion_unchecked(key_prefix);
@@ -2674,4 +2660,214 @@ mod tests {
26742660
cache.insert_read_value(&key, &Some(value));
26752661
cache.check_coherence();
26762662
}
2663+
2664+
// --- invalidate_key tests ---
2665+
2666+
#[test]
2667+
fn test_invalidate_key_does_not_cache_negatively() {
2668+
let mut cache = create_test_cache(true);
2669+
let key = vec![1, 2];
2670+
let value = vec![42];
2671+
2672+
// Insert a value, then invalidate it.
2673+
cache.insert_read_value(&key, &Some(value));
2674+
cache.check_coherence();
2675+
assert!(cache.query_read_value(&key).is_some());
2676+
2677+
cache.invalidate_key(&key);
2678+
cache.check_coherence();
2679+
2680+
// After invalidation, the key should be unknown (None), NOT DoesNotExist.
2681+
// None means "not in cache, ask the DB". Some(None) would mean "known to not exist".
2682+
assert_eq!(cache.query_read_value(&key), None);
2683+
}
2684+
2685+
#[test]
2686+
fn test_invalidate_key_removes_containing_find_key_values() {
2687+
let mut cache = create_test_cache(true);
2688+
2689+
// Insert a FindKeyValues entry for a broad prefix that contains our key.
2690+
let prefix = vec![1];
2691+
let key = vec![1, 2];
2692+
let value = vec![42];
2693+
cache.insert_find_key_values(prefix.clone(), &[(vec![2], value.clone())]);
2694+
cache.check_coherence();
2695+
2696+
// The key should be queryable via the containing entry.
2697+
assert_eq!(cache.query_read_value(&key), Some(Some(value)));
2698+
2699+
// Invalidate the specific key.
2700+
cache.invalidate_key(&key);
2701+
cache.check_coherence();
2702+
2703+
// The containing FindKeyValues entry should be removed, so the key
2704+
// is no longer resolvable from cache.
2705+
assert_eq!(cache.query_read_value(&key), None);
2706+
}
2707+
2708+
#[test]
2709+
fn test_invalidate_key_on_nonexistent_key_is_noop() {
2710+
let mut cache = create_test_cache(true);
2711+
let key = vec![1, 2];
2712+
2713+
// Invalidating a key that was never cached should not panic or insert anything.
2714+
cache.invalidate_key(&key);
2715+
cache.check_coherence();
2716+
assert_eq!(cache.query_read_value(&key), None);
2717+
}
2718+
2719+
// --- invalidate_prefix tests ---
2720+
2721+
#[test]
2722+
fn test_invalidate_prefix_does_not_cache_negatively() {
2723+
let mut cache = create_test_cache(true);
2724+
let key1 = vec![1, 2];
2725+
let key2 = vec![1, 3];
2726+
let value = vec![42];
2727+
2728+
cache.insert_read_value(&key1, &Some(value.clone()));
2729+
cache.insert_read_value(&key2, &Some(value.clone()));
2730+
cache.check_coherence();
2731+
2732+
cache.invalidate_prefix(&[1]);
2733+
cache.check_coherence();
2734+
2735+
// Keys should be unknown (None), NOT DoesNotExist (Some(None)).
2736+
assert_eq!(cache.query_read_value(&key1), None);
2737+
assert_eq!(cache.query_read_value(&key2), None);
2738+
}
2739+
2740+
#[test]
2741+
fn test_invalidate_prefix_removes_containing_find_key_values() {
2742+
let mut cache = create_test_cache(true);
2743+
2744+
// Insert a FindKeyValues entry for prefix [1] containing sub-keys.
2745+
let prefix = vec![1];
2746+
let key1 = vec![1, 2, 3];
2747+
let key2 = vec![1, 2, 4];
2748+
let value = vec![42];
2749+
cache.insert_find_key_values(
2750+
prefix.clone(),
2751+
&[(vec![2, 3], value.clone()), (vec![2, 4], value.clone())],
2752+
);
2753+
cache.check_coherence();
2754+
2755+
// Both keys should be queryable via the containing entry.
2756+
assert_eq!(cache.query_read_value(&key1), Some(Some(value.clone())));
2757+
assert_eq!(cache.query_read_value(&key2), Some(Some(value.clone())));
2758+
2759+
// Invalidate a sub-prefix that covers both keys.
2760+
cache.invalidate_prefix(&[1, 2]);
2761+
cache.check_coherence();
2762+
2763+
// The containing entry should be gone — keys are unknown, not negatively cached.
2764+
assert_eq!(cache.query_read_value(&key1), None);
2765+
assert_eq!(cache.query_read_value(&key2), None);
2766+
}
2767+
2768+
#[test]
2769+
fn test_invalidate_prefix_removes_matching_find_keys() {
2770+
let mut cache = create_test_cache(true);
2771+
2772+
let prefix = vec![1, 2];
2773+
let keys = vec![vec![3], vec![4]];
2774+
cache.insert_find_keys(prefix.clone(), &keys);
2775+
cache.check_coherence();
2776+
assert!(cache.query_find_keys(&prefix).is_some());
2777+
2778+
// Invalidate a broader prefix that covers the FindKeys entry.
2779+
cache.invalidate_prefix(&[1]);
2780+
cache.check_coherence();
2781+
2782+
// The FindKeys entry should be completely gone.
2783+
assert_eq!(cache.query_find_keys(&prefix), None);
2784+
}
2785+
2786+
// --- delete_prefix regression tests ---
2787+
2788+
#[test]
2789+
fn test_delete_prefix_records_negative_via_find_key_values() {
2790+
let mut cache = create_test_cache(true);
2791+
let key1 = vec![1, 2];
2792+
let value = vec![42];
2793+
2794+
cache.insert_read_value(&key1, &Some(value));
2795+
cache.check_coherence();
2796+
2797+
// delete_prefix should record the deletion so queries return DoesNotExist.
2798+
cache.delete_prefix(&[1]);
2799+
cache.check_coherence();
2800+
2801+
// Key should be known to not exist (Some(None)), not just unknown (None).
2802+
assert_eq!(cache.query_read_value(&key1), Some(None));
2803+
}
2804+
2805+
#[test]
2806+
fn test_delete_prefix_updates_containing_find_keys_entry() {
2807+
let mut cache = create_test_cache(true);
2808+
2809+
// Insert a FindKeys entry with a broader prefix.
2810+
let find_keys_prefix = vec![1];
2811+
let keys = vec![vec![2, 3], vec![2, 4], vec![3, 5]];
2812+
cache.insert_find_keys(find_keys_prefix.clone(), &keys);
2813+
cache.check_coherence();
2814+
2815+
// Delete a sub-prefix.
2816+
cache.delete_prefix(&[1, 2]);
2817+
cache.check_coherence();
2818+
2819+
// The containing FindKeys entry should have the matching keys removed.
2820+
let remaining = cache.query_find_keys(&find_keys_prefix).unwrap();
2821+
assert!(!remaining.contains(&vec![2, 3]));
2822+
assert!(!remaining.contains(&vec![2, 4]));
2823+
assert!(remaining.contains(&vec![3, 5]));
2824+
}
2825+
2826+
// --- Cancellation safety scenario test ---
2827+
2828+
#[test]
2829+
fn test_invalidate_then_record_does_not_lose_data() {
2830+
// Simulates the write_batch pattern: invalidate before write,
2831+
// record after successful write.
2832+
let mut cache = create_test_cache(true);
2833+
let key = vec![1, 2];
2834+
let old_value = vec![10];
2835+
let new_value = vec![20];
2836+
2837+
// Initial state: key has old value.
2838+
cache.insert_read_value(&key, &Some(old_value.clone()));
2839+
cache.check_coherence();
2840+
assert_eq!(cache.query_read_value(&key), Some(Some(old_value)));
2841+
2842+
// Step 1: invalidate (pre-write).
2843+
cache.invalidate_key(&key);
2844+
cache.check_coherence();
2845+
// Key is now unknown — reads would go to DB.
2846+
assert_eq!(cache.query_read_value(&key), None);
2847+
2848+
// Step 2: write succeeds, record new value.
2849+
cache.put_key_value(&key, &new_value);
2850+
cache.check_coherence();
2851+
assert_eq!(cache.query_read_value(&key), Some(Some(new_value)));
2852+
}
2853+
2854+
#[test]
2855+
fn test_invalidate_without_followup_is_safe() {
2856+
// Simulates cancellation: invalidate runs but the write is cancelled,
2857+
// so no record step happens.
2858+
let mut cache = create_test_cache(true);
2859+
let key = vec![1, 2];
2860+
let value = vec![42];
2861+
2862+
cache.insert_read_value(&key, &Some(value));
2863+
cache.check_coherence();
2864+
2865+
// Invalidate (pre-write), then "cancel" — no record step.
2866+
cache.invalidate_key(&key);
2867+
cache.check_coherence();
2868+
2869+
// Key is unknown — subsequent reads will go to DB and get correct data.
2870+
// Crucially, it must NOT return the old stale value.
2871+
assert_eq!(cache.query_read_value(&key), None);
2872+
}
26772873
}

0 commit comments

Comments
 (0)