Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос

- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал
  пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не
  только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под
  удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а
  при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как
  «разобрано целиком»
- каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка
  собрана из всего, от чего зависит ответ: версии витрины (`data_version` с
  закреплённого соединения плюс поколение — значение локально для соединения и
  не переживает переоткрытия), горизонта измерения и области действия ресурса.
  Версия снимается до и после сборки: снятая после пометила бы устаревший снимок
  свежим номером
- предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной
  ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший
  значение точки в сыром буфере записи лога
This commit is contained in:
av
2026-08-02 20:42:22 +03:00
parent 6b729bbd2f
commit 8db2ec7ff4
37 changed files with 3575 additions and 156 deletions
+50 -2
View File
@@ -10,6 +10,7 @@ import (
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@@ -294,6 +295,16 @@ func newAPI(t *testing.T, writeTokens []string) (http.Handler, *store.Store) {
// переписывать два десятка вызовов ради одного параметра.
func newAPITokens(t *testing.T, writeTokens, readTokens []string) (http.Handler, *store.Store, *catalog.Service) {
t.Helper()
h, st, cat, _ := newAPILogged(t, writeTokens, readTokens)
return h, st, cat
}
// newAPILogged отдаёт ещё и записи лога. Нужен там, где лог служит ОРАКУЛОМ, а
// не наблюдением: единственный внешний признак того, что каталог собирался, —
// его предупреждения владельцу.
func newAPILogged(t *testing.T, writeTokens, readTokens []string) (http.Handler, *store.Store, *catalog.Service, *records) {
t.Helper()
dir := t.TempDir()
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
@@ -307,7 +318,8 @@ func newAPITokens(t *testing.T, writeTokens, readTokens []string) (http.Handler,
t.Fatalf("archive.New: %v", err)
}
log := slog.New(slog.DiscardHandler)
seen := &records{}
log := slog.New(seen)
cat := catalog.New(st, log)
h := httpapi.New(httpapi.Options{
Ingest: ingest.New(arch, st, nil, log),
@@ -320,5 +332,41 @@ func newAPITokens(t *testing.T, writeTokens, readTokens []string) (http.Handler,
// и это ровно тот транспорт, на котором приём обязан продолжать работать.
IngestWriteBudget: time.Minute,
})
return h, st, cat
return h, st, cat, seen
}
// records — slog.Handler, копящий сообщения. Значений атрибутов не хранит:
// проверяется факт записи, а данные о здоровье в тесты тащить незачем.
type records struct {
mu sync.Mutex
msg []string
}
func (r *records) Enabled(context.Context, slog.Level) bool { return true }
func (r *records) Handle(_ context.Context, rec slog.Record) error {
r.mu.Lock()
defer r.mu.Unlock()
r.msg = append(r.msg, rec.Message)
return nil
}
func (r *records) WithAttrs([]slog.Attr) slog.Handler { return r }
func (r *records) WithGroup(string) slog.Handler { return r }
func (r *records) has(msg string) bool {
r.mu.Lock()
defer r.mu.Unlock()
for _, m := range r.msg {
if m == msg {
return true
}
}
return false
}
func (r *records) reset() {
r.mu.Lock()
defer r.mu.Unlock()
r.msg = nil
}