Skip to main content

ant_core/data/client/
cache.rs

1//! LRU chunk cache for reducing redundant network requests.
2//!
3//! Caches recently fetched chunks in memory to avoid re-fetching
4//! the same content-addressed data from the network.
5
6use ant_protocol::XorName;
7use bytes::Bytes;
8use lru::LruCache;
9use std::num::NonZeroUsize;
10use std::sync::{Mutex, PoisonError};
11
12/// Default cache capacity (number of chunks).
13const DEFAULT_CACHE_CAPACITY: usize = 1024;
14
15/// An LRU cache for content-addressed chunks.
16pub struct ChunkCache {
17    inner: Mutex<LruCache<XorName, Bytes>>,
18}
19
20impl ChunkCache {
21    /// Create a new chunk cache with the given capacity.
22    ///
23    /// Falls back to the default capacity (1024) if zero is provided.
24    #[must_use]
25    pub fn new(capacity: usize) -> Self {
26        // Use provided capacity, falling back to default if zero
27        let effective = if capacity == 0 {
28            DEFAULT_CACHE_CAPACITY
29        } else {
30            capacity
31        };
32        // SAFETY: effective is guaranteed non-zero by the check above
33        let cap = NonZeroUsize::new(effective).unwrap_or(NonZeroUsize::MIN);
34        Self {
35            inner: Mutex::new(LruCache::new(cap)),
36        }
37    }
38
39    /// Create a cache with the default capacity.
40    #[must_use]
41    pub fn with_default_capacity() -> Self {
42        Self::new(DEFAULT_CACHE_CAPACITY)
43    }
44
45    /// Get a chunk from the cache.
46    #[must_use]
47    pub fn get(&self, address: &XorName) -> Option<Bytes> {
48        let mut cache = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
49        cache.get(address).cloned()
50    }
51
52    /// Insert a chunk into the cache.
53    pub fn put(&self, address: XorName, content: Bytes) {
54        let mut cache = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
55        cache.put(address, content);
56    }
57
58    /// Check if a chunk is in the cache.
59    #[must_use]
60    pub fn contains(&self, address: &XorName) -> bool {
61        let cache = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
62        cache.contains(address)
63    }
64
65    /// Get the number of cached chunks.
66    #[must_use]
67    pub fn len(&self) -> usize {
68        let cache = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
69        cache.len()
70    }
71
72    /// Check if the cache is empty.
73    #[must_use]
74    pub fn is_empty(&self) -> bool {
75        self.len() == 0
76    }
77
78    /// Remove a specific entry from the cache.
79    pub fn remove(&self, address: &XorName) {
80        let mut cache = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
81        cache.pop(address);
82    }
83
84    /// Clear all cached chunks.
85    pub fn clear(&self) {
86        let mut cache = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
87        cache.clear();
88    }
89}
90
91impl Default for ChunkCache {
92    fn default() -> Self {
93        Self::with_default_capacity()
94    }
95}
96
97#[cfg(test)]
98#[allow(clippy::unwrap_used, clippy::expect_used)]
99mod tests {
100    use super::*;
101
102    fn make_address(byte: u8) -> XorName {
103        let mut addr = [0u8; 32];
104        addr[0] = byte;
105        addr
106    }
107
108    #[test]
109    fn test_new_cache_default_capacity() {
110        let cache = ChunkCache::default();
111        assert!(cache.is_empty());
112        assert_eq!(cache.len(), 0);
113    }
114
115    #[test]
116    fn test_zero_capacity_falls_back_to_default() {
117        let cache = ChunkCache::new(0);
118        let addr = make_address(1);
119        cache.put(addr, Bytes::from_static(b"hello"));
120        assert_eq!(cache.len(), 1);
121    }
122
123    #[test]
124    fn test_put_and_get() {
125        let cache = ChunkCache::new(10);
126        let addr = make_address(1);
127        let data = Bytes::from_static(b"hello world");
128
129        cache.put(addr, data.clone());
130        let got = cache.get(&addr);
131        assert_eq!(got, Some(data));
132    }
133
134    #[test]
135    fn test_get_miss() {
136        let cache = ChunkCache::new(10);
137        let addr = make_address(1);
138        assert_eq!(cache.get(&addr), None);
139    }
140
141    #[test]
142    fn test_contains() {
143        let cache = ChunkCache::new(10);
144        let addr = make_address(1);
145        assert!(!cache.contains(&addr));
146
147        cache.put(addr, Bytes::from_static(b"data"));
148        assert!(cache.contains(&addr));
149    }
150
151    #[test]
152    fn test_lru_eviction() {
153        let cache = ChunkCache::new(2);
154
155        let addr1 = make_address(1);
156        let addr2 = make_address(2);
157        let addr3 = make_address(3);
158
159        cache.put(addr1, Bytes::from_static(b"one"));
160        cache.put(addr2, Bytes::from_static(b"two"));
161        assert_eq!(cache.len(), 2);
162
163        // Inserting a third should evict addr1 (least recently used)
164        cache.put(addr3, Bytes::from_static(b"three"));
165        assert_eq!(cache.len(), 2);
166        assert!(!cache.contains(&addr1));
167        assert!(cache.contains(&addr2));
168        assert!(cache.contains(&addr3));
169    }
170
171    #[test]
172    fn test_lru_access_refreshes() {
173        let cache = ChunkCache::new(2);
174
175        let addr1 = make_address(1);
176        let addr2 = make_address(2);
177        let addr3 = make_address(3);
178
179        cache.put(addr1, Bytes::from_static(b"one"));
180        cache.put(addr2, Bytes::from_static(b"two"));
181
182        // Access addr1 to refresh it
183        let _ = cache.get(&addr1);
184
185        // Now inserting addr3 should evict addr2 (least recently used)
186        cache.put(addr3, Bytes::from_static(b"three"));
187        assert!(cache.contains(&addr1));
188        assert!(!cache.contains(&addr2));
189        assert!(cache.contains(&addr3));
190    }
191
192    #[test]
193    fn test_clear() {
194        let cache = ChunkCache::new(10);
195        cache.put(make_address(1), Bytes::from_static(b"one"));
196        cache.put(make_address(2), Bytes::from_static(b"two"));
197        assert_eq!(cache.len(), 2);
198
199        cache.clear();
200        assert!(cache.is_empty());
201        assert_eq!(cache.len(), 0);
202    }
203
204    #[test]
205    fn test_overwrite_same_key() {
206        let cache = ChunkCache::new(10);
207        let addr = make_address(1);
208
209        cache.put(addr, Bytes::from_static(b"first"));
210        cache.put(addr, Bytes::from_static(b"second"));
211
212        assert_eq!(cache.len(), 1);
213        assert_eq!(cache.get(&addr), Some(Bytes::from_static(b"second")));
214    }
215}