ant_node/payment/
cache.rs1use lru::LruCache;
7use parking_lot::Mutex;
8use std::num::NonZeroUsize;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11
12pub use super::quote::XorName;
13
14const DEFAULT_CACHE_CAPACITY: usize = 100_000;
16
17#[derive(Clone)]
22pub struct VerifiedCache {
23 inner: Arc<Mutex<LruCache<XorName, ()>>>,
24 hits: Arc<AtomicU64>,
25 misses: Arc<AtomicU64>,
26 additions: Arc<AtomicU64>,
27}
28
29#[derive(Debug, Default, Clone, Copy)]
31pub struct CacheStats {
32 pub hits: u64,
34 pub misses: u64,
36 pub additions: u64,
38}
39
40impl CacheStats {
41 #[must_use]
43 #[allow(clippy::cast_precision_loss)]
44 pub fn hit_rate(&self) -> f64 {
45 let total = self.hits + self.misses;
46 if total == 0 {
47 0.0
48 } else {
49 (self.hits as f64 / total as f64) * 100.0
50 }
51 }
52}
53
54impl VerifiedCache {
55 #[must_use]
57 pub fn new() -> Self {
58 Self::with_capacity(DEFAULT_CACHE_CAPACITY)
59 }
60
61 #[must_use]
65 pub fn with_capacity(capacity: usize) -> Self {
66 let effective_capacity = capacity.max(1);
68 let cap = NonZeroUsize::new(effective_capacity).unwrap_or(NonZeroUsize::MIN);
71 Self {
72 inner: Arc::new(Mutex::new(LruCache::new(cap))),
73 hits: Arc::new(AtomicU64::new(0)),
74 misses: Arc::new(AtomicU64::new(0)),
75 additions: Arc::new(AtomicU64::new(0)),
76 }
77 }
78
79 #[must_use]
83 pub fn contains(&self, xorname: &XorName) -> bool {
84 let found = self.inner.lock().get(xorname).is_some();
85
86 if found {
87 self.hits.fetch_add(1, Ordering::Relaxed);
88 } else {
89 self.misses.fetch_add(1, Ordering::Relaxed);
90 }
91
92 found
93 }
94
95 pub fn insert(&self, xorname: XorName) {
99 self.inner.lock().put(xorname, ());
100 self.additions.fetch_add(1, Ordering::Relaxed);
101 }
102
103 #[must_use]
105 pub fn stats(&self) -> CacheStats {
106 CacheStats {
107 hits: self.hits.load(Ordering::Relaxed),
108 misses: self.misses.load(Ordering::Relaxed),
109 additions: self.additions.load(Ordering::Relaxed),
110 }
111 }
112
113 #[must_use]
115 pub fn len(&self) -> usize {
116 self.inner.lock().len()
117 }
118
119 #[must_use]
121 pub fn is_empty(&self) -> bool {
122 self.inner.lock().is_empty()
123 }
124
125 pub fn clear(&self) {
127 self.inner.lock().clear();
128 }
129}
130
131impl Default for VerifiedCache {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137#[cfg(test)]
138#[allow(clippy::expect_used)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_cache_basic_operations() {
144 let cache = VerifiedCache::new();
145
146 let xorname1 = [1u8; 32];
147 let xorname2 = [2u8; 32];
148
149 assert!(cache.is_empty());
151 assert!(!cache.contains(&xorname1));
152
153 cache.insert(xorname1);
155 assert!(cache.contains(&xorname1));
156 assert!(!cache.contains(&xorname2));
157 assert_eq!(cache.len(), 1);
158
159 cache.insert(xorname2);
161 assert!(cache.contains(&xorname1));
162 assert!(cache.contains(&xorname2));
163 assert_eq!(cache.len(), 2);
164 }
165
166 #[test]
167 fn test_cache_stats() {
168 let cache = VerifiedCache::new();
169 let xorname = [1u8; 32];
170
171 assert!(!cache.contains(&xorname));
173 let stats = cache.stats();
174 assert_eq!(stats.misses, 1);
175 assert_eq!(stats.hits, 0);
176
177 cache.insert(xorname);
179 let stats = cache.stats();
180 assert_eq!(stats.additions, 1);
181
182 assert!(cache.contains(&xorname));
184 let stats = cache.stats();
185 assert_eq!(stats.hits, 1);
186 assert_eq!(stats.misses, 1);
187
188 assert!((stats.hit_rate() - 50.0).abs() < 0.01);
190 }
191
192 #[test]
193 fn test_cache_lru_eviction() {
194 let cache = VerifiedCache::with_capacity(2);
196
197 let xorname1 = [1u8; 32];
198 let xorname2 = [2u8; 32];
199 let xorname3 = [3u8; 32];
200
201 cache.insert(xorname1);
202 cache.insert(xorname2);
203 assert_eq!(cache.len(), 2);
204
205 cache.insert(xorname3);
207 assert_eq!(cache.len(), 2);
208 assert!(!cache.contains(&xorname1)); }
211
212 #[test]
213 fn test_cache_clear() {
214 let cache = VerifiedCache::new();
215
216 cache.insert([1u8; 32]);
217 cache.insert([2u8; 32]);
218 assert_eq!(cache.len(), 2);
219
220 cache.clear();
221 assert!(cache.is_empty());
222 }
223
224 #[test]
225 fn test_with_capacity_zero_defaults_to_one() {
226 let cache = VerifiedCache::with_capacity(0);
227 cache.insert([1u8; 32]);
229 assert_eq!(cache.len(), 1);
230 }
231
232 #[test]
233 fn test_default_impl() {
234 let cache = VerifiedCache::default();
235 assert!(cache.is_empty());
236 cache.insert([1u8; 32]);
237 assert!(cache.contains(&[1u8; 32]));
238 }
239
240 #[test]
241 fn test_hit_rate_zero_total() {
242 let stats = CacheStats::default();
243 assert!(stats.hit_rate().abs() < f64::EPSILON);
244 }
245
246 #[test]
247 fn test_hit_rate_all_hits() {
248 let stats = CacheStats {
249 hits: 10,
250 misses: 0,
251 additions: 0,
252 };
253 assert!((stats.hit_rate() - 100.0).abs() < 0.01);
254 }
255
256 #[test]
257 fn test_hit_rate_all_misses() {
258 let stats = CacheStats {
259 hits: 0,
260 misses: 10,
261 additions: 0,
262 };
263 assert!(stats.hit_rate().abs() < f64::EPSILON);
264 }
265
266 #[test]
267 fn test_clear_does_not_reset_stats() {
268 let cache = VerifiedCache::new();
269 cache.insert([1u8; 32]);
270 let _ = cache.contains(&[1u8; 32]); let _ = cache.contains(&[2u8; 32]); cache.clear();
274
275 let stats = cache.stats();
277 assert_eq!(stats.hits, 1);
278 assert_eq!(stats.misses, 1);
279 assert_eq!(stats.additions, 1);
280 }
281
282 #[test]
283 fn test_concurrent_insert_and_contains() {
284 use std::sync::Arc;
285 use std::thread;
286
287 let cache = Arc::new(VerifiedCache::with_capacity(1000));
288 let mut handles = Vec::new();
289
290 for i in 0..10u8 {
292 let c = cache.clone();
293 handles.push(thread::spawn(move || {
294 let xorname = [i; 32];
295 c.insert(xorname);
296 }));
297 }
298
299 for i in 0..10u8 {
301 let c = cache.clone();
302 handles.push(thread::spawn(move || {
303 let xorname = [i; 32];
304 let _ = c.contains(&xorname);
305 }));
306 }
307
308 for handle in handles {
309 handle.join().expect("thread panicked");
310 }
311
312 assert_eq!(cache.len(), 10);
314 }
315
316 #[test]
317 fn test_cache_stats_copy() {
318 let stats = CacheStats {
319 hits: 5,
320 misses: 3,
321 additions: 8,
322 };
323 let stats2 = stats; assert_eq!(stats.hits, stats2.hits);
325 assert_eq!(stats.misses, stats2.misses);
326 assert_eq!(stats.additions, stats2.additions);
327 }
328}