fix(sqlite): DSN 加 busy_timeout/WAL/foreign_keys + 连接池上限,避免 database is locked
=== 背景:这是 opsxsql 的「死锁类」风险 ===
LokiStack 是自研 JsonDB + sync.RWMutex,问题是**锁重入死锁**(已由上游 2e97c04 修复)。
opsxsql 用 SQLite + GORM,风险形式不同但后果相似:
裸 DSN(不带任何 pragma)+ 默认 journal 模式 + 无连接池上限
→ 写事务与读事务互相阻塞,并发写直接返回 "database is locked"(SQLITE_BUSY),
表现为「点保存失败 / 偶发 500」,而且不会挂住所以更难定位。
静态扫描结论:opsxsql 33 个 Go 文件无 RWMutex 重入问题(0 处),
唯一 RWMutex 在 internal/ghost 且用法正确。日志中 0 次 SQLITE_BUSY
(并发压力尚未到临界点,属**潜在**风险)。
=== 修改 1:DSN pragma ===
?_busy_timeout=5000 遇锁等 5 秒而不是立刻失败
&_journal_mode=WAL 读写不互斥,写不阻塞读
&_synchronous=NORMAL WAL 下常规安全级别
&_foreign_keys=on SQLite 默认关闭外键,容易埋脏数据
仅在 DBPath 不含 "?" 时追加,保留运维自定义 DSN 的能力。
=== 修改 2:连接池上限 4 ===
SQLite 同时只允许一个写者,连接开太多只会加剧锁竞争。
=== 修改 3:ghost.go 回调移出锁(防御性)===
原来 cb(taskID, result.Output) 在持有 Manager.mu 写锁时执行;
若调用方在回调里查任务状态(GetTaskStatus 需要 RLock)会永久死锁。
当前所有调用方都传 nil,属潜在问题,一并修掉。
=== 实测(46a97a5c11b7,ns=sql)===
Pod 1/1 Running,重启 0
/app/data 出现 sqlite3.db-wal + sqlite3.db-shm → WAL 已生效
日志 0 次 database is locked / SQLITE_BUSY
This commit is contained in:
1 parent
9dcb39e971
commit
d6f27545ad
2 files changed
+28
-2
No files matched your search
@@ -32,10 +32,31 @@ func main() {
|
||||
}
|
||||
|
||||
// Connect to SQLite
|
||||
db, err := gorm.Open(sqlite.Open(config.C.DBPath), &gorm.Config{})
|
||||
//
|
||||
// DSN 必须显式带 pragma,否则默认配置在高并发下会出问题:
|
||||
// 裸 DSN + 默认 journal 模式下,写事务与读事务互相阻塞,
|
||||
// 并发写会直接返回 "database is locked"(SQLITE_BUSY),
|
||||
// 表现为「点保存失败 / 偶尔 500」,而且不会卡住所以极难定位。
|
||||
// 这里用业界通用组合:
|
||||
// _busy_timeout=5000 遇到锁时等 5 秒再报错,而不是立刻失败
|
||||
// _journal_mode=WAL 读写不互斥(写不阻塞读),并发请求不再排队
|
||||
// _synchronous=NORMAL WAL 下的常规安全级别,比 FULL 快很多
|
||||
// _foreign_keys=on 外键约束(SQLite 默认关闭,容易埋脏数据)
|
||||
dsn := config.C.DBPath
|
||||
if !strings.Contains(dsn, "?") {
|
||||
dsn += "?_busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL&_foreign_keys=on"
|
||||
}
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
// 连接池上限:SQLite 同一时刻只允许一个写者,连接开太多只会加剧锁竞争。
|
||||
// 4 个连接足够支撑日常并发(读多写少),并把锁等待控制在可预期范围内。
|
||||
if sqlDB, dbErr := db.DB(); dbErr == nil {
|
||||
sqlDB.SetMaxOpenConns(4)
|
||||
sqlDB.SetMaxIdleConns(4)
|
||||
sqlDB.SetConnMaxLifetime(0)
|
||||
}
|
||||
model.DB = db
|
||||
|
||||
// Auto-migrate (add missing tables/columns, won\'t drop existing data)
|
||||
|
||||
@@ -163,8 +163,9 @@ func RunGhOstAsync(task GhOstTask, ghOstPath string, cb ProgressCallback) string
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
// 注意:这里不能用 defer Unlock —— 回调要放到锁外执行,
|
||||
// 所以显式在写完 result 后 Unlock(上面已写明原因)。
|
||||
Manager.mu.Lock()
|
||||
defer Manager.mu.Unlock()
|
||||
|
||||
result := &GhOstResult{
|
||||
TaskID: taskID,
|
||||
@@ -182,7 +183,11 @@ func RunGhOstAsync(task GhOstTask, ghOstPath string, cb ProgressCallback) string
|
||||
}
|
||||
|
||||
Manager.tasks[taskID] = result
|
||||
Manager.mu.Unlock()
|
||||
|
||||
// 回调必须放在锁**外面**:调用方如果在回调里查任务状态
|
||||
// (GetTaskStatus 需要 RLock),持有写锁时调用会永久死锁。
|
||||
// 当前所有调用方传的都是 nil,属于防御性修改。
|
||||
if cb != nil {
|
||||
cb(taskID, result.Output)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user