#![cfg(feature = "scheduler")]
use std::{
sync::{
Arc,
atomic::{AtomicI64, Ordering},
},
thread,
time::{Duration, Instant},
};
use parking_lot::Mutex;
use hyphae::{
AuditExt, BackpressureExt, BufferTimeExt, Cell, ColdExt, ConcatExt, DebounceExt, DelayExt,
FinalizeExt, Gettable, Materialize, Mutable, RetryExt, Signal, ThrottleExt, TimeoutExt,
Watchable, batch,
};
static SCHEDULER_TEST_LOCK: Mutex<()> = Mutex::new(());
fn scheduler_test_serial() -> parking_lot::MutexGuard<'static, ()> {
hyphae::scheduler::set_wave_threshold_for_test(4);
SCHEDULER_TEST_LOCK.lock()
}
fn index_value(index: usize) -> i64 {
i64::try_from(index).unwrap_or(i64::MAX)
}
const WIDE: usize = 16;
const TIMEOUT_ITERS: i64 = 100;
const STRUCTURAL_ITERS: i64 = 200;
const DROP_NEWEST_CAPACITY: usize = 8;
fn wait_until(deadline: Duration, mut settled: impl FnMut() -> bool) -> bool {
let start = Instant::now();
loop {
if settled() {
return true;
}
if start.elapsed() >= deadline {
return false;
}
thread::sleep(Duration::from_millis(5));
}
}
const SETTLE_DEADLINE: Duration = Duration::from_secs(5);
#[test]
fn debounce_wide_parallel_input_wave_all_fire() {
let _serial = scheduler_test_serial();
let dur = Duration::from_millis(30);
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().debounce(dur).materialize())
.collect();
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(1000 + index_value(i));
}
});
wait_until(SETTLE_DEADLINE, || {
outs.iter()
.enumerate()
.all(|(i, o)| o.get() == 1000 + index_value(i))
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
1000 + index_value(i),
"debounce instance {i} settled on the wrong value under a wide input wave"
);
}
}
#[test]
fn throttle_wide_parallel_wave_leading_emit_correct() {
let _serial = scheduler_test_serial();
let dur = Duration::from_millis(20);
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().throttle(dur).materialize())
.collect();
for round in 0..3i64 {
thread::sleep(Duration::from_millis(60));
let base = (round + 1) * 1000;
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(base + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
base + index_value(i),
"throttle instance {i} leading-edge emit wrong under a wide wave (round {round})"
);
}
}
}
#[test]
fn delay_wide_parallel_input_wave_all_fire() {
let _serial = scheduler_test_serial();
let dur = Duration::from_millis(30);
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().delay(dur).materialize())
.collect();
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(2000 + index_value(i));
}
});
wait_until(SETTLE_DEADLINE, || {
outs.iter()
.enumerate()
.all(|(i, o)| o.get() == 2000 + index_value(i))
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
2000 + index_value(i),
"delay instance {i} delivered the wrong value under a wide input wave"
);
}
}
#[test]
fn timeout_wide_parallel_wave_value_passthrough_correct() {
let _serial = scheduler_test_serial();
let dur = Duration::from_millis(500);
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().timeout(dur).materialize())
.collect();
for it in 1..=TIMEOUT_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
it * 1000 + index_value(i),
"timeout instance {i} passed a torn/stale value under a wide wave (iter {it})"
);
}
}
}
#[test]
fn buffer_time_wide_parallel_input_wave_all_collect() {
let _serial = scheduler_test_serial();
let dur = Duration::from_millis(30);
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().buffer_time(dur).materialize())
.collect();
let seen: Vec<Arc<Mutex<Vec<i64>>>> = (0..WIDE)
.map(|_| Arc::new(Mutex::new(Vec::new())))
.collect();
let mut guards = Vec::new();
for (out, sink) in outs.iter().zip(&seen) {
let sink = sink.clone();
guards.push(out.subscribe(move |sig| {
if let Signal::Value(v) = sig {
sink.lock().extend(v.iter().copied());
}
}));
}
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(3000 + index_value(i));
}
});
wait_until(SETTLE_DEADLINE, || {
seen.iter()
.enumerate()
.all(|(i, s)| *s.lock() == vec![3000 + index_value(i)])
});
for (i, s) in seen.iter().enumerate() {
assert_eq!(
*s.lock(),
vec![3000 + index_value(i)],
"buffer_time instance {i} lost/leaked its buffered value under a wide input wave"
);
}
drop(guards);
}
#[test]
fn audit_wide_parallel_input_wave_all_fire() {
let _serial = scheduler_test_serial();
let dur = Duration::from_millis(30);
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().audit(dur).materialize())
.collect();
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(4000 + index_value(i));
}
});
wait_until(SETTLE_DEADLINE, || {
outs.iter()
.enumerate()
.all(|(i, o)| o.get() == 4000 + index_value(i))
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
4000 + index_value(i),
"audit instance {i} sampled the wrong last value under a wide input wave"
);
}
}
#[test]
fn backpressure_drop_oldest_wide_parallel_wave_correct() {
let _serial = scheduler_test_serial();
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().drop_oldest(8).materialize())
.collect();
for it in 1..=STRUCTURAL_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
it * 1000 + index_value(i),
"drop_oldest instance {i} wrong under a wide wave (iter {it})"
);
}
}
}
#[test]
fn backpressure_drop_newest_wide_parallel_wave_correct() {
let _serial = scheduler_test_serial();
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().drop_newest(DROP_NEWEST_CAPACITY).materialize())
.collect();
for it in 1..=STRUCTURAL_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
let expected = it.min(index_value(DROP_NEWEST_CAPACITY)) * 1000 + index_value(i);
assert_eq!(
out.get(),
expected,
"drop_newest instance {i} wrong under a wide wave (iter {it})"
);
}
}
}
#[test]
fn backpressure_sample_latest_wide_parallel_wave_correct() {
let _serial = scheduler_test_serial();
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().sample_latest().materialize())
.collect();
for it in 1..=STRUCTURAL_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
it * 1000 + index_value(i),
"sample_latest instance {i} wrong under a wide wave (iter {it})"
);
}
}
}
#[test]
fn concat_wide_parallel_wave_both_sides_correct() {
let _serial = scheduler_test_serial();
let firsts: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let seconds: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = firsts
.iter()
.zip(seconds.iter())
.map(|(f, s)| f.clone().concat(s.clone()).materialize())
.collect();
batch(|| {
for (i, f) in firsts.iter().enumerate() {
f.set(5000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
5000 + index_value(i),
"concat instance {i} wrong on the first-input side under a wide wave"
);
}
for f in &firsts {
f.complete();
}
batch(|| {
for (i, s) in seconds.iter().enumerate() {
s.set(6000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
6000 + index_value(i),
"concat instance {i} wrong on the second-input side after the completion hand-off"
);
}
}
#[test]
fn cold_wide_parallel_wave_settles_some() {
let _serial = scheduler_test_serial();
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().cold().materialize())
.collect();
for (i, out) in outs.iter().enumerate() {
assert_eq!(out.get(), None, "cold instance {i} should start None");
}
for it in 1..=STRUCTURAL_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
Some(Arc::new(it * 1000 + index_value(i))),
"cold instance {i} settled wrong under a wide wave (iter {it})"
);
}
}
}
#[test]
fn finalize_wide_parallel_wave_passthrough_and_terminal() {
let _serial = scheduler_test_serial();
let flags: Vec<Arc<AtomicI64>> = (0..WIDE).map(|_| Arc::new(AtomicI64::new(0))).collect();
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.zip(flags.iter())
.map(|(s, flag)| {
let flag = flag.clone();
s.clone()
.finalize(move || {
flag.fetch_add(1, Ordering::SeqCst);
})
.materialize()
})
.collect();
for it in 1..=STRUCTURAL_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
it * 1000 + index_value(i),
"finalize instance {i} passed a wrong value under a wide wave (iter {it})"
);
}
}
for s in &sources {
s.complete();
}
for (i, flag) in flags.iter().enumerate() {
assert_eq!(
flag.load(Ordering::SeqCst),
1,
"finalize instance {i} terminal callback did not fire exactly once"
);
}
}
#[test]
fn retry_wide_parallel_wave_value_passthrough() {
let _serial = scheduler_test_serial();
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let outs: Vec<_> = sources
.iter()
.map(|s| s.clone().retry(1_000).materialize())
.collect();
for it in 1..=STRUCTURAL_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, out) in outs.iter().enumerate() {
assert_eq!(
out.get(),
it * 1000 + index_value(i),
"retry instance {i} passed a wrong value under a wide wave (iter {it})"
);
}
}
}
#[test]
fn parallel_wide_parallel_input_wave_correct() {
let _serial = scheduler_test_serial();
let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
let cells: Vec<_> = sources.iter().map(hyphae::ParallelExt::parallel).collect();
let slots: Vec<Arc<AtomicI64>> = (0..WIDE)
.map(|_| Arc::new(AtomicI64::new(i64::MIN)))
.collect();
let mut guards = Vec::new();
for (cell, slot) in cells.iter().zip(&slots) {
let slot = slot.clone();
guards.push(cell.subscribe(move |sig| {
if let Signal::Value(v) = sig {
slot.store(**v, Ordering::SeqCst);
}
}));
}
for it in 1..=STRUCTURAL_ITERS {
batch(|| {
for (i, s) in sources.iter().enumerate() {
s.set(it * 1000 + index_value(i));
}
});
for (i, slot) in slots.iter().enumerate() {
assert_eq!(
slot.load(Ordering::SeqCst),
it * 1000 + index_value(i),
"parallel instance {i} delivered a wrong value under a wide wave (iter {it})"
);
}
}
drop(guards);
}