5.2 KiB
Ответ Опуса — фоновая классификация при 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-таблица в БД:
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
Минимум сейчас (без переписывания архитектуры)
- Лог-файл вместо DEVNULL:
stdout=open(log_path, 'w') - Лимит симулятора: увеличить
MAX_WAITдо 600с (10 мин) для bulk-тестов - Guard от двойного classify: файл-лок
/tmp/classify_<batch_id>.lock— если существует, вернуть "already running"