fix(tradein/avito): повтор после блока терял прокси и уходил с адреса машины #3143

Merged
lekss361 merged 1 commit from fix/3034-detail-retry-loses-proxy into main 2026-08-27 12:40:12 +00:00
Owner

Найдено при проверке, помогла ли правка отпечатка из #3034. Помогла — и вскрыла следующий слой.

Дефект

Обе ветки повтора в fetch_detail — 403/firewall и 429 — пересоздавали эфемерную сессию вызовом _build_detail_session() без config. Прокси kit-версия читает только из config.scraper_proxy_url, поэтому такая сессия уходила напрямую.

Замысел ветки прямо обратный, он записан в её же комментарии:

Эфемерная свежая сессия (новый CONNECT-туннель = свежий exit-IP)

И ветка вообще исполняется только при backconnect=True, который вычисляется как «задан config.scraper_proxy_url». То есть в момент вызова достоверно известно, что прокси есть — и ровно здесь он терялся.

Почему это не было видно

Пока адрес самой машины не был заблокирован, прямой повтор часто срабатывал, и подмена канала выглядела как успех.

Замер 27.08 из прод-контейнера, один и тот же URL Авито, один и тот же код:

канал результат
через прокси 200, 3.3 МБ настоящей страницы
напрямую 429, «доступ ограничен», firewall

С момента, когда адрес машины попал под ограничение, каждый повтор после блока стал обречён.

Совпадение с данными

Обогащение avito_detail_backfill по суткам:

сутки обогащено блоков
16–20.08 (до правки отпечатка) 2–10 0–1
21.08 178 77
22.08 129 95
23.08 147 134
24.08 138 93
25.08 111 107
26.08 23 66
27.08 0 25

Почасовой срез показывает, что ноль наступает начиная с окна, в котором сменился адрес машины, и держится все последующие прогоны подряд.

Попутно это закрывает вопрос по #3034: правка отпечатка дала рост примерно с 4 до 140 обогащений в сутки и держалась пять суток. Обвал — другая причина, и она здесь.

Тесты

Три штуки:

  1. Обе ветки на месте — страховка от того, что тест начнёт проверять пустоту после рефакторинга.
  2. Ни одна ветка не строит сессию без config — собственно инвариант.
  3. Без config прокси в сессии действительно нет — доказываем цену пропуска на настоящей сессии, а не верим на слово.

Проверил, что тест краснеет на старом коде:

AssertionError: повтор строит сессию как _build_detail_session('') —
прокси теряется, запрос уйдёт напрямую
$ uv run python -m pytest tests/scrapers/ -q
107 passed

$ uv run ruff check …
All checks passed!

Тот же класс ошибки чинили в #2330 для build_warmed_session — в пути повтора он оставался.

Refs #3034, #3045

Найдено при проверке, помогла ли правка отпечатка из #3034. Помогла — и вскрыла следующий слой. ## Дефект Обе ветки повтора в `fetch_detail` — 403/firewall и 429 — пересоздавали эфемерную сессию вызовом `_build_detail_session()` **без `config`**. Прокси kit-версия читает только из `config.scraper_proxy_url`, поэтому такая сессия уходила напрямую. Замысел ветки прямо обратный, он записан в её же комментарии: > Эфемерная свежая сессия (**новый CONNECT-туннель = свежий exit-IP**) И ветка вообще исполняется только при `backconnect=True`, который вычисляется как «задан `config.scraper_proxy_url`». То есть в момент вызова достоверно известно, что прокси есть — и ровно здесь он терялся. ## Почему это не было видно Пока адрес самой машины не был заблокирован, прямой повтор часто срабатывал, и подмена канала выглядела как успех. Замер 27.08 из прод-контейнера, один и тот же URL Авито, один и тот же код: | канал | результат | |---|---| | через прокси | **200**, 3.3 МБ настоящей страницы | | напрямую | **429**, «доступ ограничен», firewall | С момента, когда адрес машины попал под ограничение, каждый повтор после блока стал обречён. ## Совпадение с данными Обогащение `avito_detail_backfill` по суткам: | сутки | обогащено | блоков | |---|---:|---:| | 16–20.08 (до правки отпечатка) | 2–10 | 0–1 | | 21.08 | **178** | 77 | | 22.08 | 129 | 95 | | 23.08 | 147 | 134 | | 24.08 | 138 | 93 | | 25.08 | 111 | 107 | | 26.08 | 23 | 66 | | **27.08** | **0** | 25 | Почасовой срез показывает, что ноль наступает начиная с окна, в котором сменился адрес машины, и держится все последующие прогоны подряд. Попутно это закрывает вопрос по #3034: правка отпечатка дала рост примерно с 4 до 140 обогащений в сутки и держалась пять суток. Обвал — другая причина, и она здесь. ## Тесты Три штуки: 1. **Обе ветки на месте** — страховка от того, что тест начнёт проверять пустоту после рефакторинга. 2. **Ни одна ветка не строит сессию без `config`** — собственно инвариант. 3. **Без `config` прокси в сессии действительно нет** — доказываем цену пропуска на настоящей сессии, а не верим на слово. Проверил, что тест краснеет на старом коде: ``` AssertionError: повтор строит сессию как _build_detail_session('') — прокси теряется, запрос уйдёт напрямую ``` ``` $ uv run python -m pytest tests/scrapers/ -q 107 passed $ uv run ruff check … All checks passed! ``` Тот же класс ошибки чинили в #2330 для `build_warmed_session` — в пути повтора он оставался. Refs #3034, #3045
lekss361 added 1 commit 2026-08-27 12:34:02 +00:00
fix(tradein/avito): повтор после блока терял прокси и уходил с адреса машины
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 8s
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 Trade-In / backend-tests (pull_request) Successful in 4m52s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
26cd8dbe34
Обе ветки повтора в `fetch_detail` — 403/firewall и 429 — пересоздавали
эфемерную сессию вызовом `_build_detail_session()` БЕЗ `config`. Прокси
kit-версия читает только из `config.scraper_proxy_url`, поэтому такая сессия
уходила напрямую.

Замысел ветки прямо обратный, он записан в её же комментарии: «эфемерная
свежая сессия (новый CONNECT-туннель = свежий exit-IP)». Ветка вообще
исполняется только при backconnect=True, а он вычисляется как «задан
config.scraper_proxy_url» — то есть в момент вызова достоверно известно, что
прокси есть, и он терялся.

ПОЧЕМУ НЕ БЫЛО ВИДНО. Пока адрес самой машины не был заблокирован, прямой
повтор часто срабатывал, и подмена канала выглядела как успех. Замер 27.08 из
прод-контейнера, один и тот же URL Авито:

    через прокси — 200, 3.3 МБ страницы
    напрямую     — 429, «доступ ограничен», firewall

С этого момента каждый повтор после блока обречён. Обогащение
avito_detail_backfill по суткам: 21-25.08 — 178/129/147/138/111, 26.08 — 23,
27.08 — 0 при 25 блоках. Обвал начинается ровно с окна, в котором сменился
адрес машины.

Тот же класс ошибки чинили в #2330 для build_warmed_session; в пути повтора он
оставался.

Три теста: обе ветки на месте (страховка от проверки пустоты), ни одна не
строит сессию без config, и отдельно доказано, что без config прокси в сессии
действительно нет. Проверил красноту на старом коде — падает с точным текстом.
Прогон: 107 тестов scrapers зелёные, ruff чист.

Refs #3034, #3045
Author
Owner

Дополнение: диагноз подтверждается естественным опытом, который уже поставлен за нас.

Соседний провайдер делает то же самое — но правильно

providers/avito/serp.py::_build_cffi_sessionметод, читающий self._config.scraper_proxy_url:

def _build_cffi_session(self) -> AsyncSession:
    ...
    return build_document_session(proxy_url=self._config.scraper_proxy_url, timeout=25)

Его пересоздание (_reset_cffi) сделано ради ровно той же цели — «новый CONNECT-туннель = свежий exit-IP» — и прокси при этом не теряет, потому что берёт его из состояния объекта, а не из аргумента.

В detail.py та же операция вынесена в свободную функцию с необязательным config, и два места из четырёх вызывали её пустой.

Что из этого следует для чисел

Если диагноз верен, страдать должен ровно detail_backfill, а свипы через serp.py — нет. Проверил по scrape_runs, деля на «до/после 21.08 17:02» (правка отпечатка):

источник какой код до после
avito_city_sweep serp.py (прокси сохраняется) 53% 57%
avito_newbuilding_sweep serp.py 87% 83%
avito_detail_backfill detail.py (прокси терялся) 13% 0%

Свипы держат свои полтора-восемь десятых и никак не реагируют на блокировку адреса машины — они через него и не ходят. Умер только тот путь, где повтор сваливался на прямое соединение.

Проверил заодно остальные _build_*_session() в kit: вызовов без аргументов больше нет нигде, кроме этих двух исправленных.

Дополнение: диагноз подтверждается естественным опытом, который уже поставлен за нас. ## Соседний провайдер делает то же самое — но правильно `providers/avito/serp.py::_build_cffi_session` — **метод**, читающий `self._config.scraper_proxy_url`: ```python def _build_cffi_session(self) -> AsyncSession: ... return build_document_session(proxy_url=self._config.scraper_proxy_url, timeout=25) ``` Его пересоздание (`_reset_cffi`) сделано ради ровно той же цели — «новый CONNECT-туннель = свежий exit-IP» — и прокси при этом не теряет, потому что берёт его из состояния объекта, а не из аргумента. В `detail.py` та же операция вынесена в **свободную функцию** с необязательным `config`, и два места из четырёх вызывали её пустой. ## Что из этого следует для чисел Если диагноз верен, страдать должен ровно `detail_backfill`, а свипы через `serp.py` — нет. Проверил по `scrape_runs`, деля на «до/после 21.08 17:02» (правка отпечатка): | источник | какой код | до | после | |---|---|---:|---:| | `avito_city_sweep` | serp.py (прокси сохраняется) | 53% | **57%** | | `avito_newbuilding_sweep` | serp.py | 87% | **83%** | | `avito_detail_backfill` | detail.py (прокси терялся) | 13% | **0%** | Свипы держат свои полтора-восемь десятых и никак не реагируют на блокировку адреса машины — они через него и не ходят. Умер только тот путь, где повтор сваливался на прямое соединение. Проверил заодно остальные `_build_*_session()` в kit: вызовов без аргументов больше нет нигде, кроме этих двух исправленных.
lekss361 merged commit 1cefd806b3 into main 2026-08-27 12:40:12 +00:00
lekss361 deleted branch fix/3034-detail-retry-loses-proxy 2026-08-27 12:40:12 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#3143
No description provided.