use super::port::{Entry, Port, STOP, Wait, bool_result, owned};
use crate::{Error, Result};
use std::{
collections::VecDeque,
io,
os::windows::io::{AsRawHandle, OwnedHandle},
ptr,
sync::{Arc, Condvar, Mutex},
thread::JoinHandle,
};
use windows_sys::Win32::{
Foundation::HANDLE,
System::Threading::{CreateEventW, SetEvent},
};
struct Queue {
entries: VecDeque<Entry>,
high_water: usize,
stopping: bool,
error: Option<Error>,
}
struct Shared {
queue: Mutex<Queue>,
room: Condvar,
event: OwnedHandle,
#[cfg(test)]
wait_error: std::sync::atomic::AtomicI32,
}
pub struct EventIntegration {
shared: Arc<Shared>,
port: Arc<Port>,
worker: Option<JoinHandle<()>>,
}
impl EventIntegration {
pub fn new(port: Arc<Port>) -> io::Result<Self> {
let event = unsafe { owned(CreateEventW(ptr::null(), 0, 0, ptr::null())) }?;
let shared = Arc::new(Shared {
queue: Mutex::new(Queue {
entries: VecDeque::with_capacity(128),
high_water: 0,
stopping: false,
error: None,
}),
room: Condvar::new(),
event,
#[cfg(test)]
wait_error: std::sync::atomic::AtomicI32::new(0),
});
let worker_shared = Arc::clone(&shared);
let worker_port = Arc::clone(&port);
let worker = std::thread::Builder::new()
.name("turnloop-iocp-event".into())
.spawn(move || {
if let Err(error) = pump(&worker_shared, &worker_port) {
let mut queue = worker_shared
.queue
.lock()
.unwrap_or_else(|e| e.into_inner());
queue.error = Some(error.into());
worker_shared.room.notify_all();
unsafe {
SetEvent(worker_shared.event.as_raw_handle());
}
}
})?;
Ok(Self {
shared,
port,
worker: Some(worker),
})
}
pub fn event(&self) -> HANDLE {
self.shared.event.as_raw_handle()
}
#[cfg(test)]
pub(super) fn fail_and_wait(&self, code: i32) {
use std::sync::atomic::Ordering;
self.shared.wait_error.store(code, Ordering::Release);
self.port
.post(super::port::WAKE, 0)
.expect("wake pump for fault");
let queue = self.shared.queue.lock().expect("fault queue");
let (queue, timeout) = self
.shared
.room
.wait_timeout_while(queue, std::time::Duration::from_secs(5), |q| {
q.error.is_none()
})
.expect("fault publication");
assert!(!timeout.timed_out(), "pump did not run injected failure");
assert_eq!(queue.error.expect("published error").os, Some(code));
assert_eq!(self.shared.wait_error.load(Ordering::Acquire), 0);
}
pub fn check(&self) -> Result<()> {
let queue = self
.shared
.queue
.lock()
.map_err(|_| io::Error::other("helper queue poisoned"))?;
if let Some(error) = queue.error {
unsafe {
SetEvent(self.event());
}
return Err(error);
}
Ok(())
}
pub fn drain(&mut self, out: &mut [Entry]) -> Result<usize> {
self.check()?;
self.drain_retained(out)
}
pub fn drain_retained(&mut self, out: &mut [Entry]) -> Result<usize> {
let mut queue = self
.shared
.queue
.lock()
.map_err(|_| io::Error::other("helper queue poisoned"))?;
let mut n = 0;
for slot in out {
let Some(entry) = queue.entries.pop_front() else {
break;
};
*slot = entry;
n += 1;
}
if !queue.entries.is_empty() {
bool_result(unsafe { SetEvent(self.event()) })?;
}
self.shared.room.notify_one();
Ok(n)
}
pub fn shutdown(&mut self) -> io::Result<()> {
if self.worker.is_none() {
return Ok(());
}
{
let mut queue = self.shared.queue.lock().unwrap_or_else(|e| e.into_inner());
queue.stopping = true;
self.shared.room.notify_all();
}
self.port.post(STOP, 0)?;
if let Some(worker) = self.worker.take() {
worker
.join()
.map_err(|_| io::Error::other("helper panicked"))?;
}
Ok(())
}
}
impl Drop for EventIntegration {
fn drop(&mut self) {
let _ = self.shutdown();
}
}
fn pump(shared: &Shared, port: &Port) -> io::Result<()> {
let mut batch = [Entry::default(); 64];
loop {
{
let mut queue = shared
.queue
.lock()
.map_err(|_| io::Error::other("helper queue poisoned"))?;
while queue.entries.len() > 128 - batch.len() && !queue.stopping {
queue = shared
.room
.wait(queue)
.map_err(|_| io::Error::other("helper queue poisoned"))?;
}
if queue.stopping {
return Ok(());
}
}
#[cfg(test)]
{
let code = shared
.wait_error
.swap(0, std::sync::atomic::Ordering::AcqRel);
if code != 0 {
return Err(io::Error::from_raw_os_error(code));
}
}
let Wait::Entries(n) = port.wait(None, false, &mut batch)? else {
continue;
};
let mut queue = shared
.queue
.lock()
.map_err(|_| io::Error::other("helper queue poisoned"))?;
for entry in &batch[..n] {
if entry.key == STOP {
queue.stopping = true;
} else {
queue.entries.push_back(*entry); }
}
queue.high_water = queue.high_water.max(queue.entries.len());
#[cfg(test)]
shared.room.notify_all();
bool_result(unsafe { SetEvent(shared.event.as_raw_handle()) })?;
if queue.stopping {
return Ok(());
}
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
#[test]
fn shutdown_preserves_every_packet_under_full_queue_backpressure() {
let port = Arc::new(Port::new().expect("port"));
for i in 0..128 {
port.post(17, i).expect("post subject packet");
}
let mut helper = EventIntegration::new(Arc::clone(&port)).expect("helper");
{
let queue = helper.shared.queue.lock().expect("queue");
let (queue, timeout) = helper
.shared
.room
.wait_timeout_while(queue, std::time::Duration::from_secs(5), |q| {
q.high_water != 128
})
.expect("pump filled queue");
assert!(!timeout.timed_out(), "pump never reached capacity");
assert_eq!(queue.entries.len(), 128);
}
helper.shutdown().expect("join backpressured worker");
let mut batch = [Entry::default(); 7]; let mut seen = [false; 128];
let mut count = 0;
loop {
let n = helper.drain_retained(&mut batch).expect("retained batch");
if n == 0 {
break;
}
for entry in &batch[..n] {
assert_eq!(entry.key, 17);
assert!(!seen[entry.bytes as usize]);
seen[entry.bytes as usize] = true;
count += 1;
}
}
assert_eq!(count, 128);
assert!(seen.into_iter().all(|seen| seen));
}
}