From 8a5d323608edb8bd30030c4e8f412c018e2db3ce Mon Sep 17 00:00:00 2001 From: bot-backend Date: Tue, 25 Aug 2026 08:18:16 +0300 Subject: [PATCH] =?UTF-8?q?fix(ops):=20=D0=B2=D0=BE=D1=81=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=B0=D0=B4=D0=B0=D0=B5=D1=82=20=D0=B2=20=D1=85?= =?UTF-8?q?=D0=B2=D0=BE=D1=81=D1=82=D0=B5=20=D0=B8=D0=B7-=D0=B7=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=B7=D0=B0=D0=B2=D0=B5=D0=B4=D1=91=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=80=D0=BE=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дамп backup.sh — это одна база, а роли живут в кластере и уезжают соседним файлом gendesign_globals_.sql.gz. Сам дамп на эти роли ссылается: OWNER TO и GRANT. psql здесь идёт с ON_ERROR_STOP=1, поэтому на кластере, где ролей ещё нет, восстановление обрывается на первом же таком операторе — но данные к тому моменту уже залиты и закоммичены, в одну транзакцию psql дамп не оборачивает. Итог: наполовину собранная база и невнятная ошибка в самом конце, после всей работы. Ровно этот путь и нужен при переезде: на новом хосте кластер пустой, ролей нет. Теперь соседний globals подхватывается автоматически и грузится первым — так уже давно устроен ops/restore-drill.sh, разошлись только эти два скрипта. Правило имени взято оттуда же. Побочный эффект назван в шапке прямо: globals несёт пароли ролей на момент снятия дампа, и если пароль меняли после, он вернётся к старому. Для поднятия кластера с нуля это правильное поведение, для точечного отката данных в живой базе — нет, поэтому есть RESTORE_SKIP_GLOBALS=1 и RESTORE_GLOBALS_FILE. Сам globals грузится БЕЗ ON_ERROR_STOP: на живом кластере роли уже есть и CREATE ROLE для каждой законно падает с already exists. Значимая часть — идущие следом ALTER ROLE, они отрабатывают. Если соседа нет и он не отключён явно — печатаем предупреждение ДО заливки, чтобы «упадёт через час» не стало сюрпризом. Не падаем: на живом кластере роли на месте и всё пройдёт как раньше. Проверено: bash -n; ветка автоопределения прогнана на паре файлов с реальными именами — сосед находится. Поведение на живом кластере не меняется: globals идёт первым, дальше всё как было. Refs #3057 --- ops/restore.sh | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/ops/restore.sh b/ops/restore.sh index e72ee29d..0463d7c2 100755 --- a/ops/restore.sh +++ b/ops/restore.sh @@ -9,6 +9,28 @@ # # Example: # RESTORE_CONFIRM=yes ops/restore.sh ./gendesign_20260427_033000.sql.gz +# +# GLOBALS (#3057). Дамп из backup.sh не содержит ролей: pg_dump выгружает одну +# базу, а роли живут в кластере и уезжают отдельным файлом +# gendesign_globals_.sql.gz, который backup.sh кладёт РЯДОМ. При этом дамп +# ссылается на роли в OWNER TO и GRANT, а psql здесь идёт с ON_ERROR_STOP=1 — +# значит на кластере, где роли ещё не заведены, восстановление падает на первом +# же таком операторе. Данные к тому моменту уже залиты и закоммичены (psql не +# оборачивает дамп в одну транзакцию), то есть работа уходит в наполовину +# собранную базу с невнятной ошибкой в самом хвосте. +# +# Поэтому globals-файл рядом с дампом подхватывается АВТОМАТИЧЕСКИ и грузится +# ПЕРВЫМ — ровно как это давно делает ops/restore-drill.sh. +# +# ПОБОЧНЫЙ ЭФФЕКТ, о котором надо знать: globals несёт пароли ролей на момент +# снятия дампа. Если пароль меняли после — он вернётся к старому значению. +# Для поднятия кластера с нуля это правильно, для точечного отката данных в +# живой базе может быть неожиданно, поэтому есть два рычага: +# RESTORE_SKIP_GLOBALS=1 — не трогать роли вообще +# RESTORE_GLOBALS_FILE=... — взять конкретный файл, а не соседний +# +# Если globals не нашёлся и не отключён явно — предупреждаем ДО заливки, чтобы +# «упало через час» не стало сюрпризом. set -euo pipefail @@ -34,6 +56,43 @@ DB_NAME=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_D DB_USER=${DB_USER:-gendesign} DB_NAME=${DB_NAME:-gendesign} +# --- globals: роли кластера, без которых упадут OWNER TO / GRANT --------------- +# Имя соседа строится по той же схеме, что использует backup.sh: +# gendesign_.sql.gz -> gendesign_globals_.sql.gz +GLOBALS_FILE="${RESTORE_GLOBALS_FILE:-}" +if [[ "${RESTORE_SKIP_GLOBALS:-0}" != "1" && -z "$GLOBALS_FILE" ]]; then + dump_dir=$(cd "$(dirname "$DUMP_FILE")" && pwd) + dump_base=$(basename "$DUMP_FILE") + if [[ "$dump_base" =~ ^([A-Za-z0-9]+)_([0-9]{8}_[0-9]{6})\.sql\.gz$ ]]; then + candidate="${dump_dir}/${BASH_REMATCH[1]}_globals_${BASH_REMATCH[2]}.sql.gz" + [[ -f "$candidate" ]] && GLOBALS_FILE="$candidate" + fi +fi + +if [[ "${RESTORE_SKIP_GLOBALS:-0}" == "1" ]]; then + echo "Globals: пропущены явно (RESTORE_SKIP_GLOBALS=1)." +elif [[ -n "$GLOBALS_FILE" ]]; then + if [[ ! -f "$GLOBALS_FILE" ]]; then + echo "ОШИБКА: globals-файл не найден: $GLOBALS_FILE" >&2 + exit 2 + fi + echo "Globals: $GLOBALS_FILE -> кластер (роли и их пароли на момент дампа)" + # ON_ERROR_STOP здесь НЕ ставим: на живом кластере роли уже есть, и + # CREATE ROLE для каждой из них законно падает с «already exists». + # Значимая часть — идущие следом ALTER ROLE, они отрабатывают. + gunzip -c "$GLOBALS_FILE" | \ + docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U "$DB_USER" -d postgres +else + echo "############################################################" >&2 + echo "# ВНИМАНИЕ: globals-файл рядом с дампом не найден. #" >&2 + echo "# Дамп ссылается на роли кластера в OWNER TO и GRANT. #" >&2 + echo "# Если роли на этом кластере не заведены, psql упадёт по #" >&2 + echo "# ON_ERROR_STOP уже ПОСЛЕ заливки данных. #" >&2 + echo "# Ожидаемое имя: <проект>_globals_<та же метка>.sql.gz #" >&2 + echo "############################################################" >&2 +fi + echo "Restoring $DUMP_FILE → ${DB_NAME} as ${DB_USER}" gunzip -c "$DUMP_FILE" | \