[tradein] cleanup-волна после стабилизации scraper-kit: удалить legacy-дубли скрапперов + косметика pool #2277

Open
opened 2026-07-03 07:30:13 +00:00 by lekss361 · 6 comments
Owner

Когда

После ~недели стабильного kit-пути на проде (активация 2026-07-03, эпики #2126/#2160 закрыты). НЕ раньше — legacy нужен как fallback (USE_KIT_SCHEDULER=false = мгновенный откат без деплоя).

Scope

1. Удаление дублей (основное)

  • tradein-mvp/backend/app/services/scrapers/* — дубли перенесённого в packages/scraper-kit
  • Legacy sweep-функции в app/services/scrape_pipeline.py и app/services/scheduler.py (trigger_*, 27-веточный if/elif)
  • Аудит importers ОБЯЗАТЕЛЕН: detail/imv/valuation/newbuilding/cadastral backfills всё ещё живут на старых app-модулях — product_handlers их зовут. НЕ удалять живые. rg-скан всех import-site'ов перед каждым удалением.
  • admin.py ручки (start_cian_city_sweep и т.п.) — перевести на kit-путь или удалить.
  • После удаления legacy-копий: убрать parity-тесты kit↔legacy (test_cian_anchor_timeout_parity_with_scraper_kit и golden-parity, потерявшие второй операнд).

2. Косметика proxy-pool (мелочь, можно отдельным PR раньше)

  • leased_by=-1 → реальный run_id (BrowserFetcher не знает run_id — прокинуть или убрать колонку-семантику «кто держит»)
  • mark_health(..., exit_ip=None) на fetch-пути — либо доставать exit_ip, либо не затирать значение health-checker'а
  • cian WARNING timed out after %ds%.0fs (float, обе копии — если legacy ещё жив)

3. Опционально (perf, обсудить)

  • Sticky proxy lease per-anchor/session для browser-путей: camoufox relaunch при каждой смене прокси = 13-45s/страница. Sticky в рамках якоря сократил бы sweep в ~2-3×, сохранив распределение по источникам. Trade-off: меньше ротации IP per-page.

Контекст

  • ADR: vault decisions/Decision_Adr_Scraper_Kit_Market_Contract.md
  • Активация: #2126 / #2160 (закрывающие комменты), PR #2268 (флаги + browser wiring), PR #2272 (cian watchdog)
## Когда После **~недели стабильного** kit-пути на проде (активация 2026-07-03, эпики #2126/#2160 закрыты). НЕ раньше — legacy нужен как fallback (`USE_KIT_SCHEDULER=false` = мгновенный откат без деплоя). ## Scope ### 1. Удаление дублей (основное) - `tradein-mvp/backend/app/services/scrapers/*` — дубли перенесённого в `packages/scraper-kit` - Legacy sweep-функции в `app/services/scrape_pipeline.py` и `app/services/scheduler.py` (trigger_*, 27-веточный if/elif) - **Аудит importers ОБЯЗАТЕЛЕН**: detail/imv/valuation/newbuilding/cadastral backfills всё ещё живут на старых app-модулях — product_handlers их зовут. НЕ удалять живые. `rg`-скан всех import-site'ов перед каждым удалением. - `admin.py` ручки (start_cian_city_sweep и т.п.) — перевести на kit-путь или удалить. - После удаления legacy-копий: убрать parity-тесты kit↔legacy (`test_cian_anchor_timeout_parity_with_scraper_kit` и golden-parity, потерявшие второй операнд). ### 2. Косметика proxy-pool (мелочь, можно отдельным PR раньше) - `leased_by=-1` → реальный run_id (BrowserFetcher не знает run_id — прокинуть или убрать колонку-семантику «кто держит») - `mark_health(..., exit_ip=None)` на fetch-пути — либо доставать exit_ip, либо не затирать значение health-checker'а - cian WARNING `timed out after %ds` → `%.0fs` (float, обе копии — если legacy ещё жив) ### 3. Опционально (perf, обсудить) - Sticky proxy lease per-anchor/session для browser-путей: camoufox relaunch при каждой смене прокси = 13-45s/страница. Sticky в рамках якоря сократил бы sweep в ~2-3×, сохранив распределение по источникам. Trade-off: меньше ротации IP per-page. ## Контекст - ADR: vault `decisions/Decision_Adr_Scraper_Kit_Market_Contract.md` - Активация: #2126 / #2160 (закрывающие комменты), PR #2268 (флаги + browser wiring), PR #2272 (cian watchdog)
lekss361 added the
scope/backend
scrapers
tech-debt
tradein
labels 2026-07-03 07:30:29 +00:00
Author
Owner

Аудит зависимостей legacy-скрапперов (2026-07-03, day 0 стабилизации) — до истечения недели рано удалять что-либо, но scope оказался шире, чем описано выше. Фиксирую, чтобы через неделю не наступить на грабли.

Путь пакета уточнён: scraper_kit — НЕ под app/, это отдельный устанавливаемый пакет tradein-mvp/packages/scraper-kit/src/scraper_kit/ (импортируется как top-level scraper_kit.*). n1-скраппера нет ни в legacy, ни в kit — убрать из scope, если упоминался.

1. Дублирование подтверждено — 24 из 26 файлов app/services/scrapers/* (14 751 строк) имеют kit-двойник (диффнул avito.py vs providers/avito/serp.py — идентичны кроме одного нового helper'а в kit). 2 файла БЕЗ kit-эквивалента: domclick_detail.py (584 строк), ekb_geoportal_client.py (238 строк) — их сначала нужно портировать в kit, потом уже удалять legacy.

2. Флаги НЕ покрывают всё (главная находка). USE_KIT_SCHEDULER переключает только scheduler_main.py между legacy scheduler_loop и kit-путём — это влияет ТОЛЬКО на SERP sweep/collection. product_handlers.py намеренно ("не дублируем логику") зовёт ОДНИ И ТЕ ЖЕ app.tasks.*/app.services.* job-бодис для обоих scheduler-путей — а эти бодис напрямую импортируют legacy scrapers/*. С флагами=true legacy всё равно остаётся ЕДИНСТВЕННОЙ реализацией для: IMV backfill, valuation (⚠️ app/services/estimator.py — core-логика оценки — импортирует avito_imv/cian_valuation/yandex_valuation), detail-enrichment, newbuilding-enrichment, domclick detail, admin debug-ручки.

3. 12 неймигрированных importers (ни один не ссылается на scraper_kit): app/api/v1/admin.py (manual run-parser debug, безусловно), app/services/estimator.py, house_imv_backfill.py, cian_price_history.py, cian_session.py, yandex_price_history.py, app/tasks/{avito_detail_backfill,cian_history_backfill,ekb_geoportal_ingest,newbuilding_enrich_backfill,yandex_detail_backfill,yandex_newbuilding_sweep}.py, scripts/ingest_domclick_jsonl.py.

4. Побочная находка: scraper_adapters.py docstring утверждает "никто их не использует" — устарело, модуль реально wired в _run_kit_scheduler. Поправить комментарий заодно с cleanup, иначе кто-то удалит рабочий код по описанию.

Вывод: раздел "1. Удаление дублей" в scope выше — не безопасно начинать даже через неделю без предварительного шага "мигрировать 12 importers + запортировать domclick_detail/ekb_geoportal_client в kit". Это отдельный подэтап перед удалением, крупнее чем подразумевает формулировка "cleanup-волна". Полный разбор — vault events/Event_Proxy_AB_Cian_Audit_Jul03 (раздел scraper duplication) + отдельная запись готовится.

**Аудит зависимостей legacy-скрапперов (2026-07-03, day 0 стабилизации)** — до истечения недели рано удалять что-либо, но scope оказался шире, чем описано выше. Фиксирую, чтобы через неделю не наступить на грабли. **Путь пакета уточнён:** `scraper_kit` — НЕ под `app/`, это отдельный устанавливаемый пакет `tradein-mvp/packages/scraper-kit/src/scraper_kit/` (импортируется как top-level `scraper_kit.*`). n1-скраппера нет ни в legacy, ни в kit — убрать из scope, если упоминался. **1. Дублирование подтверждено** — 24 из 26 файлов `app/services/scrapers/*` (14 751 строк) имеют kit-двойник (диффнул `avito.py` vs `providers/avito/serp.py` — идентичны кроме одного нового helper'а в kit). **2 файла БЕЗ kit-эквивалента**: `domclick_detail.py` (584 строк), `ekb_geoportal_client.py` (238 строк) — их сначала нужно портировать в kit, потом уже удалять legacy. **2. Флаги НЕ покрывают всё (главная находка).** `USE_KIT_SCHEDULER` переключает только `scheduler_main.py` между legacy `scheduler_loop` и kit-путём — это влияет ТОЛЬКО на SERP sweep/collection. `product_handlers.py` намеренно ("не дублируем логику") зовёт ОДНИ И ТЕ ЖЕ `app.tasks.*`/`app.services.*` job-бодис для обоих scheduler-путей — а эти бодис напрямую импортируют legacy `scrapers/*`. С флагами=true legacy всё равно остаётся ЕДИНСТВЕННОЙ реализацией для: IMV backfill, valuation (⚠️ `app/services/estimator.py` — core-логика оценки — импортирует `avito_imv`/`cian_valuation`/`yandex_valuation`), detail-enrichment, newbuilding-enrichment, domclick detail, admin debug-ручки. **3. 12 неймигрированных importers** (ни один не ссылается на `scraper_kit`): `app/api/v1/admin.py` (manual run-parser debug, безусловно), `app/services/estimator.py`, `house_imv_backfill.py`, `cian_price_history.py`, `cian_session.py`, `yandex_price_history.py`, `app/tasks/{avito_detail_backfill,cian_history_backfill,ekb_geoportal_ingest,newbuilding_enrich_backfill,yandex_detail_backfill,yandex_newbuilding_sweep}.py`, `scripts/ingest_domclick_jsonl.py`. **4. Побочная находка:** `scraper_adapters.py` docstring утверждает "никто их не использует" — устарело, модуль реально wired в `_run_kit_scheduler`. Поправить комментарий заодно с cleanup, иначе кто-то удалит рабочий код по описанию. **Вывод:** раздел "1. Удаление дублей" в scope выше — не безопасно начинать даже через неделю без предварительного шага **"мигрировать 12 importers + запортировать `domclick_detail`/`ekb_geoportal_client` в kit"**. Это отдельный подэтап перед удалением, крупнее чем подразумевает формулировка "cleanup-волна". Полный разбор — vault `events/Event_Proxy_AB_Cian_Audit_Jul03` (раздел scraper duplication) + отдельная запись готовится.
Author
Owner

Разбил "Аудит importers" + "Удаление дублей" на sub-issues (S/M размер каждый, порядок с учётом риска — non-core → core valuation последней, per .claude/rules/delegation.md):

  • #2304 — Stage 0: общий parity-test harness (блокирует всё остальное)
  • #2305 — Group A: admin/scripts debug (низкий риск)
  • #2306 — Group B: cian/yandex price-history & session
  • #2310 — Group C: backfill/enrichment tasks (ekb_geoportal_ingest зависит от #2307)
  • #2307 — Group D: порт orphan-модулей без kit-версии (domclick_detail.py, ekb_geoportal_client.py)
  • #2308 — Group E: core valuation (estimator.py) — максимальный риск, идёт последней, обязателен deep-code-reviewer APPROVE
  • #2309 — Group F: широкий рефактор структуры providers/* — после того как A-E стабильны

"Удаление дублей" из scope этого issue остаётся финальным шагом ПОСЛЕ всех sub-issues + прохождения условия по неделе стабильности (~2026-07-10+) — не меняю формулировку, просто уточняю: теперь она наступает после этой цепочки, не раньше.

Стартую Stage 0 (#2304) сейчас.

Разбил "Аудит importers" + "Удаление дублей" на sub-issues (S/M размер каждый, порядок с учётом риска — non-core → core valuation последней, per `.claude/rules/delegation.md`): - #2304 — Stage 0: общий parity-test harness (блокирует всё остальное) - #2305 — Group A: admin/scripts debug (низкий риск) - #2306 — Group B: cian/yandex price-history & session - #2310 — Group C: backfill/enrichment tasks (`ekb_geoportal_ingest` зависит от #2307) - #2307 — Group D: порт orphan-модулей без kit-версии (`domclick_detail.py`, `ekb_geoportal_client.py`) - #2308 — Group E: core valuation (`estimator.py`) — максимальный риск, идёт последней, обязателен `deep-code-reviewer` APPROVE - #2309 — Group F: широкий рефактор структуры `providers/*` — после того как A-E стабильны "Удаление дублей" из scope этого issue остаётся финальным шагом ПОСЛЕ всех sub-issues + прохождения условия по неделе стабильности (~2026-07-10+) — не меняю формулировку, просто уточняю: теперь она наступает после этой цепочки, не раньше. Стартую Stage 0 (#2304) сейчас.
Author
Owner

Финальное удаление legacy — частичный шаг выполнен (PR #2394, merged)

Стабилизационный gate (~2026-07-10) снят пользователем досрочно (2026-07-04). Перед удалением — exhaustive-аудит всех 26 legacy-модулей в app/services/scrapers/ + full-suite verify (зелёный).

Удалено сегодня (5 truly orphaned, 0 рантайм-импортёров):
avito_imv.py, yandex_valuation.py (orphaned после E4 #2337), yandex_detail.py (Group C), domclick_detail.py, ekb_geoportal_client.py (Group D). −3829 строк. Тесты: 2 parity-файла удалены, ~13 конвертированы в kit-only (покрытие сохранено), ~8 частично отредактированы.

НЕ удалено (21 модуль, ещё живые импортёры) — что разблокирует полное удаление:

Блокер Модули Разблокируется
legacy scrape_pipeline.py (module-level на старте) avito.py, avito_detail.py, avito_exceptions.py, avito_houses.py, base.py, browser_fetcher.py, cian.py, cian_detail.py, domclick.py, yandex_realty.py + транзитивные (avito_shared, cian_state_parser, price_brackets, house_type_normalizer, repair_state_normalizer, snapshot_writer, domclick_exceptions, yandex_helpers) Миграция SERP-sweep оркестрации на kit pipeline.py (USE_KIT_SCHEDULER) — отдельный крупный шаг, вне текущего эпика
cian_history_backfill.pycian_valuation cian_valuation.py Domиграция call-site (было отложено в E4)
admin/sweep/backfill → legacy newbuilding cian_newbuilding.py, yandex_newbuilding.py #2385 (сделать config mandatory) + re-migrate callers
avito_detail_backfill.pybuild_warmed_session (avito_detail.py уже блокирован pipeline) #2330 caller re-migration

Т.е. полное удаление директории требует: (а) миграции legacy scrape_pipeline.py оркестрации на kit — это отдельный эпик (SERP-sweep всё ещё крутится через legacy при USE_KIT_SCHEDULER=False), (б) domиграции 6 отложенных call-sites (#2385/#2386/#2330). До тех пор — 21 модуль остаётся.

Rollback PR #2394: тривиальный git revert (восстанавливает 5 модулей + тесты).

## Финальное удаление legacy — частичный шаг выполнен (PR #2394, merged) Стабилизационный gate (~2026-07-10) снят пользователем досрочно (2026-07-04). Перед удалением — exhaustive-аудит всех 26 legacy-модулей в `app/services/scrapers/` + full-suite verify (зелёный). **Удалено сегодня (5 truly orphaned, 0 рантайм-импортёров):** `avito_imv.py`, `yandex_valuation.py` (orphaned после E4 #2337), `yandex_detail.py` (Group C), `domclick_detail.py`, `ekb_geoportal_client.py` (Group D). −3829 строк. Тесты: 2 parity-файла удалены, ~13 конвертированы в kit-only (покрытие сохранено), ~8 частично отредактированы. **НЕ удалено (21 модуль, ещё живые импортёры) — что разблокирует полное удаление:** | Блокер | Модули | Разблокируется | |---|---|---| | legacy `scrape_pipeline.py` (module-level на старте) | avito.py, avito_detail.py, avito_exceptions.py, avito_houses.py, base.py, browser_fetcher.py, cian.py, cian_detail.py, domclick.py, yandex_realty.py + транзитивные (avito_shared, cian_state_parser, price_brackets, house_type_normalizer, repair_state_normalizer, snapshot_writer, domclick_exceptions, yandex_helpers) | Миграция SERP-sweep оркестрации на kit `pipeline.py` (USE_KIT_SCHEDULER) — отдельный крупный шаг, вне текущего эпика | | `cian_history_backfill.py` → `cian_valuation` | cian_valuation.py | Domиграция call-site (было отложено в E4) | | admin/sweep/backfill → legacy newbuilding | cian_newbuilding.py, yandex_newbuilding.py | #2385 (сделать config mandatory) + re-migrate callers | | `avito_detail_backfill.py` → `build_warmed_session` | (avito_detail.py уже блокирован pipeline) | #2330 caller re-migration | Т.е. полное удаление директории требует: (а) миграции legacy `scrape_pipeline.py` оркестрации на kit — это отдельный эпик (SERP-sweep всё ещё крутится через legacy при `USE_KIT_SCHEDULER=False`), (б) domиграции 6 отложенных call-sites (#2385/#2386/#2330). До тех пор — 21 модуль остаётся. Rollback PR #2394: тривиальный `git revert` (восстанавливает 5 модулей + тесты).
Author
Owner

FYI для cleanup-волны (не блокирует, не трогал код — вне scope этой сессии, app/services/scrapers/** = ваша активная зона):

Прод сейчас (2026-07-04 09:53 UTC) спамит numeric field overflow на INSERT INTO offer_price_history для source='domklik'diff_percent NUMERIC(8,2) (макс ±999999.99, после миграции 131) переполняется значением из entry.get("diff") в domclick_detail.py::save_detail_enrichment (priceHistory-парсинг), которое приходит НЕ клампленным от источника (в отличие от DB-триггера record_listing_price_change(), который клампит с миграции 131). Инсерт обёрнут в db.begin_nested() + try/except → не роняет батч, только шумит в postgres error log.

Проверил git log --alldomclick_detail.py уже удалён вашей веткой (a9b87aa3, ancestor of forgejo/main — 584 строки убрано, заменено на scraper-kit путь). Прод-ошибка сейчас идёт со СТАРОГО задеплоенного образа (до этого мержа), так что вероятно самоустранится когда эта миграция задеплоится — но стоит убедиться, что kit-эквивалент parse_detail_html клампит/валидирует diff/diffPercent перед инсертом (тот же класс бага, что чинила migration 131 для триггер-пути).

cian_detail.py (пока живой, в scope вашего удаления) имеет ИДЕНТИЧНЫЙ незаклампленный паттерн (change.get("diff_percent")CAST(:diff AS numeric) без валидации, строка ~393) — просто ещё не словил garbage-значение от cian. Стоит проверить то же самое в kit-эквиваленте при миграции, или добавить LEAST(GREATEST(x, -999999.99), 999999.99)-кламп на Python-уровне если legacy-путь проживёт ещё какое-то время.

Не создавал отдельный fix/issue — решил что это ваш scope сейчас, дублировать усилия смысла нет.

FYI для cleanup-волны (не блокирует, не трогал код — вне scope этой сессии, `app/services/scrapers/**` = ваша активная зона): **Прод сейчас (2026-07-04 09:53 UTC) спамит `numeric field overflow`** на `INSERT INTO offer_price_history` для `source='domklik'` — `diff_percent NUMERIC(8,2)` (макс ±999999.99, после миграции 131) переполняется значением из `entry.get("diff")` в `domclick_detail.py::save_detail_enrichment` (priceHistory-парсинг), которое приходит НЕ клампленным от источника (в отличие от DB-триггера `record_listing_price_change()`, который клампит с миграции 131). Инсерт обёрнут в `db.begin_nested()` + try/except → не роняет батч, только шумит в postgres error log. Проверил `git log --all` — `domclick_detail.py` уже удалён вашей веткой (`a9b87aa3`, ancestor of `forgejo/main` — 584 строки убрано, заменено на scraper-kit путь). Прод-ошибка сейчас идёт со СТАРОГО задеплоенного образа (до этого мержа), так что вероятно самоустранится когда эта миграция задеплоится — но стоит убедиться, что kit-эквивалент `parse_detail_html` клампит/валидирует `diff`/`diffPercent` перед инсертом (тот же класс бага, что чинила migration 131 для триггер-пути). **`cian_detail.py` (пока живой, в scope вашего удаления) имеет ИДЕНТИЧНЫЙ незаклампленный паттерн** (`change.get("diff_percent")` → `CAST(:diff AS numeric)` без валидации, строка ~393) — просто ещё не словил garbage-значение от cian. Стоит проверить то же самое в kit-эквиваленте при миграции, или добавить `LEAST(GREATEST(x, -999999.99), 999999.99)`-кламп на Python-уровне если legacy-путь проживёт ещё какое-то время. Не создавал отдельный fix/issue — решил что это ваш scope сейчас, дублировать усилия смысла нет.
Author
Owner

Working on this in PR #2404 — клампим diff_percent перед прямым INSERT в offer_price_history (3 пути: оба scraper-kit provider'а + legacy cian_detail.py), фикс прод-инцидента с numeric field overflow.

Working on this in PR #2404 — клампим diff_percent перед прямым INSERT в offer_price_history (3 пути: оба scraper-kit provider'а + legacy cian_detail.py), фикс прод-инцидента с numeric field overflow.
Author
Owner

Финал эпика: legacy scraper-стек полностью удалён

Umbrella #2397 (decommission legacy orchestration) закрыт. Последовательность: Part C (#2402, удаление scheduler_loop) → Partial E (#2405 scrape_pipeline.py, #2407 6 orphaned модулей) → Part D (#2410/#2411/#2415/#2414/#2412 — 5 call-site миграций на kit с config=) → final E (#2419 — весь app/services/scrapers/ каталог, 16 модулей).

Итог: app/services/scrapers/ больше не существует, scrape_pipeline.py / legacy scheduler.py-оркестрация удалены. Trade-in-скрапинг (SERP, detail, newbuilding, valuation, houses, все провайдеры + scheduler) — 100% на scraper_kit. Прод verified GREEN (deployed 9c6afcc8: kit-scheduler 29 handlers, миграции импортятся, GlitchTip чист).

Остаточный test-only долг: #2406, #2408, #2422 (восстановить standalone kit-покрытие взамен удалённых legacy golden-parity). Эпик #2277 можно закрывать.

## Финал эпика: legacy scraper-стек полностью удалён Umbrella #2397 (decommission legacy orchestration) закрыт. Последовательность: Part C (#2402, удаление `scheduler_loop`) → Partial E (#2405 `scrape_pipeline.py`, #2407 6 orphaned модулей) → Part D (#2410/#2411/#2415/#2414/#2412 — 5 call-site миграций на kit с `config=`) → final E (#2419 — весь `app/services/scrapers/` каталог, 16 модулей). Итог: `app/services/scrapers/` больше не существует, `scrape_pipeline.py` / legacy `scheduler.py`-оркестрация удалены. Trade-in-скрапинг (SERP, detail, newbuilding, valuation, houses, все провайдеры + scheduler) — 100% на `scraper_kit`. Прод verified GREEN (deployed `9c6afcc8`: kit-scheduler 29 handlers, миграции импортятся, GlitchTip чист). Остаточный test-only долг: #2406, #2408, #2422 (восстановить standalone kit-покрытие взамен удалённых legacy golden-parity). Эпик #2277 можно закрывать.
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#2277
No description provided.