"""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")