From c82ec7c3af5b461d93095b6828e28664c238d099 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 6 Sep 2026 00:28:24 +0500 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20=D0=BF=D0=BE=D0=BD=D0=B8=D0=B6?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=83=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=D0=BD=D0=B0,=20=D0=BA=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D1=8B=D0=B9=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83=20?= =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20(#3288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прошлая правка понижала banned→failed/done по одному диагнозу infra и ломала обратный контракт: нулевой прогон с infra (yandex 5xx #3196, финализатор #2764) получал 'failed' — настоящий бан площадки, опознанный как infra, прятался под «нашу поломку». Хуже исходного дефекта: 2 красных теста в полном прогоне. Понижение сужено до случая прогона 5425 — dominant='infra' И produced > 0: брейкер оборвал по доле, а карточки при этом обогащались → 'done'. Нулевой прогон остаётся 'banned' (честность несёт ban_kind), пустая перепись — тем более: dominant='unknown', статус не трогаем. Тесты: контроль на обратную ошибку (ноль результата → banned+infra) и на пустой census (→ banned+unknown); основной кейс {'infra': 20} при 10 обогащённых — не banned. --- .../backend/app/services/scrape_runs.py | 31 ++++++------ .../tests/test_3288b_block_kind_ratio.py | 49 +++++++++++++++---- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/tradein-mvp/backend/app/services/scrape_runs.py b/tradein-mvp/backend/app/services/scrape_runs.py index 40129059..4475253c 100644 --- a/tradein-mvp/backend/app/services/scrape_runs.py +++ b/tradein-mvp/backend/app/services/scrape_runs.py @@ -919,26 +919,27 @@ def mark_backfill_finished( counters["ban_kinds"] = dict(census) # type: ignore[assignment] dominant = _dominant_ban_kind(census) - if dominant == BAN_KIND_INFRA: + if dominant == BAN_KIND_INFRA and produced > 0: # #3288: 'banned' означает «площадка нас заблокировала» — и читается так - # же (триггер ротации IP, алерты, разбор простоя). Прогон, у которого - # доминирует infra, показывает обратное: отказал НАШ тракт (сайдкар не - # отдал страницу), площадка при этом молчала или вообще отвечала — прогон - # 5425 при 41 infra из 48 «блоков» честно обогатил 41 карточку и всё равно - # рапортовал «остановлен блоками источника». Диагноз уже посчитан - # (_dominant_ban_kind), здесь он впервые влияет на СТАТУС: не 'banned', а - # обычные правила ниже — 'failed' при нулевом результате (наша поломка, - # это она и есть) и 'done', если прогон всё-таки что-то обогатил. + # же (триггер ротации IP, алерты, разбор простоя). Прогон 5425 при 41 + # infra из 48 «блоков» честно обогатил 41 карточку — его оборвал брейкер + # по доле, а не площадка, — и всё равно рапортовал «остановлен блоками + # источника». Понижаем ровно этот случай: диагноз infra И прогон работу + # сделал → 'done'. + # + # Нулевой прогон с infra остаётся 'banned' — контракт #2764/#3196: + # там диагноз несёт ban_kind строки ('infra'), а статус говорит «прогон + # оборван отказами». Понижать его до 'failed' по одному лишь диагнозу + # опаснее исходного дефекта: при пустом/отсутствующем census (источник + # видов не различает) dominant='unknown', а настоящий бан площадки, + # опознанный как infra по 5xx, спрятался бы под «нашей поломкой». reason = ( - f"backfill-honest-status: {source} остановлен отказами НАШЕГО тракта — " - f"blocked={blocked} (диагноз '{BAN_KIND_INFRA}' у большинства), " + f"backfill-honest-status: {source} оборван брейкером на отказах НАШЕГО " + f"тракта — blocked={blocked} (диагноз '{BAN_KIND_INFRA}' у большинства), " f"обогащено {enriched} из {attempted} попыток{hint} (#3288)" ) logger.error("%s run_id=%d", reason, run_id) - if produced == 0: - mark_failed(db, run_id, reason, counters) - else: - mark_done(db, run_id, counters) + mark_done(db, run_id, counters) return reason = ( diff --git a/tradein-mvp/backend/tests/test_3288b_block_kind_ratio.py b/tradein-mvp/backend/tests/test_3288b_block_kind_ratio.py index 524a328b..61fd0460 100644 --- a/tradein-mvp/backend/tests/test_3288b_block_kind_ratio.py +++ b/tradein-mvp/backend/tests/test_3288b_block_kind_ratio.py @@ -38,7 +38,11 @@ from scraper_kit.proxy_errors import NoProxyAvailableError # noqa: E402 from app.services import scrape_runs as runs_svc # noqa: E402 from app.services.backfill_block_breaker import BlockRatioBreaker # noqa: E402 -from app.services.scrape_runs import BAN_KIND_INFRA, BAN_KIND_PLATFORM # noqa: E402 +from app.services.scrape_runs import ( # noqa: E402 + BAN_KIND_INFRA, + BAN_KIND_PLATFORM, + BAN_KIND_UNKNOWN, +) from app.tasks.avito_detail_backfill import ( # noqa: E402 AvitoDetailBackfillResult, run_avito_detail_backfill, @@ -315,17 +319,44 @@ def test_infra_dominant_run_is_not_marked_banned() -> None: ) -def test_infra_dominant_run_without_result_is_failed() -> None: - """Тот же диагноз, но ноль результата — это наша поломка: 'failed'.""" +def test_pure_infra_run_with_result_is_not_banned() -> None: + """Основной кейс без примеси: перепись целиком infra, карточки есть → не 'banned'.""" + banned, failed, done = _finish({BAN_KIND_INFRA: 20}, enriched=10) + + assert (banned.call_count, failed.call_count, done.call_count) == (0, 0, 1), ( + f"mark_banned={banned.call_count} mark_failed={failed.call_count} " + f"mark_done={done.call_count}: 20 отказов сайдкара при 10 обогащённых — 'done'" + ) + + +def test_infra_dominant_run_without_result_stays_banned() -> None: + """Контроль на ОБРАТНУЮ ошибку: ноль результата — по-прежнему 'banned' (#2764/#3196). + + Диагноз infra на нулевом прогоне неотличим от бана площадки, опознанного по 5xx + (yandex, #3196): статус остаётся 'banned', честность несёт ban_kind. Понижение + до 'failed' прятало бы настоящий бан под «нашу поломку» — падало 2 теста CI. + """ banned, failed, done = _finish({BAN_KIND_INFRA: 41, BAN_KIND_PLATFORM: 7}, enriched=0) - assert banned.call_count == 0, "'banned' при доминирующем infra" - assert (failed.call_count, done.call_count) == (1, 0), ( - f"mark_failed={failed.call_count} mark_done={done.call_count}: 0 обогащено " - "отказами нашего тракта — 'failed', не 'done'" + assert (banned.call_count, failed.call_count, done.call_count) == (1, 0, 0), ( + f"mark_banned={banned.call_count} mark_failed={failed.call_count} " + f"mark_done={done.call_count}: нулевой прогон обязан остаться 'banned'" ) - assert BAN_KIND_INFRA in failed.call_args.args[2], ( - f"причина={failed.call_args.args[2]!r}: статус обязан назвать диагноз" + assert banned.call_args.kwargs["ban_kind"] == BAN_KIND_INFRA, ( + f"ban_kind={banned.call_args.kwargs['ban_kind']!r}: диагноз обязан доехать" + ) + + +def test_run_without_census_stays_banned() -> None: + """Источник видов не различает (ban_kinds пуст) → прежнее поведение: 'banned'.""" + banned, failed, done = _finish({}, enriched=41) + + assert (banned.call_count, failed.call_count, done.call_count) == (1, 0, 0), ( + f"mark_banned={banned.call_count} mark_failed={failed.call_count} " + f"mark_done={done.call_count}: без переписи диагноза нет — статус не меняем" + ) + assert banned.call_args.kwargs["ban_kind"] == BAN_KIND_UNKNOWN, ( + f"ban_kind={banned.call_args.kwargs['ban_kind']!r}, ожидали 'unknown'" )