МЕРА: в истории цен Яндекса больше нет выдуманных «прошлых цен» в 10 и 1000 раз ниже #3564
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3564
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/yandex-price-decimal-slip"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#3385 — ×10 в yandex-истории цен
Что было. На проде 17.09.2026 у yandex 7 скачков ×10 в
offer_price_history(на 06.09 было 6, 02.09 прибавился10558950). Issue предполагал, что ошибочна верхняя точка и ловить её надо «на следующем наблюдении», когда цена вернётся к базе.Почему не так — проверено на проде (только SELECT):
price.previous:change_timeровно на 24 ч раньше второй точки,recorded_atу обеих один (один батч писателя). Ни одна серия к «базе» не вернулась.listings.price_rub, а10387563дальше дешевеет от неё же (14M → 13M → 12M).change_time = recorded_at), 20 245 — первые точки, 8 313 — вторые точки посева, 0 — из этой ветки. Сменуlistings.price_rubраньше пишет триггерrecord_listing_price_change(save_listings идёт первым), и писатель видитlatest == lot.price_rub. То есть отлов «на следующем наблюдении» вyandex_price_history.pyстоял бы в неисполняемой ветке и поймал бы 0 из 7.10 900 → 10 900 000,25 000 → 25 000 000,5 699 → 5 650 000,20 500 → 3 600 000: цена в тысячах / чужая сделка). Распределение отношений посевов: самый большой честный скачок ×2.13, между ×2.13 и ×9.74 пусто, «вниз» на порядок — 0.Что сделано.
tradein-mvp/backend/app/services/yandex_price_history.py: при пустой историиprice.previous, отличающаяся от текущей цены в 9.5 раза и больше в любую сторону (или не > 0), не сеется; текущая точка пишется как раньше. Счётчикseed_rejectedв итоговой строке лога + warning сsource_id. Посев синтетический, так что теряется только фиктивная точка.310_yandex_seed_decimal_slips.sql:DELETEyandex-посевов по подписи батча (s.recorded_at = o.recorded_at,s.change_time = o.change_time + interval '24 hours') и тому же порогу ≥ 9.5.SET LOCAL lock_timeout = '5s', остановка с откатом при > 30 строк. Тот же критерий SELECT-ом на проде 17.09 — ровно 11 строк (7 × 10 + 4 × 175…1000), id: 14295, 10353, 3435360, 8156846, 23142090, 23142518, 23144066, 23146347, 23146404, 23152732, 23174965.diff_percentу вторых строк у всех NULL (писатель его не заполняет) — пересчитывать нечего. Повторный прогон — 0.listings.price_rub, их 286 сознательно не трогает.Отклонение от плана триажа: он предлагал окно ×9.5–10.5 и 7 строк; с окном остались бы 4 посева ×175…×1000 того же происхождения, поэтому порог односторонний (≥ 9.5).
Тесты
uv run python -m pytest tests/test_yandex_price_history.py— 19 passed (новые: 4 отвергнутых посева на прод-ценах ×10/×9.74/×1000/÷10 → в истории одна текущая точка; 3 сохранённых ×1.06/×2.13/×9.45 → две точки).tests/test_3385_migration_310_yandex_seed_slips.py— настоящий файл миграции на живом Postgres (временнаяoffer_price_historyзатеняет реальную в сессии): удаляются только yandex-посевы ×10/×1000/÷10; остаются ×2.13, ×9.45, cian-посев, триггерные ×10 и более поздняя триггерная строка; второй прогон ничего не меняет; 31 пара → исключение и откат. Локально на postgres:16.4: 2 passed; проверено, что строки реальнойpublic.offer_price_historyпри этом не тронуты. Без БД — skip, записан вskip_allowlist.txt; в ci-tradein.yml бегут по-настоящему.psql -v ON_ERROR_STOP=on, как в деплое:удалено = 1, затем= 0, rc=0.DATABASE_URL=... uv run python -m pytest tests/ -q -p no:cacheprovider— 6232 passed, 44 skipped, rc=0.ruff check app tests— чисто;ruff format --checkпо изменённым — чисто;scripts/check-migration-lock-timeout.py— rc=0;test_migration_numbering.py— 3 passed.Фальсификация
_implausible_seed→return False):4 failed, 15 passed,assert 2 == 1у всех четырёхtest_first_capture_previous_off_by_order_is_not_seeded[...], rc=1.s.recorded_at = o.recorded_at):At index 10 diff: (7, 10000000) != (7, 1000000)— удалена триггерная строка, rc=1.BETWEEN 9.5 AND 10.5:At index 2 diff: (2, 10900) != (2, 10900000)— посев ×1000 выжил, rc=1.> 30→> 3000:Failed: DID NOT RAISE Exception, rc=1.Исходники восстановлены,
diff -qпуст; после восстановления 21 passed.Приёмка на проде
После деплоя (проверить не раньше, чем в логе деплоя будет
→ Applying migration: 310_yandex_seed_decimal_slips.sql):NOTICE: offer_price_history: удалено yandex-посевов на порядок = 11(больше 11 — только если до деплоя успели появиться новые посевы; > 30 — деплой красный, миграция откатилась).SELECT count(*) FROM offer_price_history o JOIN offer_price_history s ON s.source='yandex' AND s.listing_id=o.listing_id AND s.recorded_at=o.recorded_at AND s.change_time=o.change_time+interval '24 hours' WHERE o.source='yandex' AND greatest(o.price_rub,s.price_rub)/least(o.price_rub,s.price_rub) >= 9.5;→ 0, и запрос скачков ×10 по соседним yandex-точкам из issue → 0.yandex_city_sweepвdocker logs tradein-scraperстрокиyandex_price_history: lots=... seed_rejected=N(словаseed_rejectedв старом коде нет).Деплой пересоздаёт tradein-backend/tradein-scraper; перед мержем проверить
scrape_runs.status='running'.Closes #3385
🤖 Generated with Claude Code