feat(tradein): чекпоинты #3074 — наследование при claim + yandex_city_sweep #3098
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3098
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/3074-checkpoint-survives-claim"
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?
Два шарда #3074 (по коммиту на каждый).
1. Чекпоинт переживает обрыв до первой новой корзины
Прод-факт: прогон 4707 (23.08) подхватил 42 корзины у 4117, был убит деплоем на 26-й минуте до завершения первой НОВОЙ корзины — и не успел ни разу написать heartbeat с
done_buckets. Его чекпоинт пуст → следующий кандидат увидел быno_checkpoint, цепочка оборвалась бы. Фикс:_resume_decisionприokкладётdone_bucketsпредшественника в counters-заготовку нового прогона (персистится при claim; heartbeat мержит jsonb — первый bucket-heartbeat перезапишет надмножеством). Отказные вердикты чекпоинт не наследуют.2. Чекпоинты для yandex_city_sweep
Из таблицы убитых деплоем: 15.08 — 65 мин, 12.08 — 2ч33м, оба потеряны целиком (у свипа чекпоинтов не было вовсе). Единица — combo (сегмент × комнатность × диапазон цены), ровно то, чем цикл уже итерируется.
fetch_around_multi_room:skip_combos(ни одного HTTP по собранным) +on_comboдля каждого ПРОЙДЕННОГО combo, включая пустые — иначе пустой combo не попадал бы в чекпоинт и перечитывался бы вечно. Оборванный отказом combo on_combo не вызывает — вызов означает «combo пройден до конца». Других потребителей on_combo у яндекса нет (проверено grep'ом; cian'овский fetch_around_multi_room — другой класс).run_yandex_city_sweep(resume_run_id=...):done_combos→ heartbeat сdone_buckets(мерж jsonb — финализаторы mark_done/failed/banned не затирают, проверено по SQL всех трёх). Подхват гейтится единственным якорем: combo_label не содержит якоря, multi-anchor подхват пропускал бы чужие якоря; на проде все свипы одноякорные._job_yandex_city_sweep:resume_run_id=_pick_resume(...)— весь generic-механизм #2845 (params-идентичность, свежесть точки, потолок цепочки_MAX_RESUME_CHAIN, машиночитаемыйresume_reason) достаётся бесплатно.Проверки
test_3074_checkpoint_survives_claim.py(2): вердикт ok несёт чекпоинт (красный на main по ЗНАЧЕНИЮ), отказный — нетtest_3074_yandex_sweep_checkpoint.py(4): пустой пройденный combo доходит до on_combo (красный на main по ЗНАЧЕНИЮ —[]), skip_combos не порождает HTTP, планировщик отдаёт resume_run_id (красный по значению — None≠4117), пайплайн-проводка skip→scraper + done_buckets→heartbeatПриёмка на проде: следующий оборванный yandex_city_sweep → у преемника в counters
resume_reason='ok',done_bucketsрастёт между прогонами; сравнить длительность возобновлённого прогона с полным.Остаток #3074 (не здесь): domclick_city_sweep / avito_city_sweep по образцу.
🤖 Generated with Claude Code
fix(tradein/scheduler): чекпоинт предшественника переживает обрыв до первой новой корзины (#3074)to feat(tradein): чекпоинты #3074 — наследование при claim + yandex_city_sweepРевью: код корректен, мержу. Два замечания, оба не блокирующие
Проверял по диффу от настоящей базы слияния (
729e9acc), а не от текущего main — иначе в дифф попадает чужойtest_3082.Что проверил предметно
done_bucketsв_resume_decisionв области видимости — берётся изprev_countersпредшественника (стр. 23 функции), а веткаokдостижима только когдаdone_n > 0(ниже есть гардelif done_n == 0 → no_checkpoint), так чтоif done_buckets else []— защита от невозможного, не дыра._pick_resumeвызывает_kit_runs.update_heartbeat(db, run_id, verdict), аdone_bucketsтеперь часть того же вердикта, что иresume_from/resume_chain. Едет по уже существующему пути.done_bucketsи делаетreturn, и я боялся, что непустая фиксирует только счётчики. Нет — тамupdate_heartbeat(db, run_id, {**counters.to_dict(), "done_buckets": sorted(done_combos)}). Иначе в чекпоинт копились бы только пустые combo.new_lotsне гоняетsave_listings— контрактon_comboрасширен (теперь вызывается и на пустых), и вызывающий это переживает.combo_labelякоря не содержит, и пропуск был бы пропуском ЧУЖИХ якорей. Гард поlen(_anchors) == 1правильный.CAST(:rid AS bigint), а не:rid::bigint— по правилу psycopg v3.1. Тело PR противоречит диффу
В разделе «Что остаётся в #3074 (не здесь)» написано: «
yandex_city_sweep— чекпоинтов нет вовсе… отдельный заход». А дифф ровно это и делает:providers/yandex/serp.py(+22) иtest_3074_yandex_sweep_checkpoint.py(+241).Похоже, описание писалось до того, как scope вырос. Важно не ради аккуратности: ревьюер, доверившийся описанию, смержил бы, не заметив, что изменился обход двух-с-половиной-часового прод-скрапера. Стоит поправить тело задним числом или отметить в #3074, что яндекс уже закрыт этим PR.
2. Инвариант «оборванный combo не попадает в чекпоинт» не покрыт тестом
Ключевая строка —
if on_combo is not None and not combo_skipped. Именно её нарушение даёт тихую потерю данных: combo, оборванный gate/extraction-отказом, был бы помечен пройденным, следующий прогон его пропустил бы, и объявления оттуда не собрались бы никогда — причём молча, потому что прогон закончится штатно.Тесты покрывают соседние случаи (
on_comboна пустом пройденном combo, нулевые HTTP при skip, передача чекпоинта планировщиком, resume+checkpoint пайплайна) и вердикты ok/refusal, но случаяcombo_skipped=Trueсреди них нет — инвариант держится только комментарием.Не блокирую: код виден глазами и очевидно верен, а текущее состояние (убитый прогон теряет ВСЁ) строго хуже. Но это ровно тот инвариант, чей будущий регресс будет неотличим от нормальной работы — просится тест в следующий шард.
Контекст
Прогон 4707 из «прод-факта» — тот самый, что я мерил вчера в #3074: убит деплоем на 26-й минуте, 0 собрано. Приятно видеть его закрытым с той стороны, с которой он и должен был чиниться.