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
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
Обе ветки повтора в `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
This commit is contained in:
parent
9e37262ea0
commit
26cd8dbe34
2 changed files with 103 additions and 2 deletions
|
|
@ -0,0 +1,88 @@
|
|||
"""Повтор после блока обязан сохранять прокси, ради которого он и затеян.
|
||||
|
||||
БАГ. В `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`; здесь он
|
||||
оставался в пути повтора.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import re
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from scraper_kit.providers.avito import detail as kit_detail
|
||||
|
||||
SOURCE = Path(inspect.getfile(kit_detail)).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _retry_rebuild_calls() -> list[str]:
|
||||
"""Все места, где пересоздаётся эфемерная `retry_session`."""
|
||||
return re.findall(r"retry_session = _build_detail_session\(([^)]*)\)", SOURCE)
|
||||
|
||||
|
||||
def test_both_retry_branches_exist() -> None:
|
||||
"""Страховка от того, что тест начнёт проверять пустоту.
|
||||
|
||||
Веток две — 403/firewall и 429; если рефакторинг их переименует, тест ниже
|
||||
молча перестанет что-либо проверять, поэтому фиксируем количество.
|
||||
"""
|
||||
assert len(_retry_rebuild_calls()) == 2, (
|
||||
f"ожидались две ветки повтора, найдено: {_retry_rebuild_calls()}"
|
||||
)
|
||||
|
||||
|
||||
def test_retry_session_is_rebuilt_with_config() -> None:
|
||||
"""Ни одна ветка повтора не смеет строить сессию без `config`.
|
||||
|
||||
Пустой аргумент = `proxy_url=None` = выход напрямую с адреса машины, тогда
|
||||
как вся ветка существует ради свежего exit-IP ЧЕРЕЗ прокси.
|
||||
"""
|
||||
for arg in _retry_rebuild_calls():
|
||||
assert arg.strip() == "config", (
|
||||
"повтор строит сессию как _build_detail_session("
|
||||
f"{arg!r}) — прокси теряется, запрос уйдёт напрямую"
|
||||
)
|
||||
|
||||
|
||||
def test_session_without_config_really_has_no_proxy() -> None:
|
||||
"""Доказываем цену пропуска, а не верим на слово.
|
||||
|
||||
Ровно то же построение, что делала сломанная ветка: без config прокси в
|
||||
сессии нет, с config — есть.
|
||||
"""
|
||||
proxy = "http://user:pass@proxy.example:8080"
|
||||
with_cfg = kit_detail._build_detail_session(SimpleNamespace(scraper_proxy_url=proxy))
|
||||
without_cfg = kit_detail._build_detail_session()
|
||||
try:
|
||||
assert proxy in str(getattr(with_cfg, "proxies", "")), "прокси не доехал из config"
|
||||
assert not getattr(without_cfg, "proxies", None), (
|
||||
"сессия без config неожиданно с прокси — тест ниже потерял бы смысл"
|
||||
)
|
||||
finally:
|
||||
for s in (with_cfg, without_cfg):
|
||||
close = getattr(s, "close", None)
|
||||
if close is not None and not inspect.iscoroutinefunction(close):
|
||||
close()
|
||||
|
|
@ -597,7 +597,9 @@ async def fetch_detail(
|
|||
await retry_session.close()
|
||||
except Exception:
|
||||
pass
|
||||
retry_session = _build_detail_session()
|
||||
# config ОБЯЗАТЕЛЕН: без него прокси не подставляется
|
||||
# вовсе — разбор того, чем это кончается, у 429-ветки ниже.
|
||||
retry_session = _build_detail_session(config)
|
||||
attempt_session = retry_session
|
||||
logger.info(
|
||||
"avito detail 403/firewall/soft-block %s — backconnect reconnect "
|
||||
|
|
@ -624,7 +626,18 @@ async def fetch_detail(
|
|||
await retry_session.close()
|
||||
except Exception:
|
||||
pass
|
||||
retry_session = _build_detail_session()
|
||||
# config ОБЯЗАТЕЛЕН. Без него `_build_detail_session` берёт
|
||||
# proxy_url=None, и «эфемерная сессия ради свежего exit-IP»
|
||||
# уходит НАПРЯМУЮ с адреса самой машины — то есть делает
|
||||
# ровно обратное замыслу. Ветка исполняется только когда
|
||||
# backconnect=True, а он и означает «прокси задан»: мы
|
||||
# достоверно знаем, что прокси есть, и теряли его здесь.
|
||||
#
|
||||
# Замер 27.08 из прод-контейнера, один и тот же URL:
|
||||
# через прокси — 200, 3.3 МБ страницы
|
||||
# напрямую — 429, «доступ ограничен», firewall
|
||||
# То есть каждый повтор после блока был обречён.
|
||||
retry_session = _build_detail_session(config)
|
||||
attempt_session = retry_session
|
||||
logger.info(
|
||||
"avito detail HTTP 429 short-retry exhausted %s — "
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue