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 BlockRead for CachingDevice {
99    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
100        let cacheable =
101            buf.len() as u64 == self.block_size && offset.is_multiple_of(self.block_size);
102        if !cacheable {
103            return self.inner.read_at(offset, buf);
104        }
105
106        {
107            let mut s = self.state.lock().unwrap();
108            if let Some(pos) = s.entries.iter().position(|(o, _)| *o == offset) {
109                let entry = s.entries.remove(pos).unwrap();
110                buf.copy_from_slice(&entry.1);
111                s.entries.push_front(entry);
112                s.hits += 1;
113                return Ok(());
114            }
115            s.misses += 1;
116        }
117
118        self.inner.read_at(offset, buf)?;
119        let data = Arc::new(buf.to_vec());
120        let mut s = self.state.lock().unwrap();
121        if s.entries.len() >= s.capacity {
122            s.entries.pop_back();
123        }
124        s.entries.push_front((offset, data));
125        Ok(())
126    }
127
128    fn size_bytes(&self) -> u64 {
129        self.inner.size_bytes()
130    }
131}
132
133impl BlockDevice for CachingDevice {
134    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
135        // THE CACHE IS INVALIDATED EVEN IF THE WRITE THEN FAILS, and
136        // deliberately: dropping entries the write would have made stale
137        // costs a re-read, while keeping them past a write that half
138        // succeeded serves bytes the device no longer holds.
139        let end = offset.saturating_add(buf.len() as u64);
140        {
141            let mut s = self.state.lock().unwrap();
142            let bs = self.block_size;
143            Self::invalidate_range(&mut s, offset, end, bs);
144        }
145        let Some(writable) = self.writable.as_ref() else {
146            return Err(crate::error::Error::ReadOnly);
147        };
148        writable.write_at(offset, buf)
149    }
150
151    fn flush(&self) -> Result<()> {
152        match self.writable.as_ref() {
153            Some(writable) => writable.flush(),
154            // Nothing was written, so there is nothing to flush. An
155            // error here would make a caller that flushes defensively
156            // fail on a read-only volume.
157            None => Ok(()),
158        }
159    }
160
161    fn is_writable(&self) -> bool {
162        self.writable.as_ref().is_some_and(|w| w.is_writable())
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::test_device::Bytes;
170
171    const BS: u64 = 512;
172
173    fn backing() -> Arc<Bytes> {
174        Arc::new(Bytes::new((0..4096u32).map(|i| i as u8).collect()))
175    }
176
177    /// THE CASE THAT COULD NOT BE EXPRESSED BEFORE: a device that is
178    /// only ever read, wrapped in a cache.
179    ///
180    /// Every driver in this family mounts through a `BlockRead`, so
181    /// this is not an exotic configuration — it is the ordinary one,
182    /// and requiring `BlockDevice` is why four of the six drivers used
183    /// no cache at all.
184    #[test]
185    fn a_read_only_device_can_be_cached() {
186        let inner = backing();
187        let cache = CachingDevice::read_only(inner, BS, 4);
188
189        let mut first = vec![0u8; BS as usize];
190        let mut again = vec![0u8; BS as usize];
191        cache.read_at(0, &mut first).expect("first read");
192        cache.read_at(0, &mut again).expect("second read");
193
194        assert_eq!(first, again, "the cache must serve what the device held");
195        assert_eq!(cache.stats(), (1, 1), "one hit after one miss");
196    }
197
198    /// A cache over a read-only device says so, and refuses a write
199    /// with the answer the device underneath would have given.
200    #[test]
201    fn writing_through_a_read_only_cache_is_refused() {
202        let cache = CachingDevice::read_only(backing(), BS, 4);
203        assert!(!cache.is_writable());
204        assert!(matches!(
205            cache.write_at(0, &[1u8; 8]),
206            Err(crate::error::Error::ReadOnly)
207        ));
208        // And flushing is not an error: a caller that flushes
209        // defensively must not fail on a volume it never wrote.
210        assert!(cache.flush().is_ok());
211    }
212}