From 41f21c4969c63c8125267f45c441c8c98f713ce2 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 2 Sep 2026 14:53:02 +0500 Subject: [PATCH] =?UTF-8?q?docs(scraper-kit):=20=D0=BE=D0=B1=D0=BE=D1=81?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20#3319=20=E2=80=94?= =?UTF-8?q?=20=D1=87=D1=82=D0=BE=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=BD=D0=BE=20?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D1=8F=D0=BB=D0=BE=20=D1=87=D0=B5=D0=BA=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ревью опровергло формулировку «точку терял каждый финализатор»: все четыре писателя в 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) — метка дрейна не меняет вердикт «нечего подхватывать». --- .../tests/test_3319_citysweep_checkpoint.py | 23 ++++++++++++++----- .../src/scraper_kit/orchestration/pipeline.py | 18 ++++++++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/tradein-mvp/backend/tests/test_3319_citysweep_checkpoint.py b/tradein-mvp/backend/tests/test_3319_citysweep_checkpoint.py index 6ae05a73..a238119b 100644 --- a/tradein-mvp/backend/tests/test_3319_citysweep_checkpoint.py +++ b/tradein-mvp/backend/tests/test_3319_citysweep_checkpoint.py @@ -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" diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py index 2cbacf9c..654927ef 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py @@ -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}