#!/usr/bin/env python3 """Small dependency-free concurrency gate for the persisted read paths.""" from __future__ import annotations import argparse import json import math import time import urllib.error import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed PATHS = ( "/api/v1/dashboard", "/api/v1/vehicles?page=1&page_size=25", "/api/v1/bookings?page=1&page_size=25&sort=operational", "/api/v1/data-quality/issues?status=open&page=1&page_size=25", "/api/v1/audit?page=1&page_size=25", "/api/v1/system/status", ) def request(url: str, cookie: str, timeout: float) -> tuple[float, int]: started = time.perf_counter() try: with urllib.request.urlopen( urllib.request.Request(url, headers={"Cookie": cookie}), timeout=timeout ) as response: response.read() status = response.status except urllib.error.HTTPError as error: status = error.code return (time.perf_counter() - started) * 1000, status def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--base-url", default="http://localhost:1228") parser.add_argument("--requests", type=int, default=240) parser.add_argument("--concurrency", type=int, default=12) parser.add_argument("--timeout", type=float, default=5.0) parser.add_argument("--max-p95-ms", type=float, default=1500.0) args = parser.parse_args() if args.requests < 1 or args.concurrency < 1: raise SystemExit("requests and concurrency must be positive") login = urllib.request.Request( f"{args.base_url.rstrip('/')}/api/v1/demo/login", data=json.dumps({"role": "operations_manager"}).encode(), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(login, timeout=args.timeout) as response: cookie = response.headers.get("Set-Cookie", "").split(";", 1)[0] if not cookie: raise SystemExit("demo login returned no session cookie") base = args.base_url.rstrip("/") work = [f"{base}{PATHS[index % len(PATHS)]}" for index in range(args.requests)] results: list[tuple[float, int]] = [] with ThreadPoolExecutor(max_workers=args.concurrency) as executor: futures = [executor.submit(request, url, cookie, args.timeout) for url in work] results.extend(future.result() for future in as_completed(futures)) failures = [status for _, status in results if status != 200] durations = sorted(duration for duration, _ in results) p95 = durations[max(0, math.ceil(len(durations) * 0.95) - 1)] print( f"read-only load smoke: requests={len(results)} concurrency={args.concurrency} " f"failures={len(failures)} p95_ms={p95:.1f} max_ms={durations[-1]:.1f}" ) if failures: raise SystemExit(f"read-only load smoke returned non-200 statuses: {sorted(set(failures))}") if p95 > args.max_p95_ms: raise SystemExit(f"p95 {p95:.1f} ms exceeds {args.max_p95_ms:.1f} ms") if __name__ == "__main__": main()