fix: make local queue PopItem can be notified (#39011)

This commit is contained in:
wxiaoguang
2026-08-22 09:30:41 +08:00
committed by GitHub
parent 5eba4f92ce
commit d2bc0097bc
11 changed files with 145 additions and 88 deletions
+21 -28
View File
@@ -13,10 +13,18 @@ var (
backoffUpper = 2 * time.Second backoffUpper = 2 * time.Second
) )
type ( type backoffFunc[T any] func() (retry bool, ret T, err error)
backoffFuncRetErr[T any] func() (retry bool, ret T, err error)
backoffFuncErr func() (retry bool, err error) type backoffOptions struct {
) begin, upper time.Duration
notify <-chan struct{}
end <-chan time.Time
}
func backoffOptionsDefault(notify <-chan struct{}, end <-chan time.Time) backoffOptions {
return backoffOptions{begin: backoffBegin, upper: backoffUpper, notify: notify, end: end}
}
func mockBackoffDuration(d time.Duration) func() { func mockBackoffDuration(d time.Duration) func() {
oldBegin, oldUpper := backoffBegin, backoffUpper oldBegin, oldUpper := backoffBegin, backoffUpper
@@ -26,18 +34,9 @@ func mockBackoffDuration(d time.Duration) func() {
} }
} }
func backoffRetErr[T any](ctx context.Context, begin, upper time.Duration, end <-chan time.Time, fn backoffFuncRetErr[T]) (ret T, err error) { func backoffCall[T any](ctx context.Context, opts backoffOptions, fn backoffFunc[T]) (ret T, err error) {
d := begin d := opts.begin
for { for {
// check whether the context has been cancelled or has reached the deadline, return early
select {
case <-ctx.Done():
return ret, ctx.Err()
case <-end:
return ret, context.DeadlineExceeded
default:
}
// call the target function // call the target function
retry, ret, err := fn() retry, ret, err := fn()
if err != nil { if err != nil {
@@ -47,25 +46,19 @@ func backoffRetErr[T any](ctx context.Context, begin, upper time.Duration, end <
return ret, nil return ret, nil
} }
// wait for a while before retrying, and also respect the context & deadline // wait for a while before retrying, and also respect the context & deadline & notify
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ret, ctx.Err() return ret, ctx.Err()
case <-opts.end:
return ret, context.DeadlineExceeded
case <-opts.notify:
continue
case <-time.After(d): case <-time.After(d):
d *= 2 d *= 2
if d > upper { if d > opts.upper {
d = upper d = opts.upper
} }
case <-end:
return ret, context.DeadlineExceeded
} }
} }
} }
func backoffErr(ctx context.Context, begin, upper time.Duration, end <-chan time.Time, fn backoffFuncErr) error {
_, err := backoffRetErr(ctx, begin, upper, end, func() (retry bool, ret any, err error) {
retry, err = fn()
return retry, nil, err
})
return err
}
+25
View File
@@ -40,3 +40,28 @@ func popItemByChan(ctx context.Context, popItemFn func(ctx context.Context) ([]b
}() }()
return chanItem, chanErr return chanItem, chanErr
} }
type baseQueueNotifiableInterface interface {
getNotifySignalChan() chan struct{}
}
type baseQueueNotifiable struct {
notifySignal chan struct{}
}
var _ baseQueueNotifiableInterface = (*baseQueueNotifiable)(nil)
func (n *baseQueueNotifiable) notifyPushItem() {
select {
case n.notifySignal <- struct{}{}:
default:
}
}
func (n *baseQueueNotifiable) getNotifySignalChan() chan struct{} {
return n.notifySignal
}
func newBaseQueueNotifiable() *baseQueueNotifiable {
return &baseQueueNotifiable{notifySignal: make(chan struct{}, 1)}
}
+2 -2
View File
@@ -6,6 +6,6 @@ package queue
import "testing" import "testing"
func TestBaseChannel(t *testing.T) { func TestBaseChannel(t *testing.T) {
testQueueBasic(t, newBaseChannelSimple, &BaseConfig{ManagedName: "baseChannel", Length: 10}, false) testQueueBasic(t, newBaseChannelSimple, &BaseConfig{ManagedName: "baseChannel", Length: 10}, testQueueBasicOptions{})
testQueueBasic(t, newBaseChannelUnique, &BaseConfig{ManagedName: "baseChannel", Length: 10}, true) testQueueBasic(t, newBaseChannelUnique, &BaseConfig{ManagedName: "baseChannel", Length: 10}, testQueueBasicOptions{UniqueQueue: true})
} }
+6 -11
View File
@@ -15,6 +15,7 @@ import (
) )
type baseLevelQueue struct { type baseLevelQueue struct {
*baseLevelQueueCommonImpl
internal atomic.Pointer[levelqueue.Queue] internal atomic.Pointer[levelqueue.Queue]
conn string conn string
@@ -22,7 +23,10 @@ type baseLevelQueue struct {
db *leveldb.DB db *leveldb.DB
} }
var _ baseQueue = (*baseLevelQueue)(nil) var (
_ baseQueue = (*baseLevelQueue)(nil)
_ baseQueueNotifiableInterface = (*baseLevelQueue)(nil)
)
func newBaseLevelQueueGeneric(cfg *BaseConfig, unique bool) (baseQueue, error) { func newBaseLevelQueueGeneric(cfg *BaseConfig, unique bool) (baseQueue, error) {
if unique { if unique {
@@ -42,19 +46,10 @@ func newBaseLevelQueueSimple(cfg *BaseConfig) (baseQueue, error) {
return nil, err return nil, err
} }
q.internal.Store(lq) q.internal.Store(lq)
q.baseLevelQueueCommonImpl = baseLevelQueueCommon(q.cfg, nil, func() baseLevelQueuePushPoper { return q.internal.Load() })
return q, nil return q, nil
} }
func (q *baseLevelQueue) PushItem(ctx context.Context, data []byte) error {
c := baseLevelQueueCommon(q.cfg, nil, func() baseLevelQueuePushPoper { return q.internal.Load() })
return c.PushItem(ctx, data)
}
func (q *baseLevelQueue) PopItem(ctx context.Context) ([]byte, error) {
c := baseLevelQueueCommon(q.cfg, nil, func() baseLevelQueuePushPoper { return q.internal.Load() })
return c.PopItem(ctx)
}
func (q *baseLevelQueue) HasItem(ctx context.Context, data []byte) (bool, error) { func (q *baseLevelQueue) HasItem(ctx context.Context, data []byte) (bool, error) {
return false, nil return false, nil
} }
+17 -12
View File
@@ -25,35 +25,40 @@ type baseLevelQueuePushPoper interface {
} }
type baseLevelQueueCommonImpl struct { type baseLevelQueueCommonImpl struct {
*baseQueueNotifiable
length int length int
internalFunc func() baseLevelQueuePushPoper internalFunc func() baseLevelQueuePushPoper
mu *sync.Mutex muCommon *sync.Mutex
} }
func (q *baseLevelQueueCommonImpl) PushItem(ctx context.Context, data []byte) error { func (q *baseLevelQueueCommonImpl) PushItem(ctx context.Context, data []byte) error {
return backoffErr(ctx, backoffBegin, backoffUpper, time.After(pushBlockTime), func() (retry bool, err error) { _, err := backoffCall(ctx, backoffOptionsDefault(noNotifyChan, time.After(pushBlockTime)), func() (retry bool, ret any, err error) {
if q.mu != nil { if q.muCommon != nil {
q.mu.Lock() q.muCommon.Lock()
defer q.mu.Unlock() defer q.muCommon.Unlock()
} }
cnt := int(q.internalFunc().Len()) cnt := int(q.internalFunc().Len())
if cnt >= q.length { if cnt >= q.length {
return true, nil return true, nil, nil
} }
retry, err = false, q.internalFunc().RPush(data) retry, err = false, q.internalFunc().RPush(data)
if err == levelqueue.ErrAlreadyInQueue { if err == levelqueue.ErrAlreadyInQueue {
err = ErrAlreadyInQueue err = ErrAlreadyInQueue
} }
return retry, err if err == nil {
q.notifyPushItem()
}
return retry, nil, err
}) })
return err
} }
func (q *baseLevelQueueCommonImpl) PopItem(ctx context.Context) ([]byte, error) { func (q *baseLevelQueueCommonImpl) PopItem(ctx context.Context) ([]byte, error) {
return backoffRetErr(ctx, backoffBegin, backoffUpper, infiniteTimerC, func() (retry bool, data []byte, err error) { return backoffCall(ctx, backoffOptionsDefault(q.notifySignal, infiniteTimerC), func() (retry bool, data []byte, err error) {
if q.mu != nil { if q.muCommon != nil {
q.mu.Lock() q.muCommon.Lock()
defer q.mu.Unlock() defer q.muCommon.Unlock()
} }
data, err = q.internalFunc().LPop() data, err = q.internalFunc().LPop()
@@ -68,7 +73,7 @@ func (q *baseLevelQueueCommonImpl) PopItem(ctx context.Context) ([]byte, error)
} }
func baseLevelQueueCommon(cfg *BaseConfig, mu *sync.Mutex, internalFunc func() baseLevelQueuePushPoper) *baseLevelQueueCommonImpl { func baseLevelQueueCommon(cfg *BaseConfig, mu *sync.Mutex, internalFunc func() baseLevelQueuePushPoper) *baseLevelQueueCommonImpl {
return &baseLevelQueueCommonImpl{length: cfg.Length, mu: mu, internalFunc: internalFunc} return &baseLevelQueueCommonImpl{length: cfg.Length, muCommon: mu, internalFunc: internalFunc, baseQueueNotifiable: newBaseQueueNotifiable()}
} }
func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) { func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) {
+4 -2
View File
@@ -22,8 +22,10 @@ func TestBaseLevelDB(t *testing.T) {
_, err = newBaseLevelQueueGeneric(&BaseConfig{DataFullDir: "relative"}, false) _, err = newBaseLevelQueueGeneric(&BaseConfig{DataFullDir: "relative"}, false)
assert.ErrorContains(t, err, "invalid leveldb data dir") assert.ErrorContains(t, err, "invalid leveldb data dir")
testQueueBasic(t, newBaseLevelQueueSimple, toBaseConfig("baseLevelQueue", setting.QueueSettings{Datadir: t.TempDir() + "/queue-test", Length: 10}), false) optsSimple := testQueueBasicOptions{NotifiableQueue: true}
testQueueBasic(t, newBaseLevelQueueUnique, toBaseConfig("baseLevelQueueUnique", setting.QueueSettings{ConnStr: "leveldb://" + t.TempDir() + "/queue-test", Length: 10}), true) optsUnique := testQueueBasicOptions{UniqueQueue: true, NotifiableQueue: true}
testQueueBasic(t, newBaseLevelQueueSimple, toBaseConfig("baseLevelQueue", setting.QueueSettings{Datadir: t.TempDir() + "/queue-test", Length: 10}), optsSimple)
testQueueBasic(t, newBaseLevelQueueUnique, toBaseConfig("baseLevelQueueUnique", setting.QueueSettings{ConnStr: "leveldb://" + t.TempDir() + "/queue-test", Length: 10}), optsUnique)
} }
func TestCorruptedLevelQueue(t *testing.T) { func TestCorruptedLevelQueue(t *testing.T) {
+15 -20
View File
@@ -16,16 +16,20 @@ import (
) )
type baseLevelQueueUnique struct { type baseLevelQueueUnique struct {
*baseLevelQueueCommonImpl
internal atomic.Pointer[levelqueue.UniqueQueue] internal atomic.Pointer[levelqueue.UniqueQueue]
conn string conn string
cfg *BaseConfig cfg *BaseConfig
db *leveldb.DB db *leveldb.DB
mu sync.Mutex // the levelqueue.UniqueQueue is not thread-safe, there is no mutex protecting the underlying queue&set together muBase sync.Mutex // the levelqueue.UniqueQueue is not thread-safe, there is no mutex protecting the underlying queue&set together
} }
var _ baseQueue = (*baseLevelQueueUnique)(nil) var (
_ baseQueue = (*baseLevelQueueUnique)(nil)
_ baseQueueNotifiableInterface = (*baseLevelQueueUnique)(nil)
)
func newBaseLevelQueueUnique(cfg *BaseConfig) (baseQueue, error) { func newBaseLevelQueueUnique(cfg *BaseConfig) (baseQueue, error) {
conn, db, err := prepareLevelDB(cfg) conn, db, err := prepareLevelDB(cfg)
@@ -38,34 +42,25 @@ func newBaseLevelQueueUnique(cfg *BaseConfig) (baseQueue, error) {
return nil, err return nil, err
} }
q.internal.Store(lq) q.internal.Store(lq)
q.baseLevelQueueCommonImpl = baseLevelQueueCommon(q.cfg, &q.muBase, func() baseLevelQueuePushPoper { return q.internal.Load() })
return q, nil return q, nil
} }
func (q *baseLevelQueueUnique) PushItem(ctx context.Context, data []byte) error {
c := baseLevelQueueCommon(q.cfg, &q.mu, func() baseLevelQueuePushPoper { return q.internal.Load() })
return c.PushItem(ctx, data)
}
func (q *baseLevelQueueUnique) PopItem(ctx context.Context) ([]byte, error) {
c := baseLevelQueueCommon(q.cfg, &q.mu, func() baseLevelQueuePushPoper { return q.internal.Load() })
return c.PopItem(ctx)
}
func (q *baseLevelQueueUnique) HasItem(ctx context.Context, data []byte) (bool, error) { func (q *baseLevelQueueUnique) HasItem(ctx context.Context, data []byte) (bool, error) {
q.mu.Lock() q.muBase.Lock()
defer q.mu.Unlock() defer q.muBase.Unlock()
return q.internal.Load().Has(data) return q.internal.Load().Has(data)
} }
func (q *baseLevelQueueUnique) Len(ctx context.Context) (int, error) { func (q *baseLevelQueueUnique) Len(ctx context.Context) (int, error) {
q.mu.Lock() q.muBase.Lock()
defer q.mu.Unlock() defer q.muBase.Unlock()
return int(q.internal.Load().Len()), nil return int(q.internal.Load().Len()), nil
} }
func (q *baseLevelQueueUnique) Close() error { func (q *baseLevelQueueUnique) Close() error {
q.mu.Lock() q.muBase.Lock()
defer q.mu.Unlock() defer q.muBase.Unlock()
err := q.internal.Load().Close() err := q.internal.Load().Close()
q.db = nil // the db is not managed by us, it's managed by the nosql manager q.db = nil // the db is not managed by us, it's managed by the nosql manager
_ = nosql.GetManager().CloseLevelDB(q.conn) _ = nosql.GetManager().CloseLevelDB(q.conn)
@@ -73,8 +68,8 @@ func (q *baseLevelQueueUnique) Close() error {
} }
func (q *baseLevelQueueUnique) RemoveAll(ctx context.Context) error { func (q *baseLevelQueueUnique) RemoveAll(ctx context.Context) error {
q.mu.Lock() q.muBase.Lock()
defer q.mu.Unlock() defer q.muBase.Unlock()
lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.QueueFullName)) lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.QueueFullName))
lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.SetFullName)) lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.SetFullName))
lq, err := levelqueue.NewUniqueQueue(q.db, []byte(q.cfg.QueueFullName), []byte(q.cfg.SetFullName), false) lq, err := levelqueue.NewUniqueQueue(q.db, []byte(q.cfg.QueueFullName), []byte(q.cfg.SetFullName), false)
+18 -9
View File
@@ -16,6 +16,7 @@ import (
) )
type baseRedis struct { type baseRedis struct {
*baseQueueNotifiable
client redis.UniversalClient client redis.UniversalClient
isUnique bool isUnique bool
cfg *BaseConfig cfg *BaseConfig
@@ -23,7 +24,10 @@ type baseRedis struct {
mu sync.Mutex // the old implementation is not thread-safe, the queue operation and set operation should be protected together mu sync.Mutex // the old implementation is not thread-safe, the queue operation and set operation should be protected together
} }
var _ baseQueue = (*baseRedis)(nil) var (
_ baseQueue = (*baseRedis)(nil)
_ baseQueueNotifiableInterface = (*baseRedis)(nil)
)
func newBaseRedisGeneric(cfg *BaseConfig, unique bool) (baseQueue, error) { func newBaseRedisGeneric(cfg *BaseConfig, unique bool) (baseQueue, error) {
client := nosql.GetManager().GetRedisClient(cfg.ConnStr) client := nosql.GetManager().GetRedisClient(cfg.ConnStr)
@@ -41,7 +45,7 @@ func newBaseRedisGeneric(cfg *BaseConfig, unique bool) (baseQueue, error) {
return nil, err return nil, err
} }
return &baseRedis{cfg: cfg, client: client, isUnique: unique}, nil return &baseRedis{cfg: cfg, client: client, isUnique: unique, baseQueueNotifiable: newBaseQueueNotifiable()}, nil
} }
func newBaseRedisSimple(cfg *BaseConfig) (baseQueue, error) { func newBaseRedisSimple(cfg *BaseConfig) (baseQueue, error) {
@@ -53,33 +57,38 @@ func newBaseRedisUnique(cfg *BaseConfig) (baseQueue, error) {
} }
func (q *baseRedis) PushItem(ctx context.Context, data []byte) error { func (q *baseRedis) PushItem(ctx context.Context, data []byte) error {
return backoffErr(ctx, backoffBegin, backoffUpper, time.After(pushBlockTime), func() (retry bool, err error) { _, err := backoffCall(ctx, backoffOptionsDefault(noNotifyChan, time.After(pushBlockTime)), func() (retry bool, ret any, err error) {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock() defer q.mu.Unlock()
cnt, err := q.client.LLen(ctx, q.cfg.QueueFullName).Result() cnt, err := q.client.LLen(ctx, q.cfg.QueueFullName).Result()
if err != nil { if err != nil {
return false, err return false, nil, err
} }
if int(cnt) >= q.cfg.Length { if int(cnt) >= q.cfg.Length {
return true, nil return true, nil, nil
} }
if q.isUnique { if q.isUnique {
added, err := q.client.SAdd(ctx, q.cfg.SetFullName, data).Result() added, err := q.client.SAdd(ctx, q.cfg.SetFullName, data).Result()
if err != nil { if err != nil {
return false, err return false, nil, err
} }
if added == 0 { if added == 0 {
return false, ErrAlreadyInQueue return false, nil, ErrAlreadyInQueue
} }
} }
return false, q.client.RPush(ctx, q.cfg.QueueFullName, data).Err() retry, err = false, q.client.RPush(ctx, q.cfg.QueueFullName, data).Err()
if err == nil {
q.notifyPushItem()
}
return retry, nil, err
}) })
return err
} }
func (q *baseRedis) PopItem(ctx context.Context) ([]byte, error) { func (q *baseRedis) PopItem(ctx context.Context) ([]byte, error) {
return backoffRetErr(ctx, backoffBegin, backoffUpper, infiniteTimerC, func() (retry bool, data []byte, err error) { return backoffCall(ctx, backoffOptionsDefault(q.notifySignal, infiniteTimerC), func() (retry bool, data []byte, err error) {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock() defer q.mu.Unlock()
+4 -2
View File
@@ -13,6 +13,8 @@ import (
func TestBaseRedis(t *testing.T) { func TestBaseRedis(t *testing.T) {
redisConn := test.PrepareTestRedis(t) redisConn := test.PrepareTestRedis(t)
queueSetting := setting.QueueSettings{Length: 10, ConnStr: redisConn} queueSetting := setting.QueueSettings{Length: 10, ConnStr: redisConn}
testQueueBasic(t, newBaseRedisSimple, toBaseConfig("baseRedis", queueSetting), false) optsSimple := testQueueBasicOptions{NotifiableQueue: true}
testQueueBasic(t, newBaseRedisUnique, toBaseConfig("baseRedisUnique", queueSetting), true) optsUnique := testQueueBasicOptions{UniqueQueue: true, NotifiableQueue: true}
testQueueBasic(t, newBaseRedisSimple, toBaseConfig("baseRedis", queueSetting), optsSimple)
testQueueBasic(t, newBaseRedisUnique, toBaseConfig("baseRedisUnique", queueSetting), optsUnique)
} }
+30 -1
View File
@@ -6,13 +6,20 @@ package queue
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error), cfg *BaseConfig, isUnique bool) { type testQueueBasicOptions struct {
UniqueQueue bool
NotifiableQueue bool
}
func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error), cfg *BaseConfig, opts testQueueBasicOptions) {
isUnique := opts.UniqueQueue
t.Run(fmt.Sprintf("testQueueBasic-%s-unique:%v", cfg.ManagedName, isUnique), func(t *testing.T) { t.Run(fmt.Sprintf("testQueueBasic-%s-unique:%v", cfg.ManagedName, isUnique), func(t *testing.T) {
q, err := newFn(cfg) q, err := newFn(cfg)
assert.NoError(t, err) assert.NoError(t, err)
@@ -85,6 +92,28 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error)
assert.ErrorIs(t, err, context.Canceled) assert.ErrorIs(t, err, context.Canceled)
assert.Nil(t, it) assert.Nil(t, it)
t.Run("PushNotify", func(t *testing.T) {
defer mockBackoffDuration(5000 * time.Millisecond)()
// pop an empty queue, but it can be notified and pop the item immediately
wg := sync.WaitGroup{}
wg.Go(func() {
it, err := q.PopItem(ctx) // it should return immediately after PushItem, no "backoff" waiting
assert.NoError(t, err)
assert.Equal(t, "item-notify", string(it))
})
time.Sleep(10 * time.Millisecond)
err = q.PushItem(ctx, []byte("item-notify"))
wg.Wait()
if opts.NotifiableQueue {
v, _ := q.(baseQueueNotifiableInterface)
assert.Empty(t, v.getNotifySignalChan(), "notify signal should have been read")
assert.NoError(t, q.PushItem(ctx, []byte("item-dummy")))
assert.Len(t, v.getNotifySignalChan(), 1, "notify signal should exist for newly pushed item")
_, err = q.PopItem(ctx)
assert.NoError(t, err)
}
})
// test blocking push if queue is full // test blocking push if queue is full
for i := 0; i < cfg.Length; i++ { for i := 0; i < cfg.Length; i++ {
err = q.PushItem(ctx, fmt.Appendf(nil, "item-%d", i)) err = q.PushItem(ctx, fmt.Appendf(nil, "item-%d", i))
+3 -1
View File
@@ -14,7 +14,9 @@ import (
) )
var ( var (
infiniteTimerC = make(chan time.Time) noNotifyChan chan struct{}
infiniteTimerC chan time.Time
batchDebounceDuration = 100 * time.Millisecond batchDebounceDuration = 100 * time.Millisecond
workerIdleDuration = 1 * time.Second workerIdleDuration = 1 * time.Second
shutdownDefaultTimeout = 2 * time.Second shutdownDefaultTimeout = 2 * time.Second