refactor(tradein): одна реализация scrape_runs — kit orchestration/runs.py, app-модуль = алиас; counters везде мержатся (#3390) #3400

Merged
bot-backend merged 2 commits from refactor/3390-single-runs-module into main 2026-09-06 08:11:53 +00:00
Collaborator

Closes #3390.

Дефект. Две живые копии одного модуля с противоположной семантикой counters: app scrape_runs.py ЗАМЕНЯЛ (CAST(:counters AS jsonb)), kit orchestration/runs.py МЕРЖИТ (||). Дважды за сутки давало ложные выводы у ревью (#3388, #3355); прод уже обходил app-копию как дефектную (app/services/scheduler.py в трёх местах намеренно зовёт kit_runs.update_heartbeat, потому что своя стирала resume_verdict, #3168).

Инвентаризация (в отчёте воркера полностью): app-only функции перенесены в kit — honors_cancel (+константы; admin.py:2603, mark_cancelled), mark_backfill_finished (4 detail-бэкфилла), _dominant_ban_kind (cian_history_backfill.py:94), distinct_sources (admin.py:2631); mark_skipped была только в kit. Расхождения общих функций: update_heartbeat/mark_done/mark_failed/mark_bannedмерж (kit); mark_cancelledверсия app с гейтом honors_cancel (kit без гейта пометил бы cancelled любой прогон); _alert_if_consecutive_zero_results — kit (if sentry_sdk is None); остальное идентично. Ни одного вызывающего, который полагается на удаление ключей заменой (строка прогона создаётся пустой, резюм читает counters предыдущего прогона по id, финальный to_dict() у avito/domclick содержит тот же набор ключей); единственный обратный случай admin.py:2018 mark_failed(..., {}) при мерже сохраняет замер пульса — улучшение.

Решение. app/services/scrape_runs.py (36 строк) — sys.modules[__name__] = _kit_runs: оба имени — один объект модуля. Реэкспорт имён сломал бы ≥11 тест-файлов (patch("app.services.scrape_runs.sentry_sdk") ×24, patch.object(runs_mod, "mark_done") + mark_backfill_finished и т.д.) — патч на обёртке не доезжал бы до глобалей kit; алиас снимает это целиком, правок в тестах ноль. scheduler.py/pipeline.py — только комментарии; текстовый гейт test_3168 цел (схлопывание двух имён kit_runs/runs_mod в scheduler.py — отдельный PR, упирается в этот гейт).

Тесты tests/test_3390_single_runs_module.py (25): оба пути импорта — один объект на каждую функцию; heartbeat {a:5}mark_failed({b:1}){a:5,b:1} через обе точки входа; done_buckets переживают финализатор; mark_cancelled с гейтом honors_cancel.

Фальсификация (на main, тест-файл на месте): 16 failed по значению — финализатор ЗАМЕНИЛ counters — замер heartbeat'а потерян, KeyError: 'done_buckets', create_run: app-копия и kit-копия — разные объекты, mark_cancelled kit без honors_cancel-гейта. После правки 25 passed.

Прогоны: полный backend 5586 passed, 35 skipped (rc=0); ruff OK. Тестов, проверявших именно ЗАМЕНУ по значению, в сьюте нет.

Приёмка на проде: после деплоя у app-задач (avito/domclick_detail_backfill, источники scheduler.py) counters финализированных прогонов ⊇ ключей последнего heartbeat (стало БОЛЬШЕ ключей, не меньше); interrupted/done_buckets переживают финализатор; total_seen/new_count не затронуты; POST /admin/scrape/{id}/cancel на прогоне источника без honors_cancel по-прежнему отказывает.

Closes #3390. **Дефект.** Две живые копии одного модуля с противоположной семантикой counters: app `scrape_runs.py` ЗАМЕНЯЛ (`CAST(:counters AS jsonb)`), kit `orchestration/runs.py` МЕРЖИТ (`||`). Дважды за сутки давало ложные выводы у ревью (#3388, #3355); прод уже обходил app-копию как дефектную (`app/services/scheduler.py` в трёх местах намеренно зовёт `kit_runs.update_heartbeat`, потому что своя стирала `resume_verdict`, #3168). **Инвентаризация (в отчёте воркера полностью):** app-only функции перенесены в kit — `honors_cancel` (+константы; `admin.py:2603`, `mark_cancelled`), `mark_backfill_finished` (4 detail-бэкфилла), `_dominant_ban_kind` (`cian_history_backfill.py:94`), `distinct_sources` (`admin.py:2631`); `mark_skipped` была только в kit. Расхождения общих функций: `update_heartbeat`/`mark_done`/`mark_failed`/`mark_banned` — **мерж** (kit); `mark_cancelled` — **версия app с гейтом `honors_cancel`** (kit без гейта пометил бы `cancelled` любой прогон); `_alert_if_consecutive_zero_results` — kit (`if sentry_sdk is None`); остальное идентично. **Ни одного вызывающего, который полагается на удаление ключей заменой** (строка прогона создаётся пустой, резюм читает counters предыдущего прогона по id, финальный `to_dict()` у avito/domclick содержит тот же набор ключей); единственный обратный случай `admin.py:2018 mark_failed(..., {})` при мерже сохраняет замер пульса — улучшение. **Решение.** `app/services/scrape_runs.py` (36 строк) — `sys.modules[__name__] = _kit_runs`: оба имени — один объект модуля. Реэкспорт имён сломал бы ≥11 тест-файлов (`patch("app.services.scrape_runs.sentry_sdk")` ×24, `patch.object(runs_mod, "mark_done")` + `mark_backfill_finished` и т.д.) — патч на обёртке не доезжал бы до глобалей kit; алиас снимает это целиком, правок в тестах ноль. `scheduler.py`/`pipeline.py` — только комментарии; текстовый гейт `test_3168` цел (схлопывание двух имён `kit_runs`/`runs_mod` в scheduler.py — отдельный PR, упирается в этот гейт). **Тесты** `tests/test_3390_single_runs_module.py` (25): оба пути импорта — один объект на каждую функцию; heartbeat `{a:5}` → `mark_failed({b:1})` → `{a:5,b:1}` через обе точки входа; `done_buckets` переживают финализатор; `mark_cancelled` с гейтом `honors_cancel`. **Фальсификация** (на main, тест-файл на месте): 16 failed по значению — `финализатор ЗАМЕНИЛ counters — замер heartbeat'а потерян`, `KeyError: 'done_buckets'`, `create_run: app-копия и kit-копия — разные объекты`, `mark_cancelled kit без honors_cancel-гейта`. После правки 25 passed. **Прогоны:** полный backend `5586 passed, 35 skipped` (rc=0); ruff OK. Тестов, проверявших именно ЗАМЕНУ по значению, в сьюте нет. **Приёмка на проде:** после деплоя у app-задач (`avito/domclick_detail_backfill`, источники `scheduler.py`) `counters` финализированных прогонов ⊇ ключей последнего heartbeat (стало БОЛЬШЕ ключей, не меньше); `interrupted`/`done_buckets` переживают финализатор; `total_seen`/`new_count` не затронуты; `POST /admin/scrape/{id}/cancel` на прогоне источника без `honors_cancel` по-прежнему отказывает.
bot-backend added 1 commit 2026-09-06 06:49:04 +00:00
refactor(tradein/runs): одна реализация scrape_runs — kit, семантика counters мерж (#3390)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
CI / changes (pull_request) Successful in 9s
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
36f2429fbe
Две живые копии одного модуля с противоположной семантикой counters: app
`mark_done`/`mark_failed`/`mark_banned`/`update_heartbeat` ЗАМЕНЯЛИ
(`counters = CAST(:counters AS jsonb)`), kit — МЕРЖИЛИ
(`COALESCE(counters,'{}') || …`). Расхождение дважды за сутки дало ложные
выводы на ревью (#3388 «отдать только флаг, остальное домержится» — на
replace это стёрло бы измеренное; #3355). Разошлись и другие места: гейт
статуса, `honors_cancel` у mark_cancelled (был только в app), `mark_skipped`
(только в kit), `mark_backfill_finished`/`distinct_sources` (только в app).

Реализация теперь одна — `scraper_kit.orchestration.runs`; в неё перенесены
app-only функции. `app.services.scrape_runs` — алиас kit-модуля через
sys.modules, а не реэкспорт имён: реэкспорт разводит патч-цели
(`patch("app.services.scrape_runs.sentry_sdk")`, `patch.object(runs_mod,
"mark_done")` правили бы глобаль модуля-обёртки, а тело функции читает
глобаль kit'а) — тест остался бы зелёным, не подменив ничего. С алиасом оба
имени ведут в единственную реализацию, и ни один из ~40 вызывающих и ~30
патч-сайтов в тестах не правится.

Победила семантика мержа: у строки прогона несколько писателей (пульс,
финализатор, дрейн), каждый знает лишь свои ключи, и замена теряла чужие —
чекпоинт done_buckets (#930), метку interrupted (#3391), замер из пульса
(#3384). Обратной зависимости («вызывающий рассчитывает, что финализатор
УДАЛИТ ключ заменой») нет: строка создаётся пустой в create_run, резюм читает
counters ПРЕДЫДУЩЕГО прогона по его id.

Тесты по значению на обоих путях импорта (двойник сессии читает SQL: `||`
против CAST, WHERE-гейт из текста): пульс {a:5} + mark_failed {b:1} → {a,b};
пульс/финализатор по финализированной строке — no-op; mark_cancelled
отказывает источнику, который отмену не опрашивает. На main эти тесты
красные для app-пути.

Комментарии в app/services/scheduler.py и kit/pipeline.py, утверждавшие про
живого «перезаписывающего двойника», приведены в соответствие.
Author
Collaborator

Deep-ревью (06.09): ⚠️ minor, блокеров нет. Проверено исполнением: все четыре формы импорта дают kit-модуль, атрибут пакета — на подменённый (_load() перечитывает sys.modules), в kit runs.py ни одного app.* импорта; AST-сверка — из app-копии не потеряно ничего, тела расходятся только у 4 писателей (||) и _alert_if_consecutive_zero_results (гард sentry_sdk is None); create_run counters не пишет → чужих ключей новый прогон не получает; DKP-резюм читает только last_id/interrupted; done_buckets в backend/app не пишет никто; kit-версия mark_cancelled без гейта была мёртвым кодом (вызывающих 0); три мутации краснят (мерж → замена: [app] и [kit]; гейт отмены; ранний return в алерте → 24 errors в test_scrape_run_alert — патчи на app.services.scrape_runs.sentry_sdk доезжают до kit); дублей в runs.py нет. Полный сьют 5586 passed.

Безвредные дельты, которых нет в описании: admin.py:2018 mark_failed(..., {}) теперь сохраняет частичные counters; финальная строка DKP сохраняет resume_from/reason/candidate; различие «mark_banned сохраняет чекпоинт, mark_failed теряет» исчезло. Потолок: мерж || проверяется SQL-двойниками по тексту statement'а, живого Postgres в тестах нет (как и раньше).

Докатываю до мержа три текстовых находки: scheduler.py:132-137 («counters ЗАМЕНЯЕТ … scrape_runs.py:738» — теперь ложь), test_3384:20/203-206 (та же мотивировка), test_3168:168-176 (текстовый гейт с докстрингом «замена vs мерж» — теперь один объект).

Deep-ревью (06.09): ⚠️ minor, блокеров нет. Проверено исполнением: все четыре формы импорта дают kit-модуль, атрибут пакета — на подменённый (`_load()` перечитывает `sys.modules`), в kit `runs.py` ни одного `app.*` импорта; AST-сверка — из app-копии не потеряно ничего, тела расходятся только у 4 писателей (`||`) и `_alert_if_consecutive_zero_results` (гард `sentry_sdk is None`); `create_run` counters не пишет → чужих ключей новый прогон не получает; DKP-резюм читает только `last_id`/`interrupted`; `done_buckets` в backend/app не пишет никто; kit-версия `mark_cancelled` без гейта была мёртвым кодом (вызывающих 0); три мутации краснят (мерж → замена: `[app]` и `[kit]`; гейт отмены; ранний return в алерте → 24 errors в `test_scrape_run_alert` — патчи на `app.services.scrape_runs.sentry_sdk` доезжают до kit); дублей в `runs.py` нет. Полный сьют 5586 passed. Безвредные дельты, которых нет в описании: `admin.py:2018 mark_failed(..., {})` теперь сохраняет частичные counters; финальная строка DKP сохраняет `resume_from/reason/candidate`; различие «mark_banned сохраняет чекпоинт, mark_failed теряет» исчезло. Потолок: мерж `||` проверяется SQL-двойниками по тексту statement'а, живого Postgres в тестах нет (как и раньше). Докатываю до мержа три текстовых находки: `scheduler.py:132-137` («counters ЗАМЕНЯЕТ … scrape_runs.py:738» — теперь ложь), `test_3384:20/203-206` (та же мотивировка), `test_3168:168-176` (текстовый гейт с докстрингом «замена vs мерж» — теперь один объект).
Light1YT added 1 commit 2026-09-06 07:35:07 +00:00
docs(#3390): убрать ложные «counters ЗАМЕНЯЕТ» из комментариев/докстрингов; гейт test_3168 — честная формулировка
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
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 4m51s
ae6ad9e440
bot-backend merged commit 06ad504505 into main 2026-09-06 08:11:53 +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#3400
No description provided.