Skip to main content

FileDevice

Struct FileDevice 

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

A file opened as a block device.

§Readers share the lock; writers take it alone

A read was once seek then read under a plain mutex, which made the file’s cursor shared state: two threads reading different offsets had to take turns, not because the device could not serve them at once but because one would have moved the other’s cursor.

On Unix the cursor is not involved at all — pread takes the offset as an argument — so readers hold the lock shared and genuinely overlap. On Windows the equivalent (seek_read) does move the file pointer, so readers there take it exclusively and only that platform pays for the cursor.

Writers take it exclusively on both, because write_at is seek plus write_all and neither another writer’s seek nor a reader may land in the middle of it.

§THE LOCK IS NOT AN OPTIMISATION, IT IS THE READ/WRITE CONTRACT

Reads briefly took no lock at all, which read as a natural consequence of positioned reads needing no cursor. It was not: it silently dropped the exclusion between readers and writers that the single mutex had provided, so a read overlapping a write_at could observe part of it. write_all is permitted to become several write calls, and a read is a loop of positioned reads — either split is a window, and the second one does not need the first.

So a reader holds the lock for the whole of FileDevice::read_at, not for each positioned read inside it. Per-read guards would leave exactly the same hole one level down, and a rarer tear is worse than a common one because nobody can reproduce it.

§A known limit, stated rather than fixed

std::sync::RwLock does not promise writer preference on every platform, and the read path here is the hot one. A device under sustained parallel reads can therefore make a writer wait longer than a fair queue would. That is a throughput property, not a correctness one, and it is left alone rather than solved with a hand-rolled queue nobody would be able to audit.

Implementations§

Source§

impl FileDevice

Source

pub fn open<P: AsRef<Path>>(path: P) -> Result<Self>

Open read-only.

Source

pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self>

Open read-write. Errors if the path is not writable.

Source

pub fn open_best_effort<P: AsRef<Path>>(path: P) -> Result<Self>

Open read-write if possible, fall back to read-only otherwise.

Trait Implementations§

Source§

impl BlockDevice for FileDevice

Source§

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

A write past the end is refused, not an extension.

write_all at a seeked offset EXTENDS a file, and this method had no bound of its own, so a write straddling the end grew the backing store while size_bytes went on reporting the length taken at construction – measured on a 4096-byte file: write_at(4094, 8 bytes) returned Ok, the file became 4102 bytes, size_bytes() stayed 4096, and read_at(4096, 6) then handed those bytes back. The two halves of one device disagreed about where it ended, and a caller bounding its reads by size_bytes – which is what crate::CachingDevice does, clamping every block it fetches – could never reach them.

RwBytes in this crate’s own test devices already refuses the same operation, commenting “a device is not a Vec”, and the slice adapters in crate::slice clamp their window specifically because this method did not: slice_rw_length_is_clamped_and_a_write_past_it_does_not_grow_the_image names the file’s length on disk as its oracle. This is the same rule one layer down, where it was missing.

The alternative – letting the size move and reopening – is what BlockRead::size_bytes’s contract forbids. See rust-fs-core#70.

Source§

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

Set the file’s length, and the length this device reports, as one operation.

§THE POINT IS THAT THE TWO MOVE TOGETHER

#75 refused a write past the end because write_all at a seeked offset grew the FILE while size_bytes went on reporting the length taken at construction, so the two halves of one device disagreed about where it ended and a caller bounding its reads by size_bytes could never reach what it had written (rust-fs-core#70). That bound stays. This method is the other half: growth that says so, and moves the number with it.

An implementation that called File::set_len and left self.size alone would be #70 with a nicer name on it.

§THE NARROWER LENGTH IS PUBLISHED FIRST, IN BOTH DIRECTIONS

ftruncate and the store are two steps, and one of the two orderings has a window in it. Take a shrink done store-then- publish: between the truncate and the store, this device declares 8192 bytes over a 4096-byte file, so a concurrent read inside the declared device falls off the end of the real one and comes back ShortRead. The other direction is harmless — a device that briefly declares 4096 bytes over an 8192-byte file is only under-reporting, which is the state every FileDevice is in whenever something else appends to its file.

So: a GROW truncates and then stores, and a SHRINK stores and then truncates. The invariant is one sentence — THE DECLARED SIZE NEVER EXCEEDS THE FILE’S REAL LENGTH — and it holds at every instant rather than only at the ends.

§io_lock EXCLUSIVELY, LIKE A WRITE

For the same reason write_at and flush take it: this changes the file underneath every reader, and a read must not observe half of it. size_bytes deliberately does NOT take the lock — see the field — so the ordering above is what keeps a reader that asked the size mid-call from being misled, not the lock.

§WHAT IT REFUSES

Error::ReadOnly on a handle opened with FileDevice::open, before touching the file. Error::Custom on a handle that is writable but not a regular file — a block device node, whose length belongs to the kernel — naming that reason rather than letting ftruncate’s EINVAL stand in for it. See FileDevice::can_grow, which is the question to ask instead of discovering either of these.

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§

fn can_grow(&self) -> bool

Whether set_len is likely to succeed. Default: false. Read more
Source§

impl BlockRead for FileDevice

Source§

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

Read exactly buf.len() bytes starting at offset (bytes from the start of the device).
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.