Skip to main content

saorsa_node/payment/
cache.rs

1//! LRU cache for verified `XorName` values.
2//!
3//! Caches `XorName` values that have been verified to exist on the autonomi network,
4//! reducing the number of network queries needed for repeated/popular data.
5
6use lru::LruCache;
7use parking_lot::Mutex;
8use std::num::NonZeroUsize;
9use std::sync::Arc;
10
11/// `XorName` type - 32-byte content hash.
12/// TODO: Import from saorsa-core or ant-protocol when available.
13pub type XorName = [u8; 32];
14
15/// Default cache capacity (100,000 entries = 3.2MB memory).
16const DEFAULT_CACHE_CAPACITY: usize = 100_000;
17
18/// LRU cache for verified `XorName` values.
19///
20/// This cache stores `XorName` values that have been verified to exist on the
21/// autonomi network, avoiding repeated network queries for the same data.
22#[derive(Clone)]
23pub struct VerifiedCache {
24    inner: Arc<Mutex<LruCache<XorName, ()>>>,
25    stats: Arc<Mutex<CacheStats>>,
26}
27
28/// Cache statistics for monitoring.
29#[derive(Debug, Default, Clone)]
30pub struct CacheStats {
31    /// Number of cache hits.
32    pub hits: u64,
33    /// Number of cache misses.
34    pub misses: u64,
35    /// Number of entries added.
36    pub additions: u64,
37}
38
39impl CacheStats {
40    /// Calculate hit rate as a percentage.
41    #[must_use]
42    #[allow(clippy::cast_precision_loss)]
43    pub fn hit_rate(&self) -> f64 {
44        let total = self.hits + self.misses;
45        if total == 0 {
46            0.0
47        } else {
48            (self.hits as f64 / total as f64) * 100.0
49        }
50    }
51}
52
53impl VerifiedCache {
54    /// Create a new cache with default capacity.
55    #[must_use]
56    pub fn new() -> Self {
57        Self::with_capacity(DEFAULT_CACHE_CAPACITY)
58    }
59
60    /// Create a new cache with the specified capacity.
61    ///
62    /// If capacity is 0, defaults to 1.
63    #[must_use]
64    pub fn with_capacity(capacity: usize) -> Self {
65        // Use max(1, capacity) to ensure non-zero, avoiding unsafe or expect
66        let effective_capacity = capacity.max(1);
67        // This is guaranteed to succeed since effective_capacity >= 1
68        // Using if-let pattern since we know it will always be Some
69        let cap = NonZeroUsize::new(effective_capacity).unwrap_or(NonZeroUsize::MIN);
70        Self {
71            inner: Arc::new(Mutex::new(LruCache::new(cap))),
72            stats: Arc::new(Mutex::new(CacheStats::default())),
73        }
74    }
75
76    /// Check if a `XorName` is in the cache.
77    ///
78    /// Returns `true` if the `XorName` is cached (verified to exist on autonomi).
79    #[must_use]
80    pub fn contains(&self, xorname: &XorName) -> bool {
81        let found = self.inner.lock().get(xorname).is_some();
82
83        let mut stats = self.stats.lock();
84        if found {
85            stats.hits += 1;
86        } else {
87            stats.misses += 1;
88        }
89        drop(stats);
90
91        found
92    }
93
94    /// Add a `XorName` to the cache.
95    ///
96    /// This should be called after verifying that data exists on the autonomi network.
97    pub fn insert(&self, xorname: XorName) {
98        self.inner.lock().put(xorname, ());
99        self.stats.lock().additions += 1;
100    }
101
102    /// Get current cache statistics.
103    #[must_use]
104    pub fn stats(&self) -> CacheStats {
105        self.stats.lock().clone()
106    }
107
108    /// Get the current number of entries in the cache.
109    #[must_use]
110    pub fn len(&self) -> usize {
111        self.inner.lock().len()
112    }
113
114    /// Check if the cache is empty.
115    #[must_use]
116    pub fn is_empty(&self) -> bool {
117        self.inner.lock().is_empty()
118    }
119
120    /// Clear all entries from the cache.
121    pub fn clear(&self) {
122        self.inner.lock().clear();
123    }
124}
125
126impl Default for VerifiedCache {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_cache_basic_operations() {
138        let cache = VerifiedCache::new();
139
140        let xorname1 = [1u8; 32];
141        let xorname2 = [2u8; 32];
142
143        // Initially empty
144        assert!(cache.is_empty());
145        assert!(!cache.contains(&xorname1));
146
147        // Insert and check
148        cache.insert(xorname1);
149        assert!(cache.contains(&xorname1));
150        assert!(!cache.contains(&xorname2));
151        assert_eq!(cache.len(), 1);
152
153        // Insert another
154        cache.insert(xorname2);
155        assert!(cache.contains(&xorname1));
156        assert!(cache.contains(&xorname2));
157        assert_eq!(cache.len(), 2);
158    }
159
160    #[test]
161    fn test_cache_stats() {
162        let cache = VerifiedCache::new();
163        let xorname = [1u8; 32];
164
165        // Miss
166        assert!(!cache.contains(&xorname));
167        let stats = cache.stats();
168        assert_eq!(stats.misses, 1);
169        assert_eq!(stats.hits, 0);
170
171        // Add
172        cache.insert(xorname);
173        let stats = cache.stats();
174        assert_eq!(stats.additions, 1);
175
176        // Hit
177        assert!(cache.contains(&xorname));
178        let stats = cache.stats();
179        assert_eq!(stats.hits, 1);
180        assert_eq!(stats.misses, 1);
181
182        // Hit rate should be 50%
183        assert!((stats.hit_rate() - 50.0).abs() < 0.01);
184    }
185
186    #[test]
187    fn test_cache_lru_eviction() {
188        // Small cache for testing eviction
189        let cache = VerifiedCache::with_capacity(2);
190
191        let xorname1 = [1u8; 32];
192        let xorname2 = [2u8; 32];
193        let xorname3 = [3u8; 32];
194
195        cache.insert(xorname1);
196        cache.insert(xorname2);
197        assert_eq!(cache.len(), 2);
198
199        // Insert third, should evict xorname1 (least recently used)
200        cache.insert(xorname3);
201        assert_eq!(cache.len(), 2);
202        assert!(!cache.contains(&xorname1)); // evicted
203                                             // Note: after contains call on evicted item, stats will show a miss
204    }
205
206    #[test]
207    fn test_cache_clear() {
208        let cache = VerifiedCache::new();
209
210        cache.insert([1u8; 32]);
211        cache.insert([2u8; 32]);
212        assert_eq!(cache.len(), 2);
213
214        cache.clear();
215        assert!(cache.is_empty());
216    }
217}