Retry transient SQLITE_BUSY at migration Begin and race on a materialized db #139
@@ -2,9 +2,21 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// isBusy 识别 SQLite SQLITE_BUSY(含连接打开阶段的 journal/建库竞争)。
|
||||
func isBusy(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "database is locked") || strings.Contains(msg, "SQLITE_BUSY")
|
||||
}
|
||||
|
||||
// 版本化迁移(设计 446:每版本附回滚;双档 CI 验证)。
|
||||
//
|
||||
// 纪律:
|
||||
@@ -552,9 +564,22 @@ func (s *Store) migratePG() error {
|
||||
// 中途失败整体回滚——不存在"DDL 已生效而账本未记"的中间态;两步间崩溃
|
||||
// 的历史缺口(先提交 DDL 再单独 INSERT 版本)由此消除。
|
||||
func (s *Store) migratePass() error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate: %w", err)
|
||||
// Begin 阶段包含连接首次打开(应用 busy_timeout/journal_mode 等 DSN
|
||||
// pragma)。并发首开同一 SQLite 文件时,建库/journal 模式切换阶段可即时
|
||||
// 返回 SQLITE_BUSY——busy handler 不覆盖该阶段(慢速盘 CI 实测,与
|
||||
// busy_timeout 无关)。对 Begin 的瞬时 BUSY 做有界退避重试;进入
|
||||
// IMMEDIATE 事务后的语句由写锁保护,不需要重试。
|
||||
var tx *sql.Tx
|
||||
var err error
|
||||
for attempt := 0; ; attempt++ {
|
||||
tx, err = s.db.Begin()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if attempt >= 4 || !isBusy(err) {
|
||||
return fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
time.Sleep(time.Duration(200*(attempt+1)) * time.Millisecond)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)`); err != nil {
|
||||
|
||||
@@ -127,6 +127,18 @@ func TestMigrateAtomicDDLAndLedger(t *testing.T) {
|
||||
// 两边都成功,账本恰好一套(不因竞争出现重复或部分版本)。
|
||||
func TestMigrateRacingSQLite(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "race.db")
|
||||
// 先物化文件与 WAL 模式(空库、无 schema):并发首开的建库/journal 阶段
|
||||
// 可即时 SQLITE_BUSY 且不受 busy_timeout 保护(慢速盘 CI 实测),属连接
|
||||
// 建立竞争而非迁移互斥;KTD8 要验证的是「已部署库上的并发迁移者」。
|
||||
pre, err := sql.Open("sqlite", fmt.Sprintf("file:%s?_pragma=busy_timeout(30000)&_pragma=journal_mode(WAL)", path))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pre.Exec(`SELECT 1`); err != nil {
|
||||
pre.Close()
|
||||
t.Fatalf("materialize wal: %v", err)
|
||||
}
|
||||
pre.Close()
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, 4)
|
||||
for i := range errs {
|
||||
|
||||
Reference in New Issue
Block a user