Documentation
¶
Overview ¶
Package schemaguard — ЧИТАТЕЛЬ точки невозврата НА ПУТИ СТАРТА: слот готовности отвечает «не готов», когда схема под образом не та, которую образ умеет обслуживать.
Предмет ¶
Мигратор идёт при КАЖДОМ раскате (init-контейнер того же пода, тот же образ), поэтому откат выкатки ставит ПРЕЖНИЙ образ на НОВУЮ схему. Схему откат не возвращает: секция `Down` на этом пути не исполняется вовсе. До этого пакета такой под объявлялся ГОТОВЫМ — слот проверял, что процесс отвечает, а не что он способен обслужить схему, которая под ним лежит. Отказ приходил позже, на первом обращении к колонке, которой в образе ещё нет либо в схеме уже нет, — и приходил КЛИЕНТУ, а не выкатке, потому что балансировщик уже отдавал трафик.
Гейт `internal/repohygiene` (задача #1690) сделал точку невозврата МАШИННОЧИТАЕМОЙ и честно назвал свою границу: «читателя у объявления на пути старта нет вовсе». Этот пакет — тот самый читатель (задача #1734).
Решение — ЧИСТАЯ ФУНКЦИЯ, поэтому проверяется без базы ¶
Set.Verdict принимает два числа и объявленные точки невозврата и не касается ни сети, ни диска. Чтения `goose_db_version` касается только PgxVersionReader и интеграционная проба.
dbVersion > imageVersion НЕ ГОТОВ. Схема ушла вперёд образа: миграций,
которые к ней применены, у образа НЕТ, значит он
не может УСТАНОВИТЬ, что способен её обслужить, —
а неустановленный ответ не есть «да». Fail-closed.
dbVersion < imageVersion НЕ ГОТОВ, если в промежутке (dbVersion..imageVersion]
объявлена точка невозврата: образ требует изменения
схемы, которого в схеме нет. Иначе ГОТОВ — накат
ещё идёт, а совместимые вперёд миграции обслуживать
не мешают.
dbVersion == imageVersion ГОТОВ.
ОБЕ ВЕЛИЧИНЫ НАЗЫВАЮТСЯ ОПЕРАТОРУ (Result.Reason) и попадают в тело `/readyz`, а не в общий журнал: человек, разбирающий выкатку в три часа ночи, обязан отличить «сломан продукт» от «образ не той версии, что схема» — не читая кода.
ЦЕНА НАЗВАНА, А НЕ УМОЛЧАНА ¶
Первое правило означает, что при накатывании миграции поды ПРЕДЫДУЩЕЙ ревизии выходят из ротации, как только новая ревизия применила схему. Это и есть предмет задачи, а не побочный ущерб: под прежнего образа на новой схеме обслуживать её не обязан и обслуживать не должен. Событие ОГРАНИЧЕНО выкаткой — новая ревизия к этому моменту уже готова, потому что мигратор ей init-контейнер, — и заметно оператору, тогда как прежний отказ был не заметен никому, кроме клиента.
Точки невозврата ВЫВОДЯТСЯ, а не выписываются ¶
Describe берёт и версию, и точки из ВСТРОЕННОГО набора миграций сервиса — того же `embed.FS`, который применяет мигратор. Выписанная константа разошлась бы с набором молча, и разошлась бы именно там, где расхождение не видно: на образе, который уже развёрнут.
ГРАНИЦА, названная честно ¶
Точки невозврата читаются ИЗ ВСТРОЕННОГО набора, поэтому обратная ветвь (образ впереди схемы) видит только миграции, объявившие себя признаком В СЕБЕ. Миграции, лежавшие в дереве до заведения #1690, объявлены СЧЁТНОЙ ВЕДОМОСТЬЮ, а ведомость в образ не встраивается — она предмет решения, а не свойство набора. Значит обратная ветвь работает начиная с первой миграции, написанной по правилу #1690, и не раньше.
СКОЛЬКО миграций несут признак в себе, здесь НЕ ВЫПИСАНО намеренно: это число уже измеряет и печатает перепись гейта #1690 (`internal/repohygiene`, `schemaRollbackCensus`, поле «объявлено признаком»). Второе место об одном числе разошлось бы с первым молча — и разошлось бы в сторону «покрыто шире, чем есть».
ПЕРВОЕ правило от этой границы НЕ зависит: оно fail-closed и точек невозврата не спрашивает вовсе — образ, которому схема ушла вперёд, судить о ней не может ни при каком объявлении.
Index ¶
Constants ¶
const CheckerName = "schema-version"
CheckerName — одно имя на все семь точек провязки. Имя видно оператору в теле `/readyz`, поэтому оно объявлено здесь, а не переписывается в каждом корне.
const PointOfNoReturnMarker = "-- +kacho point-of-no-return:"
PointOfNoReturnMarker — признак, которым миграция объявляет: после неё ПРЕЖНИЙ образ схему обслуживать не может.
Токен объявлен ЗДЕСЬ, в прод-коде, и отсюда же его читает гейт `internal/repohygiene`, требующий признака на коммите. Два места об одном имени разошлись бы молча — и разошлись бы там, где расхождение не видно: автор прочитал бы в подсказке гейта одно, а страж старта искал бы другое.
Variables ¶
var ErrEmptySet = errors.New("schemaguard: embedded migration set is empty")
ErrEmptySet — набор миграций пуст. Это НЕ «версия 0»: сервис со встроенным набором обязан его нести, а пустой набор означает сломанную сборку (директива `embed` перестала что-либо захватывать) — и молчаливая «версия 0» сделала бы такой образ готовым на любой схеме.
Functions ¶
func CheckFromFS ¶
CheckFromFS — провязка одной строкой: разбор встроенного набора плюс тело проверки.
Почему сломанный набор даёт ВЕЧНУЮ НЕГОТОВНОСТЬ, а не отказ старта ¶
Отказ старта был бы виден только в журнале и событиях пода, а неготовность НАЗЫВАЕТ ПРИЧИНУ в теле `/readyz` — там же, где оператор читает остальные зависимости. Обе формы снимают под из ротации; различаются они тем, что вторая отвечает на вопрос «почему», не требуя чтения кода.
Молчаливого прохода здесь нет ни при каком входе: разобрать набор не удалось — значит образ не может установить, что способен обслужить схему, и это тот же fail-closed, что у первого правила.
func DeclaresPointOfNoReturn ¶
DeclaresPointOfNoReturn — объявляет ли тело миграции точку невозврата.
Пустое обоснование признаком НЕ считается: иначе токен становится печатью, которую ставят не читая.
Types ¶
type Querier ¶
Querier — минимальный контракт пула (его выполняет *pgxpool.Pool).
Тип строки взят ЧУЖОЙ (`pgx.Row`), а не объявлен свой: Go не приводит возвращаемые типы, поэтому интерфейс со своим типом строки не выполнил бы НИ ОДИН реальный пул — провязка не собралась бы, и «минимальный контракт» оказался бы контрактом, которому никто не отвечает.
type Result ¶
type Result struct {
Ready bool
DBVersion int64
ImageVersion int64
// Crossed — объявленные точки невозврата, которых схеме НЕ ХВАТАЕТ
// (обратная ветвь). Пусто в остальных случаях.
Crossed []int64
// Reason — текст ДЛЯ ОПЕРАТОРА: называет обе величины. Пуст, когда готов.
Reason string
}
Result — вердикт о паре «схема ↔ образ».
type Set ¶
type Set struct {
// Version — старшая версия встроенного набора.
Version int64
// Points — версии набора, объявившие точку невозврата, по возрастанию.
Points []int64
// Files — сколько файлов миграций осмотрено. Объём осмотренного: «точек
// невозврата 0» обязано быть отличимо от «не прочитано ни одного файла».
Files int
}
Set — то, что образ УМЕЕТ обслуживать, выведенное из его встроенного набора.
func Describe ¶
Describe выводит Set из встроенного набора миграций.
Имя файла goose: `<версия>_<имя>.sql`. Версия — ведущие цифры до первого подчёркивания; файл, из имени которого версия не читается, — ОТКАЗ, а не пропуск: молчаливый пропуск занизил бы `Version`, а заниженная версия делает образ «отставшим» и уводит диагностику не туда.
func (Set) Check ¶
func (s Set) Check(read VersionReader) func(context.Context) error
Check — тело проверки готовности. Форма `func(context.Context) error`, а не готовый чекер, намеренно: носителей готовности в дереве ДВА (общий `pkg/observability/health` и собственный у службы прав), и пакет не вправе требовать одного из них.
Ошибка чтения версии — НЕ ГОТОВ (fail-closed): неполученный ответ не есть «да», и на пути старта это ровно тот случай, ради которого проверка заведена.
type VersionReader ¶
VersionReader — источник версии схемы. Отдельный тип затем, что решение проверяется БЕЗ базы: проба подставляет свой источник, а живой читатель строится PgxVersionReader.
func PgxVersionReader ¶
func PgxVersionReader(q Querier) VersionReader
PgxVersionReader — живой читатель. Таблица разрешается через `search_path` пула, который уже несёт схему сервиса, — второго объявления схемы здесь не заводится.