Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос
- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
This commit is contained in:
@@ -23,6 +23,10 @@ var migrationsFS embed.FS
|
||||
// Store — доступ к витрине.
|
||||
type Store struct {
|
||||
db *sqlx.DB
|
||||
// probe — закреплённое соединение версии витрины, заводится по первому
|
||||
// запросу версии. См. version.go: значение `data_version` локально для
|
||||
// соединения, и брать его из пула нельзя.
|
||||
probe versionProbe
|
||||
}
|
||||
|
||||
// Open открывает БД по пути, сверяет версию схемы и накатывает миграции.
|
||||
@@ -152,12 +156,32 @@ func readSchemaVersion(ctx context.Context, db *sqlx.DB) (inDB, inBinary int64,
|
||||
|
||||
// Close закрывает соединение с БД.
|
||||
func (s *Store) Close() error {
|
||||
// Щуп версии закрывается сам и раньше пула: закреплённое соединение
|
||||
// переживает `db.Close()` и продолжает отвечать на запросы — проверено.
|
||||
s.closeProbe()
|
||||
if err := s.db.Close(); err != nil {
|
||||
return fmt.Errorf("close sqlite: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// `journal_size_limit` держит верхнюю границу файла журнала. Чекпойнт
|
||||
// возвращает страницы в базу, но файл оставляет на пике: измерено — 51 МБ до и
|
||||
// после успешного переноса 12502 страниц. С лимитом первая же следующая запись
|
||||
// усекает файл до предела.
|
||||
//
|
||||
// Величина взята из чужой практики (гайды по SQLite в проде ставят 26–64 МБ) и
|
||||
// собственного измерения: суточный поток даёт около 23 МБ архива, то есть
|
||||
// журнал такого размера означает не всплеск, а удерживаемый снимок. С пределом
|
||||
// тела приёма она НЕ связана, хотя и совпадает по порядку: журнал растёт от
|
||||
// чтения, а не от размера доставки, и менять её вслед за `ingest.max_body_mb`
|
||||
// незачем.
|
||||
const journalSizeLimit = 64 << 20
|
||||
|
||||
// JournalSizeLimitMB — тот же предел для строки старта: владелец обязан видеть
|
||||
// в логе, с какими параметрами обслуживается журнал, а число живёт здесь.
|
||||
const JournalSizeLimitMB = journalSizeLimit >> 20
|
||||
|
||||
// dsn собирает строку подключения: WAL для параллельного чтения во время
|
||||
// записи, busy_timeout — чтобы конкурентная запись ждала, а не падала.
|
||||
//
|
||||
@@ -166,11 +190,14 @@ func (s *Store) Close() error {
|
||||
// запись после уже прочитанного снимка даёт SQLITE_BUSY_SNAPSHOT, которого
|
||||
// busy_timeout не покрывает. Измерено на восьми писателях: 242 успешных
|
||||
// слияния из 800 против 800 из 800.
|
||||
//
|
||||
// `journal_size_limit` — см. константу выше.
|
||||
func dsn(path string) string {
|
||||
q := url.Values{}
|
||||
q.Add("_pragma", "journal_mode(WAL)")
|
||||
q.Add("_pragma", "busy_timeout(5000)")
|
||||
q.Add("_pragma", "foreign_keys(on)")
|
||||
q.Add("_pragma", fmt.Sprintf("journal_size_limit(%d)", journalSizeLimit))
|
||||
q.Add("_txlock", "immediate")
|
||||
return "file:" + path + "?" + q.Encode()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user