win-durable-ioring
A ring for Windows file I/O with durability scheduled into its work: the Windows driver for
durable-ioring, built on
windows-ioring-sys's IoRing. A consumer submits reads and
writes as it would to the ring, tags each write with an epoch, asks for durability with
make_durable_through(n), and learns -- in the same completion stream as its operations -- when
everything through n has become durable, or that it has failed and which writes are suspect as a
result.
What is here, and what is not
Here (DI-D-50): Dioring, the threaded driver
that implements the contract over the Windows backend; the backend and its file target
(windows_backend), the thread-pool and pumped and signalled delivery of its completions, the
readiness Event and the harness check of it (readiness), the interrupt-time clock the driver
stamps with, and a way to read an HRESULT out of an io::Error (error_code_of). It forbids
unsafe code: a Win32 call no lower crate wraps safely is added to that crate.
Not here: the contract, the core that decides when writes are durable, the target and backend traits, the in-memory backend and the conformance oracle. Those are durable-ioring, which has no Windows dependency, so another platform's driver can be built on it as this one is. The specification is that crate's CONTRACT.md; its spelling as Rust types is API.md. This crate re-exports the portable crate's API, so a consumer needs only this one.
A blocking push is not an API of either crate: a refused push names its cause, and the loop that waits it out is a maintained example, examples/blocking_push/, to use as it stands or adapt (DI-D-56).
DeliveryMode::Pool, DeliveryMode::Signalled and EntryDelivery::new take
windows-threadpool-sys's CallbackEnviron, so that crate's types are part of this crate's public
API. A minor release of windows-threadpool-sys that changes those types is a minor release of this
crate too, in the 0.x series: 0.3 of this crate pairs with 0.3 of windows-threadpool-sys.
Opening a file
A file target is made from a file this crate opened, and from nothing else
(DI-D-60). How a handle was opened decides what its
target can claim, and the only call that reports it is one this repository does not make, so the
open mode is recorded by the type that opens the file instead of asked of a handle afterwards.
OpenedFile<Cached> is a file opened for cached I/O: it claims BestEffort and needs no alignment.
OpenedFile<Unbuffered> is opened with no intermediate buffering: it claims SharedDevice, needs
a sector's alignment (4096 unless told less with with_sector_size), and offers per-write
write-through. A consumer that needs the stronger warranty asks for OpenedFile<Unbuffered> in its
own signature, and a cached file cannot be passed. A handle opened elsewhere cannot be given to a
target in this version.
use ;
use ;
let path = temp_dir.join;
write?;
let options = new.read.write;
let cached = new;
assert_eq!;
let unbuffered = new;
assert_eq!;
assert!;
# remove_file?;
# Ok::
An unbuffered file is refused on a remote device, where the claim cannot hold.
A raw volume
A raw volume is a target too (DI-D-61): one handle, one
commit per volume, and regions are offsets the consumer allocates inside it, not targets of their
own. OpenedVolume opens it by a path that can only name a volume, a drive letter or a volume GUID,
and finds what the backing must know for itself with documented device controls
(win-volume-sys): the disks it sits on, which become its flush
domains (disk_flush_domain names a disk, so a consumer can declare the same domain for a file on
it), and the sector it is addressed in, which is the alignment of its operations. A volume that is
remote, or not a volume, is refused. It claims SharedDevice when opened unbuffered and BestEffort when opened cached, and never
Complete: a lock keeps other handles off the volume, not other writers off its disks. Opening one for writing needs an
administrator, and VolumeOptions::lock takes the volume lock, which a write to a volume with a
file system on it needs and which fails if anything else has the volume open.
use ;
let path = drive?;
let volume = open?;
let target = from_volume;
# Ok::
A whole disk
OpenedDisk opens a physical disk by its number, \\.\PhysicalDriveN, rather than a volume on it
(DI-D-62). The disk is its own flush domain, named by
disk_flush_domain as a volume on it would name it, and its operations are aligned to its sector,
which a device control reports. It claims SharedDevice when opened unbuffered and BestEffort when opened cached, never
Complete, for the reason a volume does not. Windows documents that it blocks a direct write to a sector a mounted volume covers, so a
disk with volumes on it is written through them, or has them locked or is taken offline first; the
tests here use a disk with no partition. Opening one for writing needs an administrator.
use ;
let disk = open?;
let target = from_disk;
# Ok::
Worked examples
Each example is dioring running the trace in its comments, compiled and run as a test, so a change
that breaks one breaks the build. In the comments, -> is a call the consumer makes and <- an
entry it pops. Every write is in the default lineage, so the lineage is not shown; construction
returns a handle to it, and tag(n) is its epoch n as a write or a seal takes it, epoch(n) as a
query or an entry names it. A commit failure cannot be provoked on a healthy machine through a Win32
file, so each example arms one with dioring's fault seam, fail_next_flush, which makes the next
flush of a file report the error given whatever the device answered. Setting up the instance, and a
pop_until that pops until an entry it is looking for, are hidden.
Healing a failure
# use ;
# use *;
# type Ring = ;
# const FILE: IoTargetKey = IoTargetKey;
#
# let path = temp_dir.join;
# write?;
# let file = open?;
# let = new
# .expect;
let tag = ; // what a write or a seal takes
let epoch = ; // what a query or an entry names
// -> write(tag 41, A) ; write(tag 41, B) ; write(tag 41, C)
for offset in
ring.fail_next_flush; // ERROR_IO_DEVICE, for the flush the seal issues
// -> make_durable_through(41) // seals everything <= 41
ring.make_durable_through.expect;
// <- WriteDone(A) ; WriteDone(B) ; WriteDone(C)
// <- Failed { failure: F1, cause: Commit, suspect: [A@41, B@41, C@41] }
let Failed = pop_until else ;
assert!;
assert_eq!;
assert_eq!; // nothing is durable yet
// -> write(tag 41, A') // refused: 41 is sealed (guarantee 6)
let refused = ring.write.expect_err;
assert!;
// -> write(tag 42, A') ; write(tag 42, B') ; write(tag 42, C')
for offset in
// -> heal(F1) // "I have re-issued what I need"
ring.resolve.expect;
// -> make_durable_through(42) // the first after the heal reaching the re-issue
ring.make_durable_through.expect;
// <- WriteDone(A') ; WriteDone(B') ; WriteDone(C')
// <- Durable { through: 42 } // F1 healed; 41 and 42 durable
pop_until;
assert_eq!;
assert!;
# drop;
# remove_file?;
# Ok::
Without the heal, the last entry is Blocked { through: 42, by: F1 }: 42's own commit succeeded, but
the high-water mark cannot pass the unresolved failure at 41.
Failures and epochs are many-to-many
P is pushed before the seal, rather than after it as a consumer might, so that it is certainly
pushed before the failure is observed: the seal's commit is issued as soon as A completes.
# use ;
# use *;
# type Ring = ;
# const FILE: IoTargetKey = IoTargetKey;
#
# let path = temp_dir.join;
# write?;
# let file = open?;
# let = new
# .expect;
let tag = ; // what a write or a seal takes
let epoch = ; // what a query or an entry names
let ops = ;
// -> write(tag 41, A) ; write(tag 43, P) // 43 is open
let a = ring.write.expect;
let p = ring.write.expect;
ring.fail_next_flush;
// -> make_durable_through(41) // seals 41
ring.make_durable_through.expect;
// <- WriteDone(A) ; WriteDone(P)
// <- Failed { failure: F1, cause: Commit, suspect: [A@41, P@43] }
let Failed = pop_until else ;
assert_eq!; // one failure, two epochs: sealed 41 and open 43
// -> write(tag 43, R) // still allowed: 43 is open
let r = ring.write.expect;
// <- WriteDone(R)
pop_until;
// -> import_failure() // the consumer learned of an outside failure
let f2_id = ring.import_failure.expect.failure;
// <- Failed { failure: F2, cause: Imported, suspect: [A@41, P@43, R@43] }
let Failed = pop_until else ;
assert_eq!;
assert_eq!; // epoch 43 now sits in F1 and F2
// The first ending: heal both. Re-issue A, P and R under tag 44, resolve both in one atomic
// call, and seal 44.
for offset in
ring.resolve
.expect;
ring.make_durable_through.expect;
// <- Durable { through: 44 } // 41 and 43 passed once both failures were resolved
pop_until;
assert_eq!;
assert_eq!;
# drop;
# remove_file?;
# Ok::
The second ending: heal F1, abandon F2. Epochs 41 and 43 both sit in F2, so both become abandoned -- one abandonment is enough -- even though F1 was healed. The high-water mark moves past them once every failure containing them is resolved: F2 at once, and F1 when its heal takes effect, at the first seal made after it that reaches the re-issue. Any operation gated on 41 or 43 fails as never issued.
# use ;
# use *;
# type Ring = ;
# const FILE: IoTargetKey = IoTargetKey;
#
# let path = temp_dir.join;
# write?;
# let file = open?;
# let = new
# .expect;
# let tag = ;
# let epoch = ;
# ring.write.expect;
# ring.write.expect;
# ring.fail_next_flush;
# ring.make_durable_through.expect;
# let Failed = pop_until else ;
# let r = ring.write.expect;
# pop_until;
# ring.import_failure.expect;
# let Failed = pop_until else ;
// The trace above, to F2's arrival, is hidden. Then:
ring.resolve
.expect;
// <- Abandoned { failure: F2, suspect: [A@41, P@43, R@43] }
pop_until;
assert_eq!;
assert_eq!;
// -> make_durable_through(44) // F1's heal takes effect here
ring.make_durable_through.expect;
// <- Durable { through: 44 } // "durable through 44" means durable or abandoned
pop_until;
assert_eq!;
assert_eq!;
# drop;
# remove_file?;
# Ok::