Help us shape the future of Go! Take the 2026 Go Developer Survey

schemaguard

package
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 9 Imported by: 1

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

View Source
const CheckerName = "schema-version"

CheckerName — одно имя на все семь точек провязки. Имя видно оператору в теле `/readyz`, поэтому оно объявлено здесь, а не переписывается в каждом корне.

View Source
const PointOfNoReturnMarker = "-- +kacho point-of-no-return:"

PointOfNoReturnMarker — признак, которым миграция объявляет: после неё ПРЕЖНИЙ образ схему обслуживать не может.

Токен объявлен ЗДЕСЬ, в прод-коде, и отсюда же его читает гейт `internal/repohygiene`, требующий признака на коммите. Два места об одном имени разошлись бы молча — и разошлись бы там, где расхождение не видно: автор прочитал бы в подсказке гейта одно, а страж старта искал бы другое.

Variables

View Source
var ErrEmptySet = errors.New("schemaguard: embedded migration set is empty")

ErrEmptySet — набор миграций пуст. Это НЕ «версия 0»: сервис со встроенным набором обязан его нести, а пустой набор означает сломанную сборку (директива `embed` перестала что-либо захватывать) — и молчаливая «версия 0» сделала бы такой образ готовым на любой схеме.

Functions

func CheckFromFS

func CheckFromFS(fsys fs.FS, read VersionReader) func(context.Context) error

CheckFromFS — провязка одной строкой: разбор встроенного набора плюс тело проверки.

Почему сломанный набор даёт ВЕЧНУЮ НЕГОТОВНОСТЬ, а не отказ старта

Отказ старта был бы виден только в журнале и событиях пода, а неготовность НАЗЫВАЕТ ПРИЧИНУ в теле `/readyz` — там же, где оператор читает остальные зависимости. Обе формы снимают под из ротации; различаются они тем, что вторая отвечает на вопрос «почему», не требуя чтения кода.

Молчаливого прохода здесь нет ни при каком входе: разобрать набор не удалось — значит образ не может установить, что способен обслужить схему, и это тот же fail-closed, что у первого правила.

func DeclaresPointOfNoReturn

func DeclaresPointOfNoReturn(body string) bool

DeclaresPointOfNoReturn — объявляет ли тело миграции точку невозврата.

Пустое обоснование признаком НЕ считается: иначе токен становится печатью, которую ставят не читая.

Types

type Querier

type Querier interface {
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

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

func Describe(fsys fs.FS) (Set, error)

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): неполученный ответ не есть «да», и на пути старта это ровно тот случай, ради которого проверка заведена.

func (Set) Verdict

func (s Set) Verdict(dbVersion int64) Result

Verdict — решение. Чистая функция: ни сети, ни диска, ни часов.

type VersionReader

type VersionReader func(ctx context.Context) (int64, error)

VersionReader — источник версии схемы. Отдельный тип затем, что решение проверяется БЕЗ базы: проба подставляет свой источник, а живой читатель строится PgxVersionReader.

func PgxVersionReader

func PgxVersionReader(q Querier) VersionReader

PgxVersionReader — живой читатель. Таблица разрешается через `search_path` пула, который уже несёт схему сервиса, — второго объявления схемы здесь не заводится.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL