feat(scrapers): ночной avito detail-enrichment backfill для legacy listings (Closes #1551) #1552

Merged
bot-backend merged 1 commit from feat/avito-detail-backfill into main 2026-06-16 10:32:58 +00:00
Owner

Проблема (Closes #1551)

15243/15917 avito-объявлений имеют detail_enriched_at IS NULL — никогда не обогащались detail-страницей. Свип (scrape_pipeline Stage 5 ENRICH_DETAIL) берёт только fresh/proximate top-N → legacy застревают. Прокси Avito уже работает (#788/#805, runbook avito_mobile_proxy.md) — это не anti-bot стена, а отсутствие backfill. Прямой аналог geocode-gap (PR #1549).

Фикс

Ночной backfill, проходящий legacy через тот же прокси-путь, что и pipeline. Паттерн — копия geocode_missing_listings / yandex_address_backfill:

  • app/tasks/avito_detail_backfill.py (new) — run_avito_detail_backfill(run_id, params):
    • прокси-сессия собирается ТОЧНО как run_avito_pipeline (AsyncSession impersonate=chrome120 proxies=_avito_proxies(), либо browser-mode BrowserFetcher); own session/browser закрывается в finally;
    • snapshot SELECT один раз (detail_enriched_at IS NULL, приоритет is_active DESC, scraped_at DESC, LIMIT batch_size) → итерация по фикс-списку → гарантия завершения;
    • fetch_detailsave_detail_enrichment (сам ставит detail_enriched_at=NOW() при успехе);
    • на AvitoBlockedError/RateLimited: _rotate_ip() + consecutive_blocks++; после max_consecutive_blocks → break + mark_done (НЕ failed — блок временный, объявление остаётся NULL → повтор след. прогон); счётчик сбрасывается только на успехе;
    • wall-clock budget_sec guard; db.rollback() на ошибке (защита от aborted Session, #1368); run-lifecycle heartbeat/mark_done/mark_failed.
  • app/services/scheduler.pytrigger_avito_detail_backfill_run (зеркало geocode-триггера) + dispatch elif.
  • data/sql/112_scrape_schedules_seed_avito_detail_backfill.sql — seed enabled=true, окно 9-12 UTC (разнесено с avito_city_sweep ~3-4 UTC, чтобы не делить прокси-IP одновременно), batch_size=800 budget_sec=3600 request_delay_sec=6 max_consecutive_blocks=5, idempotent.
  • tests — 7 кейсов: snapshot до конца, block→rotate+abort после max, budget break, success→save+counters, mark_failed на top-level exception.

Эффект

Догонит 15243 legacy → точные data-map координаты (помимо geocode-fallback #1549) + поля квартир (living_area_m2/repair_state/ceiling_height_m/etc, поля захватываются после #1550). Параметры в default_params jsonb — тюнятся оператором без кода.

Проверки

  • uv run pytest (полный suite, CI deselect): 1809 passed, 0 failures (7 новых).
  • ruff check + ruff format --check: clean.
  • code-reviewer: APPROVE (все 7 фокус-пунктов чисто, гарантия завершения + прокси-путь подтверждены).
  • Воркер сам поймал+починил 2 бага (UnboundLocalError на start, двойной time.monotonic()).

Зависимость

Миграция 112 (110/111 уже в main). Нет пересечения файлов с открытыми PR.

## Проблема (Closes #1551) 15243/15917 avito-объявлений имеют `detail_enriched_at IS NULL` — никогда не обогащались detail-страницей. Свип (`scrape_pipeline` Stage 5 ENRICH_DETAIL) берёт только fresh/proximate top-N → legacy застревают. Прокси Avito **уже работает** (#788/#805, runbook `avito_mobile_proxy.md`) — это не anti-bot стена, а отсутствие backfill. Прямой аналог geocode-gap (PR #1549). ## Фикс Ночной backfill, проходящий legacy через тот же прокси-путь, что и pipeline. Паттерн — копия `geocode_missing_listings` / `yandex_address_backfill`: - **`app/tasks/avito_detail_backfill.py`** (new) — `run_avito_detail_backfill(run_id, params)`: - прокси-сессия собирается ТОЧНО как `run_avito_pipeline` (`AsyncSession impersonate=chrome120 proxies=_avito_proxies()`, либо browser-mode `BrowserFetcher`); own session/browser закрывается в `finally`; - **snapshot SELECT один раз** (`detail_enriched_at IS NULL`, приоритет `is_active DESC, scraped_at DESC`, LIMIT batch_size) → итерация по фикс-списку → гарантия завершения; - `fetch_detail` → `save_detail_enrichment` (сам ставит `detail_enriched_at=NOW()` при успехе); - на `AvitoBlockedError`/`RateLimited`: `_rotate_ip()` + `consecutive_blocks++`; после `max_consecutive_blocks` → break + `mark_done` (НЕ failed — блок временный, объявление остаётся NULL → повтор след. прогон); счётчик сбрасывается только на успехе; - wall-clock `budget_sec` guard; `db.rollback()` на ошибке (защита от aborted Session, #1368); run-lifecycle heartbeat/mark_done/mark_failed. - **`app/services/scheduler.py`** — `trigger_avito_detail_backfill_run` (зеркало geocode-триггера) + dispatch elif. - **`data/sql/112_scrape_schedules_seed_avito_detail_backfill.sql`** — seed enabled=true, окно **9-12 UTC** (разнесено с `avito_city_sweep` ~3-4 UTC, чтобы не делить прокси-IP одновременно), `batch_size=800 budget_sec=3600 request_delay_sec=6 max_consecutive_blocks=5`, idempotent. - **tests** — 7 кейсов: snapshot до конца, block→rotate+abort после max, budget break, success→save+counters, mark_failed на top-level exception. ## Эффект Догонит 15243 legacy → точные `data-map` координаты (помимо geocode-fallback #1549) + поля квартир (`living_area_m2`/`repair_state`/`ceiling_height_m`/etc, поля захватываются после #1550). Параметры в `default_params` jsonb — тюнятся оператором без кода. ## Проверки - `uv run pytest` (полный suite, CI deselect): **1809 passed, 0 failures** (7 новых). - `ruff check` + `ruff format --check`: clean. - code-reviewer: ✅ APPROVE (все 7 фокус-пунктов чисто, гарантия завершения + прокси-путь подтверждены). - Воркер сам поймал+починил 2 бага (UnboundLocalError на `start`, двойной `time.monotonic()`). ## Зависимость Миграция **112** (110/111 уже в main). Нет пересечения файлов с открытыми PR.
lekss361 added 1 commit 2026-06-16 10:30:42 +00:00
feat(tradein): avito detail-enrichment nightly backfill (#1551)
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / changes (pull_request) Successful in 6s
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
227e82dc01
- New task app/tasks/avito_detail_backfill.py with run_avito_detail_backfill()
  * Single snapshot SELECT at start (guarantees termination)
  * Same proxy/AsyncSession path as scrape_pipeline.py step 5
  * Budget guard (budget_sec), consecutive block abort (mark_done not mark_failed)
  * rotate_ip() on every AvitoBlockedError; rollback on generic Exception (#1368)
  * start = time.monotonic() initialized before try so except can reference it
- Scheduler wiring: trigger_avito_detail_backfill_run() + elif in scheduler_loop()
- Migration 112: scrape_schedules INSERT window 09-12 UTC, batch_size=800,
  budget_sec=3600, request_delay_sec=6, max_consecutive_blocks=5
- 7 unit tests (no pytest-mock, unittest.mock only): all 7 passing,
  full CI suite 1809 passed
lekss361 added the
enhancement
scope/backend
scrapers
labels 2026-06-16 10:30:51 +00:00
bot-backend merged commit 1fc375c2cc into main 2026-06-16 10:32:58 +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#1552
No description provided.