diff --git a/tradein-mvp/backend/app/tasks/avito_detail_backfill.py b/tradein-mvp/backend/app/tasks/avito_detail_backfill.py index b7b0fc48..5e3374c9 100644 --- a/tradein-mvp/backend/app/tasks/avito_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/avito_detail_backfill.py @@ -39,6 +39,8 @@ from scraper_kit.browser_fetcher import BrowserFetcher # зависимость от scrape_pipeline.py перед его удалением. from scraper_kit.providers._base import DOCUMENT_HEADERS, http_proxies from scraper_kit.providers.avito.detail import ( + _AVITO_WARM_SEARCH_URL, + build_warmed_session, fetch_detail, research_in_session, save_detail_enrichment, @@ -52,20 +54,14 @@ from app.core.shutdown import shutdown_requested from app.services import scrape_runs as runs_mod from app.services.scraper_adapters import RealScraperConfig -# _AVITO_WARM_SEARCH_URL/build_warmed_session остаются legacy (#2310): kit's -# scraper_kit.providers.avito.detail.build_warmed_session() строит сессию через -# _build_detail_session() БЕЗ config-параметра (сигнатура вообще не принимает -# ScraperConfig — в отличие от fetch_detail, здесь нет caller-side способа -# прокинуть прокси). Мигрировав этот импорт, warm-batch curl-путь (use_curl=True, -# ДЕФОЛТ прод-режима, avito_detail_backfill_use_curl=True) молча перестал бы -# использовать settings.scraper_proxy_url (sticky МГТС-прокси) — прямое -# datacenter-подключение вместо backconnect. Тот же класс бага, что #2322 -# (недостающая инъекция конфига в scraper_kit provider-функции), но другая -# функция — заведён как отдельный follow-up, НЕ #2322. См. отчёт issue #2310. -from app.services.scrapers.avito_detail import ( - _AVITO_WARM_SEARCH_URL, - build_warmed_session, -) +# #2397 Part D1 (#2330 закрыт): _AVITO_WARM_SEARCH_URL/build_warmed_session больше +# НЕ legacy — kit's scraper_kit.providers.avito.detail.build_warmed_session() теперь +# принимает 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. logger = logging.getLogger(__name__) @@ -174,7 +170,10 @@ async def run_avito_detail_backfill( # фикс. exit-IP, per-connection ротации нет (rebuild != новый IP); on-block — # cooldown + in-session re-search, не дискард сессии. own_session = True - session = await build_warmed_session() + # config= обязателен: #2397 Part D1 — kit build_warmed_session() без него + # молча теряет settings.scraper_proxy_url (sticky МГТС-прокси), см. модуль- + # ный комментарий у импорта выше. + session = await build_warmed_session(config=RealScraperConfig()) elif not use_curl: # curl_cffi legacy path (scraper_fetch_mode="curl_cffi", use_curl=False): # строим shared сессию через auv, как делает run_avito_city_sweep (kit). @@ -296,8 +295,12 @@ async def run_avito_detail_backfill( # #1950: bound сетевой re-warm; строим НОВУЮ сессию до close старой — на # timeout/fail сохраняем текущую рабочую сессию (graceful degrade). try: + # config= обязателен здесь тоже (тот же footgun, что и в setup- + # вызове build_warmed_session выше) -- иначе периодический re-warm + # молча пересобрал бы сессию БЕЗ sticky-прокси после первого раза. new_session = await asyncio.wait_for( - build_warmed_session(), timeout=warm_timeout_s + build_warmed_session(config=RealScraperConfig()), + timeout=warm_timeout_s, ) try: await session.close() 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 5183dd4a..eae34c4a 100644 --- a/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py +++ b/tradein-mvp/backend/tests/tasks/test_avito_detail_backfill.py @@ -148,6 +148,48 @@ async def test_backfill_processes_snapshot_to_completion() -> None: assert isinstance(mock_scraper_cls.call_args.args[0], RealScraperConfig) +@pytest.mark.asyncio +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), + 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).""" + from app.services.scraper_adapters import RealScraperConfig + + snapshot = _make_snapshot(1) + db = _mock_db(snapshot) + runs = MagicMock() + mock_enrichment = MagicMock() + mock_fetch = AsyncMock(return_value=mock_enrichment) + mock_save = MagicMock(return_value=True) + fake_settings = MagicMock( + scraper_fetch_mode="cffi", + avito_detail_backfill_use_curl=True, + ) + with ( + patch(_SETTINGS, fake_settings), + patch(_SCRAPER), + patch(_RUNS, runs), + patch(_FETCH, mock_fetch), + patch(_SAVE, mock_save), + patch(_SLEEP, new_callable=AsyncMock), + patch(_BUILD_WARM, AsyncMock(return_value=AsyncMock())) as mock_build, + ): + result = await run_avito_detail_backfill( + db, run_id=15, params={"batch_size": 10, "budget_sec": 3600} + ) + + assert result.enriched == 1 + mock_build.assert_awaited_once() + _, build_call_kwargs = mock_build.call_args + assert isinstance(build_call_kwargs.get("config"), RealScraperConfig) + runs.mark_done.assert_called_once() + runs.mark_failed.assert_not_called() + + @pytest.mark.asyncio async def test_backfill_blocked_abort_after_max_consecutive() -> None: """5 consecutive AvitoBlockedError -> abort, mark_done (NOT mark_failed).