101 lines
5.2 KiB
Markdown
101 lines
5.2 KiB
Markdown
# Ответ Опуса — фоновая классификация при 50+ файлах
|
||
|
||
Ответ на `History/opus-classify-async-question.md` от 25.06.2026.
|
||
|
||
---
|
||
|
||
## Диагноз: что рвётся на самом деле
|
||
|
||
**nginx НЕ виноват.** `classify-batch` отдаёт `202` мгновенно. `batch-progress` — короткие независимые запросы, укладываются в 30с. Классификация идёт `classify_worker → api.aillm.ru` напрямую, минуя nginx.
|
||
|
||
Реальное узкое место — **симулятор сдаётся на 4-й минуте** (120 итераций × 2с), а 69 файлов при 4 воркерах не успевают: 69 / 4 ≈ 18 волн × 10-15с/LLM-вызов = 3-5 минут > лимита симулятора.
|
||
|
||
---
|
||
|
||
## Ответы
|
||
|
||
### Q1. `subprocess.Popen` для прода — норм?
|
||
|
||
Для масштаба сотни-тысячи файлов регулярно `Popen`-на-запрос **неадекватен**:
|
||
- Нет супервизии: упал worker — никто не узнал (stdout/stderr в DEVNULL)
|
||
- Нет авто-рестарта
|
||
- Гонки при параллельных батчах с одним `batch_id`
|
||
|
||
**Целевая архитектура:** постоянный worker-сервис под отдельным systemd-юнитом + очередь задач на основе БД.
|
||
|
||
Почему БД-очередь, а не Redis:
|
||
- Work items уже в БД (`documents.classify_status='pending'`)
|
||
- Устойчиво к рестартам ВМ — resumable
|
||
- Redis не установлен в проде
|
||
- Для одной ВМ Celery/RQ — оверкилл
|
||
|
||
Дополнительно при тысячах файлов:
|
||
- retry/backoff на 429/5xx от api.aillm.ru
|
||
- rate-limit к LLM API
|
||
- Аккуратное повышение параллелизма (не 4 воркера, а 10-15)
|
||
|
||
### Q2. nginx при долгих запросах
|
||
|
||
nginx НЕ узкое место. classify-batch=202, batch-progress<30s, классификация идёт worker→LLM напрямую. Реальное узкое: throughput (4 воркера) + лимит симулятора. Прогресс durable в БД, UI не зависит от дедлайна.
|
||
|
||
### Q3. Два classify подряд — два Popen
|
||
|
||
- **Разные `batch_id`** — безопасно. Каждый процесс работает только над своими документами.
|
||
- **Один и тот же `batch_id` дважды** — гонка! Оба сделают `reset_classify_status` + повторные LLM-вызовы → двойные траты токенов, неконсистентные счётчики.
|
||
|
||
**Решение:** НЕ убивать предыдущий процесс (опасно — может испортить БД). Guard через проверку: если для `batch_id` уже есть running-процесс → вернуть `{"ok": false, "error": "already running"}`. Реализация: либо файл-лок (`/tmp/classify_<batch_id>.lock`), либо запись в БД.
|
||
|
||
### Q4. Мониторинг/логирование
|
||
|
||
DEVNULL недопустим на масштабе — падения невидимы.
|
||
|
||
**Минимум сейчас:** лог-файл per batch `/home/naeel/contracts/logs/classify_<batch_id>.log` (Python logging) вместо DEVNULL.
|
||
|
||
**Целевое:** job-таблица в БД:
|
||
```sql
|
||
CREATE TABLE classify_jobs (
|
||
batch_id UUID PRIMARY KEY,
|
||
started_at TIMESTAMP,
|
||
finished_at TIMESTAMP,
|
||
total INT, done INT, failed INT,
|
||
error_text TEXT,
|
||
pid INT
|
||
);
|
||
```
|
||
Даёт: честный прогресс, видимость падений, per-doc retry, аудит.
|
||
|
||
---
|
||
|
||
## Целевая архитектура (будущее)
|
||
|
||
```
|
||
systemd: contracts.service (HTTP)
|
||
systemd: classify-worker.service (фоновая классификация)
|
||
|
||
flow:
|
||
HTTP → 202 + запись в classify_jobs (status='queued')
|
||
classify-worker (постоянно):
|
||
SELECT batch_id FROM classify_jobs WHERE status='queued' LIMIT 1
|
||
→ status='running'
|
||
→ classify_batch(batch_id)
|
||
→ status='done' + метрики
|
||
→ следующий батч
|
||
|
||
Прогресс: documents.classify_status (pending/classified/failed)
|
||
Поллинг: GET /api/batch-progress?batch=X → count by status
|
||
```
|
||
|
||
Преимущества:
|
||
- Устойчиво к рестартам (состояние в БД)
|
||
- Один процесс обрабатывает батчи последовательно — нет гонок
|
||
- systemd мониторит и рестартует при падении
|
||
- Логи systemd/journald
|
||
|
||
---
|
||
|
||
## Минимум сейчас (без переписывания архитектуры)
|
||
|
||
1. **Лог-файл вместо DEVNULL:** `stdout=open(log_path, 'w')`
|
||
2. **Лимит симулятора:** увеличить `MAX_WAIT` до 600с (10 мин) для bulk-тестов
|
||
3. **Guard от двойного classify:** файл-лок `/tmp/classify_<batch_id>.lock` — если существует, вернуть "already running"
|