Watch
0
0
Fork
You've already forked hyperhive
0

hive-priv: count a process being reaped as dead in the build-timeout test

The timeout kill was sound; the test's liveness probe was not. is_dead
read /proc/<pid>/stat and counted only state Z as dead. While init reaps
a zombie, the task shows state X (EXIT_DEAD) for a few microseconds
before its /proc entry goes away. When the poll loop's first probe saw Z
and the final assert's re-probe landed in that window and saw X, the
test failed at once with "grandchild <pid> outlived the timeout". CI run
#6059 shows this: the hive-priv binary finished in 2.00s, so the 5s poll
never ran.

is_dead now counts X as well, so it is monotone (running, Z, X, gone)
and the re-probe can no longer flip it back to alive. A unit test pins
the Z/X/S/R parsing.

Stress loop: the test binary filtered to this test, 2000 runs, 32 in
parallel, with 8 busy-loop CPU hogs. Before: 4/2000 failed, all with the
CI signature (finished in ~2.01s). After: 0/2000.
This commit is contained in:
atlas 2026-10-22 05:12:16 +02:00 • committed by mara
commit 156dd541e6

View file

@ -728,18 +728,31 @@ mod tests {
); );
} }
/// Whether `pid` is gone, or only a zombie awaiting its reaper: either /// Whether `pid` is gone, a zombie (`Z`), or being reaped (`X`): none of
/// way it is no longer running. /// these runs again, and a process passes through them in that order,
/// so once this is true it stays true.
fn is_dead(pid: &str) -> bool { fn is_dead(pid: &str) -> bool {
match std::fs::read_to_string(format!("/proc/{pid}/stat")) { match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
Err(_) => true, Err(_) => true,
// The state letter follows the parenthesised command name. Ok(stat) => stat_is_dead(&stat),
Ok(stat) => stat
.rsplit_once(") ")
.is_some_and(|(_, rest)| rest.starts_with('Z')),
} }
} }
/// The state letter of a `/proc/<pid>/stat` line, which follows the
/// parenthesised command name, is `Z` or `X`.
fn stat_is_dead(stat: &str) -> bool {
stat.rsplit_once(") ")
.is_some_and(|(_, rest)| rest.starts_with(['Z', 'X']))
}
#[test]
fn zombie_and_reaped_states_count_as_dead() {
assert!(stat_is_dead("42 (sleep) Z 1 42 42"));
assert!(stat_is_dead("42 (sleep) X 1 42 42"));
assert!(!stat_is_dead("42 (sleep) S 1 42 42"));
assert!(!stat_is_dead("42 (sleep) R 1 42 42"));
}
/// A child still running at the limit comes back `TimedOut`, and the kill /// A child still running at the limit comes back `TimedOut`, and the kill
/// reaches its whole process group: the backgrounded grandchild dies too, /// reaches its whole process group: the backgrounded grandchild dies too,
/// not just the shell that started it. /// not just the shell that started it.