fix(tradein/domclick): бан сайдкара наследуется от httpx-ошибки и уезжал в транспортные сбои (#3283) #3287
2 changed files with 71 additions and 4 deletions
|
|
@ -91,7 +91,11 @@ from dataclasses import dataclass, field
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from scraper_kit.browser_fetcher import BrowserFetcher, ban_kind_from_status
|
from scraper_kit.browser_fetcher import (
|
||||||
|
BrowserFetcher,
|
||||||
|
SidecarBanPageError,
|
||||||
|
ban_kind_from_status,
|
||||||
|
)
|
||||||
from scraper_kit.domclick_exceptions import DomClickBlockedError, DomClickParseError
|
from scraper_kit.domclick_exceptions import DomClickBlockedError, DomClickParseError
|
||||||
from scraper_kit.providers.domclick.detail import fetch_detail, save_detail_enrichment
|
from scraper_kit.providers.domclick.detail import fetch_detail, save_detail_enrichment
|
||||||
from scraper_kit.proxy_errors import NoProxyAvailableError
|
from scraper_kit.proxy_errors import NoProxyAvailableError
|
||||||
|
|
@ -221,10 +225,17 @@ def _is_transport_failure(exc: BaseException) -> bool:
|
||||||
* ветка `except Exception` — таймаут / 5xx сайдкара / транспорт, обёрнутый
|
* ветка `except Exception` — таймаут / 5xx сайдкара / транспорт, обёрнутый
|
||||||
`raise ... from exc`. Исходное исключение остаётся в `__cause__`.
|
`raise ... from exc`. Исходное исключение остаётся в `__cause__`.
|
||||||
|
|
||||||
Смотрим именно на третий случай: httpx-ошибка в цепочке причин. Отсутствие
|
ПОРЯДОК ПРОВЕРОК ЗДЕСЬ НЕСЛУЧАЕН. `SidecarBanPageError` объявлен как
|
||||||
статуса признаком служить не может — им как раз отличается генуинный блок.
|
`class SidecarBanPageError(httpx.HTTPStatusError)`, то есть ПО ТИПУ он
|
||||||
|
httpx-ошибка — и проверка на httpx, стоящая первой, объявляла генуинный
|
||||||
|
отказ площадки транспортным сбоем. Поймано на проде: прогон 5406 записал
|
||||||
|
`http=401, kind=platform` под заголовком «не блок площадки». Поэтому бан
|
||||||
|
сайдкара отсекается ДО общей httpx-ветки, а не после.
|
||||||
"""
|
"""
|
||||||
return any(isinstance(c, httpx.HTTPError) for c in _iter_causes(exc))
|
causes = _iter_causes(exc)
|
||||||
|
if any(isinstance(c, SidecarBanPageError) for c in causes):
|
||||||
|
return False
|
||||||
|
return any(isinstance(c, httpx.HTTPError) for c in causes)
|
||||||
|
|
||||||
|
|
||||||
def _ban_kind_of_block(exc: DomClickBlockedError) -> str:
|
def _ban_kind_of_block(exc: DomClickBlockedError) -> str:
|
||||||
|
|
|
||||||
|
|
@ -202,3 +202,59 @@ async def test_success_resets_the_soft_streak() -> None:
|
||||||
result, _ = await _run(seq, n=18, max_consecutive_soft_failures=4)
|
result, _ = await _run(seq, n=18, max_consecutive_soft_failures=4)
|
||||||
assert result.attempted == 18
|
assert result.attempted == 18
|
||||||
assert result.enriched == 9
|
assert result.enriched == 9
|
||||||
|
|
||||||
|
|
||||||
|
# ── SidecarBanPageError — httpx-ошибка ПО ТИПУ, но генуинный отказ по смыслу ──
|
||||||
|
# Поймано на проде уже ПОСЛЕ первой правки: прогон 5406 записал отказ площадки
|
||||||
|
# (http=401, kind=platform) под заголовком «не блок площадки». Причина —
|
||||||
|
# `class SidecarBanPageError(httpx.HTTPStatusError)`: проверка на httpx, стоящая
|
||||||
|
# первой, забирала генуинный бан себе. Тесты ниже закрепляют порядок проверок.
|
||||||
|
|
||||||
|
|
||||||
|
def _sidecar_ban_page() -> DomClickBlockedError:
|
||||||
|
"""Форма ровно как в fetch_detail: DomClickBlockedError ← SidecarBanPageError."""
|
||||||
|
from scraper_kit.browser_fetcher import SidecarBanPageError
|
||||||
|
|
||||||
|
ban = SidecarBanPageError(
|
||||||
|
"tradein-browser[domclick]: статический отказ площадки",
|
||||||
|
request=httpx.Request("POST", "http://tradein-browser:3000/fetch"),
|
||||||
|
response=httpx.Response(500),
|
||||||
|
upstream_status=401,
|
||||||
|
)
|
||||||
|
exc = DomClickBlockedError("DomClick detail: sidecar detected platform refusal", status=401)
|
||||||
|
exc.__cause__ = ban
|
||||||
|
return exc
|
||||||
|
|
||||||
|
|
||||||
|
def test_sidecar_ban_page_is_a_subclass_of_httpx_error() -> None:
|
||||||
|
"""Предпосылка теста ниже — зафиксировать её, а не подразумевать.
|
||||||
|
|
||||||
|
Если иерархия однажды изменится, тест скажет об этом прямо, а не молча
|
||||||
|
перестанет проверять то, ради чего написан.
|
||||||
|
"""
|
||||||
|
from scraper_kit.browser_fetcher import SidecarBanPageError
|
||||||
|
|
||||||
|
assert issubclass(SidecarBanPageError, httpx.HTTPError)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sidecar_ban_page_counts_as_block_not_transport() -> None:
|
||||||
|
"""Отказ, опознанный сайдкаром по маркерам тела, — блок площадки."""
|
||||||
|
result, _ = await _run([_sidecar_ban_page()] * 10, n=10, max_consecutive_blocks=3)
|
||||||
|
assert result.blocked == 3
|
||||||
|
assert result.failed == 0
|
||||||
|
assert result.attempted == 3
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sidecar_ban_page_aborts_the_run() -> None:
|
||||||
|
"""Три отказа площадки подряд обязаны рвать прогон — ровно как до #3283."""
|
||||||
|
_, runs = await _run([_sidecar_ban_page()] * 10, n=10, max_consecutive_blocks=3)
|
||||||
|
runs.mark_backfill_finished.assert_called_once()
|
||||||
|
assert runs.mark_backfill_finished.call_args.kwargs["aborted_by_blocks"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_plain_sidecar_500_still_counts_as_transport() -> None:
|
||||||
|
"""Обычная 500 от сайдкара остаётся сбоем: правка не должна отменить #3283."""
|
||||||
|
result, _ = await _run([_sidecar_500()] * 5 + [None] * 5, n=10)
|
||||||
|
assert result.blocked == 0
|
||||||
|
assert result.failed == 5
|
||||||
|
assert result.enriched == 5
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue