use std::os::windows::io::AsRawHandle;
use std::time::Duration;
use windows_ioring_sys::{Completion, IoRing};
#[cfg(feature = "fault-injection")]
use windows_ioring_sys::IoRingErrorExt;
use super::{Committer, Epoch};
const WAIT: Duration = Duration::from_secs(30);
#[cfg(feature = "fault-injection")]
const INJECTED: windows_ioring_sys::InjectedFailure =
windows_ioring_sys::InjectedFailure::Win32(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED);
fn scratch(tag: &str) -> (std::path::PathBuf, std::fs::File) {
let path = std::env::temp_dir().join(format!(
"windows-ioring-sys-epoch-commit-{}-{tag}.tmp",
std::process::id()
));
std::fs::write(&path, b"x").expect("create fixture");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.expect("open fixture");
(path, file)
}
fn commit_and_pop(
ring: &mut IoRing,
committer: &mut Committer,
file: &std::fs::File,
) -> (Epoch, Completion) {
let closed = committer
.commit(ring, file.as_raw_handle())
.expect("push the commit");
let (completion, _held) = ring
.pop_within(WAIT)
.expect("pop_within")
.expect("the commit's completion arrives well inside the bound");
(closed, completion)
}
#[cfg(feature = "fault-injection")]
#[test]
fn a_failed_commit_leaves_its_epoch_not_durable() {
let mut ring = IoRing::new(16, 16).expect("create ring");
let (path, file) = scratch("failed");
let mut committer = Committer::new();
let (closed, completion) = commit_and_pop(&mut ring, &mut committer, &file);
let error = committer
.claim(&completion.with_injected_failure(INJECTED))
.expect_err("a failed flush must be reported, not swallowed");
assert_eq!(
(error.as_ioring_error().expect("an IoRingError").code() as u32) & 0xFFFF,
windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED,
"the error reaching the caller must be the one that was injected"
);
assert!(
committer.durable_through().is_none(),
"a failed commit advances the watermark not at all"
);
assert!(
!committer.is_durable(closed),
"the epoch its flush was named for is not durable yet"
);
let _ = std::fs::remove_file(&path);
}
#[cfg(feature = "fault-injection")]
#[test]
fn a_later_successful_commit_covers_an_epoch_whose_own_commit_failed() {
let mut ring = IoRing::new(16, 16).expect("create ring");
let (path, file) = scratch("covered");
let mut committer = Committer::new();
let (first, completion) = commit_and_pop(&mut ring, &mut committer, &file);
committer
.claim(&completion.with_injected_failure(INJECTED))
.expect_err("epoch 0's own commit fails");
assert!(
!committer.is_durable(first),
"epoch 0 must not be durable between its failure and the next success"
);
let (second, completion) = commit_and_pop(&mut ring, &mut committer, &file);
let advanced = committer
.claim(&completion)
.expect("epoch 1's commit succeeds")
.expect("and it is one of ours");
assert_eq!(advanced, second);
assert!(
committer.is_durable(first),
"epoch 1's covering flush reached epoch 0's writes, so epoch 0 is durable now"
);
assert!(committer.is_durable(second));
let _ = std::fs::remove_file(&path);
}
#[test]
fn an_epoch_above_the_watermark_is_never_durable() {
let mut ring = IoRing::new(16, 16).expect("create ring");
let (path, file) = scratch("above");
let mut committer = Committer::new();
let (closed, completion) = commit_and_pop(&mut ring, &mut committer, &file);
committer
.claim(&completion)
.expect("the commit succeeds")
.expect("and it is ours");
assert!(committer.is_durable(closed));
assert!(
!committer.is_durable(committer.open_epoch()),
"the epoch still open was never committed and must not report durable"
);
assert!(
!committer.is_durable(Epoch(closed.0 + 7)),
"nor may an epoch that does not exist yet"
);
let _ = std::fs::remove_file(&path);
}
#[cfg(feature = "fault-injection")]
#[test]
fn durability_stays_monotonic_across_a_failed_commit() {
let mut ring = IoRing::new(16, 32).expect("create ring");
let (path, file) = scratch("monotonic");
let mut committer = Committer::new();
let (first, completion) = commit_and_pop(&mut ring, &mut committer, &file);
committer.claim(&completion).expect("epoch 0 commits");
let (second, completion) = commit_and_pop(&mut ring, &mut committer, &file);
committer
.claim(&completion.with_injected_failure(INJECTED))
.expect_err("epoch 1's commit fails");
assert!(
!committer.is_durable(second),
"the watermark must not move past a failure"
);
assert!(
committer.is_durable(first),
"and it must not move backwards either"
);
let (third, completion) = commit_and_pop(&mut ring, &mut committer, &file);
committer.claim(&completion).expect("epoch 2 commits");
for epoch in 0..=third.0 {
assert!(
committer.is_durable(Epoch(epoch)),
"epoch {epoch} is at or below the watermark and must report durable"
);
}
let _ = std::fs::remove_file(&path);
}
#[cfg(feature = "fault-injection")]
#[test]
fn a_failed_commit_is_no_longer_in_flight() {
let mut ring = IoRing::new(16, 16).expect("create ring");
let (path, file) = scratch("in-flight");
let mut committer = Committer::new();
let (_, completion) = commit_and_pop(&mut ring, &mut committer, &file);
assert_eq!(committer.in_flight(), 1, "pushed but not yet observed");
committer
.claim(&completion.with_injected_failure(INJECTED))
.expect_err("the commit fails");
assert_eq!(
committer.in_flight(),
0,
"a failure still settles the accounting; only the watermark is withheld"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_completion_that_belongs_to_someone_else_is_not_claimed() {
use windows_ioring_sys::{Batch, FlushCoverage, FlushMode};
let mut ring = IoRing::new(16, 16).expect("create ring");
let (path, file) = scratch("foreign");
let mut committer = Committer::new();
let (closed, completion) = commit_and_pop(&mut ring, &mut committer, &file);
committer.claim(&completion).expect("our own commit");
let before = committer.durable_through();
let mut batch = Batch::new(&mut ring);
let foreign_id = unsafe {
batch.flush_raw(
file.as_raw_handle(),
FlushCoverage::Unordered,
FlushMode::Default,
)
}
.expect("queue a flush nobody is tracking");
batch.submit().expect("submit");
let (foreign, _held) = ring
.pop_within(WAIT)
.expect("pop_within")
.expect("the foreign flush completes");
assert_eq!(foreign.user_data(), foreign_id);
assert!(
committer
.claim(&foreign)
.expect("a foreign completion is not an error")
.is_none(),
"a completion this committer never pushed is not its business"
);
assert_eq!(
committer.durable_through(),
before,
"and it must not move the watermark"
);
assert!(committer.is_durable(closed));
let _ = std::fs::remove_file(&path);
}