schemaguard

package
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

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