Планировщик: зомби после ребута хоста блокируют источники до 6 часов — нужен boot-reap по границе старта контейнера #3122

Closed
opened 2026-08-27 08:59:40 +00:00 by bot-backend · 1 comment
Collaborator

Прод-факт (сегодня, 27.08 08:57 UTC, после ночного офлайна #3119)

Контейнер tradein-scraper перезапущен ~07:57. Час спустя в scrape_runs — 4 строки running, чьи процессы доказуемо мертвы (стартовали ДО старта контейнера, пульс замер на отметке старта):

id source старт пульс молчит
5018 avito_newbuilding_sweep 03:30 327 мин
5029 avito_city_sweep_pervouralsk 05:08 229 мин
5038 avito_city_sweep 06:46 131 мин
5040 avito_city_sweep_verkhnyaya_pyshma 07:08 109 мин

Единственный живой прогон (5081, Серов, старт 08:55) при этом реально работал — лог тащил страницы.

Цена

has_running_run гейтит claim по статусу — источник с зомби-строкой не планируется, пока 6-часовой reap_zombies (ZOMBIE_THRESHOLD_HOURS=6) её не снимет:

  • avito_newbuilding_sweep заблокирован 03:30 → ~09:30;
  • avito_city_sweep_pervouralsk 05:08 → ~11:08;
  • avito_city_sweep 06:46 → ~12:46;
  • avito_city_sweep_verkhnyaya_pyshma 07:08 → ~13:08.

То есть до шести часов слепоты на источник — ровно ПОСЛЕ простоя, когда догон нужнее всего. Вчерашний офлайн был ~15 часов; эти хвосты добавляют к нему до 5 часов на четыре источника.

Почему 6-часовой порог тут не аргумент

Порог 6ч выбран осознанно против ложных срабатываний по ПУЛЬСУ (#2702: у cian_history_backfill нормальный прогон 5+ часов, пульс движется редко). Но здесь критерий другой и точный, без эвристики: прогон, стартовавший раньше старта собственного контейнера, мёртв по построению — его процесс жил в предыдущем контейнере и умер вместе с ним. Пульс вообще не участвует.

Предложение

При старте планировщика (scheduler_main / первый тик scheduler_loop) — однократный boot-reap:

UPDATE scrape_runs SET status='zombie', finished_at=clock_timestamp()
WHERE status='running' AND started_at < :container_started_at

container_started_at — время старта процесса (например, время импорта модуля), с минутой запаса на часовой дрейф.

Проверить при реализации: единственная ситуация, где критерий мог бы соврать — два живых scraper-контейнера одновременно (новый репает прогоны ещё дренящегося старого). Атомарный recreate деплоя (#1951: drain ДО пересоздания) это исключает, но подтвердить стоит на месте.

Метки: обнаружено при разборе гейта мержа PR #3121; связано с #2845 (там reap-семантика уже обсуждалась — 'zombie' не в _RESUME_STATUSES намеренно, boot-reap этому не противоречит: у boot-зомби процесс гарантированно мёртв, их чекпоинт как раз БЕЗОПАСНО подхватывать — стоит рассмотреть добавление отдельного статуса или флага, чтобы resume их видел).

## Прод-факт (сегодня, 27.08 08:57 UTC, после ночного офлайна #3119) Контейнер tradein-scraper перезапущен ~07:57. Час спустя в `scrape_runs` — 4 строки `running`, чьи процессы доказуемо мертвы (стартовали ДО старта контейнера, пульс замер на отметке старта): | id | source | старт | пульс молчит | |---|---|---|---| | 5018 | avito_newbuilding_sweep | 03:30 | 327 мин | | 5029 | avito_city_sweep_pervouralsk | 05:08 | 229 мин | | 5038 | avito_city_sweep | 06:46 | 131 мин | | 5040 | avito_city_sweep_verkhnyaya_pyshma | 07:08 | 109 мин | Единственный живой прогон (5081, Серов, старт 08:55) при этом реально работал — лог тащил страницы. ## Цена `has_running_run` гейтит claim по статусу — источник с зомби-строкой не планируется, пока 6-часовой `reap_zombies` (ZOMBIE_THRESHOLD_HOURS=6) её не снимет: - avito_newbuilding_sweep заблокирован 03:30 → ~09:30; - avito_city_sweep_pervouralsk 05:08 → ~11:08; - avito_city_sweep 06:46 → ~12:46; - avito_city_sweep_verkhnyaya_pyshma 07:08 → ~13:08. То есть до шести часов слепоты на источник — ровно ПОСЛЕ простоя, когда догон нужнее всего. Вчерашний офлайн был ~15 часов; эти хвосты добавляют к нему до 5 часов на четыре источника. ## Почему 6-часовой порог тут не аргумент Порог 6ч выбран осознанно против ложных срабатываний по ПУЛЬСУ (#2702: у cian_history_backfill нормальный прогон 5+ часов, пульс движется редко). Но здесь критерий другой и точный, без эвристики: **прогон, стартовавший раньше старта собственного контейнера, мёртв по построению** — его процесс жил в предыдущем контейнере и умер вместе с ним. Пульс вообще не участвует. ## Предложение При старте планировщика (scheduler_main / первый тик scheduler_loop) — однократный boot-reap: ```sql UPDATE scrape_runs SET status='zombie', finished_at=clock_timestamp() WHERE status='running' AND started_at < :container_started_at ``` `container_started_at` — время старта процесса (например, время импорта модуля), с минутой запаса на часовой дрейф. Проверить при реализации: единственная ситуация, где критерий мог бы соврать — два живых scraper-контейнера одновременно (новый репает прогоны ещё дренящегося старого). Атомарный recreate деплоя (#1951: drain ДО пересоздания) это исключает, но подтвердить стоит на месте. Метки: обнаружено при разборе гейта мержа PR #3121; связано с #2845 (там reap-семантика уже обсуждалась — 'zombie' не в _RESUME_STATUSES намеренно, boot-reap этому не противоречит: у boot-зомби процесс гарантированно мёртв, их чекпоинт как раз БЕЗОПАСНО подхватывать — стоит рассмотреть добавление отдельного статуса или флага, чтобы resume их видел).
Author
Collaborator

Сделано и принято живьём (PR #3123 + follow-up #3124, деплой ff233589)

Механизм: reap_boot_zombies на старте scheduler_loop — критерий started_at < старт процесса − 60с, пульс не участвует (ложные срабатывания класса #2702 невозможны по построению). Маркер counters.boot_reaped=true открывает boot-зомби подхват чекпоинта в _resume_decision — «движущейся точки», из-за которой 'zombie' исключён из resume, у мёртвого процесса быть не может; пороговый zombie без маркера по-прежнему отвергается (тест-инвариант).

Живая приёмка (27.08 09:47, планировщик 39 секунд от роду):

INFO scheduler: boot-reap — прогонов предыдущего контейнера нет (0)

Ноль здесь честный: сегодняшних четырёх зомби успел снять деплоевский startup-reap (cancelled — они в _RESUME_STATUSES, чекпоинты подхватятся). Boot-reap — страховка ровно для случая из шапки задачи: ребут ХОСТА без деплоя, когда деплоевского reap'а не существует. Первая же приёмка вскрыла и починила слепоту при нуле (#3124): механизм теперь оставляет ровно одну из двух строк в логе всегда — исполнение доказуемо грепом лога, а не грепом кода.

Датированная проверка боевого случая: при следующем ребуте хоста без деплоя в логе должен появиться warning-вариант со списком снятых прогонов, и источники не слепнут на 6ч.

## Сделано и принято живьём (PR #3123 + follow-up #3124, деплой ff233589) **Механизм**: `reap_boot_zombies` на старте scheduler_loop — критерий `started_at < старт процесса − 60с`, пульс не участвует (ложные срабатывания класса #2702 невозможны по построению). Маркер `counters.boot_reaped=true` открывает boot-зомби подхват чекпоинта в `_resume_decision` — «движущейся точки», из-за которой 'zombie' исключён из resume, у мёртвого процесса быть не может; пороговый zombie без маркера по-прежнему отвергается (тест-инвариант). **Живая приёмка** (27.08 09:47, планировщик 39 секунд от роду): ``` INFO scheduler: boot-reap — прогонов предыдущего контейнера нет (0) ``` Ноль здесь честный: сегодняшних четырёх зомби успел снять деплоевский startup-reap (`cancelled` — они в `_RESUME_STATUSES`, чекпоинты подхватятся). Boot-reap — страховка ровно для случая из шапки задачи: ребут ХОСТА без деплоя, когда деплоевского reap'а не существует. Первая же приёмка вскрыла и починила слепоту при нуле (#3124): механизм теперь оставляет ровно одну из двух строк в логе всегда — исполнение доказуемо грепом лога, а не грепом кода. Датированная проверка боевого случая: при следующем ребуте хоста без деплоя в логе должен появиться warning-вариант со списком снятых прогонов, и источники не слепнут на 6ч.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3122
No description provided.