Прод-инцидент 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.