mark_health(ok) выставляется в __exit__ контекст-менеджера и ставит
ok=False только если из блока вылетело исключение. Мягкий бан приходит
валидным HTTP 200 с заглушкой (Авито «Доступ ограничен», капча Циана,
gate-заглушка Яндекса, QRATOR Домклика) и распознаётся ПОЗЖЕ, при разборе
HTML — уже вне блока. Итог: забаненный прокси получал ok=True,
consecutive_fails обнулялся, узел выдавали снова.
BrowserFetcher.report_ban() помечает текущий sticky-lease (#2640);
proxy_pool.mark_banned() выключает узел с disabled_reason='banned:<source>'
(колонка из #2610 — ipify-проба такой узел не воскрешает, иначе флаппинг).
Avito инструментирован на raise-сайтах, а не в __aexit__: pipeline
присваивает scraper._browser напрямую и не заходит в контекст-менеджер —
централизованный хук пропустил бы весь этот путь. Cian/Yandex проверяют
свои счётчики в __aexit__ ДО релиза lease. Curl-путь получил маркер
ProxyBanError (подмешан в Blocked-исключения; RateLimited намеренно нет —
он может быть нашим таймаутом).
Защита от выкоса пула: mark_banned не выключает узел, если он последний
достижимый для источника (EXISTS зеркалит логику acquire с учётом
affinity — domclick-узел не считается запасным для avito/cian/yandex).
pg_advisory_xact_lock сериализует проверку+апдейт: однострочный UPDATE не
атомарен поперёк строк, два параллельных бана разных узлов могли пройти
мимо защиты и выкосить пул без самолечения.
Защита от ложного бана (deep-review): у Яндекса транспортные сбои
(_http_get глотал исключения и отдавал status_code=0) считались в тот же
счётчик, что настоящая капча — добавлен признак transport_error, счётчик
трогают только контентные провалы (Циан так делал изначально). Плюс порог
attempts>=3 на обоих триггерах: админский одиночный прогон делает ровно
одну попытку, и 1/1=100% выключал бы здоровый узел. Боевой sweep делает
>=4 попытки, детект настоящего бана не страдает.
Refs #2600