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
impl CachingDevice
Sourcepub fn new(
inner: Arc<dyn BlockDevice>,
block_size: u64,
capacity: usize,
) -> Arc<Self> ⓘ
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.
Sourcepub fn read_only(
inner: Arc<dyn BlockRead>,
block_size: u64,
capacity: usize,
) -> Arc<Self> ⓘ
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));Sourcepub fn stats(&self) -> (u64, u64)
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, somissesis 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 misspub fn invalidate_all(&self)
Trait Implementations§
Source§impl BlockDevice for CachingDevice
impl BlockDevice for CachingDevice
Source§fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()>
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<()>
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
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 is_writable(&self) -> bool
fn is_writable(&self) -> bool
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
impl BlockRead for CachingDevice
Source§fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()>
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.