fix(tradein/avito): браузерный бэкфилл ходил мимо пула прокси #3145
3 changed files with 125 additions and 6 deletions
|
|
@ -71,7 +71,7 @@ from app.core.config import settings
|
|||
from app.core.shutdown import shutdown_requested
|
||||
from app.services import scrape_runs as runs_mod
|
||||
from app.services.proxy_egress import resolve_proxy_url
|
||||
from app.services.scraper_adapters import RealScraperConfig
|
||||
from app.services.scraper_adapters import RealProxyProvider, RealScraperConfig
|
||||
|
||||
# #2397 Part D1 (#2330 закрыт): _AVITO_WARM_SEARCH_URL/build_warmed_session больше
|
||||
# НЕ legacy — kit's scraper_kit.providers.avito.detail.build_warmed_session() теперь
|
||||
|
|
@ -256,8 +256,29 @@ async def run_avito_detail_backfill(
|
|||
# эфемерную _build_detail_session() через settings.scraper_proxy_url (backconnect).
|
||||
# use_curl=False (legacy): browser_mode=True → BrowserFetcher/auv, как раньше.
|
||||
if browser_mode:
|
||||
# proxy_provider/use_pool/environment — обязательная часть проводки,
|
||||
# а не опция (#2698). Без них BrowserFetcher не кладёт "proxy" в тело
|
||||
# POST /fetch, сайдкар берёт свой env-прокси, и прогон уходит мимо
|
||||
# пула: ни выбора узла по affinity, ни учёта scrape_proxy_source_bans,
|
||||
# ни ротации при блоке. В логе это видно как proxy_lease_id=None.
|
||||
#
|
||||
# Ровно этот дефект уже чинили в house_imv_backfill (#2698) — там он
|
||||
# держал 35 из 35 отказов в каждом прогоне полтора месяца, пока
|
||||
# соседние свипы через ТОТ ЖЕ сайдкар тянули сотни объявлений. Здесь
|
||||
# он остался.
|
||||
#
|
||||
# Замер 27.08: прогон 5098 — mode=browser, proxy_lease_id=None,
|
||||
# 5 блоков подряд из 5 попыток, enriched=0. При этом пул здоров
|
||||
# (4 узла, все ok), а тот же URL через прокси отдаёт 200 и 3.3 МБ.
|
||||
_cfg = RealScraperConfig()
|
||||
browser_fetcher = BrowserFetcher(
|
||||
source="avito", endpoint=settings.browser_http_endpoint
|
||||
source="avito",
|
||||
endpoint=settings.browser_http_endpoint,
|
||||
proxy_provider=RealProxyProvider(),
|
||||
use_pool=_cfg.use_proxy_pool_browser,
|
||||
# Без environment отказ «пул пуст» на этом пути мёртв — фетчер
|
||||
# молча ушёл бы на env-прокси сайдкара (#2616 шаг 1).
|
||||
environment=_cfg.environment,
|
||||
)
|
||||
await browser_fetcher.__aenter__()
|
||||
own_browser = True
|
||||
|
|
|
|||
|
|
@ -777,10 +777,23 @@ async def test_backfill_use_curl_false_creates_browser_fetcher() -> None:
|
|||
)
|
||||
|
||||
# BrowserFetcher должен быть создан (source="avito", endpoint из settings — #2310
|
||||
# kit BrowserFetcher требует endpoint= обязательным keyword-only параметром)
|
||||
mock_bf_cls.assert_called_once_with(
|
||||
source="avito", endpoint=fake_settings.browser_http_endpoint
|
||||
)
|
||||
# kit BrowserFetcher требует endpoint= обязательным keyword-only параметром).
|
||||
#
|
||||
# Проверяем ВХОЖДЕНИЕ аргументов, а не полное равенство вызова. Прежняя
|
||||
# форма `assert_called_once_with(source=..., endpoint=...)` требовала, чтобы
|
||||
# других аргументов НЕ БЫЛО, и тем самым закрепляла дефект: именно
|
||||
# отсутствие проводки пула (#3045) уводило браузерный прогон мимо прокси —
|
||||
# 5 блоков из 5 попыток при полностью здоровом пуле. Проводка проверяется
|
||||
# отдельно ниже.
|
||||
mock_bf_cls.assert_called_once()
|
||||
_bf_kwargs = mock_bf_cls.call_args.kwargs
|
||||
assert _bf_kwargs["source"] == "avito"
|
||||
assert _bf_kwargs["endpoint"] == fake_settings.browser_http_endpoint
|
||||
# #3045: без этих трёх фетчер не кладёт "proxy" в тело POST /fetch, и
|
||||
# сайдкар уходит на свой env-прокси мимо пула (proxy_lease_id=None).
|
||||
assert _bf_kwargs.get("proxy_provider") is not None, "пул не подключён"
|
||||
assert "use_pool" in _bf_kwargs, "флаг пула не доезжает до фетчера"
|
||||
assert "environment" in _bf_kwargs, "без него отказ «пул пуст» мёртв (#2616)"
|
||||
# fetch_detail вызван с browser_fetcher установленным (не None)
|
||||
assert mock_fetch.call_count == 1
|
||||
_, kwargs = mock_fetch.call_args
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
"""Браузерный бэкфилл Авито обязан ходить через пул прокси, а не мимо него.
|
||||
|
||||
ДЕФЕКТ. В `avito_detail_backfill` ветка `browser_mode` конструировала
|
||||
`BrowserFetcher(source=..., endpoint=...)` без `proxy_provider`/`use_pool`/
|
||||
`environment`. Без них фетчер не кладёт `"proxy"` в тело `POST /fetch`, сайдкар
|
||||
берёт свой env-прокси, и прогон уходит мимо пула целиком: ни выбора узла по
|
||||
affinity, ни учёта `scrape_proxy_source_bans`, ни ротации при блоке. В логе это
|
||||
видно как `proxy_lease_id=None`.
|
||||
|
||||
ЭТО УЖЕ ЧИНИЛИ РЯДОМ. `house_imv_backfill` страдал ровно тем же (#2698): 35
|
||||
отказов из 35 попыток в каждом прогоне полтора месяца, пока соседние свипы через
|
||||
ТОТ ЖЕ сайдкар тянули сотни объявлений. Контракт там закреплён тестом
|
||||
`test_2698_imv_sidecar_wiring.py`; здесь тот же контракт для второго пути.
|
||||
|
||||
ЗАМЕР 27.08, прогон 5098:
|
||||
|
||||
mode=browser, proxy_lease_id=None
|
||||
BLOCKED #1..#5 подряд — firewall/soft-block (browser-mode)
|
||||
ABORT — 5 consecutive blocks, enriched=0 attempted=5
|
||||
|
||||
При этом пул был здоров (4 узла, все ok, ни один не занят), а тот же URL Авито
|
||||
через прокси отдавал 200 и 3.3 МБ страницы — то есть площадка нас пускала,
|
||||
просто запрос шёл не оттуда.
|
||||
|
||||
Проверяется исходник ровно того места, а не поведение целиком: прогнать задачу
|
||||
end-to-end означало бы поднять снапшоты, БД и сайдкар — цена, несоразмерная
|
||||
одному инварианту проводки.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||
|
||||
from app.tasks import avito_detail_backfill as adb
|
||||
|
||||
REQUIRED = ("proxy_provider", "use_pool", "environment")
|
||||
|
||||
|
||||
def _browser_fetcher_calls() -> list[ast.Call]:
|
||||
"""Все конструкторы BrowserFetcher в модуле."""
|
||||
tree = ast.parse(inspect.getsource(adb))
|
||||
return [
|
||||
node
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id == "BrowserFetcher"
|
||||
]
|
||||
|
||||
|
||||
def test_module_still_builds_a_browser_fetcher() -> None:
|
||||
"""Страховка от проверки пустоты: если конструктор уедет, тест ниже онемеет."""
|
||||
assert _browser_fetcher_calls(), (
|
||||
"в avito_detail_backfill не нашлось ни одного BrowserFetcher(...) — "
|
||||
"тест проводки перестал что-либо проверять"
|
||||
)
|
||||
|
||||
|
||||
def test_browser_fetcher_is_wired_to_the_proxy_pool() -> None:
|
||||
"""Каждый конструктор обязан получать все три аргумента проводки пула."""
|
||||
for call in _browser_fetcher_calls():
|
||||
passed = {kw.arg for kw in call.keywords if kw.arg}
|
||||
missing = [name for name in REQUIRED if name not in passed]
|
||||
assert not missing, (
|
||||
f"BrowserFetcher построен без {missing} — запрос уйдёт мимо пула "
|
||||
f"(proxy_lease_id=None), как в прогоне 5098"
|
||||
)
|
||||
|
||||
|
||||
def test_pool_flag_comes_from_config_not_hardcoded() -> None:
|
||||
"""`use_pool` читается из конфига, а не зашит константой.
|
||||
|
||||
Зашитый `True` отнял бы у владельца выключатель `USE_PROXY_POOL_BROWSER`,
|
||||
а зашитый `False` вернул бы дефект, ничего не сломав заметно.
|
||||
"""
|
||||
for call in _browser_fetcher_calls():
|
||||
for kw in call.keywords:
|
||||
if kw.arg == "use_pool":
|
||||
assert not isinstance(kw.value, ast.Constant), (
|
||||
"use_pool задан константой — выключатель конфига перестал работать"
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue