0059. Обновление со сменой схемы — после проверенной резервной копии
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-03. Изменяет запрет смены схемы в ADR 0038: раньше единственным путём был ручной upgrade --backup-record. Опирается на агент резервных копий из ADR 0056 и ADR 0057.
Контекст
Встроенный механизм обновлений скачивает stable release с GitHub, проверяет архив, останавливает службы, переключает версию и при отказе запуска возвращает прежний код. Так он работает только при той же схеме БД. Релиз со сменой схемы отклонялся везде: в консоли и автообновлении (maintenance_required), в update и в пакетах deb/rpm/Windows, которые над существующей установкой вызывают apply-installer. Новый пакет поверх установки со старой схемой падал в postinst. Оставался ручной upgrade --backup-record <файл>, а этот файл — запись оператора, а не проверка копии.
Почему схему нельзя просто мигрировать вместе с кодом:
- после успешной миграции прежний код не работает:
ready()отказывает на схеме новее своей. Вернуться можно только к резервной копии; - миграции идут одной транзакцией под блокировкой 18471/1. Упавшая миграция откатывается целиком, схема остаётся прежней. Затем вне транзакции строятся онлайн-индексы (
CREATE INDEX CONCURRENTLY), шаг повторяемый; - агент резервных копий снимает точку онлайн, пока службы работают. После снимка он ставит её структурную проверку.
Решение
Смена схемы ставится всеми путями обновления — консоль, автообновление в окне, update, пакеты через apply-installer. Обязательное условие: перед остановкой служб встроенный агент снял свежую точку в настроенный vault и структурно её проверил.
- Релиз скачивается, проверяется и распаковывается, как раньше.
- Сторож (
apps/deploy/src/backup-guard.ts) работает с API текущей версии по ключу bootstrap и обычному локальному транспорту, включая закреплённый сертификат. Он проверяет четыре условия: vault подключён, vault доступен, агент в сети, есть хотя бы одна завершённая копия. Первая полная копия терабайтов — плановая операция, а не побочный эффект обновления. POST /api/v1/backup/runsс новым Idempotency-Key. Сторож ждётcompletedсpointId, затемverifiedAtбезverifyErrorу этой точки. Опрос идёт раз в 5 с. Временные ошибки API повторяются, общий предел — 6 ч. Пока идёт ожидание, планировщик консоли продолжает писать heartbeat.- Любой отказ сторожа (
BackupRequired) возникает до любых изменений: службы работают, журнал не создан. Консоль показываетmaintenance_requiredс объяснением. Автообновление повторит попытку не раньше следующих суток. Пакет завершает postinst ошибкой с той же подсказкой. - Сценарий
applyMigration(apps/deploy/src/migration-update.ts) пишет журнал с точкой копии. Фазы:prepared,stopped,migrating,migrated,committed.- Упавшая миграция откатывает свою транзакцию. Прежний релиз запускается снова (
rolling-back→rolled-back), сервис продолжает работать на старой версии. - Если прежний релиз отказывает, значит упал шаг индексов уже после фиксации схемы. Тогда журнал переходит в
maintenance-required. - Отказ запуска нового релиза после успешной миграции тоже даёт
maintenance-required. Прежний код на такой схеме не запускается; точка копии — граница восстановления.
- Упавшая миграция откатывает свою транзакцию. Прежний релиз запускается снова (
recoverдля журнала со сменой схемы действует по фазе:- из
prepared/stoppedвозвращает прежний релиз: база не тронута; - из любой более поздней фазы идёт вперёд: повторяет миграцию (она идемпотентна) и запускает новый релиз.
- из
- Ручной
upgrade --backup-recordостаётся для установок без встроенного vault. Он использует тот же журнал и тот же откат.
Что теряется при восстановлении из точки. Изменения, принятые после снимка и до остановки служб, в точку не попадают. Точка нужна только при отказе нового релиза после миграции. Тогда оператор выбирает: исправить причину и выполнить recover (вперёд) или восстановить точку прежним релизом (BACKUP_RECOVERY).
Переход с 0.2.0. В 0.2.0 нет API резервных копий, а её планировщик сам отклоняет смену схемы. Поэтому на релиз с этим решением установку 0.2.0 переводят один раз вручную: upgrade --backup-record после копии оператора, либо пакет с последующим upgrade. Дальнейшие обновления идут автоматически.
Последствия
- Обновление со сменой схемы длится дольше на время снимка изменений и его проверки. При ежедневных копиях это минуты, а не часы.
- Автообновление со сменой схемы включено так же, как обычное: окно, одна попытка в сутки, pin запрещает. Кто не хочет миграций без человека, закрепляет версию.
- Публичный контракт
/api/v1не меняется: используются существующиеbackup/*и кодmaintenance_required, тексты консоли уточнены. - Не реализовано: восстановление в консоли, pin точки перед обновлением (точка живёт по retention, как любая другая), rolling update и HA.
Проверки
tests/schema-update.test.mjs:- отказ сторожа без vault, без доступа к vault, без агента, без первой копии и на релизе без API копий;
- неудачная копия, повреждённая точка, тайм-аут, неподтверждённое завершение;
- порядок фаз, откат при неудачной миграции, отказ без копии без единой остановки, сбой запуска после миграции.
tests/update-monitor.test.mjs: смена схемы ставится в окне; отказ копии даётmaintenance_requiredи одну попытку в сутки.tests/deployment.test.mjs: решениеmigrateдля новой схемы, отказ для схемы ниже текущей.tests/browser/updates.mjs: установка со сменой схемы доступна и объяснена; отказ показан.tests/deployment/containers.mjs(Compose, настоящие API, агент и vault). Релиз с номером схемы выше текущего:- до подключения vault отклоняется, версия и службы не меняются;
- после первой копии ставится, а журнал называет новую точку, а не прежнюю.
tests/schema-update.test.mjs: направлениеrecover— назад доmigrating, вперёд после него.