# Ответ Опуса — фоновая классификация при 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_.lock`), либо запись в БД. ### Q4. Мониторинг/логирование DEVNULL недопустим на масштабе — падения невидимы. **Минимум сейчас:** лог-файл per batch `/home/naeel/contracts/logs/classify_.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_.lock` — если существует, вернуть "already running"