Skip to main content

CachingDevice

Struct CachingDevice 

Source
pub struct CachingDevice { /* private fields */ }
Expand description

LRU read-cache wrapper.

§It caches a READ device, and writes through one only if it has one

This took an Arc<dyn BlockDevice> — the read write trait — and every driver in this family mounts a volume through an Arc<dyn BlockRead>. So a read-only mount could not wrap it at all, and four of the six drivers used no cache: not by choice, but because it was not expressible.

The read path never needed to write. It holds the read half now, and the writable half only when the caller had one to give: CachingDevice::new for a device that can be written, CachingDevice::read_only for one that cannot. A write to a cache built the second way is crate::Error::ReadOnly, which is what the underlying device would have said.

Implementations§

Source§

impl CachingDevice

Source

pub fn new( inner: Arc<dyn BlockDevice>, block_size: u64, capacity: usize, ) -> Arc<Self> ⓘ

Cache a device that can be written. Writes invalidate the entries they overlap and go through to inner.

The invalidation holds against concurrent readers as well as sequential ones: once write_at has returned, no later read can be served pre-write bytes from this cache, including a read that was already in flight when the write began. What such a read returns is still either side of the write — that is what racing means — but it is not remembered.

block_size must be non-zero and no larger than MAX_BLOCK_SIZE; see CachingDevice::read_only for why that is enforced at first use rather than here.

capacity is documented on CachingDevice::read_only; it means the same here, including that 0 still caches one block.

Source

pub fn read_only( inner: Arc<dyn BlockRead>, block_size: u64, capacity: usize, ) -> Arc<Self> ⓘ

Cache a device that is only ever read.

The case every driver here actually has: a volume mounted for reading, behind a BlockRead that was never a BlockDevice.

block_size must be non-zero and no larger than MAX_BLOCK_SIZE. Construction cannot refuse — it returns Arc<Self>, not Result — so a block size outside that range is refused by every read and every write instead, with an error naming the offending size.

capacity is a count of blocks, not bytes: at most that many entries of up to block_size bytes each are held, so the memory bound is their product and is the caller’s to choose. There is no ceiling; promotion and eviction are O(1) at any capacity.

capacity = 0 does not disable the cache. It holds one entry, so a repeated single-block read is still served as a hit, and two alternating blocks thrash it (#124). The behaviour is deliberate and pinned, not an oversight. A caller that wants no cache at zero should not construct one, and this constructor cannot do that for it because it returns Arc<Self>:

let dev: Arc<dyn BlockRead> = if blocks == 0 {
    dev
} else {
    CachingDevice::read_only(dev, block_size, blocks)
};
let device = Arc::new(CountingDevice::new(Arc::new(Mem(vec![7; 512 * 8]))));
let cache = CachingDevice::read_only(device.clone(), 512, 0);
let mut buf = [0u8; 16];
cache.read_at(0, &mut buf).unwrap();
cache.read_at(8, &mut buf).unwrap();
// A capacity of zero served the second read from the cache.
assert_eq!(device.reads(), 1);
assert_eq!(cache.stats(), (1, 1));
Source

pub fn stats(&self) -> (u64, u64)

(hits, misses), in that order.

Both count block lookups inside the cache, not reads of this device and not reads of inner (#123):

  • a hit is a block served from the cache, including one that was waiting on another thread’s fetch of the same block;
  • a miss is a block this cache fetched from inner, so misses is the number of fetches the cache itself made.

A read the cache declines to serve moves neither counter. A read reaching past the end of inner, and a read spanning enough blocks that caching it would sweep the cache (more than one block, and more than half of capacity), go straight to inner; an empty read, or one refused for its block size, touches nothing. So hits + misses is not the number of read_at calls, misses is a lower bound on the reads inner saw, and hits / (hits + misses) is a rate over the reads the cache served, which leaves out every read it chose not to. To count what reached the device, put a CountingDevice under the cache.

let device = Arc::new(CountingDevice::new(Arc::new(Mem(vec![7; 512 * 32]))));
let cache = CachingDevice::read_only(device.clone(), 512, 8);
let mut one = [0u8; 16];
cache.read_at(0, &mut one).unwrap(); // miss: fetched
cache.read_at(8, &mut one).unwrap(); // hit
let mut big = vec![0u8; 512 * 6];
cache.read_at(0, &mut big).unwrap(); // 6 blocks > capacity / 2: bypassed
assert_eq!(cache.stats(), (1, 1));
assert_eq!(device.reads(), 2); // the bypassed read is not a miss
Source

pub fn invalidate_all(&self)

Trait Implementations§

Source§

impl BlockDevice for CachingDevice

Source§

fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()>

§THE CACHE IS INVALIDATED EVEN IF THE WRITE THEN FAILS

Deliberately: dropping entries the write would have made stale costs a re-read, while keeping them past a write that half succeeded serves bytes the device no longer holds.

That applies to a write the device refused, not to one this type refused on the device’s behalf. A write rejected because there is no writable half, or because the block size is unusable, never reaches the device and so cannot have staled anything — those return above both sweeps and leave the cache exactly as it was.

§AND IT IS INVALIDATED TWICE, ONCE EITHER SIDE OF THE DEVICE

One sweep before the write is not enough. Between it and the device write landing, a concurrent CachingDevice::read_at can miss, fetch pre-write bytes, and insert them behind the sweep — an entry the sweep has already gone past and nothing else would ever drop. The second sweep is what closes that window, together with the generation check on the miss path that refuses such an insert outright.

Both are needed, and each covers what the other cannot. A read that began BEFORE this write is caught by the counter, because it recorded the generation before the first sweep bumped it. A read that begins AFTER that sweep records the bumped value, so the counter agrees with it and only the second sweep drops what it inserted.

Source§

fn set_len(&self, new_len: u64) -> Result<()>

§THE CACHE’S VIEW HAS TO MOVE WITH THE DEVICE, OR THIS IS #70

size_bytes here forwards to the device, so the NUMBER follows a grow for free. The entries do not. block() clamps every fetch to size_bytes() at the moment it runs — “the last block of a device is often short”, as its own comment says — so an entry fetched before the grow ends where the device used to. Leave it in place and a later read across the old end is served from it, runs out of bytes, and comes back ShortRead for a region the device now holds perfectly well.

Measured, with this method forwarding to the device and sweeping nothing: a 6000-byte file behind a 4096-byte cache, the short block warmed, set_len(8192), then one read across the old end:

ShortRead { offset: 5000, want: 3000, got: 1000 }

That is rust-fs-core#70’s signature exactly — the grow succeeded, the file and the reported size agreed, and only a LATER CACHED READ found the hole. It is why the growth API is a change to this file as much as to block.rs.

§FROM min(old, new) UPWARDS, WHICHEVER WAY THE LENGTH WENT

A grow only makes the block STRADDLING the old end wrong, and that block starts below the old end — so the sweep has to begin at the old length, not at the first block boundary above it, and invalidate_range drops any block whose end passes start.

A shrink makes everything from the new length up wrong instead. Taking the smaller of the two covers both without asking which happened, and the upper bound is u64::MAX because “the rest of the device” is what changed in either case.

§min RATHER THAN old, AND NO TEST HERE CAN TELL THEM APART

Said plainly because the alternative is a comment claiming a guarantee nobody measured. Sweeping from the OLD length alone leaves the blocks between the two lengths cached after a shrink, and that suite is EXIT=0 – every arm in tests/device_growth.rs passes with min removed.

It passes because nothing can read those entries. read_at forwards any read whose end passes size_bytes() straight to the device rather than serving it from blocks, so while the device is short they are unreachable; and a later grow sweeps from the smaller of ITS two lengths, which is the shrunk one, so they are dropped before they become reachable again.

min ships anyway, and not for symmetry. The argument above rests on a bound in a DIFFERENT METHOD holding forever – an entry that is stale but currently unreadable is one guard away from being stale and readable. This is the cheaper half of the invariant to state correctly, so it is stated correctly here rather than derived from somewhere else on every future read of this file.

§AND IT IS SWEPT TWICE, ONCE EITHER SIDE, FOR write_at’S REASON

One sweep before is not enough. Between it and the device call landing, a concurrent CachingDevice::read_at can miss, fetch a block clamped to the OLD length, and insert it behind the sweep. The second sweep closes that window, together with the generation check on the miss path. Each covers what the other cannot, in the same way and for the same reason write_at documents at length.

Both run whether or not the device call succeeded, also for write_at’s reason: a set_len that failed may still have moved the file, and dropping entries needlessly costs a re-read while keeping stale ones serves bytes the device no longer has.

§THE TWO REFUSALS ABOVE THE SWEEPS

No writable half, and an unusable block size — the same pair write_at refuses on, in the same order, and above both sweeps for the same two reasons. Error::ReadOnly rather than Error::Custom when there is nothing to write, because crate::stream maps only the first to PermissionDenied; and the block-size check above the sweeps because invalidate_range cannot sweep correctly with a block size of zero, so sweeping first and refusing after would do the one thing this type must never do on the way to reporting an error. Neither refusal reaches the device, so neither can have staled anything.

Source§

fn can_grow(&self) -> bool

The writable half’s answer, or false when there is no writable half — a cache cannot grow a device it can only read.

Source§

fn flush(&self) -> Result<()>

Flush pending writes to stable storage. No-op by default.
Source§

fn is_writable(&self) -> bool

Whether write_at is likely to succeed. Mount paths use this to decide whether to attempt journal replay or stay strict-read-only.
Source§

impl BlockRead for CachingDevice

Source§

fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()>

§A read is served from the blocks it falls in, whatever its size

This used to serve a read only when it was exactly one aligned block, and pass everything else through untouched — including reads of bytes it was already holding.

The drivers almost never read a whole block. Measured on am-fs-xfs against a fixture with a 4096-byte block size, the average read during a directory walk was 1040 bytes: inodes are read at inode size and group headers at sector size, so roughly three quarters of reads missed by construction.

§What it costs

A 512-byte read of an uncached block now fetches 4096. That is a trade of bytes for calls, and it is the right way round for these drivers: the block being fetched is the one holding the inode, and the next inode read is very often in it.

§Where it still passes through

A read larger than the cache’s own capacity would evict everything to hold one answer, so anything spanning more blocks than a useful fraction of the cache goes straight to the device. File data is read in large pieces and would otherwise push out the metadata this exists to keep.

Source§

fn size_bytes(&self) -> u64

Total device size in bytes. Used for bounds checks. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.