Молчаливый краш каждые восемь минут: как мы искали баг в конвейере тренд-анализа

Работаю над Trend Analysis — системой, которая вытаскивает из кластеров событий настоящие тренды. Идея простая: тренд — это не один факт, а паттерн, видимый сразу в нескольких независимых источниках. Например, “AI funding accelerating” подтверждается инвестициями OpenAI, Anthropic и Mistral одновременно.
Добавили в систему извлечение domain_tags — метаданные, которые помогают понять, в каких сферах появляются тренды. Написал миграцию базы данных (092), обновил Pydantic-модель ExtractionResult, задеплоил в production. Всё выглядело хорошо.
Потом начался ад.
Pipeline рестартовался сам по себе каждые 8–10 минут. Не crashing с ошибкой, не падая с исключением — просто выходил нормально (exit code 0), будто завершил работу. PM2 считал это штатным поведением, счётчик restarts поднялся до 450. Логи не показывали nothing — ни ошибок, ни предупреждений, ни exception’ов.
Я начал добавлять debug-маркеры на критических этапах. “PHASE_DEBUG” перед главной стадией extraction. Ждал цикла за циклом. Маркер никогда не появлялся.
Потом заметил: логи говорят “Fact extraction done”, потом сразу — крах. Между фазой extraction и следующей стадией что-то умирало молча. Проверил _propagate_domain_tags — новый код, который я добавил в event_linker. Он вызывается после commit. Обёрнут в try/except. Не должно быть проблем.
Но потом я посмотрел на главный asyncio.gather() в функции main(). Там пять задач: _crawl_start_with_flag, _retry_loop, _phase2_loop, _convergence_loop, _wal_checkpoint_loop. И gather() без флага return_exceptions=True. Это значит, если ЛЮБАЯ из них упадёт — весь gather упадёт, и процесс завершится.
Но логов нет…
А потом вспомнил: я использую asyncio.create_task() для запуска _extract_facts_pipeline ВНУТРИ crawl_once(). Это отдельная задача, не добавленная в основной gather. Если она поднимает exception — в Python 3.13 это просто логируется где-то в недрах event loop, но не убивает процесс явно. Процесс выходит чисто, потому что задача закончилась (с ошибкой).
Решение было банальным: либо добавить эту задачу в основной gather, либо завернуть её в try/except с явным логированием. Я выбрал второе — явное логирование всех ошибок внутри _extract_facts_pipeline.
После fix pipeline работал стабильно. Uptime перевалил за 30 минут. Никаких рестартов.
Урок: когда Python молчит, ищи asyncio. Необработанные исключения в create_task() — это коварный враг, потому что он не скалывается, он просто завершает процесс как ни в чём не бывало. 😄
Метаданные
- Session ID:
- grouped_trend-analisis_20260418_1955
- Branch:
- fix/trend-coherence-scoring
- Dev Joke
- Если Java работает — не трогай. Если не работает — тоже не трогай, станет хуже.