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)]
30pub struct VerifiedCache {
31 inner: Arc<Mutex<LruCache<XorName, bool>>>,
34 hits: Arc<AtomicU64>,
35 misses: Arc<AtomicU64>,
36 additions: Arc<AtomicU64>,
37}
38
39#[derive(Debug, Default, Clone, Copy)]
41pub struct CacheStats {
42 pub hits: u64,
44 pub misses: u64,
46 pub additions: u64,
48}
49
50impl CacheStats {
51 #[must_use]
53 #[allow(clippy::cast_precision_loss)]
54 pub fn hit_rate(&self) -> f64 {
55 let total = self.hits + self.misses;
56 if total == 0 {
57 0.0
58 } else {
59 (self.hits as f64 / total as f64) * 100.0
60 }
61 }
62}
63
64impl VerifiedCache {
65 #[must_use]
67 pub fn new() -> Self {
68 Self::with_capacity(DEFAULT_CACHE_CAPACITY)
69 }
70
71 #[must_use]
75 pub fn with_capacity(capacity: usize) -> Self {
76 let effective_capacity = capacity.max(1);
78 let cap = NonZeroUsize::new(effective_capacity).unwrap_or(NonZeroUsize::MIN);
81 Self {
82 inner: Arc::new(Mutex::new(LruCache::new(cap))),
83 hits: Arc::new(AtomicU64::new(0)),
84 misses: Arc::new(AtomicU64::new(0)),
85 additions: Arc::new(AtomicU64::new(0)),
86 }
87 }
88
89 #[must_use]
95 pub fn contains(&self, xorname: &XorName) -> bool {
96 let found = self.inner.lock().get(xorname).is_some();
97
98 if found {
99 self.hits.fetch_add(1, Ordering::Relaxed);
100 } else {
101 self.misses.fetch_add(1, Ordering::Relaxed);
102 }
103
104 found
105 }
106
107 #[must_use]
113 pub fn contains_client_put_verified(&self, xorname: &XorName) -> bool {
114 let found = self.inner.lock().get(xorname).copied() == Some(true);
115
116 if found {
117 self.hits.fetch_add(1, Ordering::Relaxed);
118 } else {
119 self.misses.fetch_add(1, Ordering::Relaxed);
120 }
121
122 found
123 }
124
125 pub fn insert(&self, xorname: XorName) {
130 self.inner.lock().put(xorname, true);
131 self.additions.fetch_add(1, Ordering::Relaxed);
132 }
133
134 pub fn insert_replication_verified(&self, xorname: XorName) {
141 let added = {
142 let mut inner = self.inner.lock();
143 if inner.get_mut(&xorname).is_none() {
145 inner.put(xorname, false);
146 true
147 } else {
148 false
149 }
150 };
151 if added {
152 self.additions.fetch_add(1, Ordering::Relaxed);
153 }
154 }
155
156 #[must_use]
158 pub fn stats(&self) -> CacheStats {
159 CacheStats {
160 hits: self.hits.load(Ordering::Relaxed),
161 misses: self.misses.load(Ordering::Relaxed),
162 additions: self.additions.load(Ordering::Relaxed),
163 }
164 }
165
166 #[must_use]
168 pub fn len(&self) -> usize {
169 self.inner.lock().len()
170 }
171
172 #[must_use]
174 pub fn is_empty(&self) -> bool {
175 self.inner.lock().is_empty()
176 }
177
178 pub fn clear(&self) {
180 self.inner.lock().clear();
181 }
182}
183
184impl Default for VerifiedCache {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190#[cfg(test)]
191#[allow(clippy::expect_used)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_cache_basic_operations() {
197 let cache = VerifiedCache::new();
198
199 let xorname1 = [1u8; 32];
200 let xorname2 = [2u8; 32];
201
202 assert!(cache.is_empty());
204 assert!(!cache.contains(&xorname1));
205
206 cache.insert(xorname1);
208 assert!(cache.contains(&xorname1));
209 assert!(!cache.contains(&xorname2));
210 assert_eq!(cache.len(), 1);
211
212 cache.insert(xorname2);
214 assert!(cache.contains(&xorname1));
215 assert!(cache.contains(&xorname2));
216 assert_eq!(cache.len(), 2);
217 }
218
219 #[test]
220 fn test_cache_stats() {
221 let cache = VerifiedCache::new();
222 let xorname = [1u8; 32];
223
224 assert!(!cache.contains(&xorname));
226 let stats = cache.stats();
227 assert_eq!(stats.misses, 1);
228 assert_eq!(stats.hits, 0);
229
230 cache.insert(xorname);
232 let stats = cache.stats();
233 assert_eq!(stats.additions, 1);
234
235 assert!(cache.contains(&xorname));
237 let stats = cache.stats();
238 assert_eq!(stats.hits, 1);
239 assert_eq!(stats.misses, 1);
240
241 assert!((stats.hit_rate() - 50.0).abs() < 0.01);
243 }
244
245 #[test]
246 fn test_cache_lru_eviction() {
247 let cache = VerifiedCache::with_capacity(2);
249
250 let xorname1 = [1u8; 32];
251 let xorname2 = [2u8; 32];
252 let xorname3 = [3u8; 32];
253
254 cache.insert(xorname1);
255 cache.insert(xorname2);
256 assert_eq!(cache.len(), 2);
257
258 cache.insert(xorname3);
260 assert_eq!(cache.len(), 2);
261 assert!(!cache.contains(&xorname1)); }
264
265 #[test]
266 fn test_cache_clear() {
267 let cache = VerifiedCache::new();
268
269 cache.insert([1u8; 32]);
270 cache.insert([2u8; 32]);
271 assert_eq!(cache.len(), 2);
272
273 cache.clear();
274 assert!(cache.is_empty());
275 }
276
277 #[test]
278 fn test_with_capacity_zero_defaults_to_one() {
279 let cache = VerifiedCache::with_capacity(0);
280 cache.insert([1u8; 32]);
282 assert_eq!(cache.len(), 1);
283 }
284
285 #[test]
286 fn test_default_impl() {
287 let cache = VerifiedCache::default();
288 assert!(cache.is_empty());
289 cache.insert([1u8; 32]);
290 assert!(cache.contains(&[1u8; 32]));
291 }
292
293 #[test]
294 fn test_hit_rate_zero_total() {
295 let stats = CacheStats::default();
296 assert!(stats.hit_rate().abs() < f64::EPSILON);
297 }
298
299 #[test]
300 fn test_hit_rate_all_hits() {
301 let stats = CacheStats {
302 hits: 10,
303 misses: 0,
304 additions: 0,
305 };
306 assert!((stats.hit_rate() - 100.0).abs() < 0.01);
307 }
308
309 #[test]
310 fn test_hit_rate_all_misses() {
311 let stats = CacheStats {
312 hits: 0,
313 misses: 10,
314 additions: 0,
315 };
316 assert!(stats.hit_rate().abs() < f64::EPSILON);
317 }
318
319 #[test]
320 fn test_clear_does_not_reset_stats() {
321 let cache = VerifiedCache::new();
322 cache.insert([1u8; 32]);
323 let _ = cache.contains(&[1u8; 32]); let _ = cache.contains(&[2u8; 32]); cache.clear();
327
328 let stats = cache.stats();
330 assert_eq!(stats.hits, 1);
331 assert_eq!(stats.misses, 1);
332 assert_eq!(stats.additions, 1);
333 }
334
335 #[test]
336 fn test_concurrent_insert_and_contains() {
337 use std::sync::Arc;
338 use std::thread;
339
340 let cache = Arc::new(VerifiedCache::with_capacity(1000));
341 let mut handles = Vec::new();
342
343 for i in 0..10u8 {
345 let c = cache.clone();
346 handles.push(thread::spawn(move || {
347 let xorname = [i; 32];
348 c.insert(xorname);
349 }));
350 }
351
352 for i in 0..10u8 {
354 let c = cache.clone();
355 handles.push(thread::spawn(move || {
356 let xorname = [i; 32];
357 let _ = c.contains(&xorname);
358 }));
359 }
360
361 for handle in handles {
362 handle.join().expect("thread panicked");
363 }
364
365 assert_eq!(cache.len(), 10);
367 }
368
369 #[test]
370 fn test_cache_stats_copy() {
371 let stats = CacheStats {
372 hits: 5,
373 misses: 3,
374 additions: 8,
375 };
376 let stats2 = stats; assert_eq!(stats.hits, stats2.hits);
378 assert_eq!(stats.misses, stats2.misses);
379 assert_eq!(stats.additions, stats2.additions);
380 }
381}