хранение точек часовыми объектами

- слияние read-modify-write в одной транзакции, ключ точки — интервал,
  победа более полной точки, тай-брейк по канонической форме
- хеш объекта как детектор изменений: совпал — записи нет
- _txlock=immediate и повтор всей тройки при SQLITE_BUSY/BUSY_SNAPSHOT
- миграции через goose.Provider: пакетные SetBaseFS/SetDialect писали глобалы
  и давали гонку при двух Open
This commit is contained in:
av
2026-08-01 17:37:50 +03:00
parent 01d0de59df
commit c9ddf0b4dd
5 changed files with 1143 additions and 16 deletions
+25 -5
View File
@@ -3,8 +3,10 @@
package store
import (
"context"
"embed"
"fmt"
"io/fs"
"net/url"
"time"
@@ -46,21 +48,39 @@ func (s *Store) Close() error {
// dsn собирает строку подключения: WAL для параллельного чтения во время
// записи, busy_timeout — чтобы конкурентная запись ждала, а не падала.
//
// `_txlock=immediate` обязателен, а не «на всякий случай»: запись объекта —
// это read-modify-write, и при отложенной блокировке повышение с чтения на
// запись после уже прочитанного снимка даёт SQLITE_BUSY_SNAPSHOT, которого
// busy_timeout не покрывает. Измерено на восьми писателях: 242 успешных
// слияния из 800 против 800 из 800.
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("_txlock", "immediate")
return "file:" + path + "?" + q.Encode()
}
// migrate накатывает миграции.
//
// Через Provider, а не через пакетные функции: goose.SetBaseFS и
// goose.SetDialect пишут глобальное состояние пакета, и два одновременных
// Open дают гонку — её ловит детектор. Сегодня открытие одно, на старте, но
// пересборка витрины откроет второе, и хранилище не должно зависеть от того,
// что вызывающий этого не сделает.
func migrate(db *sqlx.DB) error {
goose.SetBaseFS(migrationsFS)
goose.SetLogger(goose.NopLogger())
if err := goose.SetDialect("sqlite3"); err != nil {
return fmt.Errorf("goose dialect: %w", err)
sub, err := fs.Sub(migrationsFS, "migrations")
if err != nil {
return fmt.Errorf("goose migrations fs: %w", err)
}
if err := goose.Up(db.DB, "migrations"); err != nil {
p, err := goose.NewProvider(goose.DialectSQLite3, db.DB, sub)
if err != nil {
return fmt.Errorf("goose provider: %w", err)
}
if _, err := p.Up(context.Background()); err != nil {
return fmt.Errorf("goose up: %w", err)
}
return nil