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        // THE LAST BLOCK OF A DEVICE IS OFTEN SHORT, and asking the
114        // device for a whole one past its end is an error rather than a
115        // short read. A SquashFS image is 4 KiB and its declared block
116        // size 128 KiB; without this clamp, caching such an image failed
117        // on the first read it ever made.
118        let size = self.inner.size_bytes();
119        let end = block_start.saturating_add(self.block_size).min(size);
120        let len = end.saturating_sub(block_start) as usize;
121        let mut block = vec![0u8; len];
122        self.inner.read_at(block_start, &mut block)?;
123        let data = Arc::new(block);
124
125        let mut s = self.state.lock().unwrap();
126        if s.entries.len() >= s.capacity {
127            s.entries.pop_back();
128        }
129        s.entries.push_front((block_start, data.clone()));
130        Ok(data)
131    }
132}
133
134impl BlockRead for CachingDevice {
135    /// # A read is served from the blocks it falls in, whatever its size
136    ///
137    /// This used to serve a read only when it was **exactly one aligned
138    /// block**, and pass everything else through untouched — including
139    /// reads of bytes it was already holding.
140    ///
141    /// The drivers almost never read a whole block. Measured on
142    /// `am-fs-xfs` against a fixture with a 4096-byte block size, the
143    /// average read during a directory walk was **1040 bytes**: inodes
144    /// are read at inode size and group headers at sector size, so
145    /// roughly three quarters of reads missed by construction.
146    ///
147    /// # What it costs
148    ///
149    /// A 512-byte read of an uncached block now fetches 4096. That is a
150    /// trade of bytes for calls, and it is the right way round for these
151    /// drivers: the block being fetched is the one holding the inode,
152    /// and the next inode read is very often in it.
153    ///
154    /// # Where it still passes through
155    ///
156    /// A read larger than the cache's own capacity would evict
157    /// everything to hold one answer, so anything spanning more blocks
158    /// than a useful fraction of the cache goes straight to the device.
159    /// File data is read in large pieces and would otherwise push out
160    /// the metadata this exists to keep.
161    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
162        if buf.is_empty() {
163            return Ok(());
164        }
165        let bs = self.block_size;
166        let first = offset / bs;
167        let last = (offset + buf.len() as u64 - 1) / bs;
168        let spanned = (last - first + 1) as usize;
169
170        // A read big enough to sweep the cache is not worth caching.
171        //
172        // A SINGLE BLOCK IS NEVER "BIG ENOUGH", however small the cache.
173        // Without that clause a cache of one block bypasses every read
174        // it is ever given -- one block is more than half of one block --
175        // so the smallest cache anybody can ask for is the one that
176        // silently does nothing.
177        let sweeps_the_cache = {
178            let s = self.state.lock().unwrap();
179            spanned > 1 && spanned * 2 > s.capacity
180        };
181        if sweeps_the_cache {
182            return self.inner.read_at(offset, buf);
183        }
184
185        // A READ RUNNING PAST THE END OF THE DEVICE IS THE DEVICE'S TO
186        // REFUSE. Serving it from clamped blocks would hand back a short
187        // answer with no error, which is worse than the failure the
188        // caller would otherwise have seen.
189        if offset.saturating_add(buf.len() as u64) > self.inner.size_bytes() {
190            return self.inner.read_at(offset, buf);
191        }
192
193        let mut done = 0usize;
194        for index in first..=last {
195            let block_start = index * bs;
196            let block = self.block(block_start)?;
197            // Where this block overlaps what was asked for.
198            let from = (offset.max(block_start) - block_start) as usize;
199            // Bounded by what the BLOCK holds rather than by the block
200            // size, since the last one may be short.
201            let take = (block.len().saturating_sub(from)).min(buf.len() - done);
202            if take == 0 {
203                break;
204            }
205            buf[done..done + take].copy_from_slice(&block[from..from + take]);
206            done += take;
207        }
208        Ok(())
209    }
210
211    fn size_bytes(&self) -> u64 {
212        self.inner.size_bytes()
213    }
214}
215
216impl BlockDevice for CachingDevice {
217    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
218        // THE CACHE IS INVALIDATED EVEN IF THE WRITE THEN FAILS, and
219        // deliberately: dropping entries the write would have made stale
220        // costs a re-read, while keeping them past a write that half
221        // succeeded serves bytes the device no longer holds.
222        let end = offset.saturating_add(buf.len() as u64);
223        {
224            let mut s = self.state.lock().unwrap();
225            let bs = self.block_size;
226            Self::invalidate_range(&mut s, offset, end, bs);
227        }
228        let Some(writable) = self.writable.as_ref() else {
229            return Err(crate::error::Error::ReadOnly);
230        };
231        writable.write_at(offset, buf)
232    }
233
234    fn flush(&self) -> Result<()> {
235        match self.writable.as_ref() {
236            Some(writable) => writable.flush(),
237            // Nothing was written, so there is nothing to flush. An
238            // error here would make a caller that flushes defensively
239            // fail on a read-only volume.
240            None => Ok(()),
241        }
242    }
243
244    fn is_writable(&self) -> bool {
245        self.writable.as_ref().is_some_and(|w| w.is_writable())
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::test_device::Bytes;
253
254    const BS: u64 = 512;
255
256    fn backing() -> Arc<Bytes> {
257        Arc::new(Bytes::new((0..4096u32).map(|i| i as u8).collect()))
258    }
259
260    /// THE CASE THAT COULD NOT BE EXPRESSED BEFORE: a device that is
261    /// only ever read, wrapped in a cache.
262    ///
263    /// Every driver in this family mounts through a `BlockRead`, so
264    /// this is not an exotic configuration — it is the ordinary one,
265    /// and requiring `BlockDevice` is why four of the six drivers used
266    /// no cache at all.
267    #[test]
268    fn a_read_only_device_can_be_cached() {
269        let inner = backing();
270        let cache = CachingDevice::read_only(inner, BS, 4);
271
272        let mut first = vec![0u8; BS as usize];
273        let mut again = vec![0u8; BS as usize];
274        cache.read_at(0, &mut first).expect("first read");
275        cache.read_at(0, &mut again).expect("second read");
276
277        assert_eq!(first, again, "the cache must serve what the device held");
278        assert_eq!(cache.stats(), (1, 1), "one hit after one miss");
279    }
280
281    /// THE CASE THE OLD HIT CONDITION MISSED: a read smaller than a
282    /// block, of a block already held.
283    ///
284    /// Serving only exact aligned blocks meant the drivers' ordinary
285    /// reads — an inode at inode size, a group header at sector size —
286    /// went to the device every time, even when the block containing
287    /// them was cached.
288    #[test]
289    fn a_read_smaller_than_a_block_is_served_from_it() {
290        let cache = CachingDevice::read_only(backing(), BS, 8);
291
292        let mut whole = vec![0u8; BS as usize];
293        cache.read_at(0, &mut whole).expect("warm the block");
294        assert_eq!(cache.stats(), (0, 1), "one miss to fetch it");
295
296        // Four sub-block reads inside the block just fetched.
297        for at in [0u64, 8, 100, 504] {
298            let mut small = [0u8; 8];
299            cache.read_at(at, &mut small).expect("sub-block read");
300            assert_eq!(
301                &small[..],
302                &whole[at as usize..at as usize + 8],
303                "the bytes must be the block's own, at the right offset"
304            );
305        }
306        assert_eq!(cache.stats(), (4, 1), "four hits, and no further misses");
307    }
308
309    /// A read crossing a block boundary is stitched from both blocks,
310    /// and each is cached.
311    #[test]
312    fn a_read_spanning_two_blocks_is_stitched() {
313        let inner = backing();
314        let mut direct = vec![0u8; 16];
315        inner.read_at(BS - 8, &mut direct).expect("read it plainly");
316
317        let cache = CachingDevice::read_only(backing(), BS, 8);
318        let mut across = vec![0u8; 16];
319        cache.read_at(BS - 8, &mut across).expect("spanning read");
320
321        assert_eq!(across, direct, "the same bytes the device would give");
322        assert_eq!(cache.stats(), (0, 2), "one miss per block touched");
323
324        cache.read_at(BS - 8, &mut across).expect("again");
325        assert_eq!(cache.stats(), (2, 2), "and both are held now");
326    }
327
328    /// A read big enough to sweep the cache goes straight to the device.
329    ///
330    /// File data arrives in large pieces, and caching it would evict the
331    /// metadata this exists to hold — the opposite of the point.
332    #[test]
333    fn a_read_that_would_sweep_the_cache_passes_through() {
334        let cache = CachingDevice::read_only(backing(), BS, 4);
335        let mut big = vec![0u8; (BS * 4) as usize];
336        cache.read_at(0, &mut big).expect("a large read");
337        assert_eq!(
338            cache.stats(),
339            (0, 0),
340            "neither hit nor miss: it never consulted the cache"
341        );
342    }
343
344    /// A device smaller than one block still reads.
345    ///
346    /// THE CASE THAT BROKE. `am-fs-squashfs` declares a block size from
347    /// the archive's superblock -- 128 KiB is the usual -- and a small
348    /// image is a few kilobytes whole. Fetching "the block at zero"
349    /// asked the device for 128 KiB it did not have, which is an error
350    /// rather than a short read, so opening such an image with a cache
351    /// failed on the very first read.
352    #[test]
353    fn a_device_shorter_than_a_block_still_reads() {
354        let tiny: Arc<Bytes> = Arc::new(Bytes::new((0..100u32).map(|i| i as u8).collect()));
355        let cache = CachingDevice::read_only(tiny, BS, 4);
356
357        let mut buf = vec![0u8; 40];
358        cache
359            .read_at(10, &mut buf)
360            .expect("a read inside the device");
361        assert_eq!(buf[0], 10, "the wrong bytes came back");
362        assert_eq!(buf[39], 49);
363
364        // And the second one is a hit, so the short block was cached
365        // rather than merely tolerated.
366        cache.read_at(10, &mut buf).expect("again");
367        assert_eq!(cache.stats(), (1, 1));
368    }
369
370    /// A read running past the end of the device still fails.
371    ///
372    /// The clamp above must not turn "you asked for bytes that are not
373    /// there" into a short answer with no error. That failure is
374    /// invisible to the caller, which is the one kind this family of
375    /// crates refuses to produce.
376    #[test]
377    fn a_read_past_the_end_is_still_an_error() {
378        let tiny: Arc<Bytes> = Arc::new(Bytes::new(vec![0u8; 100]));
379        let cache = CachingDevice::read_only(tiny, BS, 4);
380
381        let mut buf = vec![0u8; 40];
382        assert!(
383            cache.read_at(80, &mut buf).is_err(),
384            "80 + 40 is past the end of a 100-byte device"
385        );
386    }
387
388    /// A cache over a read-only device says so, and refuses a write
389    /// with the answer the device underneath would have given.
390    #[test]
391    fn writing_through_a_read_only_cache_is_refused() {
392        let cache = CachingDevice::read_only(backing(), BS, 4);
393        assert!(!cache.is_writable());
394        assert!(matches!(
395            cache.write_at(0, &[1u8; 8]),
396            Err(crate::error::Error::ReadOnly)
397        ));
398        // And flushing is not an error: a caller that flushes
399        // defensively must not fail on a volume it never wrote.
400        assert!(cache.flush().is_ok());
401    }
402}