Скрапперы: чекпоинты — длинный прогон продолжается с места обрыва, а не начинается заново #3074

Closed
opened 2026-08-24 15:25:40 +00:00 by bot-backend · 6 comments
Collaborator

Эпик: #2989. Выделено из #3029 по решению владельца (2026-08-24): из трёх вариантов защиты длинных скрап-прогонов от деплоя выбран вариант 3 — чекпоинты. Он не devops, поэтому живёт отдельной задачей со scope/backend, а #3029 остаётся про перенастройку деплой-конвейера под переезд.

Зачем

Замер на проде за 30 дней (комментарий в #3029): из 2462 прогонов 10 убито деплоем (status='cancelled'), и это ровно самые дорогие — те, что собирают город целиком.

source дата прожил до отмены
avito_full_load_exhaustive 23.08 26 мин
cian_full_load 20.08 41 мин
yandex_city_sweep 15.08 65 мин
yandex_city_sweep 12.08 2 ч 33 мин
avito_full_load_exhaustive 09.08 2 ч 58 мин
cian_full_load 31.07 2 ч 10 мин

Примерно один убитый прогон каждые три дня. Убитый прогон сегодня теряется целиком: следующий запуск начинает город с нуля.

Почему существующая защита не помогает

В deploy-tradein.yml уже стоят три меры (#1951): атомарный recreate, graceful drain до 5 минут, startup-reap зависших running. Плюс SCRAPER_RECREATE пропускает дренаж, если digest образа не изменился (#2679).

Дренаж не может помочь по построению: cian_full_load идёт ~400 минут, деплой столько ждать не будет и не должен. Дренаж спасает короткие прогоны, а деплой убивает длинные.

Что делать

Прогон должен продолжаться с места обрыва, а не начинаться заново. Это снимает проблему целиком — и для деплоя, и для падений, OOM, банов и рестартов хоста, то есть для всех пяти видов обрыва сразу, а не только для одного.

Ориентировочные требования (уточнить при разборе, а не принимать на веру):

  • Единица чекпоинта — то, что уже есть в цикле обхода: страница выдачи / участок города / диапазон фильтра. Не изобретать новую разбивку, если существующий цикл уже итерируется чем-то естественным.
  • Чекпоинт пишется в БД (рядом со scrape_runs), а не в файл — контейнер пересоздаётся, том может не пережить.
  • Запись чекпоинта должна быть дешёвой и не превращаться в ещё один источник записи на каждый оффер (см. #2989: listings уже получает 198 апдейтов на строку).
  • При старте прогон обязан явно решать: продолжить незавершённый или начать новый, и это решение должно быть видно в логе и в scrape_runs.
  • Возобновлённый прогон не должен ломать дедупликацию и статистику: done по продолженному прогону обязан означать то же, что и раньше.
  • Устаревший чекпоинт (выдача уехала, фильтр больше не отдаёт ту же страницу) должен обнаруживаться, а не приводить к молчаливой дыре в покрытии.

Границы

  • Это правка Python в скрапперах trade-in (tradein-mvp/backend/...), деплой-скриптов НЕ касается.
  • Задача выглядит больше одного захода — при разборе разбить по источникам: сначала один скраппер целиком как образец (кандидат — cian_full_load, самый длинный), потом остальные по образцу.

Приёмка

  • Прогон, убитый на середине, при следующем запуске продолжается, а не начинается заново
  • Продолжение видно в scrape_runs и в логе явной записью
  • Покрытие продолженного прогона равно покрытию непрерывного (сверка на одном городе)
  • Стоимость чекпоинтов измерена: сколько дополнительных записей в БД на прогон
  • Устаревший чекпоинт обнаруживается и приводит к честному перезапуску, а не к дыре

Остаётся в #3029 (не здесь)

Вариант 1 («не пересоздавать scraper, если бежит длинный прогон — отложить») — дешёвая заплатка, доступная прямо сейчас и не требующая Python. Она строго не хуже текущего поведения (сейчас прогон и так теряется). Владелец выбрал чекпоинты как целевое решение; отдельно стоит решить, ставить ли заплатку на время до их появления — цена заплатки в том, что scraper может на несколько часов отставать от backend по образу, а образ у них ОДИН (#2679).

Refs #3029, #2989, #1951, #2679

Эпик: #2989. Выделено из #3029 по решению владельца (2026-08-24): из трёх вариантов защиты длинных скрап-прогонов от деплоя выбран **вариант 3 — чекпоинты**. Он не devops, поэтому живёт отдельной задачей со `scope/backend`, а #3029 остаётся про перенастройку деплой-конвейера под переезд. ## Зачем Замер на проде за 30 дней (комментарий в #3029): из 2462 прогонов **10 убито деплоем** (`status='cancelled'`), и это ровно самые дорогие — те, что собирают город целиком. | source | дата | прожил до отмены | |---|---|---| | avito_full_load_exhaustive | 23.08 | 26 мин | | cian_full_load | 20.08 | 41 мин | | yandex_city_sweep | 15.08 | 65 мин | | yandex_city_sweep | 12.08 | 2 ч 33 мин | | avito_full_load_exhaustive | 09.08 | 2 ч 58 мин | | cian_full_load | 31.07 | 2 ч 10 мин | Примерно один убитый прогон каждые три дня. Убитый прогон сегодня теряется целиком: следующий запуск начинает город с нуля. ## Почему существующая защита не помогает В `deploy-tradein.yml` уже стоят три меры (#1951): атомарный recreate, graceful drain до 5 минут, startup-reap зависших `running`. Плюс `SCRAPER_RECREATE` пропускает дренаж, если digest образа не изменился (#2679). Дренаж не может помочь **по построению**: `cian_full_load` идёт ~400 минут, деплой столько ждать не будет и не должен. Дренаж спасает короткие прогоны, а деплой убивает длинные. ## Что делать Прогон должен **продолжаться с места обрыва**, а не начинаться заново. Это снимает проблему целиком — и для деплоя, и для падений, OOM, банов и рестартов хоста, то есть для всех пяти видов обрыва сразу, а не только для одного. Ориентировочные требования (уточнить при разборе, а не принимать на веру): - Единица чекпоинта — то, что уже есть в цикле обхода: страница выдачи / участок города / диапазон фильтра. Не изобретать новую разбивку, если существующий цикл уже итерируется чем-то естественным. - Чекпоинт пишется в БД (рядом со `scrape_runs`), а не в файл — контейнер пересоздаётся, том может не пережить. - Запись чекпоинта должна быть дешёвой и не превращаться в ещё один источник записи на каждый оффер (см. #2989: `listings` уже получает 198 апдейтов на строку). - При старте прогон обязан **явно** решать: продолжить незавершённый или начать новый, и это решение должно быть видно в логе и в `scrape_runs`. - Возобновлённый прогон не должен ломать дедупликацию и статистику: `done` по продолженному прогону обязан означать то же, что и раньше. - Устаревший чекпоинт (выдача уехала, фильтр больше не отдаёт ту же страницу) должен обнаруживаться, а не приводить к молчаливой дыре в покрытии. ## Границы - Это правка Python в скрапперах trade-in (`tradein-mvp/backend/...`), деплой-скриптов НЕ касается. - Задача выглядит больше одного захода — при разборе разбить по источникам: сначала один скраппер целиком как образец (кандидат — `cian_full_load`, самый длинный), потом остальные по образцу. ## Приёмка - [ ] Прогон, убитый на середине, при следующем запуске продолжается, а не начинается заново - [ ] Продолжение видно в `scrape_runs` и в логе явной записью - [ ] Покрытие продолженного прогона равно покрытию непрерывного (сверка на одном городе) - [ ] Стоимость чекпоинтов измерена: сколько дополнительных записей в БД на прогон - [ ] Устаревший чекпоинт обнаруживается и приводит к честному перезапуску, а не к дыре ## Остаётся в #3029 (не здесь) Вариант 1 («не пересоздавать scraper, если бежит длинный прогон — отложить») — дешёвая заплатка, доступная прямо сейчас и не требующая Python. Она **строго не хуже** текущего поведения (сейчас прогон и так теряется). Владелец выбрал чекпоинты как целевое решение; отдельно стоит решить, ставить ли заплатку на время до их появления — цена заплатки в том, что scraper может на несколько часов отставать от backend по образу, а образ у них ОДИН (#2679). Refs #3029, #2989, #1951, #2679
Owner

Замер цены отсутствия чекпоинтов: недельный полный сбор Авито не завершился ни разу за 8 недель

Наткнулся при разборе отмен деплоем (#3029). avito_full_load_exhaustive идёт раз в неделю, по субботам. Все восемь последних прогонов:

старт статус шёл, мин собрано причина
08-23 14:24 cancelled 26 0 деплой пересоздал scraper
08-16 13:37 banned (platform) 67 582 SERP вернул 429 на page=1
08-09 14:02 cancelled 178 5496 деплой пересоздал scraper
08-02 14:52 banned (infra) 154 0 browser-sidecar недоступен (прокси лежал)
07-26 13:10 banned 3 0
07-19 14:46 banned 4 0
07-12 13:07 banned 5 0
07-05 14:21 banned 28 0

Ни одного done. Шесть банов и две отмены деплоем.

Почему это довод именно за чекпоинты

Две отмены — прямой аргумент: прогон 08-09 отдал 178 минут и 5496 объявлений в мусор, потому что перезапуск начинается с нуля. Но и шесть банов лечатся тем же: platform-бан на page=1 (08-16) и падение прокси на page=3 (08-02) — это обрывы в середине, после которых при чекпоинте прогон продолжился бы со следующей страницы через час, а не ждал бы неделю до следующего расписания.

Сейчас же любой обрыв — деплой, 429, упавший сайдкар — стоит целой недели: следующая попытка только через 7 дней и снова с нуля. Отсюда и нулевая результативность за два месяца.

Что уже сделано правильно и помогает

Диагностика есть и она честная: ban_kind различает platform и infra (#2686/#2764), а отмену деплоем механизм подписывает дословно — deploy #1951: tradein-scraper recreated mid-run (startup-reap, checkpoint ...). То есть данных для чекпоинта достаточно: момент обрыва фиксируется, страница известна. Не хватает только возобновления с него.

Замер снят на живой БД Poincare 2026-08-26; выборка — scrape_runs по source='avito_full_load_exhaustive'.

## Замер цены отсутствия чекпоинтов: недельный полный сбор Авито не завершился **ни разу за 8 недель** Наткнулся при разборе отмен деплоем (#3029). `avito_full_load_exhaustive` идёт раз в неделю, по субботам. Все восемь последних прогонов: | старт | статус | шёл, мин | собрано | причина | |---|---|---|---|---| | 08-23 14:24 | cancelled | 26 | 0 | деплой пересоздал scraper | | 08-16 13:37 | banned (`platform`) | 67 | 582 | SERP вернул 429 на page=1 | | 08-09 14:02 | cancelled | **178** | **5496** | деплой пересоздал scraper | | 08-02 14:52 | banned (`infra`) | 154 | 0 | browser-sidecar недоступен (прокси лежал) | | 07-26 13:10 | banned | 3 | 0 | | | 07-19 14:46 | banned | 4 | 0 | | | 07-12 13:07 | banned | 5 | 0 | | | 07-05 14:21 | banned | 28 | 0 | | **Ни одного `done`.** Шесть банов и две отмены деплоем. ### Почему это довод именно за чекпоинты Две отмены — прямой аргумент: прогон 08-09 отдал 178 минут и 5496 объявлений в мусор, потому что перезапуск начинается с нуля. Но и шесть банов лечатся тем же: `platform`-бан на page=1 (08-16) и падение прокси на page=3 (08-02) — это обрывы в середине, после которых при чекпоинте прогон продолжился бы со следующей страницы через час, а не ждал бы неделю до следующего расписания. Сейчас же **любой** обрыв — деплой, 429, упавший сайдкар — стоит целой недели: следующая попытка только через 7 дней и снова с нуля. Отсюда и нулевая результативность за два месяца. ### Что уже сделано правильно и помогает Диагностика есть и она честная: `ban_kind` различает `platform` и `infra` (#2686/#2764), а отмену деплоем механизм подписывает дословно — `deploy #1951: tradein-scraper recreated mid-run (startup-reap, checkpoint ...)`. То есть данных для чекпоинта достаточно: момент обрыва фиксируется, страница известна. Не хватает только возобновления с него. Замер снят на живой БД Poincare 2026-08-26; выборка — `scrape_runs` по `source='avito_full_load_exhaustive'`.
Author
Collaborator

Сделано в PR #3098 (смержен 26.08) + замер, закрывающий вопрос «остальные по образцу»

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

Прод-факт: 4707 (avito full load, 23.08) подхватил 42 корзины у 4117, был убит деплоем на 26-й минуте — до завершения первой новой корзины — и не успел ни разу написать heartbeat с done_buckets. Его собственный чекпоинт пуст → следующий кандидат увидел бы no_checkpoint, и цепочка оборвалась бы при живом resume_chain. Теперь _resume_decision при вердикте ok кладёт done_buckets предшественника в counters нового прогона прямо при claim.

2. yandex_city_sweep получил чекпоинты (второй по цене источник из таблицы убитых)

Единица — combo (сегмент × комнатность × ценовой диапазон), ровно то, чем цикл уже итерируется. Провайдер: skip_combos + on_combo для каждого пройденного combo, включая пустые (иначе пустой combo не попадает в чекпоинт и перечитывается вечно). Планировщик: generic-механизм #2845 бесплатно (params-идентичность, свежесть, потолок цепочки). Подхват гейтится единственным якорем — combo_label якоря не содержит; на проде все свипы одноякорные.

3. «Потом остальные по образцу» — замер говорит, что образец больше некуда прикладывать

30 дней, Poincare:

source прогонов оборвано ср. мин макс мин
yandex_city_sweep 30 4 45 153
cian_city_sweep 31 0 33 68
avito_city_sweep 36 15 4 18
domclick_city_sweep 31 26 3 24
  • cian_city_sweep: ноль обрывов за месяц — 33-минутные прогоны дренаж деплоя переживают; чекпоинт был бы кодом без случая применения.
  • avito/domclick_city_sweep: обрывов много, но прогоны умирают на 3-4-й минуте от банов (QRATOR) — накопленного прогресса, который стоило бы спасать, не существует. Их лечит трек #3044/#3045, а не resume.
  • avito/cian full_load покрыты #930/#2845 ещё раньше.

То есть длинные прогоны, ради которых задача заводилась, теперь чекпоинтятся все. Приёмка по яндексу: у следующего оборванного yandex_city_sweep преемник должен показать resume_reason='ok' и растущий done_buckets; при такте в сутки — проверяемо в течение недели (обрывы у него ~раз в неделю).

## Сделано в PR #3098 (смержен 26.08) + замер, закрывающий вопрос «остальные по образцу» ### 1. Дыра в уже существующем механизме: чекпоинт жил только в памяти до первого прогресса Прод-факт: 4707 (avito full load, 23.08) подхватил 42 корзины у 4117, был убит деплоем на 26-й минуте — **до завершения первой новой корзины** — и не успел ни разу написать heartbeat с `done_buckets`. Его собственный чекпоинт пуст → следующий кандидат увидел бы `no_checkpoint`, и цепочка оборвалась бы при живом `resume_chain`. Теперь `_resume_decision` при вердикте `ok` кладёт `done_buckets` предшественника в counters нового прогона прямо при claim. ### 2. yandex_city_sweep получил чекпоинты (второй по цене источник из таблицы убитых) Единица — combo (сегмент × комнатность × ценовой диапазон), ровно то, чем цикл уже итерируется. Провайдер: `skip_combos` + `on_combo` для каждого пройденного combo, **включая пустые** (иначе пустой combo не попадает в чекпоинт и перечитывается вечно). Планировщик: generic-механизм #2845 бесплатно (params-идентичность, свежесть, потолок цепочки). Подхват гейтится единственным якорем — combo_label якоря не содержит; на проде все свипы одноякорные. ### 3. «Потом остальные по образцу» — замер говорит, что образец больше некуда прикладывать 30 дней, Poincare: | source | прогонов | оборвано | ср. мин | макс мин | |---|---:|---:|---:|---:| | yandex_city_sweep | 30 | 4 | **45** | **153** | ← покрыт этим PR | cian_city_sweep | 31 | **0** | 33 | 68 | | avito_city_sweep | 36 | 15 | **4** | 18 | | domclick_city_sweep | 31 | 26 | **3** | 24 | - **cian_city_sweep**: ноль обрывов за месяц — 33-минутные прогоны дренаж деплоя переживают; чекпоинт был бы кодом без случая применения. - **avito/domclick_city_sweep**: обрывов много, но прогоны умирают на 3-4-й минуте от банов (QRATOR) — накопленного прогресса, который стоило бы спасать, не существует. Их лечит трек #3044/#3045, а не resume. - avito/cian **full_load** покрыты #930/#2845 ещё раньше. То есть длинные прогоны, ради которых задача заводилась, теперь чекпоинтятся все. Приёмка по яндексу: у следующего оборванного `yandex_city_sweep` преемник должен показать `resume_reason='ok'` и растущий `done_buckets`; при такте в сутки — проверяемо в течение недели (обрывы у него ~раз в неделю).
Author
Collaborator

Перепроверка 27.08.2026 (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре.

Посылка держится: таблиц и колонок чекпоинта нет, курсор last_id живёт в памяти прогона (services/scheduler.py:198,244 — heartbeat есть, персиста позиции нет), у *_full_load и city_sweep резюма нет вовсе.

Свежий замер ущерба: 27.08 деплой убил 4 sweep'а длиной до 343 минут; за 30 суток 14 cancelled из 2511.

**Перепроверка 27.08.2026** (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре. Посылка держится: таблиц и колонок чекпоинта нет, курсор `last_id` живёт в памяти прогона (`services/scheduler.py:198,244` — heartbeat есть, персиста позиции нет), у `*_full_load` и `city_sweep` резюма нет вовсе. Свежий замер ущерба: 27.08 деплой убил 4 sweep'а длиной до 343 минут; за 30 суток 14 cancelled из 2511.
Author
Collaborator

Поправка к моему комментарию выше — он неверен. Я написал «чекпоинтов нет, курсор живёт в памяти прогона». Механизм существует и работает; я искал слово checkpoint, а реализация называется done_buckets / _done_anchors, и смотрел прод-строки, набранные ДО деплоя.

Что есть на самом деле (проверено 27.08)

Чекпоинт живёт в scrape_runs.counters (jsonb) ключом done_buckets, пишется мержем через runs.update_heartbeat (orchestration/runs.py:686,708) — новых записей в БД не добавляет. Решение «продолжить или начать заново» — _resume_decision / _pick_resume (orchestration/scheduler.py:607,672), статусы подхвата {banned, cancelled, failed} (:570), протухание = interval_days + 1 суткиresume_reason='checkpoint_stale'.

Единица возобновления по источникам: бакет бисекции «комнатность × цена» у *_full_load, combo «сегмент/комнатность/цена» у yandex_city_sweep, якорь у avito_city_sweep / cian_city_sweep, корзина у domclick_city_sweep. resume_run_id принимают 7 точек входа в pipeline.py (1137, 2133, 2766, 3333, 3699, 3937, 4221).

Коммиты по этому тикету уже смержены: 05869517 и 68daac2a (26.08, якоря для avito/cian city sweep), ede5653a (27.08, корзины для domclick).

Почему прод показывал ноль. Образ с чекпоинтами задеплоен 27.08 в 19:12 UTC. Все прогоны с done_buckets = 0, на которые я ссылался, стартовали до этого. Первый же прогон после деплоя — yandex_city_sweep_serov, 19:57 UTC — чекпоинт пишет. Код подтверждён внутри работающего контейнера (_done_anchors встречается 6 раз в pipeline.py образа).

Что действительно осталось

  1. avito_newbuilding_sweep не умеет возобновляться вовсе. run_avito_newbuilding_sweep (pipeline.py:1939) — единственный длинный свип без параметра resume_run_id. Цена: 27.08 прогон убит на 343-й минуте, собранное потеряно целиком. Единица напрашивается страничная (pages=20).
  2. Слишком крупная единица у cian_full_load. Один бакет живёт дольше 40 минут: run 4464 отменён на 41-й минуте с done_buckets = 0, run 4999 — 79 минут и тоже ноль. Нужна под-бакетная фиксация ("room:lo:hi#page") либо промежуточная запись недособранного бакета.

Ущерб, ради которого это доделывается (14 суток, heartbeat_at - started_at): avito_city_sweep_nizhniy_tagil 360 мин zombie, avito_city_sweep_kamensk_uralskiy 360 zombie, avito_newbuilding_sweep 343 cancelled, avito_city_sweep_pervouralsk 245 cancelled. За 30 суток потеряно 14 cancelled + 8 zombie, из них 7 — только 27.08.

Курсорные backfill-циклы (WHERE id > last_id, backend/app/services/scheduler.py:244,433) — другой паттерн, вынес отдельно.

Снимаю status/needs-analysis: разбор сделан.

**Поправка к моему комментарию выше — он неверен.** Я написал «чекпоинтов нет, курсор живёт в памяти прогона». Механизм существует и работает; я искал слово `checkpoint`, а реализация называется `done_buckets` / `_done_anchors`, и смотрел прод-строки, набранные ДО деплоя. ## Что есть на самом деле (проверено 27.08) Чекпоинт живёт в `scrape_runs.counters` (jsonb) ключом `done_buckets`, пишется мержем через `runs.update_heartbeat` (`orchestration/runs.py:686,708`) — новых записей в БД не добавляет. Решение «продолжить или начать заново» — `_resume_decision` / `_pick_resume` (`orchestration/scheduler.py:607,672`), статусы подхвата `{banned, cancelled, failed}` (`:570`), протухание = `interval_days + 1 сутки` → `resume_reason='checkpoint_stale'`. Единица возобновления по источникам: бакет бисекции «комнатность × цена» у `*_full_load`, combo «сегмент/комнатность/цена» у `yandex_city_sweep`, якорь у `avito_city_sweep` / `cian_city_sweep`, корзина у `domclick_city_sweep`. `resume_run_id` принимают **7** точек входа в `pipeline.py` (1137, 2133, 2766, 3333, 3699, 3937, 4221). Коммиты по этому тикету уже смержены: `05869517` и `68daac2a` (26.08, якоря для avito/cian city sweep), `ede5653a` (27.08, корзины для domclick). **Почему прод показывал ноль.** Образ с чекпоинтами задеплоен 27.08 в 19:12 UTC. Все прогоны с `done_buckets = 0`, на которые я ссылался, стартовали до этого. Первый же прогон после деплоя — `yandex_city_sweep_serov`, 19:57 UTC — чекпоинт пишет. Код подтверждён внутри работающего контейнера (`_done_anchors` встречается 6 раз в `pipeline.py` образа). ## Что действительно осталось 1. **`avito_newbuilding_sweep` не умеет возобновляться вовсе.** `run_avito_newbuilding_sweep` (`pipeline.py:1939`) — единственный длинный свип без параметра `resume_run_id`. Цена: 27.08 прогон убит на **343-й минуте**, собранное потеряно целиком. Единица напрашивается страничная (`pages=20`). 2. **Слишком крупная единица у `cian_full_load`.** Один бакет живёт дольше 40 минут: run 4464 отменён на 41-й минуте с `done_buckets = 0`, run 4999 — 79 минут и тоже ноль. Нужна под-бакетная фиксация (`"room:lo:hi#page"`) либо промежуточная запись недособранного бакета. Ущерб, ради которого это доделывается (14 суток, `heartbeat_at - started_at`): `avito_city_sweep_nizhniy_tagil` 360 мин zombie, `avito_city_sweep_kamensk_uralskiy` 360 zombie, `avito_newbuilding_sweep` 343 cancelled, `avito_city_sweep_pervouralsk` 245 cancelled. За 30 суток потеряно 14 cancelled + 8 zombie, из них 7 — только 27.08. Курсорные backfill-циклы (`WHERE id > last_id`, `backend/app/services/scheduler.py:244,433`) — другой паттерн, вынес отдельно. Снимаю `status/needs-analysis`: разбор сделан.
bot-backend removed the
status/needs-analysis
label 2026-08-27 20:20:10 +00:00
Author
Collaborator

Поправка к числам в моём комментарии выше. Я мерил длительность как finished_at - started_at. Для cancelled и zombie это неверно: finished_at проставляет финализатор или reap_zombies (orchestration/scheduler.py:397), иногда через часы после реальной смерти прогона. Правильная мера — heartbeat_at - started_at: heartbeat_at ставится при вставке строки (runs.py:583) и обновляется по ходу цикла, а не только на завершении единицы (в avito city sweep вызовов update_heartbeat девять, в newbuilding — два внутри страничного цикла).

Что получилось при правильном замере

source прогонов за 30 сут max, мин avg, мин чекпоинт
cian_full_load 14 449 168 есть
avito_full_load 9 197 32 есть
avito_full_load_exhaustive 4 172 98 есть
avito_detail_backfill 71 150 13 нет
house_imv_backfill 13 82 26 нет
cian_history_backfill 18 79 45 нет
cian_city_sweep 31 68 33 есть
yandex_city_sweep 30 68 37 есть
yandex_detail_backfill 29 60 40 нет
geocode_missing_listings 30 53 14 нет
avito_newbuilding_sweep 30 27 8 нет

Снимаю два своих утверждения:

  • «avito_newbuilding_sweep убит на 343-й минуте» — неверно. Тот прогон 27.08 прожил 0 минут (heartbeat ни разу не сдвинулся), а 343 — отметка финализатора. За 30 суток этот свип ни разу не жил дольше 27 минут при среднем 8. Он остаётся единственным свипом без resume_run_id, но это вопрос полноты покрытия, а не потерь: приоритет P1 он не заслуживает.
  • «zombie-прогоны 27.08 прожили 360 минут» — неверно, все прожили 0 минут. Семь потерь 27.08 — это не убитые деплоем длинные прогоны, а прогоны, которые в день переезда не смогли стартовать вовсе. Отдельный разбор, к чекпоинтам отношения не имеет.

Куда на самом деле сместился приоритет. Все пять источников без чекпоинтов — курсорные backfill-циклы, и среди них avito_detail_backfill с максимумом 150 минут и 71 прогоном за месяц. Это #3168, и по числам он теперь важнее остатка здесь.

Остаток #3074 после поправки: под-бакетная гранулярность cian_full_load (449 минут максимум, один бакет живёт дольше 40 минут — run 4464 отменён на 41-й минуте с done_buckets = 0) и resume_run_id для avito_newbuilding_sweep как завершение покрытия.

**Поправка к числам в моём комментарии выше.** Я мерил длительность как `finished_at - started_at`. Для `cancelled` и `zombie` это неверно: `finished_at` проставляет финализатор или `reap_zombies` (`orchestration/scheduler.py:397`), иногда через часы после реальной смерти прогона. Правильная мера — `heartbeat_at - started_at`: `heartbeat_at` ставится при вставке строки (`runs.py:583`) и обновляется по ходу цикла, а не только на завершении единицы (в avito city sweep вызовов `update_heartbeat` девять, в newbuilding — два внутри страничного цикла). ## Что получилось при правильном замере | source | прогонов за 30 сут | max, мин | avg, мин | чекпоинт | |---|---|---|---|---| | `cian_full_load` | 14 | **449** | 168 | есть | | `avito_full_load` | 9 | 197 | 32 | есть | | `avito_full_load_exhaustive` | 4 | 172 | 98 | есть | | `avito_detail_backfill` | 71 | **150** | 13 | **нет** | | `house_imv_backfill` | 13 | 82 | 26 | **нет** | | `cian_history_backfill` | 18 | 79 | 45 | **нет** | | `cian_city_sweep` | 31 | 68 | 33 | есть | | `yandex_city_sweep` | 30 | 68 | 37 | есть | | `yandex_detail_backfill` | 29 | 60 | 40 | **нет** | | `geocode_missing_listings` | 30 | 53 | 14 | **нет** | | `avito_newbuilding_sweep` | 30 | **27** | 8 | нет | **Снимаю два своих утверждения:** - «`avito_newbuilding_sweep` убит на 343-й минуте» — неверно. Тот прогон 27.08 прожил **0 минут** (heartbeat ни разу не сдвинулся), а 343 — отметка финализатора. За 30 суток этот свип ни разу не жил дольше **27 минут** при среднем 8. Он остаётся единственным свипом без `resume_run_id`, но это вопрос полноты покрытия, а не потерь: приоритет P1 он не заслуживает. - «zombie-прогоны 27.08 прожили 360 минут» — неверно, все прожили **0 минут**. Семь потерь 27.08 — это не убитые деплоем длинные прогоны, а прогоны, которые в день переезда не смогли стартовать вовсе. Отдельный разбор, к чекпоинтам отношения не имеет. **Куда на самом деле сместился приоритет.** Все пять источников без чекпоинтов — курсорные backfill-циклы, и среди них `avito_detail_backfill` с максимумом 150 минут и 71 прогоном за месяц. Это #3168, и по числам он теперь важнее остатка здесь. Остаток #3074 после поправки: под-бакетная гранулярность `cian_full_load` (449 минут максимум, один бакет живёт дольше 40 минут — run 4464 отменён на 41-й минуте с `done_buckets = 0`) и `resume_run_id` для `avito_newbuilding_sweep` как завершение покрытия.
Owner

Ревизия открытых задач 2026-08-30. Проверено в коде на forgejo/main — сделано, закрываю.

Чекпоинты приземлены по всем свипам: tests/test_3074_avito_anchor_checkpoint.py, test_3074_avito_newbuilding_checkpoint.py, test_3074_cian_anchor_checkpoint.py, test_3074_yandex_sweep_checkpoint.py, test_3074_checkpoint_survives_claim.py.

Ревизия открытых задач 2026-08-30. Проверено в коде на forgejo/main — сделано, закрываю. Чекпоинты приземлены по всем свипам: tests/test_3074_avito_anchor_checkpoint.py, test_3074_avito_newbuilding_checkpoint.py, test_3074_cian_anchor_checkpoint.py, test_3074_yandex_sweep_checkpoint.py, test_3074_checkpoint_survives_claim.py.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#3074
No description provided.