diff --git a/tradein-mvp/backend/app/services/scrape_runs.py b/tradein-mvp/backend/app/services/scrape_runs.py index 6fca052d..10b762f1 100644 --- a/tradein-mvp/backend/app/services/scrape_runs.py +++ b/tradein-mvp/backend/app/services/scrape_runs.py @@ -391,7 +391,20 @@ def _column_counts(counters: dict[str, int]) -> tuple[int | None, int | None]: Возвращает (total_seen, new_count); None для ключа, которого нет в counters — тогда соответствующая колонка не перезаписывается (COALESCE-семантика в UPDATE). """ - return _run_result_count(counters), _pick_int( + # #3044: detail-бэкфиллы (avito/yandex/domclick_detail_backfill) пишут свой + # результат ТОЛЬКО ключом 'enriched' — колонка total_seen у них оставалась 0 + # навсегда, и замер в issue прочитал «обогащено 0 за 7 дней» при реальных 801 + # (listings.detail_enriched_at). 'enriched' — фолбэк ИМЕННО ЗДЕСЬ, а не в + # _RESULT_COUNTER_KEYS: тот список кормит ещё и zero-result-стрик, где + # «догнавший очередь» бэкфилл (attempted=0, enriched=0) стал бы измеренным + # нулём и копил бы непрерываемый стрик — ровно та ловушка, за которую ревью + # выкинуло из списка голый 'rows_inserted' (см. test_rosreestr_dkp_import_ + # healthy_zero_stays_unmeasured). Витринной колонке фолбэк безопасен: она не + # участвует в стриках (#2703 читает counters, не колонку). + total_seen = _run_result_count(counters) + if total_seen is None: + total_seen = _pick_int(counters, "enriched") + return total_seen, _pick_int( counters, "new_count", "lots_inserted", "saved_inserted", "rows_inserted" ) diff --git a/tradein-mvp/backend/tests/test_3044_enriched_result_key.py b/tradein-mvp/backend/tests/test_3044_enriched_result_key.py new file mode 100644 index 00000000..b4941329 --- /dev/null +++ b/tradein-mvp/backend/tests/test_3044_enriched_result_key.py @@ -0,0 +1,59 @@ +"""#3044 (наблюдаемость): результат detail-бэкфиллов виден колонке total_seen. + +Прод-факт (Poincare, 26.08.2026): avito_detail_backfill за 7 дней — 36 прогонов, +sum(total_seen)=0, из чего замер в issue сделал вывод «обогащено 0». Реально: +counters={'enriched': 61, ...} у прогона 25.08 09:18, а listings.detail_enriched_at +насчитал 801 обогащение за те же 7 дней. Джобы пишут результат ТОЛЬКО ключом +'enriched', которого _column_counts не знал — колонка не заполнялась ни у done, +ни у banned прогонов, и «провал и успех писали один и тот же признак» (ноль). + +Фикс: 'enriched'-фолбэк в _column_counts (обе копии: app и scraper-kit) — только +для витринной колонки. В _RESULT_COUNTER_KEYS ключ НЕ добавлен намеренно: тот +список кормит zero-result-стрик, где догнавший очередь бэкфилл (attempted=0) +стал бы непрерываемым измеренным нулём — ловушка, за которую ревью уже выкинуло +из списка 'rows_inserted' (#2703, test_rosreestr_dkp_import_healthy_zero_*). + +Тесты колонки красные на origin/main по ЗНАЧЕНИЮ (None вместо 61), не по импорту. +""" + +from __future__ import annotations + +from scraper_kit.orchestration import runs as kit_runs + +from app.services import scrape_runs as app_runs + +# Прод-прогон avito_detail_backfill 25.08 09:18 UTC (баном оборван, но 61 обогащён). +_AVITO_BANNED_RUN = { + "gone": 0, + "failed": 0, + "blocked": 39, + "enriched": 61, + "attempted": 100, + "duration_sec": 1600, +} + + +def test_column_counts_fills_total_seen_for_detail_backfill() -> None: + """_column_counts — ровно тот путь, которым mark_done/mark_banned пишут + выделенные колонки. До фикса: (None, None) → total_seen вечно 0.""" + for mod in (app_runs, kit_runs): + total_seen, new_count = mod._column_counts(_AVITO_BANNED_RUN) + assert total_seen == 61 + assert new_count is None # ключей new_count/lots_inserted/... у бэкфилла нет + + +def test_result_keys_win_over_enriched_fallback() -> None: + """У newbuilding_enrich рядом с 'enriched' лежит 'succeeded' — приоритет за ним: + поведение источников с настоящим result-ключом не меняется.""" + for mod in (app_runs, kit_runs): + total_seen, _ = mod._column_counts({"succeeded": 7, "enriched": 3}) + assert total_seen == 7 + + +def test_zero_streak_watchdog_semantics_untouched() -> None: + """Контракт #2703 сохранён: для стрик-сторожа 'enriched' НЕ результатная + метрика — догнавший очередь бэкфилл остаётся «не измерено» и стрик не копит. + Этот тест зелёный в обеих эрах (инвариант, а не фикс).""" + for mod in (app_runs, kit_runs): + assert mod._run_result_count({"attempted": 1600, "enriched": 0, "failed": 1600}) is None + assert mod._run_result_count({"enriched": 61, "attempted": 100}) is None diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/runs.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/runs.py index 725b1613..3ebebdbd 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/runs.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/runs.py @@ -391,7 +391,20 @@ def _column_counts(counters: dict[str, int]) -> tuple[int | None, int | None]: Возвращает (total_seen, new_count); None для ключа, которого нет в counters — тогда соответствующая колонка не перезаписывается (COALESCE-семантика в UPDATE). """ - return _run_result_count(counters), _pick_int( + # #3044: detail-бэкфиллы (avito/yandex/domclick_detail_backfill) пишут свой + # результат ТОЛЬКО ключом 'enriched' — колонка total_seen у них оставалась 0 + # навсегда, и замер в issue прочитал «обогащено 0 за 7 дней» при реальных 801 + # (listings.detail_enriched_at). 'enriched' — фолбэк ИМЕННО ЗДЕСЬ, а не в + # _RESULT_COUNTER_KEYS: тот список кормит ещё и zero-result-стрик, где + # «догнавший очередь» бэкфилл (attempted=0, enriched=0) стал бы измеренным + # нулём и копил бы непрерываемый стрик — ровно та ловушка, за которую ревью + # выкинуло из списка голый 'rows_inserted' (см. test_rosreestr_dkp_import_ + # healthy_zero_stays_unmeasured). Витринной колонке фолбэк безопасен: она не + # участвует в стриках (#2703 читает counters, не колонку). + total_seen = _run_result_count(counters) + if total_seen is None: + total_seen = _pick_int(counters, "enriched") + return total_seen, _pick_int( counters, "new_count", "lots_inserted", "saved_inserted", "rows_inserted" )