Skip to content

Commit 084a513

Browse files
author
Ryan Mitchell
committed
日誌庫 SQLite busy_timeout 與批量寫入重試(3.105.0-rc10)
Made-with: Cursor
1 parent a3cdf1a commit 084a513

5 files changed

Lines changed: 63 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
所有重要的專案更新都會記錄在此檔案中。
44

5+
## [3.105.0-rc10] - 2026-04-21
6+
7+
### Fixed
8+
- **日志庫 SQLite**:連接 DSN 增加 **`_busy_timeout=15000`****`batchInsert`**`database is locked` / busy 類錯誤做短重試,減輕高併發下「批量写入日志失败: database is locked」。
9+
10+
---
11+
512
## [3.105.0-rc9] - 2026-04-21
613

714
### Fixed

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import (
4545
)
4646

4747
// Version 应用版本号
48-
var Version = "3.105.0-rc9"
48+
var Version = "3.105.0-rc10"
4949

5050
// capitalDataSourceAdapter 资金數據源适配器
5151
type capitalDataSourceAdapter struct {

storage/log_storage.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,14 @@ func NewLogStorage(path string) (*LogStorage, error) {
7272
return ls, nil
7373
}
7474

75+
// logSQLiteDSN WAL + busy_timeout,避免與其它連接短暫競爭時出現 database is locked
76+
func logSQLiteDSN(path string) string {
77+
return path + "?_journal_mode=WAL&_synchronous=NORMAL&_busy_timeout=15000"
78+
}
79+
7580
// openLogStorageDB 打开數據库,若完整性检查失败则备份並重建
7681
func openLogStorageDB(path string) (*sql.DB, *LogStorage, error) {
77-
dsn := path + "?_journal_mode=WAL&_synchronous=NORMAL"
82+
dsn := logSQLiteDSN(path)
7883
db, err := sql.Open("sqlite3", dsn)
7984
if err != nil {
8085
return nil, nil, fmt.Errorf("打开日志數據库失败: %w", err)
@@ -278,13 +283,43 @@ func (ls *LogStorage) processLogs() {
278283
}
279284
}
280285

281-
// batchInsert 批量插入日志
286+
func sqliteLogLockedRetryable(err error) bool {
287+
if err == nil {
288+
return false
289+
}
290+
msg := strings.ToLower(err.Error())
291+
return strings.Contains(msg, "database is locked") || strings.Contains(msg, "locked") ||
292+
strings.Contains(msg, "busy")
293+
}
294+
295+
// batchInsert 批量插入日志(對 SQLITE_BUSY / locked 短重試,配合 DSN busy_timeout)
282296
func (ls *LogStorage) batchInsert(entries []*logEntry) error {
283297
if len(entries) == 0 {
284298
return nil
285299
}
300+
const maxAttempts = 6
301+
var lastErr error
302+
for attempt := 0; attempt < maxAttempts; attempt++ {
303+
if attempt > 0 {
304+
time.Sleep(time.Duration(20+attempt*25) * time.Millisecond)
305+
}
306+
lastErr = ls.batchInsertOnce(entries)
307+
if lastErr == nil {
308+
return nil
309+
}
310+
if !sqliteLogLockedRetryable(lastErr) {
311+
return lastErr
312+
}
313+
}
314+
return lastErr
315+
}
316+
317+
// batchInsertOnce 單次事務批量插入
318+
func (ls *LogStorage) batchInsertOnce(entries []*logEntry) error {
319+
if len(entries) == 0 {
320+
return nil
321+
}
286322

287-
// 使用事務批量插入
288323
tx, err := ls.db.Begin()
289324
if err != nil {
290325
return err
@@ -307,7 +342,6 @@ func (ls *LogStorage) batchInsert(entries []*logEntry) error {
307342
return err
308343
}
309344

310-
// 獲取插入的 ID
311345
id, _ := result.LastInsertId()
312346
insertedLogs = append(insertedLogs, &LogRecord{
313347
ID: id,
@@ -322,7 +356,6 @@ func (ls *LogStorage) batchInsert(entries []*logEntry) error {
322356
return err
323357
}
324358

325-
// 通知所有订阅者
326359
ls.notifySubscribers(insertedLogs)
327360

328361
return nil

storage/log_storage_filter_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package storage
22

33
import (
4+
"errors"
45
"path/filepath"
56
"testing"
67
"time"
@@ -61,3 +62,18 @@ func TestLogStorage_GetLogs_OptionalMessageFilters(t *testing.T) {
6162
t.Fatalf("AND of column bot_id + message filters should exclude mismatch, got total=%d", total)
6263
}
6364
}
65+
66+
func TestSqliteLogLockedRetryable(t *testing.T) {
67+
if !sqliteLogLockedRetryable(errors.New("database is locked")) {
68+
t.Fatal("expected locked message retryable")
69+
}
70+
if !sqliteLogLockedRetryable(errors.New("SQLITE_BUSY")) {
71+
t.Fatal("expected busy retryable")
72+
}
73+
if sqliteLogLockedRetryable(nil) {
74+
t.Fatal("nil should not retry")
75+
}
76+
if sqliteLogLockedRetryable(errors.New("constraint failed")) {
77+
t.Fatal("constraint should not retry")
78+
}
79+
}

webui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "quantmesh-webui",
3-
"version": "3.105.0-rc9",
3+
"version": "3.105.0-rc10",
44
"type": "module",
55
"packageManager": "yarn@4.12.0",
66
"scripts": {

0 commit comments

Comments
 (0)