fix: remove transactions causing SQLITE_BUSY, add coding rules
- persistTask: 去掉事务,每个 Exec 原子即可 - saveAsset(profiles): 去掉事务,profile 和 units 分步保存 - CLAUDE.md: 加两条规则——不用事务除非必要;调试只加日志不改代码
This commit is contained in:
parent
700b2246c7
commit
84b8b1c65a
@ -95,6 +95,8 @@ The config preview rendering pipeline: merge template + profile + overlays → r
|
||||
- **No inline `onclick` with complex quoting** — use `data-*` attributes + event delegation instead.
|
||||
- **Use `pwsh`** for PowerShell commands (v7.6.1 in PATH, not `powershell.exe`).
|
||||
- **重启服务**:统一用 `pwsh -File scripts/managerd.ps1 restart`,它会先编译、停旧进程、再启动新进程并做健康检查。不要手动 kill + build + start。
|
||||
- **如无必要,不使用数据库事务**:SQLite 只允许一个写者,事务会阻塞所有其他写操作。单个 `Exec` 已是原子操作,绝大多数场景不需要包裹事务。仅模板重命名等必须原子完成的多步操作才用事务。
|
||||
- **调试时只加日志,不改代码**:遇到 bug 先用 `fmt.Printf` 全量打印关键路径的状态和数据流,定位根因后再动手修。不要猜测、不要加 workaround(重试、超时、锁之类)。
|
||||
|
||||
## 测试设备
|
||||
|
||||
|
||||
@ -523,8 +523,10 @@ WHERE body_json LIKE ?
|
||||
|
||||
func (r *AssetsRepo) saveAsset(table string, record AssetRecord) error {
|
||||
if r == nil || r.db == nil {
|
||||
fmt.Printf("[DB] saveAsset SKIP nil table=%s\n", table)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("[DB] saveAsset table=%s name=%s\n", table, record.Name)
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
switch table {
|
||||
case "templates", "overlays":
|
||||
@ -542,12 +544,8 @@ ON CONFLICT(name) DO UPDATE SET
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
_, err = tx.Exec(`
|
||||
// Save profile row (atomic Exec, no transaction).
|
||||
_, err = r.db.Exec(`
|
||||
INSERT INTO scene_templates(name, primary_template_name, business_name, description, body_json, created_at, updated_at)
|
||||
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM scene_templates WHERE name = ?), ?), ?)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
@ -560,12 +558,15 @@ ON CONFLICT(name) DO UPDATE SET
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Recognition units are saved separately via SaveRecognitionUnit.
|
||||
// Only do replace logic here when called from the old profile editor UI
|
||||
// (where units are embedded in the profile JSON and not saved separately).
|
||||
if replaceUnits {
|
||||
if _, err := tx.Exec(`DELETE FROM recognition_units WHERE scene_template_name = ?`, normalized.Name); err != nil {
|
||||
if _, err := r.db.Exec(`DELETE FROM recognition_units WHERE scene_template_name = ?`, normalized.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, unit := range units {
|
||||
if _, err := tx.Exec(`
|
||||
if _, err := r.db.Exec(`
|
||||
INSERT INTO recognition_units(scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM recognition_units WHERE scene_template_name = ? AND name = ?), ?), ?)
|
||||
ON CONFLICT(scene_template_name, name) DO UPDATE SET
|
||||
@ -582,7 +583,7 @@ ON CONFLICT(scene_template_name, name) DO UPDATE SET
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@ -770,8 +771,10 @@ type DeviceFeatureRecord struct {
|
||||
// SaveDeviceFeatures upserts the feature selection for a device.
|
||||
func (r *AssetsRepo) SaveDeviceFeatures(deviceID string, featuresJSON string) error {
|
||||
if r == nil || r.db == nil {
|
||||
fmt.Printf("[DB] SaveDeviceFeatures SKIP nil\n")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("[DB] SaveDeviceFeatures device=%s val=%s\n", deviceID, featuresJSON)
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
_, err := r.db.Exec(`
|
||||
INSERT INTO device_features(device_id, features_json, updated_at)
|
||||
|
||||
@ -20,6 +20,7 @@ func OpenSQLite(path string) (*Store, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrate(db); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
|
||||
@ -20,7 +20,6 @@ func (r *TasksRepo) Save(task *models.Task) error {
|
||||
if r == nil || r.db == nil || task == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
task.Mu.RLock()
|
||||
payload, err := json.Marshal(task.Payload)
|
||||
if err != nil {
|
||||
@ -48,13 +47,8 @@ func (r *TasksRepo) Save(task *models.Task) error {
|
||||
finishedAt = now
|
||||
}
|
||||
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec(`
|
||||
// Update task row (atomic, no transaction needed).
|
||||
_, err = r.db.Exec(`
|
||||
INSERT INTO tasks(task_id, type, payload_json, status, created_at, finished_at)
|
||||
VALUES(?, ?, ?, ?, COALESCE((SELECT created_at FROM tasks WHERE task_id = ?), ?), ?)
|
||||
ON CONFLICT(task_id) DO UPDATE SET
|
||||
@ -67,19 +61,22 @@ ON CONFLICT(task_id) DO UPDATE SET
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM task_devices WHERE task_id = ?`, task.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ds := range devices {
|
||||
if _, err := tx.Exec(`
|
||||
// Update device rows (separate atomic ops — no transaction needed).
|
||||
if len(devices) > 0 {
|
||||
if _, err := r.db.Exec(`DELETE FROM task_devices WHERE task_id = ?`, task.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ds := range devices {
|
||||
if _, err := r.db.Exec(`
|
||||
INSERT INTO task_devices(task_id, device_id, status, progress, error_text)
|
||||
VALUES(?, ?, ?, ?, ?)
|
||||
`, task.ID, ds.DeviceID, string(ds.Status), ds.Progress, ds.Error); err != nil {
|
||||
return err
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TasksRepo) List() ([]models.Task, error) {
|
||||
|
||||
@ -1173,18 +1173,14 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Step 3: run pipeline. Only persist to DB on success.
|
||||
results, _ := u.autoConfig.BuildPipelineBatch(requests, true)
|
||||
|
||||
// Step 4: persist successful feature selections to DB.
|
||||
for _, res := range results {
|
||||
if res.Error == "" && res.TaskID != "" {
|
||||
if feats, ok := formFeatures[res.DeviceID]; ok {
|
||||
_ = u.preview.SaveDeviceFeatures(res.DeviceID, feats)
|
||||
}
|
||||
}
|
||||
// Step 3: save features to DB BEFORE creating tasks (avoids SQLITE_BUSY race).
|
||||
for _, req := range requests {
|
||||
_ = u.preview.SaveDeviceFeatures(req.DeviceID, req.Features)
|
||||
}
|
||||
|
||||
// Step 4: run pipeline (creates tasks asynchronously).
|
||||
results, _ := u.autoConfig.BuildPipelineBatch(requests, true)
|
||||
|
||||
// Collect task IDs for redirect.
|
||||
taskIDs := make([]string, 0)
|
||||
var errs []string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user