Spaces:
Sleeping
Sleeping
test(load): add Locust load test suite for /analyze and /analyze/async endpoints
Browse files- tests/load/README.md +28 -0
- tests/load/locustfile.py +62 -0
tests/load/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Load Testing
|
| 2 |
+
|
| 3 |
+
Uses [Locust](https://locust.io) to simulate burst traffic against the API.
|
| 4 |
+
|
| 5 |
+
## Run locally
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install locust
|
| 9 |
+
locust -f tests/load/locustfile.py \
|
| 10 |
+
--host=http://localhost:8000 \
|
| 11 |
+
--users=20 \
|
| 12 |
+
--spawn-rate=2 \
|
| 13 |
+
--run-time=60s \
|
| 14 |
+
--headless
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
## Environment variables
|
| 18 |
+
|
| 19 |
+
| Variable | Default | Description |
|
| 20 |
+
|---|---|---|
|
| 21 |
+
| `API_KEY` | `test-key` | API key sent in `X-API-Key` header |
|
| 22 |
+
| `LOAD_TEST_REPO` | `https://github.com/psf/requests` | Repo submitted for analysis |
|
| 23 |
+
|
| 24 |
+
## Expected behaviour under load
|
| 25 |
+
|
| 26 |
+
- `/analyze/async` should handle 20 req/min per key before returning 429
|
| 27 |
+
- `/api/v1/health` p99 should stay under 50ms
|
| 28 |
+
- Worker queue depth should stabilise, not grow unboundedly
|
tests/load/locustfile.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tests/load/locustfile.py
|
| 2 |
+
from locust import HttpUser, task, between
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
API_KEY = os.getenv("API_KEY", "test-key")
|
| 6 |
+
TEST_REPO = os.getenv("LOAD_TEST_REPO", "https://github.com/psf/requests")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class CodeReviewUser(HttpUser):
|
| 10 |
+
wait_time = between(2, 5)
|
| 11 |
+
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
|
| 12 |
+
|
| 13 |
+
@task(3)
|
| 14 |
+
def submit_async_job(self):
|
| 15 |
+
"""High-frequency task: submit to async queue (cheap endpoint)."""
|
| 16 |
+
with self.client.post(
|
| 17 |
+
"/api/v1/analyze/async",
|
| 18 |
+
json={"github_url": TEST_REPO},
|
| 19 |
+
headers=self.headers,
|
| 20 |
+
catch_response=True,
|
| 21 |
+
) as resp:
|
| 22 |
+
if resp.status_code == 200:
|
| 23 |
+
job_id = resp.json().get("job_id")
|
| 24 |
+
if job_id:
|
| 25 |
+
resp.success()
|
| 26 |
+
else:
|
| 27 |
+
resp.failure("No job_id in response")
|
| 28 |
+
elif resp.status_code == 429:
|
| 29 |
+
resp.success() # Rate limit is expected under load
|
| 30 |
+
else:
|
| 31 |
+
resp.failure(f"Unexpected status {resp.status_code}")
|
| 32 |
+
|
| 33 |
+
@task(1)
|
| 34 |
+
def poll_health(self):
|
| 35 |
+
"""Lightweight health check to measure baseline latency."""
|
| 36 |
+
self.client.get("/api/v1/health", headers=self.headers)
|
| 37 |
+
|
| 38 |
+
@task(1)
|
| 39 |
+
def poll_metrics(self):
|
| 40 |
+
"""Check internal metrics endpoint under load."""
|
| 41 |
+
self.client.get("/metrics/prometheus")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class BurstUser(HttpUser):
|
| 45 |
+
"""Simulates burst traffic hitting the sync endpoint directly."""
|
| 46 |
+
wait_time = between(5, 10)
|
| 47 |
+
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
|
| 48 |
+
|
| 49 |
+
@task(1)
|
| 50 |
+
def sync_analyze(self):
|
| 51 |
+
"""Low-frequency task: full sync analyze (expensive, rate-limited)."""
|
| 52 |
+
with self.client.post(
|
| 53 |
+
"/api/v1/analyze",
|
| 54 |
+
json={"github_url": TEST_REPO},
|
| 55 |
+
headers=self.headers,
|
| 56 |
+
catch_response=True,
|
| 57 |
+
timeout=180,
|
| 58 |
+
) as resp:
|
| 59 |
+
if resp.status_code in (200, 429):
|
| 60 |
+
resp.success()
|
| 61 |
+
else:
|
| 62 |
+
resp.failure(f"Unexpected status {resp.status_code}")
|