На большой выдаче свип не отдавал НИЧЕГО. Замер: прогон 7344 (Москва, одиночный,
прокси 13) за два часа прошёл 2 корзины из 6 — 'st' 34 минуты, '1' 86+ минут.
Причина структурная: ДомКлик режет offset на 2000, поэтому диапазон делится
бисекцией по цене и каждый лист пагинируется отдельно. У Москвы ≈23 690 лотов
вторички против ≈6 300 у ЕКБ. Формула watchdog'а считает фетчи как 6 * pages и
объём выдачи не учитывает вовсе, так что снятие по таймауту было гарантировано,
а вместе с ним терялось всё собранное: сохранение было ОДНО, после всех корзин.
Теперь save_listings зовётся из колбэка on_bucket сразу после каждой успешной
корзины, и туда же переехал чекпоинт: done_buckets означает «собрано И
сохранено». Раньше он писался из scraper.completed_buckets уже после except
TimeoutError, то есть помечал пройденными корзины, у которых в БД ноль строк, —
следующий прогон пропускал их через skip_buckets, и за два-три цикла чекпоинт
закрывался целиком. Гард _saved (#2406) закрывал это «всё или ничего»; с
инкрементальным сохранением он не нужен и снят.
Колбэк в serp.py стоит ВНЕ try/except конкретной корзины — иначе generic
обработчик проглотил бы исключение из него. Это же даёт кооперативную отмену по
корзинам, которой у ДомКлика не было вовсе: is_cancelled проверялся только перед
SERP-фазой, и повисший свип нельзя было снять до watchdog'а, все три часа держа
один из двух узлов affinity='any'. Sentinel'ы RuntimeError("cancelled") и
("shutdown") и разбор ветки — тот же приём, что в run_cian_full_load.
watchdog_sec — явный override формулы, читается из default_params в ОБОИХ
хендлерах (kit-native и продуктовом, который перекрывает его ради кук Sber ID).
None сохраняет прежнюю формулу байт-в-байт, как и on_bucket=None.
Отдельно поправлены пять тестов, ломавшихся на переезде: их стабы подменяли
fetch_city и не звали on_bucket, поэтому после правки проверяли мёртвую ветку —
save_listings не вызывался вовсе. Теперь стабы вызывают колбэк, и проверяется
реальный путь.
Прогон: полный бэкенд-набор 6469 passed, 70 skipped. ruff check и format чисто.
Claude-Session: https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs