From eb4cb7c655e5bb6796c2514aaa4a714d0b4c3b3e Mon Sep 17 00:00:00 2001 From: Nixevol Date: Wed, 30 Sep 2026 17:28:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=86=99=E9=98=9F=E5=88=97=E6=BB=A1?= =?UTF-8?q?=E6=97=B6=20Close=20=E4=B8=8D=E5=86=8D=E4=B8=8E=20enqueue=20?= =?UTF-8?q?=E6=AD=BB=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DEVIATIONS.md | 10 +++ internal/store/queue.go | 31 +++++--- internal/store/queue_test.go | 134 +++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 9 deletions(-) diff --git a/docs/DEVIATIONS.md b/docs/DEVIATIONS.md index 7deb4c5..a85b67c 100644 --- a/docs/DEVIATIONS.md +++ b/docs/DEVIATIONS.md @@ -1718,3 +1718,13 @@ issue #3 未关闭,`feat/fix-3-downlink-deadlock` 未å�ˆå…¥ `main`。下é�¢æ˜¯ - 原因:原先 12 项备注写ç�€æœªç©·å°½ä»�标通过,交付说明写æˆ�「通过 23ã€�。 - 备选方案:为æ¯�个未测å­�项补验收用例(本波ä¸�å�šï¼Œé�¿å…�为å�˜ç»¿æ”¾æ�¾æ–­è¨€ï¼‰ã€‚ - å½±å“�:汇总改为通过 19ã€�部分通过 4(F03/F08/F21/F22)ã€�失败 0。F19 ä»�引用仓库内 SDK 清å�•ã€�本波ä¸�é‡�跑。 + +### ¸´ÉóÐÞ¸´ R3-03 + +1. **д¶ÓÁÐÂúʱ Close ²»ÔÙÓë enqueue ËÀËø** + - ÈÕÆÚ£º2026-09-30 + - Ô­Ìõ¿î£ºissue #67£»Í£»úÐè Drain/Close ÔÚ³¬Ê±ÄÚ·µ»Ø£»ÒÑÈë¶ÓÈÎÎñÈÔÓÉд goroutine ´¦ÀíÍê¡£ + - ʵ¼Ê×ö·¨£ºQueue Ôö¼Ó closing Ðźš£enqueue ÔÚ³Ö sendMu.RLock ʱ select ͬʱµÈ´ý q.ch¡¢closing Óë ctx.Done()£¬Í¨µÀÂúʱ²»ÔÙÎÞÏÞ×èÈû¡£Close ÏÈ Swap(closed) ²¢ close(closing) »½ÐÑÔÚ;·¢ËÍ·½ÊͷŶÁËø£¬ÔÙÔÚÐ´ËøÄÚ close(q.ch)£¬×îºóµÈ loop Í˳ö¡£²»ÏòÒÑ¹Ø±Õ channel ·¢ËÍ¡£ + - Ô­Òò£ºÍ¨µÀÂúʱ²¢·¢ Do Õ¼×ŶÁËø¶ÂÔÚ·¢ËÍÉÏ£¬Close µÄÐ´ËøÄò»µ½£¬Í£»ú¿¨ËÀ£»Î´Ìá½»µÄÔÚ;дҲ»á¶ª¡£ + - ±¸Ñ¡·½°¸£ºÈë¶Ó¸ÄΪ·Ç×èÈû£¬ÂúÔòÁ¢¼´ ErrBusy£¨·ñ¾ö£º¸Ä±ä±³Ñ¹ÓïÒ壬Õý³£¸ß·å»áÎóÉËÌá½»£©¡£ + - Ó°Ï죺½ö internal/store/queue.go Óë²âÊÔ£»²»¸ÄÇ¨ÒÆ±àºÅ¡£ diff --git a/internal/store/queue.go b/internal/store/queue.go index 99ffb1c..20b52e0 100644 --- a/internal/store/queue.go +++ b/internal/store/queue.go @@ -36,10 +36,11 @@ type writeJob struct { type Queue struct { db *sql.DB - ch chan writeJob - done chan struct{} - closed atomic.Bool - sendMu sync.RWMutex + ch chan writeJob + done chan struct{} + closing chan struct{} // Close 时关闭,唤醒æŒ�读é”�阻塞在å�‘é€�上的 enqueue + closed atomic.Bool + sendMu sync.RWMutex mu sync.Mutex ready bool @@ -52,11 +53,16 @@ type Queue struct { // NewQueue 创建å�ˆå¹¶å†™å…¥é˜Ÿåˆ—å¹¶å�¯åЍ写 goroutine。 func NewQueue(db *sql.DB) *Queue { + return newQueue(db, queueBuffSize) +} + +func newQueue(db *sql.DB, buffSize int) *Queue { q := &Queue{ - db: db, - ch: make(chan writeJob, queueBuffSize), - done: make(chan struct{}), - ready: true, + db: db, + ch: make(chan writeJob, buffSize), + done: make(chan struct{}), + closing: make(chan struct{}), + ready: true, } go q.loop() return q @@ -115,10 +121,15 @@ func (q *Queue) enqueue(job writeJob) error { return ErrQueueClosed } q.addPending(1) + // 通é�“满时ä¸�å¾—å�ªå µåœ¨å�‘é€�上æŒ�有读é”�:Close 需è¦�写é”�关闭 q.ch。 select { case q.ch <- job: q.sendMu.RUnlock() return nil + case <-q.closing: + q.addPending(-1) + q.sendMu.RUnlock() + return ErrQueueClosed case <-job.ctx.Done(): q.addPending(-1) q.sendMu.RUnlock() @@ -376,11 +387,13 @@ func (q *Queue) Drain(ctx context.Context) error { } // Close 关闭队列:ä¸�å†�接å�—新任务,并等待写 goroutine 处ç�†å®Œå·²å…¥é˜Ÿä»»åŠ¡å�Žé€€å‡ºã€‚ -// 在写é”�内关闭数æ�®é€šé�“,é�¿å…�å¹¶å�‘ Do å�‘已关闭 channel å�‘é€�而 panic。 +// 先关闭 closing 唤醒因通é�“满而阻塞的å�‘é€�方并释放读é”�,å†�在无å�‘é€�者时关闭数æ�®é€šé�“, +// é�¿å…�å�‘已关闭 channel å�‘é€�而 panic,也é�¿å…�与æŒ�读é”�çš„ enqueue æ­»é”�。 func (q *Queue) Close() error { if q.closed.Swap(true) { return nil } + close(q.closing) q.sendMu.Lock() close(q.ch) q.sendMu.Unlock() diff --git a/internal/store/queue_test.go b/internal/store/queue_test.go index d51cb26..7f7dede 100644 --- a/internal/store/queue_test.go +++ b/internal/store/queue_test.go @@ -289,3 +289,137 @@ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.upd } } } + +// openSmallQueue 用å°�缓冲队列替æ�¢é»˜è®¤é˜Ÿåˆ—,便于测满通é�“æ—¶çš„ Close/Drain。 +func openSmallQueue(t *testing.T, buf int) *DB { + t.Helper() + dir := t.TempDir() + db, err := Open(dir, "FULL") + if err != nil { + t.Fatal(err) + } + if err := db.Queue.Close(); err != nil { + t.Fatal(err) + } + db.Queue = newQueue(db.Write, buf) + return db +} + +func TestQueueCloseUnblocksFullChannel(t *testing.T) { + t.Parallel() + const buf = 4 + db := openSmallQueue(t, buf) + defer func() { _ = db.Close() }() + q := db.Queue + + hold := make(chan struct{}) + blockerStarted := make(chan struct{}) + blockerErr := make(chan error, 1) + go func() { + blockerErr <- q.Do(context.Background(), func(tx *sql.Tx) error { + close(blockerStarted) + <-hold + return nil + }) + }() + <-blockerStarted + + ctx := context.Background() + var fillWG sync.WaitGroup + for i := 0; i < buf; i++ { + fillWG.Add(1) + go func() { + defer fillWG.Done() + _ = q.Do(ctx, func(tx *sql.Tx) error { return nil }) + }() + } + deadline := time.Now().Add(2 * time.Second) + for q.Len() < buf+1 && time.Now().Before(deadline) { + time.Sleep(2 * time.Millisecond) + } + if q.Len() < buf+1 { + t.Fatalf("channel not full: len=%d", q.Len()) + } + + blockedErr := make(chan error, 1) + go func() { + blockedErr <- q.Do(ctx, func(tx *sql.Tx) error { return nil }) + }() + // ç­‰é¢�外 Do 堵在 enqueue(pending 超过通é�“容é‡�+正在执行的一æ�¡ï¼‰ã€‚ + deadline = time.Now().Add(2 * time.Second) + for q.Len() < buf+2 && time.Now().Before(deadline) { + time.Sleep(2 * time.Millisecond) + } + + closeDone := make(chan error, 1) + go func() { closeDone <- q.Close() }() + + select { + case err := <-blockedErr: + if !errors.Is(err, ErrQueueClosed) { + t.Fatalf("blocked Do: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Close did not unblock full-channel enqueue") + } + + close(hold) + select { + case err := <-closeDone: + if err != nil { + t.Fatal(err) + } + case <-time.After(2 * time.Second): + t.Fatal("Close hung after writer released") + } + <-blockerErr + fillWG.Wait() +} + +func TestQueueDrainTimeoutWhileWriterBlocked(t *testing.T) { + t.Parallel() + const buf = 4 + db := openSmallQueue(t, buf) + defer func() { _ = db.Close() }() + q := db.Queue + + hold := make(chan struct{}) + started := make(chan struct{}) + go func() { + _ = q.Do(context.Background(), func(tx *sql.Tx) error { + close(started) + <-hold + return nil + }) + }() + <-started + + ctx := context.Background() + var wg sync.WaitGroup + for i := 0; i < buf; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = q.Do(ctx, func(tx *sql.Tx) error { return nil }) + }() + } + deadline := time.Now().Add(2 * time.Second) + for q.Len() < buf+1 && time.Now().Before(deadline) { + time.Sleep(2 * time.Millisecond) + } + + drainCtx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond) + defer cancel() + start := time.Now() + err := q.Drain(drainCtx) + elapsed := time.Since(start) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("Drain err=%v want deadline", err) + } + if elapsed > 500*time.Millisecond { + t.Fatalf("Drain took %s, should return on timeout", elapsed) + } + + close(hold) + wg.Wait() +}