Spaces:
Running
Running
| """Async load test for the TraceRAG FastAPI endpoint. | |
| Start the API first (uvicorn api:app --port 8000), then: | |
| python scripts/stress_test.py --requests 200 --concurrency 25 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import time | |
| from collections import Counter | |
| import aiohttp | |
| QUERIES = [ | |
| "What was incident INC-4471 about?", | |
| "What is the responsibility of the PaymentService?", | |
| "Who is the tech lead and owning team for PaymentService?", | |
| "What does payment-service depend on?", | |
| "How does the AuthLayer handle login and JWT issuance?", | |
| "Explain the ShopFlow commerce platform architecture.", | |
| "What is related to PR #847?", | |
| "Which component was affected in incident INC-4480?", | |
| ] | |
| async def _one(session, url, query, sem, timeout) -> dict: | |
| async with sem: | |
| start = time.perf_counter() | |
| try: | |
| async with session.post(url, json={"query": query}, timeout=timeout) as resp: | |
| await resp.read() # drain | |
| return {"status": resp.status, "latency": time.perf_counter() - start} | |
| except asyncio.TimeoutError: | |
| return {"status": "timeout", "latency": time.perf_counter() - start} | |
| except aiohttp.ClientError as exc: | |
| return {"status": f"error:{type(exc).__name__}", "latency": time.perf_counter() - start} | |
| async def run(url: str, total: int, concurrency: int, timeout: float) -> None: | |
| sem = asyncio.Semaphore(concurrency) | |
| print(f"Hammering {url}\n {total} requests, concurrency={concurrency}, " | |
| f"timeout={timeout}s\n") | |
| wall_start = time.perf_counter() | |
| async with aiohttp.ClientSession() as session: | |
| tasks = [ | |
| _one(session, url, QUERIES[i % len(QUERIES)], sem, timeout) | |
| for i in range(total) | |
| ] | |
| results = await asyncio.gather(*tasks) | |
| wall = time.perf_counter() - wall_start | |
| statuses = Counter(r["status"] for r in results) | |
| ok = [r["latency"] for r in results if r["status"] == 200] | |
| lat = sorted(r["latency"] for r in results) | |
| def pct(p: float) -> float: | |
| return lat[min(len(lat) - 1, int(len(lat) * p))] if lat else 0.0 | |
| print("=" * 60) | |
| print(f"{'Status / outcome':<28}{'count':>10}") | |
| print("-" * 60) | |
| for status, n in sorted(statuses.items(), key=lambda kv: str(kv[0])): | |
| print(f"{str(status):<28}{n:>10}") | |
| print("-" * 60) | |
| print(f"wall time {wall:>8.2f}s") | |
| print(f"throughput {total / wall:>8.1f} req/s") | |
| print(f"success (200) {len(ok):>8} / {total}") | |
| print(f"latency p50/p90/p99 {pct(.5):.3f} / {pct(.9):.3f} / {pct(.99):.3f} s") | |
| print(f"latency max {(lat[-1] if lat else 0):.3f} s") | |
| print("=" * 60) | |
| if any(str(s).startswith(("error", "timeout")) or s in (500, 502) for s in statuses): | |
| print("\n[!] Hard failures present (5xx/timeout/conn-error): the worker may " | |
| "not be degrading gracefully — check for crashes or DB read-locks.") | |
| if statuses.get(429) or statuses.get(503): | |
| print("\n[ok] Server shed load with 429/503 under pressure (graceful).") | |
| def main(argv: list[str] | None = None) -> int: | |
| p = argparse.ArgumentParser(description="TraceRAG async load test.") | |
| p.add_argument("--url", default="http://127.0.0.1:8000/api/trace") | |
| p.add_argument("--requests", type=int, default=100) | |
| p.add_argument("--concurrency", type=int, default=20) | |
| p.add_argument("--timeout", type=float, default=30.0) | |
| args = p.parse_args(argv) | |
| asyncio.run(run(args.url, args.requests, args.concurrency, args.timeout)) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |