sandbox: a setup assertion walked a tree that still held the warm-up worker #208

Merged
dcharlot merged 2 commits from dcharlot/tree-energy-setup-race into main 2026-10-10 17:16:42 -04:00 AGit
4 changed files with 36 additions and 4 deletions

View file

@ -1116,6 +1116,7 @@ dependencies = [
"hex",
"jcp-receipt",
"jcp-schema",
"serde",
"serde_json",
"thiserror 2.0.20",
]

View file

@ -412,6 +412,7 @@ dependencies = [
"hex",
"jcp-receipt",
"jcp-schema",
"serde",
"serde_json",
"thiserror",
]

View file

@ -1066,6 +1066,7 @@ dependencies = [
"hex",
"jcp-receipt",
"jcp-schema",
"serde",
"serde_json",
"thiserror 2.0.20",
]

View file

@ -106,6 +106,32 @@ fn ps_tree(root: i32) -> Vec<i32> {
tree
}
/// `root`'s tree once it has exactly `want` members. Panics after 20 s,
/// naming the tree it last saw.
///
/// A server's reply does not mean its worker is gone. The worker prints and
/// the parent reads that line while the worker still has its work to do and
/// its exit and reaping after that, so a tree walked straight after a reply
/// can still hold it. Waiting for the shape a test assumes is the only way to
/// assert that shape; asserting it on the first walk asserts a race, and that
/// race is real — the count came back 62 instead of 61 in 2 runs of 5 on this
/// host, more often the busier it is.
fn settled_tree(root: i32, want: usize) -> Vec<i32> {
let deadline = Instant::now() + Duration::from_secs(20);
loop {
let tree = ps_tree(root);
if tree.len() == want {
return tree;
}
assert!(
Instant::now() < deadline,
"the tree of {root} never settled to {want} members; last saw {}: {tree:?}",
tree.len()
);
std::thread::sleep(Duration::from_millis(20));
}
}
/// Each of `pids`' rusage once every one of them is quiet by the oracle's
/// rule: not running or queued (or exited) at two reads 20 ms apart, with
/// the same energy at both. Panics after 20 s.
@ -277,11 +303,14 @@ fn a_worker_reaped_while_its_tree_is_walked_is_never_missed() {
],
);
let root = s.pid() as u32;
// The first round trip runs once all 60 are forked; the tree is quiet
// before the first span.
// The first round trip runs once all 60 are forked. It does not leave the
// tree quiet, though — the worker it forks prints its pid before doing its
// work, so the reply arrives while that worker is still running — hence
// the wait below rather than a bare walk.
let _ = s.ask("warm up");
let tree = ps_tree(s.pid());
assert_eq!(tree.len(), 61, "setup: the server and its 60 idle children: {tree:?}");
// Not `ps_tree` + `assert_eq!` here: the warm-up round trip returns while
// its worker is still the server's child, so the first walk sees 62.
let tree = settled_tree(s.pid(), 61);
quiet(&tree);
let meter = s.meter();
let (mut exited, mut reaped, mut unsettled) = (0, 0, 0);