CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 13s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 23s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
Documentation / Validate Documentation (push) Failing after 7s
Documentation / Generate API Documentation (push) Successful in 8s
Documentation / Build Sphinx Documentation (push) Successful in 15s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s
Retrained on the gap-filled DB (592k -> 976k rows) and re-examined the flood backtests, now reproducible via scripts/backtest_render.py (renders the three docs/img charts and gates on a >=12h 2024 first-alert lead — currently failing by design and documented as such). Findings, all documented in FLOOD_FORECASTING.md: the true 2024 crossing was 24 Sep 17:00 (8h earlier than recorded; confirmed against the independent HII sensor), the historical 24h-warning claim was partly a missing-data artifact, and retrained warn classifiers collapse on the filled grid (P.1 24h PR-AUC 0.900 -> 0.288) while regression MAE improves (11.3 -> 10.5 cm). Serving therefore becomes max(classifier, sigmoid(regression)) so alerting is never worse than the regression path; metrics table, head-gating tiers, honest-limits and runbook expectations all updated to the current model (hgb-v1+d2d0e65). Perf, from Locust load testing (scripts/locustfile.py + load_test.py): single-flight lock around /forecast inference (concurrent cache misses previously each ran ~18s inference and starved the shared thread pool; 200-user run after: 105 rps, 0.01% errors), and /measurements/latest + /health moved off the event loop (synchronous DB/network calls in async handlers were stalling every request under load).
94 lines
2.6 KiB
Python
94 lines
2.6 KiB
Python
"""Locust load profile for the Ping River Monitor API + dashboard.
|
|
|
|
Two user types mirror real traffic: dashboard visitors (page + the API calls
|
|
the page makes, polling like the auto-refresh does) and API consumers
|
|
(direct endpoint hits, including heavy history queries).
|
|
|
|
Full stress against a LOCAL instance (never full-stress production — it hosts
|
|
live flood monitoring):
|
|
|
|
# separate shell: python -m uvicorn src.web_api:app --port 8125
|
|
.venv/Scripts/python.exe -m locust -f scripts/locustfile.py \
|
|
--host http://localhost:8125 --headless \
|
|
--users 200 --spawn-rate 20 --run-time 2m \
|
|
--html load-report.html
|
|
|
|
Interactive UI instead: drop --headless and open http://localhost:8089.
|
|
"""
|
|
|
|
import random
|
|
|
|
from locust import FastHttpUser, between, task
|
|
|
|
|
|
class DashboardVisitor(FastHttpUser):
|
|
"""A browser session: initial page load, then periodic refresh polling."""
|
|
|
|
weight = 3
|
|
wait_time = between(2, 6)
|
|
|
|
def on_start(self):
|
|
# What one real page load requests
|
|
self.client.get("/")
|
|
self.client.get("/stations")
|
|
self.client.get("/measurements/latest?limit=500")
|
|
self.client.get("/api/hii/waterlevel/latest")
|
|
self.client.get("/api/hii/rainfall/latest")
|
|
|
|
@task(4)
|
|
def poll_latest(self):
|
|
self.client.get("/measurements/latest?limit=500")
|
|
|
|
@task(2)
|
|
def poll_forecast(self):
|
|
self.client.get("/forecast")
|
|
|
|
@task(2)
|
|
def poll_rain(self):
|
|
self.client.get("/api/hii/rainfall/latest")
|
|
|
|
@task(1)
|
|
def view_history(self):
|
|
station = random.choice(["P.1", "P.67", "P.103", "P.75", "P.20"])
|
|
hours = random.choice([24, 168, 720])
|
|
self.client.get(
|
|
f"/measurements/history/{station}?hours={hours}",
|
|
name="/measurements/history/[station]",
|
|
)
|
|
|
|
@task(1)
|
|
def stats(self):
|
|
self.client.get("/api/stats")
|
|
|
|
|
|
class ApiConsumer(FastHttpUser):
|
|
"""A script/integration hitting the JSON API directly, no think time."""
|
|
|
|
weight = 1
|
|
wait_time = between(0.1, 1)
|
|
|
|
@task(3)
|
|
def latest(self):
|
|
self.client.get("/measurements/latest?limit=100")
|
|
|
|
@task(3)
|
|
def hii_feeds(self):
|
|
self.client.get(random.choice(
|
|
["/api/hii/waterlevel/latest", "/api/hii/rainfall/latest"]
|
|
), name="/api/hii/[feed]/latest")
|
|
|
|
@task(2)
|
|
def forecast(self):
|
|
self.client.get("/forecast")
|
|
|
|
@task(2)
|
|
def heavy_history(self):
|
|
self.client.get(
|
|
"/measurements/history/P.1?hours=8760",
|
|
name="/measurements/history/P.1 [heavy]",
|
|
)
|
|
|
|
@task(1)
|
|
def health(self):
|
|
self.client.get("/health")
|