fix(db/ci): чистый старт БД не падает на 077 и не может уехать на пустой схеме #3011

Merged
bot-reviewer merged 3 commits from fix/2990-clean-start-initdb into main 2026-08-21 12:55:49 +00:00
Owner

Закрывает блокер переезда #2990. Эпик #2989.

Что было сломано

Чистый старт на пустом томе падал. 077_dedup_hash_plain_key_backfill.sql читает foreign table gendesign_rosreestr_deals, а USER MAPPING создаёт бэкенд при старте (app/core/fdw.py:57-62) — то есть после docker-entrypoint-initdb.d. На новом сервере контейнер не поднялся бы вообще.

Не всплывало потому, что путь «пустой том» на реальном железе не исполнялся ни разу, а CI этот файл явно пропускал — гейт, который должен был поймать, был ослаблен именно на нём.

Проверка диагноза

Прошёл все 14 миграций, упоминающих FDW-таблицы. Реально читает ровно одна — 077 (FROM gendesign_rosreestr_deals). Остальные (060, 066, 072, 085, 124, 168, 169 и др.) — только CREATE/DROP FOREIGN TABLE и COMMENT, которым ни USER MAPPING, ни связь с чужой БД не нужны. Так что точечного фикса достаточно.

Ревью нашло, что первая версия сама не чинила заявленный баг (deep-review BLOCK)

Первая версия гарда в 077 считала «pending» по source='rosreestr' AND dedup_hash ~ '^[0-9a-f]{32}$' и пропускала backfill, только если таких строк 0. Это было неверно: на чистой БД такие строки есть — 003_seed_deals.sql:18-19 сеет синтетические сделки, где source выбирается CASE WHEN random() < 0.6 THEN 'rosreestr' ELSE 'domklik' END, а dedup_hash — чистый md5(...). То есть pending > 0 уже на пустом томе, гард не срабатывал, и миграция всё равно падала на user mapping not found (воспроизведено в CI run 8257 — именно тот баг, который PR должен был закрыть).

Исправлено: первичный гард теперь проверяет напрямую наличие USER MAPPING для gendesign_remote (идиома из app/core/fdw.py:57-62, pg_user_mappings), а не количество строк:

IF NOT EXISTS (
    SELECT 1 FROM pg_user_mappings
    WHERE srvname = 'gendesign_remote'
      AND (usename = current_user OR usename IS NULL)
) THEN
    RAISE NOTICE '077: USER MAPPING для gendesign_remote нет — backfill пропущен';
    RETURN;
END IF;

Счётчик pending-строк оставлен вторым гардом (после проверки мэппинга) — просто чтобы не трогать FDW зря, когда мигрировать уже нечего.

Две поправки к плану из issue (не тронуты, ревью подтвердило верными)

077 не удалена. Issue предлагал удалить файл. Это сломало бы CI: есть гейт test_applied_migration_is_not_renamed_or_deleted, который падает на удалении применённой миграции — прод трекает их по bare-filename в _schema_migrations, и под новым именем миграция прогналась бы повторно.

Монтирование initdb.d оставлено. Issue предлагал перестать монтировать backend/data/sql в /docker-entrypoint-initdb.d. Но тогда на чистом сервере схему не создаст никто.

Что дополнительно закрыто (тоже переработано по ревью)

Проба готовности postgres теперь по TCP, а не по сокету

deploy-tradein.yml ждал postgres через pg_isready без -h — это unix-сокет. На пустом томе временный init-сервер отвечает на сокете, пока docker-entrypoint-initdb.d ещё прогоняет всю цепочку миграций (270+ файлов). Проба зеленела посреди initdb, а не после него — тот же класс проблемы, который уже задокументирован и исправлен в ci-tradein.yml:157 (pg_isready -h 127.0.0.1). Добавлен -h 127.0.0.1 и в deploy: TCP-порт открывается только когда initdb.d полностью отработал и postgres перезапустился как настоящий сервер. Заодно увеличен бюджет ожидания (30 → 90 попыток × 2с), потому что на пустом томе ожидание теперь включает полный прогон всей цепочки миграций, а не только старт postgres.

Sentinel baseline-детекции — по обоим концам цепочки, а не только по listings

Первая версия различала «схема уже накачена» одной таблицей listings (миграция 002 — почти голова цепочки). Это было недостаточно: если гонка готовности (см. выше) когда-нибудь вернётся, listings уже будет создан, а хвост цепочки — ещё нет, и baseline тихо пометит недостающие миграции применёнными без прогона (ровно тот сценарий из issue: 249+ миграций помечаются applied на частично применённой схеме).

Теперь проверяются оба конца: listings (голова) и houses_geog_gist_idx — GiST-индекс из миграции 270 (#2997, самая свежая на момент правки):

  • ни головы, ни хвоста → БД пуста, baseline пропускается, цепочка применяется циклом с нуля;
  • и голова, и хвост есть → baseline корректен (прод до tracking, либо чистый старт, где initdb.d уже честно доехал до конца — теперь это гарантирует фикс пробы готовности);
  • ровно один конец есть, второго нет → неоднозначное состояние, деплой падает громко (exit 1) вместо того, чтобы гадать и молча баселайнить недокачанную схему.

На текущем проде _schema_migrations уже существует, поэтому ни одна из трёх веток не задействуется и поведение обычного (не-clean-start) деплоя не меняется.

Проверено

  • Все 14 FDW-миграций разобраны вручную — читает только 077.
  • Оба workflow проходят yaml.safe_load + извлечённый bash-блок деплоя проходит bash -n.
  • Pre-commit зелёный.
  • Ветка отребейзена (мержем, без force-push) на актуальный main — приехали миграции 269/270 (в том числе индекс, ставший хвостовым сентинелом) и chmod +x deploy/*.sh, конфликтов не было.
  • CI Trade-In / backend-tests = success на текущем head (5m15s) — прогоняет полную цепочку миграций под ON_ERROR_STOP без исключений, то есть является репетицией чистого старта.

Что осталось за рамками

  • Генеральная репетиция на реальном железе с пустым томом — акцептанс #2990, делается на новом сервере до окна.
  • Перенос проверки невалидных индексов из деплоя в CI — отдельный пункт #2990, здесь не трогал.

Refs #2990, #2989

Закрывает блокер переезда #2990. Эпик #2989. ## Что было сломано Чистый старт на пустом томе падал. `077_dedup_hash_plain_key_backfill.sql` читает foreign table `gendesign_rosreestr_deals`, а `USER MAPPING` создаёт бэкенд при старте (`app/core/fdw.py:57-62`) — то есть **после** `docker-entrypoint-initdb.d`. На новом сервере контейнер не поднялся бы вообще. Не всплывало потому, что путь «пустой том» на реальном железе не исполнялся ни разу, а CI этот файл **явно пропускал** — гейт, который должен был поймать, был ослаблен именно на нём. ## Проверка диагноза Прошёл **все 14 миграций**, упоминающих FDW-таблицы. Реально читает ровно одна — 077 (`FROM gendesign_rosreestr_deals`). Остальные (060, 066, 072, 085, 124, 168, 169 и др.) — только `CREATE`/`DROP FOREIGN TABLE` и `COMMENT`, которым ни `USER MAPPING`, ни связь с чужой БД не нужны. Так что точечного фикса достаточно. ## Ревью нашло, что первая версия сама не чинила заявленный баг (deep-review BLOCK) Первая версия гарда в 077 считала «pending» по `source='rosreestr' AND dedup_hash ~ '^[0-9a-f]{32}$'` и пропускала backfill, только если таких строк 0. **Это было неверно**: на чистой БД такие строки есть — `003_seed_deals.sql:18-19` сеет синтетические сделки, где `source` выбирается `CASE WHEN random() < 0.6 THEN 'rosreestr' ELSE 'domklik' END`, а `dedup_hash` — чистый `md5(...)`. То есть `pending > 0` уже на пустом томе, гард не срабатывал, и миграция всё равно падала на `user mapping not found` (воспроизведено в CI run 8257 — именно тот баг, который PR должен был закрыть). Исправлено: первичный гард теперь проверяет напрямую **наличие USER MAPPING** для `gendesign_remote` (идиома из `app/core/fdw.py:57-62`, `pg_user_mappings`), а не количество строк: ```sql IF NOT EXISTS ( SELECT 1 FROM pg_user_mappings WHERE srvname = 'gendesign_remote' AND (usename = current_user OR usename IS NULL) ) THEN RAISE NOTICE '077: USER MAPPING для gendesign_remote нет — backfill пропущен'; RETURN; END IF; ``` Счётчик pending-строк оставлен вторым гардом (после проверки мэппинга) — просто чтобы не трогать FDW зря, когда мигрировать уже нечего. ## Две поправки к плану из issue (не тронуты, ревью подтвердило верными) **077 не удалена.** Issue предлагал удалить файл. Это сломало бы CI: есть гейт `test_applied_migration_is_not_renamed_or_deleted`, который падает на удалении применённой миграции — прод трекает их по bare-filename в `_schema_migrations`, и под новым именем миграция прогналась бы повторно. **Монтирование `initdb.d` оставлено.** Issue предлагал перестать монтировать `backend/data/sql` в `/docker-entrypoint-initdb.d`. Но тогда на чистом сервере схему не создаст никто. ## Что дополнительно закрыто (тоже переработано по ревью) ### Проба готовности postgres теперь по TCP, а не по сокету `deploy-tradein.yml` ждал postgres через `pg_isready` **без** `-h` — это unix-сокет. На пустом томе временный init-сервер отвечает на сокете, пока `docker-entrypoint-initdb.d` ещё прогоняет всю цепочку миграций (270+ файлов). Проба зеленела **посреди** initdb, а не после него — тот же класс проблемы, который уже задокументирован и исправлен в `ci-tradein.yml:157` (`pg_isready -h 127.0.0.1`). Добавлен `-h 127.0.0.1` и в deploy: TCP-порт открывается только когда initdb.d полностью отработал и postgres перезапустился как настоящий сервер. Заодно увеличен бюджет ожидания (30 → 90 попыток × 2с), потому что на пустом томе ожидание теперь включает полный прогон всей цепочки миграций, а не только старт postgres. ### Sentinel baseline-детекции — по обоим концам цепочки, а не только по listings Первая версия различала «схема уже накачена» одной таблицей `listings` (миграция 002 — почти голова цепочки). Это было недостаточно: если гонка готовности (см. выше) когда-нибудь вернётся, `listings` уже будет создан, а хвост цепочки — ещё нет, и baseline тихо пометит недостающие миграции применёнными без прогона (ровно тот сценарий из issue: 249+ миграций помечаются applied на частично применённой схеме). Теперь проверяются оба конца: `listings` (голова) и `houses_geog_gist_idx` — GiST-индекс из миграции 270 (#2997, самая свежая на момент правки): - ни головы, ни хвоста → БД пуста, baseline пропускается, цепочка применяется циклом с нуля; - и голова, и хвост есть → baseline корректен (прод до tracking, либо чистый старт, где initdb.d уже честно доехал до конца — теперь это гарантирует фикс пробы готовности); - ровно один конец есть, второго нет → **неоднозначное состояние, деплой падает громко** (`exit 1`) вместо того, чтобы гадать и молча баселайнить недокачанную схему. На текущем проде `_schema_migrations` уже существует, поэтому ни одна из трёх веток не задействуется и поведение обычного (не-clean-start) деплоя не меняется. ## Проверено - Все 14 FDW-миграций разобраны вручную — читает только 077. - Оба workflow проходят `yaml.safe_load` + извлечённый bash-блок деплоя проходит `bash -n`. - Pre-commit зелёный. - Ветка отребейзена (мержем, без force-push) на актуальный `main` — приехали миграции 269/270 (в том числе индекс, ставший хвостовым сентинелом) и `chmod +x deploy/*.sh`, конфликтов не было. - **CI Trade-In / backend-tests = success** на текущем head (5m15s) — прогоняет полную цепочку миграций под `ON_ERROR_STOP` без исключений, то есть является репетицией чистого старта. ## Что осталось за рамками - Генеральная репетиция на реальном железе с пустым томом — акцептанс #2990, делается на новом сервере до окна. - Перенос проверки невалидных индексов из деплоя в CI — отдельный пункт #2990, здесь не трогал. Refs #2990, #2989
lekss361 added 1 commit 2026-08-20 20:32:25 +00:00
fix(db/ci): чистый старт БД больше не падает на 077 и не может уехать на пустой схеме
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Failing after 55s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m3s
CI Trade-In / frontend-checks (pull_request) Successful in 1m47s
CI / openapi-codegen-check (pull_request) Successful in 2m26s
CI / backend-tests (pull_request) Successful in 17m51s
efc965a257
Блокер переезда (#2990). Чистый старт на пустом томе падал: 077 читает foreign
table gendesign_rosreestr_deals, а USER MAPPING создаёт бэкенд при старте
(app/core/fdw.py), то есть ПОСЛЕ docker-entrypoint-initdb.d. Контейнер не
поднимался вообще. Путь «пустой том» на реальном железе не исполнялся ни разу,
а CI этот файл явно пропускал — гейт, который должен был поймать, был ослаблен.

Проверено по всем 14 миграциям, упоминающим FDW-таблицы: читает ровно одна —
077. Остальные только CREATE/DROP FOREIGN TABLE и COMMENT, им ни USER MAPPING,
ни связь с чужой БД не нужны.

077 не удалена, а сделана самозащитной: гард считает строки в md5-форме и
выходит раньше обращения к FDW, если мигрировать нечего. На чистой БД таких
строк нет по определению. Удаление файла было бы неверным — прод помнит
миграции по bare-filename в _schema_migrations, и
test_applied_migration_is_not_renamed_or_deleted падает на удалении.

Исключение в ci-tradein.yml снято: теперь цепочка применяется целиком, то есть
CI сам стал репетицией чистого старта.

Отдельно закрыт тихий отказ в deploy-tradein.yml. Ветка baseline срабатывала по
одному лишь отсутствию _schema_migrations, а это состояние неоднозначно: так
выглядит и наполненный прод до внедрения tracking, и пустая БД нового сервера.
Во втором случае baseline пометил бы все миграции применёнными, ни одной не
прогнав, и деплой уехал бы зелёным на пустой схеме. Добавлен sentinel по
listings: пусто → baseline пропускается, цепочка применяется с нуля.

Refs #2990, #2989
Author
Owner

CI красный, и падение содержательное — гейт поймал, что гард не работает. Разбор, чтобы не искать заново.

Что показал прогон

Лог CI Trade-In / backend-tests, run 8257:

NOTICE:  077: строк к конвертации: 51
ERROR:  user mapping not found for "tradein"
::error::миграция 077_dedup_hash_plain_key_backfill.sql не применилась

Почему гард не сработал

Он опирается на посылку из комментария в самой миграции:

На чистой БД строк в md5-форме нет по определению (их создавал исторический импорт)

Эта посылка неверна, и CI это доказал: на свежей базе, собранной прогоном всей цепочки с нуля, таких строк 51. Значит какая-то из более ранних миграций засевает deals строками с dedup_hash в md5-форме. Гард считает pending = 51, не выходит, доходит до gendesign_rosreestr_deals — и падает на отсутствующем USER MAPPING.

То есть проверяется не то условие. «Есть ли что мигрировать» и «доступен ли FDW» — разные вопросы, а падение вызывает именно второй.

Что предлагаю

Гардить по фактической доступности FDW, а не по числу строк. Реальное предусловие миграции — «USER MAPPING существует», и его можно проверить в каталоге, не трогая foreign table:

IF NOT EXISTS (
    SELECT 1 FROM pg_user_mappings
    WHERE srvname = 'gendesign_remote'
      AND (usename = current_user OR usename IS NULL)  -- PUBLIC-маппинг тоже годится
) THEN
    RAISE NOTICE '077: USER MAPPING для gendesign_remote отсутствует — backfill пропущен';
    RETURN;
END IF;

Существующий гард по pending при этом стоит оставить — он полезен сам по себе как ранний выход, просто он не защищает от чистого старта. Порядок: сначала проверка маппинга, потом pending.

Запасной вариант, если проверка каталога окажется неточной в каком-то краевом случае, — обернуть чтение foreign table в блок с EXCEPTION WHEN OTHERS и трактовать сбой как пропуск. Менее прицельно, но переживает любую причину недоступности FDW, а не только отсутствующий маппинг.

Что в этом PR хорошо и должно остаться

Часовой в deploy-tradein.yml — самая ценная часть, и она тут вне подозрений. Различение «наполненный прод без tracking» и «пустая БД» по живой таблице listings закрывает сценарий, где baseline помечает все миграции применёнными на пустой схеме и деплой уезжает зелёным. Это тихий отказ, который обнаружился бы уже под нагрузкой, и его в моей исходной постановке (#2990) не было — находка сверх задачи.

Снятие исключения из CI тоже правильно и уже окупилось: ровно оно и вскрыло, что гард не работает. Возвращать исключение не надо — надо чинить гард.

Смежное: эпик #2989, где чистый старт БД — предусловие переезда.

CI красный, и падение содержательное — гейт поймал, что гард не работает. Разбор, чтобы не искать заново. ## Что показал прогон Лог `CI Trade-In / backend-tests`, run 8257: ``` NOTICE: 077: строк к конвертации: 51 ERROR: user mapping not found for "tradein" ::error::миграция 077_dedup_hash_plain_key_backfill.sql не применилась ``` ## Почему гард не сработал Он опирается на посылку из комментария в самой миграции: > На чистой БД строк в md5-форме нет по определению (их создавал исторический импорт) **Эта посылка неверна, и CI это доказал:** на свежей базе, собранной прогоном всей цепочки с нуля, таких строк **51**. Значит какая-то из более ранних миграций засевает `deals` строками с `dedup_hash` в md5-форме. Гард считает `pending = 51`, не выходит, доходит до `gendesign_rosreestr_deals` — и падает на отсутствующем `USER MAPPING`. То есть проверяется не то условие. «Есть ли что мигрировать» и «доступен ли FDW» — разные вопросы, а падение вызывает именно второй. ## Что предлагаю Гардить по **фактической доступности FDW**, а не по числу строк. Реальное предусловие миграции — «USER MAPPING существует», и его можно проверить в каталоге, не трогая foreign table: ```sql IF NOT EXISTS ( SELECT 1 FROM pg_user_mappings WHERE srvname = 'gendesign_remote' AND (usename = current_user OR usename IS NULL) -- PUBLIC-маппинг тоже годится ) THEN RAISE NOTICE '077: USER MAPPING для gendesign_remote отсутствует — backfill пропущен'; RETURN; END IF; ``` Существующий гард по `pending` при этом стоит **оставить** — он полезен сам по себе как ранний выход, просто он не защищает от чистого старта. Порядок: сначала проверка маппинга, потом `pending`. Запасной вариант, если проверка каталога окажется неточной в каком-то краевом случае, — обернуть чтение foreign table в блок с `EXCEPTION WHEN OTHERS` и трактовать сбой как пропуск. Менее прицельно, но переживает любую причину недоступности FDW, а не только отсутствующий маппинг. ## Что в этом PR хорошо и должно остаться **Часовой в `deploy-tradein.yml` — самая ценная часть, и она тут вне подозрений.** Различение «наполненный прод без tracking» и «пустая БД» по живой таблице `listings` закрывает сценарий, где baseline помечает все миграции применёнными на пустой схеме и деплой уезжает зелёным. Это тихий отказ, который обнаружился бы уже под нагрузкой, и его в моей исходной постановке (#2990) не было — находка сверх задачи. **Снятие исключения из CI тоже правильно** и уже окупилось: ровно оно и вскрыло, что гард не работает. Возвращать исключение не надо — надо чинить гард. Смежное: эпик #2989, где чистый старт БД — предусловие переезда.
bot-backend added 2 commits 2026-08-21 12:36:00 +00:00
Deep-review BLOCK на PR #3011: обе правки чинили заявленный симптом только
частично.

077: гард считал pending-строки по source='rosreestr' AND dedup_hash ~
md5-паттерн и пропускал backfill, только если таких строк 0. На чистой БД
они есть — 003_seed_deals.sql сеет синтетические сделки с тем же паттерном,
значит pending > 0 уже на пустом томе, и миграция всё равно падала на
"user mapping not found" (воспроизведено в CI run 8257). Первичный гард
теперь проверяет напрямую наличие USER MAPPING для gendesign_remote
(идиома из app/core/fdw.py:57-62), счётчик pending оставлен вторым —
экономит обращение к FDW, когда мигрировать уже нечего.

deploy-tradein.yml: цикл ожидания готовности postgres ходил по unix-сокету
(pg_isready без -h). На пустом томе временный init-сервер отвечает на
сокете, пока docker-entrypoint-initdb.d ещё прогоняет цепочку миграций —
проба зеленела посреди initdb. Добавлен -h 127.0.0.1 (тот же приём уже
есть в ci-tradein.yml:157) — TCP открывается только после полного
завершения initdb.d.

Отдельно ужесточён sentinel baseline-детекции: раньше «схема уже накачена»
проверялась одной таблицей listings (миграция 002, почти голова цепочки).
Если бы гонка готовности когда-нибудь вернулась, listings был бы уже
создан, а хвост цепочки — ещё нет, и baseline тихо пометил бы недостающие
миграции применёнными без прогона. Теперь проверяются оба конца — listings
(голова) и houses_geog_gist_idx, индекс из миграции 270 (хвост); при
несовпадении (ровно один конец на месте) деплой падает громко с explicit
ошибкой вместо угадывания.
Merge remote-tracking branch 'forgejo/main' into fix/2990-clean-start-initdb
All checks were successful
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Successful in 57s
CI Trade-In / frontend-checks (pull_request) Successful in 1m40s
CI / openapi-codegen-check (pull_request) Successful in 2m45s
CI Trade-In / backend-tests (pull_request) Successful in 5m15s
CI / backend-tests (pull_request) Successful in 18m13s
4cb8f32dcc
bot-reviewer merged commit e31c537205 into main 2026-08-21 12:55:49 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3011
No description provided.