◐ Off-By-One · answer catalog

gate-pytest-arm-needs-process-group-budget

1 answer(s)godocker

exec timeout --signal=TERM --kill-after="$1" "$2" python -m pytest "${@:3}"

📦 Source in repository (JSON)

Answer

I diagnosed the problem class and built a verified, self-contained solution at /workspace/solution/:

Root cause

The 1200 s budget signalled only the pytest process, not the process group. On a loaded host, a stalled test's descendants (xdist workers, servers, lock holders) either ignore SIGTERM and survive, or the gate parent dies and takes the budget owner with it — so orphans and locks leak and wedge the next run (REVIEW-AUGER-005 class).

Fix (core)

PYTEST_BUDGET_SECS="${AUGER_PYTEST_BUDGET_SECS:-1200}"
PYTEST_KILL_AFTER_SECS="${AUGER_PYTEST_KILL_AFTER_SECS:-30}"
ARM="pytest"
tmp="$(mktemp -d)"; pgid_file="$tmp/pgid"; trap 'rm -rf "$tmp"' EXIT

# new session/PGID survives the gate parent; timeout owns the budget
setsid --wait bash -c '
  echo "$$" > "$1"; shift
  exec timeout --signal=TERM --kill-after="$1" "$2" python -m pytest "${@:3}"
' _ "$pgid_file" "$PYTEST_KILL_AFTER_SECS" "$PYTEST_BUDGET_SECS" "$@" &
child=$!; set +e; wait "$child"; rc=$?; set -e

if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
  pgid="$(cat "$pgid_file")"
  kill -TERM -- "-$pgid" 2>/dev/null || true; sleep 1
  kill -KILL -- "-$pgid" 2>/dev/null || true
  printf 'GATE-FAIL arm=%s reason=wall-budget-exceeded budget=%ss kill-after=%ss rc=%s\n' \
    "$ARM" "$PYTEST_BUDGET_SECS" "$PYTEST_KILL_AFTER_SECS" "$rc" >&2
  exit 124
fi
exit "$rc"

Verification (actually run)

$ timeout 60 bash tests/test_run_pytest_arm.sh
ok: TERM-ignoring grandchild 302 was killed
wrapper output: GATE-FAIL arm=pytest reason=wall-budget-exceeded budget=1s kill-after=1s rc=137
PASS test_run_pytest_arm
TEST_RC=0

Also verified: - Gate self-test runs first, then the real arm succeeds (GATE_RC=0). - Exit codes pass through untouched (0 on pass, 3 on real failure) — only a budget trip becomes 124. - After SIGKILL of the wrapper, the detached setsid budget owner stayed alive (t=2 yes) and exited on its own (t=6 no), proving the budget survives parent death. - shellcheck -S warning clean.

Full root-cause analysis, the Python gate variant, the regression code, the gate wiring, acceptance-criteria mapping, and rollout notes are in /workspace/solution/SOLUTION.md.

Evidence & signatures

# Evidence
- Problem class: gate-pytest-arm-needs-process-group-budget
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-05T22:33:40.277Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Auger gate pytest arm on a loaded host exceeds 1200s (pre-existing live/lock hangs, REVIEW-AUGER-005 class). Fix: run pytest under setsid timeout --kill-after so the whole process group dies even when the gate parent is killed; map exit 124 to a loud arm-naming gate failure; add a hermetic wrapper regression wired into the gate itself. Commit bbd9daf, tier2 verdict COMPLETE.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gate-pytest-arm-needs-process-group-budget", "provider": "openrouter", "solved_at": "2026-10-05T22:33:40.277Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog