Lab CI flake: a hung in-scenario command hangs the whole gate silently for 6 min (no exec timeout + all-or-nothing TAP) #88
Labels
No labels
bug
deploy
documentation
duplicate
enhancement
good first issue
help wanted
invalid
pull-request
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
femboy/blackwall#88
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
Intermittently, one lab gate in the
labCI job fails with zero stdout and the messageThe job running on runner ... has exceeded the maximum execution time of 6 minutes(the per-step timeout). It has hit different gates on different runs (deception in the C1c Part-A run,flow/flow-sflow on the main post-#83 run and on #87). It always passes on re-run.checkis unaffected; onlylabflakes.Root cause (confirmed in code)
Two independent harness defects combine to make a single hung child command an invisible 6-minute hang:
No per-command execution timeout.
netns::run(crates/blackwall-lab/src/exec/netns.rs:9) executes every one-shot command viaCommand::output(), which blocks until the child exits with no upper bound. Asserts funnel through it:assert_cmd(exec/proc.rs:56) runssh -c "<cmd>", and several gates usecmd="cargo test --offline -p <crate> --test <name> …"as the assert.wait_until's deadline (exec/proc.rs:25) is only checked between probe iterations — a single blocking.output()call is never interrupted. So one command that never exits hangs the wholelab testuntil the CI step timeout (6 min) SIGKILLs the process.All-or-nothing TAP output.
report.rs(:92) accumulates the entire TAP document into aStringand prints it only after the run completes. A hang mid-run therefore emits nothing — noTAP version 13, no per-step lines — which is why the failure is a silent 6-minute wall with no diagnostic.Why it's intermittent
The command that hangs is almost certainly
cargo test --offline, blocking on the shared Cargo build-directory locktarget/debug/.cargo-lock. Cargo printsBlocking waiting for file lock on build directoryto stderr (never stdout) and waits indefinitely when another cargo process holds the lock. Gates run sequentially, but several gates spawn a long-livedrundaemon that is itself acargo testprocess holding that lock; if teardown races (daemon not fully reaped before the next gate's assert cargo starts), the next gate's cargo blocks forever. This matches every observed property: transient, zero stdout, and hitting whichever gate happens to run next.Fix plan
netns::runfrom.output()tospawn()+ polltry_wait()against a deadline (default ~60s, well under the 6-min CI step timeout); on timeout, kill the child's process group (setprocess_group(0)at spawn,kill(-pgid, SIGKILL)) and return aLabError::Exec("command timed out …")carrying any partial stderr. A hung command then fails its step fast with a clear reason instead of hanging the job. Add a test:sh -c 'sleep 30'under a 1s bound returns a timeout error in ~1s.TAP version 13+ the plan line up front and each step's result as it completes (line-buffered), so a future hang shows exactly which step stalled and how far the run got. Keep the JUnit artifact unchanged.cargoinside scenarios. Thelabjob already pre-builds all test binaries in its build step. Have gates invoke the pre-built test binary directly (locate it once viacargo test --no-run --message-format=json) so no in-scenario cargo ever contends on.cargo-lock. This removes the lock-contention trigger entirely.Primary + diagnostics are a small, self-contained lab-hardening increment; the root-cause item is a larger follow-on.
The flow-live gate is worse than an intermittent flake — it wedges past GitHub's step
timeout-minutes. Root cause: theflow-sflow-livescenario spawnshsflowd(an external daemon), which inherits the CI step's redirected stdout (exec … >lab-gate.log); the open pipe keeps the step's process group alive even aftersudo timeoutkillslab test, so GitHub's step-timeout kill doesn't reap it and the step never terminates. That hung the wholelabjob to its job cap on 3+ consecutivemainruns, starving every gate after it (deception-syncookie, -v6, xdp never ran).Disabled the gate in CI (
if: false) in the timeout PR so the rest of the suite runs; the scenario still works locally. Proper fix for this issue: ensure the lab reaps daemons likehsflowdinto the scenario's process group (the deceptionrundaemons already teardown by pgid — extend that to hsflowd), and/or close/redirect the daemon's inherited stdout so it can't hold the step open. Then re-enable the CI gate.Re-enabling flow-live under the #137 teardown fix confirmed its wedge is a separate, in-step mechanism (not the cross-gate orphan residue #137 fixed): with #137's teardown hardening in place, flow-live STILL wedged in CI (~25min on a 6min step). So hsflowd holds the CI step's stdout pipe open during the step, past GitHub's
timeout-minuteskill, so the step never finalizes — independent of teardown. Passes locally (no wedge there).Dedicated fix needed: guarantee hsflowd cannot inherit/hold the runner's stdout/stderr pipe — e.g. spawn it with
setsid+ all fds redirected to files//dev/nulland</dev/null, and confirm no child of thelab testprocess retains fd 3/4 (theexec 3>&1 4>&2saved runner pipe) . Kept quarantined until then. The other three #137 gates (deception-resilience, flowspec, flowspec-auto) are re-enabled and being verified in CI now.