From e42e370ac98f3773f084a790cc2dec6818ca7035 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 17 Sep 2026 12:48:12 +0500 Subject: [PATCH] =?UTF-8?q?fix(tradein/avito):=20detail-=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=B8=D0=B4=D1=91=D1=82=20=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D1=83=D0=B7=D0=B5=D1=80=D0=BE=D0=BC,=20=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20curl=20(#3045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit curl_cffi не проходит QRATOR proof-of-work Авито в принципе (замер 21.08: ~6% обогащений против ~74% на браузере). Шаг 3 задачи был сделан только environment'ом сервиса scraper, дефолт в config.py остался True. Добор при этом исполняет и tradein-backend — ручка админки scrape_avito_detail_backfill вызывает run_avito_detail_backfill в своём процессе, и там printenv даёт true. Ручной прогон через админку шёл curl'ом. Дефолт avito_detail_backfill_use_curl -> False. Комментарий в задаче «true только у tradein-backend, который добор не запускает» был неверен — исправлен, как и два упоминания use_curl=True «прод-дефолтом». Тест по значению: Settings без env и без .env даёт False; контроль — env по-прежнему переключает на curl. Фальсификация: default=True -> «AssertionError: assert True is False». На проде хостовый backend/.env.runtime:37 задаёт true явно, поэтому для tradein-backend нужен ещё environment-оверрайд — он в коммите #3182. Co-Authored-By: Claude Opus 5 --- tradein-mvp/backend/app/core/config.py | 28 ++++++++----------- .../app/tasks/avito_detail_backfill.py | 18 ++++++------ .../tests/tasks/test_avito_detail_backfill.py | 2 +- .../tests/test_3045_use_curl_default.py | 28 +++++++++++++++++++ 4 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 tradein-mvp/backend/tests/test_3045_use_curl_default.py diff --git a/tradein-mvp/backend/app/core/config.py b/tradein-mvp/backend/app/core/config.py index d291f723..a6f5a5d8 100644 --- a/tradein-mvp/backend/app/core/config.py +++ b/tradein-mvp/backend/app/core/config.py @@ -1221,25 +1221,19 @@ class Settings(BaseSettings): cian_login_success_cookie: str = "DMIR_AUTH" cian_login_wait_ms: int = 4000 - # detail_backfill через curl_cffi+backconnect (mproxy) вместо браузера/auv. - # SERP (full_load/city_sweep) и detail_backfill делят один прокси-аккаунт auv - # (~5 параллельных коннектов); browser-фетч в backfill открывает десятки коннектов - # → cap превышается → HTTP 500 / краши. Backconnect (mproxy, авто-ротация, - # 1 коннект/запрос) развязывает прокси-аккаунты. - # True (дефолт) = curl_cffi через settings.scraper_proxy_url (backconnect mproxy). - # False = старое browser-поведение (BrowserFetcher/auv, как scraper_fetch_mode). - # ENV: AVITO_DETAIL_BACKFILL_USE_CURL. + # detail_backfill через curl_cffi вместо браузера. ENV: AVITO_DETAIL_BACKFILL_USE_CURL. + # False (дефолт) = BrowserFetcher (camoufox дожидается QRATOR PoW, #3045/#3046). + # True = curl_cffi через settings.scraper_proxy_url. # - # ⚠️ Обоснование выше УСТАРЕЛО для Авито (замер 2026-08-21). Авито за QRATOR - # отдаёт JS proof-of-work челлендж, который curl_cffi не решает: прогоны - # 4348/4394/4508 — 3-4 обогащённых из 46-53 попыток (~6%) против ~74% на - # браузерном пути. Опасение «browser превышает cap прокси-аккаунта auv» снято: - # браузер сериализован BROWSER_CONCURRENCY=1 и ходит через тот же backconnect. - # Прод переведён на браузер через docker-compose.prod.yml (environment - # перекрывает env_file). Дефолт оставлен True, чтобы не менять поведение - # других окружений вслепую. + # Дефолт False (#3045): Авито за QRATOR отдаёт JS proof-of-work челлендж, который + # curl_cffi не решает в принципе — замер 2026-08-21, прогоны 4348/4394/4508: 3-4 + # обогащённых из 46-53 попыток (~6%) против ~74% на браузере. Старое обоснование + # curl (браузер превышал cap прокси-аккаунта auv) снято: браузер сериализован + # BROWSER_CONCURRENCY=1. Пока дефолт был True, браузер включался только + # environment'ом сервиса scraper, а tradein-backend (ручка админки + # scrape_avito_detail_backfill исполняет добор в своём процессе) шёл curl'ом. avito_detail_backfill_use_curl: bool = Field( - default=True, validation_alias="AVITO_DETAIL_BACKFILL_USE_CURL" + default=False, validation_alias="AVITO_DETAIL_BACKFILL_USE_CURL" ) # #1950: hard-timeout на один detail-fetch внутри avito_detail_backfill. Зависший diff --git a/tradein-mvp/backend/app/tasks/avito_detail_backfill.py b/tradein-mvp/backend/app/tasks/avito_detail_backfill.py index dcbe755a..77332fa6 100644 --- a/tradein-mvp/backend/app/tasks/avito_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/avito_detail_backfill.py @@ -98,9 +98,9 @@ from app.services.scraper_adapters import RealProxyProvider, RealScraperConfig # принимает config: ScraperConfig | None (Strangler-инъекция #2330) и пробрасывает # его в _build_detail_session() для settings.scraper_proxy_url (sticky МГТС-прокси), # зеркаля fetch_detail. Оба call site'а ниже передают config=RealScraperConfig() — -# без него warm-batch curl-путь (use_curl=True, ДЕФОЛТ прод-режима, -# avito_detail_backfill_use_curl=True) молча терял бы backconnect (прямое -# datacenter-подключение вместо sticky-прокси), тот же класс бага что #2322/#2310. +# без него warm-batch curl-путь (use_curl=True, avito_detail_backfill_use_curl) молча +# терял бы backconnect (прямое datacenter-подключение вместо sticky-прокси), тот же +# класс бага что #2322/#2310. logger = logging.getLogger(__name__) @@ -232,12 +232,10 @@ async def _rotate_current_proxy( settings.scraper_proxy_url БЕЗ lease — там ротировать нечего, вызывающий цикл не зовёт эту функцию в том режиме вовсе. - Прод при этом в browser-режиме, а не в curl: у контейнера tradein-scraper (там же - живёт планировщик) проверено AVITO_DETAIL_BACKFILL_USE_CURL=false при - SCRAPER_FETCH_MODE=browser и USE_PROXY_POOL_BROWSER=true, так что ротация - активна. Значение true стоит только у tradein-backend, который добор не запускает. - Комментарий ниже по файлу (~строка 653) называет use_curl=True «прод-дефолтом» — - это предсуществующее заблуждение, а не описание текущего прода. + Прод в browser-режиме, а не в curl: дефолт avito_detail_backfill_use_curl=False + (#3045). Добор исполняют ДВА процесса — планировщик в tradein-scraper и ручка + админки scrape_avito_detail_backfill в tradein-backend, — поэтому режим задаётся + дефолтом кода, а не environment'ом одного сервиса. Отказ провайдера (лимит исчерпан, нет rotate_url, сетевой сбой, неизвестный хост) НЕ должен ронять прогон — логируем и продолжаем на текущем адресе. Эта попытка НЕ @@ -705,7 +703,7 @@ async def run_avito_detail_backfill( # config.scraper_proxy_url=None для backconnect-gate'а # (elif not use_curl own-session path), теряя reconnect- # on-403 поведение legacy (settings.scraper_proxy_url - # напрямую). Не влияет на use_curl=True (прод-дефолт) — + # напрямую). Не влияет на use_curl=True — # там reconnect_on_block=False уже гасит backconnect. config=RealScraperConfig(), origin=serp_origin, diff --git a/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py b/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py index 9e963679..e2c89336 100644 --- a/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py +++ b/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py @@ -194,7 +194,7 @@ async def test_backfill_build_warmed_session_receives_config() -> None: """#2397 Part D1 / #2330 regression guard: kit's build_warmed_session() now accepts config=ScraperConfig and silently drops settings.scraper_proxy_url (sticky МГТС-прокси) -- NO crash, just a proxy-less session -- unless config=RealScraperConfig() is passed - at the call site. use_curl=True is the prod-default path (avito_detail_backfill_use_curl), + at the call site. use_curl=True is the curl path (avito_detail_backfill_use_curl), so this is the setup-time build_warmed_session() call (~line 177). assert_awaited_once() alone wouldn't catch someone dropping the kwarg later (mirrors the fetch_detail config= guard above, lines 141-148).""" diff --git a/tradein-mvp/backend/tests/test_3045_use_curl_default.py b/tradein-mvp/backend/tests/test_3045_use_curl_default.py new file mode 100644 index 00000000..893d740b --- /dev/null +++ b/tradein-mvp/backend/tests/test_3045_use_curl_default.py @@ -0,0 +1,28 @@ +"""#3045: дефолт avito_detail_backfill_use_curl — браузер, а не curl. + +curl_cffi не решает QRATOR PoW Авито (замер 21.08: ~6% обогащений против ~74% на +браузере). Пока дефолт был True, браузер включал только environment сервиса scraper, +а tradein-backend (ручка админки scrape_avito_detail_backfill исполняет добор в своём +процессе) шёл curl'ом. Проверка по значению поля Settings без env и без .env-файла. +""" + +import os + +os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") + +import pytest + +from app.core.config import Settings + + +def test_detail_backfill_defaults_to_browser(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AVITO_DETAIL_BACKFILL_USE_CURL", raising=False) + + assert Settings(_env_file=None).avito_detail_backfill_use_curl is False + + +def test_env_still_switches_to_curl(monkeypatch: pytest.MonkeyPatch) -> None: + """Контроль: переменная окружения по-прежнему переключает режим — поле живое.""" + monkeypatch.setenv("AVITO_DETAIL_BACKFILL_USE_CURL", "true") + + assert Settings(_env_file=None).avito_detail_backfill_use_curl is True