Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос
- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
This commit is contained in:
+53
-15
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -46,6 +47,31 @@ func runServe(args []string) error {
|
||||
return serve(ctx, cfg, logging.New(cfg.Log.Level, cfg.Log.Format), nil)
|
||||
}
|
||||
|
||||
// waitBackground ждёт выхода фоновых горутин и говорит, дождался ли.
|
||||
//
|
||||
// Отдельной функцией потому, что это единственная ветка остановки, у которой
|
||||
// есть исход, и проверить её прогоном сервиса нельзя: бюджет — тридцать секунд,
|
||||
// а заставить воркер зависнуть по требованию нечем.
|
||||
//
|
||||
// Не дождались — база НЕ закрывается: её транзакцию свернёт выход процесса, и
|
||||
// доставка останется `pending`, то есть будет подобрана следующим стартом.
|
||||
// Закрытая из-под воркера, она дала бы ERROR по доставке, с которой всё в
|
||||
// порядке.
|
||||
//
|
||||
// Этап в записи называется общим именем, а не воркером свёртки: ждём мы двоих,
|
||||
// и назвать виновным одного из них значило бы угадать. Чекпойнт при этом
|
||||
// выходит по отмене немедленно, так что практически это всё тот же воркер, — но
|
||||
// лог не должен утверждать того, чего не проверял.
|
||||
func waitBackground(shutdownCtx context.Context, done <-chan struct{}, log *slog.Logger) bool {
|
||||
select {
|
||||
case <-done:
|
||||
return true
|
||||
case <-shutdownCtx.Done():
|
||||
log.Warn("shutdown budget exceeded", "stage", "background")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// serve поднимает сервис и ведёт его до отмены контекста.
|
||||
//
|
||||
// Контекст параметром, а не подпиской на сигнал внутри: иначе весь жизненный
|
||||
@@ -119,23 +145,41 @@ func serve(ctx context.Context, cfg *config.Config, log *slog.Logger, ready func
|
||||
return fmt.Errorf("listen %q: %w", cfg.Server.Addr, err)
|
||||
}
|
||||
|
||||
workerCtx, stopWorker := context.WithCancel(context.Background())
|
||||
defer stopWorker()
|
||||
workerDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(workerDone)
|
||||
// Обе фоновые горутины живут на одном контексте и ждутся вместе. Вместе —
|
||||
// потому что база закрывается ПОСЛЕ выхода обеих: закрытая из-под воркера,
|
||||
// она даёт ERROR по доставке, с которой всё в порядке, а из-под чекпойнта —
|
||||
// отказ обслуживания на ровном месте.
|
||||
bgCtx, stopBackground := context.WithCancel(context.Background())
|
||||
defer stopBackground()
|
||||
|
||||
var bg sync.WaitGroup
|
||||
bg.Go(func() {
|
||||
// Первый проход воркера и есть подбор неразобранного при старте:
|
||||
// отдельного кода для него нет намеренно.
|
||||
worker.Run(workerCtx)
|
||||
worker.Run(bgCtx)
|
||||
})
|
||||
bg.Go(func() {
|
||||
keepWAL(bgCtx, st, log, checkpointInterval)
|
||||
})
|
||||
backgroundDone := make(chan struct{})
|
||||
go func() {
|
||||
bg.Wait()
|
||||
close(backgroundDone)
|
||||
}()
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
// Параметры обслуживания журнала — в той же строке, а не отдельной:
|
||||
// горутина, которую забыли запустить, иначе неотличима от здоровой
|
||||
// ровно до того дня, когда журнал упрётся в диск. Ноль новых строк, обе
|
||||
// константы проверяемы глазами.
|
||||
log.Info("server started",
|
||||
"addr", ln.Addr().String(),
|
||||
"db_path", cfg.Storage.DBPath,
|
||||
"archive_dir", arch.Root(),
|
||||
"max_body_mb", cfg.Ingest.MaxBodyMB)
|
||||
"max_body_mb", cfg.Ingest.MaxBodyMB,
|
||||
"wal_checkpoint_sec", int64(checkpointInterval.Seconds()),
|
||||
"wal_limit_mb", store.JournalSizeLimitMB)
|
||||
|
||||
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
errCh <- fmt.Errorf("serve: %w", err)
|
||||
@@ -172,15 +216,9 @@ func serve(ctx context.Context, cfg *config.Config, log *slog.Logger, ready func
|
||||
}
|
||||
}
|
||||
|
||||
stopWorker()
|
||||
select {
|
||||
case <-workerDone:
|
||||
stopBackground()
|
||||
if waitBackground(shutdownCtx, backgroundDone, log) {
|
||||
closeStore()
|
||||
case <-shutdownCtx.Done():
|
||||
// Воркер не вышел в бюджет. База не закрывается: её транзакцию свернёт
|
||||
// выход процесса, и доставка останется `pending` — то есть будет
|
||||
// подобрана следующим стартом.
|
||||
log.Warn("shutdown budget exceeded", "stage", "fold-worker")
|
||||
}
|
||||
return serveErr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user