fix(avito): graceful sidecar HTTP 500/503 in browser SERP fetch (#1790) #1896

Merged
bot-backend merged 1 commit from fix/avito-full-load-browser-500 into main 2026-06-25 06:47:42 +00:00
Collaborator

Контекст

Follow-up к #1790. Часть 1 issue (AVITO_PROXY_MAX_ROTATIONS=0→4) уже исправлена на проде — бан-шторм прекратился (с 06-18 ноль banned у avito_full_load, см. comment). Этот PR чинит новый доминирующий отказ: browser-sidecar HTTP 500/503.

Проблема

SERP-фетч идёт через tradein-browser sidecar (POST /fetch) с BrowserFetcher. Sidecar отдаёт HTTP 500 на любую внутреннюю ошибку страницы (тело {"error": "ErrorType: message"}) — NS_ERROR_PROXY_TOO_MANY_REQUESTS (soft proxy-бан), Page.goto Timeout (transient) — и HTTP 503 когда браузер не поднялся.

_fetch_serp_html_browser вызывал self._browser.fetch(url) без обёртки — обрабатывал только rendered firewall-HTML. httpx.HTTPStatusError от sidecar всплывал неперехваченным → generic except Exception в run_avito_full_loadmark_failed + re-raise.

Прод-инцидент run #356 (06-24): unique_fetched=3207, saved_inserted=466, saved_updated=2720, затем один поздний 500 → весь run помечен failed, хотя 3186 карточек уже сохранены через инкрементальный on_bucket. Один транзиентный 500 и нукал статус, и обрывал остаток bucket-walk.

Фикс

_fetch_serp_html_browser: ловим httpx.HTTPStatusError, устойчиво парсим тело, классифицируем и роутим в существующую rotation/retry-машинерию:

Класс Сигнатура Действие
soft-ban too_many_requests / status 503 / browser unavailable ротация exit-IP (общий бюджет с firewall-путём, max_rot)
transient Timeout / прочие 5xx bounded backoff-retry того же url БЕЗ ротации (_AVITO_SIDECAR_TRANSIENT_RETRIES=2)
оба исчерпаны raise AvitoRateLimitedError → graceful mark_banned (partial preserved)

AvitoRateLimitedError ловится per-bucket tolerance в fetch_all_secondary (одиночный блок → бакет скипается, обход продолжается; только N-подряд абортит) и run-level except (AvitoBlockedError, AvitoRateLimitedError)mark_banned. Termination гарантирован: каждый continue тратит конечный бюджет (rot_done<max_rot либо transient_left>0); worst-case = max_rot + 2 + 1 итераций.

Намеренно НЕ blindly continue — это добило бы rate-limited прокси.

Изменение маршрутизации (для пост-review)

Раньше sidecar-500 на probe-странице ловился в _probe_total except httpx.HTTPStatusErrortotal=None → тихий split/skip бакета. Теперь, при исчерпании новых бюджетов, surface как AvitoRateLimitedError (ловится раньше в _probe_total) → per-bucket tolerance. Net-эффект тот же (бакет скипается, partial preserved), но отказ теперь считается как per-bucket block, а не «тихий total=None».

Тесты

tests/test_avito_browser_mode.py +5 тестов (секция #356): rotate-then-succeed, proxy-too-many exhausted → AvitoRateLimitedError, transient bounded (no infinite loop), 503 unavailable exhausted, mixed transient→soft-ban (независимые бюджеты).

uv run pytest tests/test_avito_browser_mode.py tests/test_ban_rotation_1790.py -q → 20 passed
ruff check → All checks passed

Deep-code-review: APPROVE (full propagation chain traced, termination proven, firewall-путь untouched).

Out of scope

Инфра не трогаю: tradein-browser был OOMKilled (лимит 5G), единый мобильный IP перегружен параллельным cian+avito+yandex трафиком (NS_ERROR_PROXY_TOO_MANY_REQUESTS в логах преимущественно Cian) — это отдельный proxy-pool / mem-limit follow-up.

## Контекст Follow-up к #1790. Часть 1 issue (`AVITO_PROXY_MAX_ROTATIONS=0→4`) уже исправлена на проде — бан-шторм прекратился (с 06-18 ноль `banned` у `avito_full_load`, см. [comment](https://git.gendsgn.ru/lekss361/gendesign/issues/1790#issuecomment-16347)). Этот PR чинит **новый доминирующий отказ**: browser-sidecar HTTP 500/503. ## Проблема SERP-фетч идёт через `tradein-browser` sidecar (`POST /fetch`) с `BrowserFetcher`. Sidecar отдаёт **HTTP 500** на любую внутреннюю ошибку страницы (тело `{"error": "ErrorType: message"}`) — `NS_ERROR_PROXY_TOO_MANY_REQUESTS` (soft proxy-бан), `Page.goto Timeout` (transient) — и **HTTP 503** когда браузер не поднялся. `_fetch_serp_html_browser` вызывал `self._browser.fetch(url)` **без обёртки** — обрабатывал только rendered firewall-HTML. `httpx.HTTPStatusError` от sidecar всплывал неперехваченным → generic `except Exception` в `run_avito_full_load` → `mark_failed` + re-raise. **Прод-инцидент run #356** (06-24): `unique_fetched=3207, saved_inserted=466, saved_updated=2720`, затем один поздний 500 → весь run помечен `failed`, хотя **3186 карточек уже сохранены** через инкрементальный `on_bucket`. Один транзиентный 500 и нукал статус, и обрывал остаток bucket-walk. ## Фикс `_fetch_serp_html_browser`: ловим `httpx.HTTPStatusError`, устойчиво парсим тело, классифицируем и роутим в **существующую** rotation/retry-машинерию: | Класс | Сигнатура | Действие | |---|---|---| | soft-ban | `too_many_requests` / status 503 / `browser unavailable` | ротация exit-IP (общий бюджет с firewall-путём, `max_rot`) | | transient | `Timeout` / прочие 5xx | bounded backoff-retry того же url БЕЗ ротации (`_AVITO_SIDECAR_TRANSIENT_RETRIES=2`) | | оба исчерпаны | — | `raise AvitoRateLimitedError` → graceful `mark_banned` (partial preserved) | `AvitoRateLimitedError` ловится per-bucket tolerance в `fetch_all_secondary` (одиночный блок → бакет скипается, обход продолжается; только N-подряд абортит) и run-level `except (AvitoBlockedError, AvitoRateLimitedError)` → `mark_banned`. **Termination гарантирован**: каждый `continue` тратит конечный бюджет (`rot_done<max_rot` либо `transient_left>0`); worst-case = `max_rot + 2 + 1` итераций. Намеренно НЕ blindly `continue` — это добило бы rate-limited прокси. ## Изменение маршрутизации (для пост-review) Раньше sidecar-500 на **probe**-странице ловился в `_probe_total` `except httpx.HTTPStatusError` → `total=None` → тихий split/skip бакета. Теперь, при исчерпании новых бюджетов, surface как `AvitoRateLimitedError` (ловится раньше в `_probe_total`) → per-bucket tolerance. Net-эффект тот же (бакет скипается, partial preserved), но отказ теперь **считается** как per-bucket block, а не «тихий total=None». ## Тесты `tests/test_avito_browser_mode.py` +5 тестов (секция #356): rotate-then-succeed, proxy-too-many exhausted → `AvitoRateLimitedError`, transient bounded (no infinite loop), 503 unavailable exhausted, mixed transient→soft-ban (независимые бюджеты). ``` uv run pytest tests/test_avito_browser_mode.py tests/test_ban_rotation_1790.py -q → 20 passed ruff check → All checks passed ``` Deep-code-review: ✅ APPROVE (full propagation chain traced, termination proven, firewall-путь untouched). ## Out of scope Инфра не трогаю: `tradein-browser` был `OOMKilled` (лимит 5G), единый мобильный IP перегружен параллельным cian+avito+yandex трафиком (`NS_ERROR_PROXY_TOO_MANY_REQUESTS` в логах преимущественно Cian) — это отдельный proxy-pool / mem-limit follow-up.
bot-backend added 1 commit 2026-06-25 06:45:53 +00:00
fix(avito): graceful sidecar HTTP 500/503 in browser SERP fetch (#1790)
All checks were successful
CI / changes (pull_request) Successful in 8s
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
e65b4d44d5
_fetch_serp_html_browser вызывал BrowserFetcher.fetch без обёртки, поэтому
httpx.HTTPStatusError от tradein-browser sidecar (500 NS_ERROR_PROXY_TOO_MANY_REQUESTS
/ Page.goto Timeout, 503 browser-unavailable) всплывал в generic except
run_avito_full_load → mark_failed + re-raise, роняя весь обход после тысяч
инкрементально сохранённых карточек (прод run #356: 3186 сохранено, один поздний
500 → весь run failed).

Классифицируем тело sidecar-ошибки и роутим в существующую rotation/retry-машинерию:
soft-ban (too_many_requests / 503 / browser unavailable) → ротация exit-IP;
transient (timeout / прочие 5xx) → bounded backoff-retry без ротации; оба бюджета
исчерпаны → AvitoRateLimitedError, чтобы run ушёл в graceful mark_banned
(partial preserved, scheduler retry next cycle), а не mark_failed.

Часть 1 issue #1790 (AVITO_PROXY_MAX_ROTATIONS=0→4) уже исправлена на проде;
это browser-resilience follow-up.

Refs #1790
bot-backend merged commit 4eaf184d0e into main 2026-06-25 06:47:42 +00:00
bot-backend deleted branch fix/avito-full-load-browser-500 2026-06-25 06:47:42 +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#1896
No description provided.