Compare commits

..
2 Commits
4 changed files with 61 additions and 20 deletions
+55
View File
@@ -0,0 +1,55 @@
# drhider — тесты загрузки и стресс (2026-08-23, v0.0.59–0.0.60)
_Платформа: Штурвал, кластер `naeel-test-3` (kubeconfig `~/.kube/config-naeel-test-3` на ВМ).
ВМ-буфер: `https://contracts.kube5s.ru/drhider-upload/` (nginx dav). Тест-режим: `?upload-only=1`._
## Изменения за день
- **v0.0.58** — ВМ-загрузка: PUT на ВМ + `/api/upload_refs` (egress pull) + тест-режим `?upload-only=1`.
- **v0.0.59** — убран лимит 100 МБ/файл (фронт; остался суммарный 1 ГБ).
- **v0.0.60** — дедуп по **имя+размер** (дату игнорируем), при совпадении остаётся **более свежий**.
## Edge-case тесты (v0.0.59, браузер/Playwright)
- Нулевой размер (`empty.txt` 0 B) — принимается, грузится (0 B/s), в сессии size=0. ✅
- Бинарный файл (`bin.dat`) — принимается, грузится. ✅
- Одинаковое имя, разный размер — суффикс `_2`. ✅
- Одинаковые имя+размер, разная дата — **дедуп** (было `_2`, с v0.0.60 — один, свежий). ✅
- Один файл в архиве и отдельно — **дедуп** (было `_2` из-за mtime, с v0.0.60 — один). ✅
## Стресс-тест загрузки (v0.0.60)
Под: `pythonk8s-8dbc866bb-d84p6`, requests/limits **cpu 2 / memory 4Gi**.
Путь: PUT на ВМ → `POST /api/upload_refs` (один JSON со всеми refs) → `GET /api/session_files`.
### Сессии (11 успешных + 1 OOM)
| Сес. | Файлов | Объём | PUT | refs |
|---|---|---|---|---|
| A | 120 | 364МБ | 34.4с | 37.8с |
| B | 120 | 316МБ | 29.9с | 33.3с |
| C | 1×250МБ | 250МБ | 23.2с | 23.9с |
| D | 120 | 378МБ | 40.0с | 41.6с |
| E | 120 | 301МБ | 26.7с | 31.5с |
| F | 1×200МБ | 200МБ | 18.9с | 33.8с |
| G | 120 | 360МБ | 34.8с | 36.4с |
| H | 120 | 309МБ | 29.0с | 38.4с |
| I | 1×250МБ | 250МБ | 23.2с | 24.1с |
| J | 120 | 357МБ | 33.4с | 37.9с |
| K | 120 | 366МБ | 34.3с | 40.1с |
| **L** | 120 | 369МБ | — | **FAIL (OOM)** |
| M | 120 | 347МБ | 32.5с | 36.0с (после рестарта) |
| N | 120 | 358МБ | 33.1с | 37.5с (после рестарта) |
### Память пода (рост линейный, ~1.2× объёма сессий)
55Mi (база) → 744Mi (3 сес.) → 1991Mi (6 сес.) → 3235Mi (9 сес., ~80% лимита) → **OOMKilled на L**.
### Итог OOM
- `restarts=1, lastReason=OOMKilled, exitCode=137` (лимит 4Gi).
- После авто-рестарта под работает, загрузка продолжается (M, N ok).
- **Все сессии A–L потеряны** (in-memory хранилище, TTL 30 мин, без персиста).
## Выводы / факты
1. Загрузка через ВМ стабильна на больших объёмах: до OOM — 11 сессий ~3.3ГБ, 0 ошибок, скорость не деградирует.
2. Потолок памяти = лимит пода 4Gi; OOM при ~3.5ГБ резидентных сессий.
3. Апп переживает OOM (рестарт + продолжение работы), но сессии теряются.
4. Код: `session.py MAX_SESSION_BYTES = 500МБ` (на сессию) — OOM реален только при ~8+ одновременных сессий.
5. `upload_refs` при переполнении сессии отвечает 404 «Session not found» (вводит в заблуждение — это лимит размера).
6. ГРАБЛЯ: на Krupski `dd` алиасится на `docker compose down`; прокси `172.17.192.1:10808` — тесты только `--noproxy '*'`/`NO_PROXY`.
7. Браузер-автоматизация (Playwright) видит Windows-ФС (`C:\tmp\...`) — тестовые файлы класть в `/mnt/c/tmp/` (WSL).
+4 -18
View File
@@ -11,7 +11,6 @@ import io
import os
import time
import logging
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List, Tuple, Callable, Optional
from . import extractor
@@ -118,19 +117,9 @@ class TwoPassObfuscator:
file_times: List[float] = [0.0] * total
skipped: set = set() # имена файлов, которые не удалось обработать (битые)
# A1: .doc конвертируются через HTTP-сервис liberta (IO-bound).
# Запускаем их в потоках — GIL не мешает (ожидание сети), а основное
# время (PDF/docx, CPU-bound) перекрывается с HTTP-конвертацией .doc.
doc_futures: Dict[int, object] = {}
doc_indices = [i for i, (f, _, _) in enumerate(files)
if f.lower().endswith('.doc')]
if doc_indices:
with ThreadPoolExecutor(max_workers=min(4, len(doc_indices))) as _ex:
for i in doc_indices:
fname, content, ctype = files[i]
doc_futures[i] = _ex.submit(
extractor.extract_text, fname, content, ctype
)
# A1: .doc конвертируются через HTTP-сервис liberta ПОСЛЕДОВАТЕЛЬНО.
# Без параллельности: liberta не выдерживает конкурентные вызовы
# (libreoffice падает с javaldx).
for i, (fname, content, ctype) in enumerate(files):
display_name = os.path.basename(fname) or fname
@@ -139,10 +128,7 @@ class TwoPassObfuscator:
t0 = time.time()
try:
if i in doc_futures:
text = doc_futures[i].result()
else:
text = extractor.extract_text(fname, content, ctype)
text = extractor.extract_text(fname, content, ctype)
except Exception as e:
log.warning("obfuscate: skip file %r: %r", fname, e)
skipped.add(fname)
+1 -1
View File
@@ -115,7 +115,7 @@ def scan_regex(text: str, mapping: Dict[str, str], counters: Dict[str, int]) ->
_CHUNK_BOUNDARIES = ['\n\n', '\n', '. ', '? ', '! ', '; ', ', ', ' ']
_CHUNK_SIZE = 6000 # символов на чанк (≈1500-2000 токенов RU — NER-качество)
_CHUNK_OVERLAP = 500 # перекрытие чанков (сущности на стыке)
_LLM_CONCURRENCY = 4 # параллельных LLM-вызовов
_LLM_CONCURRENCY = 1 # параллельность убрана — всё последовательно (liberta/LLM не тянет конкурентность)
def split_into_chunks(text: str, size: int = _CHUNK_SIZE,
+1 -1
View File
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях)
VERSION = "0.0.60"
VERSION = "0.0.61"
def setup_logging():