- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
151 lines
8.0 KiB
Go
151 lines
8.0 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"net/http"
|
||
"net/textproto"
|
||
"strings"
|
||
)
|
||
|
||
// Условный запрос читающих маршрутов. Помощник общий намеренно: тот же ответ
|
||
// понадобится точкам и MCP, а протокол здесь ровно такой, каким его описывает
|
||
// HTTP, — второй его экземпляр разошёлся бы с первым в мелочи вроде слабого
|
||
// сравнения.
|
||
//
|
||
// ГРАНИЦА, которую обязан знать следующий потребитель: метка действительна
|
||
// только в пределах одного адреса ресурса. У каталога ответ зависит лишь от
|
||
// состояния витрины, поэтому версии достаточно; у Read API точек ответ есть
|
||
// функция параметров запроса, а у MCP адреса нет вовсе — там в метку обязана
|
||
// входить канонизированная форма запроса, иначе «не изменилось» ответит на
|
||
// другой набор данных.
|
||
|
||
// etag собирает метку HTTP из области действия и версии ответа.
|
||
//
|
||
// Область — параметр, а не забота вызывающего: она и есть то, что помощник
|
||
// обязан не дать забыть. Метка действительна в пределах ОДНОГО ресурса, и
|
||
// маршрут, чей ответ зависит от параметров запроса (Read API точек) или у
|
||
// которого адреса нет вовсе (MCP), кладёт сюда их канонизированную форму.
|
||
// Прозой это уже было написано — и прозу компилятор не проверяет: `etag(v)` у
|
||
// второго маршрута собрался бы и отдал `304` на чужой набор данных.
|
||
//
|
||
// Метка СЛАБАЯ, и это не осторожность. Ответ есть функция не только снимка, но
|
||
// и горизонта измерения (`Now()` плюс час), а горизонт едет вместе с часами:
|
||
// пока в витрине нет меток из будущего, ход часов ответ не двигает, но метки из
|
||
// будущего в ней возможны — сбитые часы телефона, чужое тело в приёме. Слабая
|
||
// метка это допускает, сильная обещала бы побайтовое равенство, которого в этом
|
||
// случае нет. На исход `304` форма не влияет: `If-None-Match` сравнивается
|
||
// слабо в любом случае.
|
||
func etag(scope, version string) string {
|
||
if version == "" {
|
||
return ""
|
||
}
|
||
return `W/"` + scope + "." + version + `"`
|
||
}
|
||
|
||
// notModified отвечает, покрывает ли условие запроса текущую метку.
|
||
//
|
||
// Сравнение слабое: `W/"x"` и `"x"` — одна и та же метка, так предписывает
|
||
// HTTP для `If-None-Match`. Звёздочка совпадает с любой существующей меткой.
|
||
//
|
||
// Неразбираемое значение условия — не отказ, а невыполненное условие: клиент,
|
||
// приславший мусор, получает данные, а не `400`. Пустая метка (подписать ответ
|
||
// нечем) не совпадает ни с чем, включая звёздочку: подтверждать неизменность
|
||
// нечем.
|
||
//
|
||
// Алгоритм повторяет `net/http/fs.go` (`scanETag`, `etagWeakMatch`,
|
||
// `checkIfNoneMatch`): там он есть, но неэкспортирован, и копия дешевле
|
||
// зависимости. Копия ПОЛНАЯ, включая сканер: резать список по запятой нельзя —
|
||
// запятая законный символ внутри метки, а в метку читающего маршрута once
|
||
// попадёт канонизированная форма запроса, где запятая естественна. Тогда `304`
|
||
// перестал бы срабатывать вообще, и симптом («условный запрос не экономит»)
|
||
// увёл бы отладку в маршрут, а не в помощника.
|
||
func notModified(header []string, tag string) bool {
|
||
if tag == "" {
|
||
return false
|
||
}
|
||
for _, line := range header {
|
||
for {
|
||
line = textproto.TrimString(line)
|
||
if line == "" {
|
||
break
|
||
}
|
||
if line[0] == ',' {
|
||
line = line[1:]
|
||
continue
|
||
}
|
||
if line[0] == '*' {
|
||
// Звёздочка совпадает с ЛЮБОЙ существующей меткой, а не с любым
|
||
// состоянием: метки нет — условие не выполнено, и клиент со
|
||
// звёздочкой получает данные, а не вечный `304`.
|
||
return true
|
||
}
|
||
candidate, remain := scanETag(line)
|
||
if candidate == "" {
|
||
// Нечитаемая метка прекращает разбор строки: так делает stdlib,
|
||
// и это не отказ — условие просто не выполнено.
|
||
break
|
||
}
|
||
if strings.TrimPrefix(candidate, `W/`) == strings.TrimPrefix(tag, `W/`) {
|
||
return true
|
||
}
|
||
line = remain
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// scanETag откусывает метку от начала строки и отдаёт остаток. Копия
|
||
// `net/http/fs.go`; диапазоны символов — `etagc` из RFC 9110, в них ВХОДИТ
|
||
// запятая.
|
||
func scanETag(s string) (tag, remain string) {
|
||
s = textproto.TrimString(s)
|
||
start := 0
|
||
if strings.HasPrefix(s, "W/") {
|
||
start = 2
|
||
}
|
||
if len(s[start:]) < 2 || s[start] != '"' {
|
||
return "", ""
|
||
}
|
||
for i := start + 1; i < len(s); i++ {
|
||
c := s[i]
|
||
switch {
|
||
case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
|
||
// шум внутри метки — законные символы
|
||
case c == '"':
|
||
return s[:i+1], s[i+1:]
|
||
default:
|
||
return "", ""
|
||
}
|
||
}
|
||
return "", ""
|
||
}
|
||
|
||
// writeNotModified отвечает `304`: та же метка, никакого тела.
|
||
//
|
||
// Представленческие заголовки снимаются — так делает и stdlib
|
||
// (`net/http/fs.go`, `writeNotModified`) со ссылкой на RFC 9110: у ответа без
|
||
// тела нечего описывать, а `Content-Length`, доживший до `304`, вводит в
|
||
// заблуждение любой кеш. Тело рантайм и так не пропустит
|
||
// (`bodyAllowedForStatus`), но полагаться на это значило бы получать
|
||
// проглоченную ошибку записи вместо ответа.
|
||
func writeNotModified(w http.ResponseWriter, tag string) {
|
||
h := w.Header()
|
||
h.Del("Content-Type")
|
||
h.Del("Content-Length")
|
||
h.Del("Content-Encoding")
|
||
setReadHeaders(w, tag)
|
||
w.WriteHeader(http.StatusNotModified)
|
||
}
|
||
|
||
// setReadHeaders вешает на ответ чтения метку и правило кеширования.
|
||
//
|
||
// `private, no-cache` означает «кешируй, но каждый раз спрашивай»: ровно то,
|
||
// ради чего заведена метка. Заголовок обязателен, а не желателен, — без него
|
||
// промежуточный кеш вправе решить по эвристике, что выгрузку истории здоровья
|
||
// можно подержать у себя.
|
||
func setReadHeaders(w http.ResponseWriter, tag string) {
|
||
w.Header().Set("Cache-Control", "private, no-cache")
|
||
if tag != "" {
|
||
w.Header().Set("ETag", tag)
|
||
}
|
||
}
|