fix(tradein/scraper): прогон, у которого отказал каждый якорь, перестаёт быть успехом (#2625) #2813

Merged
bot-backend merged 1 commit from fix/2625-run-that-did-nothing into main 2026-08-10 09:36:24 +00:00
Collaborator

Summary

Остаток #2625 после #2642. Основную часть (капча Циана → banned, заглушки Яндекса → banned) закрыл #2642 — это подтверждено замером прода, см. ниже. Здесь чинится третий исход, у которого терминального статуса не было вовсе.

Замер до/после (прод, 2026-08-10)

Развёртки cian_city_sweep* / yandex_city_sweep* / *_full_load, окно от 21.07:

эра done из них с нулём banned failed
до #2642 (21.07 → 04.08 19:00 UTC) 89 42 (47%) 0 0
после (04.08 19:00 → 10.08) 30 1 (3%) 0 0

Честная оговорка: banned = 0 в обеих колонках. Значит падение 42 → 1 дал не детект #2642, а то, что сбор реально заработал (#2796, #2798, #2800-#2805, #2804). Детект #2642 на проде не срабатывал ни разу — его правильность подтверждена только юнит-тестами.

Что осталось

Тот единственный — прогон 3557 (yandex_city_sweep_pervouralsk, 09.08 17:11): 155 мс, {"anchors_total": 1, "anchors_done": 1, "errors_count": 1, "lots_fetched": 0}, статус done.

И он не новый. За 90 суток под тот же признак попадают 28 прогонов, в том числе yandex_city_sweep_nizhniy_tagil16 суток подряд (15-30.07), каждый ровно 240 с (таймаут якоря), ноль лотов, done каждый раз.

Почему детект #2642 их не ловит — и правильно не ловит

Он считает попытки разбора (_track_gate_result), а transport_error туда не попадает намеренно (#2600 п.1: «наш прокси сдох» не должно выглядеть баном площадки). Якорь, упавший по таймауту или исключению до первого разобранного ответа, даёт attempts == 0 — условие attempts > 0 and failures == attempts молчит, и прогон уходит в done.

Три случая разведены

случай статус где
площадка отбила (структура не извлеклась ни разу) banned #2642, есть
площадка честно отдала пустоту done есть, здоровый ответ — сохранён
мы не дошли (таймаут / исключение якоря) donefailed этот PR

Признак и что он доказывает

errors_count >= anchors_total при измеренном нуле (через существующий _run_result_count, #2703 — «не измерено» ≠ ноль).

  • Доказывает: отказом кончился каждый якорь, который у прогона был, и собрано ноль.
  • НЕ доказывает: кто виноват — капча площадки, наш прокси или наш баг.

Поэтому failed без ban_kind, а не banned/platform: ротацию IP (#2611) на догадке дёргать нельзя (#2764). Признак намеренно не опирается на список антибот-маркеров — маркеры объясняют уже случившийся отказ и молчат про неизвестный (урок 09.08, #2798).

Где стоит страж

В mark_done, а не в каждом sweep'е: у финализатора четыре десятка вызывающих, и сторож, который надо не забыть позвать, — тот же дефект оборванной проводки, из-за которого задача и появилась. Обе strangler-копии (app.services.scrape_runs + scraper_kit.orchestration.runs).

Ложная тревога не создаётся (замер, 90 суток)

  • 132 прогона с отказами, но ненулевым сбором → остаются done (частичный сбор — сбор);
  • 37 прогонов честной пустоты (errors_count = 0, ноль лотов) → остаются done;
  • flip получают ровно 28 описанных выше.

Test plan

  • tests/test_2625_run_that_did_nothing.py — 34 проверки, красный на origin/main: AssertionError: assert ['done'] == ['failed'] (не AttributeError — поведенческие тесты читают статус в SQL, а не имя новой функции). Контрольные test_partial_harvest_stays_done / test_honest_empty_stays_done проходят на старом коде — тест не требует failed огульно.
  • counters в тестах — реальные прод-строки (3557, 3320, 2930), не выдуманные.
  • Полный набор бэкенда: 4131 passed, 18 skipped.
  • ruff check + format.

Критерий приёмки на проде — записан ДО факта

Ближайшие прогоны, способные сработать: yandex_city_sweep_nizhniy_tagil (~14:10 UTC ежедневно) и yandex_city_sweep_pervouralsk (~17:10 UTC, раз в 3 суток).

Проверка 11.08 после 18:00 UTC:

SELECT id, source, status, error, counters->>'errors_count' AS errs,
       counters->>'anchors_total' AS anchors, counters->>'lots_fetched' AS lots
FROM scrape_runs
WHERE started_at >= timestamptz '2026-08-11 00:00+00'
  AND source LIKE '%_city_sweep%'
ORDER BY started_at;
  • Принято, если ни одной строки со status='done' при errors_count >= anchors_total и lots_fetched = 0; строки, попавшие под правило, несут status='failed' и error LIKE 'sweep-honest-status:%'.
  • Отклонено, если появился failed у прогона с lots_fetched > 0 или с errors_count < anchors_total — это ложная тревога, откатывать.
  • Не проверено (ожидаемый и допустимый исход): все прогоны собрали данные, правило не сработало ни разу. Тогда срок годности вердикта — до 24.08; если за две недели ни один прогон не отказал целиком, признак остаётся подтверждён только тестами, как и детект #2642.

Refs #2625

🤖 Generated with Claude Code

## Summary Остаток #2625 после #2642. Основную часть (капча Циана → `banned`, заглушки Яндекса → `banned`) закрыл #2642 — это подтверждено замером прода, см. ниже. Здесь чинится **третий исход**, у которого терминального статуса не было вовсе. ### Замер до/после (прод, 2026-08-10) Развёртки `cian_city_sweep*` / `yandex_city_sweep*` / `*_full_load`, окно от 21.07: | эра | `done` | из них с нулём | `banned` | `failed` | |---|---|---|---|---| | до #2642 (21.07 → 04.08 19:00 UTC) | 89 | **42 (47%)** | 0 | 0 | | после (04.08 19:00 → 10.08) | 30 | **1 (3%)** | 0 | 0 | **Честная оговорка:** `banned = 0` в обеих колонках. Значит падение 42 → 1 дал не детект #2642, а то, что сбор реально заработал (#2796, #2798, #2800-#2805, #2804). Детект #2642 на проде **не срабатывал ни разу** — его правильность подтверждена только юнит-тестами. ### Что осталось Тот единственный — прогон 3557 (`yandex_city_sweep_pervouralsk`, 09.08 17:11): **155 мс**, `{"anchors_total": 1, "anchors_done": 1, "errors_count": 1, "lots_fetched": 0}`, статус `done`. И он не новый. За 90 суток под тот же признак попадают **28 прогонов**, в том числе `yandex_city_sweep_nizhniy_tagil` — **16 суток подряд** (15-30.07), каждый ровно 240 с (таймаут якоря), ноль лотов, `done` каждый раз. ### Почему детект #2642 их не ловит — и правильно не ловит Он считает попытки **разбора** (`_track_gate_result`), а `transport_error` туда не попадает **намеренно** (#2600 п.1: «наш прокси сдох» не должно выглядеть баном площадки). Якорь, упавший по таймауту или исключению до первого разобранного ответа, даёт `attempts == 0` — условие `attempts > 0 and failures == attempts` молчит, и прогон уходит в `done`. ### Три случая разведены | случай | статус | где | |---|---|---| | площадка отбила (структура не извлеклась ни разу) | `banned` | #2642, есть | | площадка честно отдала пустоту | `done` | есть, здоровый ответ — сохранён | | **мы не дошли** (таймаут / исключение якоря) | ~~`done`~~ → **`failed`** | этот PR | ### Признак и что он доказывает `errors_count >= anchors_total` при **измеренном** нуле (через существующий `_run_result_count`, #2703 — «не измерено» ≠ ноль). - **Доказывает:** отказом кончился каждый якорь, который у прогона был, и собрано ноль. - **НЕ доказывает:** кто виноват — капча площадки, наш прокси или наш баг. Поэтому `failed` **без** `ban_kind`, а не `banned`/`platform`: ротацию IP (#2611) на догадке дёргать нельзя (#2764). Признак намеренно **не** опирается на список антибот-маркеров — маркеры объясняют уже случившийся отказ и молчат про неизвестный (урок 09.08, #2798). ### Где стоит страж В `mark_done`, а не в каждом sweep'е: у финализатора четыре десятка вызывающих, и сторож, который надо не забыть позвать, — тот же дефект оборванной проводки, из-за которого задача и появилась. Обе strangler-копии (`app.services.scrape_runs` + `scraper_kit.orchestration.runs`). ### Ложная тревога не создаётся (замер, 90 суток) - **132** прогона с отказами, но ненулевым сбором → остаются `done` (частичный сбор — сбор); - **37** прогонов честной пустоты (`errors_count = 0`, ноль лотов) → остаются `done`; - flip получают ровно 28 описанных выше. ## Test plan - [x] `tests/test_2625_run_that_did_nothing.py` — 34 проверки, **красный на origin/main**: `AssertionError: assert ['done'] == ['failed']` (не AttributeError — поведенческие тесты читают статус в SQL, а не имя новой функции). Контрольные `test_partial_harvest_stays_done` / `test_honest_empty_stays_done` **проходят на старом коде** — тест не требует `failed` огульно. - [x] counters в тестах — реальные прод-строки (3557, 3320, 2930), не выдуманные. - [x] Полный набор бэкенда: 4131 passed, 18 skipped. - [x] ruff check + format. ## Критерий приёмки на проде — записан ДО факта Ближайшие прогоны, способные сработать: `yandex_city_sweep_nizhniy_tagil` (~14:10 UTC ежедневно) и `yandex_city_sweep_pervouralsk` (~17:10 UTC, раз в 3 суток). Проверка **11.08 после 18:00 UTC**: ```sql SELECT id, source, status, error, counters->>'errors_count' AS errs, counters->>'anchors_total' AS anchors, counters->>'lots_fetched' AS lots FROM scrape_runs WHERE started_at >= timestamptz '2026-08-11 00:00+00' AND source LIKE '%_city_sweep%' ORDER BY started_at; ``` - **Принято**, если ни одной строки со `status='done'` при `errors_count >= anchors_total` и `lots_fetched = 0`; строки, попавшие под правило, несут `status='failed'` и `error LIKE 'sweep-honest-status:%'`. - **Отклонено**, если появился `failed` у прогона с `lots_fetched > 0` или с `errors_count < anchors_total` — это ложная тревога, откатывать. - **Не проверено** (ожидаемый и допустимый исход): все прогоны собрали данные, правило не сработало ни разу. Тогда срок годности вердикта — до 24.08; если за две недели ни один прогон не отказал целиком, признак остаётся подтверждён только тестами, как и детект #2642. Refs #2625 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-08-10 09:31:25 +00:00
fix(tradein/scraper): прогон, у которого отказал каждый якорь, перестаёт быть успехом (#2625)
All checks were successful
CI Trade-In / backend-tests (pull_request) Successful in 3m51s
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
7c8ecaaf2d
Основную часть задачи закрыл #2642 (капча Циана / заглушки Яндекса → banned).
Замер прода 2026-08-10 подтверждает симптом: развёртки cian/yandex со статусом
'done' и нулём — 42 из 89 до деплоя #2642 (04.08 19:00 UTC), 1 из 30 после.

Оставшийся один — прогон 3557 (yandex_city_sweep_pervouralsk, 09.08 17:11):
155 мс, anchors_total=1, errors_count=1, lots_fetched=0, статус 'done'. За 90
суток под тот же признак попадают 28 прогонов, включая 16 суток подряд
yandex_city_sweep_nizhniy_tagil (15-30.07), каждый ровно 240 с — таймаут якоря.

Детект #2642 их не видит и правильно не видит: он считает попытки РАЗБОРА, а
transport_error туда намеренно не попадает (#2600 п.1 — «наш прокси сдох» не
должно выглядеть баном площадки). Якорь, упавший до первого разобранного
ответа, даёт attempts=0, условие «attempts>0 и все провалились» молчит.

То есть третий исход существовал без терминального статуса:
  площадка отбила       → banned (#2642)
  площадка честно пуста → done   (здоровый ответ, сохранён)
  мы не дошли           → ???    → done   ← дефект

Признак — собственная бухгалтерия прогона, не список антибот-маркеров:
errors_count >= anchors_total при ИЗМЕРЕННОМ нуле. Доказывает: каждый якорь
кончился отказом и собрано ноль. НЕ доказывает, кто виноват — поэтому 'failed'
без ban_kind, а не 'banned'/'platform' (#2764).

Страж стоит в mark_done, а не в каждом sweep'е: у финализатора четыре десятка
вызывающих, и сторож, который надо не забыть позвать, — это тот же дефект
оборванной проводки, из-за которого задача и появилась.

Ложной тревоги нет (замер, 90 суток): 132 прогона с отказами, но ненулевым
сбором, и 37 прогонов честной пустоты (errors_count=0) остаются 'done'.
bot-backend merged commit a227877905 into main 2026-08-10 09:36:24 +00:00
bot-backend deleted branch fix/2625-run-that-did-nothing 2026-08-10 09:36:24 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2813
No description provided.