Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос
- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
This commit is contained in:
@@ -21,8 +21,36 @@ type catalogResponse struct {
|
||||
// сравнивающий разобранные структуры, этого не увидел бы — потому приёмочная
|
||||
// проверка сравнивает байты ответа. Второй страховки здесь нет намеренно:
|
||||
// подстраховка поверх подстраховки прячет отказ первой.
|
||||
//
|
||||
// Условный запрос стоит ПОСЛЕ проверки токена (её ставит роутер) и ДО сборки
|
||||
// снимка: в этом весь смысл — самый частый запрос потребителя есть повтор
|
||||
// неизменившегося, и он не должен стоить ни снимка, ни разжатия точек.
|
||||
// scopeMetrics — область действия метки каталога. Ответ маршрута не зависит от
|
||||
// параметров запроса, поэтому область постоянна; у точек и MCP на её месте
|
||||
// будет канонизированная форма запроса.
|
||||
const scopeMetrics = "metrics"
|
||||
|
||||
func (a *api) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
metrics, err := a.catalog.Metrics(r.Context())
|
||||
// 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 {
|
||||
// Исход операции логирует доменный слой, транспорт только переводит его
|
||||
// в ответ. Наружу уходит человекочитаемое сообщение, а не текст ошибки:
|
||||
@@ -30,5 +58,9 @@ func (a *api) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "каталог не собрался")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, catalogResponse{Metrics: metrics})
|
||||
// Версии может не быть — витрина изменилась, пока ответ собирался. Тогда
|
||||
// ответ уходит без метки: это ровно поведение до появления условного
|
||||
// запроса, то есть деградация в безопасную сторону.
|
||||
setReadHeaders(w, etag(scopeMetrics, snap.Version))
|
||||
writeJSON(w, http.StatusOK, catalogResponse{Metrics: snap.Metrics})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user