- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
67 lines
4.3 KiB
Go
67 lines
4.3 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"net/http"
|
||
|
||
"git.vakhrushev.me/av/healthlog/internal/catalog"
|
||
)
|
||
|
||
// catalogResponse — оболочка ответа каталога.
|
||
//
|
||
// Объект, а не голый массив: список метрик — не единственное, что каталогу
|
||
// когда-нибудь придётся отдать, а массив верхнего уровня расширить нечем.
|
||
type catalogResponse struct {
|
||
Metrics []catalog.Metric `json:"metrics"`
|
||
}
|
||
|
||
// handleMetrics отдаёт каталог разрезов с измеренным родом агрегации.
|
||
//
|
||
// Список приходит из домена уже непустым срезом: nil сериализуется в `null`, и
|
||
// пустая витрина отдавала бы клиенту `"metrics": null` вместо `[]`. Тест,
|
||
// сравнивающий разобранные структуры, этого не увидел бы — потому приёмочная
|
||
// проверка сравнивает байты ответа. Второй страховки здесь нет намеренно:
|
||
// подстраховка поверх подстраховки прячет отказ первой.
|
||
//
|
||
// Условный запрос стоит ПОСЛЕ проверки токена (её ставит роутер) и ДО сборки
|
||
// снимка: в этом весь смысл — самый частый запрос потребителя есть повтор
|
||
// неизменившегося, и он не должен стоить ни снимка, ни разжатия точек.
|
||
// scopeMetrics — область действия метки каталога. Ответ маршрута не зависит от
|
||
// параметров запроса, поэтому область постоянна; у точек и MCP на её месте
|
||
// будет канонизированная форма запроса.
|
||
const scopeMetrics = "metrics"
|
||
|
||
func (a *api) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||
// Values, а не Get: `If-None-Match` клиент вправе прислать несколькими
|
||
// строками, и `Get` увидел бы только первую — часть меток осталась бы
|
||
// нерассмотренной.
|
||
if cond := r.Header.Values("If-None-Match"); len(cond) > 0 {
|
||
// Отказ пробы глушится намеренно: он означает лишь, что условного
|
||
// ответа не будет, — а настоящий отказ базы всплывёт сборкой каталога
|
||
// строкой ниже и будет назван ею один раз.
|
||
// Версию спрашиваем У КАТАЛОГА, а не у хранилища: ответ есть функция не
|
||
// только состояния витрины, и что ещё в него входит, знает домен. Read
|
||
// API точек ответит здесь же своей версией, включающей параметры
|
||
// запроса, — транспорту эти правила знать незачем.
|
||
if version, err := a.catalog.Version(r.Context()); err == nil {
|
||
if tag := etag(scopeMetrics, version); notModified(cond, tag) {
|
||
writeNotModified(w, tag)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
snap, err := a.catalog.Metrics(r.Context())
|
||
if err != nil {
|
||
// Исход операции логирует доменный слой, транспорт только переводит его
|
||
// в ответ. Наружу уходит человекочитаемое сообщение, а не текст ошибки:
|
||
// в нём имена колонок и форма запроса.
|
||
writeError(w, http.StatusInternalServerError, "каталог не собрался")
|
||
return
|
||
}
|
||
// Версии может не быть — витрина изменилась, пока ответ собирался. Тогда
|
||
// ответ уходит без метки: это ровно поведение до появления условного
|
||
// запроса, то есть деградация в безопасную сторону.
|
||
setReadHeaders(w, etag(scopeMetrics, snap.Version))
|
||
writeJSON(w, http.StatusOK, catalogResponse{Metrics: snap.Metrics})
|
||
}
|