Files
govoplan/tools/checks/worker-runtime-drill.py
zemion d3713bf2ee
Dependency Audit / dependency-audit (push) Failing after 9s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Failing after 8s
Automate worker runtime delivery drill
2026-08-03 00:20:57 +02:00

398 lines
13 KiB
Python

#!/usr/bin/env python3
"""Exercise GovOPlaN worker delivery guarantees against a real Redis broker."""
from __future__ import annotations
import argparse
from datetime import UTC, datetime
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
from typing import Any
from urllib.parse import urlsplit, urlunsplit
import uuid
TERMINAL_STATES = {"FAILURE", "REVOKED", "SUCCESS"}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Prove Celery publish/consume, retry, warm shutdown, and worker-loss "
"redelivery against an isolated Redis database."
)
)
parser.add_argument(
"--redis-url",
default=os.environ.get("GOVOPLAN_WORKER_DRILL_REDIS_URL", ""),
)
parser.add_argument("--python", default=sys.executable)
parser.add_argument("--timeout-seconds", type=float, default=120.0)
parser.add_argument(
"--visibility-timeout-seconds",
type=int,
default=30,
)
parser.add_argument("--output", type=Path)
return parser
def _redacted_redis_url(value: str) -> str:
parsed = urlsplit(value)
hostname = parsed.hostname or ""
port = f":{parsed.port}" if parsed.port else ""
return urlunsplit((parsed.scheme, f"{hostname}{port}", parsed.path, "", ""))
def _wait_until(predicate, *, timeout_seconds: float, detail: str):
deadline = time.monotonic() + timeout_seconds
last_value: Any = None
while time.monotonic() < deadline:
last_value = predicate()
if last_value:
return last_value
time.sleep(0.2)
raise TimeoutError(f"Timed out waiting for {detail}; last value: {last_value!r}")
def _worker_command(python: str, hostname: str) -> list[str]:
return [
python,
"-m",
"celery",
"-A",
"govoplan_core.celery_app:celery",
"worker",
"--pool",
"solo",
"--queues",
"default",
"--hostname",
hostname,
"--loglevel",
"WARNING",
"--without-mingle",
]
def _start_worker(
*,
python: str,
hostname: str,
environment: dict[str, str],
log_path: Path,
) -> tuple[subprocess.Popen[bytes], Any]:
log = log_path.open("ab", buffering=0)
process = subprocess.Popen(
_worker_command(python, hostname),
env=environment,
stdin=subprocess.DEVNULL,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
)
return process, log
def _stop_worker(
process: subprocess.Popen[bytes] | None,
log: Any,
*,
timeout_seconds: float = 30.0,
) -> None:
if process is not None and process.poll() is None:
process.send_signal(signal.SIGTERM)
try:
process.wait(timeout=timeout_seconds)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=10)
if log is not None:
log.close()
def _wait_for_worker(app, process: subprocess.Popen[bytes], prefix: str, timeout: float) -> str:
def ping() -> str | None:
if process.poll() is not None:
raise RuntimeError(f"Worker exited before readiness with code {process.returncode}")
replies = app.control.ping(timeout=1.0) or []
for reply in replies:
for hostname, payload in reply.items():
if hostname.startswith(prefix) and payload.get("ok") == "pong":
return hostname
return None
return _wait_until(ping, timeout_seconds=timeout, detail=f"worker {prefix}")
def _wait_for_started(result, *, timeout_seconds: float) -> dict[str, Any]:
def started() -> dict[str, Any] | None:
state = result.state
if state in TERMINAL_STATES and state != "SUCCESS":
raise RuntimeError(f"Probe {result.id} became {state}: {result.result!r}")
info = result.info
if state in {"PROGRESS", "STARTED"} and isinstance(info, dict):
return dict(info)
return None
return _wait_until(
started,
timeout_seconds=timeout_seconds,
detail=f"probe {result.id} to start",
)
def _wait_for_result(result, *, timeout_seconds: float) -> dict[str, Any]:
value = result.get(timeout=timeout_seconds, propagate=True, disable_sync_subtasks=False)
if not isinstance(value, dict):
raise RuntimeError(f"Probe {result.id} returned an invalid result: {value!r}")
return dict(value)
def _publish(probe_task, probe_id: str, *, mode: str, delay_seconds: float):
return probe_task.apply_async(
args=(probe_id,),
kwargs={
"mode": mode,
"delay_seconds": delay_seconds,
"track_delivery": True,
},
queue="default",
)
def _assert_probe(value: dict[str, Any], *, probe_id: str) -> None:
if value.get("probe_id") != probe_id:
raise RuntimeError(f"Probe identity mismatch: {value!r}")
def run_drill(args: argparse.Namespace) -> dict[str, Any]:
redis_url = str(args.redis_url or "").strip()
parsed_redis = urlsplit(redis_url)
if parsed_redis.scheme not in {"redis", "rediss"} or not parsed_redis.hostname:
raise ValueError("--redis-url must be an explicit redis:// or rediss:// URL")
if args.visibility_timeout_seconds < 30:
raise ValueError("--visibility-timeout-seconds must be at least 30")
run_id = uuid.uuid4().hex
started_at = datetime.now(UTC)
with tempfile.TemporaryDirectory(prefix="govoplan-worker-drill-") as directory:
root = Path(directory)
database_url = f"sqlite:///{root / 'runtime.db'}"
environment = dict(os.environ)
environment.update(
{
"REDIS_URL": redis_url,
"CELERY_ENABLED": "true",
"CELERY_QUEUES": "default",
"CELERY_VISIBILITY_TIMEOUT_SECONDS": str(
args.visibility_timeout_seconds
),
"DATABASE_URL": database_url,
"ENABLED_MODULES": "access",
"APP_ENV": "development",
"GOVOPLAN_EXPECTED_WORKER_REPLICAS": "1",
"GOVOPLAN_WORKER_POOL": "acceptance-drill",
}
)
os.environ.update(environment)
from govoplan_core.db.migrations import migrate_database
migrate_database(
database_url=database_url,
enabled_modules=("access",),
)
from govoplan_core.celery_app import celery, worker_acceptance_probe
celery.backend.client.ping()
celery.control.purge()
worker: subprocess.Popen[bytes] | None = None
worker_log: Any = None
evidence: dict[str, Any] = {
"schema_version": "1.0",
"run_id": run_id,
"started_at": started_at.isoformat(),
"redis": _redacted_redis_url(redis_url),
"visibility_timeout_seconds": args.visibility_timeout_seconds,
"checks": [],
}
try:
prefix = f"govoplan-drill-{run_id[:8]}-a@"
worker, worker_log = _start_worker(
python=args.python,
hostname=prefix + "%h",
environment=environment,
log_path=root / "worker-a.log",
)
hostname = _wait_for_worker(
celery,
worker,
prefix,
args.timeout_seconds,
)
evidence["checks"].append(
{"id": "worker_startup", "state": "passed", "worker": hostname}
)
probe_id = f"{run_id}-publish"
result = _publish(
worker_acceptance_probe,
probe_id,
mode="complete",
delay_seconds=0,
)
value = _wait_for_result(result, timeout_seconds=args.timeout_seconds)
_assert_probe(value, probe_id=probe_id)
if value.get("delivery_count") != 1:
raise RuntimeError(f"Publish probe was not delivered exactly once: {value!r}")
evidence["checks"].append(
{
"id": "publish_consume",
"state": "passed",
"task_id": result.id,
"delivery_count": value["delivery_count"],
}
)
probe_id = f"{run_id}-retry"
result = _publish(
worker_acceptance_probe,
probe_id,
mode="retry_once",
delay_seconds=0,
)
value = _wait_for_result(result, timeout_seconds=args.timeout_seconds)
_assert_probe(value, probe_id=probe_id)
if value.get("retries") != 1 or value.get("delivery_count") != 2:
raise RuntimeError(f"Retry probe did not execute twice: {value!r}")
evidence["checks"].append(
{
"id": "application_retry",
"state": "passed",
"task_id": result.id,
"delivery_count": value["delivery_count"],
}
)
probe_id = f"{run_id}-warm"
result = _publish(
worker_acceptance_probe,
probe_id,
mode="complete",
delay_seconds=2,
)
_wait_for_started(result, timeout_seconds=args.timeout_seconds)
worker.send_signal(signal.SIGTERM)
value = _wait_for_result(result, timeout_seconds=args.timeout_seconds)
_assert_probe(value, probe_id=probe_id)
worker.wait(timeout=args.timeout_seconds)
if worker.returncode != 0 or value.get("delivery_count") != 1:
raise RuntimeError(
f"Warm worker shutdown did not finish in-flight work: {value!r}"
)
worker_log.close()
worker = None
worker_log = None
evidence["checks"].append(
{
"id": "graceful_shutdown",
"state": "passed",
"task_id": result.id,
"delivery_count": value["delivery_count"],
}
)
prefix = f"govoplan-drill-{run_id[:8]}-b@"
worker, worker_log = _start_worker(
python=args.python,
hostname=prefix + "%h",
environment=environment,
log_path=root / "worker-b.log",
)
_wait_for_worker(celery, worker, prefix, args.timeout_seconds)
probe_id = f"{run_id}-loss"
result = _publish(
worker_acceptance_probe,
probe_id,
mode="complete",
delay_seconds=min(120.0, args.visibility_timeout_seconds + 20.0),
)
first_started = _wait_for_started(
result,
timeout_seconds=args.timeout_seconds,
)
if first_started.get("delivery_count") != 1:
raise RuntimeError(f"Worker-loss probe did not start once: {first_started!r}")
worker.kill()
worker.wait(timeout=10)
worker_log.close()
worker = None
worker_log = None
prefix = f"govoplan-drill-{run_id[:8]}-c@"
worker, worker_log = _start_worker(
python=args.python,
hostname=prefix + "%h",
environment=environment,
log_path=root / "worker-c.log",
)
_wait_for_worker(celery, worker, prefix, args.timeout_seconds)
value = _wait_for_result(
result,
timeout_seconds=args.timeout_seconds
+ args.visibility_timeout_seconds
+ 30,
)
_assert_probe(value, probe_id=probe_id)
if value.get("delivery_count") != 2:
raise RuntimeError(f"Worker-loss probe was not redelivered: {value!r}")
evidence["checks"].append(
{
"id": "worker_loss_redelivery",
"state": "passed",
"task_id": result.id,
"delivery_count": value["delivery_count"],
"broker_redelivered": bool(value.get("redelivered")),
}
)
except BaseException as exc:
evidence["error"] = f"{type(exc).__name__}: {exc}"
evidence["result"] = {"state": "failed"}
raise
finally:
_stop_worker(worker, worker_log)
celery.control.purge()
evidence["completed_at"] = datetime.now(UTC).isoformat()
evidence["result"] = {"state": "passed"}
return evidence
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
evidence = run_drill(args)
except (OSError, RuntimeError, TimeoutError, ValueError) as exc:
print(f"worker runtime drill failed: {exc}", file=sys.stderr)
return 1
payload = json.dumps(evidence, indent=2, sort_keys=True) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
temporary.write_text(payload, encoding="utf-8")
temporary.replace(args.output)
print(f"Worker runtime evidence written to {args.output}")
else:
print(payload, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())