diff --git a/deploy/backup/README.md b/deploy/backup/README.md index dc68c16..483de6d 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 6ca3ae4..a6e2440 100644 --- a/deploy/backup/entrypoint.sh +++ b/deploy/backup/entrypoint.sh @@ -37,7 +37,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 194fa86..31dcc32 100644 --- a/deploy/backup/fs-backup.sh +++ b/deploy/backup/fs-backup.sh @@ -275,6 +275,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 \