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 }