last_id жил только в памяти процесса (import_rosreestr_dkp, scheduler.py):
heartbeat писал его в scrape_runs.counters каждый батч (комментарий рядом
прямо называл это чекпоинтом), но при старте last_id всегда инициализировался
литералом 0 — обрыв (деплой/OOM/рестарт хоста) откатывал прогресс и заставлял
пере-сканировать источник с начала.
Разведка: из пяти backfill-циклов issue (avito_detail_backfill,
house_imv_backfill, cian_history_backfill, yandex_detail_backfill,
geocode_missing_listings) ни один не имеет этого дефекта — все устроены как
WHERE ... IS NULL/NOT EXISTS ... LIMIT, естественно резюмируемы без курсора.
Единственный код, буквально описанный в issue (строки/SQL/комментарий),
это шестой, не входящий в таблицу backfill — rosreestr_dkp_import.
Фикс — _resume_dkp_cursor(db, run_id):
- кандидат — последний прогон source='rosreestr_dkp_import';
- резюмится только незавершённый штатно прогон: status running/zombie,
либо done с counters.interrupted=1 (SIGTERM-drain — эта ветка раньше
считала последующий full rescan штатным поведением, теперь помечает
себя как прерванную и резюмится наравне с zombie);
- потолок возраста чекпоинта — 24ч, старше — 'checkpoint_stale', старт с 0;
- чистый 'done' (полный проход) не резюмится — иначе ON CONFLICT DO UPDATE
перестанет ловить правки уже импортированных сделок при следующем проходе.
Вердикт и per-batch чекпоинт пишутся через kit_runs.update_heartbeat (merge
`counters || :counters`) вместо локального runs_mod.update_heartbeat (полная
замена) — иначе resume-вердикт стирался первым же heartbeat'ом батча.
Тесты: tests/test_3168_backfill_cursor_resume.py — резюм с сохранённого
last_id, резюм после SIGTERM-drain, отказ резюмить чистый done, отказ
резюмить протухший (>24ч) чекпоинт, merge не стирает посторонние ключи.
Обратимость проверена вручную (временный откат _resume_dkp_cursor красил
6 из 8 тестов).