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();
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();
181
182        if inner.blocks.contains_key(cid) {
183            inner.stats.hits += 1;
184            let block = inner.blocks.get(cid).unwrap().block.clone();
185            inner.update_access(cid);
186            Some(block)
187        } else {
188            inner.stats.misses += 1;
189            None
190        }
191    }
192
193    /// Check if the cache contains a block with the given CID
194    ///
195    /// This does not update LRU access time.
196    pub fn contains(&self, cid: &Cid) -> bool {
197        let inner = self.inner.read().unwrap();
198        inner.blocks.contains_key(cid)
199    }
200
201    /// Remove a block from the cache
202    pub fn remove(&self, cid: &Cid) -> Option<Block> {
203        let mut inner = self.inner.write().unwrap();
204
205        if let Some(entry) = inner.blocks.remove(cid) {
206            inner.current_size -= entry.size;
207            // Remove from LRU list
208            if let Some(pos) = inner.lru_list.iter().position(|c| c == cid) {
209                inner.lru_list.remove(pos);
210            }
211            Some(entry.block)
212        } else {
213            None
214        }
215    }
216
217    /// Clear all blocks from the cache
218    pub fn clear(&self) {
219        let mut inner = self.inner.write().unwrap();
220        inner.blocks.clear();
221        inner.lru_list.clear();
222        inner.current_size = 0;
223    }
224
225    /// Get the current cache statistics
226    ///
227    /// # Example
228    ///
229    /// ```rust
230    /// use ipfrs_core::{BlockCache, Block};
231    /// use bytes::Bytes;
232    ///
233    /// let cache = BlockCache::new(1024 * 1024, None);
234    /// let block = Block::new(Bytes::from_static(b"data")).unwrap();
235    ///
236    /// cache.insert(block.clone());
237    /// cache.get(block.cid()); // Hit
238    /// cache.get(block.cid()); // Another hit
239    ///
240    /// let stats = cache.stats();
241    /// assert_eq!(stats.hits, 2);
242    /// assert_eq!(stats.misses, 0);
243    /// ```
244    pub fn stats(&self) -> CacheStats {
245        let inner = self.inner.read().unwrap();
246        inner.stats.clone()
247    }
248
249    /// Get the number of blocks currently in the cache
250    pub fn len(&self) -> usize {
251        let inner = self.inner.read().unwrap();
252        inner.blocks.len()
253    }
254
255    /// Check if the cache is empty
256    pub fn is_empty(&self) -> bool {
257        let inner = self.inner.read().unwrap();
258        inner.blocks.is_empty()
259    }
260
261    /// Get the current total size of cached blocks in bytes
262    pub fn size(&self) -> u64 {
263        let inner = self.inner.read().unwrap();
264        inner.current_size
265    }
266
267    /// Get the maximum cache size in bytes
268    pub fn max_size(&self) -> u64 {
269        let inner = self.inner.read().unwrap();
270        inner.max_size_bytes
271    }
272
273    /// Get the maximum number of blocks (if configured)
274    pub fn max_blocks(&self) -> Option<usize> {
275        let inner = self.inner.read().unwrap();
276        inner.max_blocks
277    }
278}
279
280impl BlockCacheInner {
281    fn would_exceed_limits(&self, additional_size: u64) -> bool {
282        let size_exceeded = self.current_size + additional_size > self.max_size_bytes;
283        let count_exceeded = self
284            .max_blocks
285            .map(|max| self.blocks.len() >= max)
286            .unwrap_or(false);
287
288        size_exceeded || count_exceeded
289    }
290
291    fn evict_lru(&mut self) {
292        if self.lru_list.is_empty() {
293            return;
294        }
295
296        // Find the least recently used CID
297        let lru_cid = self
298            .blocks
299            .iter()
300            .min_by_key(|(_, entry)| entry.last_access_index)
301            .map(|(cid, _)| *cid);
302
303        if let Some(cid) = lru_cid {
304            if let Some(entry) = self.blocks.remove(&cid) {
305                self.current_size -= entry.size;
306                self.stats.evictions += 1;
307
308                // Remove from LRU list
309                if let Some(pos) = self.lru_list.iter().position(|c| c == &cid) {
310                    self.lru_list.remove(pos);
311                }
312            }
313        }
314    }
315
316    fn update_access(&mut self, cid: &Cid) {
317        if let Some(entry) = self.blocks.get_mut(cid) {
318            entry.last_access_index = self.lru_list.len();
319            self.lru_list.push(*cid);
320        }
321    }
322}
323
324/// Statistics for block cache operations
325#[derive(Debug, Clone, Default)]
326pub struct CacheStats {
327    /// Number of cache hits
328    pub hits: u64,
329    /// Number of cache misses
330    pub misses: u64,
331    /// Number of evictions (LRU removals)
332    pub evictions: u64,
333}
334
335impl CacheStats {
336    /// Calculate the hit rate (hits / total_requests)
337    ///
338    /// Returns 0.0 if no requests have been made.
339    pub fn hit_rate(&self) -> f64 {
340        let total = self.hits + self.misses;
341        if total == 0 {
342            0.0
343        } else {
344            self.hits as f64 / total as f64
345        }
346    }
347
348    /// Calculate the miss rate (misses / total_requests)
349    ///
350    /// Returns 0.0 if no requests have been made.
351    pub fn miss_rate(&self) -> f64 {
352        let total = self.hits + self.misses;
353        if total == 0 {
354            0.0
355        } else {
356            self.misses as f64 / total as f64
357        }
358    }
359
360    /// Get the total number of requests (hits + misses)
361    pub fn total_requests(&self) -> u64 {
362        self.hits + self.misses
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use bytes::Bytes;
370
371    fn make_block(data: &[u8]) -> Block {
372        Block::new(Bytes::copy_from_slice(data)).unwrap()
373    }
374
375    #[test]
376    fn test_cache_basic_insert_get() {
377        let cache = BlockCache::new(1024, None);
378        let block = make_block(b"test data");
379        let cid = *block.cid();
380
381        cache.insert(block.clone());
382        let retrieved = cache.get(&cid).unwrap();
383
384        assert_eq!(retrieved.data(), block.data());
385    }
386
387    #[test]
388    fn test_cache_miss() {
389        let cache = BlockCache::new(1024, None);
390        let block = make_block(b"test");
391        let fake_cid = *make_block(b"other").cid();
392
393        cache.insert(block);
394
395        assert!(cache.get(&fake_cid).is_none());
396
397        let stats = cache.stats();
398        assert_eq!(stats.misses, 1);
399    }
400
401    #[test]
402    fn test_cache_hit_tracking() {
403        let cache = BlockCache::new(1024, None);
404        let block = make_block(b"data");
405        let cid = *block.cid();
406
407        cache.insert(block);
408        cache.get(&cid);
409        cache.get(&cid);
410
411        let stats = cache.stats();
412        assert_eq!(stats.hits, 2);
413    }
414
415    #[test]
416    fn test_cache_size_limit() {
417        let cache = BlockCache::new(20, None); // Small cache
418        let block1 = make_block(b"12345678901234567890"); // 20 bytes
419        let block2 = make_block(b"extra"); // Will exceed limit
420
421        cache.insert(block1.clone());
422        cache.insert(block2.clone());
423
424        // block1 should be evicted
425        assert!(cache.get(block1.cid()).is_none());
426        assert!(cache.get(block2.cid()).is_some());
427
428        let stats = cache.stats();
429        assert_eq!(stats.evictions, 1);
430    }
431
432    #[test]
433    fn test_cache_count_limit() {
434        let cache = BlockCache::new(1024, Some(2)); // Max 2 blocks
435        let block1 = make_block(b"a");
436        let block2 = make_block(b"b");
437        let block3 = make_block(b"c");
438
439        cache.insert(block1.clone());
440        cache.insert(block2.clone());
441        cache.insert(block3.clone());
442
443        // block1 should be evicted (LRU)
444        assert!(cache.get(block1.cid()).is_none());
445        assert_eq!(cache.len(), 2);
446    }
447
448    #[test]
449    fn test_cache_lru_eviction() {
450        let cache = BlockCache::new(1024, Some(3));
451        let block1 = make_block(b"1");
452        let block2 = make_block(b"2");
453        let block3 = make_block(b"3");
454        let block4 = make_block(b"4");
455
456        cache.insert(block1.clone());
457        cache.insert(block2.clone());
458        cache.insert(block3.clone());
459
460        // Access block1 to make it more recently used
461        cache.get(block1.cid());
462
463        // Insert block4, should evict block2 (least recently used)
464        cache.insert(block4.clone());
465
466        assert!(cache.get(block1.cid()).is_some());
467        assert!(cache.get(block2.cid()).is_none());
468        assert!(cache.get(block3.cid()).is_some());
469        assert!(cache.get(block4.cid()).is_some());
470    }
471
472    #[test]
473    fn test_cache_contains() {
474        let cache = BlockCache::new(1024, None);
475        let block = make_block(b"test");
476
477        cache.insert(block.clone());
478
479        assert!(cache.contains(block.cid()));
480        assert!(!cache.contains(make_block(b"other").cid()));
481    }
482
483    #[test]
484    fn test_cache_remove() {
485        let cache = BlockCache::new(1024, None);
486        let block = make_block(b"test");
487        let cid = *block.cid();
488
489        cache.insert(block.clone());
490        assert!(cache.contains(&cid));
491
492        let removed = cache.remove(&cid);
493        assert!(removed.is_some());
494        assert!(!cache.contains(&cid));
495    }
496
497    #[test]
498    fn test_cache_clear() {
499        let cache = BlockCache::new(1024, None);
500        cache.insert(make_block(b"1"));
501        cache.insert(make_block(b"2"));
502        cache.insert(make_block(b"3"));
503
504        assert_eq!(cache.len(), 3);
505
506        cache.clear();
507
508        assert_eq!(cache.len(), 0);
509        assert_eq!(cache.size(), 0);
510    }
511
512    #[test]
513    fn test_cache_stats() {
514        let cache = BlockCache::new(1024, None);
515        let block = make_block(b"test");
516
517        cache.insert(block.clone());
518        cache.get(block.cid()); // hit
519        cache.get(block.cid()); // hit
520        cache.get(make_block(b"miss").cid()); // miss
521
522        let stats = cache.stats();
523        assert_eq!(stats.hits, 2);
524        assert_eq!(stats.misses, 1);
525        assert_eq!(stats.total_requests(), 3);
526        assert!((stats.hit_rate() - 2.0 / 3.0).abs() < 0.001);
527    }
528
529    #[test]
530    fn test_cache_size_tracking() {
531        let cache = BlockCache::new(1024, None);
532        let block1 = make_block(&[0u8; 100]);
533        let block2 = make_block(&[0u8; 200]);
534
535        cache.insert(block1.clone());
536        assert_eq!(cache.size(), 100);
537
538        cache.insert(block2.clone());
539        assert_eq!(cache.size(), 300);
540
541        cache.remove(block1.cid());
542        assert_eq!(cache.size(), 200);
543    }
544
545    #[test]
546    fn test_cache_duplicate_insert() {
547        let cache = BlockCache::new(1024, None);
548        let block = make_block(b"data");
549
550        cache.insert(block.clone());
551        cache.insert(block.clone()); // Duplicate
552
553        assert_eq!(cache.len(), 1);
554        assert_eq!(cache.size(), block.len() as u64);
555    }
556
557    #[test]
558    fn test_cache_thread_safety() {
559        use std::thread;
560
561        let cache = BlockCache::new(10240, None);
562        let cache_clone = cache.clone();
563
564        let handle = thread::spawn(move || {
565            for i in 0..100 {
566                let block = make_block(&[i as u8; 10]);
567                cache_clone.insert(block);
568            }
569        });
570
571        for i in 100..200 {
572            let block = make_block(&[i as u8; 10]);
573            cache.insert(block);
574        }
575
576        handle.join().unwrap();
577
578        // Should have blocks from both threads
579        assert!(!cache.is_empty());
580    }
581}