Merge pull request 'refactor(tradein/tasks): avito_detail_backfill build_warmed_session на kit + config= (#2397 Part D1, #2330)' (#2410) from refactor/tradein-partD1-avito-detail-backfill-kit into main
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled

This commit is contained in:
lekss361 2026-07-04 12:29:18 +00:00
commit c8343c8f3d
2 changed files with 61 additions and 16 deletions

View file

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

View file

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