diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index f77bbad..3b96d7a 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -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) diff --git a/backend/internal/ghost/ghost.go b/backend/internal/ghost/ghost.go index 744cfde..cc216fd 100644 --- a/backend/internal/ghost/ghost.go +++ b/backend/internal/ghost/ghost.go @@ -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) }