Skip to content

Commit d20848f

Browse files
committed
fix
1 parent 74f0c84 commit d20848f

File tree

3 files changed

+35
-20
lines changed

3 files changed

+35
-20
lines changed

modules/queue/workergroup.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
6363
// TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum"
6464
// The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later
6565
// So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary.
66+
// This data-race is not serious, as long as a new worker will be started soon to make sure there are enough workers,
67+
// so no need to hugely refactor at the moment.
6668
q.workerNumMu.Lock()
6769
noWorker := q.workerNum == 0
6870
if full || noWorker {
@@ -136,6 +138,15 @@ func (q *WorkerPoolQueue[T]) basePushForShutdown(items ...T) bool {
136138
return true
137139
}
138140

141+
func resetIdleTicker(t *time.Ticker, dur time.Duration) {
142+
// reset the idle ticker, and drain the tick after reset in case a tick is already triggered
143+
t.Reset(dur)
144+
select {
145+
case <-t.C:
146+
default:
147+
}
148+
}
149+
139150
// doStartNewWorker starts a new worker for the queue, the worker reads from worker's channel and handles the items.
140151
func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
141152
wp.wg.Add(1)
@@ -146,8 +157,6 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
146157
log.Debug("Queue %q starts new worker", q.GetName())
147158
defer log.Debug("Queue %q stops idle worker", q.GetName())
148159

149-
atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging
150-
151160
t := time.NewTicker(workerIdleDuration)
152161
defer t.Stop()
153162

@@ -168,12 +177,7 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
168177
continue
169178
}
170179
q.doWorkerHandle(batch)
171-
// reset the idle ticker, and drain the tick after reset in case a tick is already triggered
172-
t.Reset(workerIdleDuration)
173-
select {
174-
case <-t.C:
175-
default:
176-
}
180+
resetIdleTicker(t, workerIdleDuration) // key code for TestWorkerPoolQueueWorkerIdleReset
177181
case <-t.C:
178182
q.workerNumMu.Lock()
179183
keepWorking = q.workerNum <= 1 // keep the last worker running

modules/queue/workerqueue.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ type WorkerPoolQueue[T any] struct {
4040
workerMaxNum int
4141
workerActiveNum int
4242
workerNumMu sync.Mutex
43-
44-
workerStartedCounter int32
4543
}
4644

4745
type flushType chan struct{}

modules/queue/workerqueue_test.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ package queue
55

66
import (
77
"context"
8+
"slices"
89
"strconv"
910
"sync"
11+
"sync/atomic"
1012
"testing"
1113
"time"
1214

@@ -250,23 +252,34 @@ func TestWorkerPoolQueueShutdown(t *testing.T) {
250252

251253
func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) {
252254
defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)()
253-
defer mockBackoffDuration(10 * time.Millisecond)()
255+
defer mockBackoffDuration(5 * time.Millisecond)()
254256

257+
var q *WorkerPoolQueue[int]
258+
var handledCount atomic.Int32
259+
var hasOnlyOneWorkerRunning atomic.Bool
255260
handler := func(items ...int) (unhandled []int) {
256-
time.Sleep(50 * time.Millisecond)
261+
handledCount.Add(int32(len(items)))
262+
// make each work have different duration, and check the active worker number periodically
263+
var activeNums []int
264+
for i := 0; i < 5-items[0]%2; i++ {
265+
time.Sleep(workerIdleDuration * 2)
266+
activeNums = append(activeNums, q.GetWorkerActiveNumber())
267+
}
268+
// When the queue never becomes empty, the existing workers should keep working
269+
// It is not 100% true at the moment because the data-race in workergroup.go is not resolved, see that TODO */
270+
// If the "active worker numbers" is like [2 2 ... 1 1], it means that an existing worker exited and the no new worker is started.
271+
if slices.Equal([]int{1, 1}, activeNums[len(activeNums)-2:]) {
272+
hasOnlyOneWorkerRunning.Store(true)
273+
}
257274
return nil
258275
}
259-
260-
q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
276+
q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
261277
stop := runWorkerPoolQueue(q)
262-
for i := 0; i < 20; i++ {
278+
for i := 0; i < 100; i++ {
263279
assert.NoError(t, q.Push(i))
264280
}
265-
266281
time.Sleep(500 * time.Millisecond)
267-
assert.EqualValues(t, 2, q.GetWorkerNumber())
268-
assert.EqualValues(t, 2, q.GetWorkerActiveNumber())
269-
// when the queue never becomes empty, the existing workers should keep working
270-
assert.EqualValues(t, 2, q.workerStartedCounter)
282+
assert.Greater(t, int(handledCount.Load()), 4) // make sure there are enough items handled during the test
283+
assert.False(t, hasOnlyOneWorkerRunning.Load(), "a slow handler should not block other workers from starting")
271284
stop()
272285
}

0 commit comments

Comments
 (0)