File size: 3,681 Bytes
db284b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
"""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())