fix: 修复写队列 busy 恢复、关闭安全、备份与配置构建问题
This commit is contained in:
+113
-27
@@ -28,6 +28,7 @@ type WriteFunc func(tx *sql.Tx) error
|
||||
type writeJob struct {
|
||||
ctx context.Context
|
||||
fn WriteFunc
|
||||
raw func(*sql.DB) error
|
||||
res chan error
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ type Queue struct {
|
||||
ch chan writeJob
|
||||
done chan struct{}
|
||||
closed atomic.Bool
|
||||
sendMu sync.RWMutex
|
||||
|
||||
mu sync.Mutex
|
||||
ready bool
|
||||
@@ -65,34 +67,71 @@ func (q *Queue) Do(ctx context.Context, fn WriteFunc) error {
|
||||
if fn == nil {
|
||||
return errors.New("store: nil write func")
|
||||
}
|
||||
if q.closed.Load() {
|
||||
return ErrQueueClosed
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
job := writeJob{ctx: ctx, fn: fn, res: make(chan error, 1)}
|
||||
q.mu.Lock()
|
||||
q.pending++
|
||||
q.mu.Unlock()
|
||||
select {
|
||||
case q.ch <- job:
|
||||
case <-ctx.Done():
|
||||
q.mu.Lock()
|
||||
q.pending--
|
||||
q.mu.Unlock()
|
||||
return ctx.Err()
|
||||
case <-q.done:
|
||||
q.mu.Lock()
|
||||
q.pending--
|
||||
q.mu.Unlock()
|
||||
if err := q.enqueue(job); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.waitResult(ctx, job)
|
||||
}
|
||||
|
||||
// ExecOnWriter 在写 goroutine 上、事务外执行 fn(如 PRAGMA wal_checkpoint)。
|
||||
func (q *Queue) ExecOnWriter(ctx context.Context, fn func(*sql.DB) error) error {
|
||||
if fn == nil {
|
||||
return errors.New("store: nil exec func")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
job := writeJob{ctx: ctx, raw: fn, res: make(chan error, 1)}
|
||||
if err := q.enqueue(job); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.waitResult(ctx, job)
|
||||
}
|
||||
|
||||
// Checkpoint 在写连接上执行 PRAGMA wal_checkpoint(TRUNCATE)。
|
||||
func (q *Queue) Checkpoint(ctx context.Context) error {
|
||||
return q.ExecOnWriter(ctx, func(db *sql.DB) error {
|
||||
_, err := db.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Optimize 在写连接上执行 PRAGMA optimize。
|
||||
func (q *Queue) Optimize(ctx context.Context) error {
|
||||
return q.ExecOnWriter(ctx, func(db *sql.DB) error {
|
||||
_, err := db.ExecContext(ctx, `PRAGMA optimize`)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (q *Queue) enqueue(job writeJob) error {
|
||||
q.sendMu.RLock()
|
||||
if q.closed.Load() {
|
||||
q.sendMu.RUnlock()
|
||||
return ErrQueueClosed
|
||||
}
|
||||
q.addPending(1)
|
||||
select {
|
||||
case q.ch <- job:
|
||||
q.sendMu.RUnlock()
|
||||
return nil
|
||||
case <-job.ctx.Done():
|
||||
q.addPending(-1)
|
||||
q.sendMu.RUnlock()
|
||||
return job.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) waitResult(ctx context.Context, job writeJob) error {
|
||||
select {
|
||||
case err := <-job.res:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
// 操作可能仍在队列中执行;结果通道仍会被写端关闭式填入。
|
||||
// 操作可能仍在队列中执行;结果通道仍会被写端填入。
|
||||
select {
|
||||
case err := <-job.res:
|
||||
if err != nil {
|
||||
@@ -112,8 +151,13 @@ func (q *Queue) loop() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if job.raw != nil {
|
||||
q.runRaw(job)
|
||||
continue
|
||||
}
|
||||
batch := []writeJob{job}
|
||||
timer := time.NewTimer(batchWait)
|
||||
ranRaw := false
|
||||
collect:
|
||||
for len(batch) < maxBatchOps {
|
||||
select {
|
||||
@@ -121,25 +165,47 @@ func (q *Queue) loop() {
|
||||
if !ok {
|
||||
break collect
|
||||
}
|
||||
if j.raw != nil {
|
||||
stopTimer(timer)
|
||||
q.runBatch(batch)
|
||||
q.runRaw(j)
|
||||
ranRaw = true
|
||||
break collect
|
||||
}
|
||||
batch = append(batch, j)
|
||||
case <-timer.C:
|
||||
break collect
|
||||
}
|
||||
}
|
||||
timer.Stop()
|
||||
q.runBatch(batch)
|
||||
if !ranRaw {
|
||||
stopTimer(timer)
|
||||
q.runBatch(batch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopTimer(timer *time.Timer) {
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) runRaw(job writeJob) {
|
||||
defer q.addPending(-1)
|
||||
if err := job.ctx.Err(); err != nil {
|
||||
job.res <- err
|
||||
return
|
||||
}
|
||||
job.res <- job.raw(q.db)
|
||||
}
|
||||
|
||||
func (q *Queue) runBatch(batch []writeJob) {
|
||||
started := time.Now()
|
||||
defer func() {
|
||||
q.mu.Lock()
|
||||
q.pending -= len(batch)
|
||||
if q.pending < 0 {
|
||||
q.pending = 0
|
||||
}
|
||||
q.mu.Unlock()
|
||||
q.addPending(-len(batch))
|
||||
}()
|
||||
|
||||
// 过滤已取消的任务。
|
||||
@@ -233,6 +299,7 @@ func (q *Queue) runBatch(batch []writeJob) {
|
||||
}
|
||||
return
|
||||
}
|
||||
q.markReady()
|
||||
if q.OnBatchCommit != nil {
|
||||
q.OnBatchCommit(time.Since(started))
|
||||
}
|
||||
@@ -252,7 +319,23 @@ func (q *Queue) markBusy(err error) {
|
||||
q.lastWriteErr = err
|
||||
}
|
||||
|
||||
// IsReady 写库是否仍可用(写失败后为 false)。
|
||||
func (q *Queue) markReady() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.ready = true
|
||||
q.lastWriteErr = nil
|
||||
}
|
||||
|
||||
func (q *Queue) addPending(delta int) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.pending += delta
|
||||
if q.pending < 0 {
|
||||
q.pending = 0
|
||||
}
|
||||
}
|
||||
|
||||
// IsReady 写库是否仍可用(写失败后为 false;随后一次成功提交会恢复)。
|
||||
func (q *Queue) IsReady() bool {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
@@ -293,11 +376,14 @@ func (q *Queue) Drain(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Close 关闭队列:不再接受新任务,并等待写 goroutine 处理完已入队任务后退出。
|
||||
// 在写锁内关闭数据通道,避免并发 Do 向已关闭 channel 发送而 panic。
|
||||
func (q *Queue) Close() error {
|
||||
if q.closed.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
q.sendMu.Lock()
|
||||
close(q.ch)
|
||||
q.sendMu.Unlock()
|
||||
<-q.done
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user