Merge pull request 'fix(tradein/avito): повтор после блока терял прокси и уходил с адреса машины' (#3143) from fix/3034-detail-retry-loses-proxy into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m40s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / test (push) Successful in 3m58s
Deploy Trade-In / deploy (push) Successful in 1m17s
Deploy Trade-In / deploy-status (push) Successful in 1s
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m40s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / test (push) Successful in 3m58s
Deploy Trade-In / deploy (push) Successful in 1m17s
Deploy Trade-In / deploy-status (push) Successful in 1s
This commit is contained in:
commit
1cefd806b3
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()
|
await retry_session.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
retry_session = _build_detail_session()
|
# config ОБЯЗАТЕЛЕН: без него прокси не подставляется
|
||||||
|
# вовсе — разбор того, чем это кончается, у 429-ветки ниже.
|
||||||
|
retry_session = _build_detail_session(config)
|
||||||
attempt_session = retry_session
|
attempt_session = retry_session
|
||||||
logger.info(
|
logger.info(
|
||||||
"avito detail 403/firewall/soft-block %s — backconnect reconnect "
|
"avito detail 403/firewall/soft-block %s — backconnect reconnect "
|
||||||
|
|
@ -624,7 +626,18 @@ async def fetch_detail(
|
||||||
await retry_session.close()
|
await retry_session.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
attempt_session = retry_session
|
||||||
logger.info(
|
logger.info(
|
||||||
"avito detail HTTP 429 short-retry exhausted %s — "
|
"avito detail HTTP 429 short-retry exhausted %s — "
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue