fix(tradein/scraper): дневной снимок узнаёт свой прогон (#2701) #2707

Merged
bot-backend merged 1 commit from fix/2701-snapshot-run-id into main 2026-08-06 07:00:18 +00:00
Collaborator

Summary

Три из девяти вызовов save_listings в orchestration/pipeline.py не передавали run_id. У двух он лежал в той же функции — передан:

  • run_avito_city_sweep:1113
  • run_avito_newbuilding_sweep:1754

Это тот же run_id, под которым идёт прогон: keyword-параметр функции, по нему же зовутся runs.is_cancelled / update_heartbeat / mark_done / mark_banned и он печатается в каждой лог-строке sweep'а.

Решение по run_avito_pipeline:570 — не передавать, и вот почему

Записано комментарием в коде. У функции нет строки в scrape_runs, и продуктовых вызывающих у неё тоже нет: grep -rn "run_avito_pipeline(" по репозиторию даёт только определение; все ссылки — тесты (test_pipeline_browser_routing.py прямо называет её legacy) и докстринги. Развёртки идут через run_avito_city_sweep / run_avito_newbuilding_sweep / run_avito_full_load, которые прогон заводят. Привязывать снимок не к чему; снимок с run_id несуществующего прогона хуже пустого поля.

Писатели вне pipeline.py — проверены, помечены

место вердикт
backend/app/api/v1/admin.py:235 ручной admin-скрейп, строки в scrape_runs не заводит → NULL честен, помечено комментарием
backend/scripts/ingest_domclick_jsonl.py:193 разовый ingest файла, не прогон → NULL честен, помечено
backend/app/tasks/avito_detail_backfill.py:457 run_id СВОЕГО прогона уже передаёт — норм
providers/cian/detail.py:382 (save_detail_enrichment) зашитый run_id=None; вызывающие — backfill-задачи / admin / price-history, прогон в области видимости не у всех. НЕ трогал: доля cian 98.8%, это не дыра. Отдельная находка

Опровергнутая предпосылка issue

«Все три — Авито, и это объясняет масштаб» — не подтверждается. Заполнение по источникам:

domklik | 133 894 | 3 886  |   2.9%   <- 129 908 из 150 515 пропусков
avito   |  99 583 | 80 013 |  80.3%   <- 19 570
cian    |  78 369 | 77 457 |  98.8%
yandex  |  84 142 | 84 117 | 100.0%

Главная дыра — исторический domklik-ingest (см. таблицу выше), а не Авито. Но она историческая: ежедневный run_domclick_city_sweep run_id передаёт (59/59 за 2026-08-05). А вот avito-дыра текущая — каждый день: 944 строки за 2026-08-06, 635 за 2026-08-05, 582 за 2026-08-04 — все с пустым run_id. Именно её и чинит этот PR.

Бэкфилл 150 415 строк — невозможен, проверено

  • Привязки снимок→прогон в схеме больше нет.
  • По времени неоднозначно: на всех 36 днях с осиротевшими avito-снимками в сутках было >1 пишущего avito-прогона (максимум 29 за день — avito_city_sweep, avito_full_load, avito_newbuilding_sweep, avito_city_sweep_<город>, avito_detail_backfill).
  • observed_at не спасает: ON CONFLICT ... DO UPDATE SET observed_at = EXCLUDED.observed_at — метка принадлежит ПОСЛЕДНЕМУ писателю дня, а не тому, кто строку создал.

Ложная атрибуция хуже пустоты — бэкфилл не делается.

Test plan

  • Новые тесты: test_avito_city_sweep_passes_run_id_to_save_listings (оба anchor'а), test_avito_newbuilding_sweep_passes_run_id_to_save_listings
  • Фальсификация: с убранным run_id=run_id оба теста падают (проверено локально), с правкой — проходят
  • Вторая половина цепочки (save_listingsupsert_listing_snapshot) уже под замком: test_snapshot_writer.py::test_save_listings_snapshot_receives_run_id
  • 151 passed по sweep/snapshot/pipeline-файлам, ruff 0.7.4 clean
  • Прод: после деплоя дождаться окна avito_city_sweep → доля непустых run_id среди новых строк = 100%

Refs #2701

## Summary Три из девяти вызовов `save_listings` в `orchestration/pipeline.py` не передавали `run_id`. У двух он лежал в той же функции — передан: - `run_avito_city_sweep:1113` - `run_avito_newbuilding_sweep:1754` Это тот же `run_id`, под которым идёт прогон: keyword-параметр функции, по нему же зовутся `runs.is_cancelled / update_heartbeat / mark_done / mark_banned` и он печатается в каждой лог-строке sweep'а. ## Решение по `run_avito_pipeline:570` — не передавать, и вот почему Записано комментарием в коде. У функции **нет** строки в `scrape_runs`, и продуктовых вызывающих у неё тоже нет: `grep -rn "run_avito_pipeline("` по репозиторию даёт только определение; все ссылки — тесты (`test_pipeline_browser_routing.py` прямо называет её legacy) и докстринги. Развёртки идут через `run_avito_city_sweep` / `run_avito_newbuilding_sweep` / `run_avito_full_load`, которые прогон заводят. Привязывать снимок не к чему; снимок с `run_id` несуществующего прогона хуже пустого поля. ## Писатели вне pipeline.py — проверены, помечены | место | вердикт | |---|---| | `backend/app/api/v1/admin.py:235` | ручной admin-скрейп, строки в `scrape_runs` не заводит → NULL честен, помечено комментарием | | `backend/scripts/ingest_domclick_jsonl.py:193` | разовый ingest файла, не прогон → NULL честен, помечено | | `backend/app/tasks/avito_detail_backfill.py:457` | `run_id` СВОЕГО прогона уже передаёт — норм | | `providers/cian/detail.py:382` (`save_detail_enrichment`) | зашитый `run_id=None`; вызывающие — backfill-задачи / admin / price-history, прогон в области видимости не у всех. НЕ трогал: доля cian 98.8%, это не дыра. Отдельная находка | ## Опровергнутая предпосылка issue «Все три — Авито, и это объясняет масштаб» — **не подтверждается**. Заполнение по источникам: ``` domklik | 133 894 | 3 886 | 2.9% <- 129 908 из 150 515 пропусков avito | 99 583 | 80 013 | 80.3% <- 19 570 cian | 78 369 | 77 457 | 98.8% yandex | 84 142 | 84 117 | 100.0% ``` Главная дыра — исторический domklik-ingest (см. таблицу выше), а не Авито. Но она **историческая**: ежедневный `run_domclick_city_sweep` `run_id` передаёт (59/59 за 2026-08-05). А вот avito-дыра **текущая** — каждый день: 944 строки за 2026-08-06, 635 за 2026-08-05, 582 за 2026-08-04 — все с пустым `run_id`. Именно её и чинит этот PR. ## Бэкфилл 150 415 строк — невозможен, проверено - Привязки снимок→прогон в схеме больше нет. - По времени неоднозначно: на **всех 36** днях с осиротевшими avito-снимками в сутках было >1 пишущего avito-прогона (максимум 29 за день — `avito_city_sweep`, `avito_full_load`, `avito_newbuilding_sweep`, `avito_city_sweep_<город>`, `avito_detail_backfill`). - `observed_at` не спасает: `ON CONFLICT ... DO UPDATE SET observed_at = EXCLUDED.observed_at` — метка принадлежит ПОСЛЕДНЕМУ писателю дня, а не тому, кто строку создал. Ложная атрибуция хуже пустоты — бэкфилл не делается. ## Test plan - [x] Новые тесты: `test_avito_city_sweep_passes_run_id_to_save_listings` (оба anchor'а), `test_avito_newbuilding_sweep_passes_run_id_to_save_listings` - [x] Фальсификация: с убранным `run_id=run_id` оба теста падают (проверено локально), с правкой — проходят - [x] Вторая половина цепочки (`save_listings` → `upsert_listing_snapshot`) уже под замком: `test_snapshot_writer.py::test_save_listings_snapshot_receives_run_id` - [x] 151 passed по sweep/snapshot/pipeline-файлам, ruff 0.7.4 clean - [ ] Прод: после деплоя дождаться окна `avito_city_sweep` → доля непустых `run_id` среди новых строк = 100% Refs #2701
bot-backend added 1 commit 2026-08-06 06:45:45 +00:00
fix(tradein/scraper): дневной снимок узнаёт свой прогон (#2701)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 / 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 3m6s
8ffc2cb942
Из девяти вызовов save_listings в pipeline.py три не передавали run_id.
У двух — run_avito_city_sweep:1113 и run_avito_newbuilding_sweep:1754 —
run_id лежал в той же функции (keyword-параметр, под которым идёт
runs.mark_done/heartbeat/is_cancelled) и просто не доезжал до вызова.
Передан.

run_avito_pipeline:570 — оставлен без run_id осознанно, решение записано
в код: у функции нет строки в scrape_runs, продуктовых вызывающих нет
(единственные — тесты), привязывать снимок не к чему.

Писатели вне pipeline.py проверены и тоже помечены: admin /scrape (ручной
скрейп без прогона) и scripts/ingest_domclick_jsonl.py (разовый ingest
файла). Второй и есть главная дыра замера — 129 908 снимков domklik без
run_id, а вовсе не Авито: по источникам заполнение yandex 100%, cian 98.8%,
avito 80.3%, domklik 2.9%. Дыра domklik историческая (ежедневный
run_domclick_city_sweep run_id передаёт), а вот у avito пустые строки
приходят КАЖДЫЙ день (944 за 2026-08-06, 635 за 2026-08-05 — все без run_id).

Бэкфилл 150 515 исторических строк невозможен, проверено:
  - привязки снимок→прогон в схеме больше нет;
  - по времени неоднозначно: на всех 36 днях с осиротевшими avito-снимками
    в сутках было >1 пишущего avito-прогона (до 29);
  - observed_at не спасает — ON CONFLICT перезаписывает его последним
    писателем дня, а не тем, кто строку создал.
Ложная атрибуция хуже пустоты — бэкфилл не делается.

Refs #2701
bot-backend merged commit cb64796b44 into main 2026-08-06 07:00:18 +00:00
bot-backend deleted branch fix/2701-snapshot-run-id 2026-08-06 07:00:18 +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#2707
No description provided.