gendesign/tradein-mvp/backend/tests/test_3045_detail_backfill_browser_proxy.py
bot-backend d66a52d492
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
fix(tradein/avito): браузерный бэкфилл ходил мимо пула прокси
Ветка 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
2026-08-27 15:58:26 +03:00

85 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Браузерный бэкфилл Авито обязан ходить через пул прокси, а не мимо него.
ДЕФЕКТ. В `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 задан константой — выключатель конфига перестал работать"
)