fix(tradein/scraper): сигнал живости из середины батча — живые прогоны перестают числиться зависшими (#2725) #2727

Merged
bot-backend merged 1 commit from fix/heartbeat-in-batch into main 2026-08-06 11:30:53 +00:00
Collaborator

Summary

reap_zombies меряет scrape_runs.heartbeat_at с порогом 6 ч, а cian_history_backfill слал heartbeat ровно один раз — до батча. Любой прогон дольше шести часов помечался zombie независимо от того, жив он или висит. Правка #2718 (clock_timestamp()) этого не чинила: она вернула честное время сигналу, которого не посылают.

Замер на проде 2026-08-06 — 6 прогонов cian_history_backfill в статусе zombie, у всех сдвиг heartbeat 16-32 мс и финал ровно на started_at + 6.00 ч. Живыми они при этом были: внутри окна пятерых писались строки offer_price_history с source='cian' (плановый писатель этих строк — только этот батч), у прогона 304 последняя строка легла через 5.40 ч после старта. Штатная длительность источника доходит до 5.06 ч (прогон 346). Полные таблицы — в #2725.

Что сделано

  • backfill_cian_history и backfill_newbuilding_enrichment принимают on_progress и дёргают его на каждой сущности;
  • планировщик (_execute_cian_backfill, run_newbuilding_enrich) переливает колбэк в update_heartbeat вместе с текущими счётчиками — best-effort, сбой heartbeat не роняет уже идущую работу;
  • шаблон не новый: ровно так живёт house_imv_backfill (#1363), geocode_missing, все *_detail_backfill.

Чинится сигнал, а не критерий: пометка zombie несёт побочную функцию — снимает running-блокировку источника (has_running_run видит только status='running'), и ослабление критерия заперло бы источник на зависшем прогоне навсегда. Читатели статуса проверены: has_running_run, admin-фильтр admin.py:2323, бейдж во фронте (RunsTable.tsx, ScraperPage.tsx); _alert_if_consecutive_failures zombie не считает; resume-путей, завязанных на этот статус, в tradein нет (zombie-resume в admin_scrape.py — другое приложение, kn_scrape_runs).

newbuilding_enrich вылечен превентивно: тот же единственный стартовый вызов, на проде max 1.08 ч при limit=25, но полный прогон — 318 домов по ~2.6 мин. listing_source_snapshot (34 зомби) в правку НЕ входит: вся работа — два set-based statement'а, heartbeat невозможен по построению; вылечено иначе в #2607, с 2026-08-02 прогоны укладываются в минуту.

Test plan

  • tests/test_2725_heartbeat_in_batch.py — 5 тестов: сигнал на каждую сущность; 7-часовой прогон с продвигающимся heartbeat под критерий reap НЕ попадает; тот же прогон без продвижения — попадает (контроль); сбой heartbeat не прерывает батч; newbuilding_enrich прокидывает колбэк.
  • Фальсификация: на старом коде 4 из 5 падают (контрольный проходит).
  • Полный прогон backend-сьюта: 3754 passed, падает только пре-существующий test_search_cache_hit (тот самый, что deselect'ится в CI).
  • Прод: следующий ночной cian_history_backfill должен показать heartbeat_at > started_at и ненулевые counters (сейчас источник в skipped — куки Циана протухли, поэтому ближайшая проверка — по коду в живом контейнере).

Refs #2725

## Summary `reap_zombies` меряет `scrape_runs.heartbeat_at` с порогом 6 ч, а `cian_history_backfill` слал heartbeat ровно один раз — **до** батча. Любой прогон дольше шести часов помечался `zombie` независимо от того, жив он или висит. Правка #2718 (`clock_timestamp()`) этого не чинила: она вернула честное время сигналу, которого не посылают. Замер на проде 2026-08-06 — 6 прогонов `cian_history_backfill` в статусе `zombie`, у всех сдвиг heartbeat 16-32 мс и финал ровно на `started_at + 6.00 ч`. Живыми они при этом были: внутри окна пятерых писались строки `offer_price_history` с `source='cian'` (плановый писатель этих строк — только этот батч), у прогона 304 последняя строка легла через **5.40 ч** после старта. Штатная длительность источника доходит до 5.06 ч (прогон 346). Полные таблицы — в #2725. ## Что сделано - `backfill_cian_history` и `backfill_newbuilding_enrichment` принимают `on_progress` и дёргают его на каждой сущности; - планировщик (`_execute_cian_backfill`, `run_newbuilding_enrich`) переливает колбэк в `update_heartbeat` вместе с текущими счётчиками — best-effort, сбой heartbeat не роняет уже идущую работу; - шаблон не новый: ровно так живёт `house_imv_backfill` (#1363), `geocode_missing`, все `*_detail_backfill`. Чинится **сигнал, а не критерий**: пометка `zombie` несёт побочную функцию — снимает running-блокировку источника (`has_running_run` видит только `status='running'`), и ослабление критерия заперло бы источник на зависшем прогоне навсегда. Читатели статуса проверены: `has_running_run`, admin-фильтр `admin.py:2323`, бейдж во фронте (`RunsTable.tsx`, `ScraperPage.tsx`); `_alert_if_consecutive_failures` `zombie` не считает; resume-путей, завязанных на этот статус, в tradein нет (zombie-resume в `admin_scrape.py` — другое приложение, `kn_scrape_runs`). `newbuilding_enrich` вылечен превентивно: тот же единственный стартовый вызов, на проде max 1.08 ч при `limit=25`, но полный прогон — 318 домов по ~2.6 мин. `listing_source_snapshot` (34 зомби) в правку НЕ входит: вся работа — два set-based statement'а, heartbeat невозможен по построению; вылечено иначе в #2607, с 2026-08-02 прогоны укладываются в минуту. ## Test plan - [x] `tests/test_2725_heartbeat_in_batch.py` — 5 тестов: сигнал на каждую сущность; 7-часовой прогон с продвигающимся heartbeat под критерий reap НЕ попадает; тот же прогон без продвижения — попадает (контроль); сбой heartbeat не прерывает батч; `newbuilding_enrich` прокидывает колбэк. - [x] Фальсификация: на старом коде 4 из 5 падают (контрольный проходит). - [x] Полный прогон backend-сьюта: 3754 passed, падает только пре-существующий `test_search_cache_hit` (тот самый, что deselect'ится в CI). - [ ] Прод: следующий ночной `cian_history_backfill` должен показать `heartbeat_at > started_at` и ненулевые counters (сейчас источник в `skipped` — куки Циана протухли, поэтому ближайшая проверка — по коду в живом контейнере). Refs #2725
bot-backend added 1 commit 2026-08-06 11:19:51 +00:00
fix(tradein/scraper): сигнал живости из середины батча — живые прогоны перестают числиться зависшими (#2725)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m9s
eec62c4e1c
reap_zombies меряет scrape_runs.heartbeat_at с порогом 6 ч, а cian_history_backfill
слал heartbeat ровно один раз — ДО батча. Прогон, работающий дольше шести часов,
помечался 'zombie' независимо от того, жив он или висит.

Замер на проде 2026-08-06: 6 прогонов cian_history_backfill в статусе 'zombie', у всех
шести сдвиг heartbeat 16-32 мс (единственный стартовый вызов) и финал ровно на
started_at + 6.00 ч. Живыми они были: внутри окна пятерых писались строки
offer_price_history с source='cian' (98/523/38/60/83 — плановый писатель этих строк
только этот батч), у прогона 304 последняя строка легла через 5.40 ч после старта.
Штатная длительность источника доходит до 5.06 ч (прогон 346).

Чинится сигнал, а не критерий: пометка 'zombie' снимает running-блокировку источника
(has_running_run), и ослабление критерия заперло бы источник на зависшем прогоне
навсегда. Плюс mark_done апдейтит WHERE status='running' — после ложной пометки финал
прогона становится no-op, отсюда нулевые counters у всех шести строк.

backfill_cian_history и backfill_newbuilding_enrichment получают колбэк on_progress,
дёргаемый на каждой сущности; планировщик переливает его в update_heartbeat вместе с
текущими счётчиками (best-effort — сбой heartbeat не роняет уже идущую работу).
newbuilding_enrich лечится тем же шаблоном превентивно: тот же единственный стартовый
вызов, на проде пока max 1.08 ч при limit=25, но полный прогон — 318 домов по ~2.6 мин.

Шаблон уже был в репозитории (house_imv_backfill, #1363) — здесь он доехал до двух
оставшихся долгоживущих задач. listing_source_snapshot (34 зомби) в правку не входит:
там вся работа — два set-based statement'а, heartbeat невозможен по построению,
лечилось иначе в #2607 (statement_timeout).

Refs #2725
bot-backend merged commit 5f71fc670f into main 2026-08-06 11:30:53 +00:00
bot-backend deleted branch fix/heartbeat-in-batch 2026-08-06 11:30:53 +00:00
Sign in to join this conversation.
No reviewers
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#2727
No description provided.