fix(scraper-kit): свежесть источника — по последнему прогону, принёсшему данные, а не по статусу done #3365

Merged
bot-backend merged 1 commit from fix/3172-freshness-by-data into main 2026-09-05 19:10:14 +00:00
Collaborator

Closes #3172 (observability p2; породил ложный P1 #3118 «домклик мёртв с 05.08»). _STALE_SOURCES_SQL считал свежесть по max(finished_at) прогонов done; прогон с блоками честно переводится в banned (#2657) и при этом вставляет строки.

Мера «прогон дал данные»

runs._RESULT_COUNTER_KEYS (total_seen/lots_fetched/unique_fetched/succeeded) — тот же словарь, которым уже судит сторож нулевого результата (#2703): одна мера на оба механизма, а не второй список, который разъедется. Не lots_inserted — это новизна, а не наличие данных: здоровый sweep с полностью дедуплицированной выдачей вставляет ноль и читался бы мёртвым (#3118 наизнанку). Результат не измерен (_run_result_count() is None — 28 источников без результатного ключа: refresh_search_matview, deactivate_stale_*, мониторы) → прежняя мера, статус done: иначе «не измерено» схлопнулось бы с «ноль» и все они разом стали бы просроченными навсегда. never_ok — той же функцией.

Отдельная колонка-отметка в финализаторе отвергнута: миграция + по построению не работает на исторических строках, а приёмка требует ретроспективу с 05.08.

Логика вынесена в Python (run_brought_data / freshness_rows), SQL отдаёт завершённые прогоны — иначе нечем красить по значению (pg-фикстуры в репо нет).

Тесты

test_freshness_by_data_3172.py + перекодированный test_2670_stale_source_digest.py (пороговые тесты не тронуты): 421 passed, 1 skipped (-k stale/fresh/scheduler). Фальсификация (откат scheduler.py): 'domclick_city_sweep' not in {…age_days=21.08…}, 'yandex_city_sweep' in {}, 'avito_full_load' … never_ok=True — по значению.

Прод-проверка (ретроспектива, сделаю комментарием)

SQL воркера: old_since (по done) против new_since (по данным) для domclick% — ожидание: старая мера ≈ 05.08, новая — свежая дата.

Closes #3172 (observability p2; породил ложный P1 #3118 «домклик мёртв с 05.08»). `_STALE_SOURCES_SQL` считал свежесть по `max(finished_at)` прогонов `done`; прогон с блоками честно переводится в `banned` (#2657) и при этом вставляет строки. ## Мера «прогон дал данные» `runs._RESULT_COUNTER_KEYS` (`total_seen`/`lots_fetched`/`unique_fetched`/`succeeded`) — тот же словарь, которым уже судит сторож нулевого результата (#2703): одна мера на оба механизма, а не второй список, который разъедется. **Не `lots_inserted`** — это новизна, а не наличие данных: здоровый sweep с полностью дедуплицированной выдачей вставляет ноль и читался бы мёртвым (#3118 наизнанку). Результат не измерен (`_run_result_count() is None` — 28 источников без результатного ключа: `refresh_search_matview`, `deactivate_stale_*`, мониторы) → прежняя мера, статус `done`: иначе «не измерено» схлопнулось бы с «ноль» и все они разом стали бы просроченными навсегда. `never_ok` — той же функцией. Отдельная колонка-отметка в финализаторе отвергнута: миграция + по построению не работает на исторических строках, а приёмка требует ретроспективу с 05.08. Логика вынесена в Python (`run_brought_data` / `freshness_rows`), SQL отдаёт завершённые прогоны — иначе нечем красить по значению (pg-фикстуры в репо нет). ## Тесты `test_freshness_by_data_3172.py` + перекодированный `test_2670_stale_source_digest.py` (пороговые тесты не тронуты): `421 passed, 1 skipped` (-k stale/fresh/scheduler). Фальсификация (откат scheduler.py): `'domclick_city_sweep' not in {…age_days=21.08…}`, `'yandex_city_sweep' in {}`, `'avito_full_load' … never_ok=True` — по значению. ## Прод-проверка (ретроспектива, сделаю комментарием) SQL воркера: `old_since` (по done) против `new_since` (по данным) для `domclick%` — ожидание: старая мера ≈ 05.08, новая — свежая дата.
bot-backend added 1 commit 2026-09-05 19:03:41 +00:00
fix(scheduler): мерить свежесть источника данными, а не статусом прогона
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m9s
3a0b94e052
`_STALE_SOURCES_SQL` считала свежесть возрастом последнего прогона со
статусом 'done'. Прогон с блоком честно финализируется как 'banned'
(#2657) и при этом вставляет строки: у domclick_city_sweep 886 строк
25.08 и 128 строк 23.08 — оба 'banned'. Источник, регулярно ловящий блок
и столь же регулярно приносящий данные, числился мёртвым навсегда,
отсюда ложный P1 #3118 «домклик не собирается с 5 августа».

Запрос отдаёт завершённые прогоны, решение «прогон дал данные»
принимает `run_brought_data` ТЕМ ЖЕ результатным словарём, которым уже
судит сторож нулевого результата (runs._RESULT_COUNTER_KEYS, #2703) —
одна мера на оба механизма. Не 'lots_inserted': это новизна, а не
наличие данных (здоровый дедуплицированный sweep вставляет ноль).
Результат не измерен (28 источников без результатного ключа) → судим
прежней мерой, статусом: «не измерено» ≠ «ноль». never_ok считается той
же мерой, иначе соврал бы в другую сторону.

Closes #3172
Author
Collaborator

Прод-проверка SQL воркера (05.09 19:03 UTC) — честно: сегодня неразличима. У domclick_city_sweep и domclick_detail_backfill есть свежие done-прогоны, поэтому old_since (по статусу) = new_since (по данным) = 05.09. Ретроспектива «домклик перестаёт числиться мёртвым с 05.08» из приёмки #3172 воспроизводится только на срезе истории ДО первого done — сейчас такого окна нет.

Что подтверждается тестами по значению (не продом): источник с последним прогоном banned + данные>0 → не stale; done без результата (измеримый источник) → stale; неизмеримые (28 источников без результатного ключа) — прежняя мера по done.

Датированный критерий (до 19.09): при первом эпизоде «серия banned с данными без done ≥ 7 суток» у любого источника — дайджест _STALE_SOURCES НЕ называет его протухшим (сравнить с old_since-формулой из SQL выше).

Прод-проверка SQL воркера (05.09 19:03 UTC) — честно: **сегодня неразличима**. У `domclick_city_sweep` и `domclick_detail_backfill` есть свежие `done`-прогоны, поэтому `old_since` (по статусу) = `new_since` (по данным) = 05.09. Ретроспектива «домклик перестаёт числиться мёртвым с 05.08» из приёмки #3172 воспроизводится только на срезе истории ДО первого done — сейчас такого окна нет. Что подтверждается тестами по значению (не продом): источник с последним прогоном `banned` + данные>0 → не stale; `done` без результата (измеримый источник) → stale; неизмеримые (28 источников без результатного ключа) — прежняя мера по `done`. Датированный критерий (до 19.09): при первом эпизоде «серия `banned` с данными без `done` ≥ 7 суток» у любого источника — дайджест `_STALE_SOURCES` НЕ называет его протухшим (сравнить с `old_since`-формулой из SQL выше).
bot-backend merged commit 4a7e648068 into main 2026-09-05 19:10:14 +00:00
Author
Collaborator

Ревью (для записи), два minor — follow-up, не блокеры:

  1. «Не измерено» решается по counters конкретного прогона → источник, ПЕРЕСТАВШИЙ писать результатный ключ, молча возвращается к мере «done = свежесть». У сторожа #2703 такая слепая зона логируется (runs.py:187-201, _warn_source_has_no_result_metric) — дёрнуть тот же warn из run_brought_data, одна строка.
  2. Перф: LEFT JOIN scrape_runs … finished_at IS NOT NULL без окна → seq scan + jsonb на каждый исторический прогон при каждом рестарте планировщика (_last_stale_digest_at в памяти). На ~10k строк шум; окно finished_at > now() - interval '365 days' не меняет смысла («не собирал за год» = просрочен по любой мере).

Честный потолок меры (не дефект PR): «выдача отдала объявления» ≠ «строки легли в базу» — бан после первой страницы с lots_fetched=17 читается как живой источник; объём никто не сторожит.

Ревью ✅ (для записи), два minor — follow-up, не блокеры: 1. «Не измерено» решается по counters конкретного прогона → источник, ПЕРЕСТАВШИЙ писать результатный ключ, молча возвращается к мере «done = свежесть». У сторожа #2703 такая слепая зона логируется (`runs.py:187-201`, `_warn_source_has_no_result_metric`) — дёрнуть тот же warn из `run_brought_data`, одна строка. 2. Перф: `LEFT JOIN scrape_runs … finished_at IS NOT NULL` без окна → seq scan + jsonb на каждый исторический прогон при каждом рестарте планировщика (`_last_stale_digest_at` в памяти). На ~10k строк шум; окно `finished_at > now() - interval '365 days'` не меняет смысла («не собирал за год» = просрочен по любой мере). Честный потолок меры (не дефект PR): «выдача отдала объявления» ≠ «строки легли в базу» — бан после первой страницы с `lots_fetched=17` читается как живой источник; объём никто не сторожит.
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#3365
No description provided.