#![cfg(all(windows, feature = "fault-injection"))]
use std::os::windows::io::AsRawHandle;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use windows_ioring_sys::contract::RingContract;
use windows_ioring_sys::{
Batch, Completion, FlushCoverage, FlushMode, InjectedFailure, IoBuf, IoBufMut, IoRing,
IoRingErrorExt, PushOptions, RingCondition, SharedHandle, WriteCaching,
};
mod common;
fn temp_file(tag: &str) -> common::TempPath {
common::TempPath::new("failure-paths", tag)
}
fn await_one<T, X>(ring: &mut IoRing<T, X>) -> Completion {
ring.pop_within(std::time::Duration::from_secs(30))
.expect("pop a completion")
.expect("a completion arrived within the bound")
.0
}
#[test]
fn a_failed_read_still_hands_its_buffer_back_when_claimed() {
let path = temp_file("read");
std::fs::write(&path, b"hello").expect("create the fixture");
let file = std::fs::OpenOptions::new()
.read(true)
.open(&path)
.expect("open the fixture");
let mut ring = IoRing::<Vec<u8>>::with_inventory(16, 16).expect("create a ring");
let mut contract = RingContract::new();
let mut batch = Batch::new(&mut ring);
let id = unsafe {
batch.read_raw_owned(
file.as_raw_handle(),
vec![0_u8; 5],
(),
0,
PushOptions::new(),
)
}
.expect("queue a read");
contract.observe_push(id.user_data());
batch.submit_and_wait(1, 30_000).expect("submit and wait");
let (completion, held) = ring
.pop_within(std::time::Duration::from_secs(30))
.expect("pop a completion")
.expect("a completion arrived within the bound");
contract.observe_completion(completion.user_data());
let (buffer, ()) = held.expect("the ring was holding this read's buffer");
let failed = completion.with_injected_failure(InjectedFailure::Ring(RingCondition::Corrupt));
assert!(failed.result().is_err(), "the failure applies");
assert_eq!(
buffer.expect("a read carries a buffer"),
b"hello",
"the buffer comes back on the failure path too -- the pop is what returns it"
);
contract.assert_quiescent();
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_failed_write_still_hands_its_buffer_back_when_claimed() {
let path = temp_file("write");
std::fs::write(&path, vec![0_u8; 16]).expect("create the fixture");
let file = std::fs::OpenOptions::new()
.write(true)
.open(&path)
.expect("open the fixture");
let mut ring = IoRing::<Vec<u8>>::with_inventory(16, 16).expect("create a ring");
let mut contract = RingContract::new();
let mut batch = Batch::new(&mut ring);
let id = unsafe {
batch.write_raw_owned(
file.as_raw_handle(),
vec![7_u8; 4],
(),
0,
PushOptions::new(),
WriteCaching::Cached,
)
}
.expect("queue a write");
contract.observe_push(id.user_data());
batch.submit_and_wait(1, 30_000).expect("submit and wait");
let (completion, held) = ring
.pop_within(std::time::Duration::from_secs(30))
.expect("pop a completion")
.expect("a completion arrived within the bound");
contract.observe_completion(completion.user_data());
let (buffer, ()) = held.expect("the ring was holding this write's buffer");
let failed = completion.with_injected_failure(InjectedFailure::Win32(
windows_sys::Win32::Foundation::ERROR_DISK_FULL,
));
assert!(failed.result().is_err(), "the failure applies");
assert_eq!(
buffer.expect("a write carries a buffer"),
vec![7_u8; 4],
"the source buffer comes back"
);
contract.assert_quiescent();
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_failed_flush_reports_its_error_and_leaves_the_ring_usable() {
let path = temp_file("flush");
std::fs::write(&path, b"x").expect("create the fixture");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.expect("open the fixture");
let mut ring = IoRing::new(16, 16).expect("create a ring");
let mut contract = RingContract::new();
let mut batch = Batch::new(&mut ring);
let first = unsafe {
batch.flush_raw(
file.as_raw_handle(),
FlushCoverage::Unordered,
FlushMode::Default,
)
}
.expect("queue a flush");
contract.observe_push(first);
batch.submit_and_wait(1, 30_000).expect("submit and wait");
let completion = await_one(&mut ring);
contract.observe_completion(completion.user_data());
let failed = completion.with_injected_failure(InjectedFailure::Win32(
windows_sys::Win32::Foundation::ERROR_WRITE_FAULT,
));
assert_eq!(
(failed
.result()
.expect_err("the failure applies")
.as_ioring_error()
.expect("an IoRingError")
.code() as u32)
& 0xFFFF,
windows_sys::Win32::Foundation::ERROR_WRITE_FAULT
);
let mut batch = Batch::new(&mut ring);
let second = unsafe {
batch.flush_raw(
file.as_raw_handle(),
FlushCoverage::Unordered,
FlushMode::Default,
)
}
.expect("a failed operation must not stop the ring accepting pushes");
contract.observe_push(second);
batch.submit_and_wait(1, 30_000).expect("submit and wait");
let completion = await_one(&mut ring);
contract.observe_completion(completion.user_data());
completion
.result()
.expect("the second flush succeeds on its own merits");
contract.assert_quiescent();
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_failed_registration_drops_its_buffers_rather_than_leaking_them() {
struct DropTracking {
data: Vec<u8>,
dropped: Arc<AtomicBool>,
}
impl Drop for DropTracking {
fn drop(&mut self) {
self.dropped.store(true, Ordering::SeqCst);
}
}
unsafe impl IoBuf for DropTracking {
fn stable_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
fn bytes_len(&self) -> usize {
self.data.len()
}
}
unsafe impl IoBufMut for DropTracking {
fn stable_mut_ptr(&mut self) -> *mut u8 {
self.data.as_mut_ptr()
}
}
let dropped = Arc::new(AtomicBool::new(false));
let mut ring = IoRing::new(16, 16).expect("create a ring");
let mut batch = Batch::new(&mut ring);
let pending = batch
.register_buffers(vec![DropTracking {
data: vec![0_u8; 128],
dropped: Arc::clone(&dropped),
}])
.expect("queue the registration");
batch.submit_and_wait(1, 30_000).expect("submit and wait");
let completion = await_one(&mut ring);
assert!(
!dropped.load(Ordering::SeqCst),
"the buffer is still held while the registration is unclaimed"
);
let failed = completion.with_injected_failure(InjectedFailure::Ring(RingCondition::Corrupt));
let outcome = pending
.claim_if(&failed)
.expect("the registration claims its own completion");
assert!(
outcome.is_err(),
"a failed registration must report the failure rather than hand back buffers"
);
assert!(
dropped.load(Ordering::SeqCst),
"a claimed-but-failed registration must DROP its buffers, not leak them -- \
the matched completion is the proof its Drop was waiting for"
);
drop(ring);
}
#[test]
fn event_delivery_hands_a_failed_completion_to_the_callback() {
let path = temp_file("delivery");
std::fs::write(&path, b"x").expect("create the fixture");
let file = SharedHandle::new(
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.expect("open the fixture")
.into(),
);
let ring = IoRing::new(16, 16).expect("create a ring");
let delivered = Arc::new(AtomicUsize::new(0));
let failures = Arc::new(AtomicUsize::new(0));
let not_found = Arc::new(std::sync::atomic::AtomicIsize::new(0));
let seen = Arc::clone(&delivered);
let failed = Arc::clone(&failures);
let saw_code = Arc::clone(¬_found);
let delivery = windows_ioring_sys::EventDelivery::new(
ring,
move |completion, _held| {
seen.fetch_add(1, Ordering::SeqCst);
if let Err(error) = completion.result() {
if let Some(ring_error) = error.as_ioring_error() {
saw_code.store(ring_error.code() as isize, Ordering::SeqCst);
}
failed.fetch_add(1, Ordering::SeqCst);
}
},
None,
)
.expect("the completion event is available");
{
let mut scope = delivery.scope();
let mut batch = scope.batch();
batch
.cancel_owned_raw(&file, 0xDEAD_BEEF, ())
.expect("queue the cancel");
batch.submit().expect("submit the cancel");
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while failures.load(Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline {
std::thread::yield_now();
}
assert!(
delivered.load(Ordering::SeqCst) > 0,
"the callback must receive the completion at all"
);
assert_eq!(
failures.load(Ordering::SeqCst),
1,
"a failed completion must be delivered, not swallowed"
);
assert_eq!(
(not_found.load(Ordering::SeqCst) as u32) & 0xFFFF,
windows_sys::Win32::Foundation::ERROR_NOT_FOUND,
"the error must survive delivery intact, as ERROR_NOT_FOUND"
);
drop(delivery);
let _ = std::fs::remove_file(&path);
}