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))
|
max_consecutive_blocks = int(params.get("max_consecutive_blocks", 5))
|
||||||
warm_batch = int(params.get("warm_batch", 500))
|
warm_batch = int(params.get("warm_batch", 500))
|
||||||
research_every = int(params.get("research_every", 50))
|
research_every = int(params.get("research_every", 50))
|
||||||
|
block_cooldown_sec = float(params.get("block_cooldown_sec", 30.0))
|
||||||
|
|
||||||
# #1950: hard-timeout'ы на блокирующие await'ы внутри loop'а. Без них один
|
# #1950: hard-timeout'ы на блокирующие await'ы внутри loop'а. Без них один
|
||||||
# зависший fetch_detail/rotate ронял весь run в zombie (run 423 завис 7.7ч).
|
# зависший fetch_detail/rotate ронял весь run в zombie (run 423 завис 7.7ч).
|
||||||
|
|
@ -139,10 +140,11 @@ async def run_avito_detail_backfill(
|
||||||
own_browser = True
|
own_browser = True
|
||||||
scraper._browser = browser_fetcher
|
scraper._browser = browser_fetcher
|
||||||
elif use_curl:
|
elif use_curl:
|
||||||
# use_curl=True (#1551 warm-batch): прогретая shared-сессия — yandex-referer ->
|
# use_curl=True (#1551 warm-batch): прогретая shared-сессия на sticky МГТС-IP —
|
||||||
# avito-search сеет антибот-куки, сессия держит батч detail без 403 (доказано
|
# yandex-referer -> avito-search сеет антибот-куки, сессия держит батч detail без
|
||||||
# пробами). Ротация каждые warm_batch карточек И на блоке = свежий exit-IP +
|
# 403 (доказано пробами: ≥66 карточек подряд, 0 блоков). NB: МГТС sticky — один
|
||||||
# свежий органический прогрев.
|
# фикс. exit-IP, per-connection ротации нет (rebuild != новый IP); on-block —
|
||||||
|
# cooldown + in-session re-search, не дискард сессии.
|
||||||
own_session = True
|
own_session = True
|
||||||
session = await build_warmed_session()
|
session = await build_warmed_session()
|
||||||
elif not use_curl:
|
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
|
item_url = urlparse(source_url).path if source_url.startswith("http") else source_url
|
||||||
|
|
||||||
# use_curl warm-batch (#1551): интервалы по размеру SERP-страницы (~50).
|
# use_curl warm-batch (#1551): интервалы по размеру SERP-страницы (~50).
|
||||||
# Полная ротация (новый exit-IP + полный прогрев) каждые warm_batch (~500);
|
# МГТС sticky-IP: exit-IP константа (rebuild НЕ даёт новый IP). Каждые
|
||||||
# между ротациями — лёгкий in-session перепоиск каждые research_every (~50)
|
# warm_batch (~500) — полный re-warm (close+build) на ТОМ ЖЕ sticky exit-IP:
|
||||||
# на ТОМ ЖЕ exit-IP (освежить куки, без смены IP). On-block rebuild = backstop.
|
# 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 use_curl and session is not None:
|
||||||
if warm_batch and items_since_warm >= warm_batch:
|
if warm_batch and items_since_warm >= warm_batch:
|
||||||
# полная ротация: новый exit-IP + полный прогрев (yandex->search)
|
# периодический полный re-warm на том же sticky exit-IP (свежий TLS+куки)
|
||||||
try:
|
try:
|
||||||
await session.close()
|
await session.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -340,18 +344,16 @@ async def run_avito_detail_backfill(
|
||||||
consecutive_blocks,
|
consecutive_blocks,
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
# use_curl warm-batch (#1551): блок = текущий exit-IP залочен → закрываем
|
# МГТС sticky-IP: один фикс. exit-IP, per-connection ротации нет (проверено:
|
||||||
# прогретую сессию и строим новую (свежий exit-IP + новый органический
|
# 6/6 свежих сессий = тот же IP 109.252.125.80; ротация только вручную
|
||||||
# прогрев) вместо changeip-ротации. Иначе legacy auv/browser-путь —
|
# кнопкой). Уйти на свежий IP софтом нельзя → блок = rate-limit текущего IP:
|
||||||
# bound rotate backconnect-IP.
|
# даём окну остыть (cooldown) и освежаем куки in-session, НЕ дискардим
|
||||||
|
# рабочую прогретую сессию (rebuild на том же IP бесполезен для escape +
|
||||||
|
# грузит rate-limited IP полным прогревом). items_since_warm НЕ сбрасываем.
|
||||||
if use_curl:
|
if use_curl:
|
||||||
|
await asyncio.sleep(block_cooldown_sec)
|
||||||
if session is not None:
|
if session is not None:
|
||||||
try:
|
await research_in_session(session)
|
||||||
await session.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
session = await build_warmed_session()
|
|
||||||
items_since_warm = 0
|
|
||||||
else:
|
else:
|
||||||
# #1950: bound rotate — _rotate_ip ждёт settle + до 3 changeip-попыток;
|
# #1950: bound rotate — _rotate_ip ждёт settle + до 3 changeip-попыток;
|
||||||
# на блокирующем changeip (зависшее соединение) без timeout loop виснет.
|
# на блокирующем changeip (зависшее соединение) без timeout loop виснет.
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,21 @@ def _reset_shutdown() -> None:
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _patch_build_warmed() -> object:
|
def _patch_build_warmed() -> object:
|
||||||
"""use_curl-ветка (#1551) строит прогретую сессию через build_warmed_session
|
"""use_curl-ветка (#1551) строит прогретую сессию через build_warmed_session и
|
||||||
(реальный yandex/avito warm-up + sleep). fetch_detail в этих тестах всё равно
|
освежает куки через research_in_session (реальный yandex/avito GET + sleep).
|
||||||
замокан — мокаем build_warmed_session не-сетевой AsyncMock-сессией, чтобы setup
|
fetch_detail в этих тестах всё равно замокан — мокаем обе не-сетевыми AsyncMock,
|
||||||
и rebuild-on-block не ходили в сеть."""
|
чтобы setup / periodic re-warm / on-block cooldown не ходили в сеть."""
|
||||||
with patch(
|
with (
|
||||||
"app.tasks.avito_detail_backfill.build_warmed_session",
|
patch(
|
||||||
AsyncMock(return_value=AsyncMock()),
|
"app.tasks.avito_detail_backfill.build_warmed_session",
|
||||||
) as p:
|
AsyncMock(return_value=AsyncMock()),
|
||||||
yield p
|
),
|
||||||
|
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"
|
_SCRAPER = "app.tasks.avito_detail_backfill.AvitoScraper"
|
||||||
_SETTINGS = "app.tasks.avito_detail_backfill.settings"
|
_SETTINGS = "app.tasks.avito_detail_backfill.settings"
|
||||||
_SHUTDOWN = "app.tasks.avito_detail_backfill.shutdown_requested"
|
_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
|
# 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 kwargs.get("browser_fetcher") is not None
|
||||||
assert result.enriched == 1
|
assert result.enriched == 1
|
||||||
runs.mark_done.assert_called_once()
|
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