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}