Skip to main content

ferripfs_blockstore/
caching.rs

1// Ported from: kubo/boxo/blockstore/caching.go
2// Kubo version: v0.39.0
3// Original: https://github.com/ipfs/kubo/blob/v0.39.0/boxo/blockstore/caching.go
4//
5// Original work: Copyright (c) Protocol Labs, Inc.
6// Port: Copyright (c) 2026 ferripfs contributors
7// SPDX-License-Identifier: MIT OR Apache-2.0
8
9//! Caching blockstore wrapper.
10//!
11//! This provides a caching layer on top of any blockstore,
12//! reducing disk reads for frequently accessed blocks.
13
14use crate::{Block, Blockstore, BlockstoreResult};
15use cid::Cid;
16use lru::LruCache;
17use parking_lot::Mutex;
18use std::num::NonZeroUsize;
19
20/// Default cache size in number of blocks
21pub const DEFAULT_CACHE_SIZE: usize = 256;
22
23/// Caching blockstore that wraps another blockstore with an LRU cache.
24pub struct CachingBlockstore<B: Blockstore> {
25    inner: B,
26    cache: Mutex<LruCache<String, Block>>,
27    has_cache: Mutex<LruCache<String, bool>>,
28}
29
30impl<B: Blockstore> CachingBlockstore<B> {
31    /// Create a new caching blockstore with the given cache size.
32    pub fn new(inner: B, cache_size: usize) -> Self {
33        let cache_size = NonZeroUsize::new(cache_size.max(1)).unwrap();
34        Self {
35            inner,
36            cache: Mutex::new(LruCache::new(cache_size)),
37            has_cache: Mutex::new(LruCache::new(cache_size)),
38        }
39    }
40
41    /// Create a new caching blockstore with the default cache size.
42    pub fn with_default_cache(inner: B) -> Self {
43        Self::new(inner, DEFAULT_CACHE_SIZE)
44    }
45
46    /// Get the underlying blockstore.
47    pub fn inner(&self) -> &B {
48        &self.inner
49    }
50
51    /// Get the underlying blockstore mutably.
52    pub fn inner_mut(&mut self) -> &mut B {
53        &mut self.inner
54    }
55
56    /// Get the current cache size (number of blocks).
57    pub fn cache_len(&self) -> usize {
58        self.cache.lock().len()
59    }
60
61    /// Clear the cache.
62    pub fn clear_cache(&self) {
63        self.cache.lock().clear();
64        self.has_cache.lock().clear();
65    }
66
67    /// Get cache statistics.
68    pub fn cache_stats(&self) -> CacheStats {
69        let cache = self.cache.lock();
70        let has_cache = self.has_cache.lock();
71        CacheStats {
72            block_cache_len: cache.len(),
73            block_cache_cap: cache.cap().get(),
74            has_cache_len: has_cache.len(),
75            has_cache_cap: has_cache.cap().get(),
76        }
77    }
78}
79
80/// Cache statistics
81#[derive(Debug, Clone)]
82pub struct CacheStats {
83    pub block_cache_len: usize,
84    pub block_cache_cap: usize,
85    pub has_cache_len: usize,
86    pub has_cache_cap: usize,
87}
88
89impl<B: Blockstore> Blockstore for CachingBlockstore<B> {
90    fn get(&self, cid: &Cid) -> BlockstoreResult<Option<Block>> {
91        let key = cid.to_string();
92
93        // Check cache first
94        {
95            let mut cache = self.cache.lock();
96            if let Some(block) = cache.get(&key) {
97                return Ok(Some(block.clone()));
98            }
99        }
100
101        // Cache miss - read from inner store
102        let result = self.inner.get(cid)?;
103
104        // Cache the result
105        if let Some(ref block) = result {
106            let mut cache = self.cache.lock();
107            cache.put(key.clone(), block.clone());
108            let mut has_cache = self.has_cache.lock();
109            has_cache.put(key, true);
110        }
111
112        Ok(result)
113    }
114
115    fn put(&mut self, block: Block) -> BlockstoreResult<()> {
116        let key = block.cid().to_string();
117
118        // Write to inner store first
119        self.inner.put(block.clone())?;
120
121        // Update caches
122        {
123            let mut cache = self.cache.lock();
124            cache.put(key.clone(), block);
125        }
126        {
127            let mut has_cache = self.has_cache.lock();
128            has_cache.put(key, true);
129        }
130
131        Ok(())
132    }
133
134    fn has(&self, cid: &Cid) -> BlockstoreResult<bool> {
135        let key = cid.to_string();
136
137        // Check has_cache first
138        {
139            let mut has_cache = self.has_cache.lock();
140            if let Some(&exists) = has_cache.get(&key) {
141                return Ok(exists);
142            }
143        }
144
145        // Also check block cache
146        {
147            let cache = self.cache.lock();
148            if cache.contains(&key) {
149                let mut has_cache = self.has_cache.lock();
150                has_cache.put(key, true);
151                return Ok(true);
152            }
153        }
154
155        // Cache miss - check inner store
156        let exists = self.inner.has(cid)?;
157
158        // Cache the result
159        {
160            let mut has_cache = self.has_cache.lock();
161            has_cache.put(key, exists);
162        }
163
164        Ok(exists)
165    }
166
167    fn delete(&mut self, cid: &Cid) -> BlockstoreResult<()> {
168        let key = cid.to_string();
169
170        // Delete from inner store
171        self.inner.delete(cid)?;
172
173        // Remove from caches
174        {
175            let mut cache = self.cache.lock();
176            cache.pop(&key);
177        }
178        {
179            let mut has_cache = self.has_cache.lock();
180            has_cache.pop(&key);
181        }
182
183        Ok(())
184    }
185
186    fn get_size(&self, cid: &Cid) -> BlockstoreResult<Option<usize>> {
187        let key = cid.to_string();
188
189        // Check cache first
190        {
191            let mut cache = self.cache.lock();
192            if let Some(block) = cache.get(&key) {
193                return Ok(Some(block.size()));
194            }
195        }
196
197        // Cache miss - check inner store
198        self.inner.get_size(cid)
199    }
200
201    fn all_keys_chan(&self) -> BlockstoreResult<Box<dyn Iterator<Item = Cid> + '_>> {
202        // Delegate to inner store (caching doesn't help here)
203        self.inner.all_keys_chan()
204    }
205
206    fn hash_on_read(&self) -> bool {
207        self.inner.hash_on_read()
208    }
209
210    fn set_hash_on_read(&mut self, enabled: bool) {
211        self.inner.set_hash_on_read(enabled);
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::FlatFsBlockstore;
219    use tempfile::tempdir;
220
221    #[test]
222    fn test_caching_blockstore_get() {
223        let dir = tempdir().unwrap();
224        let inner = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
225        let mut bs = CachingBlockstore::new(inner, 10);
226
227        // Store a block
228        let block = Block::new_raw(b"cached data".to_vec()).unwrap();
229        let cid = *block.cid();
230        bs.put(block.clone()).unwrap();
231
232        // First get (should populate cache)
233        let retrieved = bs.get(&cid).unwrap().unwrap();
234        assert_eq!(retrieved.data(), b"cached data");
235
236        // Check cache is populated
237        assert_eq!(bs.cache_len(), 1);
238
239        // Second get (should come from cache)
240        let retrieved2 = bs.get(&cid).unwrap().unwrap();
241        assert_eq!(retrieved2.data(), b"cached data");
242    }
243
244    #[test]
245    fn test_caching_blockstore_has() {
246        let dir = tempdir().unwrap();
247        let inner = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
248        let mut bs = CachingBlockstore::new(inner, 10);
249
250        let block = Block::new_raw(b"test".to_vec()).unwrap();
251        let cid = *block.cid();
252
253        // Initially not present
254        assert!(!bs.has(&cid).unwrap());
255
256        // Store the block
257        bs.put(block).unwrap();
258
259        // Now it exists
260        assert!(bs.has(&cid).unwrap());
261    }
262
263    #[test]
264    fn test_caching_blockstore_delete() {
265        let dir = tempdir().unwrap();
266        let inner = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
267        let mut bs = CachingBlockstore::new(inner, 10);
268
269        let block = Block::new_raw(b"delete me".to_vec()).unwrap();
270        let cid = *block.cid();
271
272        bs.put(block).unwrap();
273        assert!(bs.has(&cid).unwrap());
274        assert_eq!(bs.cache_len(), 1);
275
276        bs.delete(&cid).unwrap();
277        assert!(!bs.has(&cid).unwrap());
278        assert_eq!(bs.cache_len(), 0);
279    }
280
281    #[test]
282    fn test_caching_blockstore_stats() {
283        let dir = tempdir().unwrap();
284        let inner = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
285        let mut bs = CachingBlockstore::new(inner, 100);
286
287        for i in 0..10 {
288            let block = Block::new_raw(format!("block {}", i).into_bytes()).unwrap();
289            bs.put(block).unwrap();
290        }
291
292        let stats = bs.cache_stats();
293        assert_eq!(stats.block_cache_len, 10);
294        assert_eq!(stats.block_cache_cap, 100);
295    }
296
297    #[test]
298    fn test_caching_blockstore_clear() {
299        let dir = tempdir().unwrap();
300        let inner = FlatFsBlockstore::new_default(dir.path().join("blocks")).unwrap();
301        let mut bs = CachingBlockstore::new(inner, 10);
302
303        let block = Block::new_raw(b"clear test".to_vec()).unwrap();
304        let cid = *block.cid();
305        bs.put(block).unwrap();
306
307        assert_eq!(bs.cache_len(), 1);
308
309        bs.clear_cache();
310        assert_eq!(bs.cache_len(), 0);
311
312        // Block should still exist in underlying store
313        assert!(bs.has(&cid).unwrap());
314    }
315}