2 Commits
Author SHA1 Message Date
NotBigGhost d12afe62d0 Merge pull request 'Защита бэкапов от пустой БД пропускает БД без таблиц (счётчики «?») (#74)' (#100) from issue-74-backup-guard into dev
Reviewed-on: #100
2026-09-30 13:04:37 +03:00
NotBigGhostandClaude Opus 5 f5f8dc03ce Бэкапы: БД без таблиц не становится снимком, первый бэкап ждёт приложение
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-19 03:15:49 +03:00
3 changed files with 38 additions and 6 deletions
+12 -5
View File
@@ -737,16 +737,23 @@
``` ```
3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`). 3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`).
Контейнер `backup` стартует одновременно с `app` и сразу пробует сделать первый бэкап. В его Контейнер `backup` стартует одновременно с `app`, ждёт, пока приложение ответит (до
журнале (`docker compose logs backup`) нормально увидеть одно из двух: 10 минут), и пробует сделать первый бэкап. В его журнале (`docker compose logs backup`)
нормально увидеть одно из трёх:
- если `backup` успел раньше, чем `app` создал БД: - если приложение так и не поднялось и БД нет:
``` ```
ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось? ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось?
Первый бэкап не удался — следующая попытка по расписанию. Первый бэкап не удался — следующая попытка по расписанию.
``` ```
- если БД есть, но без таблиц (миграции ещё не прошли):
```
Не удалось прочитать число игроков и партий: в БД нет таблиц users/matches.
```
- если БД уже была: - если БД уже была:
``` ```
@@ -763,8 +770,8 @@
``` ```
Проверьте, что у выбранного снимка в столбцах `Игроков`/`Партий` числа, а не `0` и не `?`. Проверьте, что у выбранного снимка в столбцах `Игроков`/`Партий` числа, а не `0` и не `?`.
Защита выше не распознаёт БД без таблиц: если первый бэкап попал ровно в момент создания Снимки без данных защита больше не создаёт, но снимок с `?` мог остаться от старой версии
БД, в списке может появиться свежий снимок с `?` (задача #74). Такой снимок не выбирайте. бэкапов — такой не выбирайте.
5. Восстановите, подставив ID из `list vps`: 5. Восстановите, подставив ID из `list vps`:
+15 -1
View File
@@ -37,7 +37,21 @@ if [ ! -s "$CRONTAB" ]; then
fi fi
if ! fs-backup has-snapshots; then 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 "Первый бэкап не удался — следующая попытка по расписанию." fs-backup run --scheduled || log "Первый бэкап не удался — следующая попытка по расписанию."
fi fi
+11
View File
@@ -275,6 +275,17 @@ forget_repo() {
# Защита истории от пустых данных: новый Pi до восстановления или случайно очищенная БД не # Защита истории от пустых данных: новый Pi до восстановления или случайно очищенная БД не
# должны становиться «последним снимком» (restore latest вернул бы пустоту). # должны становиться «последним снимком» (restore latest вернул бы пустоту).
guard_empty() { # guard_empty <игроков> <партий> 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 if [ "$1" != 0 ] || [ "$2" != 0 ]; then return 0; fi
for _repo in $(repos); do for _repo in $(repos); do
_prev="$(r "$_repo" snapshots latest --host "$SNAP_HOST" --json 2>/dev/null | jq -r \ _prev="$(r "$_repo" snapshots latest --host "$SNAP_HOST" --json 2>/dev/null | jq -r \