- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
113 lines
8.0 KiB
Go
113 lines
8.0 KiB
Go
package store
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
)
|
||
|
||
// checkpointQuery — пассивный чекпойнт журнала WAL.
|
||
//
|
||
// Режим PASSIVE, а не TRUNCATE или RESTART, и причина измерена: TRUNCATE
|
||
// двигает `data_version`, то есть каждый чекпойнт обнулял бы условный запрос у
|
||
// всех потребителей. PASSIVE не двигает его даже перенося 12502 страницы.
|
||
// Второй довод известнее: PASSIVE ничего не ждёт — ни читателей, ни писателей.
|
||
const checkpointQuery = `PRAGMA wal_checkpoint(PASSIVE)`
|
||
|
||
// pageSizeQuery — размер страницы базы. Свойство ФАЙЛА, зафиксированное при его
|
||
// создании, а не настройка соединения: журнал считается в страницах, а предел
|
||
// файла назван в байтах, и без этого числа их не связать.
|
||
const pageSizeQuery = `PRAGMA page_size`
|
||
|
||
// Checkpoint — исход одного чекпойнта: сколько страниц лежало в журнале и
|
||
// сколько из них перенесено в базу.
|
||
//
|
||
// Отдаётся без интерпретации: решение, считать ли это бедой, принимает не
|
||
// хранилище. Знать при этом надо обе величины, а не одну — признаком служит
|
||
// именно их расхождение.
|
||
type Checkpoint struct {
|
||
// Busy — SQLite не смог взять блокировку чекпойнта. Признаком беды флаг НЕ
|
||
// является: измерено `busy=0` при 6256 страницах в журнале и пяти
|
||
// перенесённых — обработчик занятости в пассивном режиме не зовётся, и
|
||
// «не продвинулись» флагом не выражается.
|
||
//
|
||
// Зато он выражает другое, и это обязано читаться: при `busy=1` SQLite
|
||
// отдаёт `log = checkpointed = -1`, то есть исход НЕ ИЗМЕРЕН. Воспроизведено
|
||
// шестью соединениями, чекпойнтящими один файл: 200 ответов `(1, -1, -1)`
|
||
// против 40 измеренных.
|
||
Busy bool
|
||
// Log — страниц в журнале, Checkpointed — из них перенесено в базу.
|
||
// Равенство означает, что журнал разобран целиком. Отрицательные значения
|
||
// означают «не измерено» (см. Busy) и на шкале страниц не сравниваются.
|
||
Log int
|
||
Checkpointed int
|
||
// PageSize — размер страницы этой базы в байтах. Ноль означает **«не
|
||
// измерено»** и нулём не является: без него страницы не перевести в байты,
|
||
// и признак беды молчит, а не гадает.
|
||
PageSize int
|
||
}
|
||
|
||
// Known отвечает, измерен ли исход вообще.
|
||
//
|
||
// SQLite отдаёт `-1` там, где ответа нет: чекпойнт не взял блокировку либо
|
||
// журнала не существует. Внутриполосный признак («-1 на шкале страниц») —
|
||
// ровно тот приём, который Effective Go называет неуклюжим, и он опасен
|
||
// буквально: `-1 >= -1` истинно, то есть незамеренный исход читался бы как
|
||
// «журнал разобран целиком», а владельцу уходила бы строка о выздоровлении
|
||
// посреди болезни, с числом, которого не бывает.
|
||
func (c Checkpoint) Known() bool { return !c.Busy && c.Log >= 0 && c.Checkpointed >= 0 }
|
||
|
||
// Complete отвечает, разобран ли журнал целиком. Неизмеренный исход
|
||
// разобранным не считается — «не знаем» и «разобран» разные ответы.
|
||
//
|
||
// Пассивный чекпойнт не идёт дальше снимка самого старого активного читателя и
|
||
// ошибки при этом не возвращает — растущий файл единственный след. Поэтому
|
||
// «журнал не разбирается» выражается здесь, а не флагом занятости.
|
||
func (c Checkpoint) Complete() bool { return c.Known() && c.Checkpointed >= c.Log }
|
||
|
||
// Stuck отвечает, перестал ли журнал разбираться: неразобранного накопилось
|
||
// больше, чем держит верхняя граница файла, и перенести это не вышло.
|
||
//
|
||
// Порог не своё число: это тот же `journal_size_limit`, только журнал считается
|
||
// в страницах, а предел назван в байтах. Одна величина в двух ролях (предел
|
||
// возвращает файл, порог сообщает, что вернуть его не выходит) — двумя
|
||
// константами они разъехались бы молча, сделав признак либо недостижимым, либо
|
||
// шумным.
|
||
//
|
||
// Размер страницы берётся у самой базы, а не предполагается: он фиксируется при
|
||
// создании файла, и база, созданная чужим инструментом с другим умолчанием,
|
||
// сместила бы порог в разы. Неизвестен — предикат молчит: гадать о пороге хуже,
|
||
// чем не сказать.
|
||
//
|
||
// Предикат живёт в хранилище, а не у вызывающего: семантика тройки
|
||
// `busy/log/checkpointed` принадлежит SQLite, и второй её экземпляр разошёлся
|
||
// бы с первым молча.
|
||
func (c Checkpoint) Stuck() bool {
|
||
if !c.Known() || c.PageSize <= 0 {
|
||
return false
|
||
}
|
||
return !c.Complete() && c.Log*c.PageSize >= journalSizeLimit
|
||
}
|
||
|
||
// CheckpointWAL переносит страницы журнала в базу и говорит, сколько удалось.
|
||
//
|
||
// Автоматический чекпойнт SQLite (`wal_autocheckpoint`, 1000 страниц) остаётся
|
||
// первой линией и отключать его незачем; этот вызов страхует случай, которого
|
||
// автоматический не закрывает по построению — запись прекратилась, а журнал
|
||
// остался неразобранным. Поток пачечный: ночью телефон молчит часами.
|
||
func (s *Store) CheckpointWAL(ctx context.Context) (Checkpoint, error) {
|
||
var busy, logPages, checkpointed int
|
||
if err := s.db.QueryRowContext(ctx, checkpointQuery).Scan(&busy, &logPages, &checkpointed); err != nil {
|
||
return Checkpoint{}, fmt.Errorf("wal checkpoint: %w", err)
|
||
}
|
||
|
||
// Отказ ЭТОГО запроса отказом чекпойнта не является: страницы уже
|
||
// перенесены, и объявить это провалом значило бы отправить владельца искать
|
||
// беду в чекпойнте. Неизвестный размер страницы предикат и так трактует как
|
||
// «не измерено» и молчит.
|
||
var pageSize int
|
||
if err := s.db.QueryRowContext(ctx, pageSizeQuery).Scan(&pageSize); err != nil {
|
||
pageSize = 0
|
||
}
|
||
return Checkpoint{Busy: busy != 0, Log: logPages, Checkpointed: checkpointed, PageSize: pageSize}, nil
|
||
}
|