docs: дизайн TTL-фикса, прерывания с сохранением, оценки времени и UI (план, код не менялся)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# Дизайн: TTL-фикс + прерывание с сохранением + оценка времени + UI (2026-08-24)
|
||||
|
||||
> План реализации фичи по итогам тестов 24.08 (см. `2026-08-24-tests-upload-and-obfuscation.md`).
|
||||
> Статус: **ПЛАН, код не менялся**. Порядок: TTL → прерывание → ETA → UI.
|
||||
|
||||
---
|
||||
|
||||
## 0. Исходная проблема (что тесты вскрыли)
|
||||
|
||||
- **TTL-баг**: сессия живёт 30 мин (`TTL_SECONDS = 30*60`), таймер запускается при создании
|
||||
и **не продлевается** во время обработки. Обфускация 101 файла заняла 1995с (>30 мин) →
|
||||
сессия удалена TTL → `store_result` молча `False` → `download`/`csv` = **404**, результат потерян.
|
||||
- **Нет UX для долгих прогонов**: юзер не знает, сколько ждать, и не может прервать с сохранением.
|
||||
|
||||
## 1. Требования (от пользователя)
|
||||
|
||||
1. Показывать, что обработка долгая «потому что много данных».
|
||||
2. Возможность **прерывания с сохранением** уже обработанного в выходном ZIP + CSV.
|
||||
3. Прерывание **не мгновенное** — «пусть завершает, сколько требуется» (мягкий добор).
|
||||
4. Юзер **точно знает, что будет сохранено** (до нажатия и во время).
|
||||
5. В начале — **ориентировочное время** обработки всего пакета, чтобы знать, чего ожидать.
|
||||
|
||||
---
|
||||
|
||||
## 2. Блок A — TTL-фикс (фундамент, обязателен первым)
|
||||
|
||||
**Файл:** `site/session.py`
|
||||
|
||||
### Текущее поведение
|
||||
```python
|
||||
TTL_SECONDS = 30 * 60
|
||||
def _start_timer(sid):
|
||||
def _clean(): _sessions.pop(sid, None)
|
||||
timer = threading.Timer(TTL_SECONDS, _clean); timer.daemon=True; timer.start(); return timer
|
||||
```
|
||||
Таймер одноразовый, не продлевается → долгий прогон убивает сессию.
|
||||
|
||||
### Новое поведение
|
||||
- Добавить `def touch(sid)`: отменяет старый таймер и запускает новый (продлевает жизнь).
|
||||
- При старте обработки (`process_stream`) — **отменить** таймер сессии (во время обработки
|
||||
сессия живёт «вечно», пока идёт воркер).
|
||||
- При завершении (`complete` / `error` / `cancelled`) — **запустить** таймер заново
|
||||
(результат доступен ещё 30 мин после окончания).
|
||||
- `add_file`, `get_files` — можно тоже `touch()` для единообразия (не обязательно).
|
||||
|
||||
**Итог:** долгий прогон больше не теряет сессию; результат доступен 30 мин после завершения.
|
||||
|
||||
---
|
||||
|
||||
## 3. Блок B — прерывание с сохранением (мягкая остановка)
|
||||
|
||||
### 3.1. Модель поведения (согласовано с пользователем)
|
||||
|
||||
| Где нажата «Прервать» | Поведение |
|
||||
|---|---|
|
||||
| Во время LLM-анализа | Анализ **добарается до конца** (без полного mapping нельзя сохранить ни одного документа). Прерывание срабатывает на границе фазы замены. |
|
||||
| Во время фазы замены | Дорабатывается текущий файл → стоп. В ZIP+CSV попадают все готовые файлы + полная таблица замен. |
|
||||
| После завершения | Обычный полный результат (прерывание не актуально). |
|
||||
|
||||
- Флаг отмены проверяется **только на границах файлов фазы замены** → результат всегда осмысленный.
|
||||
- LLM-фаза не прерывается (добор), что соответствует «пусть завершает, сколько требуется».
|
||||
|
||||
### 3.2. Изменения по файлам
|
||||
|
||||
**`site/session.py`**
|
||||
- В словаре сессии добавить `"cancel": threading.Event()` при создании.
|
||||
- Хелперы: `request_cancel(sid)`, `cancel_requested(sid)`.
|
||||
|
||||
**`site/routes/api_bp.py`**
|
||||
- Новый эндпоинт `POST /api/cancel/<sid>`: `request_cancel(sid)` → `{ok:true}` (или 404 если нет сессии).
|
||||
- `process_stream`:
|
||||
- При старте — `cancel_event` = событие из сессии, таймер TTL отменяется.
|
||||
- Воркер передаёт `cancel_event` в `obfuscate_files`.
|
||||
- Обработка исключения отмены → сборка **частичного результата**:
|
||||
- ZIP из файлов, прошедших замену (исключая `mapping.csv`) + полный `mapping.csv`.
|
||||
- `store_result` + `store_csv` (сейчас `store_result` сохраняет только полный zip).
|
||||
- Новое SSE-событие `cancelled`: `data: {"saved": <кол-во готовых>, "total": <всего>, ...}`.
|
||||
- После завершения/отмены — перезапуск TTL-таймера.
|
||||
|
||||
**`drhider/obfuscator.py`**
|
||||
- `obfuscate_files(..., cancel_event=None)`.
|
||||
- В цикле фазы замены (по файлам): `if cancel_event and cancel_event.is_set(): raise CancelRequested()`.
|
||||
- В LLM-цикле отмену **НЕ** проверяем (добор до конца).
|
||||
- `CancelRequested` — новое исключение (в `drhider/obfuscator.py` или общий модуль).
|
||||
- Для сборки частичного ZIP: функция должна уметь вернуть уже обработанные файлы
|
||||
(список `(имя, bytes)` заменённых) — либо прогресс-колбеком, либо исключение с данными.
|
||||
|
||||
### 3.3. Частичный результат — детали
|
||||
- Каждый файл, прошедший замену, гарантированно консистентен: глобальный mapping один для всех.
|
||||
- Частичный ZIP = готовые файлы + `mapping.csv` (полная таблица — она собрана к концу LLM).
|
||||
- Если прервано до конца LLM — прерывание не сработает (добор), значит частичный результат
|
||||
будет как минимум с теми файлами, которые успели замениться после LLM.
|
||||
|
||||
---
|
||||
|
||||
## 4. Блок C — оценка времени (ETA)
|
||||
|
||||
### 4.1. Уровень 1 — статическая оценка в начале
|
||||
- Фронт знает `N` файлов и объём `S` (МБ) до обработки.
|
||||
- Эмпирический коэффициент из замеров: 24.08 — 101 файл / ~150 КБ текста → LLM ≈ 1924с
|
||||
(~12.8 с/КБ текста); 23.08 — 100 файлов → LLM 841–985с.
|
||||
- Коэффициент задать константой на фронте/бэке (можно в `config.py`), формула:
|
||||
`оценка ≈ S_текста(КБ) × K + константа`. Для бинарных/сканов текста нет — оценка грубая,
|
||||
поэтому это «ориентировочно», с оговоркой в UI.
|
||||
|
||||
### 4.2. Уровень 2 — после извлечения (бэк знает объём текста)
|
||||
- `obfuscate_files` после фазы извлечения знает суммарный объём текста (`total_chars`).
|
||||
- Через новый колбек (или расширенный `progress_cb`) отдать `{phase:"extract_done", chars:N}`.
|
||||
- Тогда оценка точнее: `tokens ≈ chars × фактор`, `время ≈ tokens / скорость(истор.)`.
|
||||
|
||||
### 4.3. Уровень 3 — адаптивный ETA во время LLM
|
||||
- В LLM-цикле меряем фактическую скорость: обработано символов / прошедшее время.
|
||||
- Оставшийся объём = `total_chars − processed_chars`.
|
||||
- `ETA = оставшиеся символы / скорость`.
|
||||
- Отдавать в существующем heartbeat `llm`:
|
||||
`data: {"active":..., "elapsed":..., "tokens":..., "eta_sec": N, "done_chars":..., "total_chars":...}`.
|
||||
- Фронт обновляет «осталось ~X мин» в live-блоке.
|
||||
|
||||
### 4.4. Ограничения (честно)
|
||||
- Оценка **ориентировочная** (LLM-вывод варьируется). Показывать как «≈», обновлять адаптивно.
|
||||
- Для сканов/PDF без текстового слоя — извлечённый текст мал, LLM-этап быстрый, оценка завышена.
|
||||
|
||||
---
|
||||
|
||||
## 5. Блок D — UI (index.html)
|
||||
|
||||
### 5.1. Сообщение «много данных»
|
||||
- После загрузки (перед обработкой): «Загружено N файлов (X МБ). Идёт обработка —
|
||||
это может занять ~M мин.»
|
||||
- Держать в статус-строке и в live-блоке.
|
||||
|
||||
### 5.2. Кнопка «Прервать»
|
||||
- Появляется/активна на этапе обработки (Фаза 2).
|
||||
- По нажатию — **диалог-подтверждение**:
|
||||
> «Остановить обработку? Будет сохранено: документы, уже прошедшие обработку
|
||||
> (сейчас готово X из N), и таблица замен. Текущий анализ будет доведён до конца.
|
||||
> [Остановить] [Отмена]»
|
||||
- После подтверждения — `POST /api/cancel/<sid>`.
|
||||
- Во время добора: счётчик «готово X/N» + «завершаем текущий этап…».
|
||||
|
||||
### 5.3. Показ частичного результата
|
||||
- SSE-событие `cancelled` → статус «Сохранено X из N документов + таблица замен».
|
||||
- Кнопки «Скачать ZIP» / «Скачать CSV» работают как обычно (ведут на download/csv).
|
||||
|
||||
### 5.4. ETA в live-блоке
|
||||
- «Обработка… осталось ~X мин» — из `eta_sec` хартбита.
|
||||
- Статическая оценка — сразу после старта обработки.
|
||||
|
||||
---
|
||||
|
||||
## 6. Порядок реализации и проверка
|
||||
|
||||
1. **TTL-фикс** (`session.py` + `api_bp.py`): тест — долгий прогон >30 мин, скачивание после.
|
||||
2. **Прерывание** (`obfuscator.py` + `api_bp.py` + `session.py`): тест — прервать в фазе замены,
|
||||
проверить частичный ZIP+CSV.
|
||||
3. **ETA** (`obfuscator.py`/`api_bp.py` + фронт): тест — сверка оценки с фактом.
|
||||
4. **UI** (кнопка, диалог, счётчик): ручной тест в браузере.
|
||||
|
||||
Версия: бамп до **0.0.63** после реализации (всех блоков).
|
||||
|
||||
## 7. Затронутые файлы
|
||||
- `site/session.py` — TTL `touch`, флаг отмены.
|
||||
- `site/routes/api_bp.py` — `POST /api/cancel/<sid>`, событие `cancelled`, ETA в heartbeat, частичный результат.
|
||||
- `drhider/obfuscator.py` — `cancel_event`, `CancelRequested`, передача объёма текста, сборка частичного результата.
|
||||
- `site/templates/index.html` — сообщение, кнопка, диалог, счётчик, ETA.
|
||||
- `drhider/config.py` (опц.) — коэффициент оценки времени.
|
||||
|
||||
## 8. Открытые вопросы
|
||||
- Точное место сбора «готовых файлов» в `obfuscate_files` (где хранить заменённые байты для частичного ZIP).
|
||||
- Формат события `cancelled` (поля `saved/total` + причина).
|
||||
- Значение коэффициента оценки (уточнить по ещё паре замеров).
|
||||
Reference in New Issue
Block a user