gendesign/tradein-mvp/backend/app/services/scrape_runs.py
bot-backend 36f2429fbe
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
refactor(tradein/runs): одна реализация scrape_runs — kit, семантика counters мерж (#3390)
Две живые копии одного модуля с противоположной семантикой 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, утверждавшие про
живого «перезаписывающего двойника», приведены в соответствие.
2026-09-06 11:45:56 +05:00

36 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""`app.services.scrape_runs` — АЛИАС `scraper_kit.orchestration.runs` (#3390).
Реализация одна, и она в kit. Здесь нет ни функций, ни констант: модуль подменяет
себя kit-модулем в `sys.modules`, поэтому `from app.services import scrape_runs as
runs_mod` и `from scraper_kit.orchestration import runs` дают ОДИН И ТОТ ЖЕ объект.
Зачем так, а не «тонкий модуль с реэкспортом имён»: реэкспорт разводит патч-цели.
`patch("app.services.scrape_runs.sentry_sdk")` или `patch.object(runs_mod, "mark_done")`
правит глобаль ЭТОГО модуля, а тело реэкспортированной функции читает глобаль СВОЕГО —
kit'а. Тест остался бы зелёным, не подменив ничего (или покраснел бы на пустом месте),
а прод-путь оказался бы вне проверки: ровно тот класс дефекта, из-за которого заведена
#3390. С алиасом патч по любому из двух имён попадает в единственную реализацию.
Что здесь было до #3390: полная вторая копия модуля (1159 строк), которая разошлась с
kit'ом минимум четырежды — counters ЗАМЕНЯЛА (`counters = CAST(:counters AS jsonb)`)
против мержа (`COALESCE(counters,'{}') || …`) у kit, свой гейт по статусу, `honors_cancel`
только здесь, `mark_skipped` только там. Ревью дважды за сутки делало из этого ложные
выводы (#3388, #3355), потому что «вызывающий пишет через runs» не отвечало на вопрос,
какая из двух семантик применится. Победила семантика kit'а — мерж: у одной строки
прогона несколько писателей (пульс, финализатор, дрейн), каждый знает лишь свои ключи,
и замена теряла чужие (чекпоинт `done_buckets` #930, метка `interrupted` #3391, замер из
пульса #3384). Зависимости «финализатор обязан УДАЛИТЬ ключ заменой» нет ни у одного
вызывающего: строка прогона создаётся пустой (`create_run`), а резюм читает counters
ПРЕДЫДУЩЕГО прогона по его id.
НЕ добавляй сюда код: всё, что написано ниже подмены, недостижимо — импортирующий
получает kit-модуль. Новые функции — в `scraper_kit/orchestration/runs.py`.
"""
from __future__ import annotations
import sys
from scraper_kit.orchestration import runs as _kit_runs
sys.modules[__name__] = _kit_runs