feat(tradein/scheduler): планировщик подхватывает чекпоинт оборванного прогона (#930) #2845

Merged
bot-backend merged 1 commit from fix/scheduler-resume-checkpoint into main 2026-08-12 18:51:03 +00:00

1 commit

Author SHA1 Message Date
d9b4f124a6 feat(tradein/scheduler): планировщик подхватывает чекпоинт оборванного прогона (#930)
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / 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 / 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 4m24s
CI / backend-tests (pull_request) Has been skipped
#930 сделал обе половины механизма — запись точки (counters.done_buckets) и её
чтение (run_*_full_load(resume_run_id=...)) — но единственным входом оставил
админку. У avito full-load её нет вовсе, а планировщик передавал resume_run_id
литеральным None. То есть боевого пути возобновления не существовало ни дня:
433 корзины в 30 оборванных прогонах за 90 суток (avito 242, cian 134,
exhaustive 57) перебирались заново, включая 35 корзин прогона 3547, убитого
деплоем на третьем часу и лежащего с 09.08.

Одной строки resume_run_id=prev было бы мало и опасно — правка из четырёх частей.

1. Полнота корзины (иначе видимая потеря стала бы невидимой). Ключ
   done_buckets одинаков у целиком и частично собранной корзины, а частичность
   возникает тремя путями: страница молча выпала (page_html=None), исключение
   страницы проглочено gather'ом, признанный tail-loss/hard-cap. SERP-слой
   теперь отдаёт признак полноты третьим аргументом on_bucket, пайплайн пишет
   в чекпоинт ТОЛЬКО полную корзину, частичные считает (partial_buckets).

2. Точка стала монотонной. update_heartbeat/mark_* мержат counters-jsonb
   вместо замены: раньше _on_progress и фоновый heartbeat cian'а (каждые 60 с)
   слали counters без done_buckets и СТИРАЛИ точку, а mark_failed уничтожал её
   насовсем — 15 оборванных прогонов с 45k собранных лотов и без ключа вовсе.

3. Тождество задания. Кандидат — ПОСЛЕДНИЙ прогон того же source (после 'done'
   возобновлять нечего), params сверяются побайтово: incremental_days меняет
   СМЫСЛ ключа, price_cap_per_bucket — само дерево бисекции. Статусы banned/
   cancelled/failed; zombie исключён намеренно (reap не убивает процесс,
   точка может двигаться). Срок годности — такт источника плюс сутки сетки
   запуска. Цепочка возобновлений ограничена STALE_DIGEST_INTERVAL_FACTOR-1.

4. Наблюдаемость. Вердикт (resume_from/resume_reason/resume_buckets/
   resume_chain) пишется в counters нового прогона: no_prev_run, status_*,
   params_changed, no_checkpoint, checkpoint_stale, chain_limit, ok.

Плюс skip_buckets доехал до инкрементальной ветки avito — без этого resume в
боевом режиме avito_full_load (incremental_days=7) был бы чистым no-op.
2026-08-12 21:40:41 +05:00