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();
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();
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 pub fn contains(&self, cid: &Cid) -> bool {
197 let inner = self.inner.read().unwrap();
198 inner.blocks.contains_key(cid)
199 }
200
201 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 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 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 pub fn stats(&self) -> CacheStats {
245 let inner = self.inner.read().unwrap();
246 inner.stats.clone()
247 }
248
249 pub fn len(&self) -> usize {
251 let inner = self.inner.read().unwrap();
252 inner.blocks.len()
253 }
254
255 pub fn is_empty(&self) -> bool {
257 let inner = self.inner.read().unwrap();
258 inner.blocks.is_empty()
259 }
260
261 pub fn size(&self) -> u64 {
263 let inner = self.inner.read().unwrap();
264 inner.current_size
265 }
266
267 pub fn max_size(&self) -> u64 {
269 let inner = self.inner.read().unwrap();
270 inner.max_size_bytes
271 }
272
273 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 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 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#[derive(Debug, Clone, Default)]
326pub struct CacheStats {
327 pub hits: u64,
329 pub misses: u64,
331 pub evictions: u64,
333}
334
335impl CacheStats {
336 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 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 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); let block1 = make_block(b"12345678901234567890"); let block2 = make_block(b"extra"); cache.insert(block1.clone());
422 cache.insert(block2.clone());
423
424 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)); 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 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 cache.get(block1.cid());
462
463 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()); cache.get(block.cid()); cache.get(make_block(b"miss").cid()); 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()); 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 assert!(!cache.is_empty());
580 }
581}