feat: --max-time, so a stuck build ends itself and says why - #647
feat: --max-time, so a stuck build ends itself and says why#647oyvindberg wants to merge 1 commit into
Conversation
3710413 to
639bd70
Compare
|
Split: the This PR is now Stating the case against it honestly: it rests on a hypothesis we could not confirm — that during the hang the client is alive and merely stuck. 13 consecutive Windows re-runs failed to reproduce, so the mechanism is unproven in production and The case for it does not depend on the hang: a wall-clock bound on a build is a reasonable thing to want for CI, agents and scripts. Decide it on that. What is well-evidenced either way: no false fires in 13 Windows runs plus |
c765557 to
633f1bc
Compare
Three Windows CI hangs produced no diagnostics at all. The reason turned out
not to be a missing timeout: on the fourth occurrence the 1200s `run-bounded.sh`
bound, the step's `timeout-minutes: 25` and the job's 45 were all in place, and
all three failed to end the step (17:20:17 -> job destroyed 18:07:45). The log
blob was never uploaded — `BlobNotFound` — and every `if: always()` telemetry
step shows `null`, because they are steps, and the hung step never yields the
runner.
A bound that kills from outside cannot fix that. So bleep now bounds itself:
bleep test --max-time 15m
On expiry it cancels the build, writes a thread dump of the client AND the
compile server, and exits non-zero. The step then ends normally and the
telemetry steps run — the diagnostics travel the path that already exists,
rather than needing a new one.
Where it deliberately stops: it does not kill the compile server. The client
exiting is already enough to end a CI step, and killing a daemon that serves
other workspaces is collateral nobody asked for.
`timeoutAndForget`, not `timeout`. Measured, not reasoned: `timeout` cancels the
fiber and then waits for its finalizers, so against an uncancellable hang it does
not bound anything — swapping this one call makes MaxTimeTest's uncancellable
case run to completion and return SUCCESS past its deadline, rather than merely
being late.
Two bugs in ChildProcessDiagnostics that made the dump useless, both silent:
- `findJstack` only looked under `java.home`, and only for `jstack`. The client
is a native image with no JDK of its own, so it could never dump a child at
all; and with no `.exe` candidate this found nothing on Windows even in the
server — the platform where the hangs happen.
- `dumpAll` walked only `descendants()`. A shared compile server is nobody's
descendant, so the dump omitted precisely the process doing the stuck work.
Verified fixed against a daemon from an earlier run: client pid 73467 dumped
server pid 73300, which is not its descendant.
Off unless asked for. There is no duration that is right for both a laptop and a
cold CI runner, which is also why it is a per-invocation flag and not a setting
on the shared server, where whichever client spawned the daemon would impose its
choice on everyone else. `--max-time 30` is rejected rather than guessed.
CI sets it just UNDER the bound already enforced (19m against run-bounded's 20m),
so it can only convert an existing kill into a clean exit — it cannot fail a run
that passes today. `run-bounded.sh` stays as the outer backstop for the case this
structurally cannot cover: the client being unable to act at all.
MaxTimeTest is the in-process analogue of the `bound-check` job, for the reason
that job exists — every rung is asserted against a program that really hangs.
Also regenerates the CLI docs, which were stale for `max-cached-workspaces` from
an earlier merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
639bd70 to
7041e1d
Compare
Why
Three Windows CI hangs produced no diagnostics. The cause turned out not to be a missing timeout — on the fourth occurrence the 1200s
run-bounded.shbound, the step'stimeout-minutes: 25, and the job's45were all in place, and all three failed to end the step:GitHub waits for the step's output pipes, not for the process. Raising caps cannot fix that, which is why three of them didn't. And the
if: always()telemetry steps are steps — the hung step never yields the runner, so they never run.What
bleep bounds itself instead:
bleep test --max-time 15mOn expiry it cancels the build, writes a thread dump of the client and the compile server, and exits non-zero. The step then ends normally and the telemetry steps run — diagnostics travel the path that already exists.
Where it deliberately stops: it does not kill the compile server. The client exiting is already enough to end a CI step, and killing a daemon serving other workspaces is collateral nobody asked for.
timeoutAndForget, nottimeoutMeasured, not reasoned.
timeoutcancels the fiber then waits for its finalizers, so against an uncancellable hang it does not bound anything. Swapping this one call makesMaxTimeTest's uncancellable case run to completion and return SUCCESS past its deadline — not merely late.Two silent bugs in
ChildProcessDiagnosticsBoth made the dump useless exactly where it was needed:
findJstacklooked only underjava.home, and only forjstack. The client is a native image with no JDK of its own, so it could never dump a child at all — and with no.execandidate it found nothing on Windows even in the server, the platform where the hangs happen.dumpAllwalked onlydescendants(). A shared compile server is nobody's descendant, so the dump omitted precisely the process doing the stuck work. Verified fixed against a daemon from an earlier run: client pid 73467 dumped server pid 73300, which is not its descendant.Risk
Off unless asked for — no duration is right for both a laptop and a cold CI runner. That is also why it is per-invocation rather than a setting on the shared server, where whichever client spawned the daemon would impose its choice on everyone else.
--max-time 30is rejected rather than guessed.CI sets it just under the bound already enforced (19m vs
run-bounded.sh's 20m), so it can only convert an existing kill into a clean exit — it cannot fail a run that passes today.run-bounded.shstays as the outer backstop for what this structurally cannot cover: the client unable to act at all.Tests
MaxTimeTestis the in-process analogue of thebound-checkjob, for the reason that job exists — every rung is asserted against a program that really hangs, including that cancellation reaches the program rather than merely abandoning it, and that a failing dump reports both problems instead of replacing one with the other.Verified end-to-end against a real compile: bound fires, message names the duration, 17KB dump written containing 53 server threads via jstack.
Also regenerates the CLI docs, which were stale for
max-cached-workspacesfrom an earlier merge.🤖 Generated with Claude Code