diff --git a/History/2026-08-26-cleanup-legacy-upload.md b/History/2026-08-26-cleanup-legacy-upload.md index e0a2b5b..d7fae94 100644 --- a/History/2026-08-26-cleanup-legacy-upload.md +++ b/History/2026-08-26-cleanup-legacy-upload.md @@ -38,5 +38,4 @@ ## Хвост -`tests/load/stress_http.py` всё ещё бьёт по удалённому `/upload` — нужен -переписывание на `/api/upload_refs` (PUT на ВМ → refs). +`tests/load/stress_http.py` — переписан на реальный путь (`PUT` на ВМ → `POST /api/upload_refs`). diff --git a/History/2026-08-26-session-summary.md b/History/2026-08-26-session-summary.md index b93d7c6..8284505 100644 --- a/History/2026-08-26-session-summary.md +++ b/History/2026-08-26-session-summary.md @@ -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` при конкурентных сверках — отложен (нет мультитенантности). 3. E2E с реальным LLM (`gpt-oss-120b`) на проде — не проверялся автоматически (тесты на Fake LLM). diff --git a/tests/README.md b/tests/README.md index 2ee8d2f..f002e61 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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_apply_ops.py` | массовые `apply_ops`: N ADD → N UPDATE → N DELETE, целостность `spec_events` | | `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` | Масштаб через переменные окружения (дефолты — большие): diff --git a/tests/load/stress_http.py b/tests/load/stress_http.py index 5b5a19b..55e2796 100644 --- a/tests/load/stress_http.py +++ b/tests/load/stress_http.py @@ -1,19 +1,23 @@ """HTTP-стресс против contracts-flask (standalone, не pytest). -Создаёт параллельную нагрузку на /upload большими файлами + /health, -замеряет RPS, ошибки, таймауты. Цель — «разогреть» сервис и выявить +Реальный путь загрузки (VM-буфер): + 1) PUT файла на ВМ WebDAV (мимо шлюза кластера ~64KB); + 2) POST /api/upload_refs — бэк сам тянет файл с ВМ (egress). + +Замеряет RPS, ошибки, таймауты. Цель — «разогреть» сервис и выявить деградацию/обрывы (шлюз, OOM, SQLite). -Запуск: - python tests/load/stress_http.py --url http://127.0.0.1:5000 \ - --n 1000 --threads 32 --size 100000 +Запуск (локально): + python tests/load/stress_http.py --url http://127.0.0.1:5000 --n 1000 --threads 32 --size 100000 Пример против прода (осторожно): 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 time +import uuid from concurrent.futures import ThreadPoolExecutor import httpx @@ -22,6 +26,7 @@ import httpx def main(): ap = argparse.ArgumentParser(description="HTTP-стресс contracts-flask") 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("--threads", type=int, default=32, help="параллельных потоков") ap.add_argument("--size", type=int, default=100000, help="размер файла в байтах (если не задан --file)") @@ -50,12 +55,25 @@ def main(): with httpx.Client(timeout=30) as c: if args.health_only: r = c.get(f"{args.url}/health") - else: - r = c.post( - f"{args.url}/upload", - files={"files": (fname, payload, "application/octet-stream")}, - ) - if r.status_code == 200: + if r.status_code == 200: + ok += 1 + else: + err += 1 + return + # Фаза 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 else: err += 1 @@ -70,6 +88,7 @@ def main(): elapsed = time.time() - t0 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"ok={ok} err={err} timeouts={slow} elapsed={elapsed:.1f}s rps={args.n / elapsed:.1f}")