chore(ci): деплой не убивает worker посреди скрап-прогона (заплатка до #3074) #3084

Merged
lekss361 merged 1 commit from chore/3029-worker-recreate-guard into main 2026-08-24 17:25:23 +00:00

1 commit

Author SHA1 Message Date
bot-backend
6bb3189bc4 chore(ci): деплой не убивает worker посреди скрап-прогона
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / openapi-codegen-check (pull_request) Successful in 2m10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Successful in 17m32s
Каждый деплой ПТИЦЫ безусловно пересоздавал worker — а он единственный, кто
несёт скрап-прогоны (beat только триггерит, отдельного сервиса scraper в
этом compose нет вовсе). Полный прогон КН идёт часами, при неудаче — сутки;
любой мерж в это окно убивал его молча, и деплой при этом был зелёным.

Заплатка до чекпоинтов (#3074): пока прогон не умеет продолжаться с места
обрыва, дешевле не обрывать.

Пересоздание worker'а пропускается, только когда выполнены оба условия:
образ действительно сменился (иначе пересоздание и так no-op) и прямо
сейчас есть живой прогон. Маркер заводить не пришлось, он уже есть —
kn_scrape_runs и objective_scrape_runs со status='running'; литералы сверены
с backend/app/workers/lifecycle.py, а не угаданы. Анти-зомби: считаются
только прогоны свежее WORKER_GUARD_MAX_SKIP_H часов по
COALESCE(heartbeat_at, started_at) — той же паре, что индексирует
data/sql/55_schema_scrape_resume.sql и по которой фильтрует reaper. Иначе
зависший навечно 'running' заморозил бы worker бесконечно.

Fail-safe перевёрнут в сторону статус-кво: psql молчит, отдал пустое, не
число, контейнера нет, image id не читается — пересоздаём как раньше и
печатаем WARNING. Тихий no-op деплоя опаснее убитого прогона: сломанный
детект иначе оставил бы worker на старом коде навсегда и незаметно.

Ревью в свежем контексте нашло критическую дыру, из-за которой заплатка не
работала бы ровно в своём целевом случае. Guard стоял ПОСЛЕ голого
`up -d` по всему стеку, а тот сам пересоздаёт любой сервис со сменившимся
образом. То есть worker убивался раньше, чем guard успевал сравнить digest'ы,
и к моменту проверки они уже совпадали — печаталось «образ не изменился», ни
SKIPPED, ни WARNING. Теперь worker явно исключён из bulk-команды: список
берётся из `config --services`, который фильтрует по активным профилям, так
что семантика сегодняшнего `up -d` сохраняется, а решение по worker'у
принимает только guard.

Там же поправлено: `tr -d '[:space:]'` вырезал пробел ВНУТРИ timestamp от
NOW(), из-за чего CAST падал и предупреждение «старый код + новая схема»
не печаталось бы никогда.

Риск заплатки назван прямо, потому что он реальный: пропуск означает, что
worker остаётся на старом образе до конца прогона. Миграции применяются ДО
подъёма приложения, поэтому старый код может оказаться на новой схеме —
на этот случай отдельная строка в логе с готовой командой ручного recreate,
и предупреждение о расхождении digest'ов повторяется в каждом следующем
деплое, пока расхождение живо.

Откат — переменная репозитория WORKER_RECREATE_GUARD в 'off', без коммита.

Проверено: YAML парсится, шелл-блок проходит bash -n, имена колонок сверены
со схемой (run_id, heartbeat_at в обеих таблицах, _schema_migrations.applied_at),
поведение `config --services` с профилями проверено на живом Docker —
сервисы под неактивными профилями в список не попадают, регрессии с #3061 нет.

Refs #3029, #3074
2026-08-24 20:06:00 +03:00