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}