# План: резервные копии базы и автовосстановление (доработка Б, п. 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` (по умолчанию `/backups`), файлы `ripe-.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` остаются), файлы копий можно удалить вручную.