fix(scraper-kit): чекпоинт avito_city_sweep доживает до финализатора — 0/67 прогонов писали done_buckets #3330

Merged
bot-backend merged 2 commits from fix/3319-citysweep-checkpoint into main 2026-09-05 17:36:51 +00:00
Collaborator

Closes #3319. Аудит 01-02.09, линза kit-core.

П.1 (high) — чекпоинт не писался вообще

Прод-факт: 0 из 67 прогонов за 60 дней записали done_buckets; done-ветка #1950 завершала прогон ДО единственной точки записи. Фикс: локальная _ckpt(**extra) (counters + done_buckets), через неё пропущены все 14 записей счётчиков функции — heartbeat'ы, оба mark_done (включая ранний выход #1950), mark_banned, mark_failed. Правилась не «та ветка из тикета», а точка, через которую ходят все выходы.

П.2 (medium) — таймаут-якорь числился пройденным

except TimeoutError не сбрасывал _anchor_ok (generic except сбрасывал) → якорь-таймаут ложился в done_buckets и при резюме пропускался навсегда. Добавлен сброс — семантика выровнена.

П.3 (low) — SIGTERM-drain неотличим от полного done

Статусную модель не трогали (~30 читателей status='done'). Взята уже существующая в репо метка дрейна counters['interrupted']=1 (как у rosreestr_dkp). Резюм подхватывает done ТОЛЬКО с этой меткой ('done' в _RESUME_STATUSES не добавлен). Дрейн перед IMV-фазой помечен отдельно imv_phase_drained=1 без резюма — якоря там уже пройдены, подхват собрал бы ноль и спалил цикл расписания.

Тесты

Новый test_3319_citysweep_checkpoint.py: 5 passed. Соседи: 383 passed, 1 skipped (sweep/scheduler/checkpoint/resume/parity). Фальсификация двойная:

  • A — откат всего диффа: все 5 красные по значению (counters без чекпоинта — assert None == ['ekb-center', 'ekb-south'], resume_reason: 'status_done' вместо подхвата);
  • B — снята одна строка _anchor_ok = False (в прогоне A п.2 краснел бы по чужой причине): якорь-таймаут попал в чекпоинт — assert 'ekb-center' not in [...], остальные 4 зелёные.

Прод-приёмка после деплоя (сделаю отдельно)

SELECT counters ? 'done_buckets', counters->>'interrupted' FROM scrape_runs WHERE source LIKE 'avito_city_sweep%' ORDER BY id DESC LIMIT 5 — до правки ключа не было ни в одном из 67 прогонов, первый новый прогон различает эры.

Осталось за рамками (честно)

  • domclick-пункт (attempted≠enriched+failed+blocked) — не влез в бюджет, отдельный заход;
  • дрейн-метка у yandex/cian/newbuilding — механизм в планировщике общий, дописать дёшево, но это другие провайдеры (follow-up).
Closes #3319. Аудит 01-02.09, линза kit-core. ## П.1 (high) — чекпоинт не писался вообще Прод-факт: 0 из 67 прогонов за 60 дней записали `done_buckets`; done-ветка #1950 завершала прогон ДО единственной точки записи. Фикс: локальная `_ckpt(**extra)` (`counters + done_buckets`), через неё пропущены **все 14 записей счётчиков** функции — heartbeat'ы, оба `mark_done` (включая ранний выход #1950), `mark_banned`, `mark_failed`. Правилась не «та ветка из тикета», а точка, через которую ходят все выходы. ## П.2 (medium) — таймаут-якорь числился пройденным `except TimeoutError` не сбрасывал `_anchor_ok` (generic except сбрасывал) → якорь-таймаут ложился в `done_buckets` и при резюме пропускался навсегда. Добавлен сброс — семантика выровнена. ## П.3 (low) — SIGTERM-drain неотличим от полного done Статусную модель не трогали (~30 читателей `status='done'`). Взята уже существующая в репо метка дрейна `counters['interrupted']=1` (как у rosreestr_dkp). Резюм подхватывает `done` ТОЛЬКО с этой меткой (`'done'` в `_RESUME_STATUSES` не добавлен). Дрейн перед IMV-фазой помечен отдельно `imv_phase_drained=1` без резюма — якоря там уже пройдены, подхват собрал бы ноль и спалил цикл расписания. ## Тесты Новый `test_3319_citysweep_checkpoint.py`: `5 passed`. Соседи: `383 passed, 1 skipped` (sweep/scheduler/checkpoint/resume/parity). Фальсификация двойная: - **A** — откат всего диффа: все 5 красные по значению (`counters без чекпоинта — assert None == ['ekb-center', 'ekb-south']`, `resume_reason: 'status_done'` вместо подхвата); - **B** — снята одна строка `_anchor_ok = False` (в прогоне A п.2 краснел бы по чужой причине): `якорь-таймаут попал в чекпоинт — assert 'ekb-center' not in [...]`, остальные 4 зелёные. ## Прод-приёмка после деплоя (сделаю отдельно) `SELECT counters ? 'done_buckets', counters->>'interrupted' FROM scrape_runs WHERE source LIKE 'avito_city_sweep%' ORDER BY id DESC LIMIT 5` — до правки ключа не было ни в одном из 67 прогонов, первый новый прогон различает эры. ## Осталось за рамками (честно) - domclick-пункт (attempted≠enriched+failed+blocked) — не влез в бюджет, отдельный заход; - дрейн-метка у yandex/cian/newbuilding — механизм в планировщике общий, дописать дёшево, но это другие провайдеры (follow-up).
bot-backend added 1 commit 2026-09-02 09:45:58 +00:00
fix(scraper-kit): чекпоинт avito_city_sweep доживает до финализатора (#3319)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
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 5m30s
0ed934ea09
0 из 67 прогонов за 60 дней имели done_buckets в counters: точку писала одна
строка внутри цикла якорей, а каждый выход (mark_done — включая ранний #1950
«SERP собран, detail заблокирован», — mark_banned, mark_failed) отдавал голый
counters.to_dict(). Точка держалась только на jsonb-мерже в runs.py, то есть на
свойстве чужого модуля, которого этот файл не проверяет.

- payload любого выхода собирается одной функцией _ckpt() — done_buckets несут
  все 14 записей, а не одна;
- якорь, умерший по таймауту, больше не считается пройденным (тот же инвариант,
  что у generic-except): SERP мог успеть, detail нет, и резюм пропускал такой
  якорь навсегда при штатно завершившемся прогоне;
- SIGTERM-дрейн помечается counters.interrupted=1 и участвует в резюме. Статус
  остаётся 'done' — ни один читатель статуса не меняется; метка та же, что у
  rosreestr_dkp-дрейна. 'done' в _RESUME_STATUSES НЕ добавлен: чистый полный
  обход резюмить нечего. Дрейн перед IMV-фазой помечен отдельно
  (imv_phase_drained) — якоря там пройдены все, подхват собрал бы ноль.
Light1YT added 1 commit 2026-09-02 09:53:11 +00:00
docs(scraper-kit): обоснование #3319 — что именно теряло чекпоинт
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 5m13s
41f21c4969
Ревью опровергло формулировку «точку терял каждый финализатор»: все четыре
писателя в runs.py мержат jsonb (`counters || :counters`), записанный ключ
переживал mark_done/mark_banned/mark_failed. Правка закрывает выходы РАНЬШЕ
первого end-of-anchor heartbeat (cancel/дрейн на первом якоре, ранний done
#1950 на якоре №1) — комментарий и докстринг переписаны на это.

Замер «0 из 67 за 60 дней» назван тем, чем он является: запись появилась
26.08.2026 (#3074) при такте avito 7 суток, выборка почти вся из эры без
механизма. Плюс тест на прогон без ключей (эра до #3074) — метка дрейна не
меняет вердикт «нечего подхватывать».
bot-backend merged commit 64ec4a1aad into main 2026-09-05 17:36:51 +00:00
Sign in to join this conversation.
No reviewers
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#3330
No description provided.