fix(tradein/scraper): detail-backfill с нулём обогащений перестаёт называться успехом (#2674) #2695

Merged
bot-backend merged 1 commit from fix/2674-domclick-backfill-honesty into main 2026-08-06 05:59:38 +00:00
Collaborator

Проблема (числа с прода 2026-08-06)

domclick_detail_backfill: 30 прогонов, 494 попытки, 0 обогащено — и все 30 со статусом done.

Это не единичный случай, а общий дефект трёх detail-backfill'ов — все они финализировались одним mark_done. Прогонов «попытки были, результата ноль» на проде 78 из 158:

source таких прогонов суммарно attempted → enriched
domclick_detail_backfill 24 / 30 494 → 0 (blocked 63, failed 431)
avito_detail_backfill 23 / 76 в т.ч. 5 прогонов по 1500-1600 попыток с нулём
yandex_detail_backfill 31 / 52 все ровно attempted=5 failed=5 (упор в брейкер)

Почему никто не заметил. Оба существующих алерта этот класс пропускают:

  • _alert_if_consecutive_failures считает только failed/banned — а тут done;
  • _alert_if_consecutive_zero_results (#2625) смотрит total_seen, которого в counters backfill'ов нет вовсе → колонка всегда 0 → стрик не прерывается никогда → анти-спам молчит после первого срабатывания.

Что сделано

Один общий финализатор runs.mark_backfill_finished на три задачи (правка в одной точке, а не в трёх копиях). Правила — по образцу #2657 для domclick_city_sweep:

  • attempted=0done (честная пустота, кандидатов не было);
  • блоки + обрыв брейкером ИЛИ ноль результата → banned (external constraint, не наш баг; тот же статус, что #2642 дал cian/yandex, и триггер для ротации IP #2611);
  • ноль результата без блоков → failed (это уже наша поломка);
  • иначе → done, включая частичный прогон.

gone (404 у avito) считается результатом наравне с enriched — прогон, подтвердивший снятие объявлений, работу сделал.

В scrape_runs.error пишется причина с числами прогона, а не пустая строка. Побочный эффект честности: три подряд banned/failed поднимают уже существующий Sentry-алерт — ровно тот сигнал, которого не хватало.

Про корневую причину нуля у Домклика

Она внешняя и уже разобрана до этого PR: детект QRATOR-маркеров чинил #2645 (#2636), после чего 208-я миграция вернула расписание. Свежий прогон 3201 (2026-08-05) отработал ровно как задумано — attempted=3 blocked=3 за 49 с, брейкер оборвал. Второй фактор: куки test-аккаунта протухли 2026-08-03 (domclick_session_cookies.expires_at_estimate = 2026-08-03 20:10Z, last_used_at = 2026-08-01), обновление ручное через POST /scrape/domclick/upload-cookies.

Расписание намеренно не трогаем: брейкер обрывает прогон после 3 блоков за ~150 с (цена вопроса ничтожна), а куки миграцией не перезальёшь. Теперь молчать это перестанет.

Test plan

  • tests/test_backfill_honest_status.py — 10 кейсов, каждый списан с фактического прод-прогона (16×attempted=3 blocked=3, 100 failed без блоков, avito 1500/1499, yandex 5/5, пустой снапшот, частичный, gone-only).
  • Тесты трёх задач обновлены: block-abort дополнительно проверяет aborted_by_blocks=True.
  • Полный прогон backend-сьюта: 3535 passed, 1 failed — известный pre-existing test_search_cache_hit (он же deselect'ится в CI).
  • Прод-верификация после деплоя: docker logs tradein-scraper | grep backfill-honest-status (строка новая, до этого PR её не существовало) + SELECT status FROM scrape_runs WHERE source LIKE '%detail_backfill' ORDER BY id DESC.

Refs #2674

## Проблема (числа с прода 2026-08-06) `domclick_detail_backfill`: **30 прогонов, 494 попытки, 0 обогащено** — и все 30 со статусом `done`. Это не единичный случай, а общий дефект трёх detail-backfill'ов — все они финализировались одним `mark_done`. Прогонов «попытки были, результата ноль» на проде **78 из 158**: | source | таких прогонов | суммарно attempted → enriched | |---|---|---| | `domclick_detail_backfill` | 24 / 30 | 494 → 0 (blocked 63, failed 431) | | `avito_detail_backfill` | 23 / 76 | в т.ч. 5 прогонов по 1500-1600 попыток с нулём | | `yandex_detail_backfill` | 31 / 52 | все ровно attempted=5 failed=5 (упор в брейкер) | **Почему никто не заметил.** Оба существующих алерта этот класс пропускают: - `_alert_if_consecutive_failures` считает только `failed`/`banned` — а тут `done`; - `_alert_if_consecutive_zero_results` (#2625) смотрит `total_seen`, которого в counters backfill'ов нет вовсе → колонка всегда 0 → стрик не прерывается никогда → анти-спам молчит после первого срабатывания. ## Что сделано Один общий финализатор `runs.mark_backfill_finished` на три задачи (правка в одной точке, а не в трёх копиях). Правила — по образцу #2657 для `domclick_city_sweep`: - `attempted=0` → **done** (честная пустота, кандидатов не было); - блоки + обрыв брейкером ИЛИ ноль результата → **banned** (external constraint, не наш баг; тот же статус, что #2642 дал cian/yandex, и триггер для ротации IP #2611); - ноль результата без блоков → **failed** (это уже наша поломка); - иначе → **done**, включая частичный прогон. `gone` (404 у avito) считается результатом наравне с `enriched` — прогон, подтвердивший снятие объявлений, работу сделал. В `scrape_runs.error` пишется причина с числами прогона, а не пустая строка. Побочный эффект честности: три подряд `banned`/`failed` поднимают уже существующий Sentry-алерт — ровно тот сигнал, которого не хватало. ## Про корневую причину нуля у Домклика Она внешняя и уже разобрана до этого PR: детект QRATOR-маркеров чинил #2645 (#2636), после чего 208-я миграция вернула расписание. Свежий прогон 3201 (2026-08-05) отработал ровно как задумано — `attempted=3 blocked=3` за 49 с, брейкер оборвал. Второй фактор: куки test-аккаунта протухли **2026-08-03** (`domclick_session_cookies.expires_at_estimate = 2026-08-03 20:10Z`, `last_used_at = 2026-08-01`), обновление ручное через `POST /scrape/domclick/upload-cookies`. Расписание намеренно **не** трогаем: брейкер обрывает прогон после 3 блоков за ~150 с (цена вопроса ничтожна), а куки миграцией не перезальёшь. Теперь молчать это перестанет. ## Test plan - [x] `tests/test_backfill_honest_status.py` — 10 кейсов, каждый списан с фактического прод-прогона (16×`attempted=3 blocked=3`, 100 failed без блоков, avito 1500/1499, yandex 5/5, пустой снапшот, частичный, gone-only). - [x] Тесты трёх задач обновлены: block-abort дополнительно проверяет `aborted_by_blocks=True`. - [x] Полный прогон backend-сьюта: 3535 passed, 1 failed — известный pre-existing `test_search_cache_hit` (он же deselect'ится в CI). - [ ] Прод-верификация после деплоя: `docker logs tradein-scraper | grep backfill-honest-status` (строка новая, до этого PR её не существовало) + `SELECT status FROM scrape_runs WHERE source LIKE '%detail_backfill' ORDER BY id DESC`. Refs #2674
bot-backend added 1 commit 2026-08-06 05:46:42 +00:00
fix(tradein/scraper): detail-backfill с нулём обогащений перестаёт называться успехом (#2674)
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 3m1s
0e806e1101
Все три detail-backfill'а (avito/yandex/domclick) финализировались одним
mark_done — прогон, который сделал N попыток и не обогатил ни одного
объявления, отчитывался успехом. На проде это 78 прогонов из 158:
  domclick 24/30 — 494 попытки, 0 обогащено, 63 блока, 431 fail;
  avito    23/76 — в т.ч. 5 прогонов по 1500-1600 попыток с нулём;
  yandex   31/52 — все ровно attempted=5 failed=5 (упор в брейкер).

Существующие алерты этот класс не ловили: _alert_if_consecutive_failures
считает только failed/banned, а _alert_if_consecutive_zero_results смотрит
total_seen, которого в counters backfill'ов нет вовсе (всегда 0 → стрик
никогда не прерывается → анти-спам молчит после первого срабатывания).
Поэтому 30 пустых прогонов Домклика и прошли незамеченными.

Один общий финализатор runs.mark_backfill_finished на три задачи (правка в
одной точке, а не в трёх копиях), правила по образцу #2657 для
domclick_city_sweep:
  attempted=0            → done   (честная пустота, кандидатов не было);
  блоки + обрыв/ноль     → banned (external constraint, не наш баг; тот же
                                   статус, что #2642 дал cian/yandex);
  ноль результата без блоков → failed (это уже наша поломка);
  иначе                  → done   (в т.ч. частичный прогон).
`gone` (404 у avito) считается результатом наравне с enriched: прогон,
подтвердивший снятие объявлений, работу сделал.

Причина пишется в scrape_runs.error с числами прогона, а не пустой строкой.
Побочный эффект честности: три подряд banned/failed поднимают уже
существующий Sentry-алерт — ровно тот сигнал, которого не хватало.

Корневая причина нуля у Домклика — внешняя и уже разобрана в #2636/#2645
(детект QRATOR-маркеров) + куки test-аккаунта протухли 2026-08-03
(domclick_session_cookies.expires_at_estimate), обновление ручное. Расписание
не трогаем: брейкер обрывает прогон после 3 блоков за ~150 с, а миграция кук
не перезальёт.

Refs #2674
bot-backend merged commit 44470f7310 into main 2026-08-06 05:59:38 +00:00
bot-backend deleted branch fix/2674-domclick-backfill-honesty 2026-08-06 05:59:38 +00:00
Author
Collaborator

Отложенная проверка закрыта: честный статус сработал на проде, и сразу вскрыл больше, чем чинил

При мерже поведенческая проверка была невозможна — ближайшие окна задач шли только через несколько часов. Прогоны состоялись:

yandex_detail_backfill  11:05  failed  {"failed": 5, "enriched": 0, "attempted": 5, "duration_sec": 23}
avito_detail_backfill   12:40  banned  {"blocked": 5, "enriched": 0, "attempted": 5, "duration_sec": 128}

Оба до правки назывались бы done. Правило различает исходы верно: failed там, где ноль результата без блокировок, banned там, где блокировки, и обе строки причины несут отличительный маркер backfill-honest-status:.

Что честный статус показал сверх того, ради чего писался

Задача заводилась по Домклику — 494 попытки, ноль обогащений, 30 прогонов подряд done. Первые же прогоны после деплоя показывают, что на нуле стоят все три источника, а не один:

источник состояние разобрано
domclick антибот + куки протухли 03.08 #2657, у владельца в #2704
cian 403 на 50 из 50 с 22 июля, куки протухли 30.06 #2700, у владельца
yandex 5 отказов из 5 не разобрано
avito 5 блокировок из 5 не разобрано

Два последних до сегодняшнего дня были скрыты статусом done — ровно тем механизмом, который эта задача и чинила. Разбор запущен отдельно.

Показательно, что цена молчания оказалась выше заявленной в задаче: чинили видимость одного источника, а невидимыми были три.

## Отложенная проверка закрыта: честный статус сработал на проде, и сразу вскрыл больше, чем чинил При мерже поведенческая проверка была невозможна — ближайшие окна задач шли только через несколько часов. Прогоны состоялись: ``` yandex_detail_backfill 11:05 failed {"failed": 5, "enriched": 0, "attempted": 5, "duration_sec": 23} avito_detail_backfill 12:40 banned {"blocked": 5, "enriched": 0, "attempted": 5, "duration_sec": 128} ``` Оба до правки назывались бы `done`. Правило различает исходы верно: **`failed`** там, где ноль результата без блокировок, **`banned`** там, где блокировки, и обе строки причины несут отличительный маркер `backfill-honest-status:`. ## Что честный статус показал сверх того, ради чего писался Задача заводилась по Домклику — 494 попытки, ноль обогащений, 30 прогонов подряд `done`. Первые же прогоны после деплоя показывают, что **на нуле стоят все три источника**, а не один: | источник | состояние | разобрано | |---|---|---| | domclick | антибот + куки протухли 03.08 | #2657, у владельца в #2704 | | cian | 403 на 50 из 50 с 22 июля, куки протухли 30.06 | #2700, у владельца | | **yandex** | **5 отказов из 5** | **не разобрано** | | **avito** | **5 блокировок из 5** | **не разобрано** | Два последних до сегодняшнего дня были скрыты статусом `done` — ровно тем механизмом, который эта задача и чинила. Разбор запущен отдельно. Показательно, что цена молчания оказалась выше заявленной в задаче: чинили видимость одного источника, а невидимыми были три.
Author
Collaborator

Поправка: «на нуле все три источника» — неверно, и ошибок в моём выводе две

Разбор (PR #2738/#2739) опроверг обе половины. Перепроверил сам.

1. Авито обогащает детали каждый день — не через backfill-задачу, а через detail-фазу городского обхода:

источник  дата        обогащено
avito     2026-08-06      6
avito     2026-08-05      6
avito     2026-08-04      9
avito     2026-08-03     19
avito     2026-08-02     10
avito     2026-08-01     23

На нуле стоит именно backfill-задача (последнее обогащение 12 июля, 25-й день), а не источник. Разница существенная: «источник не обогащается» и «одна из двух дорог к нему закрыта» требуют разных действий.

2. У Яндекса сломана не выдача, а голова очереди. Он обогащал 21 прогон из 53, всего 1409 объявлений, последний позавчера. Мои «5 отказов из 5» — один прогон, а не картина.

Корень найден и он не сетевой. Разбор возвращает None до всякого обращения к сети, если ссылка не похожа на карточку объявления:

offer_id_match = re.search(r"/offer/(\d+)/?", offer_url)
if not offer_id_match:
    return None

В очереди Яндекса 3 524 строки из 12 941 активных (27%) ведут на сайты застройщиков — так с выдачи уходят карточки новостроек. Обогащено из них за всю историю ноль. Идут они пачками, поэтому голова очереди на момент прогона оказалась пятью такими подряд.

3. И самое неприятное: «стена капчи» была нашим собственным сообщением. Код писал ABORT -- 5 consecutive parse-None results (captcha wall?), и это уходило в мониторинг как признак противодействия площадки. Площадка тут ни при чём — пять подряд None дал наш же разбор на ссылках, которые он не умеет разбирать по построению.

Это стоит запомнить отдельно: система поставила себе неверный диагноз, и неверный диагноз был принят на веру — в том числе мной, когда я формулировал задачу. Сегодня уже был симметричный случай (#2686: наш отказ записывался как бан площадки), но там метка была слишком широкой, а здесь — прямо выдуманной.

4. Мелочь, но моя: «окна avito 12:39, yandex 11:05 ежедневно» — это не окна, а случайные времена; у обеих задач окно 0–23. Следующие прогоны 07.08 в 01:54 и 07:54 UTC.

## Поправка: «на нуле все три источника» — неверно, и ошибок в моём выводе две Разбор (PR #2738/#2739) опроверг обе половины. Перепроверил сам. **1. Авито обогащает детали каждый день** — не через backfill-задачу, а через detail-фазу городского обхода: ``` источник дата обогащено avito 2026-08-06 6 avito 2026-08-05 6 avito 2026-08-04 9 avito 2026-08-03 19 avito 2026-08-02 10 avito 2026-08-01 23 ``` На нуле стоит именно backfill-задача (последнее обогащение 12 июля, 25-й день), а не источник. Разница существенная: «источник не обогащается» и «одна из двух дорог к нему закрыта» требуют разных действий. **2. У Яндекса сломана не выдача, а голова очереди.** Он обогащал **21 прогон из 53**, всего 1409 объявлений, последний позавчера. Мои «5 отказов из 5» — один прогон, а не картина. Корень найден и он не сетевой. Разбор возвращает `None` **до всякого обращения к сети**, если ссылка не похожа на карточку объявления: ```python offer_id_match = re.search(r"/offer/(\d+)/?", offer_url) if not offer_id_match: return None ``` В очереди Яндекса **3 524 строки из 12 941 активных (27%)** ведут на сайты застройщиков — так с выдачи уходят карточки новостроек. Обогащено из них за всю историю ноль. Идут они пачками, поэтому голова очереди на момент прогона оказалась пятью такими подряд. **3. И самое неприятное: «стена капчи» была нашим собственным сообщением.** Код писал `ABORT -- 5 consecutive parse-None results (captcha wall?)`, и это уходило в мониторинг как признак противодействия площадки. Площадка тут ни при чём — пять подряд `None` дал наш же разбор на ссылках, которые он не умеет разбирать по построению. Это стоит запомнить отдельно: **система поставила себе неверный диагноз, и неверный диагноз был принят на веру** — в том числе мной, когда я формулировал задачу. Сегодня уже был симметричный случай (#2686: наш отказ записывался как бан площадки), но там метка была слишком широкой, а здесь — прямо выдуманной. **4. Мелочь, но моя:** «окна avito 12:39, yandex 11:05 ежедневно» — это не окна, а случайные времена; у обеих задач окно 0–23. Следующие прогоны 07.08 в 01:54 и 07:54 UTC.
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#2695
No description provided.