scraper-kit: ещё 4 дрейна с живым чекпоинтом без метки interrupted — domclick sweep и три full-load (cian/yandex/avito) нерезюмируемы после SIGTERM #3355

Closed
opened 2026-09-05 17:58:00 +00:00 by bot-backend · 3 comments
Collaborator

Находка ревью PR #3346 (#3333 закрыл city-свипы yandex/cian/newbuilding). Пред-существующий дефект того же класса у четырёх функций pipeline.py, у которых есть живой чекпоинт done_buckets, но дрейн финализируется чистым done без interrupted=1 — резюм их не берёт, чекпоинт пишется в никуда:

  • pipeline.py:~4372 — domclick city sweep;
  • ~3500 → mark_done ~3697/3736/3746 — cian full-load;
  • ~3840~3940/3977/3988 — yandex full-load;
  • ~4122~4187/4243/4254 — avito full-load (exhaustive; см. #3315 — там резюм критичен для бан-бюджета).

Лечение

Та же идиома, что в #3330/#3346: {**counters.to_dict(), "interrupted": 1} в дрейн-payload (heartbeat + mark_done), done_buckets не добавлять (jsonb-мерж). Для full-load'ов проверить, что резюм читает их ключи чекпоинта (структура done_buckets у них своя).

Приёмка

  • Тест по значению на каждую из 4 функций: drain → interrupted==1; чистый done → не резюмируется;
  • Ассерт на СТАТУС финализации (mark_done vs mark_failed при нулевом дрейне — honest-status гейты runs.py:754-768) — minor из того же ревью;
  • Фальсификация: снятие метки в одной функции красит её тест.

Refs #3333, #3330, #3315.

Находка ревью PR #3346 (#3333 закрыл city-свипы yandex/cian/newbuilding). Пред-существующий дефект того же класса у четырёх функций pipeline.py, у которых есть живой чекпоинт `done_buckets`, но дрейн финализируется чистым `done` без `interrupted=1` — резюм их не берёт, чекпоинт пишется в никуда: - `pipeline.py:~4372` — domclick city sweep; - `~3500` → mark_done `~3697/3736/3746` — cian full-load; - `~3840` → `~3940/3977/3988` — yandex full-load; - `~4122` → `~4187/4243/4254` — avito full-load (exhaustive; см. #3315 — там резюм критичен для бан-бюджета). ## Лечение Та же идиома, что в #3330/#3346: `{**counters.to_dict(), "interrupted": 1}` в дрейн-payload (heartbeat + mark_done), `done_buckets` не добавлять (jsonb-мерж). Для full-load'ов проверить, что резюм читает их ключи чекпоинта (структура done_buckets у них своя). ## Приёмка - [ ] Тест по значению на каждую из 4 функций: drain → `interrupted==1`; чистый done → не резюмируется; - [ ] Ассерт на СТАТУС финализации (mark_done vs mark_failed при нулевом дрейне — honest-status гейты runs.py:754-768) — minor из того же ревью; - [ ] Фальсификация: снятие метки в одной функции красит её тест. Refs #3333, #3330, #3315.
Author
Collaborator

PR #3363 смержен 05.09. Помимо четырёх дрейн-меток по доводке: domclick-дрейн теперь явно тащит done_buckets = sorted(skip_buckets) (app-level scrape_runs.py перезаписывает counters целиком — jsonb-мержа там нет, чекпоинт терялся: фальсификация показала None), cian получил метку и на detail-выходе по shutdown; у yandex/avito full-load второй точки нет.

Критерий приёмки (событийный, годно до 12.09): первый SIGTERM-дрейн (любой деплой МЕРЫ во время прогона) у cian_full_load / avito_full_load* / domclick_city_sweep

SELECT id, source, status, counters->>'interrupted', jsonb_array_length(counters->'done_buckets')
  FROM scrape_runs WHERE source IN ('cian_full_load','avito_full_load','avito_full_load_exhaustive','domclick_city_sweep')
   AND counters ? 'interrupted' ORDER BY id DESC LIMIT 5;

ожидание: interrupted=1, done_buckets непуст (у domclick — унаследованные корзины), следующий claim → resume_reason='ok'.

Открытые хвосты: #3366 (yandex full-load без job'а — метка у него диагностическая), #3369 (cancel-ветка domclick без done_buckets).

PR #3363 смержен 05.09. Помимо четырёх дрейн-меток по доводке: domclick-дрейн теперь явно тащит `done_buckets = sorted(skip_buckets)` (app-level `scrape_runs.py` перезаписывает counters целиком — jsonb-мержа там нет, чекпоинт терялся: фальсификация показала `None`), cian получил метку и на detail-выходе по shutdown; у yandex/avito full-load второй точки нет. **Критерий приёмки (событийный, годно до 12.09):** первый SIGTERM-дрейн (любой деплой МЕРЫ во время прогона) у `cian_full_load` / `avito_full_load*` / `domclick_city_sweep` → ```sql SELECT id, source, status, counters->>'interrupted', jsonb_array_length(counters->'done_buckets') FROM scrape_runs WHERE source IN ('cian_full_load','avito_full_load','avito_full_load_exhaustive','domclick_city_sweep') AND counters ? 'interrupted' ORDER BY id DESC LIMIT 5; ``` ожидание: `interrupted=1`, `done_buckets` непуст (у domclick — унаследованные корзины), следующий claim → `resume_reason='ok'`. Открытые хвосты: #3366 (yandex full-load без job'а — метка у него диагностическая), #3369 (cancel-ветка domclick без `done_buckets`).
Author
Collaborator

Поправка к моему комментарию выше (и к тексту PR #3363 про domclick): утверждение «app-level scrape_runs.py перезаписывает counters целиком — jsonb-мержа нет, чекпоинт терялся» неверно. Проверил по коду: pipeline.py:56 импортирует scraper_kit.orchestration.runs, и все его писатели мержат (runs.py:708/776/817/880: counters = COALESCE(counters,'{}') || CAST(:counters AS jsonb)); перезаписывающий app/services/scrape_runs.py (CAST(:counters AS jsonb)) — другой модуль, domclick-свипом не используемый; _pick_resume наследует done_buckets при claim (scheduler.py:741-748, #3074).

То есть чекпоинт domclick на проде не терялся; явный done_buckets в дрейн-payload (#3363) и во всех выходах (#3372) — защитная унификация на случай смены писателя, не восстановление потери. «Фальсификация показала None» — показала это на фейк-сессии теста, воспроизводящей перезапись, а не прод-писатель: классический «проба идёт не рабочим трактом».

Критерий приёмки #3355 по дрейн-метке interrupted=1 остаётся в силе — это была настоящая дыра (резюм не брал done без метки). Урок записан: два ревью разошлись в факте — проверять самому по импорту, а не выбирать более тревожное.

**Поправка к моему комментарию выше** (и к тексту PR #3363 про domclick): утверждение «app-level `scrape_runs.py` перезаписывает counters целиком — jsonb-мержа нет, чекпоинт терялся» **неверно**. Проверил по коду: `pipeline.py:56` импортирует `scraper_kit.orchestration.runs`, и все его писатели мержат (`runs.py:708/776/817/880`: `counters = COALESCE(counters,'{}') || CAST(:counters AS jsonb)`); перезаписывающий `app/services/scrape_runs.py` (`CAST(:counters AS jsonb)`) — другой модуль, domclick-свипом не используемый; `_pick_resume` наследует `done_buckets` при claim (`scheduler.py:741-748`, #3074). То есть чекпоинт domclick на проде **не терялся**; явный `done_buckets` в дрейн-payload (#3363) и во всех выходах (#3372) — защитная унификация на случай смены писателя, не восстановление потери. «Фальсификация показала None» — показала это на фейк-сессии теста, воспроизводящей перезапись, а не прод-писатель: классический «проба идёт не рабочим трактом». Критерий приёмки #3355 по дрейн-метке `interrupted=1` остаётся в силе — это была настоящая дыра (резюм не брал done без метки). Урок записан: два ревью разошлись в факте — проверять самому по импорту, а не выбирать более тревожное.
Author
Collaborator

Датированная приёмка (первый реальный SIGTERM-drain, деплой 07.09 02:36 UTC) — ОТРИЦАТЕЛЬНЫЙ результат для app-task бэкфиллов: in-flight были cian_detail_backfill 6167 и cian_history_backfill 6173; после «drain exceeded 100s grace — hard-cancelling» обе строки остались running и boot-reap нового контейнера сделал их zombie (boot_reaped=true), interrupted не записан. Для kit-свипов/full-load случай пока не наблюдался (в момент деплоя их не было). Разбор и фикс в одном месте (kit-scheduler при hard-cancel) — #3391.

**Датированная приёмка (первый реальный SIGTERM-drain, деплой 07.09 02:36 UTC) — ОТРИЦАТЕЛЬНЫЙ результат для app-task бэкфиллов:** in-flight были `cian_detail_backfill` 6167 и `cian_history_backfill` 6173; после «drain exceeded 100s grace — hard-cancelling» обе строки остались `running` и boot-reap нового контейнера сделал их `zombie` (`boot_reaped=true`), `interrupted` не записан. Для kit-свипов/full-load случай пока не наблюдался (в момент деплоя их не было). Разбор и фикс в одном месте (kit-scheduler при hard-cancel) — #3391.
Sign in to join this conversation.
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#3355
No description provided.