104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
"""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")
|