Свип ДомКлика сохраняет лоты по корзинам, а не одним махом в конце #3592

Merged
lekss361 merged 1 commit from fix/domclick-incremental-save into main 2026-09-17 13:37:49 +00:00
Owner

Снимает причину, по которой миграцией 308 (#3567) пришлось выключить домклик-свипы Москвы и области.

Замер

Прогон 7344 (domclick_city_sweep_moskva, одиночный, прокси 13, старт 06:12:48): за два часа пройдено 2 корзины из 6st 34.2 минуты, 1 86+ минут. Прогон был единственным, узел не банился, QRATOR-блока в логе нет.

Причина структурная: ДомКлик режет offset на 2000, поэтому диапазон делится бисекцией по цене и каждый лист пагинируется отдельно. У Москвы ≈23 690 лотов вторички против ≈6 300 у ЕКБ. Формула watchdog'а считает фетчи как _DOMCLICK_NUM_BUCKETS * pages и объём выдачи не учитывает: 11100 = 600 * (6 + 12) + 300. Снятие по таймауту было гарантировано.

Что было сломано

  1. Сохранение одно, после всех корзин. _domclick_phase = «fetch_city + save». Снятие фазы (asyncio.wait_forTimeoutError) теряло всё собранное.
  2. Чекпоинт при этом врал. Он писался уже после except TimeoutError, из _s.completed_buckets, то есть помечал пройденными корзины с нулём строк в БД — следующий прогон пропускал их через skip_buckets, и за два-три цикла чекпоинт закрывался целиком. Гард _saved (#2406, c7a03495) закрыл это по принципу «всё или ничего».
  3. Кооперативной отмены по корзинам не было. runs.is_cancelled проверялся только перед SERP-фазой, поэтому повисший свип нельзя было снять до watchdog'а — все три часа он держал один из двух узлов provider_affinity='any'.

Что сделано

save_listings зовётся из колбэка on_bucket сразу после каждой успешной корзины, и туда же переехал чекпоинт: done_buckets теперь означает «собрано И сохранено». Гард _saved снят как ненужный.

Колбэк в serp.py стоит вне try/except конкретной корзины — иначе generic-обработчик проглотил бы исключение из него. Это же даёт отмену по корзинам: sentinel'ы RuntimeError("cancelled") / ("shutdown") и разбор ветки — тот же приём, что в run_cian_full_load._on_bucket.

watchdog_sec — явный override формулы, читается из default_params в обоих хендлерах (kit-native и продуктовом, который перекрывает его ради кук Sber ID #3264).

Обратная совместимость: on_bucket=None и watchdog_sec=None дают прежнее поведение байт-в-байт, поэтому ЕКБ-свип не затронут.

Пять тестов поправлено — и это не косметика

Их стабы подменяли fetch_city и не звали on_bucket. После переезда сохранения внутрь цикла такой стаб проверял мёртвую ветку: save_listings не вызывался вовсе, поэтому test_domclick_unsaved_buckets_are_not_checkpointed[save_failed] получал errors_count == 0 вместо >= 1. Теперь стабы вызывают колбэк, и проверяется реальный путь.

Затронуты: test_2406_sweep_edge_cases.py, test_3369_domclick_cancel_checkpoint.py, test_scraper_kit_pipeline_parity2.py (три кейса через общий _drive_domclick).

Новые тесты

tests/test_domclick_incremental_save.py — 6 штук: колбэк зовётся по разу на каждую успешную корзину и получает лоты именно этой корзины; не зовётся для упавшей корзины и после DomClickBlockedError; исключение из колбэка прерывает обход; on_bucket=None сохраняет прежнее поведение; watchdog_sec override применяется, None даёт прежние 11100 при pages=100, delay=6.

Прогон

Полный бэкенд-набор: 6469 passed, 70 skipped (2 мин 17 с). ruff check и ruff format чисто.

Дальше

Возврат строк 77/50 — отдельной миграцией, после того как этот код доедет до прода. Чекпоинт прогонов 7333/7344 сбрасывать не нужно: у обоих buckets_completed пуст, они были задрейнены деплоем до первой корзины.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs

Снимает причину, по которой миграцией 308 (#3567) пришлось выключить домклик-свипы Москвы и области. ## Замер Прогон **7344** (`domclick_city_sweep_moskva`, одиночный, прокси 13, старт 06:12:48): за два часа пройдено **2 корзины из 6** — `st` 34.2 минуты, `1` 86+ минут. Прогон был единственным, узел не банился, QRATOR-блока в логе нет. Причина структурная: ДомКлик режет `offset` на 2000, поэтому диапазон делится бисекцией по цене и **каждый лист пагинируется отдельно**. У Москвы ≈23 690 лотов вторички против ≈6 300 у ЕКБ. Формула watchdog'а считает фетчи как `_DOMCLICK_NUM_BUCKETS * pages` и объём выдачи не учитывает: `11100 = 600 * (6 + 12) + 300`. Снятие по таймауту было гарантировано. ## Что было сломано 1. **Сохранение одно, после всех корзин.** `_domclick_phase` = «fetch_city + save». Снятие фазы (`asyncio.wait_for` → `TimeoutError`) теряло всё собранное. 2. **Чекпоинт при этом врал.** Он писался уже после `except TimeoutError`, из `_s.completed_buckets`, то есть помечал пройденными корзины с нулём строк в БД — следующий прогон пропускал их через `skip_buckets`, и за два-три цикла чекпоинт закрывался целиком. Гард `_saved` (#2406, `c7a03495`) закрыл это по принципу «всё или ничего». 3. **Кооперативной отмены по корзинам не было.** `runs.is_cancelled` проверялся только перед SERP-фазой, поэтому повисший свип нельзя было снять до watchdog'а — все три часа он держал один из **двух** узлов `provider_affinity='any'`. ## Что сделано `save_listings` зовётся из колбэка `on_bucket` сразу после каждой успешной корзины, и туда же переехал чекпоинт: `done_buckets` теперь означает «собрано **И** сохранено». Гард `_saved` снят как ненужный. Колбэк в `serp.py` стоит **вне** try/except конкретной корзины — иначе generic-обработчик проглотил бы исключение из него. Это же даёт отмену по корзинам: sentinel'ы `RuntimeError("cancelled")` / `("shutdown")` и разбор ветки — тот же приём, что в `run_cian_full_load._on_bucket`. `watchdog_sec` — явный override формулы, читается из `default_params` в **обоих** хендлерах (kit-native и продуктовом, который перекрывает его ради кук Sber ID #3264). Обратная совместимость: `on_bucket=None` и `watchdog_sec=None` дают прежнее поведение байт-в-байт, поэтому ЕКБ-свип не затронут. ## Пять тестов поправлено — и это не косметика Их стабы подменяли `fetch_city` и не звали `on_bucket`. После переезда сохранения внутрь цикла такой стаб проверял **мёртвую ветку**: `save_listings` не вызывался вовсе, поэтому `test_domclick_unsaved_buckets_are_not_checkpointed[save_failed]` получал `errors_count == 0` вместо `>= 1`. Теперь стабы вызывают колбэк, и проверяется реальный путь. Затронуты: `test_2406_sweep_edge_cases.py`, `test_3369_domclick_cancel_checkpoint.py`, `test_scraper_kit_pipeline_parity2.py` (три кейса через общий `_drive_domclick`). ## Новые тесты `tests/test_domclick_incremental_save.py` — 6 штук: колбэк зовётся по разу на каждую успешную корзину и получает лоты **именно этой** корзины; не зовётся для упавшей корзины и после `DomClickBlockedError`; исключение из колбэка прерывает обход; `on_bucket=None` сохраняет прежнее поведение; `watchdog_sec` override применяется, `None` даёт прежние 11100 при `pages=100, delay=6`. ## Прогон Полный бэкенд-набор: **6469 passed, 70 skipped** (2 мин 17 с). `ruff check` и `ruff format` чисто. ## Дальше Возврат строк 77/50 — отдельной миграцией, после того как этот код доедет до прода. Чекпоинт прогонов 7333/7344 сбрасывать не нужно: у обоих `buckets_completed` пуст, они были задрейнены деплоем до первой корзины. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
lekss361 added 1 commit 2026-09-17 13:24:07 +00:00
fix(trade-in): свип ДомКлика сохраняет лоты по корзинам, а не одним махом в конце
All checks were successful
CI Trade-In / changes (pull_request) Successful in 17s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 23s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-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 6m42s
b4e0618025
На большой выдаче свип не отдавал НИЧЕГО. Замер: прогон 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
lekss361 merged commit e5212e52ab into main 2026-09-17 13:37:49 +00:00
lekss361 deleted branch fix/domclick-incremental-save 2026-09-17 13:37:49 +00:00
Sign in to join this conversation.
No reviewers
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#3592
No description provided.