#![cfg(windows)]
use std::collections::{HashMap, HashSet};
use std::io;
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
use windows_overlapped_io_sys::{
Issued, Operation, OperationState, Submitted, UnassociatedEndpoint,
};
use windows_sys::Win32::Foundation::{ERROR_IO_PENDING, ERROR_OPERATION_ABORTED};
use windows_sys::Win32::Storage::FileSystem::{
FILE_FLAG_OVERLAPPED, ReadFile, SetFileCompletionNotificationModes, WriteFile,
};
use windows_sys::Win32::System::Pipes::CreateNamedPipeW;
use windows_threadpool_sys::callback_env::CallbackEnviron;
use windows_threadpool_sys::io::{IoCompletion, ThreadpoolIo};
const FILE_SKIP_COMPLETION_PORT_ON_SUCCESS: u8 = 0x1;
mod pipe_mode {
pub const ACCESS_DUPLEX: u32 = 0x0000_0003;
pub const TYPE_BYTE: u32 = 0x0000_0000;
}
const CALLBACK_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Record {
identity: usize,
io_result: u32,
bytes: usize,
payload: usize,
}
struct Recorder {
records: Mutex<Vec<Record>>,
arrived: Condvar,
}
impl Recorder {
fn new() -> Arc<Self> {
Arc::new(Self {
records: Mutex::new(Vec::new()),
arrived: Condvar::new(),
})
}
fn push(&self, record: Record) {
let mut records = self.records.lock().expect("record a completion");
records.push(record);
self.arrived.notify_all();
}
fn records(&self) -> Vec<Record> {
self.records.lock().expect("read completions").clone()
}
fn len(&self) -> usize {
self.records.lock().expect("read completions").len()
}
fn wait_for(&self, count: usize) -> Vec<Record> {
let records = self.records.lock().expect("await completions");
let (records, timeout) = self
.arrived
.wait_timeout_while(records, CALLBACK_TIMEOUT, |records| records.len() < count)
.expect("await completions");
assert!(
!timeout.timed_out(),
"timed out waiting for {count} completion(s); saw {}",
records.len()
);
records.clone()
}
}
#[derive(Debug)]
struct DropTracked {
index: usize,
dropped: Arc<AtomicUsize>,
}
impl Drop for DropTracked {
fn drop(&mut self) {
self.dropped.fetch_add(1, Ordering::SeqCst);
}
}
#[derive(Debug)]
struct DropTrackedWide {
_filler: [u64; 16],
dropped: Arc<AtomicUsize>,
}
impl Drop for DropTrackedWide {
fn drop(&mut self) {
self.dropped.fetch_add(1, Ordering::SeqCst);
}
}
#[derive(Clone, Copy)]
struct SharedBuffer(*mut u8);
unsafe impl Send for SharedBuffer {}
unsafe impl Sync for SharedBuffer {}
impl SharedBuffer {
unsafe fn slot(self, index: usize) -> *mut u8 {
unsafe { self.0.add(index) }
}
}
fn temp_file_with(content: &[u8], tag: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"windows-threadpool-sys-tp-io-{tag}-{}.tmp",
std::process::id()
));
std::fs::write(&path, content).expect("write temp file");
path
}
fn read_endpoint(path: &Path) -> UnassociatedEndpoint {
UnassociatedEndpoint::open(path, true, false, 0).expect("open overlapped endpoint")
}
fn pending_pipe(tag: &str) -> (UnassociatedEndpoint, std::fs::File) {
let name = format!(
"{}{}-{}",
r"\\.\pipe\windows-threadpool-sys-tp-io-",
tag,
std::process::id()
);
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let handle = unsafe {
CreateNamedPipeW(
wide.as_ptr(),
pipe_mode::ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
pipe_mode::TYPE_BYTE,
1,
4096,
4096,
0,
ptr::null(),
)
};
assert!(
!handle.is_null() && handle as isize != -1,
"CreateNamedPipeW failed: {}",
io::Error::last_os_error()
);
let endpoint =
unsafe { UnassociatedEndpoint::assume_overlapped(OwnedHandle::from_raw_handle(handle)) };
let client = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&name)
.expect("connect the client pipe end");
(endpoint, client)
}
unsafe fn issue_read(
handle: std::os::windows::io::BorrowedHandle<'_>,
overlapped: *mut windows_sys::Win32::System::IO::OVERLAPPED,
buffer: *mut u8,
len: u32,
) -> io::Result<Issued> {
let ok = unsafe {
ReadFile(
handle.as_raw_handle(),
buffer,
len,
ptr::null_mut(),
overlapped,
)
};
if ok != 0 {
return Ok(Issued::Pending);
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_IO_PENDING as i32) {
return Ok(Issued::Pending);
}
Err(error)
}
#[test]
fn immediate_failure_returns_the_operation_and_balances_accounting() {
let path = temp_file_with(b"immediate failure", "immediate-failure");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: 0,
});
},
None,
)
.expect("create TP_IO");
let source = *b"denied";
let src_ptr = source.as_ptr();
let src_len = source.len() as u32;
let operation = Operation::new(());
let submitted = unsafe {
tp.submit(operation, |handle, overlapped| {
let ok = WriteFile(
handle.as_raw_handle(),
src_ptr,
src_len,
ptr::null_mut(),
overlapped,
);
if ok != 0 {
return Ok(Issued::Pending);
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_IO_PENDING as i32) {
return Ok(Issued::Pending);
}
Err(error)
})
};
match submitted {
Submitted::Failed { operation, error } => {
assert_eq!(operation.state(), OperationState::Idle);
assert!(error.raw_os_error().is_some(), "expected an OS error");
}
other => panic!("expected an immediate failure, got {other:?}"),
}
assert_eq!(tp.outstanding(), 0, "the start must have been balanced");
assert_eq!(recorder.len(), 0, "a failed submission must not call back");
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn repeated_immediate_failures_do_not_drift_accounting() {
const ATTEMPTS: usize = 500;
let path = temp_file_with(b"repeat", "repeat-failure");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: 0,
});
},
None,
)
.expect("create TP_IO");
let dropped = Arc::new(AtomicUsize::new(0));
for index in 0..ATTEMPTS {
let operation = Operation::new(DropTracked {
index,
dropped: Arc::clone(&dropped),
});
let submitted = unsafe {
tp.submit(operation, |_handle, _overlapped| {
Err(io::Error::from_raw_os_error(5))
})
};
match submitted {
Submitted::Failed { operation, .. } => {
assert_eq!(operation.payload().index, index, "payload must survive");
}
other => panic!("expected an immediate failure, got {other:?}"),
}
assert_eq!(tp.outstanding(), 0, "accounting drifted at attempt {index}");
}
assert_eq!(
dropped.load(Ordering::SeqCst),
ATTEMPTS,
"every returned operation's payload must be dropped"
);
assert_eq!(recorder.len(), 0, "failed submissions must not call back");
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn immediate_success_balances_accounting_without_a_callback() {
let content = b"skip on success payload";
let path = temp_file_with(content, "skip-success");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<()>() };
assert_eq!(operation.state(), OperationState::Completed);
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: 0,
});
},
None,
)
.expect("create TP_IO");
let modes_ok = unsafe {
SetFileCompletionNotificationModes(
tp.handle().as_raw_handle(),
FILE_SKIP_COMPLETION_PORT_ON_SUCCESS,
)
};
assert_ne!(
modes_ok,
0,
"SetFileCompletionNotificationModes failed: {}",
io::Error::last_os_error()
);
let mut buffer = [0_u8; 64];
let buf_ptr = buffer.as_mut_ptr();
let buf_len = buffer.len() as u32;
let mut bytes: u32 = 0;
let bytes_ptr: *mut u32 = &mut bytes;
let mut operation = Operation::new(());
operation.set_offset(0);
let submitted = unsafe {
tp.submit(operation, |handle, overlapped| {
let ok = ReadFile(
handle.as_raw_handle(),
buf_ptr,
buf_len,
bytes_ptr,
overlapped,
);
if ok != 0 {
return Ok(Issued::Completed {
bytes_transferred: *bytes_ptr,
});
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_IO_PENDING as i32) {
return Ok(Issued::Pending);
}
Err(error)
})
};
match submitted {
Submitted::Completed {
operation,
bytes_transferred,
} => {
assert_eq!(operation.state(), OperationState::Completed);
assert_eq!(bytes_transferred as usize, content.len());
assert_eq!(&buffer[..content.len()], content);
assert_eq!(tp.outstanding(), 0, "the start must have been balanced");
assert_eq!(recorder.len(), 0, "no callback may run on this path");
}
Submitted::Pending(id) => {
let records = recorder.wait_for(1);
assert_eq!(records[0].identity, id.as_ptr() as usize);
assert_eq!(records[0].bytes, content.len());
tp.run_down();
assert_eq!(tp.outstanding(), 0);
}
Submitted::Failed { error, .. } => panic!("submit failed: {error}"),
}
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn pending_completion_delivers_the_callback_and_claims_the_operation() {
let content = b"thread pool overlapped read";
let path = temp_file_with(content, "pending-read");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
assert_eq!(operation.state(), OperationState::Completed);
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
let mut buffer = [0_u8; 64];
let buf_ptr = buffer.as_mut_ptr();
let buf_len = buffer.len() as u32;
let mut operation = Operation::new(0xABCD_usize);
operation.set_offset(0);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, buf_ptr, buf_len)
})
};
let id = match submitted {
Submitted::Pending(id) => id,
other => panic!("expected a pending submission, got {other:?}"),
};
let records = recorder.wait_for(1);
assert_eq!(records.len(), 1, "expected exactly one completion");
assert_eq!(
records[0].identity,
id.as_ptr() as usize,
"identity mismatch"
);
assert_eq!(records[0].io_result, 0, "expected a successful read");
assert_eq!(records[0].bytes, content.len());
assert_eq!(
records[0].payload, 0xABCD,
"payload must survive the round trip"
);
assert_eq!(&buffer[..content.len()], content);
tp.run_down();
assert_eq!(tp.outstanding(), 0);
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn simultaneously_outstanding_operations_keep_distinct_identities() {
const OPERATIONS: usize = 256;
let (endpoint, _client) = pending_pipe("identity");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
endpoint,
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
let mut identities: HashMap<usize, usize> = HashMap::new();
for slot in 0..OPERATIONS {
let operation = Operation::new(slot);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(slot), 1)
})
};
match submitted {
Submitted::Pending(id) => {
assert!(
identities.insert(id.as_ptr() as usize, slot).is_none(),
"two simultaneously outstanding operations shared identity {:p}",
id.as_ptr()
);
}
other => panic!("expected pending at slot {slot}, got {other:?}"),
}
}
assert_eq!(tp.outstanding(), OPERATIONS);
assert_eq!(identities.len(), OPERATIONS);
tp.cancel_all().expect("cancel every outstanding read");
let records = recorder.wait_for(OPERATIONS);
tp.run_down();
assert_eq!(tp.outstanding(), 0);
assert_eq!(records.len(), OPERATIONS, "every operation must call back");
let mut payloads_seen: HashSet<usize> = HashSet::new();
for record in &records {
let slot = *identities
.get(&record.identity)
.unwrap_or_else(|| panic!("unknown identity {:#x}", record.identity));
assert_eq!(
record.payload, slot,
"identity and payload disagree about the slot"
);
assert!(
payloads_seen.insert(slot),
"slot {slot} completed more than once"
);
assert_eq!(
record.io_result, ERROR_OPERATION_ABORTED,
"slot {slot} should have been aborted"
);
}
assert_eq!(payloads_seen.len(), OPERATIONS);
}
#[test]
fn many_file_reads_each_complete_once_with_their_own_data() {
const OPERATIONS: usize = 512;
let content: Vec<u8> = (0..OPERATIONS).map(|i| i as u8).collect();
let path = temp_file_with(&content, "scale-reads");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
for slot in 0..OPERATIONS {
let mut operation = Operation::new(slot);
operation.set_offset(slot as u64);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(slot), 1)
})
};
assert!(
matches!(submitted, Submitted::Pending(_)),
"slot {slot}: expected pending, got {submitted:?}"
);
}
let records = recorder.wait_for(OPERATIONS);
tp.run_down();
assert_eq!(tp.outstanding(), 0);
assert_eq!(records.len(), OPERATIONS, "every operation must call back");
let mut payloads_seen: HashSet<usize> = HashSet::new();
for record in &records {
assert!(
record.payload < OPERATIONS,
"payload {} is out of range",
record.payload
);
assert!(
payloads_seen.insert(record.payload),
"slot {} completed more than once",
record.payload
);
assert_eq!(record.io_result, 0, "slot {} failed", record.payload);
assert_eq!(
record.bytes, 1,
"slot {} read the wrong length",
record.payload
);
}
assert_eq!(payloads_seen.len(), OPERATIONS);
for (slot, byte) in landed.iter().enumerate() {
assert_eq!(*byte, slot as u8, "slot {slot} landed the wrong byte");
}
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn concurrent_submissions_from_many_threads_are_accounted_exactly_once() {
const THREADS: usize = 8;
const PER_THREAD: usize = 64;
const OPERATIONS: usize = THREADS * PER_THREAD;
let content: Vec<u8> = (0..OPERATIONS).map(|i| i as u8).collect();
let path = temp_file_with(&content, "threads");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let mut landed = vec![0_u8; OPERATIONS];
let base = SharedBuffer(landed.as_mut_ptr());
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
std::thread::scope(|scope| {
for thread in 0..THREADS {
let tp = &tp;
scope.spawn(move || {
for step in 0..PER_THREAD {
let slot = thread * PER_THREAD + step;
let mut operation = Operation::new(slot);
operation.set_offset(slot as u64);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.slot(slot), 1)
})
};
assert!(
matches!(submitted, Submitted::Pending(_)),
"slot {slot}: expected pending, got {submitted:?}"
);
}
});
}
});
let records = recorder.wait_for(OPERATIONS);
tp.run_down();
assert_eq!(tp.outstanding(), 0);
assert_eq!(records.len(), OPERATIONS);
let payloads: HashSet<usize> = records.iter().map(|record| record.payload).collect();
assert_eq!(payloads.len(), OPERATIONS, "every slot must complete once");
for record in &records {
assert_eq!(record.io_result, 0, "slot {} failed", record.payload);
}
for (slot, byte) in landed.iter().enumerate() {
assert_eq!(*byte, slot as u8, "slot {slot} landed the wrong byte");
}
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn sequential_submissions_reuse_the_object() {
const ROUNDS: usize = 25;
let content: Vec<u8> = (0..ROUNDS).map(|i| (i * 3) as u8).collect();
let path = temp_file_with(&content, "sequential");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
let mut landed = vec![0_u8; ROUNDS];
let base = landed.as_mut_ptr();
for round in 0..ROUNDS {
let mut operation = Operation::new(round);
operation.set_offset(round as u64);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(round), 1)
})
};
assert!(
matches!(submitted, Submitted::Pending(_)),
"round {round}: expected pending, got {submitted:?}"
);
recorder.wait_for(round + 1);
tp.run_down();
assert_eq!(tp.outstanding(), 0, "round {round} left work outstanding");
}
let records = recorder.records();
assert_eq!(records.len(), ROUNDS);
for (round, record) in records.iter().enumerate() {
assert_eq!(record.payload, round, "rounds completed out of order");
assert_eq!(record.io_result, 0);
}
for (round, byte) in landed.iter().enumerate() {
assert_eq!(*byte, (round * 3) as u8);
}
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn cancelling_one_operation_completes_it_as_aborted() {
let (endpoint, _client) = pending_pipe("cancel-one");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
endpoint,
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
let mut buffer = [0_u8; 32];
let buf_ptr = buffer.as_mut_ptr();
let buf_len = buffer.len() as u32;
let operation = Operation::new(7_usize);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, buf_ptr, buf_len)
})
};
let id = match submitted {
Submitted::Pending(id) => id,
other => panic!("expected a pending submission, got {other:?}"),
};
assert_eq!(tp.outstanding(), 1, "the read must still be outstanding");
assert_eq!(
recorder.len(),
0,
"a pending read must not have called back"
);
tp.cancel(id).expect("cancel the pending read");
let records = recorder.wait_for(1);
assert_eq!(records.len(), 1);
assert_eq!(records[0].identity, id.as_ptr() as usize);
assert_eq!(
records[0].io_result, ERROR_OPERATION_ABORTED,
"a cancelled operation must complete as ERROR_OPERATION_ABORTED"
);
assert_eq!(records[0].payload, 7);
tp.run_down();
assert_eq!(tp.outstanding(), 0);
}
#[test]
fn cancel_all_aborts_every_outstanding_operation() {
const OPERATIONS: usize = 32;
let (endpoint, _client) = pending_pipe("cancel-all");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
endpoint,
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
let mut identities = HashSet::new();
for slot in 0..OPERATIONS {
let operation = Operation::new(slot);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(slot), 1)
})
};
match submitted {
Submitted::Pending(id) => {
identities.insert(id.as_ptr() as usize);
}
other => panic!("expected pending at slot {slot}, got {other:?}"),
}
}
assert_eq!(tp.outstanding(), OPERATIONS);
tp.cancel_all().expect("cancel every outstanding read");
let records = recorder.wait_for(OPERATIONS);
tp.run_down();
assert_eq!(tp.outstanding(), 0);
assert_eq!(records.len(), OPERATIONS);
let mut payloads = HashSet::new();
for record in &records {
assert!(
identities.contains(&record.identity),
"unknown identity {:#x}",
record.identity
);
assert_eq!(
record.io_result, ERROR_OPERATION_ABORTED,
"slot {} was not aborted",
record.payload
);
assert!(payloads.insert(record.payload), "a slot completed twice");
}
assert_eq!(payloads.len(), OPERATIONS);
}
#[test]
fn cancel_all_with_nothing_outstanding_is_benign() {
let (endpoint, _client) = pending_pipe("cancel-empty");
let tp = ThreadpoolIo::new(endpoint, |_| {}, None).expect("create TP_IO");
let _ = tp.cancel_all();
assert_eq!(tp.outstanding(), 0);
tp.run_down();
tp.wait();
assert_eq!(tp.outstanding(), 0);
}
#[test]
fn drop_with_operations_outstanding_cancels_drains_and_terminates() {
const OPERATIONS: usize = 16;
let (endpoint, _client) = pending_pipe("drop-outstanding");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let dropped = Arc::new(AtomicUsize::new(0));
let payload_dropped = Arc::clone(&dropped);
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
{
let tp = ThreadpoolIo::new(
endpoint,
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<DropTracked>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: operation.payload().index,
});
},
None,
)
.expect("create TP_IO");
for slot in 0..OPERATIONS {
let operation = Operation::new(DropTracked {
index: slot,
dropped: Arc::clone(&payload_dropped),
});
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(slot), 1)
})
};
assert!(
matches!(submitted, Submitted::Pending(_)),
"slot {slot}: expected pending, got {submitted:?}"
);
}
assert_eq!(tp.outstanding(), OPERATIONS, "reads must be outstanding");
}
let records = recorder.records();
assert_eq!(
records.len(),
OPERATIONS,
"rundown must let every outstanding operation call back"
);
for record in &records {
assert_eq!(
record.io_result, ERROR_OPERATION_ABORTED,
"slot {} should have been aborted by rundown",
record.payload
);
}
assert_eq!(
dropped.load(Ordering::SeqCst),
OPERATIONS,
"every claimed payload must have been dropped"
);
}
#[test]
fn explicit_cancel_all_then_run_down_drains_every_operation() {
const OPERATIONS: usize = 24;
let (endpoint, _client) = pending_pipe("explicit-rundown");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
endpoint,
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
None,
)
.expect("create TP_IO");
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
for slot in 0..OPERATIONS {
let operation = Operation::new(slot);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(slot), 1)
})
};
assert!(matches!(submitted, Submitted::Pending(_)));
}
assert_eq!(tp.outstanding(), OPERATIONS);
tp.cancel_all().expect("cancel every outstanding read");
tp.run_down();
assert_eq!(tp.outstanding(), 0, "run_down must drain completely");
assert_eq!(recorder.len(), OPERATIONS, "every operation must call back");
drop(tp);
}
#[test]
fn unclaimed_completions_reclaim_their_payload() {
const OPERATIONS: usize = 256;
let content: Vec<u8> = (0..OPERATIONS).map(|i| i as u8).collect();
let path = temp_file_with(&content, "unclaimed");
let dropped = Arc::new(AtomicUsize::new(0));
let payload_dropped = Arc::clone(&dropped);
let observed = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&observed);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |_completion: &IoCompletion| {
counter.fetch_add(1, Ordering::SeqCst);
},
None,
)
.expect("create TP_IO");
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
for slot in 0..OPERATIONS {
let mut operation = Operation::new(DropTracked {
index: slot,
dropped: Arc::clone(&payload_dropped),
});
operation.set_offset(slot as u64);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(slot), 1)
})
};
assert!(
matches!(submitted, Submitted::Pending(_)),
"slot {slot}: expected pending, got {submitted:?}"
);
}
tp.run_down();
assert_eq!(tp.outstanding(), 0);
assert_eq!(observed.load(Ordering::SeqCst), OPERATIONS);
assert_eq!(
dropped.load(Ordering::SeqCst),
OPERATIONS,
"an unclaimed completion must still free its payload"
);
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn mixed_payload_types_reclaim_generically() {
const PAIRS: usize = 100;
const OPERATIONS: usize = PAIRS * 2;
let content: Vec<u8> = (0..OPERATIONS).map(|i| i as u8).collect();
let path = temp_file_with(&content, "mixed-payloads");
let narrow_dropped = Arc::new(AtomicUsize::new(0));
let wide_dropped = Arc::new(AtomicUsize::new(0));
let observed = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&observed);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |_completion: &IoCompletion| {
counter.fetch_add(1, Ordering::SeqCst);
},
None,
)
.expect("create TP_IO");
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
for pair in 0..PAIRS {
let narrow_slot = pair * 2;
let mut narrow = Operation::new(DropTracked {
index: narrow_slot,
dropped: Arc::clone(&narrow_dropped),
});
narrow.set_offset(narrow_slot as u64);
let submitted = unsafe {
tp.submit(narrow, |handle, ov| {
issue_read(handle, ov, base.add(narrow_slot), 1)
})
};
assert!(matches!(submitted, Submitted::Pending(_)));
let wide_slot = pair * 2 + 1;
let mut wide = Operation::new(DropTrackedWide {
_filler: [0; 16],
dropped: Arc::clone(&wide_dropped),
});
wide.set_offset(wide_slot as u64);
let submitted = unsafe {
tp.submit(wide, |handle, ov| {
issue_read(handle, ov, base.add(wide_slot), 1)
})
};
assert!(matches!(submitted, Submitted::Pending(_)));
}
tp.run_down();
assert_eq!(tp.outstanding(), 0);
assert_eq!(observed.load(Ordering::SeqCst), OPERATIONS);
assert_eq!(
narrow_dropped.load(Ordering::SeqCst),
PAIRS,
"narrow payloads must be reclaimed with their own thunk"
);
assert_eq!(
wide_dropped.load(Ordering::SeqCst),
PAIRS,
"wide payloads must be reclaimed with their own thunk"
);
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_past_end_of_file_still_balances_accounting() {
let content = b"short";
let path = temp_file_with(content, "past-eof");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let _operation = unsafe { completion.claim::<()>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: 0,
});
},
None,
)
.expect("create TP_IO");
let mut buffer = [0_u8; 16];
let buf_ptr = buffer.as_mut_ptr();
let buf_len = buffer.len() as u32;
let mut operation = Operation::new(());
operation.set_offset(4096);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, buf_ptr, buf_len)
})
};
match submitted {
Submitted::Pending(_) => {
let records = recorder.wait_for(1);
assert_eq!(records[0].bytes, 0, "a read past EOF transfers nothing");
}
Submitted::Failed { .. } => {
assert_eq!(recorder.len(), 0, "a failed submission must not call back");
}
Submitted::Completed {
bytes_transferred, ..
} => {
assert_eq!(bytes_transferred, 0, "a read past EOF transfers nothing");
}
}
tp.run_down();
assert_eq!(tp.outstanding(), 0);
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn zero_length_read_completes_and_balances_accounting() {
let path = temp_file_with(b"zero length read", "zero-length");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let _operation = unsafe { completion.claim::<()>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: 0,
});
},
None,
)
.expect("create TP_IO");
let mut buffer = [0_u8; 8];
let buf_ptr = buffer.as_mut_ptr();
let mut operation = Operation::new(());
operation.set_offset(0);
let submitted =
unsafe { tp.submit(operation, |handle, ov| issue_read(handle, ov, buf_ptr, 0)) };
match submitted {
Submitted::Pending(_) => {
let records = recorder.wait_for(1);
assert_eq!(records[0].bytes, 0);
}
Submitted::Completed {
bytes_transferred, ..
} => assert_eq!(bytes_transferred, 0),
Submitted::Failed { error, .. } => panic!("zero-length read failed: {error}"),
}
tp.run_down();
assert_eq!(tp.outstanding(), 0);
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn operations_run_under_an_explicit_callback_environment() {
const OPERATIONS: usize = 16;
let content: Vec<u8> = (0..OPERATIONS).map(|i| i as u8).collect();
let path = temp_file_with(&content, "callback-env");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let mut env = CallbackEnviron::new();
env.set_runs_long();
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let operation = unsafe { completion.claim::<usize>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: *operation.payload(),
});
},
Some(&mut env),
)
.expect("create TP_IO with a callback environment");
let mut landed = vec![0_u8; OPERATIONS];
let base = landed.as_mut_ptr();
for slot in 0..OPERATIONS {
let mut operation = Operation::new(slot);
operation.set_offset(slot as u64);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, base.add(slot), 1)
})
};
assert!(matches!(submitted, Submitted::Pending(_)));
}
let records = recorder.wait_for(OPERATIONS);
tp.run_down();
assert_eq!(tp.outstanding(), 0);
let payloads: HashSet<usize> = records.iter().map(|record| record.payload).collect();
assert_eq!(payloads.len(), OPERATIONS);
for (slot, byte) in landed.iter().enumerate() {
assert_eq!(*byte, slot as u8);
}
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn object_with_no_submissions_runs_down_and_drops_cleanly() {
let path = temp_file_with(b"unused", "no-submissions");
let tp = ThreadpoolIo::new(read_endpoint(&path), |_| {}, None).expect("create TP_IO");
assert_eq!(tp.outstanding(), 0);
tp.run_down();
tp.wait();
assert_eq!(tp.outstanding(), 0);
drop(tp);
let _ = std::fs::remove_file(&path);
}
#[test]
fn run_down_and_wait_are_idempotent_when_quiescent() {
let content = b"idempotent rundown";
let path = temp_file_with(content, "idempotent");
let recorder = Recorder::new();
let seen = Arc::clone(&recorder);
let tp = ThreadpoolIo::new(
read_endpoint(&path),
move |completion: &IoCompletion| {
let _operation = unsafe { completion.claim::<()>() };
seen.push(Record {
identity: completion.overlapped_ptr() as usize,
io_result: completion.io_result(),
bytes: completion.bytes_transferred(),
payload: 0,
});
},
None,
)
.expect("create TP_IO");
let mut buffer = [0_u8; 64];
let buf_ptr = buffer.as_mut_ptr();
let buf_len = buffer.len() as u32;
let mut operation = Operation::new(());
operation.set_offset(0);
let submitted = unsafe {
tp.submit(operation, |handle, ov| {
issue_read(handle, ov, buf_ptr, buf_len)
})
};
assert!(matches!(submitted, Submitted::Pending(_)));
recorder.wait_for(1);
for _ in 0..5 {
tp.run_down();
tp.wait();
assert_eq!(tp.outstanding(), 0);
}
assert_eq!(recorder.len(), 1, "rundown must not replay completions");
drop(tp);
let _ = std::fs::remove_file(&path);
}