Вернуть снос дубля индекса trade_in_estimates(expires_at) — теперь с lock_timeout #2793

Closed
opened 2026-08-07 11:28:48 +00:00 by bot-backend · 2 comments
Collaborator

Догоняющая задача к #2752. Миграция 250_drop_duplicate_expires_at_index.sql была снята с деплоя (#2792), потому что её DROP INDEX вставал в очередь за чужой аналитической сессией и блокировал весь пайплайн trade-in (29 и 16 минут ожидания, четыре смерженных PR не доехали до прода). Конвенция lock_timeout и гейт уже на месте (#2791) — осталось вернуть сам снос.

Состояние на 2026-08-07 14:30 MSK

  • В _schema_migrations записи о 250 нет — миграция не применялась ни разу.
  • Оба индекса на месте: trade_in_estimates_expires_idx (idx_scan 234, заморожен), trade_in_estimates_expires_at_idx (idx_scan 19, забирает весь живой трафик).
  • Держатель лока — чужой ручной бэктест pid 83256 (CREATE TEMP TABLE tmp_res AS ...), на момент снятия шёл 2 ч 05 мин. Не трогали.
  • Выигрыш от сноса: 48 КБ. Операционно — ничего. Срочности нет.

Что сделать

Вернуть файл (номер 250 свободен, в _manifest_applied.txt его нет) — полный текст с разбором лежит в истории: git show a9096f12:tradein-mvp/backend/data/sql/250_drop_duplicate_expires_at_index.sql. Дописать к нему раздел про инцидент с очередью и строку таймаута:

BEGIN;

-- Ограничивает ОЖИДАНИЕ лока, не работу под ним (обоснование 5 s — в шапке
-- и в .claude/rules/sql.md § lock_timeout).
SET LOCAL lock_timeout = '5s';

DROP INDEX IF EXISTS trade_in_estimates_expires_at_idx;

COMMENT ON INDEX trade_in_estimates_expires_idx IS '...';

COMMIT;

Без строки SET LOCAL гейт scripts/check-migration-lock-timeout.py уронит CI — это и есть его первый настоящий вход.

Критерий «таблица тиха» (записан ДО, а не после)

Мержить, когда на боевой БД:

SELECT count(*) FROM pg_locks l
  JOIN pg_class c ON c.oid = l.relation
 WHERE c.relname = 'trade_in_estimates' AND l.pid <> pg_backend_pid();  -- → 0

Критерий приёмки после применения

  1. SELECT * FROM _schema_migrations WHERE filename = '250_drop_duplicate_expires_at_index.sql'; — запись есть (а не «деплой зелёный»).
  2. pg_stat_user_indexes.idx_scan у trade_in_estimates_expires_idx сдвинулся с 234 в течение часа. Если остался 234 — трафик ушёл в Seq Scan, разбор #2752 опровергнут, откатывать.

Если миграция снова упрётся в чужую сессию, деплой теперь падает через 5 секунд с lock timeout вместо получасовой очереди — это ожидаемое поведение, просто повторить позже.

Refs #2752

Догоняющая задача к #2752. Миграция `250_drop_duplicate_expires_at_index.sql` была снята с деплоя (#2792), потому что её `DROP INDEX` вставал в очередь за чужой аналитической сессией и блокировал весь пайплайн trade-in (29 и 16 минут ожидания, четыре смерженных PR не доехали до прода). Конвенция `lock_timeout` и гейт уже на месте (#2791) — осталось вернуть сам снос. ## Состояние на 2026-08-07 14:30 MSK - В `_schema_migrations` записи о 250 **нет** — миграция не применялась ни разу. - Оба индекса на месте: `trade_in_estimates_expires_idx` (idx_scan 234, заморожен), `trade_in_estimates_expires_at_idx` (idx_scan 19, забирает весь живой трафик). - Держатель лока — чужой ручной бэктест pid 83256 (`CREATE TEMP TABLE tmp_res AS ...`), на момент снятия шёл 2 ч 05 мин. Не трогали. - Выигрыш от сноса: 48 КБ. Операционно — ничего. Срочности нет. ## Что сделать Вернуть файл (номер 250 свободен, в `_manifest_applied.txt` его нет) — полный текст с разбором лежит в истории: `git show a9096f12:tradein-mvp/backend/data/sql/250_drop_duplicate_expires_at_index.sql`. Дописать к нему раздел про инцидент с очередью и строку таймаута: ```sql BEGIN; -- Ограничивает ОЖИДАНИЕ лока, не работу под ним (обоснование 5 s — в шапке -- и в .claude/rules/sql.md § lock_timeout). SET LOCAL lock_timeout = '5s'; DROP INDEX IF EXISTS trade_in_estimates_expires_at_idx; COMMENT ON INDEX trade_in_estimates_expires_idx IS '...'; COMMIT; ``` Без строки `SET LOCAL` гейт `scripts/check-migration-lock-timeout.py` уронит CI — это и есть его первый настоящий вход. ## Критерий «таблица тиха» (записан ДО, а не после) Мержить, когда на боевой БД: ```sql SELECT count(*) FROM pg_locks l JOIN pg_class c ON c.oid = l.relation WHERE c.relname = 'trade_in_estimates' AND l.pid <> pg_backend_pid(); -- → 0 ``` ## Критерий приёмки после применения 1. `SELECT * FROM _schema_migrations WHERE filename = '250_drop_duplicate_expires_at_index.sql';` — запись есть (а не «деплой зелёный»). 2. `pg_stat_user_indexes.idx_scan` у `trade_in_estimates_expires_idx` **сдвинулся** с 234 в течение часа. Если остался 234 — трафик ушёл в Seq Scan, разбор #2752 опровергнут, откатывать. Если миграция снова упрётся в чужую сессию, деплой теперь падает через 5 секунд с `lock timeout` вместо получасовой очереди — это ожидаемое поведение, просто повторить позже. Refs #2752
Author
Collaborator

Working on this in PR #2795 — миграция 250 возвращена с SET LOCAL lock_timeout = '5s'. Критерий «таблица тиха» перепроверен на проде 2026-08-09 16:54 UTC: pg_locks по trade_in_estimates = 0, долгих транзакций в БД нет. Уточнение к критерию приёмки: наблюдаемый темп сканов ~7/сутки, поэтому «сдвиг idx_scan в течение часа» — недостаточное окно; детерминированная проверка сразу после деплоя — EXPLAIN с именем пережившего индекса, счётчик подтверждается за сутки.

Working on this in PR #2795 — миграция 250 возвращена с `SET LOCAL lock_timeout = '5s'`. Критерий «таблица тиха» перепроверен на проде 2026-08-09 16:54 UTC: pg_locks по trade_in_estimates = 0, долгих транзакций в БД нет. Уточнение к критерию приёмки: наблюдаемый темп сканов ~7/сутки, поэтому «сдвиг idx_scan в течение часа» — недостаточное окно; детерминированная проверка сразу после деплоя — EXPLAIN с именем пережившего индекса, счётчик подтверждается за сутки.
Author
Collaborator

Прод-верификация, PR #2795 смержен 17:10:18 UTC.

Запись в _schema_migrations (а не «деплой зелёный»): 250_drop_duplicate_expires_at_index.sql — applied_at 2026-08-09 17:15:10 UTC.

Индексы после: остался один, trade_in_estimates_expires_idx, 48 kB. Дубля нет. COMMENT перенесён.

EXPLAIN до/после (тот же запрос, прод):

ДО:     Index Scan using trade_in_estimates_expires_at_idx  (cost=0.28..623.07 rows=1061)
ПОСЛЕ:  Index Scan using trade_in_estimates_expires_idx     (cost=0.28..627.07 rows=1061)

Форма плана и Index Cond идентичны; отличается имя и +4 cost — ровно предсказанный спуск по одной лишней странице (relpages 6 против 5). План боевого запроса purge не изменился вовсе: Bitmap Index Scan on idx_trade_in_estimates_created_by_created_at — то есть опровержение мотивировки 229 держится и после сноса.

Критерий №2 (счётчик сдвинулся). idx_scan пережившего был заморожен на 234 трое суток. Через 41 минуту после применения — 236. Ветка «трафик ушёл в Seq Scan» опровергнута, откат не нужен.

При этом оговорка в PR подтвердилась как разумная: темп ~7 сканов/сутки, так что «в течение часа» дало сдвиг всего на +2 — при менее удачном тайминге час мог бы дать 0 и это ничего не значило бы.

Невалидных индексов в БД: 0.

Гейт scripts/check-migration-lock-timeout.py отработал на своём первом настоящем входе; негативный контроль (тот же файл без строки SET LOCAL) даёт exit 1. Ожидание лока не потребовалось — на момент применения блокировок по таблице было 0.

Закрываю: всё, что было в задаче, применено и проверено числом.

Прод-верификация, PR #2795 смержен 17:10:18 UTC. **Запись в _schema_migrations** (а не «деплой зелёный»): `250_drop_duplicate_expires_at_index.sql` — applied_at 2026-08-09 17:15:10 UTC. **Индексы после:** остался один, `trade_in_estimates_expires_idx`, 48 kB. Дубля нет. COMMENT перенесён. **EXPLAIN до/после** (тот же запрос, прод): ``` ДО: Index Scan using trade_in_estimates_expires_at_idx (cost=0.28..623.07 rows=1061) ПОСЛЕ: Index Scan using trade_in_estimates_expires_idx (cost=0.28..627.07 rows=1061) ``` Форма плана и Index Cond идентичны; отличается имя и +4 cost — ровно предсказанный спуск по одной лишней странице (relpages 6 против 5). План боевого запроса purge не изменился вовсе: Bitmap Index Scan on idx_trade_in_estimates_created_by_created_at — то есть опровержение мотивировки 229 держится и после сноса. **Критерий №2 (счётчик сдвинулся).** idx_scan пережившего был заморожен на 234 трое суток. Через 41 минуту после применения — **236**. Ветка «трафик ушёл в Seq Scan» опровергнута, откат не нужен. При этом оговорка в PR подтвердилась как разумная: темп ~7 сканов/сутки, так что «в течение часа» дало сдвиг всего на +2 — при менее удачном тайминге час мог бы дать 0 и это ничего не значило бы. **Невалидных индексов в БД:** 0. Гейт `scripts/check-migration-lock-timeout.py` отработал на своём первом настоящем входе; негативный контроль (тот же файл без строки SET LOCAL) даёт exit 1. Ожидание лока не потребовалось — на момент применения блокировок по таблице было 0. Закрываю: всё, что было в задаче, применено и проверено числом.
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#2793
No description provided.