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).
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 OwnedHandle;
# use ;
# use *;
# type Ring = ;
# const FILE: IoTargetKey = IoTargetKey;
#
# let path = temp_dir.join;
# write?;
# let file = new.read.write.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 OwnedHandle;
# use ;
# use *;
# type Ring = ;
# const FILE: IoTargetKey = IoTargetKey;
#
# let path = temp_dir.join;
# write?;
# let file = new.read.write.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 OwnedHandle;
# use ;
# use *;
# type Ring = ;
# const FILE: IoTargetKey = IoTargetKey;
#
# let path = temp_dir.join;
# write?;
# let file = new.read.write.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::