Skip to main content

ipfrs_core/
block_cache.rs

1//! LRU cache for blocks
2//!
3//! This module provides an efficient LRU (Least Recently Used) cache for blocks,
4//! enabling fast repeated access to frequently used blocks without hitting storage.
5//!
6//! # Features
7//!
8//! - **Thread-safe** - Can be safely shared across threads
9//! - **LRU eviction** - Automatically evicts least recently used blocks when full
10//! - **Size limits** - Configurable maximum cache size (in bytes and/or block count)
11//! - **Statistics tracking** - Monitor cache hits, misses, and evictions
12//! - **Zero-copy** - Blocks are reference-counted, so cloning is cheap
13//!
14//! # Example
15//!
16//! ```rust
17//! use ipfrs_core::{BlockCache, Block};
18//! use bytes::Bytes;
19//!
20//! // Create a cache with 10MB limit
21//! let cache = BlockCache::new(10 * 1024 * 1024, None);
22//!
23//! // Insert a block
24//! let block = Block::new(Bytes::from_static(b"Hello, cache!")).unwrap();
25//! cache.insert(block.clone());
26//!
27//! // Retrieve the block
28//! if let Some(cached_block) = cache.get(block.cid()) {
29//!     println!("Cache hit!");
30//! }
31//!
32//! // Check statistics
33//! let stats = cache.stats();
34//! println!("Hits: {}, Misses: {}", stats.hits, stats.misses);
35//! ```
36
37use crate::block::Block;
38use crate::cid::Cid;
39use std::collections::HashMap;
40use std::sync::{Arc, RwLock};
41
42/// LRU cache for blocks
43///
44/// This cache uses a Least Recently Used eviction policy to maintain a bounded
45/// set of frequently accessed blocks in memory. It's thread-safe and can be
46/// shared across multiple threads.
47///
48/// # Example
49///
50/// ```rust
51/// use ipfrs_core::{BlockCache, Block};
52/// use bytes::Bytes;
53///
54/// let cache = BlockCache::new(1024 * 1024, Some(100)); // 1MB or 100 blocks max
55///
56/// let block = Block::new(Bytes::from_static(b"cached data")).unwrap();
57/// cache.insert(block.clone());
58///
59/// assert!(cache.get(block.cid()).is_some());
60/// ```
61#[derive(Clone)]
62pub struct BlockCache {
63    inner: Arc<RwLock<BlockCacheInner>>,
64}
65
66struct BlockCacheInner {
67    blocks: HashMap<Cid, CacheEntry>,
68    lru_list: Vec<Cid>,
69    max_size_bytes: u64,
70    max_blocks: Option<usize>,
71    current_size: u64,
72    stats: CacheStats,
73}
74
75struct CacheEntry {
76    block: Block,
77    size: u64,
78    last_access_index: usize,
79}
80
81impl BlockCache {
82    /// Create a new block cache
83    ///
84    /// # Arguments
85    ///
86    /// * `max_size_bytes` - Maximum total size of cached blocks in bytes
87    /// * `max_blocks` - Optional maximum number of blocks (None = unlimited)
88    ///
89    /// # Example
90    ///
91    /// ```rust
92    /// use ipfrs_core::BlockCache;
93    ///
94    /// // Cache up to 10MB of blocks
95    /// let cache = BlockCache::new(10 * 1024 * 1024, None);
96    ///
97    /// // Cache up to 1MB or 100 blocks, whichever limit is hit first
98    /// let cache2 = BlockCache::new(1024 * 1024, Some(100));
99    /// ```
100    pub fn new(max_size_bytes: u64, max_blocks: Option<usize>) -> Self {
101        Self {
102            inner: Arc::new(RwLock::new(BlockCacheInner {
103                blocks: HashMap::new(),
104                lru_list: Vec::new(),
105                max_size_bytes,
106                max_blocks,
107                current_size: 0,
108                stats: CacheStats::default(),
109            })),
110        }
111    }
112
113    /// Insert a block into the cache
114    ///
115    /// If the cache is full, the least recently used block will be evicted.
116    ///
117    /// # Example
118    ///
119    /// ```rust
120    /// use ipfrs_core::{BlockCache, Block};
121    /// use bytes::Bytes;
122    ///
123    /// let cache = BlockCache::new(1024 * 1024, None);
124    /// let block = Block::new(Bytes::from_static(b"data")).unwrap();
125    ///
126    /// cache.insert(block);
127    /// ```
128    pub fn insert(&self, block: Block) {
129        let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
130        let cid = *block.cid();
131        let size = block.len() as u64;
132
133        // If block already exists, update access time
134        if inner.blocks.contains_key(&cid) {
135            inner.update_access(&cid);
136            return;
137        }
138
139        // Evict blocks if necessary
140        while inner.would_exceed_limits(size) && !inner.blocks.is_empty() {
141            inner.evict_lru();
142        }
143
144        // Insert the new block
145        let access_index = inner.lru_list.len();
146        inner.lru_list.push(cid);
147        inner.blocks.insert(
148            cid,
149            CacheEntry {
150                block,
151                size,
152                last_access_index: access_index,
153            },
154        );
155        inner.current_size += size;
156    }
157
158    /// Get a block from the cache
159    ///
160    /// Returns `Some(block)` if found, `None` otherwise. Updates the access
161    /// time for LRU tracking.
162    ///
163    /// # Example
164    ///
165    /// ```rust
166    /// use ipfrs_core::{BlockCache, Block};
167    /// use bytes::Bytes;
168    ///
169    /// let cache = BlockCache::new(1024 * 1024, None);
170    /// let block = Block::new(Bytes::from_static(b"data")).unwrap();
171    /// let cid = *block.cid();
172    ///
173    /// cache.insert(block);
174    ///
175    /// if let Some(cached) = cache.get(&cid) {
176    ///     assert_eq!(cached.len(), 4);
177    /// }
178    /// ```
179    pub fn get(&self, cid: &Cid) -> Option<Block> {
180        let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
181
182        if inner.blocks.contains_key(cid) {
183            inner.stats.hits += 1;
184            let block = inner
185                .blocks
186                .get(cid)
187                .expect("just confirmed key is present via contains_key")
188                .block
189                .clone();
190            inner.update_access(cid);
191            Some(block)
192        } else {
193            inner.stats.misses += 1;
194            None
195        }
196    }
197
198    /// Check if the cache contains a block with the given CID
199    ///
200    /// This does not update LRU access time.
201    pub fn contains(&self, cid: &Cid) -> bool {
202        let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
203        inner.blocks.contains_key(cid)
204    }
205
206    /// Remove a block from the cache
207    pub fn remove(&self, cid: &Cid) -> Option<Block> {
208        let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
209
210        if let Some(entry) = inner.blocks.remove(cid) {
211            inner.current_size -= entry.size;
212            // Remove from LRU list
213            if let Some(pos) = inner.lru_list.iter().position(|c| c == cid) {
214                inner.lru_list.remove(pos);
215            }
216            Some(entry.block)
217        } else {
218            None
219        }
220    }
221
222    /// Clear all blocks from the cache
223    pub fn clear(&self) {
224        let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
225        inner.blocks.clear();
226        inner.lru_list.clear();
227        inner.current_size = 0;
228    }
229
230    /// Get the current cache statistics
231    ///
232    /// # Example
233    ///
234    /// ```rust
235    /// use ipfrs_core::{BlockCache, Block};
236    /// use bytes::Bytes;
237    ///
238    /// let cache = BlockCache::new(1024 * 1024, None);
239    /// let block = Block::new(Bytes::from_static(b"data")).unwrap();
240    ///
241    /// cache.insert(block.clone());
242    /// cache.get(block.cid()); // Hit
243    /// cache.get(block.cid()); // Another hit
244    ///
245    /// let stats = cache.stats();
246    /// assert_eq!(stats.hits, 2);
247    /// assert_eq!(stats.misses, 0);
248    /// ```
249    pub fn stats(&self) -> CacheStats {
250        let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
251        inner.stats.clone()
252    }
253
254    /// Get the number of blocks currently in the cache
255    pub fn len(&self) -> usize {
256        let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
257        inner.blocks.len()
258    }
259
260    /// Check if the cache is empty
261    pub fn is_empty(&self) -> bool {
262        let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
263        inner.blocks.is_empty()
264    }
265
266    /// Get the current total size of cached blocks in bytes
267    pub fn size(&self) -> u64 {
268        let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
269        inner.current_size
270    }
271
272    /// Get the maximum cache size in bytes
273    pub fn max_size(&self) -> u64 {
274        let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
275        inner.max_size_bytes
276    }
277
278    /// Get the maximum number of blocks (if configured)
279    pub fn max_blocks(&self) -> Option<usize> {
280        let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
281        inner.max_blocks
282    }
283}
284
285impl BlockCacheInner {
286    fn would_exceed_limits(&self, additional_size: u64) -> bool {
287        let size_exceeded = self.current_size + additional_size > self.max_size_bytes;
288        let count_exceeded = self
289            .max_blocks
290            .map(|max| self.blocks.len() >= max)
291            .unwrap_or(false);
292
293        size_exceeded || count_exceeded
294    }
295
296    fn evict_lru(&mut self) {
297        if self.lru_list.is_empty() {
298            return;
299        }
300
301        // Find the least recently used CID
302        let lru_cid = self
303            .blocks
304            .iter()
305            .min_by_key(|(_, entry)| entry.last_access_index)
306            .map(|(cid, _)| *cid);
307
308        if let Some(cid) = lru_cid {
309            if let Some(entry) = self.blocks.remove(&cid) {
310                self.current_size -= entry.size;
311                self.stats.evictions += 1;
312
313                // Remove from LRU list
314                if let Some(pos) = self.lru_list.iter().position(|c| c == &cid) {
315                    self.lru_list.remove(pos);
316                }
317            }
318        }
319    }
320
321    fn update_access(&mut self, cid: &Cid) {
322        if let Some(entry) = self.blocks.get_mut(cid) {
323            entry.last_access_index = self.lru_list.len();
324            self.lru_list.push(*cid);
325        }
326    }
327}
328
329/// Statistics for block cache operations
330#[derive(Debug, Clone, Default)]
331pub struct CacheStats {
332    /// Number of cache hits
333    pub hits: u64,
334    /// Number of cache misses
335    pub misses: u64,
336    /// Number of evictions (LRU removals)
337    pub evictions: u64,
338}
339
340impl CacheStats {
341    /// Calculate the hit rate (hits / total_requests)
342    ///
343    /// Returns 0.0 if no requests have been made.
344    pub fn hit_rate(&self) -> f64 {
345        let total = self.hits + self.misses;
346        if total == 0 {
347            0.0
348        } else {
349            self.hits as f64 / total as f64
350        }
351    }
352
353    /// Calculate the miss rate (misses / total_requests)
354    ///
355    /// Returns 0.0 if no requests have been made.
356    pub fn miss_rate(&self) -> f64 {
357        let total = self.hits + self.misses;
358        if total == 0 {
359            0.0
360        } else {
361            self.misses as f64 / total as f64
362        }
363    }
364
365    /// Get the total number of requests (hits + misses)
366    pub fn total_requests(&self) -> u64 {
367        self.hits + self.misses
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use bytes::Bytes;
375
376    fn make_block(data: &[u8]) -> Block {
377        Block::new(Bytes::copy_from_slice(data)).unwrap()
378    }
379
380    #[test]
381    fn test_cache_basic_insert_get() {
382        let cache = BlockCache::new(1024, None);
383        let block = make_block(b"test data");
384        let cid = *block.cid();
385
386        cache.insert(block.clone());
387        let retrieved = cache.get(&cid).unwrap();
388
389        assert_eq!(retrieved.data(), block.data());
390    }
391
392    #[test]
393    fn test_cache_miss() {
394        let cache = BlockCache::new(1024, None);
395        let block = make_block(b"test");
396        let fake_cid = *make_block(b"other").cid();
397
398        cache.insert(block);
399
400        assert!(cache.get(&fake_cid).is_none());
401
402        let stats = cache.stats();
403        assert_eq!(stats.misses, 1);
404    }
405
406    #[test]
407    fn test_cache_hit_tracking() {
408        let cache = BlockCache::new(1024, None);
409        let block = make_block(b"data");
410        let cid = *block.cid();
411
412        cache.insert(block);
413        cache.get(&cid);
414        cache.get(&cid);
415
416        let stats = cache.stats();
417        assert_eq!(stats.hits, 2);
418    }
419
420    #[test]
421    fn test_cache_size_limit() {
422        let cache = BlockCache::new(20, None); // Small cache
423        let block1 = make_block(b"12345678901234567890"); // 20 bytes
424        let block2 = make_block(b"extra"); // Will exceed limit
425
426        cache.insert(block1.clone());
427        cache.insert(block2.clone());
428
429        // block1 should be evicted
430        assert!(cache.get(block1.cid()).is_none());
431        assert!(cache.get(block2.cid()).is_some());
432
433        let stats = cache.stats();
434        assert_eq!(stats.evictions, 1);
435    }
436
437    #[test]
438    fn test_cache_count_limit() {
439        let cache = BlockCache::new(1024, Some(2)); // Max 2 blocks
440        let block1 = make_block(b"a");
441        let block2 = make_block(b"b");
442        let block3 = make_block(b"c");
443
444        cache.insert(block1.clone());
445        cache.insert(block2.clone());
446        cache.insert(block3.clone());
447
448        // block1 should be evicted (LRU)
449        assert!(cache.get(block1.cid()).is_none());
450        assert_eq!(cache.len(), 2);
451    }
452
453    #[test]
454    fn test_cache_lru_eviction() {
455        let cache = BlockCache::new(1024, Some(3));
456        let block1 = make_block(b"1");
457        let block2 = make_block(b"2");
458        let block3 = make_block(b"3");
459        let block4 = make_block(b"4");
460
461        cache.insert(block1.clone());
462        cache.insert(block2.clone());
463        cache.insert(block3.clone());
464
465        // Access block1 to make it more recently used
466        cache.get(block1.cid());
467
468        // Insert block4, should evict block2 (least recently used)
469        cache.insert(block4.clone());
470
471        assert!(cache.get(block1.cid()).is_some());
472        assert!(cache.get(block2.cid()).is_none());
473        assert!(cache.get(block3.cid()).is_some());
474        assert!(cache.get(block4.cid()).is_some());
475    }
476
477    #[test]
478    fn test_cache_contains() {
479        let cache = BlockCache::new(1024, None);
480        let block = make_block(b"test");
481
482        cache.insert(block.clone());
483
484        assert!(cache.contains(block.cid()));
485        assert!(!cache.contains(make_block(b"other").cid()));
486    }
487
488    #[test]
489    fn test_cache_remove() {
490        let cache = BlockCache::new(1024, None);
491        let block = make_block(b"test");
492        let cid = *block.cid();
493
494        cache.insert(block.clone());
495        assert!(cache.contains(&cid));
496
497        let removed = cache.remove(&cid);
498        assert!(removed.is_some());
499        assert!(!cache.contains(&cid));
500    }
501
502    #[test]
503    fn test_cache_clear() {
504        let cache = BlockCache::new(1024, None);
505        cache.insert(make_block(b"1"));
506        cache.insert(make_block(b"2"));
507        cache.insert(make_block(b"3"));
508
509        assert_eq!(cache.len(), 3);
510
511        cache.clear();
512
513        assert_eq!(cache.len(), 0);
514        assert_eq!(cache.size(), 0);
515    }
516
517    #[test]
518    fn test_cache_stats() {
519        let cache = BlockCache::new(1024, None);
520        let block = make_block(b"test");
521
522        cache.insert(block.clone());
523        cache.get(block.cid()); // hit
524        cache.get(block.cid()); // hit
525        cache.get(make_block(b"miss").cid()); // miss
526
527        let stats = cache.stats();
528        assert_eq!(stats.hits, 2);
529        assert_eq!(stats.misses, 1);
530        assert_eq!(stats.total_requests(), 3);
531        assert!((stats.hit_rate() - 2.0 / 3.0).abs() < 0.001);
532    }
533
534    #[test]
535    fn test_cache_size_tracking() {
536        let cache = BlockCache::new(1024, None);
537        let block1 = make_block(&[0u8; 100]);
538        let block2 = make_block(&[0u8; 200]);
539
540        cache.insert(block1.clone());
541        assert_eq!(cache.size(), 100);
542
543        cache.insert(block2.clone());
544        assert_eq!(cache.size(), 300);
545
546        cache.remove(block1.cid());
547        assert_eq!(cache.size(), 200);
548    }
549
550    #[test]
551    fn test_cache_duplicate_insert() {
552        let cache = BlockCache::new(1024, None);
553        let block = make_block(b"data");
554
555        cache.insert(block.clone());
556        cache.insert(block.clone()); // Duplicate
557
558        assert_eq!(cache.len(), 1);
559        assert_eq!(cache.size(), block.len() as u64);
560    }
561
562    #[test]
563    fn test_cache_thread_safety() {
564        use std::thread;
565
566        let cache = BlockCache::new(10240, None);
567        let cache_clone = cache.clone();
568
569        let handle = thread::spawn(move || {
570            for i in 0..100 {
571                let block = make_block(&[i as u8; 10]);
572                cache_clone.insert(block);
573            }
574        });
575
576        for i in 100..200 {
577            let block = make_block(&[i as u8; 10]);
578            cache.insert(block);
579        }
580
581        handle.join().unwrap();
582
583        // Should have blocks from both threads
584        assert!(!cache.is_empty());
585    }
586}