continuous-integration/drone/push Build is failing
- acme-dns.nix: ONE dual-zone SAN cert (ci+*.ci.autonomic.zone AND ci+*.ci.commoninternet.net) via the same acmedns account — storage re-keyed by cc-ci-acme-storage-seed.service; handoff reads the new cert dir. Single secret pair => zero changes to the traefik reconciler. - dashboard/bridge/reports: dual Host rules during the bake window (bridge gets explicit parentheses so && does not shadow the dashboard on the new host). - drone abra app renamed to drone.ci.autonomic.zone (fresh DB supported: DRONE_USER_CREATE re-injects the sops bridge token); runner RPC + bootstrap-drone-oauth.sh follow. - harness: app_domain() issues *.ci.autonomic.zone run domains; RUN_APP_RE / stack-name regexes / docker-prune accept BOTH zones during the bake. Warm stacks deliberately stay on the legacy zone (data-warm volumes; post-bake migration). - URLs in bridge/dashboard defaults + recipe-report.py follow the new names.
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
"""Shared utilities for the real-kernel concurrency suite (imported by the test modules; the
|
|
fixtures in conftest.py wrap these). No flock mocking anywhere — probes use real LOCK_NB."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import fcntl
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
|
from harness import lifecycle # noqa: E402
|
|
|
|
HELPERS = os.path.join(os.path.dirname(__file__), "helpers.py")
|
|
DOMAIN = "test-abc123.ci.autonomic.zone" # matches RUN_APP_RE
|
|
|
|
|
|
class HelperPool:
|
|
"""Spawns helpers.py subprocesses and GUARANTEES their cleanup (incl. recorded grandchild
|
|
pids from `hold-with-child`/`wrapper` markers) — no leaked children in the test VM."""
|
|
|
|
def __init__(self, out_dir: str):
|
|
self.out_dir = out_dir
|
|
self.procs: list[subprocess.Popen] = []
|
|
self.extra_pids: list[int] = []
|
|
self._n = 0
|
|
|
|
def spawn(self, *args: str, env_extra: dict | None = None) -> tuple[subprocess.Popen, str]:
|
|
"""Start `helpers.py <args...>`; returns (proc, marker_file)."""
|
|
self._n += 1
|
|
out = os.path.join(self.out_dir, f"helper-{self._n}.out")
|
|
env = dict(os.environ, CCCI_HELPER_OUT=out, **(env_extra or {}))
|
|
p = subprocess.Popen( # noqa: S603
|
|
[sys.executable, HELPERS, *args],
|
|
env=env,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
self.procs.append(p)
|
|
return p, out
|
|
|
|
def track_pid(self, pid: int) -> None:
|
|
self.extra_pids.append(pid)
|
|
|
|
def cleanup(self) -> None:
|
|
for p in self.procs:
|
|
if p.poll() is None:
|
|
p.kill()
|
|
with contextlib.suppress(subprocess.TimeoutExpired):
|
|
p.wait(timeout=10)
|
|
for pid in self.extra_pids:
|
|
with contextlib.suppress(OSError):
|
|
os.kill(pid, signal.SIGKILL)
|
|
|
|
|
|
def wait_marker(out: str, token: str, timeout: float = 15.0) -> str | None:
|
|
"""Poll a helper's marker file for a line containing `token`; returns the line or None."""
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
try:
|
|
with open(out) as f:
|
|
for line in f:
|
|
if token in line:
|
|
return line.strip()
|
|
except OSError:
|
|
pass
|
|
time.sleep(0.1)
|
|
return None
|
|
|
|
|
|
def lock_state(domain: str) -> str:
|
|
"""'held' | 'free' | 'absent' for the domain's lockfile, probed with a REAL LOCK_NB."""
|
|
path = lifecycle._app_lock_path(domain) # noqa: SLF001
|
|
if not os.path.exists(path):
|
|
return "absent"
|
|
with open(path, "a") as f:
|
|
try:
|
|
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
return "free"
|
|
except BlockingIOError:
|
|
return "held"
|
|
|
|
|
|
def wait_lock_state(domain: str, want: str, timeout: float = 10.0) -> str:
|
|
"""Poll until lock_state(domain) == want (kernel release on process death is fast, but give
|
|
the scheduler room). Returns the final observed state."""
|
|
deadline = time.time() + timeout
|
|
state = lock_state(domain)
|
|
while state != want and time.time() < deadline:
|
|
time.sleep(0.1)
|
|
state = lock_state(domain)
|
|
return state
|
|
|
|
|
|
def pid_alive(pid: int) -> bool:
|
|
return os.path.exists(f"/proc/{pid}")
|
|
|
|
|
|
def wait_pid_gone(pid: int, timeout: float = 15.0) -> bool:
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
if not pid_alive(pid):
|
|
return True
|
|
time.sleep(0.1)
|
|
return False
|