fix(tradein/newbuilding): счётчики записи различают вставку и обновление (#2807) #2809

Merged
bot-backend merged 1 commit from fix/2807-price-dynamics-counter into main 2026-08-10 08:50:57 +00:00
Collaborator

Что было не так

newbuilding_enrich_backfill.py:605 считал свою работу разницей COUNT(*) до и после сохранения. Это прирост числа строк в таблице, а не число записанных точек: вставка идёт ON CONFLICT ON CONSTRAINT houses_price_dynamics_dim_key DO UPDATE, поэтому переписывание существующей точки даёт ноль.

Прод 10.08, прогон 3578: "price_dynamics_rows": 0 при 64 строках с обновлённой recorded_at по 10 домам за окно прогона. Их вставил прогон 3563 накануне — у него в тех же counters стояло 64. Ноль читался как «динамика цен снова не пишется».

Оба соседних счётчика в том же месте врали по своим причинам:

  • reliability_rows — строку вставляли обычным INSERT, а следом _dedup_reliability схлопывал дубль; net-прирост повторного прогона 0 при реально записанной строке;
  • review_rows_save_cian_reviews уже возвращал число записанных отзывов, и код это число выбрасывал в пользу разницы COUNT'ов.

Класс или случай

Случай, хоть и тройной. git grep по приёму «count после − count до» для счётчиков прогона даёт единственное второе совпадение — lots_dropped_secondary в pipeline.py:2597, и там это длина списка до/после фильтрации, то есть измерение честное по построению. Гипотеза «это класс» опровергнута, чинится одно место.

Как починено

Различать вставку и обновление умеет только сам писатель: RETURNING (xmax = 0) AS is_insert — идиома, уже применяемая в репозитории (backend/app/services/scrapers/gisogd66.py:301). save_newbuilding_enrichment возвращает NewbuildingSaveCounts(price_inserted, price_updated, reliability_inserted); вызывающий перестал восстанавливать число по таблице, а review_written берётся из того, что _save_cian_reviews и так возвращал. Остальные три вызова (cian_history_backfill, SERP-sweep в pipeline, admin-ручка) результат игнорируют — их сигнатура не ломается.

COUNT(*) до сохранения остался ровно там, где нужен по делу: had_reliability для дедупа.

Ключи counters переименованы: price_dynamics_inserted / price_dynamics_updated / reliability_inserted / review_upserted. У старых имён в истории прогонов другой смысл (net-прирост), и поменять его молча под тем же именем — ровно тот дефект, ради которого правка и делается. Читателей у ключей вне задачи и её тестов нет (git grep: только newbuilding_enrich_backfill.py + 2 теста).

Сторож нулевого результата — проверено, не ослаблен

mark_backfill_finished (#2695) судит по attempted / enriched / gone / blocked; сторож #2703 читает total_seen/lots_fetched/unique_fetched, которых у этой задачи нет вовсе. Числа записи в решение не входят ни до, ни после правки — и это закреплено тестами, а не рассуждением: прогон с enriched=0 и price_dynamics_updated=64 остаётся failed, а price_dynamics_updated=999 не двигает вердикт.

Test plan

  • Красный прогон на origin/main — сценарий прода end-to-end (10 домов × 6 точек, второй проход по тем же домам):
=== origin/main (без правки) ===
проход 1: счётчик= 60  записей писателем=60
проход 2: счётчик=  0  записей писателем=60  «обновлено»=None
КРАСНО: второй проход переписал 60 точек по 10 домам и отчитался
        price_dynamics_rows=0 — прод-симптом прогона 3578.   (exit=1)

=== ветка fix/2807 ===
проход 1: счётчик= 60  записей писателем=60
проход 2: счётчик=  0  записей писателем=60  «обновлено»=60
ЗЕЛЕНО: обновления больше не читаются как ноль.              (exit=0)
  • tests/test_2807_write_counters_honesty.py — 8 тестов (вставка/обновление на 64 точках прода, пустой график остаётся нулём, точка без цены не считается записанной, 4 теста на неприкосновенность сторожа). На коде до правки модуль не собирается: NewbuildingSaveCounts не существует.
  • Соседние: test_newbuilding_enrich_backfill + test_2767 + test_2725 + test_scraper_kit_group_c_backfill_kit_parity + test_2703 — 75 passed.
  • Полный tests/ — 3737 passed; 11 падений идентичны падениям на origin/main (локальное окружение), новых нет.
  • ruff + ruff-format.

Прод-верификация (критерий записан ДО факта)

newbuilding_enrich бежит ежесуточно ~00:28 UTC. Первый прогон после деплоя обязан показать в scrape_runs.counters непустое price_dynamics_updated при price_dynamics_inserted близком к нулю — потому что 301 из 301 fetchable-домов уже обогащены, и работа этого прогона по определению состоит из обновлений. Если оба поля нули при succeeded > 0 — правка не доехала или писатель молчит.

Closes #2807

## Что было не так `newbuilding_enrich_backfill.py:605` считал свою работу разницей `COUNT(*)` до и после сохранения. Это прирост **числа строк в таблице**, а не число записанных точек: вставка идёт `ON CONFLICT ON CONSTRAINT houses_price_dynamics_dim_key DO UPDATE`, поэтому переписывание существующей точки даёт ноль. Прод 10.08, прогон 3578: `"price_dynamics_rows": 0` при **64 строках с обновлённой `recorded_at` по 10 домам** за окно прогона. Их вставил прогон 3563 накануне — у него в тех же counters стояло 64. Ноль читался как «динамика цен снова не пишется». Оба соседних счётчика в том же месте врали по своим причинам: - `reliability_rows` — строку вставляли обычным INSERT, а следом `_dedup_reliability` схлопывал дубль; net-прирост повторного прогона 0 при реально записанной строке; - `review_rows` — `_save_cian_reviews` **уже возвращал** число записанных отзывов, и код это число выбрасывал в пользу разницы COUNT'ов. ## Класс или случай Случай, хоть и тройной. `git grep` по приёму «count после − count до» для счётчиков прогона даёт единственное второе совпадение — `lots_dropped_secondary` в `pipeline.py:2597`, и там это длина списка до/после фильтрации, то есть измерение честное по построению. Гипотеза «это класс» **опровергнута**, чинится одно место. ## Как починено Различать вставку и обновление умеет только сам писатель: `RETURNING (xmax = 0) AS is_insert` — идиома, уже применяемая в репозитории (`backend/app/services/scrapers/gisogd66.py:301`). `save_newbuilding_enrichment` возвращает `NewbuildingSaveCounts(price_inserted, price_updated, reliability_inserted)`; вызывающий перестал восстанавливать число по таблице, а `review_written` берётся из того, что `_save_cian_reviews` и так возвращал. Остальные три вызова (`cian_history_backfill`, SERP-sweep в `pipeline`, admin-ручка) результат игнорируют — их сигнатура не ломается. `COUNT(*)` до сохранения остался ровно там, где нужен по делу: `had_reliability` для дедупа. **Ключи counters переименованы**: `price_dynamics_inserted` / `price_dynamics_updated` / `reliability_inserted` / `review_upserted`. У старых имён в истории прогонов другой смысл (net-прирост), и поменять его молча под тем же именем — ровно тот дефект, ради которого правка и делается. Читателей у ключей вне задачи и её тестов нет (`git grep`: только `newbuilding_enrich_backfill.py` + 2 теста). ## Сторож нулевого результата — проверено, не ослаблен `mark_backfill_finished` (#2695) судит по `attempted` / `enriched` / `gone` / `blocked`; сторож #2703 читает `total_seen`/`lots_fetched`/`unique_fetched`, которых у этой задачи нет вовсе. Числа записи в решение не входят ни до, ни после правки — и это закреплено тестами, а не рассуждением: прогон с `enriched=0` и `price_dynamics_updated=64` остаётся `failed`, а `price_dynamics_updated=999` не двигает вердикт. ## Test plan - [x] **Красный прогон на origin/main** — сценарий прода end-to-end (10 домов × 6 точек, второй проход по тем же домам): ``` === origin/main (без правки) === проход 1: счётчик= 60 записей писателем=60 проход 2: счётчик= 0 записей писателем=60 «обновлено»=None КРАСНО: второй проход переписал 60 точек по 10 домам и отчитался price_dynamics_rows=0 — прод-симптом прогона 3578. (exit=1) === ветка fix/2807 === проход 1: счётчик= 60 записей писателем=60 проход 2: счётчик= 0 записей писателем=60 «обновлено»=60 ЗЕЛЕНО: обновления больше не читаются как ноль. (exit=0) ``` - [x] `tests/test_2807_write_counters_honesty.py` — 8 тестов (вставка/обновление на 64 точках прода, пустой график остаётся нулём, точка без цены не считается записанной, 4 теста на неприкосновенность сторожа). На коде до правки модуль не собирается: `NewbuildingSaveCounts` не существует. - [x] Соседние: `test_newbuilding_enrich_backfill` + `test_2767` + `test_2725` + `test_scraper_kit_group_c_backfill_kit_parity` + `test_2703` — 75 passed. - [x] Полный `tests/` — 3737 passed; 11 падений идентичны падениям на origin/main (локальное окружение), новых нет. - [x] ruff + ruff-format. ## Прод-верификация (критерий записан ДО факта) `newbuilding_enrich` бежит ежесуточно ~00:28 UTC. **Первый прогон после деплоя** обязан показать в `scrape_runs.counters` **непустое** `price_dynamics_updated` при `price_dynamics_inserted` близком к нулю — потому что 301 из 301 fetchable-домов уже обогащены, и работа этого прогона по определению состоит из обновлений. Если оба поля нули при `succeeded > 0` — правка не доехала или писатель молчит. Closes #2807
bot-backend added 1 commit 2026-08-10 08:28:29 +00:00
fix(tradein/newbuilding): счётчики записи различают вставку и обновление (#2807)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m16s
f86a471557
`newbuilding_enrich_backfill` считал свою работу разницей COUNT(*) до и после
сохранения — то есть приростом ЧИСЛА СТРОК, а не числом записанных точек. Вставка в
houses_price_dynamics идёт ON CONFLICT DO UPDATE, поэтому переписывание существующей
точки давало ноль.

Прод 10.08: прогон 3578 отчитался price_dynamics_rows=0, обновив за своё окно 64 строки
по 10 домам — те самые, что вставил прогон 3563 накануне (у него в counters стояло 64).
Ноль читался как «динамика цен снова не пишется».

Соседние два счётчика в том же месте врали по своим причинам: reliability_rows обнулял
_dedup_reliability, схлопывающий строку сразу после вставки, а review_rows игнорировал
число, которое _save_cian_reviews уже возвращал, в пользу разницы COUNT'ов.

Различать вставку и обновление умеет только сам писатель: RETURNING (xmax = 0) —
идиома, уже применяемая в репозитории (services/scrapers/gisogd66.py). Поэтому
save_newbuilding_enrichment возвращает NewbuildingSaveCounts, а вызывающий перестал
восстанавливать число по таблице. Остальные три вызова результат игнорируют.

Ключи counters переименованы (price_dynamics_inserted/_updated, reliability_inserted,
review_upserted): у старых имён в истории прогонов другой смысл, и молча поменять его
под тем же именем — ровно тот дефект, ради которого правка и делается.

Сторож нулевого результата не затронут: mark_backfill_finished судит по
attempted/enriched/gone/blocked, а не по числам записи, — на это добавлен тест.
bot-backend merged commit 72472c2783 into main 2026-08-10 08:50:57 +00:00
bot-backend deleted branch fix/2807-price-dynamics-counter 2026-08-10 08:50:57 +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#2809
No description provided.