#![cfg(all(feature = "test-hooks", feature = "parallel"))]
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use threadteam::test_hooks::{self, Mutation, Point};
use threadteam::{Pool, Team};
static SERIAL: Mutex<()> = Mutex::new(());
fn watchdog() -> impl Drop {
struct Stop(Sender<()>);
impl Drop for Stop {
fn drop(&mut self) {
let _ = self.0.send(());
}
}
let (tx, rx) = channel::<()>();
std::thread::spawn(move || {
if rx.recv_timeout(Duration::from_secs(45)).is_err() {
eprintln!("watchdog: test stalled");
std::process::exit(101);
}
});
Stop(tx)
}
struct Pauser {
armed: AtomicBool,
reached: Mutex<Sender<()>>,
go: Mutex<Receiver<()>>,
}
struct Handle {
pauser: Arc<Pauser>,
reached: Receiver<()>,
go: Sender<()>,
}
impl Handle {
fn new() -> Handle {
let (reached_tx, reached) = channel();
let (go, go_rx) = channel();
Handle {
pauser: Arc::new(Pauser {
armed: AtomicBool::new(false),
reached: Mutex::new(reached_tx),
go: Mutex::new(go_rx),
}),
reached,
go,
}
}
fn arm(&self) {
self.pauser.armed.store(true, Ordering::SeqCst);
}
fn wait_reached(&self) {
self.reached
.recv_timeout(Duration::from_secs(5))
.expect("the paused thread reached its hook point");
}
fn release(&self) {
self.go.send(()).unwrap();
}
}
impl Pauser {
fn hit(&self) {
if self.armed.swap(false, Ordering::SeqCst) {
let _ = self.reached.lock().unwrap().send(());
let _ = self.go.lock().unwrap().recv();
}
}
}
static PARKS: AtomicUsize = AtomicUsize::new(0);
fn wait_first_park() {
let start = std::time::Instant::now();
while PARKS.load(Ordering::SeqCst) == 0 {
assert!(
start.elapsed() < Duration::from_secs(5),
"the member never parked"
);
std::thread::sleep(Duration::from_millis(1));
}
std::thread::sleep(Duration::from_millis(20));
}
struct Reset;
impl Drop for Reset {
fn drop(&mut self) {
test_hooks::set_hook(None);
test_hooks::set_mutation(Mutation::None);
}
}
fn team() -> Team {
Pool::builder()
.threads(2)
.idle_spin(Duration::ZERO)
.acquire_timeout(Duration::from_secs(5))
.build()
.unwrap()
.into_team(2)
.expect("an idle pool")
}
fn run_section(team: &Team, hits: &AtomicUsize) {
team.exec(2).fork(2, |_, _| {
hits.fetch_add(1, Ordering::SeqCst);
});
}
fn install(raised: &Handle, checked: &Handle, wake: &Handle) {
let (a, b, c) = (
raised.pauser.clone(),
checked.pauser.clone(),
wake.pauser.clone(),
);
test_hooks::set_hook(Some(Arc::new(move |point| match point {
Point::ParkRaised => a.hit(),
Point::ParkChecked => {
PARKS.fetch_add(1, Ordering::SeqCst);
b.hit()
}
Point::WakeBegin => c.hit(),
})));
}
fn member_paused_then_dispatch(point: Point, mutation: Mutation) -> bool {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _reset = Reset;
PARKS.store(0, Ordering::SeqCst);
let (raised, checked, wake) = (Handle::new(), Handle::new(), Handle::new());
install(&raised, &checked, &wake);
let team = team();
wait_first_park();
let paused = if point == Point::ParkRaised {
&raised
} else {
&checked
};
paused.arm();
let hits = AtomicUsize::new(0);
run_section(&team, &hits);
paused.wait_reached();
test_hooks::set_mutation(mutation);
let (done_tx, done_rx) = channel();
let (team_ref, hits_ref) = (&team, &hits);
let finished = std::thread::scope(|s| {
s.spawn(move || {
run_section(team_ref, hits_ref); let _ = done_tx.send(());
});
std::thread::sleep(Duration::from_millis(50));
paused.release();
let finished = done_rx.recv_timeout(Duration::from_millis(600)).is_ok();
if !finished {
test_hooks::set_mutation(Mutation::None);
test_hooks::wake_parked(team_ref.pool());
}
finished
});
if finished {
assert_eq!(hits.load(Ordering::SeqCst), 4, "both sections ran fully");
}
finished
}
#[test]
fn a_section_published_while_the_member_is_about_to_park_is_not_missed() {
let _w = watchdog();
assert!(member_paused_then_dispatch(
Point::ParkRaised,
Mutation::None
));
}
#[test]
fn a_section_published_between_the_members_check_and_its_wait_is_not_missed() {
let _w = watchdog();
assert!(member_paused_then_dispatch(
Point::ParkChecked,
Mutation::None
));
}
#[test]
fn the_first_interleaving_stalls_when_the_member_skips_its_recheck() {
let _w = watchdog();
assert!(
!member_paused_then_dispatch(Point::ParkRaised, Mutation::SkipRecheck),
"the scenario must be able to see the lost wake-up"
);
}
#[test]
fn the_second_interleaving_stalls_when_the_leader_skips_the_lock_handoff() {
let _w = watchdog();
assert!(
!member_paused_then_dispatch(Point::ParkChecked, Mutation::NoLockHandoff),
"the scenario must be able to see the lost wake-up"
);
}
#[test]
fn a_leader_paused_between_bumping_and_reading_parked_does_not_lose_the_member() {
let _w = watchdog();
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let _reset = Reset;
PARKS.store(0, Ordering::SeqCst);
let (raised, checked, wake) = (Handle::new(), Handle::new(), Handle::new());
install(&raised, &checked, &wake);
let team = team();
wait_first_park();
raised.arm();
let hits = AtomicUsize::new(0);
run_section(&team, &hits); raised.wait_reached();
wake.arm();
let (done_tx, done_rx) = channel();
let (team_ref, hits_ref) = (&team, &hits);
std::thread::scope(|s| {
s.spawn(move || {
run_section(team_ref, hits_ref);
let _ = done_tx.send(());
});
wake.wait_reached();
raised.release();
std::thread::sleep(Duration::from_millis(50));
wake.release();
assert!(done_rx.recv_timeout(Duration::from_secs(2)).is_ok());
});
assert_eq!(hits.load(Ordering::SeqCst), 4);
}