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:
parent
31b7452e56
commit
156dd541e6
1 changed files with 19 additions and 6 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue