d6c99c3527
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
216 lines
6.1 KiB
Rust
216 lines
6.1 KiB
Rust
//! The scheduler under panics, idle workers and a full edge, with real threads behind a
|
|
//! watchdog. Evidence: `context/test-evidence/improvise-incremental.md`.
|
|
|
|
mod support;
|
|
|
|
use improvise_incremental::sched::{Executor, Pool, Turn, Unit};
|
|
use improvise_incremental::staged::{Cell, Staged, single_circuit};
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use support::watchdog::within;
|
|
use zset::ZSet;
|
|
|
|
struct Boom;
|
|
|
|
impl Unit for Boom {
|
|
fn turn(&mut self, _: usize) -> Turn {
|
|
panic!("boom");
|
|
}
|
|
|
|
fn neighbours(&self) -> &[usize] {
|
|
&[]
|
|
}
|
|
}
|
|
|
|
/// A unit that counts its turns.
|
|
struct Count(Arc<AtomicUsize>);
|
|
|
|
impl Unit for Count {
|
|
fn turn(&mut self, _: usize) -> Turn {
|
|
self.0.fetch_add(1, Ordering::SeqCst);
|
|
Turn {
|
|
progress: false,
|
|
more: false,
|
|
}
|
|
}
|
|
|
|
fn neighbours(&self) -> &[usize] {
|
|
&[]
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_panicking_turn_does_not_hang_finish() {
|
|
within(5, "Pool::finish after a unit panicked", || {
|
|
let exec = Arc::new(Executor::new(vec![Box::new(Boom)], 1));
|
|
let pool = Pool::start(exec.clone(), 2);
|
|
exec.kick(0);
|
|
let _ = pool.finish();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_panicking_unit_is_reported_and_its_neighbours_still_run() {
|
|
let turns = Arc::new(AtomicUsize::new(0));
|
|
let t = turns.clone();
|
|
let err = within(5, "finish with one dead unit", move || {
|
|
let units: Vec<Box<dyn Unit>> = vec![Box::new(Boom), Box::new(Count(t))];
|
|
let exec = Arc::new(Executor::new(units, 1));
|
|
let pool = Pool::start(exec.clone(), 2);
|
|
exec.kick_all();
|
|
let err = pool.finish().err();
|
|
exec.kick(0);
|
|
exec.kick(1);
|
|
err
|
|
});
|
|
let err = err.expect("[panic] finish reports the panic");
|
|
assert_eq!(err.unit, 0);
|
|
assert!(
|
|
err.message.contains("boom"),
|
|
"[panic] the payload: {}",
|
|
err.message
|
|
);
|
|
assert!(
|
|
turns.load(Ordering::SeqCst) >= 1,
|
|
"[panic] the live unit ran"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_dead_unit_is_never_run_again() {
|
|
let exec = Executor::new(vec![Box::new(Boom)], 1);
|
|
exec.kick(0);
|
|
let mut c = exec.claim(|_| 0).unwrap();
|
|
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| c.run(1)));
|
|
assert!(r.is_err());
|
|
exec.abandon(c, "boom".to_string());
|
|
exec.kick(0);
|
|
assert!(
|
|
exec.claim(|_| 0).is_none(),
|
|
"[dead] a kick does not revive it"
|
|
);
|
|
assert!(exec.idle());
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "no such unit")]
|
|
fn a_kick_of_a_unit_that_does_not_exist_panics_before_the_lock() {
|
|
Executor::new(vec![Box::new(Boom)], 1).kick(5);
|
|
}
|
|
|
|
#[test]
|
|
fn an_executor_survives_a_bad_kick() {
|
|
let exec = Executor::new(vec![Box::new(Boom)], 1);
|
|
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| exec.kick(5)));
|
|
assert!(r.is_err());
|
|
assert!(
|
|
exec.idle(),
|
|
"[lock] the lock was not poisoned by the bad index"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_worker_stays_through_wakes_with_nothing_queued() {
|
|
within(10, "the pool with idle wakes", || {
|
|
let turns = Arc::new(AtomicUsize::new(0));
|
|
let units: Vec<Box<dyn Unit>> =
|
|
vec![Box::new(Count(turns.clone())), Box::new(Count(turns))];
|
|
let exec = Arc::new(Executor::new(units, 1));
|
|
let pool = Pool::start(exec.clone(), 4);
|
|
assert_eq!(exec.workers(), 4);
|
|
for round in 0..200 {
|
|
exec.kick(round % 2);
|
|
if round % 20 == 0 {
|
|
let _ = exec.wait_idle();
|
|
}
|
|
}
|
|
let _ = exec.wait_idle();
|
|
assert_eq!(
|
|
exec.workers(),
|
|
4,
|
|
"[workers] a wake with nothing queued sent a worker home"
|
|
);
|
|
let _ = pool.finish();
|
|
assert_eq!(exec.workers(), 0, "[workers] finish stops them all");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_pool_that_is_dropped_stops_its_threads() {
|
|
within(10, "dropping a pool", || {
|
|
let exec = Arc::new(Executor::new(vec![Box::new(Count(Arc::default()))], 1));
|
|
{
|
|
let _pool = Pool::start(exec.clone(), 3);
|
|
assert_eq!(exec.workers(), 3);
|
|
}
|
|
assert_eq!(exec.workers(), 0, "[drop] no thread is left parked");
|
|
});
|
|
}
|
|
|
|
fn cmd(r: u8, c: u8, v: i64) -> ZSet<Cell> {
|
|
[((r, c, v), 1)].into_iter().collect()
|
|
}
|
|
|
|
#[test]
|
|
fn a_full_edge_does_not_make_its_sender_spin() {
|
|
let staged = Staged::new(1, 1);
|
|
let exec = staged.executor();
|
|
// Hold the aggregate and the projection, so nothing drains the head's edge.
|
|
let agg = exec.claim(|_| 1).unwrap();
|
|
let proj = exec.claim(|_| 1).unwrap();
|
|
assert_eq!((agg.id(), proj.id()), (1, 2));
|
|
for n in 0..4 {
|
|
staged.submit(cmd(0, 0, n));
|
|
let mut head = exec.claim(|_| 0).unwrap();
|
|
let t = head.run(exec.budget());
|
|
exec.finish(head, t);
|
|
}
|
|
let mut turns = 0;
|
|
while let Some(mut head) = exec.claim(|_| 0) {
|
|
turns += 1;
|
|
assert!(
|
|
turns < 50,
|
|
"[spin] the head keeps asking to run with its edge full"
|
|
);
|
|
let t = head.run(exec.budget());
|
|
exec.finish(head, t);
|
|
}
|
|
let t = Turn {
|
|
progress: false,
|
|
more: false,
|
|
};
|
|
exec.finish(agg, t);
|
|
exec.finish(proj, t);
|
|
}
|
|
|
|
#[test]
|
|
fn a_unit_whose_kicks_coalesced_still_drains_its_edge() {
|
|
let staged = Staged::new(4, 1);
|
|
let exec = staged.executor();
|
|
// Settle the start-up requests.
|
|
while let Some(mut u) = exec.claim(|_| 0) {
|
|
let t = u.run(exec.budget());
|
|
exec.finish(u, t);
|
|
}
|
|
// Two commands cross before the aggregate runs: its two kicks are one queue entry.
|
|
staged.submit(cmd(0, 0, 1));
|
|
let mut head = exec.claim(|_| 0).unwrap();
|
|
let t = head.run(1);
|
|
exec.finish(head, t);
|
|
staged.submit(cmd(1, 1, 2));
|
|
let mut head = exec.claim(|n| n - 1).unwrap();
|
|
assert_eq!(head.id(), 0);
|
|
let t = head.run(1);
|
|
exec.finish(head, t);
|
|
while let Some(mut u) = exec.claim(|_| 0) {
|
|
let t = u.run(1);
|
|
exec.finish(u, t);
|
|
}
|
|
let want = single_circuit(&[cmd(0, 0, 1), cmd(1, 1, 2)]);
|
|
assert_eq!(
|
|
staged.grid(),
|
|
want,
|
|
"[coalesced] the second frame was left unread"
|
|
);
|
|
}
|