fix(tradein/ingest): rosreestr_dkp_import курсор переживает рестарт (#3168) #3176

Merged
bot-backend merged 1 commit from feat/3168-backfill-cursor-resume into main 2026-08-28 07:43:17 +00:00
Collaborator

Закрывает #3168 после поправки его посылки (см. комментарий в тикете): дефект не у пяти перечисленных backfill'ов, а у import_rosreestr_dkp.

Что было

backend/app/services/scheduler.py, import_rosreestr_dkp (с 162): курсор WHERE id > CAST(:last_id AS bigint) ORDER BY id идёт от last_id, который живёт только в памяти процесса. Heartbeat пишется каждый батч, но позицию не сохраняет — комментарий на :198 при этом называет heartbeat чекпоинтом.

Обрыв — деплой, OOM, рестарт — и следующий запуск начинает импорт с нуля.

Что стало

  • Курсор лёг в scrape_runs.counters.last_id, пишется мержем через kit_runs.update_heartbeat (counters || :counters). Локальный runs_mod.update_heartbeat делает полную замену — на нём вердикт резюме стирался бы первым же батчем.
  • Решение о подхвате — локальный ладдер _resume_dkp_cursor. Общий _pick_resume не переиспользован намеренно: у него другой контракт (bucket-множество, params, interval_days), а здесь курсор числовой.
  • Резюмируются running, zombie и done с меткой counters.interrupted = 1. Ветка SIGTERM-drain теперь помечает себя прерванной, а не мнимо-штатной. Чистый done не резюмируется — иначе ON CONFLICT DO UPDATE перестанет подхватывать правки старых сделок.
  • Потолок возраста курсора — 24 часа. Тот же порядок, что у суточной сетки _resume_decision, с запасом перекрывает наибольший наблюдаемый разрыв heartbeat у соседних циклов (150 минут).

Проверка

8 passed — resume, SIGTERM-drain, чистый done, отсутствие предыдущего прогона, протухший курсор, граница потолка, мерж ×2.

Обратимость подтверждена: с временно откаченным _resume_dkp_cursor6 failed, 2 passed; после восстановления снова 8 passed. ruff check и format чисто, CRLF сохранён (589/589), паттерна ::type нет.

Ветка перебазирована на актуальный main — без этого PR откатил бы #3170 и #3118, смерженные этой ночью.

Refs #3168, #2989

Закрывает #3168 после поправки его посылки (см. комментарий в тикете): дефект не у пяти перечисленных backfill'ов, а у `import_rosreestr_dkp`. ## Что было `backend/app/services/scheduler.py`, `import_rosreestr_dkp` (с 162): курсор `WHERE id > CAST(:last_id AS bigint) ORDER BY id` идёт от `last_id`, который живёт только в памяти процесса. Heartbeat пишется каждый батч, но позицию не сохраняет — комментарий на `:198` при этом называет heartbeat чекпоинтом. Обрыв — деплой, OOM, рестарт — и следующий запуск начинает импорт с нуля. ## Что стало - Курсор лёг в `scrape_runs.counters.last_id`, пишется **мержем** через `kit_runs.update_heartbeat` (`counters || :counters`). Локальный `runs_mod.update_heartbeat` делает полную замену — на нём вердикт резюме стирался бы первым же батчем. - Решение о подхвате — локальный ладдер `_resume_dkp_cursor`. Общий `_pick_resume` не переиспользован намеренно: у него другой контракт (bucket-множество, `params`, `interval_days`), а здесь курсор числовой. - Резюмируются `running`, `zombie` и `done` с меткой `counters.interrupted = 1`. Ветка SIGTERM-drain теперь помечает себя прерванной, а не мнимо-штатной. **Чистый `done` не резюмируется** — иначе `ON CONFLICT DO UPDATE` перестанет подхватывать правки старых сделок. - Потолок возраста курсора — 24 часа. Тот же порядок, что у суточной сетки `_resume_decision`, с запасом перекрывает наибольший наблюдаемый разрыв heartbeat у соседних циклов (150 минут). ## Проверка `8 passed` — resume, SIGTERM-drain, чистый `done`, отсутствие предыдущего прогона, протухший курсор, граница потолка, мерж ×2. Обратимость подтверждена: с временно откаченным `_resume_dkp_cursor` — `6 failed, 2 passed`; после восстановления снова `8 passed`. ruff check и format чисто, CRLF сохранён (589/589), паттерна `::type` нет. Ветка перебазирована на актуальный `main` — без этого PR откатил бы #3170 и #3118, смерженные этой ночью. Refs #3168, #2989
bot-backend added 1 commit 2026-08-27 22:09:39 +00:00
fix(tradein/ingest): rosreestr_dkp_import курсор переживает рестарт (#3168)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m45s
51027d8b02
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 тестов).
bot-backend merged commit a326bc9b69 into main 2026-08-28 07:43:17 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#3176
No description provided.