fix(tradein/scraper): блок QRATOR у Домклика больше не помечает прогон успешным (#2657) #2667
3 changed files with 77 additions and 16 deletions
|
|
@ -1,10 +1,10 @@
|
|||
"""Offline unit tests for DomClickCitySweepCounters + DomClick parser.
|
||||
|
||||
Legacy `run_domclick_city_sweep` orchestration behavior (sweep phases, cooperative
|
||||
cancel, honest mark_done/mark_failed statuses) удалён вместе с
|
||||
cancel, honest mark_done/mark_failed/mark_banned statuses) удалён вместе с
|
||||
`app.services.scrape_pipeline` (#2397 Part E1) — эквивалентная regression-coverage
|
||||
теперь в `test_scraper_kit_pipeline_parity2.py::test_domclick_city_sweep_{done,
|
||||
blocked_failed}` (kit `run_domclick_city_sweep`).
|
||||
теперь в `test_scraper_kit_pipeline_parity2.py::test_domclick_city_sweep_*`
|
||||
(kit `run_domclick_city_sweep`).
|
||||
|
||||
`app.services.scrapers.domclick` удалён (#2397 Part E2, 0 runtime-импортёров) —
|
||||
`_map_item` ретаргетирован на kit `scraper_kit.providers.domclick.serp.DomClickScraper`
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ sweep). Здесь — остальные 7 sweep'ов (#2135 F2):
|
|||
City sweep'ы (приоритет — активны в проде):
|
||||
- run_yandex_city_sweep — combos SERP + save + price-history
|
||||
- run_cian_city_sweep — SERP + newbuilding_only-фильтр + save
|
||||
- run_domclick_city_sweep — BFF citywide + честный статус (done / failed)
|
||||
- run_domclick_city_sweep — BFF citywide + честный статус (done / failed / banned)
|
||||
Плюс:
|
||||
- run_avito_newbuilding_sweep — citywide novostroyka SERP + save
|
||||
Full load'ы (smoke — импорт + базовый прогон через on_bucket):
|
||||
|
|
@ -388,7 +388,11 @@ async def test_cian_city_sweep_honest_empty_stays_done() -> None:
|
|||
|
||||
|
||||
async def _drive_domclick(
|
||||
*, lots_n: int, blocked: bool, capture: dict[str, Any] | None = None
|
||||
*,
|
||||
lots_n: int,
|
||||
blocked: bool,
|
||||
fetch_errors: int = 0,
|
||||
capture: dict[str, Any] | None = None,
|
||||
) -> _DriveResult:
|
||||
"""capture: опционально — если передан, кладём save_mock (#2594, инспекция city=...)."""
|
||||
recorder = _RunsRecorder()
|
||||
|
|
@ -398,7 +402,7 @@ async def _drive_domclick(
|
|||
fetch_city=AsyncMock(return_value=lots),
|
||||
blocked=blocked,
|
||||
geo_filtered=0,
|
||||
fetch_errors=0,
|
||||
fetch_errors=fetch_errors,
|
||||
)
|
||||
save_mock = MagicMock(side_effect=[(lots_n, 0)] if lots_n else [])
|
||||
if capture is not None:
|
||||
|
|
@ -425,14 +429,45 @@ async def test_domclick_city_sweep_done() -> None:
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_domclick_city_sweep_blocked_failed() -> None:
|
||||
"""QRATOR-блок + 0 lots → mark_failed (честный статус #1968)."""
|
||||
async def test_domclick_city_sweep_blocked_banned() -> None:
|
||||
"""QRATOR-блок + 0 lots → mark_banned (честный статус #1968, #2657)."""
|
||||
counters, calls = await _drive_domclick(lots_n=0, blocked=True)
|
||||
assert counters["lots_fetched"] == 0
|
||||
assert counters["blocked"] == 1
|
||||
assert calls[-1][0] == "mark_banned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_domclick_city_sweep_blocked_with_lots_marks_banned() -> None:
|
||||
"""#2657: блок оборвал бакеты ПОСЛЕ части лотов → banned, не done.
|
||||
|
||||
Прод-случай: 13 из 13 прогонов с blocked=1 уходили в done, потому что
|
||||
honest-status требовал ещё и lots_fetched == 0.
|
||||
"""
|
||||
counters, calls = await _drive_domclick(lots_n=4, blocked=True)
|
||||
assert counters["lots_fetched"] == 4
|
||||
assert counters["blocked"] == 1
|
||||
assert calls[-1][0] == "mark_banned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_domclick_city_sweep_fetch_errors_without_block_stays_failed() -> None:
|
||||
"""#2657 анти-оверрич: 0 лотов + fetch-ошибки, но БЕЗ блока → failed, не banned."""
|
||||
counters, calls = await _drive_domclick(lots_n=0, blocked=False, fetch_errors=2)
|
||||
assert counters["blocked"] == 0
|
||||
assert counters["errors_count"] == 2
|
||||
assert calls[-1][0] == "mark_failed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_domclick_city_sweep_honest_empty_stays_done() -> None:
|
||||
"""#2657 анти-оверрич: честная пустота (0 лотов, ни блока, ни ошибок) → done."""
|
||||
counters, calls = await _drive_domclick(lots_n=0, blocked=False)
|
||||
assert counters["lots_fetched"] == 0
|
||||
assert counters["blocked"] == 0
|
||||
assert calls[-1][0] == "mark_done"
|
||||
|
||||
|
||||
# ── Avito newbuilding sweep ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3702,8 +3702,10 @@ async def run_domclick_city_sweep(
|
|||
Инжекция (#2135 F2): config/matcher/shutdown_requested приходят снаружи вместо
|
||||
прямых импортов app.* (см. scraper_kit.contracts).
|
||||
|
||||
ЧЕСТНЫЙ СТАТУС (#1968): если scraper сообщил QRATOR-блок И lots == 0 →
|
||||
mark_failed. Иначе mark_done.
|
||||
ЧЕСТНЫЙ СТАТУС (#1968, ужесточён #2657): распознанный QRATOR-блок → mark_banned
|
||||
(независимо от числа собранных лотов — блок обрывает ОСТАВШИЕСЯ комнатные бакеты,
|
||||
прогон не доделал работу). Ноль лотов с fetch-ошибками, но без блока → mark_failed.
|
||||
Иначе mark_done.
|
||||
|
||||
Возвращает DomClickCitySweepCounters.
|
||||
"""
|
||||
|
|
@ -3797,19 +3799,43 @@ async def run_domclick_city_sweep(
|
|||
counters.pages_fetched = _num_fetches
|
||||
runs.update_heartbeat(db, run_id, counters.to_dict())
|
||||
|
||||
# ── ЧЕСТНЫЙ СТАТУС (#1968) ────────────────────────────────────────────
|
||||
if counters.lots_fetched == 0 and (counters.blocked or counters.errors_count > 0):
|
||||
# ── ЧЕСТНЫЙ СТАТУС (#1968, ужесточён #2657) ───────────────────────────
|
||||
# Распознанный QRATOR-блок НИКОГДА не даёт done. Домклик тут структурно
|
||||
# отличается от cian/yandex (#2625/#2642): там независимые anchor'ы, и
|
||||
# провал одного среди успешных — не бан (анти-флап). Здесь anchor'ов нет,
|
||||
# sweep линейный по ROOM_BUCKETS, и первый же блок делает `break` —
|
||||
# оставшиеся бакеты не пробуются вовсе (fetch_city). То есть блок = прогон
|
||||
# оборван, сколько бы лотов он ни успел взять до этого; на проде это дало
|
||||
# 13 из 13 «успешных» прогонов с blocked=1 (39-464 лота вместо ~6300).
|
||||
# banned (а не failed): это external constraint, не наш баг, и тот же
|
||||
# статус, что #2642 дал cian/yandex — доступен как триггер ротации IP
|
||||
# (#2611; сама ротация здесь не вызывается). Пометка прокси-пула живёт
|
||||
# отдельно и раньше — fetcher.report_ban в serp.py (#2600 п.1).
|
||||
if counters.blocked:
|
||||
logger.error(
|
||||
"domclick-sweep run_id=%d: 0 listings with blocked=%d errors=%d "
|
||||
"— marking failed",
|
||||
"domclick-sweep run_id=%d: QRATOR block aborted remaining room buckets "
|
||||
"(lots=%d, errors=%d) — marking banned (#2657)",
|
||||
run_id,
|
||||
counters.lots_fetched,
|
||||
counters.errors_count,
|
||||
)
|
||||
runs.mark_banned(
|
||||
db,
|
||||
run_id,
|
||||
f"QRATOR block aborted sweep — {counters.lots_fetched} listings "
|
||||
"collected before abort (#2657)",
|
||||
counters.to_dict(),
|
||||
)
|
||||
elif counters.lots_fetched == 0 and counters.errors_count > 0:
|
||||
logger.error(
|
||||
"domclick-sweep run_id=%d: 0 listings with errors=%d — marking failed",
|
||||
run_id,
|
||||
counters.blocked,
|
||||
counters.errors_count,
|
||||
)
|
||||
runs.mark_failed(
|
||||
db,
|
||||
run_id,
|
||||
"QRATOR block or fetch errors — 0 listings",
|
||||
"fetch errors — 0 listings",
|
||||
counters.to_dict(),
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue