fix(tradein/avito): detail-добор по умолчанию идёт браузером, а не curl (#3045)

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 <noreply@anthropic.com>
This commit is contained in:
bot-backend 2026-09-17 12:48:12 +05:00
parent 34642e1dd5
commit e42e370ac9
4 changed files with 48 additions and 28 deletions

View file

@ -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. Зависший

View file

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

View file

@ -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)."""

View file

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