fix(tradein/avito-detail): sticky-IP реальность — on-block cooldown+research вместо бесполезного rebuild, честные комменты (МГТС не ротирует)
This commit is contained in:
parent
d311efb431
commit
5bb998a625
2 changed files with 94 additions and 27 deletions
|
|
@ -98,6 +98,7 @@ async def run_avito_detail_backfill(
|
|||
max_consecutive_blocks = int(params.get("max_consecutive_blocks", 5))
|
||||
warm_batch = int(params.get("warm_batch", 500))
|
||||
research_every = int(params.get("research_every", 50))
|
||||
block_cooldown_sec = float(params.get("block_cooldown_sec", 30.0))
|
||||
|
||||
# #1950: hard-timeout'ы на блокирующие await'ы внутри loop'а. Без них один
|
||||
# зависший fetch_detail/rotate ронял весь run в zombie (run 423 завис 7.7ч).
|
||||
|
|
@ -139,10 +140,11 @@ async def run_avito_detail_backfill(
|
|||
own_browser = True
|
||||
scraper._browser = browser_fetcher
|
||||
elif use_curl:
|
||||
# use_curl=True (#1551 warm-batch): прогретая shared-сессия — yandex-referer ->
|
||||
# avito-search сеет антибот-куки, сессия держит батч detail без 403 (доказано
|
||||
# пробами). Ротация каждые warm_batch карточек И на блоке = свежий exit-IP +
|
||||
# свежий органический прогрев.
|
||||
# use_curl=True (#1551 warm-batch): прогретая shared-сессия на sticky МГТС-IP —
|
||||
# yandex-referer -> avito-search сеет антибот-куки, сессия держит батч detail без
|
||||
# 403 (доказано пробами: ≥66 карточек подряд, 0 блоков). NB: МГТС sticky — один
|
||||
# фикс. exit-IP, per-connection ротации нет (rebuild != новый IP); on-block —
|
||||
# cooldown + in-session re-search, не дискард сессии.
|
||||
own_session = True
|
||||
session = await build_warmed_session()
|
||||
elif not use_curl:
|
||||
|
|
@ -255,12 +257,14 @@ async def run_avito_detail_backfill(
|
|||
item_url = urlparse(source_url).path if source_url.startswith("http") else source_url
|
||||
|
||||
# use_curl warm-batch (#1551): интервалы по размеру SERP-страницы (~50).
|
||||
# Полная ротация (новый exit-IP + полный прогрев) каждые warm_batch (~500);
|
||||
# между ротациями — лёгкий in-session перепоиск каждые research_every (~50)
|
||||
# на ТОМ ЖЕ exit-IP (освежить куки, без смены IP). On-block rebuild = backstop.
|
||||
# МГТС sticky-IP: exit-IP константа (rebuild НЕ даёт новый IP). Каждые
|
||||
# warm_batch (~500) — полный re-warm (close+build) на ТОМ ЖЕ sticky exit-IP:
|
||||
# session-hygiene (свежий TLS-хендшейк + свежие куки yandex->search), НЕ смена
|
||||
# IP. Между ними — лёгкий in-session перепоиск каждые research_every (~50)
|
||||
# (re-GET search той же сессией, освежить куки). On-block — cooldown ниже.
|
||||
if use_curl and session is not None:
|
||||
if warm_batch and items_since_warm >= warm_batch:
|
||||
# полная ротация: новый exit-IP + полный прогрев (yandex->search)
|
||||
# периодический полный re-warm на том же sticky exit-IP (свежий TLS+куки)
|
||||
try:
|
||||
await session.close()
|
||||
except Exception:
|
||||
|
|
@ -340,18 +344,16 @@ async def run_avito_detail_backfill(
|
|||
consecutive_blocks,
|
||||
e,
|
||||
)
|
||||
# use_curl warm-batch (#1551): блок = текущий exit-IP залочен → закрываем
|
||||
# прогретую сессию и строим новую (свежий exit-IP + новый органический
|
||||
# прогрев) вместо changeip-ротации. Иначе legacy auv/browser-путь —
|
||||
# bound rotate backconnect-IP.
|
||||
# МГТС sticky-IP: один фикс. exit-IP, per-connection ротации нет (проверено:
|
||||
# 6/6 свежих сессий = тот же IP 109.252.125.80; ротация только вручную
|
||||
# кнопкой). Уйти на свежий IP софтом нельзя → блок = rate-limit текущего IP:
|
||||
# даём окну остыть (cooldown) и освежаем куки in-session, НЕ дискардим
|
||||
# рабочую прогретую сессию (rebuild на том же IP бесполезен для escape +
|
||||
# грузит rate-limited IP полным прогревом). items_since_warm НЕ сбрасываем.
|
||||
if use_curl:
|
||||
await asyncio.sleep(block_cooldown_sec)
|
||||
if session is not None:
|
||||
try:
|
||||
await session.close()
|
||||
except Exception:
|
||||
pass
|
||||
session = await build_warmed_session()
|
||||
items_since_warm = 0
|
||||
await research_in_session(session)
|
||||
else:
|
||||
# #1950: bound rotate — _rotate_ip ждёт settle + до 3 changeip-попыток;
|
||||
# на блокирующем changeip (зависшее соединение) без timeout loop виснет.
|
||||
|
|
|
|||
|
|
@ -29,15 +29,21 @@ def _reset_shutdown() -> None:
|
|||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_build_warmed() -> object:
|
||||
"""use_curl-ветка (#1551) строит прогретую сессию через build_warmed_session
|
||||
(реальный yandex/avito warm-up + sleep). fetch_detail в этих тестах всё равно
|
||||
замокан — мокаем build_warmed_session не-сетевой AsyncMock-сессией, чтобы setup
|
||||
и rebuild-on-block не ходили в сеть."""
|
||||
with patch(
|
||||
"app.tasks.avito_detail_backfill.build_warmed_session",
|
||||
AsyncMock(return_value=AsyncMock()),
|
||||
) as p:
|
||||
yield p
|
||||
"""use_curl-ветка (#1551) строит прогретую сессию через build_warmed_session и
|
||||
освежает куки через research_in_session (реальный yandex/avito GET + sleep).
|
||||
fetch_detail в этих тестах всё равно замокан — мокаем обе не-сетевыми AsyncMock,
|
||||
чтобы setup / periodic re-warm / on-block cooldown не ходили в сеть."""
|
||||
with (
|
||||
patch(
|
||||
"app.tasks.avito_detail_backfill.build_warmed_session",
|
||||
AsyncMock(return_value=AsyncMock()),
|
||||
),
|
||||
patch(
|
||||
"app.tasks.avito_detail_backfill.research_in_session",
|
||||
AsyncMock(return_value=True),
|
||||
) as research,
|
||||
):
|
||||
yield research
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -66,6 +72,8 @@ _SESSION = "app.tasks.avito_detail_backfill.AsyncSession"
|
|||
_SCRAPER = "app.tasks.avito_detail_backfill.AvitoScraper"
|
||||
_SETTINGS = "app.tasks.avito_detail_backfill.settings"
|
||||
_SHUTDOWN = "app.tasks.avito_detail_backfill.shutdown_requested"
|
||||
_BUILD_WARM = "app.tasks.avito_detail_backfill.build_warmed_session"
|
||||
_RESEARCH = "app.tasks.avito_detail_backfill.research_in_session"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
|
|
@ -536,3 +544,60 @@ async def test_backfill_use_curl_false_creates_browser_fetcher() -> None:
|
|||
assert kwargs.get("browser_fetcher") is not None
|
||||
assert result.enriched == 1
|
||||
runs.mark_done.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_backfill_use_curl_block_cooldown_research_no_rebuild() -> None:
|
||||
"""#1551 sticky-IP: on-block (use_curl) даёт cooldown + in-session research, НЕ
|
||||
пересоздаёт прогретую сессию и НЕ дёргает changeip-ротацию.
|
||||
|
||||
МГТС sticky — один фикс. exit-IP (rebuild != новый IP), уйти на свежий IP софтом
|
||||
нельзя. Блок = rate-limit текущего IP → cooldown + re-search той же сессией.
|
||||
fetch: 1-й вызов BLOCKED, 2-й — успех.
|
||||
"""
|
||||
from app.services.scrapers.avito_exceptions import AvitoBlockedError
|
||||
|
||||
snapshot = _make_snapshot(2)
|
||||
db = _mock_db(snapshot)
|
||||
runs = MagicMock()
|
||||
mock_enrichment = MagicMock()
|
||||
blocked_exc = AvitoBlockedError("rate-limited")
|
||||
mock_fetch = AsyncMock(side_effect=[blocked_exc, mock_enrichment])
|
||||
warmed_session = AsyncMock()
|
||||
fake_settings = MagicMock(
|
||||
scraper_fetch_mode="cffi",
|
||||
avito_detail_backfill_use_curl=True,
|
||||
)
|
||||
with (
|
||||
patch(_SETTINGS, fake_settings),
|
||||
patch(_SCRAPER) as mock_scraper,
|
||||
patch(_RUNS, runs),
|
||||
patch(_FETCH, mock_fetch),
|
||||
patch(_SAVE, return_value=True),
|
||||
patch(_SLEEP, new_callable=AsyncMock),
|
||||
patch(_BUILD_WARM, AsyncMock(return_value=warmed_session)) as mock_build,
|
||||
patch(_RESEARCH, new_callable=AsyncMock) as mock_research,
|
||||
):
|
||||
result = await run_avito_detail_backfill(
|
||||
db,
|
||||
run_id=14,
|
||||
params={
|
||||
"batch_size": 10,
|
||||
"budget_sec": 3600,
|
||||
"max_consecutive_blocks": 5,
|
||||
"block_cooldown_sec": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
assert result.blocked == 1
|
||||
assert result.enriched == 1
|
||||
assert result.attempted == 2
|
||||
# on-block: research_in_session вызван (освежить куки in-session)...
|
||||
mock_research.assert_awaited_once()
|
||||
# ...а build_warmed_session НЕ дёргался повторно (вызван только 1× в setup —
|
||||
# прогретая сессия НЕ пересоздаётся на блоке, sticky-IP rebuild бесполезен).
|
||||
assert mock_build.await_count == 1
|
||||
# changeip-ротация (legacy путь) под use_curl НЕ дёргается.
|
||||
mock_scraper.return_value._rotate_ip.assert_not_called()
|
||||
runs.mark_done.assert_called_once()
|
||||
runs.mark_failed.assert_not_called()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue