linera_cache/
unique_value_cache.rs1use std::{hash::Hash, num::NonZeroUsize, sync::Mutex};
7
8use lru::LruCache;
9
10pub struct UniqueValueCache<K, V>
15where
16 K: Hash + Eq,
17{
18 cache: Mutex<LruCache<K, V>>,
19}
20
21impl<K, V> UniqueValueCache<K, V>
22where
23 K: Hash + Eq + Copy,
24{
25 pub fn new(size: usize) -> Self {
27 let size = NonZeroUsize::try_from(size).expect("Cache size must be greater than zero");
28 UniqueValueCache {
29 cache: Mutex::new(LruCache::new(size)),
30 }
31 }
32
33 pub fn insert(&self, key: &K, value: V) -> bool {
37 let mut cache = self.cache.lock().unwrap();
38 if cache.contains(key) {
39 cache.promote(key);
40 false
41 } else {
42 cache.push(*key, value);
43 true
44 }
45 }
46
47 pub fn remove(&self, key: &K) -> Option<V> {
49 self.cache.lock().unwrap().pop(key)
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::UniqueValueCache;
56
57 const TEST_CACHE_SIZE: usize = 10;
58
59 #[test]
60 fn test_retrieve_missing_value() {
61 let cache = UniqueValueCache::<u64, String>::new(TEST_CACHE_SIZE);
62 assert!(cache.remove(&42).is_none());
63 }
64
65 #[test]
66 fn test_insert_and_remove() {
67 let cache = UniqueValueCache::<u64, String>::new(TEST_CACHE_SIZE);
68
69 assert!(cache.insert(&1, "hello".to_string()));
70 assert_eq!(cache.remove(&1), Some("hello".to_string()));
71 assert!(cache.remove(&1).is_none());
73 }
74
75 #[test]
76 fn test_insert_duplicate_returns_false() {
77 let cache = UniqueValueCache::<u64, String>::new(TEST_CACHE_SIZE);
78
79 assert!(cache.insert(&1, "first".to_string()));
80 assert!(!cache.insert(&1, "second".to_string()));
81
82 assert_eq!(cache.remove(&1), Some("first".to_string()));
84 }
85
86 #[test]
87 fn test_insert_many_values() {
88 let cache = UniqueValueCache::<u64, String>::new(TEST_CACHE_SIZE);
89
90 for i in 0..TEST_CACHE_SIZE as u64 {
91 assert!(cache.insert(&i, format!("value-{i}")));
92 }
93
94 for i in 0..TEST_CACHE_SIZE as u64 {
95 assert_eq!(cache.remove(&i), Some(format!("value-{i}")));
96 }
97 }
98
99 #[test]
100 fn test_lru_eviction() {
101 let cache = UniqueValueCache::<u64, String>::new(TEST_CACHE_SIZE);
102
103 for i in 0..TEST_CACHE_SIZE as u64 {
105 cache.insert(&i, format!("value-{i}"));
106 }
107
108 cache.insert(&(TEST_CACHE_SIZE as u64), "extra".to_string());
110
111 assert!(cache.remove(&0).is_none(), "LRU entry should be evicted");
112 assert_eq!(
113 cache.remove(&(TEST_CACHE_SIZE as u64)),
114 Some("extra".to_string()),
115 "newest entry should be present"
116 );
117 }
118
119 #[test]
120 fn test_reinsertion_promotes_entry() {
121 let cache = UniqueValueCache::<u64, String>::new(TEST_CACHE_SIZE);
122
123 for i in 0..TEST_CACHE_SIZE as u64 {
125 cache.insert(&i, format!("value-{i}"));
126 }
127
128 assert!(!cache.insert(&0, "should-be-ignored".to_string()));
130
131 cache.insert(&(TEST_CACHE_SIZE as u64), "extra".to_string());
133
134 assert!(
135 cache.remove(&0).is_some(),
136 "re-inserted entry should survive eviction"
137 );
138 assert!(
139 cache.remove(&1).is_none(),
140 "entry after promoted one should be evicted"
141 );
142 }
143}