fix(tradein/scraper): длинная серия неудач перестаёт замолкать, оборванный прогон — называться успехом (#2670) #2720

Merged
bot-backend merged 2 commits from fix/2670-streak-and-coverage into main 2026-08-06 10:06:58 +00:00
Collaborator

1. Анти-спам, запертый в «один раз навсегда»

Оба сторожа слали алерт ровно на N-й подряд неудаче и дальше молчали. Пока источники падали вперемешку с успехами, серия рвалась и алерт взводился заново; у постоянно сломанного источника рваться нечему.

Прод 2026-08-06 — сколько «замолкло»:

источник текущий стрик failed/banned последний успешный прогон
avito_full_load 31 03.07 (34 дня назад)
avito_full_load_exhaustive 5
avito_city_sweep_pervouralsk 1
domclick_city_sweep 1

То есть 2 источника «замолкли»: алерт был ровно один, на третьей неудаче, а простой продолжается 34 дня.

Опровергнутая предпосылка. Условие прерывания у этого сторожа, в отличие от #2703, достижимо — и по данным прода достигается (domclick_city_sweep: стрик 1 при 47 завершённых прогонах). Ломается не достижимость, а срок: реальный простой длиннее одного напоминания. Поэтому применён не «сделать условие достижимым» (оно и так), а тот же принцип на другом конце — молчание не должно быть неограниченным.

Правка: разреженная лестница вех N, 2N, 4N…, дальше не реже одной на STREAK_ALERT_MAX_PERIOD × N прогонов; достигнутый потолок сканирования сам по себе повод для алерта — «замолчать навсегда» невозможно по построению. Лестница по прогонам, а не календарная: источники идут разным тактом (domclick — раз в сутки, proxy_healthcheck — раз в полчаса), календарное разрежение для одного из них всегда будет либо спамом, либо молчанием. Первый алерт приходит там же, где и раньше.

Та же лестница — у сторожа нулей (#2625/#2703): это сиблинги, чинить один и оставить второй значит оставить ту же дыру.

2. Оборванный прогон

Где терялся признак: в цикле DomClickScraper.fetch_city по ROOM_BUCKETSbreak на блоке, continue на битом бакете. Наружу он не выходил: метод возвращает голый список лотов, а pipeline писал в counters.pages_fetched расчётную оценку бакеты × страницы — оценку, не измерение. Прогон, прошедший 3 бакета из 6, был неотличим от полного.

Правка несёт признак оттуда, где он рождается: buckets_completed/buckets_total на самом скрейпере (тот же канал, что у blocked), включая случай снятия фазы по таймауту — ссылка на скрейпер там жива. Честный статус сравнивает собранное с ожидаемым охватом, а не с нулём.

Порядок веток: блок → banned; ноль лотов при ошибках → failed; неполный охват → failed; иначе done. Охват 0/0 (скрейпер не успел создаться) вердикта не выдумывает.

Сколько «оборвано, но done» на проде: 0 — у всех 26 done-прогонов domclick errors_count = 0. Это профилактика, как и написано в issue; и она честно измерима только теперь — до правки охват нигде не сохранялся, так что «0» относится к единственному доступному ретро-признаку, а не к охвату.

Побочно: _DOMCLICK_NUM_BUCKETS перестаёт быть вторым литералом рядом с ROOM_BUCKETS.

Test plan

  • tests/test_2670_streak_and_partial_coverage.py — 68 тестов, обе копии runs-модуля.
  • Фальсификация: на старом коде падают 43 из 68.
  • Полный прогон backend-сьюта: 3710 passed.

Refs #2670

## 1. Анти-спам, запертый в «один раз навсегда» Оба сторожа слали алерт **ровно** на N-й подряд неудаче и дальше молчали. Пока источники падали вперемешку с успехами, серия рвалась и алерт взводился заново; у постоянно сломанного источника рваться нечему. Прод 2026-08-06 — сколько «замолкло»: | источник | текущий стрик failed/banned | последний успешный прогон | |---|---|---| | `avito_full_load` | **31** | 03.07 (34 дня назад) | | `avito_full_load_exhaustive` | **5** | — | | `avito_city_sweep_pervouralsk` | 1 | — | | `domclick_city_sweep` | 1 | — | То есть 2 источника «замолкли»: алерт был ровно один, на третьей неудаче, а простой продолжается 34 дня. **Опровергнутая предпосылка.** Условие прерывания у этого сторожа, в отличие от #2703, **достижимо** — и по данным прода достигается (`domclick_city_sweep`: стрик 1 при 47 завершённых прогонах). Ломается не достижимость, а срок: реальный простой длиннее одного напоминания. Поэтому применён не «сделать условие достижимым» (оно и так), а тот же принцип на другом конце — молчание не должно быть неограниченным. Правка: разреженная лестница вех N, 2N, 4N…, дальше не реже одной на `STREAK_ALERT_MAX_PERIOD × N` прогонов; достигнутый потолок сканирования сам по себе повод для алерта — «замолчать навсегда» невозможно по построению. Лестница по **прогонам**, а не календарная: источники идут разным тактом (domclick — раз в сутки, proxy_healthcheck — раз в полчаса), календарное разрежение для одного из них всегда будет либо спамом, либо молчанием. Первый алерт приходит там же, где и раньше. Та же лестница — у сторожа нулей (#2625/#2703): это сиблинги, чинить один и оставить второй значит оставить ту же дыру. ## 2. Оборванный прогон Где терялся признак: в цикле `DomClickScraper.fetch_city` по `ROOM_BUCKETS` — `break` на блоке, `continue` на битом бакете. Наружу он не выходил: метод возвращает голый список лотов, а pipeline писал в `counters.pages_fetched` расчётную оценку `бакеты × страницы` — оценку, не измерение. Прогон, прошедший 3 бакета из 6, был неотличим от полного. Правка несёт признак оттуда, где он рождается: `buckets_completed`/`buckets_total` на самом скрейпере (тот же канал, что у `blocked`), включая случай снятия фазы по таймауту — ссылка на скрейпер там жива. Честный статус сравнивает собранное с ожидаемым охватом, а не с нулём. Порядок веток: блок → `banned`; ноль лотов при ошибках → `failed`; неполный охват → `failed`; иначе `done`. Охват `0/0` (скрейпер не успел создаться) вердикта не выдумывает. Сколько «оборвано, но done» на проде: **0** — у всех 26 `done`-прогонов domclick `errors_count = 0`. Это профилактика, как и написано в issue; и она честно измерима только теперь — до правки охват нигде не сохранялся, так что «0» относится к единственному доступному ретро-признаку, а не к охвату. Побочно: `_DOMCLICK_NUM_BUCKETS` перестаёт быть вторым литералом рядом с `ROOM_BUCKETS`. ## Test plan - [x] `tests/test_2670_streak_and_partial_coverage.py` — 68 тестов, обе копии runs-модуля. - [x] Фальсификация: на старом коде падают 43 из 68. - [x] Полный прогон backend-сьюта: 3710 passed. Refs #2670
bot-backend added 1 commit 2026-08-06 09:50:20 +00:00
fix(tradein/scraper): длинная серия неудач перестаёт замолкать, оборванный прогон — называться успехом (#2670)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
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
CI Trade-In / backend-tests (pull_request) Successful in 3m1s
fb4e44e521
1. Анти-спам «один раз на стрик» был заперт в «один раз навсегда». Оба сторожа
слали алерт РОВНО на N-й подряд неудаче и дальше молчали: пока источники падали
вперемешку с успехами, серия рвалась и алерт взводился заново, а у постоянно
сломанного источника рваться нечему. Прод 2026-08-06: avito_full_load — 31 неудача
подряд, последний успешный прогон 03.07 (34 дня без сбора), алерт был ровно один;
avito_full_load_exhaustive — 5 подряд.

Условие прерывания у этого сторожа, в отличие от #2703, ДОСТИЖИМО — по данным прода
оно достигается (domclick_city_sweep: текущий стрик 1 при 47 завершённых прогонах).
Ломается не достижимость, а срок: реальный простой длиннее одного напоминания.
Поэтому вместо «ровно N» — разреженная лестница вех N, 2N, 4N…, дальше не реже
одной на STREAK_ALERT_MAX_PERIOD×N прогонов; достигнутый потолок сканирования сам
по себе повод для алерта, так что «замолчать навсегда» невозможно по построению.
Лестница по ПРОГОНАМ, а не календарная: источники идут разным тактом (domclick —
раз в сутки, proxy_healthcheck — раз в полчаса).

2. Признак обрыва рождался в цикле fetch_city по ROOM_BUCKETS (break на блоке,
continue на битом бакете) и наружу не выходил: метод возвращает голый список лотов,
а pipeline писал в counters.pages_fetched расчётную оценку «бакеты × страницы» —
оценку, не измерение. Прогон, прошедший 3 бакета из 6, был неотличим от полного и
уходил в done. Теперь охват считает сам скрейпер (buckets_completed/buckets_total)
и несёт наружу тем же каналом, что и blocked, — включая случай снятия фазы по
таймауту, где ссылка на скрейпер жива. Честный статус сравнивает собранное с
ожидаемым охватом, а не с нулём.

Порядок веток: блок (banned) → ноль лотов при ошибках (failed) → неполный охват
(failed) → done. Охват 0/0 (скрейпер не успел создаться) вердикта не выдумывает.

Заодно _DOMCLICK_NUM_BUCKETS перестаёт быть вторым литералом рядом с ROOM_BUCKETS.

На проде случаев неполного охвата пока не зафиксировано (0 прогонов domclick со
статусом done и errors_count>0) — это профилактика, и она честно измерима только
теперь: до правки охват нигде не сохранялся.

Refs #2670
Light1YT added 1 commit 2026-08-06 09:58:39 +00:00
merge main (#2702 clock_timestamp) в ветку #2670
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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
CI Trade-In / backend-tests (pull_request) Successful in 3m5s
a5ce832069
bot-backend merged commit b8afe13ae4 into main 2026-08-06 10:06:58 +00:00
bot-backend deleted branch fix/2670-streak-and-coverage 2026-08-06 10:06:58 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#2720
No description provided.