Documentation
¶
Overview ¶
dialect.go — шаг «открыть базу, дождаться её готовности и настроить goose под диалект», объявленный ОДИН раз на дерево.
Предмет: седьмое объявление одного шага, а не две формы ¶
Соседний preconditions.go свёл ту половину тракта, что базы не касается. Этот шаг базы касается — и потому ждал «проб на живой базе», названных предусловием в docs/architecture/migrator-form.md. Ждал он их семь объявлений подряд: три в обёртках `services/{vpc,iam,nlb}/internal/apps/migrator/ postgres.go` (`openPgxDB`/`setupGoose`) и четыре встроенно в `services/{compute,geo,registry,storage}/cmd/migrator/main.go` (#1383).
Почему предусловие его НЕ покрывало ¶
Предусловие писалось про накат — про `goose.Up*`, отказ которого означает «сервис не разворачивается». Этот шаг цепочки НЕ применяет: он открывает соединение и ждёт готовности. Проверяется он одиночным контейнером (`opendb_integration_test.go`), а не поднятым стендом, — и мерка самого решения («часть тракта, проверяемая без стенда, выносится сразу») этим исполнена, а не обойдена.
Что расходилось, и в какую сторону ¶
Из трёх текстов отказа два были у двух форм разными, причём у прямой формы — беднее: `"open db: %w"` против `"open db (driver=%s): %w"` и `"goose dialect: %w"` против `"goose set dialect %q: %w"`. Оператор, читающий лог init-контейнера, узнавал имя драйвера и имя диалекта или не узнавал — в зависимости от того, какой сервис накатывает.
Сведено к той редакции, что НАЗЫВАЕТ ЗНАЧЕНИЕ. Довод не в возрасте строки: `"goose dialect: %w"` не говорит даже, чтение это было или установка, а `"open db:"` не отличает опечатку в имени драйвера от опечатки в DSN. Более длинная строка здесь несёт больше, и стоит это ноль.
notice.go — то, что сервер СКАЗАЛ во время наката, доезжает до оператора.
Предмет: возможность объявлена и неисполнима ¶
Миграция вправе объявить наблюдаемым то, чего оператор не увидит ни при каком вводе. Уведомление сервера библиотека отдаёт клиенту ТОЛЬКО при заданном обработчике: ветка выдачи в pgx читает непустой `OnNotice`, а слой совместимости с `database/sql` его не задаёт вовсе. Значит накат, открывающий соединение простым `sql.Open`, терял КАЖДОЕ уведомление — молча, полностью и всегда.
Производитель НЕ ТОЛЬКО `RAISE`, и это выяснилось замером ¶
Первая редакция этого файла считала производителями операторы `RAISE NOTICE` / `RAISE WARNING` в миграциях — их в дереве 18 файлов, самый разговорчивый несёт пять. Предикат был слеп к самой многочисленной форме: уведомление поднимает и САМ Postgres — на `DROP … IF EXISTS`, `CREATE … IF NOT EXISTS` и подобных («constraint … does not exist, skipping»). Опровергла посылку не проверка текста, а прогон: цепочка, выбранная как заведомо молчаливая, доставила два.
Цена не в потерянной строке, а в том, что «сказали оператору» становится неотличимо от «промолчали». Ближайший пример в дереве поимённо называет величины, перестающие действовать, и печатает перепись — чтобы «ноль перенесённого» было отличимо от «ничего не осмотрено». На штатном накате оператор не получал ни одного из этих сообщений (#2544).
Перепись печатается ВСЕГДА, и это несущее свойство ¶
Клиент не знает, сколько уведомлений сервер ПОДНЯЛ, — он знает лишь, сколько ему ДОСТАВИЛИ. Поэтому «ноль доставленных» само по себе не различает две вещи: цепочка ничего не сказала или её никто не слушал. Различает их сама перепись: строка в выводе доказывает, что обработчик стоял и был позван. Без неё правка выглядела бы исполненной ровно так же, как её отсутствие.
Предел объявлен и назван числом ¶
Один `RAISE` внутри цикла даёт столько сообщений, сколько строк в выборке, и вывод init-контейнера перестаёт читаться. Поэтому доставка ограничена сверху, а отброшенное СЧИТАЕТСЯ и попадает в перепись: подавление, о котором сказано, — не то же, что подавление молчком.
noticethreshold.go — порог, которым СЕРВЕР решает, отдавать ли уведомление, возвращается перед каждой миграцией.
Предмет: второй слой той же потери, и обработчик его НЕ закрывает ¶
Соседний notice.go закрыл клиентскую половину: без заданного обработчика библиотека роняет каждое уведомление молча (#2544). Половина серверная осталась открытой, и она сильнее: сообщение, не прошедшее порог `client_min_messages`, сервер НЕ ОТПРАВЛЯЕТ вовсе — обработчику нечего доставлять, сколь бы верно он ни стоял.
Производитель в дереве есть, назван координатой и один: `services/iam/internal/migrations/0001_initial.sql` объявляет `SET client_min_messages = warning`. `SET` без `LOCAL` переживает фиксацию своей транзакции и держится ДО КОНЦА СЕССИИ, а цепочку применяет одна сессия — значит всякое `RAISE NOTICE` каждой ПОСЛЕДУЮЩЕЙ миграции гаснет на сервере (#2560).
Почему это чинится в тракте, а не в миграции ¶
Свод применён, а применённую миграцию править запрещено (ban #5). Остаётся соединение: оно обязано вернуть себе порог, допускающий уведомление.
Точка возврата выбрана ЗАМЕРОМ устройства goose, а не наугад ¶
Легаси-путь goose применяет КАЖДУЮ миграцию отдельным `m.UpContext(ctx, db)`, то есть отдельным `db.BeginTx` (`up.go`, цикл `migrationsToApply`). Между миграциями соединение возвращается в пул, а `database/sql` зовёт `ResetSession` при следующей выдаче — там и стоит возврат. Провайдерный путь goose держит ОДНО соединение на весь прогон (`provider_run.go`, `initialize` → `db.Conn`), и на нём этот возврат не сработал бы: место возврата есть свойство того, как цепочку применяют, а не общая истина.
`RESET`, а не `SET … = notice` ¶
`RESET` возвращает величину, с которой соединение НАЧАЛОСЬ, — то есть выбор оператора (параметры DSN, настройка сервера, роль, база). Подстановка `notice` перебила бы оператора, который порог поднял НАМЕРЕННО, и накат стал бы решать за него.
Возвращается ОДИН параметр, а не `RESET ALL` ¶
Тот же свод объявляет `SET search_path TO kaname, public`, и последующие миграции на него опираются. `RESET ALL` снял бы и его — то есть починка диагностики сломала бы сам накат.
Отказ возврата — НЕ «продолжим молча» ¶
`database/sql` выбрасывает всякую ошибку `ResetSession`, кроме driver.ErrBadConn, и продолжает на том же соединении: вернуть ошибку как есть значило бы продолжить с заглушённой сессией и не сказать об этом. Ответ driver.ErrBadConn заставляет пул выбросить соединение и открыть новое, а новое начинается с порога оператора BY CONSTRUCTION — то есть отказ возврата чинит сам себя, а не проглатывается.
Package migratorcli — разбор командной строки мигратора, ОДИН на все точки наката прямой формы.
Предмет: у семи сервисов одной платформы был разный инструмент (#1461) ¶
Точек наката семь. Три делегируют своей обёртке и разбирают аргументы cobra, четыре звали goose прямо из main.go и разбирали аргументы стандартным `flag`. Разница не решалась никем — она накопилась от того, что сервисы заводились в разное время.
Оператору это стоило не косметики. `flag.Parse` останавливается на ПЕРВОМ не-флаге, поэтому флаг, написанный ПОСЛЕ подкоманды, отбрасывался без единого слова:
--dsn XXX up -> dsn="XXX" up --dsn XXX -> dsn="" ← отброшен молча
Пустой DSN уезжает в запасной путь (окружение, затем конфигурация сервиса), поэтому исход выглядит УСПЕХОМ — миграции накатаны, только не туда, куда просил оператор. Cobra принимает оба порядка, и `services/nlb/Makefile` пишет флаги именно во втором.
Что этот пакет обещает ¶
Ровно ту поверхность, которую предъявляет делегирующая тройка:
kacho-migrator [--dsn DSN] [--dialect postgres] {up|down|status} [--target VERSION]
- флаг принимается ДО и ПОСЛЕ подкоманды, с одинаковым исходом;
- неизвестный флаг, неизвестная подкоманда и лишний позиционный аргумент
отвергаются ЯВНО и называются в тексте отказа;
- `--target` живёт у up и down, у status его нет ни в одной из семи точек;
- `--help` печатает форму вызова и не является ошибкой.
Чего этот пакет НЕ делает, и почему ¶
Он не открывает базу и не зовёт goose: накат остаётся в main.go каждого сервиса. Сведение самого наката в общий пакет — предмет #1383, и у него названо предусловие (у четырёх миграторов из семи нет проб вовсе). Разбор аргументов вынесен сюда потому, что он проверяем БЕЗ базы и без стенда, — то есть его вынос не требует той сети, которой ждёт #1383.
Тексты отказов — по-английски, и это решение, а не недосмотр: делегирующая тройка печатает сообщения cobra, и смешанный язык на одной поверхности читался бы как два разных инструмента — ровно то, что задача снимает.
preconditions.go — что обязано быть заполнено ДО обращения к базе, объявлено ОДИН раз на дерево.
Предмет: один набор проверок, три текста отказа ¶
Проверки эти жили тремя копиями в `services/{vpc,iam,nlb}/internal/apps/ migrator/runner.go` (#1383). Пять из шести были побайтово одинаковы, шестая — про источник DSN — расходилась, и расхождение объяснялось тем, что каждый сервис называет свой источник.
Почему объяснение больше не действует, и это замер, а не мнение ¶
После #1461 DSN у ВСЕХ СЕМИ резолвит один ResolveDSN с приоритетом `--dsn` > EnvDSN > конфигурация сервиса. То есть два первых источника живы везде — а собственный текст `nlb` называл только ТРЕТИЙ и умалчивал ВТОРОЙ, тот, что его перебивает. Оператор `nlb`, прочитав отказ, узнавал про конфигурацию и не узнавал про переменную окружения, которая её переопределяет.
Так расхождение перестало быть «каждый прав по-своему» и стало обычной неполнотой, пережившей свой довод.
Форма, которая не даёт этому повториться ¶
Два всегда живых источника печатает ПАКЕТ, сервис объявляет лишь RunnerPreconditions.DSNExtraSources — то, что у него СВЕРХ них. Умолчать перебивающий источник теперь нельзя by construction: его печатает не сервис.
Чего здесь НЕТ и почему ¶
Самого диалекта: его тип у трёх копий разный (у двух интерфейс, у одной структура), и сведение типа тянет за собой накат — часть тракта, которая ходит в базу и потому ждёт проб (предусловие названо в `docs/architecture/migrator-form.md`). Сюда вынесено ровно то, что проверяется БЕЗ базы: заполненность полей, их порядок и тексты отказа. Вызывающий сводит свой диалект к двум значениям — задан ли он и как называется его spec.
Index ¶
- Constants
- Variables
- func DSNSourceList(extra ...string) string
- func OpenDB(ctx context.Context, dsn string, spec DialectSpec, notices *NoticeRelay) (*sql.DB, error)
- func ParseTargetVersion(s string) (int64, error)
- func ReportError(w io.Writer, err error)
- func ResolveDSN(flagDSN string, fromConfig func() (string, error)) (string, error)
- func SetupGoose(fsys fs.FS, spec DialectSpec) error
- func UnexpectedArgumentError(commandPath, given string) error
- func UnknownCommandError(binary, given string) error
- func UnknownFlagError(name string) error
- func Usage(name string) string
- type DialectSpec
- type NoticeRelay
- type Options
- type RunnerPreconditions
Constants ¶
const ( CommandUp = "up" CommandDown = "down" CommandStatus = "status" )
Подкоманды. Перечень закрыт: у всех семи точек наката он один и тот же.
const CommandHelp = "help"
CommandHelp — запрос формы вызова подкомандой. Cobra доводит эту команду к дереву сама и снять её из перечня нельзя иначе как под чужим именем, поэтому равенство семи достигнуто с этой стороны: прямая форма её тоже понимает.
const DialectPostgres = "postgres"
DialectPostgres — единственный поддерживаемый диалект. Продукт Postgres-only; флаг существует ради равенства с делегирующей формой и ради того, чтобы ЧУЖОЕ значение было названо, а не проигнорировано.
const EnvDSN = "KACHO_MIGRATOR_DSN"
EnvDSN — переменная окружения второго приоритета. Имя одно на все сервисы: оператор, знающий один, применяет знание к соседнему.
const NoticeLimit = 500
NoticeLimit — сколько уведомлений доставляется дословно за один накат.
Число выбрано замером, а не ощущением, и обе величины замера названы рядом.
ВЕРХНЯЯ ГРАНИЦА ПО ДЕРЕВУ — сколько операторов цепочки ВООБЩЕ способны поднять уведомление (`RAISE NOTICE|WARNING|INFO|LOG` плюс `IF EXISTS` / `IF NOT EXISTS`, на которых говорит сам сервер). Предикат:
for d in services/*/internal/migrations; do git ls-files "$d/*.sql" | xargs grep -oiE 'RAISE (NOTICE|WARNING|INFO|LOG)|IF (NOT )?EXISTS' | wc -l done
Разброс — от 29 (iam) до 332 (vpc); это ПОТОЛОК, а не факт: оператор `IF EXISTS`, которому нечего пропускать, молчит.
ФАКТ НА ПУСТОЙ БАЗЕ — сколько доставлено настоящим накатом настоящего бинаря: vpc 42 · nlb 9 · compute 8 · registry 8 · storage 6 · geo 2. Числа сняты пробами `internal/migratorapply/notice_integration_test.go`, и они печатают перепись на КАЖДОМ прогоне — помнить эти значения не надо, их видно. Седьмой точки (iam) здесь нет намеренно: её модуль тянет платформу пином, и до бампа доставки там не будет вовсе.
500 стоит ВЫШЕ потолка самой длинной цепочки, то есть ни одна цепочка сегодняшнего дерева его не тронет. Заведён он ради единственного случая, которого статический счёт не видит вовсе: `RAISE` внутри цикла даёт сообщение на строку выборки, и его число не ограничено ничем.
Ручкой предел НЕ настраивается намеренно: ручка без умолчания завела бы вопрос «сколько», на который у оператора нет данных, а ручка с умолчанием — второе место, где это число живёт.
Variables ¶
var ErrHelpRequested = errors.New("migratorcli: help requested")
ErrHelpRequested — оператор попросил форму вызова. Это не отказ: вызывающий печатает Usage и выходит успехом, как это делает cobra у делегирующей тройки.
var ErrNoCommand = errors.New("no command given")
ErrNoCommand — командная строка пуста. Это ОТКАЗ, а не помощь: скрипт или init-контейнер, потерявший аргумент, иначе объявляется выполнившим накат.
var SpecPostgres = DialectSpec{ Name: DialectPostgres, GooseDialect: DialectPostgres, SQLDriver: driverPgx, }
SpecPostgres — метадата единственного поддерживаемого диалекта. Продукт Postgres-only, поэтому второй диалект заводится прямой веткой, когда станет реальным требованием, — без registry-таблицы под единственный элемент (non-negotiable #11).
Name берётся из DialectPostgres, а не пишется литералом: имя диалекта уже объявлено там и печатается в помощи и в отказе разбора. Две редакции одного имени разошлись бы молча.
Functions ¶
func DSNSourceList ¶
DSNSourceList перечисляет источники DSN в порядке убывания приоритета — два общих всегда, объявленные сервисом следом.
Два общих не параметр: их читает ResolveDSN у всех семи, поэтому сервис не вправе ни отменить их, ни забыть назвать. Именно это и произошло однажды — текст назвал третий источник и умолчал второй.
func OpenDB ¶
func OpenDB(ctx context.Context, dsn string, spec DialectSpec, notices *NoticeRelay) (*sql.DB, error)
OpenDB открывает соединение, ПРОВЯЗЫВАЕТ доставку уведомлений сервера и ДОЖИДАЕТСЯ готовности сервера.
Барьер готовности — не украшение и не повтор того, что делает открытие: открытие ЛЕНИВО и до сервера не дозванивается, поэтому гонка init-контейнера с подом Postgres проявлялась не здесь, а на первой операции goose — мигратор падал отказом и уходил в CrashLoopBackOff до подъёма PG.
Ждём ТОЛЬКО «база не принимает соединения» и ТОЛЬКО в пределах бюджета; неверный пароль, несуществующая база и негодный DSN падают сразу, а не проедают бюджет ожидания.
На отказе соединение закрывается здесь: вызывающий получает ошибку и nil, и закрывать ему нечего.
notices — ОБЯЗАТЕЛЬНЫЙ параметр, и это построение, а не строгость ¶
Уведомления сервера (`RAISE NOTICE` / `RAISE WARNING` внутри миграции) доставляются только при заданном обработчике; незаданный роняет их молча и полностью. Приёмник поэтому не умолчание и не ручка, а параметр: забыть его нельзя — компилятор не даст, — а `nil` отвергается, потому что «доставка в никуда» и есть тот дефект, ради которого параметр заведён (#2544).
Почему [stdlib.OpenDB], а не регистрация имени соединения ¶
Обработчик живёт в конфигурации соединения pgx, поэтому открывать надо ЧЕРЕЗ неё. Форм две, и вторая отвергнута замером её цены, а не вкусом: `stdlib.RegisterConnConfig` кладёт конфигурацию в ПАКЕТНУЮ карту и возвращает имя для `sql.Open`, а снять запись нельзя, пока жив `*sql.DB` (пул резолвит имя на каждом новом соединении). У экспортируемой функции общего пакета это означало бы глобальное состояние, растущее с каждым вызовом, и договор об уборке, который вызывающий выполнить не может. `stdlib.OpenDB` собирает коннектор напрямую: ни карты, ни имени, ни уборки.
func ParseTargetVersion ¶
ParseTargetVersion переводит значение --target в версию goose.
Строг на ДВУХ осях, и это union строгостей двух прежних разборов, а не пересечение. До сведения (#1383) одно значение читали две функции, каждая пропускала то, что ловила соседняя:
"12abc" "-5" strconv (здесь) отказ принимал → goose.UpTo(db, dir, -5) Sscanf (копии) принимал КАК 12 отказ
Оператор получал разный исход на одном вводе в зависимости от того, какой сервис накатывает. Ослабить любую ось ради единообразия значило бы разменять живую проверку на симметрию, поэтому взято строгое с обеих сторон.
strconv, а не fmt.Sscanf: Sscanf на "12abc" возвращает 12 БЕЗ ошибки, то есть молча накатывает не туда, куда просили, — тот же класс, ради которого этот пакет заведён. Ведущие нули приняты (`0010` в имени файла — законная запись).
func ReportError ¶
ReportError печатает отказ в форме, одной на семь: `Error: <предмет>`.
Форма взята у делегирующей тройки. Прямая четвёрка печатала отказ через журнал, то есть с меткой времени впереди, — для однократного инструмента командной строки это шум, и он же делал две редакции из одной. Возврат записи здесь намеренно не поднимается выше: печать отказа — последнее, что делает инструмент перед выходом с ненулевым кодом, и сообщать о неудаче печати уже некуда. Отбрасывание сделано ЯВНЫМ, чтобы следующий читатель видел решение, а не пропущенную проверку.
func ResolveDSN ¶
ResolveDSN выбирает DSN: --dsn > ENV EnvDSN > конфигурация сервиса.
fromConfig принадлежит вызывающему — набор переменных у каждого сервиса свой, и общий пакет не вправе называть оператору чужое имя. Отказ конфигурации доезжает наружу: пустой DSN, полученный молча, означает накат в никуда.
func SetupGoose ¶
func SetupGoose(fsys fs.FS, spec DialectSpec) error
SetupGoose наводит goose на набор миграций и на диалект.
Обе операции пакетно-глобальны у самого goose, поэтому параллельный накат разными диалектами из одного процесса не поддерживается. CLI гоняет одну команду за раз — это и есть та посадка, под которую шаг написан.
func UnexpectedArgumentError ¶
UnexpectedArgumentError — лишний позиционный аргумент. `up 800001` — обычная догадка о том, как задать версию, и отказ называет верный способ.
func UnknownCommandError ¶
UnknownCommandError — подкоманда не из закрытого перечня. Перечень назван в самом отказе: отказ обязан восстанавливать следующий шаг оператора.
func UnknownFlagError ¶
UnknownFlagError — флаг вне набора.
Формулировка ЗАИМСТВОВАНА у делегирующей формы дословно: её производит библиотека разбора, переписать её там нельзя, а два текста об одном предмете разошлись бы молча. Стандартная библиотека говорит иначе («flag provided but not defined: -X»), поэтому её формулировка здесь переводится в общую.
Types ¶
type DialectSpec ¶
type DialectSpec struct {
// Name — имя диалекта для CLI (`--dialect`).
Name string
// GooseDialect — строка, ожидаемая goose.SetDialect.
GooseDialect string
// SQLDriver — имя драйвера, которым открывается соединение.
//
// # Здесь стояло «общий пакет драйвер не тянет» — с #2544 это неверно
//
// Прежняя редакция объявляла выбор драйвера свойством бинаря: точка наката
// регистрировала его blank-импортом, а общий шаг лишь называл имя. Держать
// это дальше нельзя, и причина не в удобстве: доставка уведомлений сервера
// живёт НА УРОВНЕ ДРАЙВЕРА (обработчик задаётся в конфигурации соединения
// pgx, слой совместимости с `database/sql` его не выставляет вовсе). Шаг,
// который драйвера не знает, доставить их не может — и не доставлял ни разу.
//
// Поэтому поле сменило роль: оно больше не выбирает драйвер, а ОБЪЯВЛЯЕТ
// ожидаемый, и [OpenDB] на любом другом имени ОТКАЗЫВАЕТ. Это fail-closed, а
// не педантизм: второй диалект, заведённый молча, применил бы цепочку и
// выбросил каждое сказанное сервером слово — тот же дефект, только уже
// объяснённый в коде.
//
// Blank-импорты `_ "github.com/jackc/pgx/v5/stdlib"` в семи точках наката
// после этого несущими быть перестали (драйвер тянет сам общий пакет), но
// сняты НЕ БУДУТ: `services/iam` — отдельный Go-модуль, пиненный на прежнюю
// ревизию платформы, и снятие импорта там сломало бы сборку до бампа пина.
// Шесть из семи без седьмого — расхождение, а не уборка.
SQLDriver string
}
DialectSpec — описательная метадата диалекта: имя для CLI, имя для goose и имя драйвера для sql.Open.
Это НЕ runtime-поведение: применение цепочки живёт у вызывающего. Тип нужен, чтобы OpenDB и SetupGoose принимали три имени одним значением, а не тремя строками, которые легко переставить местами.
func ResolveDialectSpec ¶
func ResolveDialectSpec(name string) (DialectSpec, error)
ResolveDialectSpec возвращает метадату диалекта по имени, названному оператором. Поддерживается один — DialectPostgres; любое другое имя даёт отказ, называющий поддерживаемое.
Пустое имя ОТВЕРГАЕТСЯ, и это решение ¶
Фабрик диалекта было три, и они разошлись: две отвергали пустое имя, третья принимала его как умолчание (`case "", …`) — живая строка ведомости различий `dialect-empty-accepted` (#1383). Различие снято в пользу отказа, потому что умолчание подставляет РАЗБОР аргументов (Parse и флаг cobra), и до сюда пустая строка доезжает только как явное `--dialect ""` — то есть как названный и неподдерживаемый диалект, а не как несказанный.
Текст отказа тот же, каким его печатает Parse: две редакции одной строки разошлись бы молча, и один и тот же вход отвечал бы оператору по-разному в зависимости от того, где его перехватили.
type NoticeRelay ¶
type NoticeRelay struct {
// contains filtered or unexported fields
}
NoticeRelay доставляет оператору то, что сервер сказал во время наката, и ведёт счёт доставленному и отброшенному.
Один экземпляр на одно открытое соединение: обработчик зовётся из горутин пула, поэтому счёт и запись идут под замком.
func NewNoticeRelay ¶
func NewNoticeRelay(service string, out io.Writer) *NoticeRelay
NewNoticeRelay заводит доставку уведомлений службы service в out.
service — чей накат: вывод init-контейнеров нескольких служб читается вместе, и строка, не называющая себя, адресована никому. out обязателен: доставка «в никуда» и есть то состояние, ради которого этот файл заведён.
func (*NoticeRelay) Delivered ¶
func (r *NoticeRelay) Delivered() int
Delivered / Suppressed — обе половины счёта. Порознь ни одна ничего не значит: «доставлено 3» без второго числа не говорит, было ли отброшено что-нибудь.
func (*NoticeRelay) Handler ¶
func (r *NoticeRelay) Handler() func(*pgconn.PgConn, *pgconn.Notice)
Handler — обработчик для pgx. Отдаётся значением, потому что библиотека требует функцию, а не интерфейс.
func (*NoticeRelay) Summary ¶
func (r *NoticeRelay) Summary() string
Summary — перепись одной строкой: сколько доставлено, сколько отброшено и каков предел.
На нуле строка договаривает то, чего числа сказать не могут: обработчик стоял. Без этого хвоста «0 delivered» читалось бы как состояние ДО правки, когда обработчика не было вовсе, — то есть перепись подтверждала бы ровно тот дефект, который она заведена наблюдать.
Хвост НЕ утверждает, что цепочка молчала, — он этого не знает ¶
Прежняя редакция говорила «and the chain raised nothing». Клиент такого факта не имеет: сообщение, не прошедшее порог `client_min_messages`, сервер не отправляет вовсе, и приёмнику оно неотличимо от несказанного. Наблюдалось дословно: цепочка, поднявшая уведомление после `SET client_min_messages = warning`, дала перепись «0 delivered … the chain raised nothing» — то есть утверждение о цепочке, сделанное вместо утверждения о доставке (#2560).
Поэтому хвост называет ОБА условия нуля: обработчик стоял, а порог сессии ничего выше себя не пропустил. Возврат порога живёт в noticethreshold.go и делает первый случай штатным, но не отменяет второго: порог вправе поднять сам оператор.
func (*NoticeRelay) Suppressed ¶
func (r *NoticeRelay) Suppressed() int
func (*NoticeRelay) WriteCensus ¶
func (r *NoticeRelay) WriteCensus()
WriteCensus печатает перепись туда же, куда шли сами уведомления.
Назначение НЕ параметр: перепись и то, что она считает, — один отчёт, и разведённые по разным потокам половины читались бы как два разных.
type Options ¶
type Options struct {
// Command — одна из [CommandUp], [CommandDown], [CommandStatus].
Command string
// DSN — значение флага --dsn; пустое означает «не задан», см. [ResolveDSN].
DSN string
// Dialect — всегда [DialectPostgres]; чужое значение до сюда не доходит.
Dialect string
// Target — версия, до которой накатывать или откатывать; пустое означает
// «до головы» (up) либо «на шаг назад» (down).
Target string
}
Options — разобранная командная строка.
type RunnerPreconditions ¶
type RunnerPreconditions struct {
// Service — имя сервиса, чью цепочку применяет накат. Обязательное: без
// него живой счёт строк перед сносом не может назвать, что он стережёт, а
// безымянный отказ в логе init-контейнера некому адресовать.
Service string
// DialectSet — диалект задан. Отдельным флагом, а не самим диалектом:
// тип диалекта у сервисов разный, а вопрос к нему здесь один.
DialectSet bool
// DialectSpecName — имя spec'а заданного диалекта. Читается вызывающим
// ТОЛЬКО когда DialectSet, иначе это разыменование nil.
DialectSpecName string
// DSN — строка подключения.
DSN string
// DSNExtraSources — источники DSN СВЕРХ двух общих (`--dsn` и [EnvDSN]),
// перечисленные в порядке убывания приоритета. Пусто у сервиса, который
// ничем сверх них DSN не заполняет.
DSNExtraSources []string
// MigrationsFSSet — файловая система с миграциями передана.
MigrationsFSSet bool
// MigrationsDir — путь внутри неё; для корня embed — ".".
MigrationsDir string
}
RunnerPreconditions — поля, которые накат обязан иметь до первого обращения к базе, в форме, не зависящей от типа диалекта конкретного сервиса.
func (RunnerPreconditions) Validate ¶
func (p RunnerPreconditions) Validate() error
Validate проверяет предусловия в том порядке, в каком их обязан читать вызывающий. Порядок — часть контракта: «диалект задан» стоит раньше «как он называется» ровно потому, что второе на незаданном диалекте не вычисляется.