Перейти к основному содержимому

ADR 0152 — Резервное копирование и восстановление томов для compose

Статус​

Proposed. Существует потому, что FASE 29 должна мигрировать предыдущую установку, а путь, на который для этого указывала документация, не годится.

Контекст​

Текущее резервное копирование покрывает одну базу и работает в одном месте:

  • docker/backup/backup.sh делает pg_dump --format=custom | aws s3 cp - (:124-140), прямо в S3 без касания диска, с хранением по количеству (:160-182) и строкой в backup_runs при каждом выполнении, успешном или нет (trap registrar EXIT, :44-80).
  • docker/backup/restore.sh восстанавливает в тестовую базу (brabo_restore_test, :18), проверяет три вещи — список таблиц, количество критичных в окне резервной копии и плотную непрерывность seq session_events (:139-224) — и никогда не трогает источник (:8-9).
  • make test-restore → deploy/k8s/test-restore.sh, который требует kubectl, кластер и CronJob brabo-backup в namespace (:56-58).

Три последствия, все измерены:

  1. У миграции установки через compose нет пути. test-restore — это Kubernetes; эквивалентной цели для compose не существует, а в docker-compose.prod.yml нет сервиса backup (образ собирается bake и используется только в k8s).
  2. Резервная копия покрывает только Postgres. Ничто в docker/backup/ не касается neo4j_data, git_local_repos, project_workspaces, ollama_data или brabo_projects_base (docker-compose.prod.yml:474-486).
  3. Она также требует S3 — пять обязательных переменных (backup.sh:11-15). В установке на одной машине требовать bucket, чтобы иметь возможность мигрировать, — значит требовать инфраструктуру, о которой установщик только что сказал, что она не нужна.

Решение​

1. Классифицировать тома, прежде чем их копировать​

Лёгкая ошибка — "сделать резервную копию всех томов". Семь томов — не одно и то же, и одинаковое обращение с ними стоит места и скрывает важное:

томприродарешение
pgdataисточник истины — event log, действия, всёуже покрыт pg_dump; остаётся логическим дампом, а не копией файлов
git_local_reposисточник истины — bare repos LocalGitProviderвходит, и это находка этого ADR
neo4j_dataпроизводный — граф является памятью, спроецированной из event log (ADR 0099), никогда не источником истиныне входит: не хватает перепроецирования, а не резервной копии
project_workspacesпроизводный — worktrees, которые WorktreeManager пересоздаёт из bareне входит
ollama_dataповторно получаемый — модели, которые скачиваются зановоне входит
brabo_projects_baseпользователя, а не продуктане входит, и установщик никогда её не удаляет (ADR 0150)

git_local_repos — реальная дыра. У проекта с provider local там лежит bare-репозиторий; потеря этого тома — потеря кода, произведённого агентами, и сегодня ни одна резервная копия его не покрывает. Он не восстанавливается из Postgres — event log хранит повествование, а не объекты git.

2. Локальное назначение, S3 необязательно​

Резервное копирование начинает принимать назначение на диске помимо S3. В установке на одной машине каталог — честное назначение; требовать bucket для миграции значило бы требовать инфраструктуру, которую установщик не просит.

Пять переменных S3 перестают быть обязательными и становятся тем, чем на практике уже были: конфигурацией одного из назначений.

3. Путь восстановления, работающий для compose​

Рождается эквивалент test-restore без кластера: поднимает образ резервного копирования через docker compose run, восстанавливает в тестовую базу и выполняет те же три проверки из restore.sh:139-224. Никакой новой логики проверки — меняется оболочка, а не суждение.

deploy/k8s/test-restore.sh остаётся нетронутым: это два окружения с двумя оболочками, и их объединение заставило бы compose-вариант зависеть от kubectl.

4. Перепроецирование графа — ответ для Neo4j​

Восстановление neo4j_data из резервной копии восстановило бы, возможно, старую проекцию рядом с Postgres, восстановленным на другой момент, — два производных состояния разных моментов, без ничего, что их согласует. Правильный ответ для производной памяти — перепроецировать из источника.

Эта фаза не строит перепроецирование: она объявляет, что это путь и что до его появления мигрированная установка рождается с пустым графом — что является известной деградацией (RAG остаётся в pgvector, который живёт в Postgres и приходит в дампе), а не потерей данных.

5. Установщик использует это и не гадает​

Путь миграции ADR 0150: резервная копия → подтверждение → удаление → установка → восстановление, с восстановлением, доказанным решением 3. Без хорошей резервной копии установщик не переходит к удалению.

Что этот ADR явно отвергает​

  • Копировать все тома. По решению 1: три из семи производные или повторно получаемые, и их копирование создаёт впечатление покрытия, не добавляя восстановимости.
  • Копию файлов pgdata. Копирование каталога данных работающего Postgres даёт резервную копию, которая может не восстановиться; логический дамп остаётся.
  • Восстанавливать neo4j_data. По решению 4.
  • Выполнять восстановление в исходную базу. restore.sh никогда не трогает источник (:8-9), и это свойство не обсуждается ради удобства миграции.
  • Объединять с deploy/k8s/test-restore.sh.

Последствия​

  • Миграция существующей установки получает доказанный путь, а установщик получает право удалять тома — потому что доказал, что может вернуть их обратно.
  • git_local_repos входит в резервную копию, и это разница, которую этот ADR вносит вне области установки: сегодня любая потеря этого тома — потеря кода, в любом окружении, и никто это не зафиксировал.
  • Мигрированная установка рождается с пустым графом, пока не появится перепроецирование. Объявлено, с названным эффектом: чтения, зависящие от графа, деградируют; RAG не затрагивается.
  • Резервное копирование получает второе назначение, а с ним второй путь сбоя — заполненный диск. Строка в backup_runs продолжает записываться в обоих случаях уже существующим trap.