ci: clean up backend test processes on cancel [CI]
This commit is contained in:
1 parent
bf3481ebae
commit
04f116e092
2 files changed
+90
-20
No files matched your search
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run a command in its own process group and clean it up on timeout/cancel."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
GRACE_SECONDS = 10
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("usage: run-with-timeout.py <seconds> <command> [args...]", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
try:
|
||||||
|
timeout_seconds = float(sys.argv[1])
|
||||||
|
except ValueError:
|
||||||
|
print("timeout must be a number of seconds", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
process = subprocess.Popen(sys.argv[2:], start_new_session=True)
|
||||||
|
stop_signal: int | None = None
|
||||||
|
stop_started: float | None = None
|
||||||
|
timed_out = False
|
||||||
|
|
||||||
|
def request_stop(signum: int, _frame: object) -> None:
|
||||||
|
nonlocal stop_signal, stop_started
|
||||||
|
if stop_signal is None:
|
||||||
|
stop_signal = signum
|
||||||
|
stop_started = time.monotonic()
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGTERM)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
||||||
|
signal.signal(signum, request_stop)
|
||||||
|
|
||||||
|
deadline = time.monotonic() + timeout_seconds
|
||||||
|
while True:
|
||||||
|
now = time.monotonic()
|
||||||
|
if stop_signal is None and not timed_out and now >= deadline:
|
||||||
|
timed_out = True
|
||||||
|
stop_started = now
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGTERM)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
elif stop_started is not None and now - stop_started >= GRACE_SECONDS:
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
stop_started = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = process.wait(timeout=0.2)
|
||||||
|
if stop_signal is not None:
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
return 128 + stop_signal
|
||||||
|
if timed_out:
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
return 124
|
||||||
|
return result
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+8
-20
@@ -84,31 +84,19 @@ status=0
|
|||||||
# Each test module can spawn its own testhost. Run the whole command in a new
|
# Each test module can spawn its own testhost. Run the whole command in a new
|
||||||
# session and time out its process group, so orphaned testhosts cannot keep the
|
# session and time out its process group, so orphaned testhosts cannot keep the
|
||||||
# Woodpecker step and the shared cache lock alive.
|
# Woodpecker step and the shared cache lock alive.
|
||||||
if ! command -v setsid >/dev/null 2>&1; then
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
echo "setsid is required to enforce the backend test timeout" >&2
|
echo "python3 is required to enforce the backend test timeout" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
run_with_timeout() {
|
run_with_timeout() {
|
||||||
local seconds="$1" marker="$cache/test-timeout.marker" test_pid watcher_pid result=0
|
local seconds="$1"
|
||||||
shift
|
shift
|
||||||
rm -f "$marker"
|
# A Python supervisor creates the test's session directly and owns its PID.
|
||||||
(
|
# This avoids setsid(1) forking away from the PID tracked by the old shell
|
||||||
cd "$src/backend"
|
# watchdog, which could leave testhosts alive (and holding the cache lock)
|
||||||
setsid "$@" &
|
# after Woodpecker canceled the step.
|
||||||
test_pid=$!
|
(cd "$src/backend" && python3 "$root/ci/run-with-timeout.py" "$seconds" "$@")
|
||||||
setsid bash -c 'sleep "$1"; : > "$2"; kill -TERM -- "-$3" 2>/dev/null || true' _ "$seconds" "$marker" "$test_pid" >/dev/null 2>&1 &
|
|
||||||
watcher_pid=$!
|
|
||||||
wait "$test_pid" || result=$?
|
|
||||||
if [[ -f "$marker" ]]; then
|
|
||||||
kill -KILL -- "-$test_pid" 2>/dev/null || true
|
|
||||||
result=124
|
|
||||||
fi
|
|
||||||
kill -TERM -- "-$watcher_pid" 2>/dev/null || true
|
|
||||||
wait "$watcher_pid" 2>/dev/null || true
|
|
||||||
rm -f "$marker"
|
|
||||||
return "$result"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ignore exit 8 from other assemblies with no matching tests; both suites have a hard wall-clock cap.
|
# Ignore exit 8 from other assemblies with no matching tests; both suites have a hard wall-clock cap.
|
||||||
|
|||||||
Reference in new issue
Block a user