This commit is contained in:
lda committed 2026-03-01 19:29:47 +07:00
1 parent 4201a3cbb1
commit 0cd543a21a
10 files changed
+1457 -256

No files matched your search

+265
View File
@@ -0,0 +1,265 @@
"""Stress-test live control endpoints for the smoking detection server.
Usage example:
python scripts/live_stress_test.py --cycles 20 --request-timeout 8 --sleep-ms 120
"""
from __future__ import annotations
import argparse
import json
import time
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
@dataclass
class StepResult:
name: str
ok: bool
duration_ms: int
status_code: int | None
body: str
def _call(
base_url: str,
name: str,
method: str,
path: str,
timeout_seconds: float,
form: dict[str, Any] | None = None,
) -> StepResult:
url = f"{base_url}{path}"
data = None
headers: dict[str, str] = {}
if form is not None:
data = urlencode(form).encode("utf-8")
headers["Content-Type"] = "application/x-www-form-urlencoded"
request = Request(url=url, data=data, method=method, headers=headers)
start = time.perf_counter()
try:
with urlopen(request, timeout=timeout_seconds) as response:
body = response.read().decode("utf-8", errors="replace")
status = getattr(response, "status", None)
duration_ms = int((time.perf_counter() - start) * 1000)
return StepResult(
name=name, ok=True, duration_ms=duration_ms, status_code=status, body=body
)
except HTTPError as err:
duration_ms = int((time.perf_counter() - start) * 1000)
try:
body = err.read().decode("utf-8", errors="replace")
except Exception:
body = str(err)
return StepResult(
name=name,
ok=False,
duration_ms=duration_ms,
status_code=err.code,
body=body,
)
except URLError as err:
duration_ms = int((time.perf_counter() - start) * 1000)
return StepResult(
name=name,
ok=False,
duration_ms=duration_ms,
status_code=None,
body=str(err.reason),
)
except Exception as err:
duration_ms = int((time.perf_counter() - start) * 1000)
return StepResult(
name=name,
ok=False,
duration_ms=duration_ms,
status_code=None,
body=str(err),
)
def _summarize(results: list[StepResult]) -> dict[str, Any]:
ok_results = [r for r in results if r.ok]
err_results = [r for r in results if not r.ok]
durations = [r.duration_ms for r in ok_results]
return {
"total": len(results),
"ok": len(ok_results),
"err": len(err_results),
"avg_ms": round(sum(durations) / len(durations), 1) if durations else 0,
"max_ms": max(durations) if durations else 0,
"over_500ms": sum(1 for d in durations if d > 500),
"over_1000ms": sum(1 for d in durations if d > 1000),
}
def run_stress(args: argparse.Namespace) -> tuple[list[StepResult], dict[str, Any]]:
results: list[StepResult] = []
def add(result: StepResult) -> None:
results.append(result)
if args.verbose or not result.ok:
status = "OK" if result.ok else "ERR"
print(
f"{status}|{result.name}|{result.duration_ms}ms|"
f"status={result.status_code}|{result.body[:180]}"
)
add(
_call(
args.base_url, "warmup-status", "GET", "/live/status", args.request_timeout
)
)
add(
_call(
args.base_url,
"warmup-start",
"POST",
"/live/start",
args.request_timeout,
{"source": args.source_b},
)
)
for cycle in range(1, args.cycles + 1):
add(
_call(
args.base_url,
f"cycle{cycle}-status-before",
"GET",
"/live/status",
args.request_timeout,
)
)
src = args.source_b if cycle % 2 == 0 else args.source_a
add(
_call(
args.base_url,
f"cycle{cycle}-switch-{src}",
"POST",
"/live/switch_source",
args.request_timeout,
{"source": src},
)
)
if args.pause_every > 0 and cycle % args.pause_every == 0:
add(
_call(
args.base_url,
f"cycle{cycle}-pause",
"POST",
"/live/pause",
args.request_timeout,
)
)
time.sleep(args.pause_sleep_ms / 1000.0)
add(
_call(
args.base_url,
f"cycle{cycle}-resume",
"POST",
"/live/resume",
args.request_timeout,
)
)
add(
_call(
args.base_url,
f"cycle{cycle}-status-after",
"GET",
"/live/status",
args.request_timeout,
)
)
time.sleep(args.sleep_ms / 1000.0)
add(
_call(
args.base_url, "final-status", "GET", "/live/status", args.request_timeout
)
)
summary = _summarize(results)
return results, summary
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Stress test /live/* endpoints")
parser.add_argument("--base-url", default="http://127.0.0.1:8000")
parser.add_argument("--cycles", type=int, default=20)
parser.add_argument("--request-timeout", type=float, default=8.0)
parser.add_argument("--sleep-ms", type=int, default=120)
parser.add_argument("--pause-every", type=int, default=3)
parser.add_argument("--pause-sleep-ms", type=int, default=120)
parser.add_argument("--source-a", default="0")
parser.add_argument("--source-b", default="2")
parser.add_argument("--out", default="reports/live_stress_latest.json")
parser.add_argument("--verbose", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
started_at = datetime.now().isoformat(timespec="seconds")
results, summary = run_stress(args)
report = {
"started_at": started_at,
"base_url": args.base_url,
"config": {
"cycles": args.cycles,
"request_timeout": args.request_timeout,
"sleep_ms": args.sleep_ms,
"pause_every": args.pause_every,
"pause_sleep_ms": args.pause_sleep_ms,
"source_a": args.source_a,
"source_b": args.source_b,
},
"summary": summary,
"errors": [asdict(r) for r in results if not r.ok],
"tail": [asdict(r) for r in results[-10:]],
"all_results": [asdict(r) for r in results],
}
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(
"SUMMARY|"
f"total={summary['total']}|ok={summary['ok']}|err={summary['err']}|"
f"avg_ms={summary['avg_ms']}|max_ms={summary['max_ms']}|"
f"over_500ms={summary['over_500ms']}|over_1000ms={summary['over_1000ms']}"
)
print(f"REPORT|{out_path.as_posix()}")
if summary["err"] > 0:
print("ERRORS_BEGIN")
for err in report["errors"][:8]:
print(
f"ERR|{err['name']}|{err['duration_ms']}ms|"
f"status={err['status_code']}|{err['body'][:180]}"
)
print("ERRORS_END")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())