From 52fe6bff6f2ef97a4709647a5ee2070efaf5fb4c Mon Sep 17 00:00:00 2001 From: David Charlot Date: Sat, 10 Oct 2026 09:58:26 -0400 Subject: [PATCH 1/2] sandbox: a setup assertion walked a tree that still held the warm-up worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `a_worker_reaped_while_its_tree_is_walked_is_never_missed` fails about one run in five on the dev Mac, always the same way: assertion `left == right` failed: setup: the server and its 60 idle children left: 62 right: 61 Not load, and not the behaviour under test — the setup. The perl server's worker is if ($p == 0) { print "$$\n"; my $x = 0; $x += $_ for 1 .. 150000; exit 0 } so it prints its pid *before* doing its work. `ask("warm up")` returns as soon as that line is read, which is while the worker is still running, still the server's child, and not yet reaped. The next line walks the tree and finds 62 members. The comment above it said "the tree is quiet before the first span", and that is the part that was not true; load only widens the window. So the setup waits for the shape it asserts instead of asserting it on the first walk, through a `settled_tree(root, want)` in the same idiom as this file's `quiet()` — poll, return when it matches, panic after 20 s naming the tree it last saw. The test's own assertions are untouched, and the guard still fails the test if the tree never settles, so nothing here is weakened: a walk that cannot find the shape still stops the run. Measured on this host: 2 failures in 5 runs before, 12 passes in 12 after. The file's three other tree-count assertions were checked and are sound — their workers are long-lived (the awk blocks on `getline`), or the walk happens before any request, so there is no transient child to catch. Found while replicating `macos-energy-backend` locally for #183; it is the only failure in that job and it is unrelated to that PR, which touches `mesh/` alone. Co-Authored-By: Claude Opus 5 --- .../crates/sandbox-meter/tests/tree_energy.rs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/sandbox/sandbox-rs/crates/sandbox-meter/tests/tree_energy.rs b/sandbox/sandbox-rs/crates/sandbox-meter/tests/tree_energy.rs index cdf75aae3..6538ddf51 100644 --- a/sandbox/sandbox-rs/crates/sandbox-meter/tests/tree_energy.rs +++ b/sandbox/sandbox-rs/crates/sandbox-meter/tests/tree_energy.rs @@ -106,6 +106,32 @@ fn ps_tree(root: i32) -> Vec { 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 { + 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); -- 2.54.0 (Apple Git-157) From b1d7e1e2b29feadb80811ff3a8ef88f47c4f4e90 Mon Sep 17 00:00:00 2001 From: David Charlot Date: Sat, 10 Oct 2026 10:01:38 -0400 Subject: [PATCH 2/2] lockfiles: three workspaces' locks did not list a dependency jcp-efficiency grew MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `jcp-efficiency` gained a `serde` dependency. Its own workspace's lock (`jcp/jcp-rs`) records it; the three other workspaces that reach that crate through a cross-workspace path dependency do not, so on current main: cargo metadata --locked in sandbox-rs, jouledesk-rs, joulehook-rs FAILS Measured both ways — those three fail `--locked` at 8f62e388 and pass with this commit. Nothing else moved: one `"serde",` line per lock, resolved by cargo itself rather than written by hand. Nothing caught it because no workflow passes `--locked`, so CI silently re-resolves and goes green while the committed lock is wrong. The visible symptom is instead that building any of those three workspaces leaves a dirty `Cargo.lock` behind, which is how this surfaced: a `cargo test` in sandbox-rs dirtied the tree while I was verifying the commit before this one. This is the shape the repo keeps producing — a shared crate's manifest changes and only its own workspace's lock is refreshed. There are ~25 workspaces and no single build that covers them, so the rule has to be: after changing a crate's dependencies, refresh every lock that names it (`grep -rl 'name = ""' --include=Cargo.lock`), not just the one beside it. Co-Authored-By: Claude Opus 5 --- jouledesk/jouledesk-rs/Cargo.lock | 1 + joulehook/joulehook-rs/Cargo.lock | 1 + sandbox/sandbox-rs/Cargo.lock | 1 + 3 files changed, 3 insertions(+) diff --git a/jouledesk/jouledesk-rs/Cargo.lock b/jouledesk/jouledesk-rs/Cargo.lock index 5876b5edc..b5eb9e6ff 100644 --- a/jouledesk/jouledesk-rs/Cargo.lock +++ b/jouledesk/jouledesk-rs/Cargo.lock @@ -1116,6 +1116,7 @@ dependencies = [ "hex", "jcp-receipt", "jcp-schema", + "serde", "serde_json", "thiserror 2.0.20", ] diff --git a/joulehook/joulehook-rs/Cargo.lock b/joulehook/joulehook-rs/Cargo.lock index c7d87299f..ea9836e82 100644 --- a/joulehook/joulehook-rs/Cargo.lock +++ b/joulehook/joulehook-rs/Cargo.lock @@ -412,6 +412,7 @@ dependencies = [ "hex", "jcp-receipt", "jcp-schema", + "serde", "serde_json", "thiserror", ] diff --git a/sandbox/sandbox-rs/Cargo.lock b/sandbox/sandbox-rs/Cargo.lock index 04882f2bf..0777052a6 100644 --- a/sandbox/sandbox-rs/Cargo.lock +++ b/sandbox/sandbox-rs/Cargo.lock @@ -1066,6 +1066,7 @@ dependencies = [ "hex", "jcp-receipt", "jcp-schema", + "serde", "serde_json", "thiserror 2.0.20", ] -- 2.54.0 (Apple Git-157)