Merge pull request 'fix(db/ci): чистый старт БД не падает на 077 и не может уехать на пустой схеме' (#3011) from fix/2990-clean-start-initdb into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Successful in 44s
Deploy Trade-In / build-frontend (push) Successful in 2m25s
Deploy Trade-In / test (push) Successful in 3m36s
Deploy Trade-In / build-backend (push) Successful in 49s
Deploy Trade-In / deploy (push) Successful in 1m53s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Successful in 44s
Deploy Trade-In / build-frontend (push) Successful in 2m25s
Deploy Trade-In / test (push) Successful in 3m36s
Deploy Trade-In / build-backend (push) Successful in 49s
Deploy Trade-In / deploy (push) Successful in 1m53s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
This commit is contained in:
commit
e31c537205
3 changed files with 165 additions and 40 deletions
|
|
@ -179,16 +179,13 @@ jobs:
|
|||
"CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE ROLE gendesign_reader;"
|
||||
# Исключений НЕТ (#2990). Раньше здесь пропускалась 077 — единственная
|
||||
# миграция, читающая foreign table через postgres_fdw, которой в CI нет.
|
||||
# Пропуск означал, что гейт не проверял ровно тот файл, который потом
|
||||
# ронял чистый старт на реальном железе. Теперь 077 сама выходит раньше
|
||||
# обращения к FDW, если мигрировать нечего, и в CI проходит честно.
|
||||
for sql_file in $(ls -1 tradein-mvp/backend/data/sql/*.sql | sort); do
|
||||
fname=$(basename "$sql_file")
|
||||
# ЕДИНСТВЕННОЕ исключение, и оно названо вслух: 077 — не DDL, а
|
||||
# backfill, читающий foreign table gendesign_rosreestr_deals из БД
|
||||
# ДРУГОГО стека через postgres_fdw. В CI второй БД нет, USER MAPPING
|
||||
# создать не из чего. На пустых таблицах backfill всё равно no-op.
|
||||
if [ "$fname" = "077_dedup_hash_plain_key_backfill.sql" ]; then
|
||||
echo "⚠ пропускаю $fname — postgres_fdw к БД gendesign, которой в CI нет"
|
||||
continue
|
||||
fi
|
||||
docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q < "$sql_file" \
|
||||
|| { echo "::error::миграция $fname не применилась"; docker logs --tail 20 "$CI_PG" 2>&1 || true; exit 1; }
|
||||
done
|
||||
|
|
|
|||
|
|
@ -747,11 +747,29 @@ jobs:
|
|||
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps postgres
|
||||
|
||||
# (2) Ждём готовности postgres (pg_isready в цикле, НЕ тупой sleep).
|
||||
#
|
||||
# `-h 127.0.0.1` ОБЯЗАТЕЛЕН (#2990) — по тому же образцу, что уже в
|
||||
# ci-tradein.yml:157. На пустом томе образ postgres поднимает
|
||||
# ВРЕМЕННЫЙ сервер с listen_addresses='' на время прогона
|
||||
# docker-entrypoint-initdb.d (сюда смонтирован весь
|
||||
# backend/data/sql/*.sql, см. docker-compose.prod.yml). Этот временный
|
||||
# сервер отвечает "accepting connections" по unix-сокету уже через
|
||||
# пару секунд — а pg_isready БЕЗ -h ходит именно по сокету через
|
||||
# `docker compose exec`. Проба зеленела посреди initdb, до того как
|
||||
# цепочка миграций реально доехала до конца, и код ниже (детект
|
||||
# baseline vs пустая БД) видел частично накаченную схему. TCP-порт
|
||||
# 5432 открывается только когда initdb.d полностью отработал и
|
||||
# postgres перезапустился как настоящий сервер — проба по 127.0.0.1
|
||||
# зеленеет ровно тогда, когда БД реально готова.
|
||||
#
|
||||
# 90 попыток × 2с = до 3 минут: на пустом томе postgres прогоняет
|
||||
# ВСЮ цепочку миграций (270+ файлов) внутри initdb, это медленнее,
|
||||
# чем ожидание живого сервера на непустом томе (обычный деплой).
|
||||
echo "→ Ожидание готовности postgres..."
|
||||
pg_ready=""
|
||||
for i in $(seq 1 30); do
|
||||
for i in $(seq 1 90); do
|
||||
if docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
pg_isready -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein >/dev/null 2>&1; then
|
||||
pg_isready -h 127.0.0.1 -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein >/dev/null 2>&1; then
|
||||
pg_ready="yes"; break
|
||||
fi
|
||||
sleep 2
|
||||
|
|
@ -782,6 +800,35 @@ jobs:
|
|||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \
|
||||
"SELECT to_regclass('public._schema_migrations') IS NOT NULL;" | tr -d '[:space:]')
|
||||
|
||||
# Sentinel (#2990): отсутствия _schema_migrations НЕДОСТАТОЧНО, чтобы
|
||||
# заключить «схема уже накачена». Ровно два разных состояния дают одно
|
||||
# и то же отсутствие таблицы:
|
||||
# 1) наполненный прод до внедрения tracking → baseline корректен;
|
||||
# 2) ПУСТАЯ БД на новом сервере → baseline пометил бы все миграции
|
||||
# применёнными, ни одной не прогнав, и деплой уехал бы зелёным
|
||||
# на пустой схеме. Отказ тихий и обнаружился бы уже под нагрузкой.
|
||||
#
|
||||
# Раньше различали одной живой таблицей listings — она создаётся
|
||||
# миграцией 002, то есть почти в САМОМ НАЧАЛЕ цепочки. Этого мало: на
|
||||
# пустом томе до фикса ожидания готовности (см. выше, -h 127.0.0.1)
|
||||
# проба зеленела ПОСРЕДИ initdb, когда listings уже создан, а хвост
|
||||
# цепочки — ещё нет; результат — тихий baseline недокачанной схемы.
|
||||
# Фикс готовности эту гонку убирает (TCP открывается только после
|
||||
# полного прохода initdb.d), но сентинел всё равно проверяем по ОБОИМ
|
||||
# концам цепочки как defense-in-depth: если голова и хвост когда-нибудь
|
||||
# разъедутся — это тот самый гоночный симптом, и его надо ловить явно,
|
||||
# а не гадать.
|
||||
#
|
||||
# Хвост — houses_geog_gist_idx, индекс из миграции 270 (#2997, самая
|
||||
# свежая на момент правки #2990). При добавлении новых миграций после
|
||||
# 270 обнови этот сентинел на объект из новой последней миграции.
|
||||
schema_head_present=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \
|
||||
"SELECT to_regclass('public.listings') IS NOT NULL;" | tr -d '[:space:]')
|
||||
schema_tail_present=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \
|
||||
"SELECT to_regclass('public.houses_geog_gist_idx') IS NOT NULL;" | tr -d '[:space:]')
|
||||
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on -c "
|
||||
CREATE TABLE IF NOT EXISTS _schema_migrations (
|
||||
|
|
@ -790,12 +837,23 @@ jobs:
|
|||
);
|
||||
"
|
||||
|
||||
if [ "$migrations_table_existed" != "t" ]; then
|
||||
# BASELINE: таблицы не было → seed ВСЕ текущие миграции как applied
|
||||
# БЕЗ их прогона. prod уже работает на этой схеме; помечаем её
|
||||
# текущим состоянием, чтобы под строгий gate попадали только НОВЫЕ
|
||||
# (077+) миграции. INSERT ... ON CONFLICT DO NOTHING — идемпотентно.
|
||||
echo "→ _schema_migrations отсутствовала — baseline существующих миграций (без прогона)"
|
||||
if [ "$migrations_table_existed" != "t" ] && [ "$schema_head_present" != "t" ] && [ "$schema_tail_present" != "t" ]; then
|
||||
# ПУСТАЯ БД: ни головы, ни хвоста цепочки — baseline пропускаем
|
||||
# намеренно. Цикл ниже применит всю цепочку с нуля под
|
||||
# ON_ERROR_STOP — это и есть штатный путь чистого старта на новом
|
||||
# сервере (initdb.d уже должен был всё применить сам; этот цикл —
|
||||
# подстраховка на случай, если монтирование почему-то не сработало).
|
||||
echo "→ БД пуста (нет ни _schema_migrations, ни listings, ни хвоста цепочки) — baseline ПРОПУЩЕН,"
|
||||
echo " вся цепочка миграций будет применена циклом ниже."
|
||||
elif [ "$migrations_table_existed" != "t" ] && [ "$schema_head_present" = "t" ] && [ "$schema_tail_present" = "t" ]; then
|
||||
# BASELINE: таблицы не было, но и голова, и хвост цепочки на месте →
|
||||
# seed ВСЕ текущие миграции как applied БЕЗ их прогона. Это либо
|
||||
# наполненный прод до внедрения tracking, либо чистый старт, где
|
||||
# initdb.d уже честно доехал до конца сам (ожидание готовности это
|
||||
# теперь гарантирует). В обоих случаях повторный прогон не нужен —
|
||||
# помечаем текущим состоянием, чтобы под строгий gate попадали
|
||||
# только НОВЫЕ миграции. INSERT ... ON CONFLICT DO NOTHING — идемпотентно.
|
||||
echo "→ _schema_migrations отсутствовала, но схема на месте целиком (голова + хвост) — baseline существующих миграций (без прогона)"
|
||||
for sql_file in $(ls -1 backend/data/sql/*.sql 2>/dev/null | sort); do
|
||||
fname=$(basename "$sql_file")
|
||||
echo " baseline: $fname"
|
||||
|
|
@ -804,6 +862,21 @@ jobs:
|
|||
"INSERT INTO _schema_migrations (filename) VALUES ('$fname') ON CONFLICT DO NOTHING;"
|
||||
done
|
||||
echo "Baseline complete — existing schema marked as applied."
|
||||
elif [ "$migrations_table_existed" != "t" ]; then
|
||||
# НЕОДНОЗНАЧНО: ровно один из концов цепочки на месте, второго нет,
|
||||
# а _schema_migrations отсутствует. Это и есть симптом гонки
|
||||
# готовности (см. комментарий выше) — молча баселайнить тут нельзя:
|
||||
# либо схема реально недокачана (baseline пометил бы недостающий
|
||||
# хвост как применённый без прогона), либо и то и другое пусто, но
|
||||
# тогда tail-check не должен был сработать. Падаем громко, а не
|
||||
# гадаем — новый app-код НЕ поднят, старые контейнеры не тронуты.
|
||||
echo "ERROR: неоднозначное состояние схемы — _schema_migrations нет,"
|
||||
echo " listings присутствует=${schema_head_present}, houses_geog_gist_idx присутствует=${schema_tail_present}."
|
||||
echo " Похоже на недокачанную схему (гонка готовности postgres) —"
|
||||
echo " baseline пропущен намеренно, чтобы не пометить недостающие"
|
||||
echo " миграции применёнными без прогона. Прерываю деплой; нужен"
|
||||
echo " ручной разбор состояния тома перед повторным запуском."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for sql_file in $(ls -1 backend/data/sql/*.sql 2>/dev/null | sort); do
|
||||
|
|
|
|||
|
|
@ -13,10 +13,26 @@
|
|||
-- не могут совпасть с md5-hex (разный формат), поэтому транзиентного UNIQUE-violation
|
||||
-- по deals_dedup_hash_key во время UPDATE не возникает.
|
||||
--
|
||||
-- FDW dependency: читает foreign table gendesign_rosreestr_deals (SERVER gendesign_remote,
|
||||
-- создан в 060_postgres_fdw_extension.sql; foreign table — в 072_..._cian_rosreestr.sql;
|
||||
-- USER MAPPING — backend startup core/fdw.py). Деплой применяет миграции ПОСЛЕ `compose up -d`,
|
||||
-- т.е. FDW-сервер уже поднят и foreign table читаема.
|
||||
-- ── Гард чистого старта (#2990) ───────────────────────────────────────────────
|
||||
-- Это ЕДИНСТВЕННАЯ миграция во всей цепочке, которая ЧИТАЕТ foreign table
|
||||
-- (проверено 2026-08-20 по всем 14 файлам, упоминающим FDW-таблицы: остальные —
|
||||
-- только CREATE/DROP FOREIGN TABLE и COMMENT, им ни USER MAPPING, ни связь не нужны).
|
||||
--
|
||||
-- А USER MAPPING создаёт бэкенд при старте (app/core/fdw.py), то есть ПОСЛЕ initdb.
|
||||
-- На чистом томе (новый сервер, CI, локальная разработка) FDW ещё не отображён и
|
||||
-- SELECT падал бы с "user mapping not found", обрывая docker-entrypoint-initdb.d
|
||||
-- и не давая контейнеру подняться вообще.
|
||||
--
|
||||
-- ВАЖНО: гард по количеству pending-строк тут НЕ защищает от FDW-падения —
|
||||
-- 003_seed_deals.sql на чистой БД сеет синтетические сделки с source='rosreestr'
|
||||
-- и md5-хэшем в dedup_hash, то есть pending > 0 уже на пустом томе. Первичный
|
||||
-- гард обязан проверять именно наличие USER MAPPING (pg_user_mappings), а не
|
||||
-- количество строк. Счётчик pending оставлен вторым, уже после проверки
|
||||
-- мэппинга — он просто экономит обращение к FDW, когда мигрировать нечего.
|
||||
--
|
||||
-- Файл НЕ удалён намеренно: прод помнит его по bare-filename в _schema_migrations,
|
||||
-- а tests/test_migration_numbering.py::test_applied_migration_is_not_renamed_or_deleted
|
||||
-- падает на удалении применённой миграции.
|
||||
--
|
||||
-- Фильтр src CTE — байт-в-байт совпадает с live-импортом (scheduler.py:437-444 /
|
||||
-- import-rosreestr.sh), чтобы каждая импортированная сделка нашлась и сконвертировалась.
|
||||
|
|
@ -24,25 +40,64 @@
|
|||
|
||||
BEGIN;
|
||||
|
||||
WITH src AS (
|
||||
SELECT
|
||||
id,
|
||||
md5('ros:dkp:' || CAST(id AS text)) AS h
|
||||
FROM gendesign_rosreestr_deals
|
||||
WHERE region_code = 66
|
||||
AND city ILIKE '%катеринбург%'
|
||||
AND realestate_type_code = '002001003000'
|
||||
AND area BETWEEN 18 AND 200
|
||||
AND deal_price BETWEEN 1000000 AND 100000000
|
||||
AND street IS NOT NULL AND trim(street) <> ''
|
||||
AND doc_type = 'ДКП'
|
||||
AND period_start_date >= DATE '2024-01-01'
|
||||
)
|
||||
UPDATE deals d
|
||||
SET dedup_hash = 'ros:dkp:' || CAST(src.id AS text),
|
||||
source_id = CAST(src.id AS text)
|
||||
FROM src
|
||||
WHERE d.source = 'rosreestr'
|
||||
AND d.dedup_hash = src.h;
|
||||
DO $migration_077$
|
||||
DECLARE
|
||||
pending bigint;
|
||||
BEGIN
|
||||
-- Гард 1 (первичный, обязателен): USER MAPPING создаётся бэкендом при
|
||||
-- старте (app/core/fdw.py), то есть ПОСЛЕ docker-entrypoint-initdb.d. На
|
||||
-- чистом томе его физически ещё не существует, а количество pending-строк
|
||||
-- тут ни при чём — счётчик ниже не защищает от FDW-падения, потому что
|
||||
-- сидовые данные (003_seed_deals.sql) генерируют строки с
|
||||
-- source='rosreestr' и md5-хэшем, то есть pending > 0 уже на пустой БД.
|
||||
-- Идиома совпадает с проверкой в app/core/fdw.py:57-62.
|
||||
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;
|
||||
|
||||
-- Гард 2 (вторичный): считаем строки, оставшиеся в md5-форме. Плоский
|
||||
-- ключ 'ros:dkp:N' под этот шаблон не подходит, поэтому после успешного
|
||||
-- прогона pending = 0. Ранний выход тут — просто чтобы не трогать FDW
|
||||
-- зря, когда мигрировать нечего (мэппинг уже есть, но backfill уже
|
||||
-- применён).
|
||||
SELECT count(*) INTO pending
|
||||
FROM deals
|
||||
WHERE source = 'rosreestr'
|
||||
AND dedup_hash ~ '^[0-9a-f]{32}$';
|
||||
|
||||
IF pending = 0 THEN
|
||||
RAISE NOTICE '077: строк в md5-форме нет — backfill пропущен';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE '077: строк к конвертации: %', pending;
|
||||
|
||||
WITH src AS (
|
||||
SELECT
|
||||
id,
|
||||
md5('ros:dkp:' || CAST(id AS text)) AS h
|
||||
FROM gendesign_rosreestr_deals
|
||||
WHERE region_code = 66
|
||||
AND city ILIKE '%катеринбург%'
|
||||
AND realestate_type_code = '002001003000'
|
||||
AND area BETWEEN 18 AND 200
|
||||
AND deal_price BETWEEN 1000000 AND 100000000
|
||||
AND street IS NOT NULL AND trim(street) <> ''
|
||||
AND doc_type = 'ДКП'
|
||||
AND period_start_date >= DATE '2024-01-01'
|
||||
)
|
||||
UPDATE deals d
|
||||
SET dedup_hash = 'ros:dkp:' || CAST(src.id AS text),
|
||||
source_id = CAST(src.id AS text)
|
||||
FROM src
|
||||
WHERE d.source = 'rosreestr'
|
||||
AND d.dedup_hash = src.h;
|
||||
END
|
||||
$migration_077$;
|
||||
|
||||
COMMIT;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue