Files
healthlog/internal/store/tx.go
T
av c9ddf0b4dd хранение точек часовыми объектами
- слияние read-modify-write в одной транзакции, ключ точки — интервал,
  победа более полной точки, тай-брейк по канонической форме
- хеш объекта как детектор изменений: совпал — записи нет
- _txlock=immediate и повтор всей тройки при SQLITE_BUSY/BUSY_SNAPSHOT
- миграции через goose.Provider: пакетные SetBaseFS/SetDialect писали глобалы
  и давали гонку при двух Open
2026-08-01 17:37:50 +03:00

91 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sqlite "modernc.org/sqlite"
sqlite3 "modernc.org/sqlite/lib"
)
// Сколько раз повторять транзакцию, отменённую из-за занятости.
//
// Повтор оборачивает ВСЮ тройку чтение-слияние-запись, а не отдельный запрос:
// после отката прочитанное состояние недействительно, и дописывать в него
// пришедшие точки значит терять чужие.
const (
txRetries = 5
txRetryDelay = 20 * time.Millisecond
)
// inTx выполняет работу в транзакции, повторяя её при занятости базы.
//
// Транзакция открывается сразу на запись (`_txlock=immediate` в DSN). Без
// этого read-modify-write ломается измеримо: при восьми писателях и ста
// слияниях каждый deferred-транзакция дала 242 успеха из 800, immediate — 800
// из 800. Причина в том, что повышение блокировки с чтения на запись после
// того, как снимок уже прочитан, даёт SQLITE_BUSY_SNAPSHOT (код 517), а его
// `busy_timeout` не покрывает: ждать бесполезно, снимок уже устарел.
func (s *Store) inTx(ctx context.Context, fn func(*sql.Tx) error) error {
var lastErr error
for attempt := range txRetries {
if attempt > 0 {
select {
case <-ctx.Done():
return fmt.Errorf("ожидание повтора транзакции: %w", ctx.Err())
case <-time.After(txRetryDelay * time.Duration(attempt)):
}
}
err := runTx(ctx, s.db.DB, fn)
if err == nil {
return nil
}
if !isBusy(err) {
return err
}
lastErr = err
}
return fmt.Errorf("транзакция не прошла за %d попыток: %w", txRetries, lastErr)
}
func runTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
if err := fn(tx); err != nil {
_ = tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
_ = tx.Rollback()
return fmt.Errorf("commit tx: %w", err)
}
return nil
}
// isBusy распознаёт отказ из-за занятости базы.
//
// Проверка по типу, а не по тексту: сообщения драйвера не контракт. Кодов два
// — SQLITE_BUSY и SQLITE_BUSY_SNAPSHOT; второй возвращается, когда снимок
// транзакции устарел, и на него `busy_timeout` не действует.
func isBusy(err error) bool {
var serr *sqlite.Error
if !errors.As(err, &serr) {
return false
}
switch serr.Code() {
case sqlite3.SQLITE_BUSY, sqlite3.SQLITE_BUSY_SNAPSHOT:
return true
default:
return false
}
}