tradein: 38% дневных снимков объявлений не знают своего прогона — три вызова save_listings из девяти теряют run_id #2701

Closed
opened 2026-08-06 06:00:23 +00:00 by bot-backend · 1 comment
Collaborator

Найдено при разборе position_in_serp (эпик #2674, PR #2694). Соседний аргумент того же писателя теряется тем же способом.

Замер

listings_snapshots: 396 062 строки | run_id заполнен 245 647 | 62.0%

38% дневных снимков не знают, какой прогон их записал.

Где рвётся

Девять вызовов save_listings(...) в packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py. Шесть передают run_id=, три — нет:

Строка Функция run_id в области видимости?
570 run_avito_pipeline нет
1113 run_avito_city_sweep да — просто не передан
1754 run_avito_newbuilding_sweep да — просто не передан
1995, 2552, 2978, 3301, 3540, 3766 остальные передают

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

Все три — Авито, и это объясняет масштаб: Авито крупнейший источник (48 222 объявления против 21 799 у Циана).

Чем это мешает

run_id — единственная связь снимка с прогоном, который его сделал. Без неё нельзя ответить на вопросы, ради которых колонку и заводили: что именно принёс конкретный прогон; какие снимки писал прогон, оказавшийся неполным или отменённым; чем отличаются два прогона одного источника за одни сутки.

Существеннее: неполнота молчаливая. Запрос «снимки прогона N» вернёт строки и будет выглядеть исправным — просто без 38% данных, и по авито-источнику доля пропусков заведомо выше средней. Это тот же класс, что и остальные находки эпика: ответ есть, он неверен, и ничто на это не указывает.

Что нужно

  1. Передать run_id в двух вызовах, где он уже в области видимости (1113, 1754).
  2. Отдельно решить по run_avito_pipeline:570 — прокидывать ли туда run_id или это осознанно вне-прогонный путь; если второе, записать это в код, чтобы вопрос не возникал заново.
  3. Проверить остальные писатели listings_snapshots вне pipeline.pybackend/app/api/v1/admin.py:235 и backend/scripts/ingest_domclick_jsonl.py:193 вызовы тоже есть) — этот замер их не покрывает.
  4. Исторические 150 415 строк восстановлению не подлежат: соответствия снимок→прогон нигде больше нет. Бэкфилл невозможен, и это стоит записать, чтобы его не искали.

Связано: #2674, #2694, #2697.

Найдено при разборе `position_in_serp` (эпик #2674, PR #2694). Соседний аргумент того же писателя теряется тем же способом. ## Замер ``` listings_snapshots: 396 062 строки | run_id заполнен 245 647 | 62.0% ``` **38% дневных снимков не знают, какой прогон их записал.** ## Где рвётся Девять вызовов `save_listings(...)` в `packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py`. Шесть передают `run_id=`, три — нет: | Строка | Функция | `run_id` в области видимости? | |---:|---|---| | 570 | `run_avito_pipeline` | нет | | **1113** | **`run_avito_city_sweep`** | **да — просто не передан** | | **1754** | **`run_avito_newbuilding_sweep`** | **да — просто не передан** | | 1995, 2552, 2978, 3301, 3540, 3766 | остальные | передают | Два случая из трёх — чистая потеря на вызове: значение лежит рядом, в той же функции. Третий (`run_avito_pipeline`) требует отдельного решения — там `run_id` в функцию не заходит вообще. Все три — Авито, и это объясняет масштаб: Авито крупнейший источник (48 222 объявления против 21 799 у Циана). ## Чем это мешает `run_id` — единственная связь снимка с прогоном, который его сделал. Без неё нельзя ответить на вопросы, ради которых колонку и заводили: что именно принёс конкретный прогон; какие снимки писал прогон, оказавшийся неполным или отменённым; чем отличаются два прогона одного источника за одни сутки. Существеннее: **неполнота молчаливая**. Запрос «снимки прогона N» вернёт строки и будет выглядеть исправным — просто без 38% данных, и по авито-источнику доля пропусков заведомо выше средней. Это тот же класс, что и остальные находки эпика: ответ есть, он неверен, и ничто на это не указывает. ## Что нужно 1. Передать `run_id` в двух вызовах, где он уже в области видимости (1113, 1754). 2. Отдельно решить по `run_avito_pipeline:570` — прокидывать ли туда `run_id` или это осознанно вне-прогонный путь; если второе, записать это в код, чтобы вопрос не возникал заново. 3. Проверить остальные писатели `listings_snapshots` вне `pipeline.py` (в `backend/app/api/v1/admin.py:235` и `backend/scripts/ingest_domclick_jsonl.py:193` вызовы тоже есть) — этот замер их не покрывает. 4. Исторические 150 415 строк восстановлению не подлежат: соответствия снимок→прогон нигде больше нет. Бэкфилл невозможен, и это стоит записать, чтобы его не искали. Связано: #2674, #2694, #2697.
Author
Collaborator

Починено — PR #2707. И одновременно: атрибуция масштаба в теле этой задачи НЕВЕРНА

Я написал выше «все три вызова — Авито, и это объясняет масштаб». Это не подтверждается. Заполнение run_id по источникам (проверено на проде сейчас):

источник   снимков    с run_id    доля
domklik    133 894       3 886     2.9%
avito       99 731      80 161    80.4%
cian        78 482      77 570    98.8%
yandex      84 254      84 229   100.0%
n1             174         174   100.0%

Из 150 515 пропусков 129 908 — Домклик, и это исторический разовый ingest JSONL-файла, у которого прогона не было и быть не могло. Ежедневный run_domclick_city_sweep run_id передаёт исправно.

То есть общая доля 62% почти целиком объясняется одной исторической заливкой, а не тремя найденными вызовами.

Почему правка всё равно была нужна, и именно та

Авито-дыра — текущая, каждый день:

06.08   944 снимка без run_id
05.08   635
04.08   582

Это не наследие, это ежедневная потеря. Так что вывод задачи («передать run_id в двух вызовах, где он уже в области видимости») остаётся верным — ошибочна была только оценка того, сколько от общей цифры приходится на этот дефект.

Прод-верификация: 100% на новых строках

Деплой 07:06 UTC. Снимки за сегодня:

run_id      строк   время
(NULL)        944   00:28:56 – 06:25:34   ← утро, старый код
3258          137   02:49:45
3280          113   07:06:46              ← после деплоя
3283          148   07:40:47
3284          112   07:43:47

Ни одной новой пустой строки после деплоя.

Решение по третьему вызову

run_avito_pipeline:570run_id НЕ прокидывается, решение записано комментарием в коде. Обоснование: строки в scrape_runs у этой функции нет вообще, привязывать снимок не к чему; все ссылки на неё, кроме определения, — тесты, причём один из них сам называет её legacy.

Писатели вне pipeline.py разобраны: ручной скрейп из админки и разовый ingest файла — прогона нет, NULL там честен, оба помечены комментарием. Найден ещё один, которого в задаче не было: providers/cian/detail.py:382 пишет зашитый run_id=None — не чинилось сознательно, у большинства вызывающих прогона в области видимости нет, а Циан и так 98.8%.

Бэкфилл невозможен — проверено, а не предположено

  • Привязки в схеме нет.
  • По времени неоднозначно: на всех 36 днях с осиротевшими avito-снимками в сутках работало больше одного пишущего avito-прогона (доходило до 29 за день).
  • observed_at не спасает: ON CONFLICT ... DO UPDATE SET observed_at = EXCLUDED.observed_at — метка принадлежит последнему писателю дня, а не создателю строки.

Ложная атрибуция здесь была бы хуже пустоты, поэтому не делалось.

## Починено — PR #2707. И одновременно: атрибуция масштаба в теле этой задачи НЕВЕРНА Я написал выше «все три вызова — Авито, и это объясняет масштаб». Это не подтверждается. Заполнение `run_id` по источникам (проверено на проде сейчас): ``` источник снимков с run_id доля domklik 133 894 3 886 2.9% avito 99 731 80 161 80.4% cian 78 482 77 570 98.8% yandex 84 254 84 229 100.0% n1 174 174 100.0% ``` Из 150 515 пропусков **129 908 — Домклик**, и это исторический разовый ingest JSONL-файла, у которого прогона не было и быть не могло. Ежедневный `run_domclick_city_sweep` `run_id` передаёт исправно. То есть общая доля 62% почти целиком объясняется одной исторической заливкой, а не тремя найденными вызовами. ## Почему правка всё равно была нужна, и именно та Авито-дыра — **текущая, каждый день**: ``` 06.08 944 снимка без run_id 05.08 635 04.08 582 ``` Это не наследие, это ежедневная потеря. Так что вывод задачи («передать `run_id` в двух вызовах, где он уже в области видимости») остаётся верным — ошибочна была только оценка того, сколько от общей цифры приходится на этот дефект. ## Прод-верификация: 100% на новых строках Деплой 07:06 UTC. Снимки за сегодня: ``` run_id строк время (NULL) 944 00:28:56 – 06:25:34 ← утро, старый код 3258 137 02:49:45 3280 113 07:06:46 ← после деплоя 3283 148 07:40:47 3284 112 07:43:47 ``` **Ни одной новой пустой строки после деплоя.** ## Решение по третьему вызову `run_avito_pipeline:570` — `run_id` НЕ прокидывается, решение записано комментарием в коде. Обоснование: строки в `scrape_runs` у этой функции нет вообще, привязывать снимок не к чему; все ссылки на неё, кроме определения, — тесты, причём один из них сам называет её legacy. Писатели вне `pipeline.py` разобраны: ручной скрейп из админки и разовый ingest файла — прогона нет, `NULL` там честен, оба помечены комментарием. Найден ещё один, которого в задаче не было: `providers/cian/detail.py:382` пишет зашитый `run_id=None` — не чинилось сознательно, у большинства вызывающих прогона в области видимости нет, а Циан и так 98.8%. ## Бэкфилл невозможен — проверено, а не предположено - Привязки в схеме нет. - По времени неоднозначно: на **всех 36** днях с осиротевшими avito-снимками в сутках работало больше одного пишущего avito-прогона (доходило до 29 за день). - `observed_at` не спасает: `ON CONFLICT ... DO UPDATE SET observed_at = EXCLUDED.observed_at` — метка принадлежит последнему писателю дня, а не создателю строки. Ложная атрибуция здесь была бы хуже пустоты, поэтому не делалось.
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#2701
No description provided.