#![cfg(feature = "scheduler")]
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering},
},
thread,
time::Instant,
};
use hyphae::{
Cell, CellMutable, MapExt, MaterializeDefinite, Mutable, Signal, Watchable, batch, join_vec,
scheduler::no_coalesce,
};
fn scheduler_test_serial() -> std::sync::MutexGuard<'static, ()> {
hyphae::scheduler::set_wave_threshold_for_test(4);
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn no_op_is_dropped_or_stranded_under_sustained_contention() {
let _serial = scheduler_test_serial();
const N_THREADS: usize = 16;
const OPS_PER_THREAD: u64 = 50_000;
const EXPECTED: u64 = N_THREADS as u64 * OPS_PER_THREAD;
let sink = no_coalesce(|| Cell::<u64, CellMutable>::new(0));
let observed = Arc::new(AtomicU64::new(0));
let obs = observed.clone();
let seen_first = Arc::new(AtomicBool::new(false));
let guard = sink.clone().lock().subscribe(move |sig| {
if matches!(sig, Signal::Value(_)) {
if !seen_first.swap(true, Ordering::SeqCst) {
return; }
obs.fetch_add(1, Ordering::SeqCst);
}
});
let start = Instant::now();
thread::scope(|s| {
for t in 0..N_THREADS {
let sink = sink.clone();
s.spawn(move || {
for i in 0..OPS_PER_THREAD {
batch(|| sink.set((t as u64) << 32 | i));
}
});
}
});
let elapsed = start.elapsed();
let got = observed.load(Ordering::SeqCst);
eprintln!(
"observed={got} expected={EXPECTED} elapsed={elapsed:?} ({N_THREADS} threads x {OPS_PER_THREAD} ops)"
);
assert_eq!(
got, EXPECTED,
"some ops were dropped/stranded under concurrent contention (or double-counted)"
);
drop(guard);
}
#[test]
fn joiner_batches_never_hang_under_sustained_pressure() {
let _serial = scheduler_test_serial();
let stop = Arc::new(AtomicBool::new(false));
let bg_stop = stop.clone();
let bg_cell = no_coalesce(|| Cell::<u64, CellMutable>::new(0));
let bg_handle = {
let bg_cell = bg_cell.clone();
thread::spawn(move || {
let mut i = 0u64;
while !bg_stop.load(Ordering::Relaxed) {
batch(|| bg_cell.set(i));
i += 1;
}
})
};
const N_THREADS: usize = 8;
const OPS_PER_THREAD: usize = 5_000;
let completed = Arc::new(AtomicU64::new(0));
thread::scope(|s| {
for _ in 0..N_THREADS {
let completed = completed.clone();
let cell = bg_cell.clone();
s.spawn(move || {
for i in 0..OPS_PER_THREAD {
batch(|| cell.set(i as u64));
completed.fetch_add(1, Ordering::SeqCst);
}
});
}
});
stop.store(true, Ordering::Relaxed);
bg_handle.join().unwrap();
assert_eq!(
completed.load(Ordering::SeqCst),
(N_THREADS * OPS_PER_THREAD) as u64,
"a joiner batch() call never returned (would have hung the thread::scope join above)"
);
}
#[test]
fn join_vec_wide_wave_settles_correctly_under_repeated_concurrent_execution() {
let _serial = scheduler_test_serial();
const N: i64 = 16;
const ITERATIONS: i64 = 2_000;
let s = Cell::new(0i64);
let cells: Vec<_> = (0..N)
.map(|k| s.clone().map(move |x| x * (k + 1)).materialize())
.collect();
let combined = join_vec(cells);
let last = Arc::new(Mutex::new(vec![0i64; N as usize]));
let sink = last.clone();
let guard = combined.subscribe(move |sig| {
if let Signal::Value(v) = sig {
*sink.lock().unwrap() = (**v).clone();
}
});
for i in 0..ITERATIONS {
batch(|| s.set(i));
let expected: Vec<i64> = (0..N).map(|k| i * (k + 1)).collect();
assert_eq!(
*last.lock().unwrap(),
expected,
"join_vec settled on a value inconsistent with the most recently set input at i={i}"
);
}
drop(guard);
}