fix(tradein/domclick): подтверждённый отказ площадки уехал в ветку «сбой транспорта» и перестал банить узел #3241

Merged
lekss361 merged 1 commit from fix/3239-sidecar-ban-page-reaches-ban-path into main 2026-08-29 16:18:51 +00:00
Owner

Summary

Регрессия моего же #3237, найденная при его приёмке. Сайдкар научился опознавать статический отказ Домклика сам — это работает и остаётся. Но отказ стал приезжать наверх обычной 500-кой, browser_fetcher.py:781 на ней обнуляет last_response_status, и в detail.py срабатывает ветка except Exception, которая по построению не зовёт report_ban (#2600 п.4 — не смешивать «бан» и «сетевой сбой»).

Три изменения возвращают отказ на ban-путь, не ломая разделение, ради которого #3237 и делался:

  • browser/server.py — в тело ошибки кладётся структурный признак ban_page и апстрим-статус. HTTP-код остаётся 500: на него завязана classify_browser_probe.
  • browser_fetcher.pySidecarBanPageError как подкласс httpx.HTTPStatusError, поэтому ловля у Авито/Циана/Яндекса и retry-политика fetch() нового типа не замечают.
  • providers/domclick/detail.py — различает две ветки: подтверждённый отказ → report_ban + статус из исключения; транспортный сбой — ровно как раньше.

Замер, из которого это видно

Прогон 5287 (29.08, 15:56 UTC, уже на образе с #3237):

{"failed": 0, "blocked": 3, "enriched": 0, "attempted": 3,
 "ban_kinds": {"unknown": 3}, "duration_sec": 97}

Раньше на том же месте было {"platform": 3}. В логах сайдкара — шесть статический отказ площадки подряд, в scrape_proxy_source_bans после прогона ни одной новой записи.

Вред практический, не косметический: platform — единственный диагноз, запускающий ротацию IP. Без него пул считает отказавшие узлы здоровыми и выдаёт их следующему прогону.

Два решения, которые стоит отметить

Статус несём отдельным полем, а не через last_response_status: на error-пути fetch() его обнуляет, а у Домклика отказ приходит с 401 — без него классификатор ставит unknown. Тест ..._carries_upstream_status держит именно это (MagicMock отдал бы last_response_status как Mock, и ассерт упал бы, читай код оттуда).

Признак структурный, а не подстрока в тексте: _raise_for_sidecar_status обрезает тело до 300 символов, и формулировка отказа менялась дважды за месяц.

Test plan

  • Сайдкар целиком — 177 passed (в т.ч. 3 новых: признак есть у бан-страницы, отсутствует у транспортной ошибки, status: null не подменяется выдуманным кодом)
  • Полный backend-прогон — 5064 passed, 37 skipped
  • ruff check на всех шести файлах
  • Обе ветки покрыты раздельно на всех трёх уровнях, включая ловушку bool-как-int из #3196
  • CI зелёный

Приёмка на проде

Прогон domclick_detail_backfill на отказывающем узле: ban_kinds = platform, в scrape_proxy_source_bans появляется запись, следующий прогон берёт ДРУГОЙ узел. При этом незавершённое рукопожатие (#3237) по-прежнему не банит.

Оговорка та же, что и в #3237: узлы пула 1/9/10/11 подпорчены сегодняшними диагностическими пробами, поэтому первый прогон упрётся в остаточные отказы площадки. Судить по тому, ЗАПИСАЛСЯ ЛИ БАН, а не по enriched.

Closes #3239

## Summary Регрессия моего же #3237, найденная при его приёмке. Сайдкар научился опознавать статический отказ Домклика сам — это работает и остаётся. Но отказ стал приезжать наверх обычной 500-кой, `browser_fetcher.py:781` на ней обнуляет `last_response_status`, и в `detail.py` срабатывает ветка `except Exception`, которая **по построению не зовёт** `report_ban` (#2600 п.4 — не смешивать «бан» и «сетевой сбой»). Три изменения возвращают отказ на ban-путь, не ломая разделение, ради которого #3237 и делался: - `browser/server.py` — в тело ошибки кладётся структурный признак `ban_page` и апстрим-статус. **HTTP-код остаётся 500**: на него завязана `classify_browser_probe`. - `browser_fetcher.py` — `SidecarBanPageError` как подкласс `httpx.HTTPStatusError`, поэтому ловля у Авито/Циана/Яндекса и retry-политика `fetch()` нового типа не замечают. - `providers/domclick/detail.py` — различает две ветки: подтверждённый отказ → `report_ban` + статус из исключения; транспортный сбой — ровно как раньше. ## Замер, из которого это видно Прогон 5287 (29.08, 15:56 UTC, уже на образе с #3237): ``` {"failed": 0, "blocked": 3, "enriched": 0, "attempted": 3, "ban_kinds": {"unknown": 3}, "duration_sec": 97} ``` Раньше на том же месте было `{"platform": 3}`. В логах сайдкара — шесть `статический отказ площадки` подряд, в `scrape_proxy_source_bans` после прогона **ни одной новой записи**. Вред практический, не косметический: `platform` — единственный диагноз, запускающий ротацию IP. Без него пул считает отказавшие узлы здоровыми и выдаёт их следующему прогону. ## Два решения, которые стоит отметить **Статус несём отдельным полем**, а не через `last_response_status`: на error-пути `fetch()` его обнуляет, а у Домклика отказ приходит с 401 — без него классификатор ставит `unknown`. Тест `..._carries_upstream_status` держит именно это (MagicMock отдал бы `last_response_status` как Mock, и ассерт упал бы, читай код оттуда). **Признак структурный, а не подстрока в тексте**: `_raise_for_sidecar_status` обрезает тело до 300 символов, и формулировка отказа менялась дважды за месяц. ## Test plan - [x] Сайдкар целиком — 177 passed (в т.ч. 3 новых: признак есть у бан-страницы, отсутствует у транспортной ошибки, `status: null` не подменяется выдуманным кодом) - [x] Полный backend-прогон — **5064 passed, 37 skipped** - [x] `ruff check` на всех шести файлах - [x] Обе ветки покрыты раздельно на всех трёх уровнях, включая ловушку bool-как-int из #3196 - [ ] CI зелёный ## Приёмка на проде Прогон `domclick_detail_backfill` на отказывающем узле: `ban_kinds` = `platform`, в `scrape_proxy_source_bans` появляется запись, следующий прогон берёт ДРУГОЙ узел. При этом незавершённое рукопожатие (#3237) по-прежнему не банит. Оговорка та же, что и в #3237: узлы пула 1/9/10/11 подпорчены сегодняшними диагностическими пробами, поэтому первый прогон упрётся в остаточные отказы площадки. Судить по тому, ЗАПИСАЛСЯ ЛИ БАН, а не по `enriched`. Closes #3239
lekss361 added 1 commit 2026-08-29 16:13:08 +00:00
fix(tradein/domclick): подтверждённый отказ площадки уехал в ветку «сбой транспорта» и перестал банить узел
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 / browser-tests (pull_request) Successful in 1m17s
CI Trade-In / backend-tests (pull_request) Successful in 4m54s
c5784e85bc
#3237 научил сайдкар опознавать статический отказ Домклика самостоятельно —
это правильно и работает, но вместе с распознаванием ban-сигнал переехал не
туда. Отказ стал приезжать обычной 500-кой: browser_fetcher обнуляет на ней
last_response_status, и в detail.py срабатывает ветка except Exception,
которая по построению НЕ зовёт report_ban («не подтверждённый маркер-бан, а
сбой транспорта», #2600 п.4).

Итог на проде (прогон 5287): ban_kinds сменился с platform на unknown, и при
шести «статический отказ площадки» подряд в логах сайдкара в
scrape_proxy_source_bans не появилось НИ ОДНОЙ записи. Это не косметика
счётчиков — platform единственный диагноз, запускающий ротацию IP, поэтому мы
продолжали бы долбиться в отказавший узел вместо перехода на свободный.

Правка возвращает отказ на ban-путь, сохраняя разделение, ради которого
#3237 и делался:

- сайдкар кладёт в тело ошибки структурный признак ban_page и апстрим-статус.
  HTTP-код НЕ меняем: на 500 завязана classify_browser_probe;
- SidecarBanPageError — подкласс httpx.HTTPStatusError, поэтому ловля у
  прочих поставщиков и retry-политика fetch() не замечают нового типа;
- detail.py различает две ветки: подтверждённый отказ → report_ban + статус
  из исключения, транспортный сбой — как раньше.

Статус несём отдельным полем, а не через last_response_status: на error-пути
fetch() его обнуляет, а у Домклика отказ приходит с 401, без которого
классификатор ставит unknown. Подстрокой в тексте исключения признак искать
нельзя — _raise_for_sidecar_status обрезает тело до 300 символов, и
формулировка отказа менялась дважды за месяц.

Тесты держат обе ветки раздельно на всех трёх уровнях: сайдкар (признак есть
у бан-страницы, отсутствует у транспортной ошибки), фетчер (тип и
upstream_status, включая ловушку bool-как-int из #3196), detail.py
(report_ban зовётся / не зовётся, статус доезжает).

Closes #3239
lekss361 merged commit 3d51c08e44 into main 2026-08-29 16:18:51 +00:00
lekss361 deleted branch fix/3239-sidecar-ban-page-reaches-ban-path 2026-08-29 16:18:51 +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#3241
No description provided.