fix(tradein): прогоны, оборванные деплоем (interrupted=1), невидимы лестницам стриков и honest-status-гейтам (#3393) #3395

Merged
bot-backend merged 2 commits from fix/3393-interrupted-runs-outside-streaks into main 2026-09-06 05:53:25 +00:00
Collaborator

Closes #3393.

Дефект. После #3392 прерванные деплоем прогоны финализируются mark_done/mark_failed (раньше — zombie) и попадают в лестницы: _failed_ratio_too_high судит частичные counters (20 % отказов при обрыве → failed с диагнозом «сбор деградировал» + алерт по 3 подряд), а done cian-бэкфилла обнуляет стрик банов источника (задокументированный вред orchestration/scheduler.py:90-108).

Фикс (одно место на лестницу, обе копии модуля — app scrape_runs.py и kit orchestration/runs.py, #3390):

  • _was_interrupted(counters) рядом с _leading_streak;
  • сторож failed/banned (scrape_runs.py:429-464, runs.py:428-463) и сторож zero-result (:481-563 / :480-562): rows = [r for r in rows if not _was_interrupted(...)] — прерванный прогон не продлевает и не обнуляет («как будто его не было»; cancelled оставлен прерывателем — там решение человека). Фильтр в Python: сторож под except Exception: pass, живого Postgres в тестах нет, SQL-фильтр был бы непроверяем по значению; в SELECT добавлена колонка counters, getattr — чтобы отсутствие колонки не выключило сторож молча;
  • mark_done (:691-731 / :765-805): три honest-status-гейта пропускаются при interrupted с logger.warning("… оборван деплоем (SIGTERM-drain), частичный результат — honest-status-гейты пропущены (#3393)"); колонка error не трогается (машиночитаемая метка есть в counters).

Семантика: failed, failed, failed+interrupted, failed → стрик 3 (не 4 и не 2); banned, banned, done+interrupted, banned → 3.

Тесты tests/test_3393_interrupted_runs_outside_streaks.py (параметризованы по обеим копиям) + 4 контроля «те же формы без метки ведут себя как раньше».

Фальсификация (git apply -R исходников): 10 failed по значению — Expected 'capture_message' to have been called once. Called 0 times, assert ['failed'] == ['done'], в логе failed-ratio-honest-status … 20 %; контроли зелёные и на main. Восстановлено.

Прогоны: полный backend 5544 passed, 35 skipped (rc=0); ruff OK.

Приёмка на проде: маркер _was_interrupted в обеих копиях контейнера scraper (на main символа нет); событийно — после первого деплоя, убившего бегущий прогон: строки с interrupted='1' без error LIKE '%failed-ratio-honest-status%', в логах оборван деплоем (SIGTERM-drain) … гейты пропущены (#3393) для run_id из scheduler: drain — … interrupted, и стрик источника (считая без interrupted-строк) совпадает с числом в ближайшем алерте.

Closes #3393. **Дефект.** После #3392 прерванные деплоем прогоны финализируются `mark_done`/`mark_failed` (раньше — `zombie`) и попадают в лестницы: `_failed_ratio_too_high` судит частичные counters (20 % отказов при обрыве → `failed` с диагнозом «сбор деградировал» + алерт по 3 подряд), а `done` cian-бэкфилла обнуляет стрик банов источника (задокументированный вред `orchestration/scheduler.py:90-108`). **Фикс (одно место на лестницу, обе копии модуля — app `scrape_runs.py` и kit `orchestration/runs.py`, #3390):** - `_was_interrupted(counters)` рядом с `_leading_streak`; - сторож failed/banned (`scrape_runs.py:429-464`, `runs.py:428-463`) и сторож zero-result (`:481-563` / `:480-562`): `rows = [r for r in rows if not _was_interrupted(...)]` — прерванный прогон **не продлевает и не обнуляет** («как будто его не было»; `cancelled` оставлен прерывателем — там решение человека). Фильтр в Python: сторож под `except Exception: pass`, живого Postgres в тестах нет, SQL-фильтр был бы непроверяем по значению; в SELECT добавлена колонка `counters`, `getattr` — чтобы отсутствие колонки не выключило сторож молча; - `mark_done` (`:691-731` / `:765-805`): три honest-status-гейта пропускаются при `interrupted` с `logger.warning("… оборван деплоем (SIGTERM-drain), частичный результат — honest-status-гейты пропущены (#3393)")`; колонка `error` не трогается (машиночитаемая метка есть в counters). Семантика: `failed, failed, failed+interrupted, failed` → стрик 3 (не 4 и не 2); `banned, banned, done+interrupted, banned` → 3. **Тесты** `tests/test_3393_interrupted_runs_outside_streaks.py` (параметризованы по обеим копиям) + 4 контроля «те же формы без метки ведут себя как раньше». **Фальсификация** (`git apply -R` исходников): 10 failed по значению — `Expected 'capture_message' to have been called once. Called 0 times`, `assert ['failed'] == ['done']`, в логе `failed-ratio-honest-status … 20 %`; контроли зелёные и на main. Восстановлено. **Прогоны:** полный backend `5544 passed, 35 skipped` (rc=0); ruff OK. **Приёмка на проде:** маркер `_was_interrupted` в обеих копиях контейнера scraper (на main символа нет); событийно — после первого деплоя, убившего бегущий прогон: строки с `interrupted='1'` без `error LIKE '%failed-ratio-honest-status%'`, в логах `оборван деплоем (SIGTERM-drain) … гейты пропущены (#3393)` для run_id из `scheduler: drain — … interrupted`, и стрик источника (считая без interrupted-строк) совпадает с числом в ближайшем алерте.
bot-backend added 1 commit 2026-09-06 04:44:10 +00:00
fix(tradein/scrape_runs): оборванный деплоем прогон вне лестниц стриков и honest-status (#3393)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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
CI Trade-In / backend-tests (pull_request) Successful in 4m57s
5f94bf0eeb
После #3392 SIGTERM-дрейн финализирует in-flight прогоны штатными
mark_done/mark_failed с counters.interrupted=1 — раньше они оставались
'running' → 'zombie' и сторожей не касались. В популяции стриков эти строки
судят частичные счётчики:

  - detail-бэкфилл, убитый на 20% отказов, получал 'failed' с диагнозом
    «сбор деградировал» (_failed_ratio_too_high) — диагноз про площадку,
    которого никто не измерял, — и входил в стрик неудач;
  - cian-бэкфилл (без attempted) получал 'done' и ОБНУЛЯЛ стрик банов —
    ровно вред, задокументированный в orchestration/scheduler.py:99
    («5 банов подряд обнулил один 'cancelled' 09.08»).

Прогон с меткой interrupted теперь считается так, будто его не было: он
стрик ни продлевает, ни обнуляет (обе лестницы, обе копии модуля), а три
honest-status-гейта в mark_done пропускаются — статус остаётся 'done' с
меткой interrupted (конвенция #3319/#3333/#3355), причина в логе.
'cancelled' оставлен как был: там прогон прервал человек.

SELECT сторожа неудач дополнен колонкой counters — по ней и идёт отбор.
Author
Collaborator

Code-review (06.09): minor. Подтверждено: обе копии байт-в-байт (кроме кросс-ссылки в докстринге); interrupted не наследуется при resume (все 7 resume-сайтов pipeline берут только done_buckets, DKP — last_id) → пропуск гейтов в mark_done не расширяется за пределы дрейна; zero-сторож решает «измерен ли» по свежайшему судимому прогону; рефактор трёх гейтов в or-цепочку эквивалентен (все не-None ветки — непустые f-строки).

Реальная находка (докатываю до мержа): capped = streak >= STREAK_SCAN_LIMIT считается по отфильтрованному окну — одна interrupted-строка в 500 делает capped недостижимым (макс. стрик 499), вехи лестницы после 384 идут только на 768 → сломанный наглухо источник после 384-й неудачи молчит навсегда, и каждый деплой подкладывает новую interrupted-строку (класс #2670/#2703). Ревьюер прогнал вживую: 500 failed без interrupted → алерт; с одной interrupted → SILENT. Фикс: scanned = len(rows) до фильтра, capped = scanned >= LIMIT and streak >= len(rows), в обеих копиях + тест-контрпример + контроль без метки для zero-сторожа.

Code-review (06.09): minor. Подтверждено: обе копии байт-в-байт (кроме кросс-ссылки в докстринге); `interrupted` не наследуется при resume (все 7 resume-сайтов pipeline берут только `done_buckets`, DKP — `last_id`) → пропуск гейтов в `mark_done` не расширяется за пределы дрейна; zero-сторож решает «измерен ли» по свежайшему судимому прогону; рефактор трёх гейтов в `or`-цепочку эквивалентен (все не-None ветки — непустые f-строки). **Реальная находка (докатываю до мержа):** `capped = streak >= STREAK_SCAN_LIMIT` считается по отфильтрованному окну — одна `interrupted`-строка в 500 делает `capped` недостижимым (макс. стрик 499), вехи лестницы после 384 идут только на 768 → сломанный наглухо источник после 384-й неудачи молчит навсегда, и каждый деплой подкладывает новую interrupted-строку (класс #2670/#2703). Ревьюер прогнал вживую: 500 failed без interrupted → алерт; с одной interrupted → SILENT. Фикс: `scanned = len(rows)` до фильтра, `capped = scanned >= LIMIT and streak >= len(rows)`, в обеих копиях + тест-контрпример + контроль без метки для zero-сторожа.
Light1YT added 1 commit 2026-09-06 05:07:43 +00:00
fix(#3393): capped по сканированному окну — interrupted-строка не глушит лестницу навсегда; контроль zero-сторожа
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
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
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
ba4fb03a05
bot-backend merged commit fd4bd65126 into main 2026-09-06 05:53:25 +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#3395
No description provided.