fix(tradein): апсерт не переписывает неизменившуюся строку — гейт IS DISTINCT FROM + МСК-день (#2992) #3016
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#3016
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2992-upsert-distinct-gate"
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?
Что было
ON CONFLICT … DO UPDATE SETв апсертеlistingsбезусловно присваивал 41 колонку при каждом повторном скрейпе, включаяdescription = COALESCE(...)— заново тостил текст и плодил TOAST-чанки.listing_sources— тот же анти-паттерн.Замер прода 20.08 (тело задачи) и мой контроль 21.08 08:37 UTC совпали:
Гейт — два условия, при любом апдейт идёт
card_hashбыл забракован за то, что ломает дозаполнение.last_seen_atещё не сегодняшний по МСК — метка живости обязана сдвигаться хотя бы раз в сутки: деактиватор (TTL), снапшоты (is_active = last_seen_at > now()-7d), эстиматор (scraped_at > NOW()-14d, #2206), монитор. Это семантика уже существующегоskip_seen_today, но в SQL, race-free и для всех 18 путейsave_listings, а не трёхfull_load, к которымskip_seen_todayподключён (в этом и была причина 20 млн).При пропуске RETURNING пуст →
listing_idберётся из pre-read (в SELECT добавленid), downstream — снапшот, матчинг,listing_sources— идёт как прежде.listing_sourcesполучает симметричный гейт с тем же МСК-критерием — иначе разъехались быlistings.last_seen_atиlisting_sources.last_seen_at, равные сегодня у 100 % пар.Как проверено — на живом Postgres, двусторонне
test_2992_upsert_unchanged_gate.py— через реальныйsave_listings/upsert_listing_sourceк tradein-postgres (туннель; в CI Trade-In — postgres-сервис; без БД skip с причиной в allowlist). Свои строкиt2992-*удаляет вfinally.Измеритель —
ctidстроки. Первая редакция мерилаpg_stat_xact_user_tables.n_tup_updи давала «0 == 0» тавтологией:save_listingsкоммитит, транзакционные счётчики сбрасываются. Поймано на контроле «цена изменилась», где счётчик тоже показал 0. UPDATE всегда создаёт новую версию →ctidменяется; пропуск → тот же.Два контроля в первой редакции были сконструированы не через тот путь (город полем лота; лот без адреса — матчинг не зовётся и в первый раз) и краснели по своей вине, не по вине гейта — оба исправлены с проверкой «первый вызов проходит».
pytest tradein-mvp/backend— 4655 passed, 29 skipped, rc=0 (шесть live-тестов в allowlist).Что НЕ измерено и будет измерено — с датой
Эффект в числах:
n_tup_upd/сутки и WAL/сутки после деплоя против зафиксированного «до». Замер — 22.08 ~09:00 UTC (сутки после). Ожидание из модели: потолок экономии — повторные заходы в один МСК-день; за 10 минут утром 150 строк трогалось приn_tup_updбез движения, так что суточная картина — единственный честный срез.Известное ограничение: реконcile-ветка (drift
dedup_hash, прямойUPDATE … WHERE source, source_id) гейта не имеет — она редкая и по построению меняет строку.Часть #2992.