fix(tradein/observability): результат detail-бэкфиллов доезжает до total_seen (#3044) #3096

Merged
bot-backend merged 1 commit from fix/3044-enriched-total-seen into main 2026-08-26 07:30:23 +00:00
3 changed files with 87 additions and 2 deletions

View file

@ -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"
)

View file

@ -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

View file

@ -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"
)