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. ClawSweeper review in progressClawSweeper is reviewing this revision. This supersedes any previous blocked status. |
|
Codex review: blocked before merge. Reviewed September 4, 2026, 3:54 PM ET / 19:54 UTC. ClawSweeper reviewWhat this changesThe PR bounds Docker cleanup commands and makes the native evaluator terminate, kill, and bounded-wait reap a cancelled subprocess so hung Docker clients cannot indefinitely stall a trial. Merge readiness⛔ Blocked before merge - 2 items remain This remains a focused, proof-backed repair for native-evaluation teardown; no merged same-repository change is shown to supersede it, and the patch has no concrete correctness finding. Priority: P2 Review scores
Verification
How this fits togetherShellBench’s native evaluator runs benchmark tasks in Docker containers and records trial output. Its Docker commands flow through a shared async subprocess helper, so cancellation handling determines whether a timed-out trial can complete teardown. flowchart LR
A[Benchmark trial] --> B[Docker task environment]
B --> C[Async subprocess helper]
C --> D[Docker CLI command]
D --> E[Trial logs]
C --> F[Cancellation and reaping]
F --> G[Bounded teardown]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Land the shared child-reaping fix while retaining the explicit bounded cleanup budget, so timed-out trials finish rather than leaving Docker client processes behind. Do we have a high-confidence way to reproduce the issue? Yes. The supplied live trace uses the changed production subprocess helper with a real long-running child under a 0.4-second cancellation deadline, and the focused test preserves the same PID-reaping scenario. Is this the best way to solve the issue? Yes. Centralizing cancellation cleanup in the existing subprocess owner covers all three affected Docker cleanup callers without adding a parallel command runner. AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against c1a79f731541. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (7 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