ipfrs_core/
block_cache.rs1use crate::block::Block;
38use crate::cid::Cid;
39use std::collections::HashMap;
40use std::sync::{Arc, RwLock};
41
42#[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 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 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 inner.blocks.contains_key(&cid) {
135 inner.update_access(&cid);
136 return;
137 }
138
139 while inner.would_exceed_limits(size) && !inner.blocks.is_empty() {
141 inner.evict_lru();
142 }
143
144 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
331pub struct CacheStats {
332 pub hits: u64,
334 pub misses: u64,
336 pub evictions: u64,
338}
339
340impl CacheStats {
341 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 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 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); let block1 = make_block(b"12345678901234567890"); let block2 = make_block(b"extra"); cache.insert(block1.clone());
427 cache.insert(block2.clone());
428
429 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)); 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 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 cache.get(block1.cid());
467
468 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()); cache.get(block.cid()); cache.get(make_block(b"miss").cid()); 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()); 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 assert!(!cache.is_empty());
585 }
586}