fix(tradein/proxy): проба узла ходит браузерным трактом, вердикт живёт отдельно от HTTP-пробы (#2723) #2736

Merged
bot-backend merged 1 commit from fix/2723-healthcheck-real-path into main 2026-08-06 12:50:24 +00:00
Collaborator

Что не подтвердилось в постановке

Числа из issue воспроизвелись точно (прод, scrape_runs), механизм — нет.

дата реальных отказов проб checked за сутки
07-30 … 07-21 4-5/сут 48/сут 48
07-31 4 48 105
08-01 1 48 179
08-03 1 48 192
08-04 … 08-06 0 48 192

checked=48 в сутки при 48 прогонах = один узел за прогон. В те десять суток проба не «врала про пул» — она вообще не видела остальных узлов: scrape_proxies id 10 и 11 созданы 2026-07-31 12:59, а самовосстановление disabled-узлов (#2600 п.1) доехало 01.08 (до этого в counters нет ключей revived/bans_purged).

Главное: браузерный путь Авито подключён к пулу только 02.08 — PR #2637, AvitoScraper.__init__(proxy_provider=...). До него build_browser_fetcher(config, "avito") шёл без провайдера → в теле /fetch не было proxy → сайдкар брал env-SCRAPER_PROXY_URL (мёртвый mobileproxy, #2613). То есть проба и работа ходили через разные прокси, а не по разным трактам одного узла. Это и есть корень 90 обрывов, и он уже починен #2637/#2616 — отказы упали до нуля 04-06.08.

Живая проверка сегодня (нейтральный адрес, нулевая нагрузка на площадки): все 4 узла проходят и HTTP-пробу, и браузерную, exit-IP совпадают. Узла в состоянии «HTTP да, браузер нет» сейчас не существует, воспроизвести исторический случай нечем.

Что при этом действительно сломано — и починено здесь

Пул теперь подключён (USE_PROXY_POOL_BROWSER=true), и оба тракта пишут вердикт в один счётчик:

  • боевой /fetch через сайдкар репортит mark_health(ok=False)consecutive_fails++;
  • BrowserFetcher меняет lease после 3 подряд, acquire перестаёт выдавать узел при MAX_CONSECUTIVE_FAILS=3;
  • но ipify-проба через ≤30 мин делает consecutive_fails=0 + enabled=true.

Дешёвая проба стирает вердикт дорогого тракта. Узел, мёртвый для браузера, гарантированно возвращается в выдачу каждые полчаса и снова забирает прогон — вечный цикл, до порога DISABLE_THRESHOLD=5 он не дойдёт никогда. Это ровно #2686 (два диагноза в одном флаге) в consecutive_fails.

Как разведены диагнозы

Приём #2711 — поле рядом, а не новое значение существующего флага. Миграция 228: browser_fail_streak, browser_unfit_since, browser_check_at.

владеет такт кто ставит
«узел мёртв целиком» consecutive_fails, enabled, exit_ip 30 мин ipify-проба
«жив для HTTP, не для браузера» browser_* 360 мин браузерная проба

Пересечения нет ни в одну сторону: успешная ipify не обнуляет browser_fail_streak, провал браузерной пробы не трогает enabled/consecutive_fails.

Пометка не выводит узел из пула. При 4 узлах (#2638) голодание хуже работы через плохой узел — та же политика, что у защиты последнего узла в mark_banned. acquire лишь отдаёт непригодный узел последним (ORDER BY (browser_unfit_since IS NOT NULL), …) и громко логирует, если пригодных не осталось. Путь обратно — успешная браузерная проба (browser_refit), плюс реальный трафик, когда узел всё-таки выдан.

Чужой отказ узлу не засчитывается. classify_browser_probeproxy (503 browser unavailable (proxy may be down) / 500 NS_ERROR_PROXY_*) считается; sidecar (сайдкар лежит, no proxy configured) и page — нет. Иначе одна упавшая общая зависимость пометила бы непригодным весь пул разом.

Адрес и тракт пробы

POST /fetchhttps://www.avito.ru/robots.txt, прокси узла в теле, source='generic'.

  • /fetch, а не /fetch-json: последний сначала делает goto на главную площадки — это уже заметная нагрузка, ради избегания которой проба и затевалась.
  • robots.txt: статический ~4 КБ, читать его автоматическим клиентам прямо предписано. Не выдача, не карточка.
  • avito.ru: 100% записанных обрывов — avito; проба обязана трогать её edge, а не нейтральный хост.
  • source='generic': сайдкар держит по инстансу camoufox на провайдера со своим локом. Проба с source='avito' забирала бы лок боевого инстанса и релончила его (прокси пробы ≠ прокси сессии) — ровно тот relaunch-шторм, который лечил sticky-lease фикс.

Стоимость — по числам, не по ощущению (замер на проде 06.08)

сейчас если каждым прогоном принято (360 мин)
/fetch сайдкара в сутки ~1000 +192 (+19%) +16 (+1.6%)
запусков camoufox в сутки ~190 +192 (+100%) +16 (+8%)

Одна браузерная проба = 8.3 с и один запуск camoufox (замерено вживую, включая relaunch). Гнать её каждые 30 минут = удвоить самую дорогую операцию сайдкара ради диагностики. Такт 360 мин + два сокращения: проба идёт только по узлам, прошедшим ipify (не прошедший мёртв целиком, диагноз уже есть), и только при use_proxy_pool_browser (иначе браузер ходит мимо пула). Неподтверждённый провал такт не двигает → второе подтверждение приходит на следующем прогоне (~30 мин), а не через 6 часов.

Test plan

  • tests/test_2723_browser_probe.py — 20 тестов: классификация отказа (7 кейсов), тракт пробы (эндпоинт/адрес/прокси в теле), неразрушение вердикта, обе стороны разведения, лежащий сайдкар не винит никого, порядок выдачи, «все непригодны → прокси всё равно выдаётся», реанимация, такт, оба гейта.
  • Фальсификация точечная: возврат пред-фиксной семантики (mark_health(ok=True) обнуляет и браузерный вердикт) → красные test_ipify_success_does_not_erase_browser_verdict, test_healthcheck_marks_unfit_when_http_green_browser_red, test_healthcheck_revives_unfit_node. На origin/main файл не импортируется (нет ни функции, ни констант).
  • Мок FakeSession расширен так, что «починить» боевой SQL за него нельзя: новые ветки гейтятся по подстрокам самого запроса.
  • Полный прогон backend-сьюта: 3774 passed, 9 skipped, 1 deselected (штатный).
  • ruff check / ruff format чисто по изменённым файлам (3 ошибки в test_estimator_pure_units.py — pre-existing, воспроизводятся на origin/main).
  • Правленые пути входят в scraper-allowlist deploy-tradein.yml через packages/scraper-kit/** — без этого proxy_pool.py (app/services/** в allowlist нет) уехал бы в образ, но tradein-scraper, где и крутится healthcheck, остался бы на старом коде.

Прод-верификация после мержа

Маркер — новые ключи в counters: SELECT counters FROM scrape_runs WHERE source='proxy_healthcheck' ORDER BY id DESC LIMIT 1 → должны появиться browser_checked/browser_ok/browser_unfit/browser_refit. Первый прогон после деплоя даст browser_checked=4 (у всех узлов browser_check_at IS NULL).

Refs #2723

## Что не подтвердилось в постановке Числа из issue воспроизвелись точно (прод, `scrape_runs`), **механизм — нет**. | дата | реальных отказов | проб | `checked` за сутки | |---|---:|---:|---:| | 07-30 … 07-21 | 4-5/сут | 48/сут | **48** | | 07-31 | 4 | 48 | 105 | | 08-01 | 1 | 48 | 179 | | 08-03 | 1 | 48 | 192 | | 08-04 … 08-06 | **0** | 48 | 192 | `checked=48` в сутки при 48 прогонах = **один узел за прогон**. В те десять суток проба не «врала про пул» — она **вообще не видела остальных узлов**: `scrape_proxies` id 10 и 11 созданы `2026-07-31 12:59`, а самовосстановление disabled-узлов (#2600 п.1) доехало 01.08 (до этого в counters нет ключей `revived`/`bans_purged`). Главное: **браузерный путь Авито подключён к пулу только 02.08** — PR #2637, `AvitoScraper.__init__(proxy_provider=...)`. До него `build_browser_fetcher(config, "avito")` шёл без провайдера → в теле `/fetch` не было `proxy` → сайдкар брал env-`SCRAPER_PROXY_URL` (мёртвый mobileproxy, #2613). То есть проба и работа ходили **через разные прокси**, а не по разным трактам одного узла. Это и есть корень 90 обрывов, и он уже починен #2637/#2616 — отказы упали до нуля 04-06.08. Живая проверка сегодня (нейтральный адрес, нулевая нагрузка на площадки): все 4 узла проходят и HTTP-пробу, и браузерную, exit-IP совпадают. Узла в состоянии «HTTP да, браузер нет» **сейчас не существует**, воспроизвести исторический случай нечем. ## Что при этом действительно сломано — и починено здесь Пул теперь подключён (`USE_PROXY_POOL_BROWSER=true`), и оба тракта пишут вердикт в **один** счётчик: - боевой `/fetch` через сайдкар репортит `mark_health(ok=False)` → `consecutive_fails++`; - `BrowserFetcher` меняет lease после 3 подряд, `acquire` перестаёт выдавать узел при `MAX_CONSECUTIVE_FAILS=3`; - **но** ipify-проба через ≤30 мин делает `consecutive_fails=0` + `enabled=true`. Дешёвая проба **стирает вердикт дорогого тракта**. Узел, мёртвый для браузера, гарантированно возвращается в выдачу каждые полчаса и снова забирает прогон — вечный цикл, до порога `DISABLE_THRESHOLD=5` он не дойдёт никогда. Это ровно #2686 (два диагноза в одном флаге) в `consecutive_fails`. ## Как разведены диагнозы Приём #2711 — поле **рядом**, а не новое значение существующего флага. Миграция **228**: `browser_fail_streak`, `browser_unfit_since`, `browser_check_at`. | | владеет | такт | кто ставит | |---|---|---|---| | «узел мёртв целиком» | `consecutive_fails`, `enabled`, `exit_ip` | 30 мин | ipify-проба | | «жив для HTTP, не для браузера» | `browser_*` | 360 мин | браузерная проба | Пересечения нет ни в одну сторону: успешная ipify не обнуляет `browser_fail_streak`, провал браузерной пробы не трогает `enabled`/`consecutive_fails`. **Пометка не выводит узел из пула.** При 4 узлах (#2638) голодание хуже работы через плохой узел — та же политика, что у защиты последнего узла в `mark_banned`. `acquire` лишь отдаёт непригодный узел **последним** (`ORDER BY (browser_unfit_since IS NOT NULL), …`) и громко логирует, если пригодных не осталось. Путь обратно — успешная браузерная проба (`browser_refit`), плюс реальный трафик, когда узел всё-таки выдан. **Чужой отказ узлу не засчитывается.** `classify_browser_probe` → `proxy` (503 `browser unavailable (proxy may be down)` / 500 `NS_ERROR_PROXY_*`) считается; `sidecar` (сайдкар лежит, `no proxy configured`) и `page` — нет. Иначе одна упавшая общая зависимость пометила бы непригодным **весь пул разом**. ## Адрес и тракт пробы `POST /fetch` → `https://www.avito.ru/robots.txt`, прокси узла в теле, `source='generic'`. - **`/fetch`, а не `/fetch-json`**: последний сначала делает `goto` на **главную** площадки — это уже заметная нагрузка, ради избегания которой проба и затевалась. - **robots.txt**: статический ~4 КБ, читать его автоматическим клиентам прямо предписано. Не выдача, не карточка. - **avito.ru**: 100% записанных обрывов — avito; проба обязана трогать её edge, а не нейтральный хост. - **`source='generic'`**: сайдкар держит по инстансу camoufox на провайдера со своим локом. Проба с `source='avito'` забирала бы лок боевого инстанса и релончила его (прокси пробы ≠ прокси сессии) — ровно тот relaunch-шторм, который лечил sticky-lease фикс. ## Стоимость — по числам, не по ощущению (замер на проде 06.08) | | сейчас | если каждым прогоном | принято (360 мин) | |---|---:|---:|---:| | /fetch сайдкара в сутки | ~1000 | +192 (+19%) | +16 (+1.6%) | | запусков camoufox в сутки | ~190 | +192 (**+100%**) | +16 (+8%) | Одна браузерная проба = **8.3 с** и один запуск camoufox (замерено вживую, включая relaunch). Гнать её каждые 30 минут = удвоить самую дорогую операцию сайдкара ради диагностики. Такт 360 мин + два сокращения: проба идёт только по узлам, **прошедшим ipify** (не прошедший мёртв целиком, диагноз уже есть), и только при `use_proxy_pool_browser` (иначе браузер ходит мимо пула). Неподтверждённый провал такт **не двигает** → второе подтверждение приходит на следующем прогоне (~30 мин), а не через 6 часов. ## Test plan - [x] `tests/test_2723_browser_probe.py` — 20 тестов: классификация отказа (7 кейсов), тракт пробы (эндпоинт/адрес/прокси в теле), неразрушение вердикта, обе стороны разведения, лежащий сайдкар не винит никого, порядок выдачи, «все непригодны → прокси всё равно выдаётся», реанимация, такт, оба гейта. - [x] Фальсификация точечная: возврат пред-фиксной семантики (`mark_health(ok=True)` обнуляет и браузерный вердикт) → красные `test_ipify_success_does_not_erase_browser_verdict`, `test_healthcheck_marks_unfit_when_http_green_browser_red`, `test_healthcheck_revives_unfit_node`. На `origin/main` файл не импортируется (нет ни функции, ни констант). - [x] Мок `FakeSession` расширен так, что «починить» боевой SQL за него нельзя: новые ветки гейтятся по подстрокам самого запроса. - [x] Полный прогон backend-сьюта: **3774 passed**, 9 skipped, 1 deselected (штатный). - [x] `ruff check` / `ruff format` чисто по изменённым файлам (3 ошибки в `test_estimator_pure_units.py` — pre-existing, воспроизводятся на `origin/main`). - [x] Правленые пути входят в scraper-allowlist `deploy-tradein.yml` через `packages/scraper-kit/**` — без этого `proxy_pool.py` (`app/services/**` в allowlist нет) уехал бы в образ, но `tradein-scraper`, где и крутится healthcheck, остался бы на старом коде. ## Прод-верификация после мержа Маркер — новые ключи в counters: `SELECT counters FROM scrape_runs WHERE source='proxy_healthcheck' ORDER BY id DESC LIMIT 1` → должны появиться `browser_checked/browser_ok/browser_unfit/browser_refit`. Первый прогон после деплоя даст `browser_checked=4` (у всех узлов `browser_check_at IS NULL`). Refs #2723
bot-backend added 1 commit 2026-08-06 12:44:57 +00:00
fix(tradein/proxy): проба узла ходит браузерным трактом, вердикт живёт отдельно от HTTP-пробы (#2723)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / 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 / 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
9803708109
ipify-проба отвечает на «узел жив вообще», а сбор Авито с 02.08 (#2637) ходит через
сайдкар браузером: camoufox стартует С ЭТИМ прокси, потом навигация. Это разные
свойства узла, и до сих пор они писались в ОДИН счётчик: боевой /fetch репортил
mark_health(ok=False), а следующая (≤30 мин) успешная ipify-проба делала
consecutive_fails=0 + enabled=true. Дешёвая проба стирала вердикт дорогого тракта —
узел, мёртвый для браузера, выходил из карантина каждые полчаса и снова забирал прогон.

Разведено тем же приёмом, что #2711 (scrape_runs.ban_kind) — поле РЯДОМ, а не новое
значение существующего флага. Миграция 228: browser_fail_streak / browser_unfit_since /
browser_check_at. Успешная ipify их не трогает, провал браузерной пробы не трогает
enabled/consecutive_fails.

Проба: POST /fetch (одна навигация) на robots.txt Авито через сайдкар с прокси узла в
теле. /fetch, а не /fetch-json — последний сначала грузит главную площадки. source=
'generic', чтобы не забирать лок боевого инстанса и не релончить его.

Такт решён замером на проде: одна браузерная проба = 8.3с и один запуск camoufox,
боевая нагрузка сайдкара = ~1000 /fetch и ~190 запусков camoufox в сутки. Каждым
прогоном healthcheck по 4 узлам это +192 запуска (удвоение), поэтому такт 360 мин:
+16 запусков (+8%). Неподтверждённый провал такт не двигает — второе подтверждение
приходит на следующем прогоне (~30 мин), не через 6 часов.

Пометка НЕ выводит узел из пула (пул 4 узла, #2638): acquire отдаёт его последним
(ORDER BY) и громко логирует, если пригодных не осталось. Путь обратно — успешная
браузерная проба (browser_refit). Отказ, принадлежащий сайдкару или площадке, узлу не
засчитывается — иначе одна упавшая общая зависимость пометила бы весь пул (#2686).

Refs #2723
bot-backend merged commit 64a7975549 into main 2026-08-06 12:50:24 +00:00
bot-backend deleted branch fix/2723-healthcheck-real-path 2026-08-06 12:50:25 +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#2736
No description provided.