fix(tradein/scrapers): прогон больше не рапортует «done» поверх провала и нуля #2892

Merged
lekss361 merged 2 commits from fix/tradein-honest-run-status into main 2026-08-15 16:20:31 +00:00
Owner

Проблема

Из аудита 15.08. Это усилитель всех остальных проблем со сбором: пока прогоны рапортуют успех, деградация источников не видна ни на одном экране, и мы узнаём о ней только когда клиент не получает оценку.

Три части, все проверены по прод-БД:

Доля отказов не влияла на статус. avito_detail_backfill 15.08: {"attempted":64,"failed":57,"enriched":6}status='done'. 89% отказов, галка зелёная.

Сторож нулевого результата был слеп к части прогонов. yandex_newbuilding_sweep — десять прогонов подряд с 26.07 по 10.08, все done при succeeded=0, rows_inserted=0. Причина: _RESULT_COUNTER_KEYS перечислял ключи, которых этот sweep не пишет.

Витрина читала не те ключи. new_count показывал ноль при реальных saved_inserted 482 / 214 / 239.

Что сделано

Правило по доле отказов, дополненные результатные ключи и починенное чтение витрины.

Что поймало ревью

Первый круг — MAJOR, и замечание оказалось точным: в результатные ключи попал голый rows_inserted, который на проде пишет не только целевой sweep, но и rosreestr_dkp_import — 67 прогонов за 90 суток, из них 66 штатно нулевые (данные приходят раз в квартал). Правка объявила бы ежедневный импорт сделок сломанным.

Заменено на succeeded. Проверено запросом: этот ключ пишут ровно два источника — yandex_newbuilding_sweep и newbuilding_enrich, а rosreestr_dkp_import не пишет ни разу.

Заодно убран processed: это счётчик попыток, а не результата — у newbuilding_enrich он равен лимиту даже при полном провале.

Одно замечание автор опроверг, и обоснованно: ревьюер сам пометил его LOW и предложил закрыть не кодом, а фиксацией в описании. Фиксирую ниже.

Второй круг: MINOR.

Ожидаемый побочный эффект — следить первые ночи

Backfill'ы с высокой долей отказов теперь получают failed вместо done. Суточная сводка планировщика считает просроченным источник по возрасту последнего успешного прогона — значит такие источники начнут попадать в «просрочено».

Это не регресс, а именно то, чего мы добивались: раньше они выглядели живыми, будучи мёртвыми. Но первые пару ночей сводка будет непривычно красной, и важно не принять это за поломку. Сейчас под правило попадает прежде всего avito_detail_backfill (HTTP 439, #2827) — то есть сводка начнёт показывать ровно ту проблему, которая реально есть.

Test plan

  • тесты на все три части с числами из прод-примеров
  • проверено по прод-БД, кто пишет каждый результатный ключ
  • первые ночи после деплоя: сверить, что «красными» стали только реально деградировавшие источники, а спящие расписания области не шумят
## Проблема Из аудита 15.08. Это усилитель всех остальных проблем со сбором: пока прогоны рапортуют успех, деградация источников не видна ни на одном экране, и мы узнаём о ней только когда клиент не получает оценку. Три части, все проверены по прод-БД: **Доля отказов не влияла на статус.** `avito_detail_backfill` 15.08: `{"attempted":64,"failed":57,"enriched":6}` → `status='done'`. 89% отказов, галка зелёная. **Сторож нулевого результата был слеп к части прогонов.** `yandex_newbuilding_sweep` — десять прогонов подряд с 26.07 по 10.08, **все** `done` при `succeeded=0, rows_inserted=0`. Причина: `_RESULT_COUNTER_KEYS` перечислял ключи, которых этот sweep не пишет. **Витрина читала не те ключи.** `new_count` показывал ноль при реальных `saved_inserted` 482 / 214 / 239. ## Что сделано Правило по доле отказов, дополненные результатные ключи и починенное чтение витрины. ## Что поймало ревью Первый круг — **MAJOR**, и замечание оказалось точным: в результатные ключи попал голый `rows_inserted`, который на проде пишет не только целевой sweep, но и `rosreestr_dkp_import` — 67 прогонов за 90 суток, из них 66 штатно нулевые (данные приходят раз в квартал). Правка объявила бы ежедневный импорт сделок сломанным. Заменено на `succeeded`. Проверено запросом: этот ключ пишут ровно два источника — `yandex_newbuilding_sweep` и `newbuilding_enrich`, а `rosreestr_dkp_import` не пишет ни разу. Заодно убран `processed`: это счётчик попыток, а не результата — у `newbuilding_enrich` он равен лимиту даже при полном провале. Одно замечание автор **опроверг**, и обоснованно: ревьюер сам пометил его LOW и предложил закрыть не кодом, а фиксацией в описании. Фиксирую ниже. Второй круг: **MINOR**. ## Ожидаемый побочный эффект — следить первые ночи Backfill'ы с высокой долей отказов теперь получают `failed` вместо `done`. Суточная сводка планировщика считает просроченным источник по возрасту последнего **успешного** прогона — значит такие источники начнут попадать в «просрочено». Это не регресс, а именно то, чего мы добивались: раньше они выглядели живыми, будучи мёртвыми. Но первые пару ночей сводка будет непривычно красной, и важно не принять это за поломку. Сейчас под правило попадает прежде всего `avito_detail_backfill` (HTTP 439, #2827) — то есть сводка начнёт показывать ровно ту проблему, которая реально есть. ## Test plan - [x] тесты на все три части с числами из прод-примеров - [x] проверено по прод-БД, кто пишет каждый результатный ключ - [ ] первые ночи после деплоя: сверить, что «красными» стали только реально деградировавшие источники, а спящие расписания области не шумят
lekss361 added 2 commits 2026-08-15 16:02:18 +00:00
Три прод-факта, где status='done' врал о реальном исходе прогона:

- avito_detail_backfill 15.08: {"attempted":64,"failed":57,"enriched":6,"blocked":1}
  -> 'done'. mark_backfill_finished звал mark_done, потому что produced=6 (>0);
  ни _sweep_run_did_nothing (нет anchors_total/errors_count у backfill'ов), ни
  _phase_totally_failed (голые "attempted"/"failed" без фазового префикса) эту
  форму counters не ловили. Новый _failed_ratio_too_high внутри mark_done:
  failed/attempted >= 0.5 -> 'failed', >= 0.15 -> тоже 'failed' (другая
  формулировка причины в error-тексте) — 'partial' статусом не заведён: это
  потребовало бы DROP+ADD CHECK constraint (051_scrape_runs_extend.sql) и
  дообучения ещё 4 мест (Literal-фильтр admin API, статусы фронта, оба
  IN-списка сторожей) — тот же класс проводки, что и у ban_kind (#2686/#2764),
  который сознательно не стал новым статусом.

- yandex_newbuilding_sweep 26.07-10.08: десять прогонов подряд 'done' при
  processed=5 succeeded=0 rows_inserted=0 failed_resolve=4-5 — сторож нулевого
  результата (_alert_if_consecutive_zero_results) не видел ни один результатный
  ключ этого sweep'а и молчал навсегда. _RESULT_COUNTER_KEYS дополнен
  rows_inserted/processed (именно в этом порядке — rows_inserted это результат,
  processed это попытки; иначе "5 обработано, 0 записано" замаскировалось бы
  под measured-5).

- admin-витрина показывала new_count=0 у трёх подряд cian_full_load при реально
  сохранённых saved_inserted=482/214/239 — full-load'ы не пишут ни 'new_count',
  ни 'lots_inserted'. _column_counts дополнен saved_inserted/rows_inserted.

Правки продублированы в scraper_kit/orchestration/runs.py (byte-эквивалент
app.services.scrape_runs, см. докстринг модуля) для параллели: единственный
текущий писатель "attempted"/"failed" (mark_backfill_finished) живёт только в
app-копии, но приоритет ключей/константы держим синхронными на будущее.

Не тронуто: сознательно пустые sweep'ы (errors_count=0, honest empty) и малые
батчи (attempted < 3) — доля отказов на них не считается диагнозом.

Tests: tests/test_honest_run_status_failed_ratio.py (41 кейс, оба модуля,
включая точные прод-числа из трёх фактов выше) + regression-прогон 609 тестов
по всем файлам, трогающим scrape_runs/orchestration.runs — 0 регрессий.
fix(tradein/scrapers): не путать rows_inserted/processed с честным результатным ключом
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / 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 5m4s
e9ca744e85
Ревью честного run-status нашло, что _RESULT_COUNTER_KEYS ловил не только целевой
yandex_newbuilding_sweep, но и rosreestr_dkp_import (rows_inserted, 66 из 67 прод-
прогонов = здоровый ноль догнавшего инкрементального импорта) и newbuilding_enrich
(processed — счётчик попыток, ==limit даже при частичном провале). Первое завело бы
практически непрерываемый ложный zero-стрик у здорового источника, второе маскировало
бы реальные отказы под measured-N.

Проверено по прод-БД (2026-08-15): "succeeded" пишут ТОЛЬКО yandex_newbuilding_sweep
(42 прогона/90д) и newbuilding_enrich (65/90д) — ни разу rosreestr_dkp_import; у
yandex_newbuilding_sweep succeeded численно совпадает с rows_inserted на всех 42/42
прогонах. Заменил "rows_inserted"+"processed" на "succeeded" в _RESULT_COUNTER_KEYS
(app-копия и byte-эквивалентная kit-копия) — цель (b) исходной правки сохранена, ложный
стрик у rosreestr_dkp_import снят, попутно newbuilding_enrich получает честное
измерение вместо счётчика попыток.

Также поправлены докстринги test_backfill_honest_status.py — два кейса (76%/72%
отказов -> 'done') проверяют только выбор финализатора mark_backfill_finished
(mark_done там замокан); реальный mark_done с honest-run-status переквалифицирует их
в 'failed' через _failed_ratio_too_high — это не документировалось явно.
lekss361 merged commit 85414dabd2 into main 2026-08-15 16:20:31 +00:00
lekss361 deleted branch fix/tradein-honest-run-status 2026-08-15 16:20:31 +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#2892
No description provided.