优化,排查项目问题

This commit is contained in:
truewhile
2026-09-05 12:34:17 +08:00
parent 7fa05391e1
commit 1407b9b5c4
85 changed files with 1940 additions and 638 deletions
+76 -24
View File
@@ -5,12 +5,21 @@ import (
"fmt"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"gorm.io/gorm"
"github.com/truewhile/MeBox/internal/config"
)
// sqliteGateHoldLimit 是写闸持有者的最长合法持有时长。语句级写闸在 SQL
// 执行 panic 时 After 回调不会运行,令牌会泄漏并让后续所有写入永久等锁;
// 超过该时长的持有者按泄漏强制回收(60s 内单条写语句远未到,正常写路径
// 不受影响)。
const sqliteGateHoldLimit = 60 * time.Second
func installSQLiteWriteGate(db *gorm.DB) {
if db == nil {
return
@@ -22,15 +31,18 @@ func installSQLiteWriteGate(db *gorm.DB) {
if tx.Statement != nil && tx.Statement.Context != nil {
ctx = tx.Statement.Context
}
if err := gate.Lock(ctx); err != nil {
holder, err := gate.Lock(ctx)
if err != nil {
_ = tx.AddError(err)
return
}
tx.InstanceSet(lockedKey, struct{}{})
tx.InstanceSet(lockedKey, holder)
}
unlock := func(tx *gorm.DB) {
if _, ok := tx.InstanceGet(lockedKey); ok {
gate.Unlock()
if holder, ok := tx.InstanceGet(lockedKey); ok {
if h, ok := holder.(*sqliteGateHolder); ok {
gate.Unlock(h)
}
}
}
rawLock := func(tx *gorm.DB) {
@@ -64,38 +76,76 @@ func isReadOnlySQL(sql string) bool {
return false
}
// sqliteWriteGate serializes in-process SQLite writes while respecting the
// statement context, so request cancellation can break out of a queued write.
// sqliteWriteGate serializes in-process SQLite writes. 所有权令牌(而非裸
// 信号量)保证只有持有者本人能释放;持有超时按泄漏自动回收,避免一次
// panic 让进程的 SQLite 写入半永久性瘫痪。
type sqliteWriteGate struct {
ch chan struct{}
mu sync.Mutex
cond *sync.Cond
owner *sqliteGateHolder
}
type sqliteGateHolder struct {
id uint64
acquired time.Time
}
var sqliteGateHolderSeq atomic.Uint64
func newSQLiteWriteGate() *sqliteWriteGate {
return &sqliteWriteGate{ch: make(chan struct{}, 1)}
g := &sqliteWriteGate{}
g.cond = sync.NewCond(&g.mu)
return g
}
func (g *sqliteWriteGate) Lock(ctx context.Context) error {
select {
case g.ch <- struct{}{}:
return nil
default:
}
func (g *sqliteWriteGate) Lock(ctx context.Context) (*sqliteGateHolder, error) {
g.mu.Lock()
defer g.mu.Unlock()
if ctx == nil {
ctx = context.Background()
}
select {
case g.ch <- struct{}{}:
return nil
case <-ctx.Done():
return ctx.Err()
// ctx 取消时唤醒等待者(cond 无法感知 ctx,用旁路 goroutine 广播)。
if done := ctx.Done(); done != nil {
stop := make(chan struct{})
defer close(stop)
go func() {
select {
case <-done:
g.cond.Broadcast()
case <-stop:
}
}()
}
for {
if g.owner == nil {
holder := &sqliteGateHolder{
id: sqliteGateHolderSeq.Add(1),
acquired: time.Now(),
}
g.owner = holder
return holder, nil
}
if ctx.Err() != nil {
return nil, ctx.Err()
}
if time.Since(g.owner.acquired) > sqliteGateHoldLimit {
// 持有者疑似 panic 泄漏(After 回调未执行):强制回收。
g.owner = nil
g.cond.Broadcast()
continue
}
g.cond.Wait()
}
}
func (g *sqliteWriteGate) Unlock() {
select {
case <-g.ch:
default:
func (g *sqliteWriteGate) Unlock(h *sqliteGateHolder) {
g.mu.Lock()
defer g.mu.Unlock()
if h == nil || g.owner != h {
return
}
g.owner = nil
g.cond.Broadcast()
}
func buildSQLiteDSN(cfg *config.Config) string {
@@ -104,7 +154,9 @@ func buildSQLiteDSN(cfg *config.Config) string {
// keep as-is to respect user-provided relative paths.
dbPath = filepath.Clean(dbPath)
}
dsn := dbPath + "?_pragma=foreign_keys(1)"
// _txlock=immediate:事务以写锁开始。此前 deferred BEGIN 在并发事务
// 升级写锁时会绕过 busy_timeout 直接报 SQLITE_BUSY。
dsn := dbPath + "?_txlock=immediate&_pragma=foreign_keys(1)"
if cfg.Database.WALMode {
dsn += "&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)"
}