use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Barrier, Mutex};
use std::time::{Duration, Instant};
use super::Builder;
use crate::rt::workers::{THREAD_FAILS, Workers};
fn within(within: Duration, mut done: impl FnMut() -> bool) -> bool {
let deadline = Instant::now() + within;
let mut pause = Duration::from_micros(50);
while !done() {
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(pause);
pause = (pause * 2).min(Duration::from_millis(5));
}
true
}
#[derive(Default)]
struct Gate(AtomicBool);
impl Gate {
fn hold(&self) {
within(Duration::from_secs(10), || self.0.load(Ordering::SeqCst));
}
fn open(&self) {
self.0.store(true, Ordering::SeqCst);
}
}
#[test]
fn a_timed_shutdown_waits_for_a_running_worker_past_an_exit_left_from_before() {
let rt = Builder::new()
.worker_threads(1)
.max_worker_threads(2)
.pin_workers(false)
.handoff_after(None)
.build()
.unwrap();
assert_eq!(Workers::start(rt.inner(), 1).unwrap(), 1);
assert!(rt.inner().workers.stop_one(), "the second worker stops");
assert!(
within(Duration::from_secs(10), || !rt.exits.is_empty()),
"the stopped worker never sent its exit"
);
let (started, began) = std::sync::mpsc::channel();
let done = Arc::new(AtomicBool::new(false));
let (inner, ran) = (Arc::clone(rt.inner()), Arc::clone(&done));
rt.spawn(async move {
started.send(()).unwrap();
within(Duration::from_secs(10), || inner.lingering().is_closing());
std::thread::sleep(Duration::from_millis(50));
ran.store(true, Ordering::SeqCst);
})
.detach();
began.recv_timeout(Duration::from_secs(10)).unwrap();
rt.shutdown_timeout(Duration::from_secs(20));
assert!(
done.load(Ordering::SeqCst),
"the timed shutdown returned while a worker still ran its task"
);
}
#[test]
fn a_worker_whose_thread_cannot_start_drops_no_task_placed_meanwhile() {
let rt = Builder::new()
.worker_threads(0)
.max_worker_threads(1)
.pin_workers(false)
.build()
.unwrap();
let handle = rt.handle();
let placed = Rc::new(RefCell::new(None));
let slot = Rc::clone(&placed);
THREAD_FAILS.with(|fails| {
*fails.borrow_mut() = Some(Box::new(move || {
*slot.borrow_mut() = Some(handle.spawn(async { 5u32 }));
}));
});
assert!(
Workers::start(rt.inner(), 1).is_err(),
"the thread start did not fail"
);
let task = placed
.borrow_mut()
.take()
.expect("no spawn ran as the start failed");
let out = rt.block_on(crate::time::timeout(Duration::from_secs(10), task.join()));
assert!(
matches!(out, Ok(Ok(5))),
"the task placed as the thread start failed was dropped unrun"
);
}
enum Round {
Late(std::thread::JoinHandle<()>, [Arc<Gate>; 2]),
Missed,
}
fn late_take_round() -> Round {
let target = Arc::new(Mutex::new(None));
let held = Arc::new(Gate::default());
let in_hook = Arc::new(AtomicBool::new(false));
let (t, h, entered) = (Arc::clone(&target), Arc::clone(&held), Arc::clone(&in_hook));
let rt = Builder::new()
.worker_threads(2)
.pin_workers(false)
.handoff_after(Some(Duration::from_millis(20)))
.on_long_poll(move |meta| {
if *t.lock().unwrap() == Some(meta.id()) {
entered.store(true, Ordering::SeqCst);
h.hold();
}
})
.build()
.unwrap();
let handle = rt.handle();
assert!(within(Duration::from_secs(10), || handle.workers() == 2));
let gates = [Arc::new(Gate::default()), Arc::new(Gate::default())];
let at_once = Arc::new(Barrier::new(2));
let tasks: Vec<_> = (0..2)
.map(|k| {
let (gate, at_once) = (Arc::clone(&gates[k]), Arc::clone(&at_once));
handle
.spawn_on_worker(k, async move {
at_once.wait();
gate.hold();
})
.unwrap()
})
.collect();
*target.lock().unwrap() = Some(tasks[0].id());
tasks.into_iter().for_each(crate::Task::detach);
assert!(
within(Duration::from_secs(10), || in_hook.load(Ordering::SeqCst)),
"the first worker's blocked poll was never told"
);
let inner = Arc::clone(rt.inner());
let second = inner
.workers
.monitor_first_life(1)
.expect("the second place is watched");
let early = second.is_detached();
let dropper = std::thread::spawn(move || drop(rt));
within(Duration::from_secs(1), || {
inner.workers.threads_listed() == 0
});
held.open();
if !early && within(Duration::from_secs(1), || second.is_detached()) {
return Round::Late(dropper, gates);
}
gates.iter().for_each(|gate| gate.open());
dropper.join().unwrap();
Round::Missed
}
#[test]
fn a_drop_never_waits_on_a_thread_its_monitor_takes_as_it_closes() {
for _ in 0..10 {
let Round::Late(dropper, gates) = late_take_round() else {
continue;
};
let returned = within(Duration::from_secs(3), || dropper.is_finished());
gates.iter().for_each(|gate| gate.open());
assert!(
returned,
"the drop waits on a thread blocked in a poll whose shard was taken"
);
dropper.join().unwrap();
return;
}
panic!("no round took a shard after the close");
}
fn drop_with_pause(
rt: super::Runtime,
after_take: bool,
pause: impl FnOnce() + Send + 'static,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
super::TEARDOWN_PAUSE.with(|slot| *slot.borrow_mut() = Some((after_take, Box::new(pause))));
drop(rt);
})
}
#[test]
fn a_drop_never_waits_for_good_on_a_worker_started_as_it_stopped_the_shards() {
let rt = Builder::new()
.worker_threads(1)
.max_worker_threads(2)
.pin_workers(false)
.build()
.unwrap();
let inner = Arc::clone(rt.inner());
let starter = Arc::clone(&inner);
let (started_tx, started_rx) = std::sync::mpsc::channel();
let dropper = drop_with_pause(rt, false, move || {
started_tx.send(Workers::start(&starter, 1).ok()).unwrap();
});
let started = started_rx
.recv_timeout(Duration::from_secs(10))
.expect("the drop never came to its take");
let returned = within(Duration::from_secs(3), || dropper.is_finished());
if !returned {
inner.workers.stop_all();
}
assert!(
returned,
"the drop waits for good on a worker started as it stopped the shards"
);
dropper.join().unwrap();
assert_eq!(
started,
Some(1),
"the start in the drop's window started no worker"
);
}
#[test]
fn a_worker_start_after_a_drop_took_its_threads_starts_nothing() {
let rt = Builder::new()
.worker_threads(1)
.max_worker_threads(2)
.pin_workers(false)
.build()
.unwrap();
let inner = Arc::clone(rt.inner());
let starter = Arc::clone(&inner);
let (started_tx, started_rx) = std::sync::mpsc::channel();
let dropper = drop_with_pause(rt, true, move || {
started_tx.send(Workers::start(&starter, 1).ok()).unwrap();
});
let started = started_rx
.recv_timeout(Duration::from_secs(10))
.expect("the drop never took its list");
assert!(
within(Duration::from_secs(10), || dropper.is_finished()),
"the drop never returned"
);
dropper.join().unwrap();
let listed = inner.workers.threads_listed();
inner.workers.stop_all();
assert_eq!(
started,
Some(0),
"a start after the drop took its threads started a worker"
);
assert_eq!(
listed, 0,
"a thread went on the list after the drop took it"
);
}