feat(tradein/domclick): чекпоинты для city_sweep — корзина как единица возобновления (#3118) #3140

Merged
lekss361 merged 1 commit from feat/3118-domclick-checkpoint into main 2026-08-27 12:00:31 +00:00
Collaborator

Реализация вывода из форензика #3118 (и исправление моего же раннего «домклику чекпоинтить нечего» из #3074 — тот замер по средним 3 минутам был опровергнут данными #3118).

Почему теперь да

QRATOR рубит свип внутри 1-2-й корзины при ЛЮБОМ старте (buckets_completed ≤ 1 из 6 во всех прогонах), при этом banned-прогоны собирают 59–1815 лотов. Сдвиг #2854 лишь распределяет потери по корзинам; чекпоинт превращает случайную ротацию в систематический обход — шесть прогонов закрывают шесть корзин вместо случайных повторов. Это прямая цитата вывода владельца в #3118.

Как (зеркально yandex-чекпоинту #3074/PR #3098)

  • провайдер fetch_city(skip_buckets=...): по собранным корзинам ни одного запроса; completed_buckets — ИМЕНА завершённых (счётчик остаётся); buckets_total сжимается до объёма ЭТОГО прогона — иначе honest-status (#2625/#2700) читал бы возобновлённый прогон как вечно-частичный; гард «цепочка накопила все 6» → честный no-op без падения на пустом обходе;
  • пайплайн: стандартный resume-блок + done_buckets = унаследованное ∪ завершённое, записывается heartbeat'ом сразу после фазы (мерж jsonb — финализаторы done/banned/failed ключ не затирают);
  • планировщик: resume_run_id=_pick_resume(...) — весь generic-механизм #2845 (params-идентичность, свежесть, потолок цепочки, machine-readable отказы) бесплатно;
  • ротация #2854 сохранена — сдвиг упорядочивает оставшиеся после скипа корзины.

Проверки

  • test_3118_domclick_checkpoint.py (4): скип не порождает запросов + total сжат; блок на середине сохраняет имена завершённых; все-6-в-чекпоинте → no-op; планировщик отдаёт resume_run_id (красный на main по значению: None ≠ 6001)
  • соседи: 181 passed (domclick/domklik-сьюты, test_930, test_3074×2, scheduler_parity)

Приёмка на проде: у следующего оборванного domclick_city_sweep преемник показывает resume_reason='ok' и растущий done_buckets; за ≤6 прогонов покрываются все 6 корзин (сейчас — никогда). Обрывы у домклика ежедневные, приёмка снимется за 1-2 суток.

🤖 Generated with Claude Code

Реализация вывода из форензика #3118 (и исправление моего же раннего «домклику чекпоинтить нечего» из #3074 — тот замер по средним 3 минутам был опровергнут данными #3118). ## Почему теперь да QRATOR рубит свип внутри 1-2-й корзины при ЛЮБОМ старте (`buckets_completed ≤ 1` из 6 во всех прогонах), при этом banned-прогоны собирают 59–1815 лотов. Сдвиг #2854 лишь распределяет потери по корзинам; чекпоинт превращает случайную ротацию в **систематический обход** — шесть прогонов закрывают шесть корзин вместо случайных повторов. Это прямая цитата вывода владельца в #3118. ## Как (зеркально yandex-чекпоинту #3074/PR #3098) - **провайдер** `fetch_city(skip_buckets=...)`: по собранным корзинам ни одного запроса; `completed_buckets` — ИМЕНА завершённых (счётчик остаётся); `buckets_total` сжимается до объёма ЭТОГО прогона — иначе honest-status (#2625/#2700) читал бы возобновлённый прогон как вечно-частичный; гард «цепочка накопила все 6» → честный no-op без падения на пустом обходе; - **пайплайн**: стандартный resume-блок + `done_buckets = унаследованное ∪ завершённое`, записывается heartbeat'ом сразу после фазы (мерж jsonb — финализаторы done/banned/failed ключ не затирают); - **планировщик**: `resume_run_id=_pick_resume(...)` — весь generic-механизм #2845 (params-идентичность, свежесть, потолок цепочки, machine-readable отказы) бесплатно; - ротация #2854 сохранена — сдвиг упорядочивает оставшиеся после скипа корзины. ## Проверки - `test_3118_domclick_checkpoint.py` (4): скип не порождает запросов + total сжат; блок на середине сохраняет имена завершённых; все-6-в-чекпоинте → no-op; планировщик отдаёт resume_run_id (**красный на main по значению**: None ≠ 6001) - соседи: 181 passed (domclick/domklik-сьюты, test_930, test_3074×2, scheduler_parity) Приёмка на проде: у следующего оборванного `domclick_city_sweep` преемник показывает `resume_reason='ok'` и растущий `done_buckets`; за ≤6 прогонов покрываются все 6 корзин (сейчас — никогда). Обрывы у домклика ежедневные, приёмка снимется за 1-2 суток. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-08-27 11:53:20 +00:00
feat(tradein/domclick): чекпоинты для city_sweep — корзина как единица возобновления (#3118, #3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-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 4m43s
ede5653aba
Прод-факты #3118: QRATOR рубит свип внутри 1-2-й корзины при любом старте
(buckets_completed ≤ 1 из 6), banned-прогоны собирают 59–1815 лотов. Сдвиг
#2854 лишь распределяет потери; чекпоинт превращает случайную ротацию в
систематический обход — шесть прогонов закрывают шесть корзин.

Моё раннее «домклику чекпоинтить нечего» (замер в #3074 по средним 3 мин)
опровергнуто данными #3118 — этот коммит исправляет тот вывод кодом.

Зеркально yandex-чекпоинту (#3074): провайдер — skip_buckets + имена
завершённых корзин + гард «цепочка накопила все 6» (честный no-op);
buckets_total сжимается до объёма ЭТОГО прогона; пайплайн — done_buckets =
унаследованное ∪ завершённое, heartbeat с мержем jsonb до финализации;
планировщик — generic _pick_resume (#2845).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lekss361 merged commit 7f62bc71e7 into main 2026-08-27 12:00:31 +00:00
lekss361 deleted branch feat/3118-domclick-checkpoint 2026-08-27 12:00:31 +00:00
Owner

Смержил после разбора. Отмечу, что проверял и что осталось под наблюдением.

Что проверял

Правки в общем pipeline.py (−41/+60) — целиком косметика форматтера. Переносы строк вокруг _try_rotate_within_budget и склейка логов; ни одного изменения логики. Это важно, потому что файл общий для avito/cian/yandex, и регресс там задел бы всех.

Механизм не новый. _pick_resume уже обслуживает четыре задания (строки 847, 904, 924, 953) — здесь к нему подключается пятое. Тождество задания сверяется по source + params побайтово, что и требуется.

Цепочка resume завершается. _RESUME_STATUSES = {banned, cancelled, failed} — от прогона со статусом done наследование не происходит вовсе, поэтому «собрали всё → следующий начинает заново» работает по построению.

Остаточный край — стоит посмотреть после первого возобновлённого прогона

No-op ветка (все шесть корзин уже в чекпоинте предшественника) опирается на то, что вызывающий финализирует такой прогон как done. Тест test_all_skipped_is_honest_noop проверяет, что обход не падает, но терминальный статус не фиксирует.

Если такой прогон почему-то финализируется как banned, получится замкнутый круг: следующий прогон снова унаследует полный чекпоинт, снова ничего не соберёт, и domklik встанет насовсем. Косвенный довод против: no-op не делает ни одного запроса, значит сигнала блокировки взяться неоткуда, и honest-status должен дать done.

Проверить это дёшево — после первого прогона, у которого resume_reason не no_checkpoint:

SELECT id, status, counters->>'resume_reason', counters->'done_buckets'
FROM scrape_runs WHERE source = 'domclick_city_sweep'
ORDER BY id DESC LIMIT 5;

Пока это не наблюдалось: за неделю у domclick_city_sweep 0 успешных прогонов из 6, все banned, так что хуже текущего состояния изменение сделать не может.

Смержил после разбора. Отмечу, что проверял и что осталось под наблюдением. ## Что проверял **Правки в общем `pipeline.py` (−41/+60) — целиком косметика форматтера.** Переносы строк вокруг `_try_rotate_within_budget` и склейка логов; ни одного изменения логики. Это важно, потому что файл общий для avito/cian/yandex, и регресс там задел бы всех. **Механизм не новый.** `_pick_resume` уже обслуживает четыре задания (строки 847, 904, 924, 953) — здесь к нему подключается пятое. Тождество задания сверяется по `source` + `params` побайтово, что и требуется. **Цепочка resume завершается.** `_RESUME_STATUSES = {banned, cancelled, failed}` — от прогона со статусом `done` наследование не происходит вовсе, поэтому «собрали всё → следующий начинает заново» работает по построению. ## Остаточный край — стоит посмотреть после первого возобновлённого прогона No-op ветка (все шесть корзин уже в чекпоинте предшественника) опирается на то, что вызывающий финализирует такой прогон как **`done`**. Тест `test_all_skipped_is_honest_noop` проверяет, что обход не падает, но терминальный статус не фиксирует. Если такой прогон почему-то финализируется как `banned`, получится замкнутый круг: следующий прогон снова унаследует полный чекпоинт, снова ничего не соберёт, и domklik встанет насовсем. Косвенный довод против: no-op не делает ни одного запроса, значит сигнала блокировки взяться неоткуда, и honest-status должен дать `done`. Проверить это дёшево — после первого прогона, у которого `resume_reason` не `no_checkpoint`: ```sql SELECT id, status, counters->>'resume_reason', counters->'done_buckets' FROM scrape_runs WHERE source = 'domclick_city_sweep' ORDER BY id DESC LIMIT 5; ``` Пока это не наблюдалось: за неделю у `domclick_city_sweep` 0 успешных прогонов из 6, все `banned`, так что хуже текущего состояния изменение сделать не может.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3140
No description provided.