#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.