use crate::lock;
use std::io;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
#[cfg(test)]
use std::sync::atomic::{AtomicU64, Ordering};
const STOP_BOUND: Duration = Duration::from_secs(2);
const WAKE_RETRY: Duration = Duration::from_millis(100);
pub(crate) struct Stopper {
stopped: Mutex<bool>,
changed: Condvar,
#[cfg(test)]
wakes: AtomicU64,
}
impl Stopper {
pub(crate) fn new() -> Self {
Self {
stopped: Mutex::new(false),
changed: Condvar::new(),
#[cfg(test)]
wakes: AtomicU64::new(0),
}
}
pub(crate) fn stop(&self) {
*lock(&self.stopped) = true;
self.changed.notify_all();
}
pub(crate) fn wait(&self, timeout: Duration) -> bool {
let guard = lock(&self.stopped);
let (guard, _) = self
.changed
.wait_timeout_while(guard, timeout, |stopped| !*stopped)
.unwrap_or_else(|poisoned| poisoned.into_inner());
#[cfg(test)]
self.wakes.fetch_add(1, Ordering::SeqCst);
*guard
}
#[cfg(test)]
pub(crate) fn note_wake(&self) {
self.wakes.fetch_add(1, Ordering::SeqCst);
}
#[cfg(test)]
pub(crate) fn wakes(&self) -> u64 {
self.wakes.load(Ordering::SeqCst)
}
}
struct Done {
finished: Mutex<bool>,
changed: Condvar,
}
impl Done {
fn new() -> Self {
Self {
finished: Mutex::new(false),
changed: Condvar::new(),
}
}
fn set(&self) {
*lock(&self.finished) = true;
self.changed.notify_all();
}
fn wait(&self, timeout: Duration) -> bool {
let guard = lock(&self.finished);
let (guard, _) = self
.changed
.wait_timeout_while(guard, timeout, |finished| !*finished)
.unwrap_or_else(|poisoned| poisoned.into_inner());
*guard
}
}
struct MarkDone(Arc<Done>);
impl Drop for MarkDone {
fn drop(&mut self) {
self.0.set();
}
}
pub(crate) struct Worker {
handle: JoinHandle<()>,
done: Arc<Done>,
wake: Box<dyn Fn() + Send + Sync>,
}
impl Worker {
pub(crate) fn spawn(
name: &str,
wake: impl Fn() + Send + Sync + 'static,
body: impl FnOnce() + Send + 'static,
) -> io::Result<Worker> {
let done = Arc::new(Done::new());
let mark = MarkDone(done.clone());
let handle = thread::Builder::new()
.name(name.to_string())
.spawn(move || {
let _mark = mark;
body();
})?;
Ok(Worker {
handle,
done,
wake: Box::new(wake),
})
}
pub(crate) fn stop(self) -> bool {
self.stop_within(STOP_BOUND)
}
pub(crate) fn stop_within(self, bound: Duration) -> bool {
if self.handle.thread().id() == thread::current().id() {
return false;
}
let deadline = Instant::now() + bound;
loop {
(self.wake)();
if self.done.wait(WAKE_RETRY) {
let _ = self.handle.join();
return true;
}
if Instant::now() >= deadline {
return false;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::AtomicBool;
use std::sync::mpsc;
#[test]
fn a_stop_ends_an_hour_long_wait_at_once() {
let stopper = Arc::new(Stopper::new());
let (s, (tx, rx)) = (stopper.clone(), mpsc::channel());
let thread = thread::spawn(move || {
let _ = tx.send(s.wait(Duration::from_secs(3600)));
});
stopper.stop();
assert_eq!(
rx.recv_timeout(Duration::from_secs(10)),
Ok(true),
"the wait must end on stop"
);
thread.join().unwrap();
assert!(stopper.wait(Duration::from_secs(3600)));
}
#[test]
fn a_wait_without_a_stop_times_out_and_is_counted() {
let stopper = Stopper::new();
assert!(!stopper.wait(Duration::from_millis(20)));
assert_eq!(stopper.wakes(), 1);
stopper.note_wake();
assert_eq!(stopper.wakes(), 2);
}
#[test]
fn a_connection_to_itself_ends_a_blocked_accept_and_frees_the_port() {
let listener = TcpListener::bind(("0.0.0.0", 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let stopping = Arc::new(AtomicBool::new(false));
let flag = stopping.clone();
let worker = Worker::spawn(
"rk-send-test-accept",
move || {
let address = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
let _ = TcpStream::connect_timeout(&address, Duration::from_millis(250));
},
move || {
while !flag.load(Ordering::SeqCst) {
let _ = listener.accept();
}
},
)
.unwrap();
thread::sleep(Duration::from_millis(100));
stopping.store(true, Ordering::SeqCst);
assert!(
worker.stop_within(Duration::from_secs(10)),
"the blocked accept must end"
);
TcpListener::bind(("0.0.0.0", port)).expect("the port is free once the worker has ended");
}
#[test]
fn a_thread_that_will_not_end_is_left_after_the_bound() {
let (release, wait) = mpsc::channel::<()>();
let worker = Worker::spawn(
"rk-send-test-stuck",
|| {},
move || {
let _ = wait.recv();
},
)
.unwrap();
assert!(!worker.stop_within(Duration::from_millis(300)));
let _ = release.send(());
}
#[test]
fn a_thread_that_ends_on_its_own_is_joined() {
let worker = Worker::spawn("rk-send-test-short", || {}, || {}).unwrap();
assert!(worker.stop_within(Duration::from_secs(10)));
}
}