fix(tradein/scraper): сторож «ноль результатов» перестаёт читать «не измерено» как ноль (#2703) #2713

Merged
bot-backend merged 1 commit from fix/2703-zero-result-watchdog-blind into main 2026-08-06 08:29:00 +00:00
Collaborator

Summary

Сторож смотрел колонку scrape_runs.total_seen, у которой DEFAULT 0. Замер по всем 53 источникам (прод, 2026-08-06):

Видимость для сторожа Источников Прогонов
видит (lots_fetched) 21 455
не видел, начинает видеть (unique_fetched — full-load'ы avito/cian/yandex) 4 133
результатного ключа нет вовсе 28 2650

total_seen как ключ counters не пишет ни один источник за всю историю — колонку заполняет _column_counts из lots_fetched.

Полный список 28 слепых источников (по числу прогонов): proxy_healthcheck 1650, avito_detail_backfill 76, deactivate_stale_avito 62, rosreestr_dkp_import 58, asking_to_sold_ratio_refresh 56, refresh_search_matview 55, newbuilding_enrich 53, geocode_missing_listings 53, yandex_detail_backfill 52, deactivate_stale_yandex 51, deactivate_stale_cian 51, yandex_address_backfill 50, cadastral_geo_match 49, yandex_newbuilding_sweep 41, house_imv_backfill 39, deals_freshness_monitor 36, geoportal_coords_backfill 35, osm_poi_ekb_refresh 35, domclick_detail_backfill 30, deactivate_stale_domklik 26, sber_freshness_monitor 24, listing_source_snapshot 22, cian_history_backfill 18, sber_index_pull 9, deal_city_price_bands_refresh 6, house_dedup_merge 6, rosreestr_quarter_poll 6, deactivate_stale_n1 1.

Для них сторож читал 0 всегда, в том числе у полностью успешного прогона → стрик не прерывался никогда → анти-спам «один раз на стрик» превращался в «один раз навсегда»: сработав однажды на третьем прогоне источника, сторож замолкал навсегда, продолжая выглядеть настроенным. Задача в issue называла три backfill'а — на деле задето 28 источников из 53.

Решение: не «свести счётчики» и не «выучить словари»

Свести счётчики к total_seen невозможно и вредно: у 28 источников 28 разных словарей (deactivated / rows_written / poi_loaded / snapshotted / upserted / listings_matched …), у refresh_search_matview counters пусты буквально ({} во всех 55 строках), у трёх мониторов результата нет по смыслу, а ноль у deactivate_stale_*здоровый ответ. Отображение total_seen ↔ attempted было бы прямо вредным: 1600 попыток с нулём обогащений читались бы как «результат 1600».

Вместо этого:

  • признак берётся из counters, а не из колонки — «не измерено» (None) отличимо от «измерено, ноль»;
  • прогон без результатной метрики стрик ПРЕРЫВАЕТ → недостижимое условие прерывания стало невозможным по построению, а не по корректности счётчиков (требование 3);
  • слепая зона логируется явно (_warn_source_has_no_result_metric, раз на источник за процесс) вместо молчаливого нуля;
  • в словарь добавлен unique_fetched — 4 full-load'а входят в зону видимости (у cian_full_load 6 из 38 успешных прогонов дали настоящий ноль) и перестают показывать total_seen=0 в админке.

Зеро-прогоны backfill'ов при этом не остаются без присмотра: #2695 (mark_backfill_finished) уже переводит их в failed/banned, где их видит _alert_if_consecutive_failures.

Test plan

  • tests/test_2703_zero_result_watchdog.py — 28 тестов на обеих копиях runs-модуля (kit + app): различие None/0, слепой источник не копит стрик и логирует, немеряный прогон прерывает стрик, настоящий нулевой стрик по-прежнему алертит (lots_fetched и unique_fetched), анти-спам не сломан.
  • Фальсификация: при возврате семантики «отсутствие == ноль» ((_run_result_count(...) or 0) == 0 + снятие guard'а) падают 4 ключевых теста на обеих копиях — ровно то поведение, что было на main.
  • Обновлены фикстуры tests/test_scrape_run_alert.py (#2625): строки теперь несут counters, а не только колонку.
  • Полный прогон: 3612 passed, 1 deselected (pre-existing test_search_cache_hit).
  • Правленый путь tradein-mvp/packages/scraper-kit/** входит в scraper-allowlist deploy-tradein.yml → правка доедет до tradein-scraper. (app/services/scrape_runs.py в allowlist НЕ входит — сам по себе он до скрапера не доехал бы.)

Чего проверить нельзя

Поведенческая прод-верификация невозможна: получатель уведомлений в GlitchTip не настроен (#2673) — правил 0, получателей 0, отправлено 0 за всю историю при 2861 накопленной группе ошибок. Событие создастся и никуда не уйдёт. Проверяется код в живом контейнере + тесты.

Refs #2703

## Summary Сторож смотрел колонку `scrape_runs.total_seen`, у которой `DEFAULT 0`. Замер по всем 53 источникам (прод, 2026-08-06): | Видимость для сторожа | Источников | Прогонов | |---|---:|---:| | видит (`lots_fetched`) | 21 | 455 | | **не видел, начинает видеть** (`unique_fetched` — full-load'ы avito/cian/yandex) | 4 | 133 | | **результатного ключа нет вовсе** | 28 | 2650 | `total_seen` как ключ counters не пишет **ни один** источник за всю историю — колонку заполняет `_column_counts` из `lots_fetched`. Полный список 28 слепых источников (по числу прогонов): `proxy_healthcheck` 1650, `avito_detail_backfill` 76, `deactivate_stale_avito` 62, `rosreestr_dkp_import` 58, `asking_to_sold_ratio_refresh` 56, `refresh_search_matview` 55, `newbuilding_enrich` 53, `geocode_missing_listings` 53, `yandex_detail_backfill` 52, `deactivate_stale_yandex` 51, `deactivate_stale_cian` 51, `yandex_address_backfill` 50, `cadastral_geo_match` 49, `yandex_newbuilding_sweep` 41, `house_imv_backfill` 39, `deals_freshness_monitor` 36, `geoportal_coords_backfill` 35, `osm_poi_ekb_refresh` 35, `domclick_detail_backfill` 30, `deactivate_stale_domklik` 26, `sber_freshness_monitor` 24, `listing_source_snapshot` 22, `cian_history_backfill` 18, `sber_index_pull` 9, `deal_city_price_bands_refresh` 6, `house_dedup_merge` 6, `rosreestr_quarter_poll` 6, `deactivate_stale_n1` 1. Для них сторож читал 0 **всегда**, в том числе у полностью успешного прогона → стрик не прерывался никогда → анти-спам «один раз на стрик» превращался в «один раз навсегда»: сработав однажды на третьем прогоне источника, сторож замолкал навсегда, продолжая выглядеть настроенным. Задача в issue называла три backfill'а — на деле задето 28 источников из 53. ### Решение: не «свести счётчики» и не «выучить словари» Свести счётчики к `total_seen` невозможно и вредно: у 28 источников 28 разных словарей (`deactivated` / `rows_written` / `poi_loaded` / `snapshotted` / `upserted` / `listings_matched` …), у `refresh_search_matview` counters пусты буквально (`{}` во всех 55 строках), у трёх мониторов результата нет по смыслу, а ноль у `deactivate_stale_*` — **здоровый** ответ. Отображение `total_seen ↔ attempted` было бы прямо вредным: 1600 попыток с нулём обогащений читались бы как «результат 1600». Вместо этого: - признак берётся из `counters`, а не из колонки — «не измерено» (`None`) отличимо от «измерено, ноль»; - **прогон без результатной метрики стрик ПРЕРЫВАЕТ** → недостижимое условие прерывания стало невозможным по построению, а не по корректности счётчиков (требование 3); - слепая зона логируется явно (`_warn_source_has_no_result_metric`, раз на источник за процесс) вместо молчаливого нуля; - в словарь добавлен `unique_fetched` — 4 full-load'а входят в зону видимости (у `cian_full_load` 6 из 38 успешных прогонов дали настоящий ноль) и перестают показывать `total_seen=0` в админке. Зеро-прогоны backfill'ов при этом не остаются без присмотра: #2695 (`mark_backfill_finished`) уже переводит их в `failed`/`banned`, где их видит `_alert_if_consecutive_failures`. ## Test plan - [x] `tests/test_2703_zero_result_watchdog.py` — 28 тестов на обеих копиях runs-модуля (kit + app): различие None/0, слепой источник не копит стрик и логирует, немеряный прогон прерывает стрик, настоящий нулевой стрик по-прежнему алертит (`lots_fetched` и `unique_fetched`), анти-спам не сломан. - [x] Фальсификация: при возврате семантики «отсутствие == ноль» (`(_run_result_count(...) or 0) == 0` + снятие guard'а) падают 4 ключевых теста на обеих копиях — ровно то поведение, что было на `main`. - [x] Обновлены фикстуры `tests/test_scrape_run_alert.py` (#2625): строки теперь несут `counters`, а не только колонку. - [x] Полный прогон: 3612 passed, 1 deselected (pre-existing `test_search_cache_hit`). - [x] Правленый путь `tradein-mvp/packages/scraper-kit/**` входит в scraper-allowlist `deploy-tradein.yml` → правка доедет до `tradein-scraper`. (`app/services/scrape_runs.py` в allowlist НЕ входит — сам по себе он до скрапера не доехал бы.) ## Чего проверить нельзя Поведенческая прод-верификация невозможна: получатель уведомлений в GlitchTip не настроен (#2673) — правил `0`, получателей `0`, отправлено `0` за всю историю при 2861 накопленной группе ошибок. Событие создастся и никуда не уйдёт. Проверяется код в живом контейнере + тесты. Refs #2703
bot-backend added 1 commit 2026-08-06 08:24:49 +00:00
fix(tradein/scraper): сторож «ноль результатов» перестаёт читать «не измерено» как ноль (#2703)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 3m5s
8fd58aae8b
Сторож смотрел колонку scrape_runs.total_seen, у которой DEFAULT 0. Замер на
проде (53 источника): колонку заполняет только lots_fetched — 21 источник, 455
прогонов. Ещё 4 источника (133 прогона, full-load'ы avito/cian/yandex) пишут
unique_fetched, которого сторож не знал, хотя у cian_full_load 6 из 38 успешных
прогонов дали настоящий ноль. Оставшиеся 28 источников (2650 прогонов) не пишут
ни одного результатного ключа вовсе: proxy_healthcheck 1650, три detail-
backfill'а, все deactivate_stale_*, refresh_search_matview с буквально пустыми
{} во всех 55 строках.

Для этих 28 сторож читал 0 ВСЕГДА, в том числе у полностью успешного прогона →
стрик не прерывался никогда → анти-спам «один раз на стрик» превращался в «один
раз навсегда»: сработав однажды на третьем прогоне источника, сторож замолкал
навсегда, продолжая выглядеть настроенным.

Свести счётчики к словарю сторожа невозможно: у 28 источников 28 разных
словарей (deactivated / rows_written / poi_loaded / snapshotted / upserted /
listings_matched …), у трёх мониторов результата нет по смыслу, а ноль у
deactivate_stale_* — здоровый ответ. Поэтому:

- признак берётся из counters, а не из колонки: «не измерено» (None) отличимо
  от «измерено, ноль»;
- прогон без результатной метрики стрик ПРЕРЫВАЕТ — недостижимое условие
  прерывания стало невозможным по построению, а не по корректности счётчиков;
- слепая зона логируется явно (раз на источник за процесс), а не молчит;
- в словарь добавлен unique_fetched — 4 full-load'а входят в зону видимости
  сторожа и перестают показывать total_seen=0 в админке.

Доставка уведомлений этим НЕ чинится: получатель в GlitchTip не настроен (#2673),
за всю историю наружу не ушло ни одного события.

Refs #2703
bot-backend merged commit 396d8a74b6 into main 2026-08-06 08:29:00 +00:00
bot-backend deleted branch fix/2703-zero-result-watchdog-blind 2026-08-06 08:29:00 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2713
No description provided.