mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
优化,排查项目问题
This commit is contained in:
@@ -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)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user