11 changed files with 540 additions and 360 deletions
@ -1,82 +0,0 @@ |
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package queue |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"time" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log" |
|
||||||
) |
|
||||||
|
|
||||||
// BatchedChannelQueueType is the type for batched channel queue
|
|
||||||
const BatchedChannelQueueType Type = "batched-channel" |
|
||||||
|
|
||||||
// BatchedChannelQueueConfiguration is the configuration for a BatchedChannelQueue
|
|
||||||
type BatchedChannelQueueConfiguration struct { |
|
||||||
QueueLength int |
|
||||||
BatchLength int |
|
||||||
Workers int |
|
||||||
} |
|
||||||
|
|
||||||
// BatchedChannelQueue implements
|
|
||||||
type BatchedChannelQueue struct { |
|
||||||
*ChannelQueue |
|
||||||
batchLength int |
|
||||||
} |
|
||||||
|
|
||||||
// NewBatchedChannelQueue create a memory channel queue
|
|
||||||
func NewBatchedChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { |
|
||||||
configInterface, err := toConfig(BatchedChannelQueueConfiguration{}, cfg) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
config := configInterface.(BatchedChannelQueueConfiguration) |
|
||||||
return &BatchedChannelQueue{ |
|
||||||
&ChannelQueue{ |
|
||||||
queue: make(chan Data, config.QueueLength), |
|
||||||
handle: handle, |
|
||||||
exemplar: exemplar, |
|
||||||
workers: config.Workers, |
|
||||||
}, |
|
||||||
config.BatchLength, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Run starts to run the queue
|
|
||||||
func (c *BatchedChannelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { |
|
||||||
atShutdown(context.Background(), func() { |
|
||||||
log.Warn("BatchedChannelQueue is not shutdownable!") |
|
||||||
}) |
|
||||||
atTerminate(context.Background(), func() { |
|
||||||
log.Warn("BatchedChannelQueue is not terminatable!") |
|
||||||
}) |
|
||||||
for i := 0; i < c.workers; i++ { |
|
||||||
go func() { |
|
||||||
delay := time.Millisecond * 300 |
|
||||||
var datas = make([]Data, 0, c.batchLength) |
|
||||||
for { |
|
||||||
select { |
|
||||||
case data := <-c.queue: |
|
||||||
datas = append(datas, data) |
|
||||||
if len(datas) >= c.batchLength { |
|
||||||
c.handle(datas...) |
|
||||||
datas = make([]Data, 0, c.batchLength) |
|
||||||
} |
|
||||||
case <-time.After(delay): |
|
||||||
delay = time.Millisecond * 100 |
|
||||||
if len(datas) > 0 { |
|
||||||
c.handle(datas...) |
|
||||||
datas = make([]Data, 0, c.batchLength) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func init() { |
|
||||||
queuesMap[BatchedChannelQueueType] = NewBatchedChannelQueue |
|
||||||
} |
|
@ -1,46 +0,0 @@ |
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package queue |
|
||||||
|
|
||||||
import "testing" |
|
||||||
|
|
||||||
import "github.com/stretchr/testify/assert" |
|
||||||
|
|
||||||
import "context" |
|
||||||
|
|
||||||
func TestBatchedChannelQueue(t *testing.T) { |
|
||||||
handleChan := make(chan *testData) |
|
||||||
handle := func(data ...Data) { |
|
||||||
assert.True(t, len(data) == 2) |
|
||||||
for _, datum := range data { |
|
||||||
testDatum := datum.(*testData) |
|
||||||
handleChan <- testDatum |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
nilFn := func(_ context.Context, _ func()) {} |
|
||||||
|
|
||||||
queue, err := NewBatchedChannelQueue(handle, BatchedChannelQueueConfiguration{QueueLength: 20, BatchLength: 2, Workers: 1}, &testData{}) |
|
||||||
assert.NoError(t, err) |
|
||||||
|
|
||||||
go queue.Run(nilFn, nilFn) |
|
||||||
|
|
||||||
test1 := testData{"A", 1} |
|
||||||
test2 := testData{"B", 2} |
|
||||||
|
|
||||||
queue.Push(&test1) |
|
||||||
go queue.Push(&test2) |
|
||||||
|
|
||||||
result1 := <-handleChan |
|
||||||
assert.Equal(t, test1.TestString, result1.TestString) |
|
||||||
assert.Equal(t, test1.TestInt, result1.TestInt) |
|
||||||
|
|
||||||
result2 := <-handleChan |
|
||||||
assert.Equal(t, test2.TestString, result2.TestString) |
|
||||||
assert.Equal(t, test2.TestInt, result2.TestInt) |
|
||||||
|
|
||||||
err = queue.Push(test1) |
|
||||||
assert.Error(t, err) |
|
||||||
} |
|
@ -0,0 +1,239 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package queue |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
) |
||||||
|
|
||||||
|
// WorkerPool takes
|
||||||
|
type WorkerPool struct { |
||||||
|
lock sync.Mutex |
||||||
|
baseCtx context.Context |
||||||
|
cancel context.CancelFunc |
||||||
|
cond *sync.Cond |
||||||
|
numberOfWorkers int |
||||||
|
batchLength int |
||||||
|
handle HandlerFunc |
||||||
|
dataChan chan Data |
||||||
|
blockTimeout time.Duration |
||||||
|
boostTimeout time.Duration |
||||||
|
boostWorkers int |
||||||
|
} |
||||||
|
|
||||||
|
// Push pushes the data to the internal channel
|
||||||
|
func (p *WorkerPool) Push(data Data) { |
||||||
|
p.lock.Lock() |
||||||
|
if p.blockTimeout > 0 && p.boostTimeout > 0 { |
||||||
|
p.lock.Unlock() |
||||||
|
p.pushBoost(data) |
||||||
|
} else { |
||||||
|
p.lock.Unlock() |
||||||
|
p.dataChan <- data |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (p *WorkerPool) pushBoost(data Data) { |
||||||
|
select { |
||||||
|
case p.dataChan <- data: |
||||||
|
default: |
||||||
|
p.lock.Lock() |
||||||
|
if p.blockTimeout <= 0 { |
||||||
|
p.lock.Unlock() |
||||||
|
p.dataChan <- data |
||||||
|
return |
||||||
|
} |
||||||
|
ourTimeout := p.blockTimeout |
||||||
|
timer := time.NewTimer(p.blockTimeout) |
||||||
|
p.lock.Unlock() |
||||||
|
select { |
||||||
|
case p.dataChan <- data: |
||||||
|
if timer.Stop() { |
||||||
|
select { |
||||||
|
case <-timer.C: |
||||||
|
default: |
||||||
|
} |
||||||
|
} |
||||||
|
case <-timer.C: |
||||||
|
p.lock.Lock() |
||||||
|
if p.blockTimeout > ourTimeout { |
||||||
|
p.lock.Unlock() |
||||||
|
p.dataChan <- data |
||||||
|
return |
||||||
|
} |
||||||
|
p.blockTimeout *= 2 |
||||||
|
log.Warn("Worker Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) |
||||||
|
ctx, cancel := context.WithCancel(p.baseCtx) |
||||||
|
go func() { |
||||||
|
<-time.After(p.boostTimeout) |
||||||
|
cancel() |
||||||
|
p.lock.Lock() |
||||||
|
p.blockTimeout /= 2 |
||||||
|
p.lock.Unlock() |
||||||
|
}() |
||||||
|
p.addWorkers(ctx, p.boostWorkers) |
||||||
|
p.lock.Unlock() |
||||||
|
p.dataChan <- data |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NumberOfWorkers returns the number of current workers in the pool
|
||||||
|
func (p *WorkerPool) NumberOfWorkers() int { |
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
return p.numberOfWorkers |
||||||
|
} |
||||||
|
|
||||||
|
// AddWorkers adds workers to the pool
|
||||||
|
func (p *WorkerPool) AddWorkers(number int, timeout time.Duration) context.CancelFunc { |
||||||
|
var ctx context.Context |
||||||
|
var cancel context.CancelFunc |
||||||
|
if timeout > 0 { |
||||||
|
ctx, cancel = context.WithTimeout(p.baseCtx, timeout) |
||||||
|
} else { |
||||||
|
ctx, cancel = context.WithCancel(p.baseCtx) |
||||||
|
} |
||||||
|
|
||||||
|
p.addWorkers(ctx, number) |
||||||
|
return cancel |
||||||
|
} |
||||||
|
|
||||||
|
// addWorkers adds workers to the pool
|
||||||
|
func (p *WorkerPool) addWorkers(ctx context.Context, number int) { |
||||||
|
for i := 0; i < number; i++ { |
||||||
|
p.lock.Lock() |
||||||
|
if p.cond == nil { |
||||||
|
p.cond = sync.NewCond(&p.lock) |
||||||
|
} |
||||||
|
p.numberOfWorkers++ |
||||||
|
p.lock.Unlock() |
||||||
|
go func() { |
||||||
|
p.doWork(ctx) |
||||||
|
|
||||||
|
p.lock.Lock() |
||||||
|
p.numberOfWorkers-- |
||||||
|
if p.numberOfWorkers <= 0 { |
||||||
|
// numberOfWorkers can't go negative but...
|
||||||
|
p.numberOfWorkers = 0 |
||||||
|
p.cond.Broadcast() |
||||||
|
} |
||||||
|
p.lock.Unlock() |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Wait for WorkerPool to finish
|
||||||
|
func (p *WorkerPool) Wait() { |
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
if p.cond == nil { |
||||||
|
p.cond = sync.NewCond(&p.lock) |
||||||
|
} |
||||||
|
if p.numberOfWorkers <= 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
p.cond.Wait() |
||||||
|
} |
||||||
|
|
||||||
|
// CleanUp will drain the remaining contents of the channel
|
||||||
|
// This should be called after AddWorkers context is closed
|
||||||
|
func (p *WorkerPool) CleanUp(ctx context.Context) { |
||||||
|
log.Trace("CleanUp") |
||||||
|
close(p.dataChan) |
||||||
|
for data := range p.dataChan { |
||||||
|
p.handle(data) |
||||||
|
select { |
||||||
|
case <-ctx.Done(): |
||||||
|
log.Warn("Cleanup context closed before finishing clean-up") |
||||||
|
return |
||||||
|
default: |
||||||
|
} |
||||||
|
} |
||||||
|
log.Trace("CleanUp done") |
||||||
|
} |
||||||
|
|
||||||
|
func (p *WorkerPool) doWork(ctx context.Context) { |
||||||
|
delay := time.Millisecond * 300 |
||||||
|
var data = make([]Data, 0, p.batchLength) |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-ctx.Done(): |
||||||
|
if len(data) > 0 { |
||||||
|
log.Trace("Handling: %d data, %v", len(data), data) |
||||||
|
p.handle(data...) |
||||||
|
} |
||||||
|
log.Trace("Worker shutting down") |
||||||
|
return |
||||||
|
case datum, ok := <-p.dataChan: |
||||||
|
if !ok { |
||||||
|
// the dataChan has been closed - we should finish up:
|
||||||
|
if len(data) > 0 { |
||||||
|
log.Trace("Handling: %d data, %v", len(data), data) |
||||||
|
p.handle(data...) |
||||||
|
} |
||||||
|
log.Trace("Worker shutting down") |
||||||
|
return |
||||||
|
} |
||||||
|
data = append(data, datum) |
||||||
|
if len(data) >= p.batchLength { |
||||||
|
log.Trace("Handling: %d data, %v", len(data), data) |
||||||
|
p.handle(data...) |
||||||
|
data = make([]Data, 0, p.batchLength) |
||||||
|
} |
||||||
|
default: |
||||||
|
timer := time.NewTimer(delay) |
||||||
|
select { |
||||||
|
case <-ctx.Done(): |
||||||
|
if timer.Stop() { |
||||||
|
select { |
||||||
|
case <-timer.C: |
||||||
|
default: |
||||||
|
} |
||||||
|
} |
||||||
|
if len(data) > 0 { |
||||||
|
log.Trace("Handling: %d data, %v", len(data), data) |
||||||
|
p.handle(data...) |
||||||
|
} |
||||||
|
log.Trace("Worker shutting down") |
||||||
|
return |
||||||
|
case datum, ok := <-p.dataChan: |
||||||
|
if timer.Stop() { |
||||||
|
select { |
||||||
|
case <-timer.C: |
||||||
|
default: |
||||||
|
} |
||||||
|
} |
||||||
|
if !ok { |
||||||
|
// the dataChan has been closed - we should finish up:
|
||||||
|
if len(data) > 0 { |
||||||
|
log.Trace("Handling: %d data, %v", len(data), data) |
||||||
|
p.handle(data...) |
||||||
|
} |
||||||
|
log.Trace("Worker shutting down") |
||||||
|
return |
||||||
|
} |
||||||
|
data = append(data, datum) |
||||||
|
if len(data) >= p.batchLength { |
||||||
|
log.Trace("Handling: %d data, %v", len(data), data) |
||||||
|
p.handle(data...) |
||||||
|
data = make([]Data, 0, p.batchLength) |
||||||
|
} |
||||||
|
case <-timer.C: |
||||||
|
delay = time.Millisecond * 100 |
||||||
|
if len(data) > 0 { |
||||||
|
log.Trace("Handling: %d data, %v", len(data), data) |
||||||
|
p.handle(data...) |
||||||
|
data = make([]Data, 0, p.batchLength) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue