feat(tradein/avito): ротация exit-IP по счётчику попыток в доборе карточек #3302

Merged
lekss361 merged 2 commits from feat/avito-rotate-by-attempt-counter into main 2026-08-31 12:09:04 +00:00
Owner

Summary

Включает в работу инфраструктуру ротации из #3298. Каждые N попыток (не успехов) задача зовёт rotate_proxy(), ждёт reconnect_delay_s, затем request_context_reset() — свежий адрес со старыми куками бесполезен.

  • Порог — настройка AVITO_DETAIL_BACKFILL_ROTATE_AFTER_ATTEMPTS, дефолт 15.
  • Счётчик обнуляется до вызова: иначе исчерпанный лимит провайдера заставил бы дёргать ротацию на каждой следующей попытке.
  • Отказ ротации не роняет прогон, не вызывает сброс контекста и не считается блокировкой площадки — в counters.blocked / ban_kinds не попадает и прерывание по критерию «14 блоков из 20» не приближает.
  • BrowserFetcher.lease_id — read-only property, lease-механика (sticky-lease, heartbeat через touch()) не тронута.

Зачем — замеры на живых портах

Один и тот же стенд, 39 карточек:

порт оператор ротация итог четверти
#540298 Билайн, mobile=false нет 13/39 = 33% 100 / 44 / 0 / 0
#540723 МегаФон, mobile=true нет 27/39 = 69% 78 / 67 / 78 / 58
#540298 Билайн (тот же) каждые 15 35/45 = 78% 91 / 64 / 73 / 83

Билайновский порт умирает на 18-20 карточке и не восстанавливается. С ротацией обвал исчезает совсем. Порог вынесен в настройку именно потому, что зависит от оператора: Билайну нужно ~15, настоящим мобильным хватает ~30.

Ожидание после смены не косметическое: сразу после ротации канал 2-12 с нерабочий — в замере две карточки подряд ушли в таймаут, ещё одна получила 439. Без паузы каждая ротация дарила бы два ложных «блока».

Про режим прода

Ротация работает только в browser-режиме — только там задача держит lease с proxy_id. Проверено на проде: планировщик живёт в tradein-scraper, а там AVITO_DETAIL_BACKFILL_USE_CURL=false при SCRAPER_FETCH_MODE=browser и USE_PROXY_POOL_BROWSER=true — фича активна. Значение true стоит только у tradein-backend, который добор не запускает. Предсуществующий комментарий в файле называл use_curl=True «прод-дефолтом» — это неверно, поправлено в докстринге новой функции.

Test plan

  • pytest tests/tasks/test_avito_detail_backfill.py — 41 passed (8 новых)
  • pytest tests/tasks — 203 passed, 2 skipped
  • pytest tests/services — 298 passed
  • ruff check — чисто
  • Счётчик считает все исходы: между инкрементом (стр. 580) и блоком ротации (стр. 858) нет ни одного continue
  • Ожидание через await asyncio.sleep, event loop не блокируется
  • code-reviewer: APPROVE, блокеров нет
## Summary Включает в работу инфраструктуру ротации из #3298. Каждые N **попыток** (не успехов) задача зовёт `rotate_proxy()`, ждёт `reconnect_delay_s`, затем `request_context_reset()` — свежий адрес со старыми куками бесполезен. - Порог — настройка `AVITO_DETAIL_BACKFILL_ROTATE_AFTER_ATTEMPTS`, дефолт 15. - Счётчик обнуляется **до** вызова: иначе исчерпанный лимит провайдера заставил бы дёргать ротацию на каждой следующей попытке. - Отказ ротации не роняет прогон, не вызывает сброс контекста и **не считается блокировкой площадки** — в `counters.blocked` / `ban_kinds` не попадает и прерывание по критерию «14 блоков из 20» не приближает. - `BrowserFetcher.lease_id` — read-only property, lease-механика (sticky-lease, heartbeat через `touch()`) не тронута. ## Зачем — замеры на живых портах Один и тот же стенд, 39 карточек: | порт | оператор | ротация | итог | четверти | |---|---|---|---|---| | #540298 | Билайн, `mobile=false` | нет | 13/39 = **33%** | 100 / 44 / **0** / **0** | | #540723 | МегаФон, `mobile=true` | нет | 27/39 = **69%** | 78 / 67 / 78 / 58 | | #540298 | Билайн (тот же) | каждые 15 | 35/45 = **78%** | 91 / 64 / 73 / 83 | Билайновский порт умирает на 18-20 карточке и не восстанавливается. С ротацией обвал исчезает совсем. Порог вынесен в настройку именно потому, что зависит от оператора: Билайну нужно ~15, настоящим мобильным хватает ~30. **Ожидание после смены не косметическое:** сразу после ротации канал 2-12 с нерабочий — в замере две карточки подряд ушли в таймаут, ещё одна получила 439. Без паузы каждая ротация дарила бы два ложных «блока». ## Про режим прода Ротация работает только в browser-режиме — только там задача держит lease с `proxy_id`. Проверено на проде: планировщик живёт в `tradein-scraper`, а там `AVITO_DETAIL_BACKFILL_USE_CURL=false` при `SCRAPER_FETCH_MODE=browser` и `USE_PROXY_POOL_BROWSER=true` — фича активна. Значение `true` стоит только у `tradein-backend`, который добор не запускает. Предсуществующий комментарий в файле называл `use_curl=True` «прод-дефолтом» — это неверно, поправлено в докстринге новой функции. ## Test plan - [x] `pytest tests/tasks/test_avito_detail_backfill.py` — 41 passed (8 новых) - [x] `pytest tests/tasks` — 203 passed, 2 skipped - [x] `pytest tests/services` — 298 passed - [x] `ruff check` — чисто - [x] Счётчик считает все исходы: между инкрементом (стр. 580) и блоком ротации (стр. 858) нет ни одного `continue` - [x] Ожидание через `await asyncio.sleep`, event loop не блокируется - [x] code-reviewer: ✅ APPROVE, блокеров нет
lekss361 added 1 commit 2026-08-31 11:38:44 +00:00
feat(tradein/avito): ротация exit-IP по счётчику попыток в доборе карточек
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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) Failing after 5m22s
ef99ab2ea1
Один 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 вообще без ротации).

Счётчик обнуляется ДО вызова, иначе исчерпанный лимит провайдера заставил бы
дёргать ротацию на каждой следующей попытке. Отказ ротации не роняет прогон и
не считается блокировкой площадки.
bot-backend added 1 commit 2026-08-31 11:58:22 +00:00
test(tradein/avito): дефолт порога ротации в _fake_settings warm-context тестов
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 5m15s
8cf31aba04
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 и комментарий, почему он тут нужен.
lekss361 merged commit 9219ab6507 into main 2026-08-31 12:09:04 +00:00
lekss361 deleted branch feat/avito-rotate-by-attempt-counter 2026-08-31 12:09:05 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3302
No description provided.