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

Ревью опровергло формулировку «точку терял каждый финализатор»: все четыре
писателя в 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) — метка дрейна не
меняет вердикт «нечего подхватывать».
This commit is contained in:
bot-backend 2026-09-02 14:53:02 +05:00
parent 0ed934ea09
commit 41f21c4969
2 changed files with 29 additions and 12 deletions

View file

@ -1,11 +1,15 @@
"""Чекпоинт avito_city_sweep доживает до финализатора (#3319).
Прод-факт, из которого выросла задача: 0 из 67 прогонов за 60 дней имеют в
counters ключ done_buckets. Механизм #3074 (запись точки) и механизм #930
(подхват точки) существуют оба, но между ними нет ни одного прогона: точку
писала ровно одна строка внутри цикла якорей, а КАЖДЫЙ выход из прогона
(mark_done включая ранний выход #1950 «SERP собран, detail заблокирован», —
mark_banned, mark_failed) отдавал голый counters.to_dict() без неё.
Точку писала ровно одна строка end-of-anchor heartbeat в конце итерации цикла
якорей. Финализаторы её не стирали (все писатели в runs.py мержат jsonb:
`counters || :counters`), дыра в другом: выходы, случившиеся РАНЬШЕ первой такой
записи, точки не оставляли вовсе cancel/SIGTERM-дрейн на границе первого якоря
и ранний done #1950 («SERP собран, detail заблокирован») на якоре №1. Ими и
кончается типичный прод-прогон с `anchors_done: 1` из 5.
Замер «0 из 67 прогонов за 60 дней несут done_buckets» тут НЕ доказательство:
строка записи появилась только 26.08.2026 (#3074) при такте avito 7 суток —
выборка почти целиком из эры, где механизма не существовало.
Три инварианта, ради которых тест:
1. done-выход несёт done_buckets иначе точка существует только в логе.
@ -212,3 +216,10 @@ def test_drained_done_is_resumable_but_clean_done_is_not() -> None:
resume_from, verdict = _resume_decision(_prev_run(ckpt))
assert resume_from is None, "полный обход подхватывать нечего"
assert verdict["resume_reason"] == "status_done"
# Прогон из эры до #3074: ключей нет вовсе — метка дрейна не должна менять
# вердикт «нечего подхватывать» на что-то другое.
_, verdict = _resume_decision(_prev_run({}))
assert verdict["resume_reason"] == "status_done"
_, verdict = _resume_decision(_prev_run({"interrupted": 1}))
assert verdict["resume_reason"] == "no_checkpoint"

View file

@ -1186,12 +1186,18 @@ async def run_avito_city_sweep(
def _ckpt(**extra: Any) -> dict[str, Any]:
"""Счётчики прогона ВМЕСТЕ с чекпоинтом — payload любого выхода (#3319).
До этого точку писала ровно одна строка внутри цикла якорей, а все
финализаторы (mark_done/mark_banned/mark_failed, включая ранний выход
#1950 «SERP OK, detail заблокирован») отдавали голый `counters.to_dict()`.
Точка держалась исключительно на jsonb-мерже в runs.py на свойстве
ЧУЖОГО модуля, которого этот файл ничем не проверяет; выход, случившийся
раньше первой записи (или писатель без мержа), терял её молча.
До этого точку писала ровно одна строка end-of-anchor heartbeat в конце
итерации цикла. Финализаторы её НЕ стирали: все четыре писателя в runs.py
мержат jsonb (`counters || :counters`), уже записанный ключ переживал и
mark_done, и mark_banned, и mark_failed. Закрывается другая дыра выходы,
случившиеся РАНЬШЕ первой такой записи: cancel/SIGTERM-дрейн на границе
первого якоря и ранний done #1950 («SERP собран, detail заблокирован») на
якоре 1. Именно им и кончается типичный прод-прогон, у которого
`anchors_done: 1` из 5.
Замер «0 из 67 прогонов за 60 дней несут done_buckets» сам по себе этого НЕ
доказывает: строка записи появилась только 26.08.2026 (#3074), а такт avito
7 суток, так что выборка почти целиком из эры, где механизма не было.
"""
return {**counters.to_dict(), "done_buckets": sorted(_done_anchors), **extra}