Files
ayurishchevandClaude Sonnet 5 02f7b49e12 Add database backups and automatic restore
Daemon job "backup" (online copy, quick_check, rotation), restore of the
newest valid copy when the database cannot be opened, change journal reset
after restore, last_restore in /health, docs and tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 08:55:42 +03:00

7.4 KiB

План: резервные копии базы и автовосстановление (доработка Б, п. 1.3-1.4)

Цель

База ripe.db копируется автоматически по расписанию, а при порче файла сервис сам поднимает последнюю исправную копию вместо пустой базы (риск 3 анализа). Откат к копии заметен в логе и в /health, а не происходит молча.

Дизайн

Резервное копирование

  • Задание backup в демоне (третье после asn и fqdn, те же расписание, состояние в status.json, защита от наложения). Расписание: schedule.backup в config.json, по умолчанию 30 4 * * *; менять можно через POST /schedule (тип backup).
  • Как копируем: онлайн-копия средствами SQLite (Connection.backup), безопасная при работающих API и сборщике (WAL, чтение снимка). Файл сначала пишется как *.tmp, затем переименовывается: неполная копия не появляется.
  • Проверка: перед копированием PRAGMA quick_check живой базы, после копирования она же на копии. Если проверка не пройдена, копия не создаётся или удаляется, старые хорошие копии не трогаются, задание получает last_error, /health показывает degraded.
  • Хранение: RIPE_BACKUP_DIR (по умолчанию <DATA_DIR>/backups), файлы ripe-<UTC-время>.db. Ключ backup_keep в config.json (по умолчанию 7): лишние старые копии удаляются только после успешной новой.
  • Ограничение: по умолчанию копии лежат на том же томе /data, что и база. Это защищает от порчи файла, но не от потери тома. Вынос на отдельный том или хост описывается в README (переменная RIPE_BACKUP_DIR и копирование каталога), не автоматизируется.

Автовосстановление

  • Где: в db.connect(), там, где сейчас порченая база убирается в *.corrupt-*. После этого берётся самая свежая копия, проходящая quick_check; копия кладётся на место базы (через временный файл и os.replace), соединение открывается заново. Если исправных копий нет, поведение прежнее: StorageError, API отвечает 503.
  • Гонка API и демона: оба процесса могут обнаружить порчу одновременно. Восстановление идёт под файловой блокировкой (file_lock), внутри блокировки база проверяется повторно: если другой процесс уже восстановил, повторных действий нет.
  • Курсоры diff после отката: журнал изменений откатывается вместе с базой, и номера записей после точки копии могли уже быть выданы клиентам. Чтобы старые курсоры не совпали с новой историей, после восстановления счётчик журнала сдвигается на 1 000 000 и «горизонт» ставится на новое значение (время горизонта = момент восстановления). Все прежние курсоры и времена дают 410, клиенты делают полную выгрузку. Это эвристика: она надёжна, пока между копиями накапливается меньше миллиона изменений (при суточной копии и редких изменениях RIPE запас большой).
  • Заметность: запись в лог уровня ERROR и файл last_restore.json (время, использованная копия, путь карантина); /health показывает поле last_restore. Статус degraded восстановление не вызывает (сервис работает), но событие не теряется.
  • Ограничение: автоматически ловится порча, обнаруженная при открытии базы. Порча страниц внутри файла проявится при чтении (ответ 503) и будет замечена заданием backup (quick_check не пройдёт, /health = degraded); восстановление в этом случае ручное (порядок в README).

Что не входит

Копии вне сервера, шифрование, восстановление на момент времени, команда CLI restore, ручной запуск копии через POST /collect (можно добавить отдельно).

Изменения

  1. cidr_collector.py: BACKUP_DIR, RESTORE_FILE, DEFAULT_BACKUP_KEEP.
  2. db.py: backup_database() (копия, проверка, ротация), _restore_latest_backup() под блокировкой, вызов из connect(), сдвиг журнала.
  3. collector_daemon.py: задание backup (реестр COLLECTORS становится реестром заданий с вызываемыми объектами, DEFAULT_CRONS["backup"]).
  4. api_server.py: ScheduleUpdate.type допускает backup; /health показывает last_restore.
  5. README.md: расписание и ключи (schedule.backup, backup_keep), RIPE_BACKUP_DIR, ручное восстановление, вынос копий с тома.
  6. Тесты (2): копия проходит проверку, ротация оставляет backup_keep штук, повреждённая живая база копию не создаёт; порченая база при открытии восстанавливается из копии (карантин сохранён, старые курсоры дают 410), без копий остаётся StorageError.

Проверка

Тесты в контейнере. Вручную на копии данных: задание backup создаёт файл, порча ripe.db (запись мусора) при работающих API и демоне приводит к восстановлению, /health показывает last_restore, запрос since со старым курсором даёт 410. Проверка в Docker Compose (общий том).

Откат

Удалить задание backup и вызов восстановления в connect(): схема базы не меняется (журнал и meta остаются), файлы копий можно удалить вручную.