Padmanav commited on
Commit
4466add
·
1 Parent(s): 0992b80

test(load): add Locust load test suite for /analyze and /analyze/async endpoints

Browse files
Files changed (2) hide show
  1. tests/load/README.md +28 -0
  2. 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}")