refactor(tradein): одна реализация scrape_runs — kit orchestration/runs.py, app-модуль = алиас; counters везде мержатся (#3390) #3400
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3400
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "refactor/3390-single-runs-module"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #3390.
Дефект. Две живые копии одного модуля с противоположной семантикой counters: app
scrape_runs.pyЗАМЕНЯЛ (CAST(:counters AS jsonb)), kitorchestration/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по-прежнему отказывает.Две живые копии одного модуля с противоположной семантикой 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, утверждавшие про живого «перезаписывающего двойника», приведены в соответствие.Deep-ревью (06.09): ⚠️ minor, блокеров нет. Проверено исполнением: все четыре формы импорта дают kit-модуль, атрибут пакета — на подменённый (
_load()перечитываетsys.modules), в kitruns.pyни одногоapp.*импорта; AST-сверка — из app-копии не потеряно ничего, тела расходятся только у 4 писателей (||) и_alert_if_consecutive_zero_results(гардsentry_sdk is None);create_runcounters не пишет → чужих ключей новый прогон не получает; 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 мерж» — теперь один объект).