fix(eval): time out hung docker kill and compose down - #66
Conversation
After an agent timeout, docker kill, compose down, and docker rm ran through unbounded run_process. A hung Docker CLI never finished the trial. Wrap those cleanup calls in asyncio.timeout(30). Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs maintainer review before merge. Reviewed August 20, 2026, 3:34 AM ET / 07:34 UTC. ClawSweeper reviewWhat this changesThe PR adds deadlines to Docker cleanup commands and makes the native evaluator terminate, kill, and reap cancelled Docker CLI subprocesses. Regression provenancePossible regression — suspected (reviewed change). No predecessor PR is attributed. Merge readinessKeep this PR open for normal merge review: the revised helper bounds both graceful and forced-reap waits, and the focused tests plus real-child transcript support the intended recovery behavior. No blocking correctness defect was found in the current head. Priority: P2 Review scores
Verification
Live VerificationCommand: Result: FAIL (failed) — execution before step 1 Assertions:
How this fits togetherShellBench’s native evaluator launches Docker commands for benchmark trials and writes their output to trial logs. When a trial or teardown times out, the evaluator must clean up the Docker CLI child before returning control to the trial lifecycle. flowchart LR
A[Native evaluation trial] --> B[Docker CLI subprocess]
B --> C[Trial and environment logs]
A --> D[Timeout or cancellation]
D --> E[Terminate and reap child]
E --> F[Docker cleanup command]
F --> G[Trial lifecycle returns]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Land the centralized subprocess-reaping behavior with the existing focused tests, retaining bounded cleanup so a wedged Docker client cannot indefinitely stall a benchmark trial. Do we have a high-confidence way to reproduce the issue? Yes. The PR supplies a concrete real-child timeout transcript, and current source plus focused tests provide a high-confidence path that exercises the same cancellation and reaping helper. Is this the best way to solve the issue? Yes. Centralizing reaping in the subprocess helper covers the Docker cleanup calls without creating a parallel cleanup mechanism. AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against 884dd1bb5511. LabelsLabel changes:
Label justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (2 earlier review cycles)
|
Enclosing asyncio.timeout only cancelled the await. run_process now terminates and waits for the subprocess so a hung docker kill/compose down/rm does not leak. Drop the release-owned changelog hunk. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
@clawsweeper re-review
Done on |
|
🦞🧹 I asked ClawSweeper to review this item again. |
After terminate times out, wait() after kill had no deadline. A child stuck in uninterruptible I/O could still pin the 30s cleanup timeout. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
@clawsweeper re-review
Done on |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
What Problem This Solves
Native eval already bounds
docker execwithasyncio.timeout. After thattimeout fires, cleanup still called
run_process(["docker", "kill", ...])with no deadline.
stop()did the same fordocker compose downanddocker rm -f.An enclosing timeout only cancelled the await.
run_processhad alreadystarted the Docker CLI child and never terminated it, so a hung
dockerprocess leaked for every affected trial.
Evidence
Live
pythonon this branch importedrun_processand spawned a real30-second child (
python -csleep). A 0.4s deadline cancelled the awaitand the helper terminated the child.
The same helper is used for
docker kill,docker compose down, anddocker rm -f.Real behavior proof
Behavior or issue addressed: Hung Docker CLI cleanup after an agent timeout leaked the child.
run_processnow terminates and reaps the subprocess when the enclosing deadline expires.Real environment tested: macOS, Python 3.14, branch
fix/docker-cleanup-timeoutat/tmp/shellbench-66.Exact steps or command run after this patch:
Evidence after fix: terminal output from the live command:
Observed result after fix: Control returns in 0.40s. The child PID is gone. After SIGKILL,
wait()is also bounded (2s). A child stuck in uninterruptible I/O cannot pin the 30s cleanup deadline.What was not tested: A real dockerd hang on this machine. The live command uses a real long-lived child in place of a stuck Docker CLI.
What does this PR do?
Own the Docker CLI child inside
run_process. On cancel or timeout,terminate, then kill, and bound both
wait()calls so a stuck childcannot pin cleanup. Keep the 30s deadline around
docker kill,compose down, anddocker rm -f.Why?
Introduced in #42
(
69f75c6629c4,2026-07-29). Related wait hardening: #19.
Related 30s bound: #8.
Claw review on
c1a5352asked to terminate and reap the timed-outclient. Review on
636c2d4asked to bound the wait after SIGKILL.Changes
_reap_processonTimeoutError/CancelledErrorinrun_processTests
python3 -m pytest -q tests/test_native_eval_runtime.pypasses locallypython3 -m ruff check/ruff format --checkon the changed files