test: stress_http переписан на реальный VM-путь (PUT на ВМ → /api/upload_refs)
This commit is contained in:
@@ -38,5 +38,4 @@
|
|||||||
|
|
||||||
## Хвост
|
## Хвост
|
||||||
|
|
||||||
`tests/load/stress_http.py` всё ещё бьёт по удалённому `/upload` — нужен
|
`tests/load/stress_http.py` — переписан на реальный путь (`PUT` на ВМ → `POST /api/upload_refs`).
|
||||||
переписывание на `/api/upload_refs` (PUT на ВМ → refs).
|
|
||||||
|
|||||||
@@ -55,6 +55,6 @@
|
|||||||
|
|
||||||
## Хвосты / открытые вопросы
|
## Хвосты / открытые вопросы
|
||||||
|
|
||||||
1. `tests/load/stress_http.py` бьёт по удалённому `/upload` — переписать на `/api/upload_refs`.
|
1. ~~`tests/load/stress_http.py` бьёт по удалённому `/upload`~~ — переписан на VM-путь (`PUT` ВМ → `/api/upload_refs`).
|
||||||
2. Прод-риск `database is locked` при конкурентных сверках — отложен (нет мультитенантности).
|
2. Прод-риск `database is locked` при конкурентных сверках — отложен (нет мультитенантности).
|
||||||
3. E2E с реальным LLM (`gpt-oss-120b`) на проде — не проверялся автоматически (тесты на Fake LLM).
|
3. E2E с реальным LLM (`gpt-oss-120b`) на проде — не проверялся автоматически (тесты на Fake LLM).
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ pytest -m "not load" -q # быстрые юнит-тесты без на
|
|||||||
| `load/test_load_pipeline.py` | массовый `run_pipeline`: N контрактов × M допников (Fake LLM) — проверка, что `full_replace` не дублирует строки в масштабе |
|
| `load/test_load_pipeline.py` | массовый `run_pipeline`: N контрактов × M допников (Fake LLM) — проверка, что `full_replace` не дублирует строки в масштабе |
|
||||||
| `load/test_load_apply_ops.py` | массовые `apply_ops`: N ADD → N UPDATE → N DELETE, целостность `spec_events` |
|
| `load/test_load_apply_ops.py` | массовые `apply_ops`: N ADD → N UPDATE → N DELETE, целостность `spec_events` |
|
||||||
| `load/test_load_concurrency.py` | конкурентная запись в SQLite (WAL + `busy_timeout`), проверка отсутствия потери данных |
|
| `load/test_load_concurrency.py` | конкурентная запись в SQLite (WAL + `busy_timeout`), проверка отсутствия потери данных |
|
||||||
| `load/stress_http.py` | standalone HTTP-стресс (`/health`; загрузка `/upload` удалена — реальный путь `/api/upload_refs`) |
|
| `load/stress_http.py` | standalone HTTP-стресс: `PUT` на ВМ → `POST /api/upload_refs` (реальный VM-путь) + `/health` |
|
||||||
|
|
||||||
Масштаб через переменные окружения (дефолты — большие):
|
Масштаб через переменные окружения (дефолты — большие):
|
||||||
|
|
||||||
|
|||||||
+31
-12
@@ -1,19 +1,23 @@
|
|||||||
"""HTTP-стресс против contracts-flask (standalone, не pytest).
|
"""HTTP-стресс против contracts-flask (standalone, не pytest).
|
||||||
|
|
||||||
Создаёт параллельную нагрузку на /upload большими файлами + /health,
|
Реальный путь загрузки (VM-буфер):
|
||||||
замеряет RPS, ошибки, таймауты. Цель — «разогреть» сервис и выявить
|
1) PUT файла на ВМ WebDAV (мимо шлюза кластера ~64KB);
|
||||||
|
2) POST /api/upload_refs — бэк сам тянет файл с ВМ (egress).
|
||||||
|
|
||||||
|
Замеряет RPS, ошибки, таймауты. Цель — «разогреть» сервис и выявить
|
||||||
деградацию/обрывы (шлюз, OOM, SQLite).
|
деградацию/обрывы (шлюз, OOM, SQLite).
|
||||||
|
|
||||||
Запуск:
|
Запуск (локально):
|
||||||
python tests/load/stress_http.py --url http://127.0.0.1:5000 \
|
python tests/load/stress_http.py --url http://127.0.0.1:5000 --n 1000 --threads 32 --size 100000
|
||||||
--n 1000 --threads 32 --size 100000
|
|
||||||
|
|
||||||
Пример против прода (осторожно):
|
Пример против прода (осторожно):
|
||||||
python tests/load/stress_http.py --url https://contractor.pythonk8s.dev.nubes.ru \
|
python tests/load/stress_http.py --url https://contractor.pythonk8s.dev.nubes.ru \
|
||||||
--n 200 --threads 16 --size 500000
|
--file "/mnt/y/MY/Nubes/примеры_договоров_для_ИИ/спецификация-XXX001-03700.docx" \
|
||||||
|
--n 200 --threads 16
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -22,6 +26,7 @@ import httpx
|
|||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(description="HTTP-стресс contracts-flask")
|
ap = argparse.ArgumentParser(description="HTTP-стресс contracts-flask")
|
||||||
ap.add_argument("--url", default="http://127.0.0.1:5000")
|
ap.add_argument("--url", default="http://127.0.0.1:5000")
|
||||||
|
ap.add_argument("--vm-url", default="https://contracts.kube5s.ru/contracts-upload/", help="ВМ WebDAV-буфер (PUT файла)")
|
||||||
ap.add_argument("--n", type=int, default=1000, help="число запросов")
|
ap.add_argument("--n", type=int, default=1000, help="число запросов")
|
||||||
ap.add_argument("--threads", type=int, default=32, help="параллельных потоков")
|
ap.add_argument("--threads", type=int, default=32, help="параллельных потоков")
|
||||||
ap.add_argument("--size", type=int, default=100000, help="размер файла в байтах (если не задан --file)")
|
ap.add_argument("--size", type=int, default=100000, help="размер файла в байтах (если не задан --file)")
|
||||||
@@ -50,12 +55,25 @@ def main():
|
|||||||
with httpx.Client(timeout=30) as c:
|
with httpx.Client(timeout=30) as c:
|
||||||
if args.health_only:
|
if args.health_only:
|
||||||
r = c.get(f"{args.url}/health")
|
r = c.get(f"{args.url}/health")
|
||||||
else:
|
if r.status_code == 200:
|
||||||
r = c.post(
|
ok += 1
|
||||||
f"{args.url}/upload",
|
else:
|
||||||
files={"files": (fname, payload, "application/octet-stream")},
|
err += 1
|
||||||
)
|
return
|
||||||
if r.status_code == 200:
|
# Фаза 1: PUT файла на ВМ WebDAV (мимо шлюза ~64KB)
|
||||||
|
token = uuid.uuid4().hex
|
||||||
|
vm_url = args.vm_url.rstrip("/") + "/" + token + "_0"
|
||||||
|
r1 = c.put(vm_url, content=payload, headers={"Content-Type": "application/octet-stream"})
|
||||||
|
if not r1.is_success:
|
||||||
|
err += 1
|
||||||
|
return
|
||||||
|
# Фаза 2: POST /api/upload_refs — бэк тянет файл с ВМ (egress)
|
||||||
|
body = {
|
||||||
|
"batch_id": token,
|
||||||
|
"files": [{"name": fname, "size": real_size, "url": vm_url}],
|
||||||
|
}
|
||||||
|
r2 = c.post(f"{args.url}/api/upload_refs", json=body)
|
||||||
|
if r2.status_code == 200:
|
||||||
ok += 1
|
ok += 1
|
||||||
else:
|
else:
|
||||||
err += 1
|
err += 1
|
||||||
@@ -70,6 +88,7 @@ def main():
|
|||||||
|
|
||||||
elapsed = time.time() - t0
|
elapsed = time.time() - t0
|
||||||
print(f"url={args.url}")
|
print(f"url={args.url}")
|
||||||
|
print(f"vm_url={args.vm_url}")
|
||||||
print(f"n={args.n} threads={args.threads} size={real_size}B file={args.file or '(gen)'} health_only={args.health_only}")
|
print(f"n={args.n} threads={args.threads} size={real_size}B file={args.file or '(gen)'} health_only={args.health_only}")
|
||||||
print(f"ok={ok} err={err} timeouts={slow} elapsed={elapsed:.1f}s rps={args.n / elapsed:.1f}")
|
print(f"ok={ok} err={err} timeouts={slow} elapsed={elapsed:.1f}s rps={args.n / elapsed:.1f}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user