From f5f8dc03ce37bdf2c91b688393cca6d2660e6553 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Sat, 19 Sep 2026 03:15:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=91=D1=8D=D0=BA=D0=B0=D0=BF=D1=8B:=20=D0=91?= =?UTF-8?q?=D0=94=20=D0=B1=D0=B5=D0=B7=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=20=D0=BD=D0=B5=20=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=81=D1=8F=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC,=20=D0=BF=D0=B5=D1=80=D0=B2=D1=8B=D0=B9=20=D0=B1=D1=8D?= =?UTF-8?q?=D0=BA=D0=B0=D0=BF=20=D0=B6=D0=B4=D1=91=D1=82=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit guard_empty считал неизвестные счётчики («?» — нет таблиц users/matches) непустыми, и снимок без данных уходил в оба репозитория и становился latest — restore latest в сценарии «Pi умер» падал на verify_staging. Теперь это отказ, как для пустой БД (обход — --allow-empty). entrypoint перед самым первым бэкапом ждёт /api/health приложения (до BACKUP_APP_WAIT_SECONDS, 600 с): оно отвечает только после миграций. Не дождались — пробуем всё равно, guard не пропустит БД без таблиц. README, раздел 9: новые строки журнала, снимки с «?» больше не создаются. #74 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd --- deploy/backup/README.md | 17 ++++++++++++----- deploy/backup/entrypoint.sh | 16 +++++++++++++++- deploy/backup/fs-backup.sh | 11 +++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/deploy/backup/README.md b/deploy/backup/README.md index 8048f06..8dcbb41 100644 --- a/deploy/backup/README.md +++ b/deploy/backup/README.md @@ -737,16 +737,23 @@ ``` 3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`). - Контейнер `backup` стартует одновременно с `app` и сразу пробует сделать первый бэкап. В его - журнале (`docker compose logs backup`) нормально увидеть одно из двух: + Контейнер `backup` стартует одновременно с `app`, ждёт, пока приложение ответит (до + 10 минут), и пробует сделать первый бэкап. В его журнале (`docker compose logs backup`) + нормально увидеть одно из трёх: - - если `backup` успел раньше, чем `app` создал БД: + - если приложение так и не поднялось и БД нет: ``` ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось? Первый бэкап не удался — следующая попытка по расписанию. ``` + - если БД есть, но без таблиц (миграции ещё не прошли): + + ``` + Не удалось прочитать число игроков и партий: в БД нет таблиц users/matches. + ``` + - если БД уже была: ``` @@ -763,8 +770,8 @@ ``` Проверьте, что у выбранного снимка в столбцах `Игроков`/`Партий` числа, а не `0` и не `?`. - Защита выше не распознаёт БД без таблиц: если первый бэкап попал ровно в момент создания - БД, в списке может появиться свежий снимок с `?` (задача #74). Такой снимок не выбирайте. + Снимки без данных защита больше не создаёт, но снимок с `?` мог остаться от старой версии + бэкапов — такой не выбирайте. 5. Восстановите, подставив ID из `list vps`: diff --git a/deploy/backup/entrypoint.sh b/deploy/backup/entrypoint.sh index e1028ca..f0d44a1 100644 --- a/deploy/backup/entrypoint.sh +++ b/deploy/backup/entrypoint.sh @@ -29,7 +29,21 @@ if [ ! -s "$CRONTAB" ]; then fi if ! fs-backup has-snapshots; then - log "Снимков в локальном репозитории ещё нет — делаю первый бэкап сразу." + # Новый сервер: app и backup стартуют разом, и БД может быть ещё без таблиц. /api/health + # отвечает только после миграций и bootstrap — ждём его (#74). Не дождались — пробуем всё + # равно: guard_empty не сохранит БД без таблиц. + _app="${BACKUP_APP_HOST:-app}" + _wait="${BACKUP_APP_WAIT_SECONDS:-600}" + log "Снимков в локальном репозитории ещё нет — жду приложение (до ${_wait} с) и делаю первый бэкап." + _waited=0 + until wget -q -T 3 -O /dev/null "http://$_app:8000/api/health" 2>/dev/null; do + if [ "$_waited" -ge "$_wait" ]; then + log "Приложение не ответило за ${_wait} с — пробую бэкап без него." + break + fi + sleep 10 + _waited=$((_waited + 10)) + done fs-backup run --scheduled || log "Первый бэкап не удался — следующая попытка по расписанию." fi diff --git a/deploy/backup/fs-backup.sh b/deploy/backup/fs-backup.sh index 0757614..047ef1a 100644 --- a/deploy/backup/fs-backup.sh +++ b/deploy/backup/fs-backup.sh @@ -178,6 +178,17 @@ forget_repo() { # Защита истории от пустых данных: новый Pi до восстановления или случайно очищенная БД не # должны становиться «последним снимком» (restore latest вернул бы пустоту). guard_empty() { # guard_empty <игроков> <партий> + # «?» — счётчики не прочитались: в БД нет таблиц (миграции ещё не прошли). Такой снимок + # стал бы latest и сломал бы restore latest (#74) — отказ, как и для пустой БД. + case "$1$2" in + *'?'*) + for _r in $(repos); do mark "run-$_r" err "в БД нет таблиц users/matches — бэкап не сделан"; done + log "Не удалось прочитать число игроков и партий: в БД нет таблиц users/matches." + log "Похоже, приложение ещё не применило миграции. Бэкап НЕ сделан, чтобы снимок без данных" + log "не стал последним. Дождитесь запуска приложения; если так и задумано:" + die "fs-backup run --allow-empty" + ;; + esac if [ "$1" != 0 ] || [ "$2" != 0 ]; then return 0; fi for _repo in $(repos); do _prev="$(r "$_repo" snapshots latest --host "$SNAP_HOST" --json 2>/dev/null | jq -r \ -- 2.54.0