fix(tradein/avito): браузерный бэкфилл ходил мимо пула прокси
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m53s

Ветка browser_mode в avito_detail_backfill конструировала BrowserFetcher без
proxy_provider/use_pool/environment. Без них фетчер не кладёт "proxy" в тело
POST /fetch, сайдкар берёт свой env-прокси, и прогон уходит мимо пула целиком:
ни выбора узла по affinity, ни учёта scrape_proxy_source_bans, ни ротации при
блоке. В логе это ровно `proxy_lease_id=None`.

Ровно этот дефект чинили рядом — #2698 в house_imv_backfill, где он держал
35 отказов из 35 попыток в каждом прогоне полтора месяца, пока соседние свипы
через ТОТ ЖЕ сайдкар тянули сотни объявлений. Здесь он остался.

Замер 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 МБ
страницы. То есть площадка нас пускала, запрос шёл не оттуда.

Это объясняет, почему предыдущая правка (#3143, прокси в повторах curl-пути)
не восстановила сбор: боевой режим бэкфилла — browser, и он до curl-веток
вообще не доходит.

Три теста: конструктор на месте (страховка от проверки пустоты), все три
аргумента проводки передаются, use_pool читается из конфига а не зашит
константой (зашитый True отнял бы у владельца выключатель, зашитый False вернул
бы дефект незаметно). Проверил красноту на коде без проводки.
Прогон: 113 тестов зелёные, ruff чист.

Refs #3045, #3034, #2698
This commit is contained in:
bot-backend 2026-08-27 15:58:26 +03:00
parent 1cefd806b3
commit d66a52d492
2 changed files with 108 additions and 2 deletions

View file

@ -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

View file

@ -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 задан константой — выключатель конфига перестал работать"
)