feat(tradein/scraper): чекпоинт по якорям для cian_city_sweep (#3074) #3117

Merged
bot-backend merged 1 commit from feat/3074-cian-anchor-checkpoint into main 2026-08-26 14:28:16 +00:00
Owner

Третий шард после yandex (#3098) и avito (#3112).

Почему именно этот источник

Замер за 60 дней:

свип прогонов убито деплоем ср. мин макс
cian_city_sweep 65 2 35 72

Пятиминутного дренажа из #3029 на прогоны такой длительности не хватает: убитый на 35-й минуте сбор начинался заново с первого якоря. Свип живой и результативный — последний прогон собрал 1680 лотов, — поэтому терять его целиком особенно обидно.

Отличие от avito-шарда, и почему здесь проще

У avito успех и неудача якоря сходились в одной строке counters.anchors_done = idx, и понадобился отдельный флаг _anchor_ok.

У циана граница уже проведена самим потоком управления: все ветки отказа (TimeoutError, generic Exception, баны) делают return или continue и до записи чекпоинта не доходят. Точка успеха — единственная, после всех except. Добавлять флаг значило бы дублировать то, что уже выражено структурой, поэтому чекпоинт пишется ровно там.

Тест на этот инвариант всё равно есть — отдельный. Рефакторинг, сливающий ветки в одну, сломал бы границу незаметно: упавший якорь попал бы в чекпоинт, следующий прогон пропустил бы его навсегда, и прогон при этом завершался бы штатно. Часть города просто перестала бы собираться.

Решения

Ключ — имя якоря, а не индекс. Состав списка зависит от city_slug: областные свипы (cian_city_sweep_nizhniy_tagil, _serov, _pervouralsk и ещё четыре) идут по своим наборам, и позиция между городами не устойчива.

Гарда по числу якорей не нужна — в отличие от combo-чекпоинта яндекса, где combo_label якоря не содержал и пропуск был бы пропуском чужих якорей. Здесь якорь и есть ключ.

Пропущенный якорь двигает anchors_done — чтобы счётчик продолжал означать «докуда дошли по списку», а не «сколько собрал именно этот прогон».

Тесты

tradein-mvp/backend/tests/test_3074_cian_anchor_checkpoint.py, 4 поведенческих — с подменой CianScraper и save_listings:

  1. якорь из чекпоинта не опрашивается вовсе — ни одного обращения к источнику;
  2. пройденный дописывается поверх унаследованных, а не затирает их;
  3. без чекпоинта обходятся все якоря — прежнее поведение цело;
  4. упавший якорь в чекпоинт не попадает, исправный — попадает.

Двойнику пришлось добавить счётчики state_extraction_attempts/failures: конвейер читает их у скрапера после каждого якоря (#2625), и без них падал бы сам двойник, а не проверяемая логика — то есть тест «краснел» бы по причине, не имеющей отношения к делу.

Фальсификация: на исходном коде краснеют все 4 (TypeError: unexpected keyword argument 'resume_run_id' — как и в avito-шарде, это доказывает отсутствие параметра; поведенческую часть держат сами утверждения). Весь набор #307414 passed. ruff чист.

Что остаётся в #3074

domclick_city_sweep. Утром я вынес его из плана по замеру отмен деплоем (54 прогона, ноль отмен, среднее 3 минуты) — и это решение стоит пересмотреть: по свежим данным сбор рвётся блоком QRATOR на середине, собрав 1815 объявлений 25.08 и 235 сегодня, а статус при этом banned. Чекпоинт там окупился бы, если блок временный — это ещё не проверено.

Refs #3074, #3098, #3112

Третий шард после yandex (#3098) и avito (#3112). ## Почему именно этот источник Замер за 60 дней: | свип | прогонов | убито деплоем | ср. мин | макс | |---|---|---|---|---| | **cian_city_sweep** | 65 | 2 | **35** | 72 | Пятиминутного дренажа из #3029 на прогоны такой длительности не хватает: убитый на 35-й минуте сбор начинался заново с первого якоря. Свип живой и результативный — последний прогон собрал 1680 лотов, — поэтому терять его целиком особенно обидно. ## Отличие от avito-шарда, и почему здесь проще У avito успех и неудача якоря сходились в одной строке `counters.anchors_done = idx`, и понадобился отдельный флаг `_anchor_ok`. У циана **граница уже проведена самим потоком управления**: все ветки отказа (`TimeoutError`, generic `Exception`, баны) делают `return` или `continue` и до записи чекпоинта не доходят. Точка успеха — единственная, после всех `except`. Добавлять флаг значило бы дублировать то, что уже выражено структурой, поэтому чекпоинт пишется ровно там. Тест на этот инвариант всё равно есть — отдельный. Рефакторинг, сливающий ветки в одну, сломал бы границу **незаметно**: упавший якорь попал бы в чекпоинт, следующий прогон пропустил бы его навсегда, и прогон при этом завершался бы штатно. Часть города просто перестала бы собираться. ## Решения **Ключ — имя якоря, а не индекс.** Состав списка зависит от `city_slug`: областные свипы (`cian_city_sweep_nizhniy_tagil`, `_serov`, `_pervouralsk` и ещё четыре) идут по своим наборам, и позиция между городами не устойчива. **Гарда по числу якорей не нужна** — в отличие от combo-чекпоинта яндекса, где `combo_label` якоря не содержал и пропуск был бы пропуском чужих якорей. Здесь якорь и есть ключ. **Пропущенный якорь двигает `anchors_done`** — чтобы счётчик продолжал означать «докуда дошли по списку», а не «сколько собрал именно этот прогон». ## Тесты `tradein-mvp/backend/tests/test_3074_cian_anchor_checkpoint.py`, 4 поведенческих — с подменой `CianScraper` и `save_listings`: 1. якорь из чекпоинта не опрашивается **вовсе** — ни одного обращения к источнику; 2. пройденный дописывается поверх унаследованных, а не затирает их; 3. без чекпоинта обходятся все якоря — прежнее поведение цело; 4. упавший якорь в чекпоинт **не попадает**, исправный — попадает. Двойнику пришлось добавить счётчики `state_extraction_attempts/failures`: конвейер читает их у скрапера после каждого якоря (#2625), и без них падал бы сам двойник, а не проверяемая логика — то есть тест «краснел» бы по причине, не имеющей отношения к делу. **Фальсификация:** на исходном коде краснеют все 4 (`TypeError: unexpected keyword argument 'resume_run_id'` — как и в avito-шарде, это доказывает отсутствие параметра; поведенческую часть держат сами утверждения). Весь набор #3074 — **14 passed**. `ruff` чист. ## Что остаётся в #3074 `domclick_city_sweep`. Утром я вынес его из плана по замеру отмен деплоем (54 прогона, ноль отмен, среднее 3 минуты) — и это решение стоит пересмотреть: по свежим данным сбор рвётся блоком QRATOR **на середине**, собрав 1815 объявлений 25.08 и 235 сегодня, а статус при этом `banned`. Чекпоинт там окупился бы, если блок временный — это ещё не проверено. Refs #3074, #3098, #3112
lekss361 added 1 commit 2026-08-26 14:22:32 +00:00
feat(tradein/scraper): чекпоинт по якорям для cian_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m43s
68daac2a30
Третий шард после yandex (#3098) и avito (#3112). Выбран по замеру за 60 дней:
65 прогонов, среднее 35 минут, максимум 72, две отмены деплоем. Пятиминутного
дренажа (#3029) на такие прогоны не хватает - убитый на 35-й минуте сбор
начинался заново с первого якоря.

Ключ чекпоинта - ИМЯ якоря, а не индекс: состав списка зависит от city_slug
(областные свипы идут по своим наборам), позиция между городами не устойчива.
По той же причине гарда по числу якорей не нужна - в отличие от combo-чекпоинта
яндекса, где ключ якоря не содержал.

Отличие от avito-шарда: там успех и неудача якоря сходились в одной строке и
потребовался отдельный флаг _anchor_ok. У циана граница уже проведена самим
потоком управления - все ветки отказа делают return или continue и до записи
чекпоинта не доходят. Добавлять флаг значило бы дублировать то, что уже
выражено структурой; достаточно писать чекпоинт в единственной точке успеха.
Тест сторожит эту границу отдельно, потому что рефакторинг, сливающий ветки,
сломал бы её незаметно.

Пропущенный якорь двигает anchors_done - чтобы счётчик продолжал означать
"докуда дошли по списку", а не "сколько собрал именно этот прогон".

Тесты (4) поведенческие, с подменой CianScraper и save_listings: якорь из
чекпоинта не опрашивается вовсе; пройденный дописывается поверх унаследованных;
без чекпоинта обходятся все; упавший в чекпоинт не попадает. Двойнику пришлось
добавить счётчики state_extraction_* - конвейер читает их после каждого якоря
(#2625), и без них падал бы сам двойник, а не проверяемая логика.

Фальсификация: на исходном коде краснеют все 4. Весь набор #3074 (yandex, avito,
cian, claim) - 14 passed.
bot-backend merged commit 54438039e5 into main 2026-08-26 14:28:16 +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#3117
No description provided.