Files
Aurora-Core/tests/load/locustfile.py
T

104 lines
3.5 KiB
Python
Raw Normal View History

2026-09-04 17:34:03 -05:00
"""Load test for Aurora Core 0.1 (section 30/2).
Simulates the read-heavy interactive traffic Core 0.1 targets: searching,
opening movie/series/person detail pages, and paging through search
results. Run against a seeded environment (ingest a reasonable movie/series/
person catalog first — an empty database will pass load tests trivially but
tell you nothing about real query cost).
Usage:
pip install locust
locust -f tests/load/locustfile.py --host http://localhost:8000
Then open http://localhost:8089 and start a run with up to 500 users to
verify the section 2 targets (search p95 < 500ms, other endpoints p95 <
250ms, 5xx rate < 0.1%). Locust reports p50/p95/p99 latency and failure
rate directly; compare them against docs/architecture.md's targets.
See docs/architecture.md for how to seed sample data before running this.
"""
from __future__ import annotations
import random
from locust import HttpUser, between, task
SEARCH_TERMS = [
"matrix",
"star wars",
"alien",
"the office",
"breaking bad",
"keanu reeves",
"christopher nolan",
"marvel",
"batman",
"inception",
]
class AuroraCoreUser(HttpUser):
"""A typical interactive client: search, then drill into a result."""
wait_time = between(1, 3)
def on_start(self) -> None:
self._known_movie_ids: list[str] = []
self._known_series_ids: list[str] = []
self._known_person_ids: list[str] = []
@task(5)
def search(self) -> None:
query = random.choice(SEARCH_TERMS)
limit = random.choice([5, 10, 20])
with self.client.get(
f"/api/v1/search?q={query}&limit={limit}",
name="/api/v1/search",
catch_response=True,
) as response:
if response.status_code != 200:
response.failure(f"search failed: {response.status_code}")
return
body = response.json()
self._known_movie_ids.extend(m["id"] for m in body.get("movies", []))
self._known_series_ids.extend(s["id"] for s in body.get("series", []))
self._known_person_ids.extend(p["id"] for p in body.get("people", []))
# Keep the pools bounded so memory doesn't grow unbounded over a
# long run.
self._known_movie_ids = self._known_movie_ids[-200:]
self._known_series_ids = self._known_series_ids[-200:]
self._known_person_ids = self._known_person_ids[-200:]
@task(4)
def open_movie(self) -> None:
if not self._known_movie_ids:
return
movie_id = random.choice(self._known_movie_ids)
self.client.get(f"/api/v1/movies/{movie_id}", name="/api/v1/movies/[id]")
self.client.get(
f"/api/v1/movies/{movie_id}/credits", name="/api/v1/movies/[id]/credits"
)
@task(2)
def open_series(self) -> None:
if not self._known_series_ids:
return
series_id = random.choice(self._known_series_ids)
self.client.get(f"/api/v1/series/{series_id}", name="/api/v1/series/[id]")
self.client.get(
f"/api/v1/series/{series_id}/seasons", name="/api/v1/series/[id]/seasons"
)
@task(2)
def open_person(self) -> None:
if not self._known_person_ids:
return
person_id = random.choice(self._known_person_ids)
self.client.get(f"/api/v1/people/{person_id}", name="/api/v1/people/[id]")
@task(1)
def health_probe(self) -> None:
self.client.get("/health", name="/health")