fix(tradein/scrapers): egress по источнику из пула с учётом банов, а не статичный env #2831

Merged
lekss361 merged 1 commit from fix/tradein-egress into main 2026-08-11 06:29:49 +00:00

1 commit

Author SHA1 Message Date
bot-backend
276f544e66 fix(tradein/scrapers): egress по источнику из пула с учётом банов, а не статичный env
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
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
CI Trade-In / backend-tests (pull_request) Successful in 3m58s
Прод-инцидент 2026-08-10: SCRAPER_PROXY_URL был единственным egress для
прямых curl_cffi/httpx-сессий в app/services/* и app/tasks/*, идущих мимо
proxy_pool и scraper_kit-оркестрации. Он указывал на asocks-residential-1 —
узел, забаненный в scrape_proxy_source_bans и для avito, и для cian до
13.08, при трёх здоровых мобильных узлах в пуле.

Система вела учёт банов в БД, но фактический выход в сеть про эту таблицу
не знал и месяц ходил через забаненный узел. Снаружи это выглядело как
«Cian: плохие куки» и «Avito: IP banned» — то есть маскировалось под
проблему доступов.

- proxy_egress.resolve_proxy_url(db, source) — read-only выбор узла:
  enabled, consecutive_fails < MAX_CONSECUTIVE_FAILS, нет активного бана
  для этого источника; при равенстве — свежий last_ok_at. Без lease:
  вызывающие делают короткие одноразовые fetch'и без гарантии release.
- fail-closed при исчерпании (принцип #2616): пул НЕ пуст, но все узлы
  отсеяны → ProxyPoolExhaustedError и отказ, без обхода через env. Иначе
  в худший момент система возвращалась бы ровно к поведению, вызвавшему
  инцидент. Пустой пул (dev без БД) → env с warning, это легитимно.
  Ошибка БД трактуется как «пул пуст», а не как исчерпание, — временный
  сбой соединения не должен останавливать сбор.
- Наблюдаемость: выбранный узел логируется (id + label/host:port, никогда
  не URL целиком — он несёт credentials); тексты «пуст» и «FAIL-CLOSED»
  различаются, у второго уровень error. Отсутствие такого сигнала и
  спрятало проблему на месяц.
- Переведены cian_session, yandex_address_backfill, avito_detail_backfill,
  yandex_detail_backfill. browser/server.py намеренно не тронут — у него
  свой контракт через env.

Хвосты вне этого PR: #2830.
2026-08-11 09:22:33 +03:00