Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос
- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
This commit is contained in:
+75
-11
@@ -204,6 +204,54 @@ type Metric struct {
|
||||
Layers []LayerRange `json:"layers"`
|
||||
}
|
||||
|
||||
// Snapshot — каталог вместе с версией ответа.
|
||||
//
|
||||
// Версия пустая, когда подписать ответ нечем: витрина изменилась, пока он
|
||||
// собирался, или прочитать её версию не удалось. Это не отказ — ответ уходит
|
||||
// целиком, просто без условной метки, ровно как до появления условного запроса.
|
||||
//
|
||||
// Имя перекликается с `store.CatalogSnapshot` намеренно и означает другое: тот
|
||||
// снимок — вход измерения (объекты и разрезы), этот — готовый ответ.
|
||||
type Snapshot struct {
|
||||
Version string
|
||||
Metrics []Metric
|
||||
}
|
||||
|
||||
// Version — версия ОТВЕТА каталога: версия витрины плюс горизонт измерения.
|
||||
//
|
||||
// Горизонт входит в неё, потому что ответ есть функция обоих. Час объекта
|
||||
// сравнивается с `Now() + horizonSlack`, и с ходом часов состав окна меняется
|
||||
// без единого коммита: метка из будущего, лежащая в витрине (сбитые часы
|
||||
// телефона — состояние, о котором рядом пишется WARN), въезжает в окно сама и
|
||||
// способна перевернуть измеренный род. Построено и прогнано: та же версия
|
||||
// витрины, `cumulative` против `unknown`, ноль коммитов между.
|
||||
//
|
||||
// Огрубление до часа не приблизительное, а точное: `hour_utc` объектов лежит
|
||||
// ровно на часах, поэтому отбор `hour_utc <= горизонт` меняется ровно при
|
||||
// переходе горизонта через час. Цена — один полный ответ в час на потребителя
|
||||
// при неизменившейся витрине; сборка каталога на живом корпусе стоит 45 мс.
|
||||
func (s *Service) Version(ctx context.Context) (string, error) {
|
||||
version, err := s.store.StateVersion(ctx)
|
||||
if err != nil {
|
||||
// Отказ пробы отказом маршрута не является — но и молчать о нём нельзя:
|
||||
// без метки условный запрос выключается для всех потребителей, а
|
||||
// снаружи это неотличимо от нормы. DEBUG, потому что адресат здесь
|
||||
// разработчик: владельцу об этом скажет `/stats`, когда появится.
|
||||
s.log.DebugContext(ctx, "state version unavailable", "capability", "query", "error", err)
|
||||
return "", err
|
||||
}
|
||||
return stamp(version, store.Now().Add(horizonSlack)), nil
|
||||
}
|
||||
|
||||
// stamp склеивает версию витрины с горизонтом. Пустая версия остаётся пустой:
|
||||
// подписывать нечем — значит нечем, и горизонт этого не меняет.
|
||||
func stamp(version string, horizon time.Time) string {
|
||||
if version == "" {
|
||||
return ""
|
||||
}
|
||||
return version + "." + horizon.Truncate(time.Hour).Format("2006010215")
|
||||
}
|
||||
|
||||
// Service собирает каталог по витрине.
|
||||
type Service struct {
|
||||
store *store.Store
|
||||
@@ -223,17 +271,26 @@ func New(st *store.Store, log *slog.Logger) *Service {
|
||||
// и объяснять, на каком составе данных оно снято; устаревшее при этом выглядит
|
||||
// ровно как свежее. Вычисленный на запрос род есть функция витрины, а витрина —
|
||||
// функция журнала, и устаревать в нём нечему.
|
||||
func (s *Service) Metrics(ctx context.Context) ([]Metric, error) {
|
||||
// Ответ подписывается версией витрины: она нужна условному запросу, и снимает
|
||||
// её хранилище — двумя пробами вокруг чтения. Порядок проб там же и объяснён:
|
||||
// версия, снятая после чтения, пометила бы устаревший снимок свежей меткой.
|
||||
func (s *Service) Metrics(ctx context.Context) (Snapshot, error) {
|
||||
horizon := store.Now().Add(horizonSlack)
|
||||
snap, err := s.store.ReadCatalog(ctx, store.CatalogWindow{
|
||||
Fine: string(hae.LayerMinute),
|
||||
Coarse: string(hae.LayerHour),
|
||||
Hours: Window,
|
||||
Horizon: horizon,
|
||||
CoarsePoints: coarsePoints,
|
||||
MinFinePoints: minFinePoints,
|
||||
|
||||
var snap store.CatalogSnapshot
|
||||
version, err := s.store.VersionedRead(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
snap, err = s.store.ReadCatalog(ctx, store.CatalogWindow{
|
||||
Fine: string(hae.LayerMinute),
|
||||
Coarse: string(hae.LayerHour),
|
||||
Hours: Window,
|
||||
Horizon: horizon,
|
||||
CoarsePoints: coarsePoints,
|
||||
MinFinePoints: minFinePoints,
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
if err != nil { //nolint:nestif // ветка одна, вложенность даёт лог по адресату
|
||||
// Единственный логирующий чекпоинт исхода: транспорт переводит ошибку в
|
||||
// ответ и второй раз её не пишет.
|
||||
//
|
||||
@@ -247,7 +304,7 @@ func (s *Service) Metrics(ctx context.Context) ([]Metric, error) {
|
||||
} else {
|
||||
s.log.ErrorContext(ctx, "catalog failed", "capability", "query", "error", err)
|
||||
}
|
||||
return nil, err
|
||||
return Snapshot{}, err
|
||||
}
|
||||
|
||||
out := make([]Metric, 0, len(snap.Layers))
|
||||
@@ -286,7 +343,14 @@ func (s *Service) Metrics(ctx context.Context) ([]Metric, error) {
|
||||
Layers: group.layers,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
if version == "" {
|
||||
// Витрина изменилась, пока ответ собирался (или версию не прочитать).
|
||||
// Ответ уйдёт без метки — это безопасная сторона, но след нужен: под
|
||||
// плотным потоком доставок так может уходить каждый ответ, и тогда
|
||||
// механизм не окупается вовсе.
|
||||
s.log.DebugContext(ctx, "catalog unsigned", "capability", "query")
|
||||
}
|
||||
return Snapshot{Version: stamp(version, horizon), Metrics: out}, nil
|
||||
}
|
||||
|
||||
type metricGroup struct {
|
||||
|
||||
Reference in New Issue
Block a user