Skip to main content

fs_core/
caching_device.rs

1//! Small LRU read-cache decorator. Caches only block-aligned, block-sized
2//! reads; everything else passes through. Writes invalidate any overlapping
3//! cached entries.
4
5use crate::block::{BlockDevice, BlockRead};
6use crate::error::Result;
7use std::collections::VecDeque;
8use std::sync::{Arc, Mutex};
9
10/// LRU read-cache wrapper.
11///
12/// # It caches a READ device, and writes through one only if it has one
13///
14/// This took an `Arc<dyn BlockDevice>` — the read *write* trait — and
15/// every driver in this family mounts a volume through an
16/// `Arc<dyn BlockRead>`. So a read-only mount could not wrap it at all,
17/// and four of the six drivers used no cache: not by choice, but
18/// because it was not expressible.
19///
20/// The read path never needed to write. It holds the read half now, and
21/// the writable half only when the caller had one to give:
22/// [`CachingDevice::new`] for a device that can be written,
23/// [`CachingDevice::read_only`] for one that cannot. A write to a cache
24/// built the second way is [`Error::ReadOnly`], which is what the
25/// underlying device would have said.
26pub struct CachingDevice {
27    inner: Arc<dyn BlockRead>,
28    /// The same device again, present only when it can be written. Held
29    /// separately rather than as one handle so that "can this be
30    /// written" is a property of the type rather than a flag someone
31    /// has to remember to check.
32    writable: Option<Arc<dyn BlockDevice>>,
33    block_size: u64,
34    state: Mutex<CacheState>,
35}
36
37struct CacheState {
38    /// Fixed-capacity LRU; head is most-recently used.
39    entries: VecDeque<(u64, Arc<Vec<u8>>)>,
40    capacity: usize,
41    hits: u64,
42    misses: u64,
43}
44
45impl CachingDevice {
46    /// Cache a device that can be written. Writes invalidate the
47    /// entries they overlap and go through to `inner`.
48    pub fn new(inner: Arc<dyn BlockDevice>, block_size: u64, capacity: usize) -> Arc<Self> {
49        Arc::new(Self {
50            inner: inner.clone(),
51            writable: Some(inner),
52            block_size,
53            state: Mutex::new(CacheState {
54                entries: VecDeque::with_capacity(capacity),
55                capacity,
56                hits: 0,
57                misses: 0,
58            }),
59        })
60    }
61
62    /// Cache a device that is only ever read.
63    ///
64    /// The case every driver here actually has: a volume mounted for
65    /// reading, behind a `BlockRead` that was never a `BlockDevice`.
66    pub fn read_only(inner: Arc<dyn BlockRead>, block_size: u64, capacity: usize) -> Arc<Self> {
67        Arc::new(Self {
68            inner,
69            writable: None,
70            block_size,
71            state: Mutex::new(CacheState {
72                entries: VecDeque::with_capacity(capacity),
73                capacity,
74                hits: 0,
75                misses: 0,
76            }),
77        })
78    }
79
80    pub fn stats(&self) -> (u64, u64) {
81        let s = self.state.lock().unwrap();
82        (s.hits, s.misses)
83    }
84
85    pub fn invalidate_all(&self) {
86        let mut s = self.state.lock().unwrap();
87        s.entries.clear();
88    }
89
90    fn invalidate_range(state: &mut CacheState, start: u64, end: u64, block_size: u64) {
91        state.entries.retain(|(off, _)| {
92            let block_end = off.saturating_add(block_size);
93            *off >= end || block_end <= start
94        });
95    }
96}
97
98impl CachingDevice {
99    /// The cached block at `block_start`, fetching it if it is not held.
100    fn block(&self, block_start: u64) -> Result<Arc<Vec<u8>>> {
101        {
102            let mut s = self.state.lock().unwrap();
103            if let Some(pos) = s.entries.iter().position(|(o, _)| *o == block_start) {
104                let entry = s.entries.remove(pos).expect("position just found it");
105                let data = entry.1.clone();
106                s.entries.push_front(entry);
107                s.hits += 1;
108                return Ok(data);
109            }
110            s.misses += 1;
111        }
112
113        let mut block = vec![0u8; self.block_size as usize];
114        self.inner.read_at(block_start, &mut block)?;
115        let data = Arc::new(block);
116
117        let mut s = self.state.lock().unwrap();
118        if s.entries.len() >= s.capacity {
119            s.entries.pop_back();
120        }
121        s.entries.push_front((block_start, data.clone()));
122        Ok(data)
123    }
124}
125
126impl BlockRead for CachingDevice {
127    /// # A read is served from the blocks it falls in, whatever its size
128    ///
129    /// This used to serve a read only when it was **exactly one aligned
130    /// block**, and pass everything else through untouched — including
131    /// reads of bytes it was already holding.
132    ///
133    /// The drivers almost never read a whole block. Measured on
134    /// `am-fs-xfs` against a fixture with a 4096-byte block size, the
135    /// average read during a directory walk was **1040 bytes**: inodes
136    /// are read at inode size and group headers at sector size, so
137    /// roughly three quarters of reads missed by construction.
138    ///
139    /// # What it costs
140    ///
141    /// A 512-byte read of an uncached block now fetches 4096. That is a
142    /// trade of bytes for calls, and it is the right way round for these
143    /// drivers: the block being fetched is the one holding the inode,
144    /// and the next inode read is very often in it.
145    ///
146    /// # Where it still passes through
147    ///
148    /// A read larger than the cache's own capacity would evict
149    /// everything to hold one answer, so anything spanning more blocks
150    /// than a useful fraction of the cache goes straight to the device.
151    /// File data is read in large pieces and would otherwise push out
152    /// the metadata this exists to keep.
153    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
154        if buf.is_empty() {
155            return Ok(());
156        }
157        let bs = self.block_size;
158        let first = offset / bs;
159        let last = (offset + buf.len() as u64 - 1) / bs;
160        let spanned = (last - first + 1) as usize;
161
162        // A read big enough to sweep the cache is not worth caching.
163        //
164        // A SINGLE BLOCK IS NEVER "BIG ENOUGH", however small the cache.
165        // Without that clause a cache of one block bypasses every read
166        // it is ever given -- one block is more than half of one block --
167        // so the smallest cache anybody can ask for is the one that
168        // silently does nothing.
169        let sweeps_the_cache = {
170            let s = self.state.lock().unwrap();
171            spanned > 1 && spanned * 2 > s.capacity
172        };
173        if sweeps_the_cache {
174            return self.inner.read_at(offset, buf);
175        }
176
177        let mut done = 0usize;
178        for index in first..=last {
179            let block_start = index * bs;
180            let block = self.block(block_start)?;
181            // Where this block overlaps what was asked for.
182            let from = offset.max(block_start) - block_start;
183            let take = ((bs - from) as usize).min(buf.len() - done);
184            buf[done..done + take].copy_from_slice(&block[from as usize..from as usize + take]);
185            done += take;
186        }
187        Ok(())
188    }
189
190    fn size_bytes(&self) -> u64 {
191        self.inner.size_bytes()
192    }
193}
194
195impl BlockDevice for CachingDevice {
196    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
197        // THE CACHE IS INVALIDATED EVEN IF THE WRITE THEN FAILS, and
198        // deliberately: dropping entries the write would have made stale
199        // costs a re-read, while keeping them past a write that half
200        // succeeded serves bytes the device no longer holds.
201        let end = offset.saturating_add(buf.len() as u64);
202        {
203            let mut s = self.state.lock().unwrap();
204            let bs = self.block_size;
205            Self::invalidate_range(&mut s, offset, end, bs);
206        }
207        let Some(writable) = self.writable.as_ref() else {
208            return Err(crate::error::Error::ReadOnly);
209        };
210        writable.write_at(offset, buf)
211    }
212
213    fn flush(&self) -> Result<()> {
214        match self.writable.as_ref() {
215            Some(writable) => writable.flush(),
216            // Nothing was written, so there is nothing to flush. An
217            // error here would make a caller that flushes defensively
218            // fail on a read-only volume.
219            None => Ok(()),
220        }
221    }
222
223    fn is_writable(&self) -> bool {
224        self.writable.as_ref().is_some_and(|w| w.is_writable())
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::test_device::Bytes;
232
233    const BS: u64 = 512;
234
235    fn backing() -> Arc<Bytes> {
236        Arc::new(Bytes::new((0..4096u32).map(|i| i as u8).collect()))
237    }
238
239    /// THE CASE THAT COULD NOT BE EXPRESSED BEFORE: a device that is
240    /// only ever read, wrapped in a cache.
241    ///
242    /// Every driver in this family mounts through a `BlockRead`, so
243    /// this is not an exotic configuration — it is the ordinary one,
244    /// and requiring `BlockDevice` is why four of the six drivers used
245    /// no cache at all.
246    #[test]
247    fn a_read_only_device_can_be_cached() {
248        let inner = backing();
249        let cache = CachingDevice::read_only(inner, BS, 4);
250
251        let mut first = vec![0u8; BS as usize];
252        let mut again = vec![0u8; BS as usize];
253        cache.read_at(0, &mut first).expect("first read");
254        cache.read_at(0, &mut again).expect("second read");
255
256        assert_eq!(first, again, "the cache must serve what the device held");
257        assert_eq!(cache.stats(), (1, 1), "one hit after one miss");
258    }
259
260    /// THE CASE THE OLD HIT CONDITION MISSED: a read smaller than a
261    /// block, of a block already held.
262    ///
263    /// Serving only exact aligned blocks meant the drivers' ordinary
264    /// reads — an inode at inode size, a group header at sector size —
265    /// went to the device every time, even when the block containing
266    /// them was cached.
267    #[test]
268    fn a_read_smaller_than_a_block_is_served_from_it() {
269        let cache = CachingDevice::read_only(backing(), BS, 8);
270
271        let mut whole = vec![0u8; BS as usize];
272        cache.read_at(0, &mut whole).expect("warm the block");
273        assert_eq!(cache.stats(), (0, 1), "one miss to fetch it");
274
275        // Four sub-block reads inside the block just fetched.
276        for at in [0u64, 8, 100, 504] {
277            let mut small = [0u8; 8];
278            cache.read_at(at, &mut small).expect("sub-block read");
279            assert_eq!(
280                &small[..],
281                &whole[at as usize..at as usize + 8],
282                "the bytes must be the block's own, at the right offset"
283            );
284        }
285        assert_eq!(cache.stats(), (4, 1), "four hits, and no further misses");
286    }
287
288    /// A read crossing a block boundary is stitched from both blocks,
289    /// and each is cached.
290    #[test]
291    fn a_read_spanning_two_blocks_is_stitched() {
292        let inner = backing();
293        let mut direct = vec![0u8; 16];
294        inner.read_at(BS - 8, &mut direct).expect("read it plainly");
295
296        let cache = CachingDevice::read_only(backing(), BS, 8);
297        let mut across = vec![0u8; 16];
298        cache.read_at(BS - 8, &mut across).expect("spanning read");
299
300        assert_eq!(across, direct, "the same bytes the device would give");
301        assert_eq!(cache.stats(), (0, 2), "one miss per block touched");
302
303        cache.read_at(BS - 8, &mut across).expect("again");
304        assert_eq!(cache.stats(), (2, 2), "and both are held now");
305    }
306
307    /// A read big enough to sweep the cache goes straight to the device.
308    ///
309    /// File data arrives in large pieces, and caching it would evict the
310    /// metadata this exists to hold — the opposite of the point.
311    #[test]
312    fn a_read_that_would_sweep_the_cache_passes_through() {
313        let cache = CachingDevice::read_only(backing(), BS, 4);
314        let mut big = vec![0u8; (BS * 4) as usize];
315        cache.read_at(0, &mut big).expect("a large read");
316        assert_eq!(
317            cache.stats(),
318            (0, 0),
319            "neither hit nor miss: it never consulted the cache"
320        );
321    }
322
323    /// A cache over a read-only device says so, and refuses a write
324    /// with the answer the device underneath would have given.
325    #[test]
326    fn writing_through_a_read_only_cache_is_refused() {
327        let cache = CachingDevice::read_only(backing(), BS, 4);
328        assert!(!cache.is_writable());
329        assert!(matches!(
330            cache.write_at(0, &[1u8; 8]),
331            Err(crate::error::Error::ReadOnly)
332        ));
333        // And flushing is not an error: a caller that flushes
334        // defensively must not fail on a volume it never wrote.
335        assert!(cache.flush().is_ok());
336    }
337}