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