CI поймал то, что не поймал локальный прогон tests/tasks: восемь тестов в
tests/test_3251_avito_warm_context.py держат собственную копию _fake_settings,
без нового avito_detail_backfill_rotate_after_attempts. Bare MagicMock отдаёт
на него дочерний мок, и сравнение attempts_since_rotation >= settings....
падает TypeError: '>=' not supported between 'int' and 'MagicMock'.
Ловушка в проекте известна — в этом же словаре уже лежат явные дефолты
detail_backfill_block_ratio_window/_threshold ровно по той же причине.
Добавлен реальный дефолт из config.py и комментарий, почему он тут нужен.
Один IP выдерживает ограниченное число карточек Авито, дальше выход падает до
нуля. Замер на 39 карточках: билайновский порт даёт 13/39, четверти
100/44/0/0, 24 бан-страницы, и после 18-20 карточки не восстанавливается.
Тот же порт со сменой IP каждые 15 карточек — 35/45, четверти 91/64/73/83:
обвал исчезает совсем. До этого пул не ротировал ни разу за всю историю.
Счётчик считает ПОПЫТКИ, а не успехи: неудачная карточка тратит бюджет
адреса так же, как удачная. По достижении порога зовём rotate_proxy (#3298),
ждём reconnect_delay_s и пересоздаём контекст через request_context_reset —
свежий адрес со старыми куками бесполезен.
Ожидание не косметическое: сразу после смены IP канал 2-12 секунд нерабочий,
в замере две карточки подряд ушли в таймаут, ещё одна получила 439. Без
паузы каждая ротация дарила бы два ложных блока, а 14 из 20 прерывают прогон.
Порог вынесен в настройку AVITO_DETAIL_BACKFILL_ROTATE_AFTER_ATTEMPTS: он
зависит от оператора. Билайну нужно ~15, настоящим мобильным операторам
хватает ~30 (МегаФон в том же замере дал 27/39 вообще без ротации).
Счётчик обнуляется ДО вызова, иначе исчерпанный лимит провайдера заставил бы
дёргать ротацию на каждой следующей попытке. Отказ ротации не роняет прогон и
не считается блокировкой площадки.