From 85042634fc7cac1590c7e6309aa3abfd18768f18 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 13 Nov 2019 18:28:42 +0000 Subject: [PATCH 01/35] Queue: Add generic graceful queues with settings --- custom/conf/app.ini.sample | 13 ++ .../doc/advanced/config-cheat-sheet.en-us.md | 8 + modules/queue/queue.go | 128 ++++++++++++ modules/queue/queue_batch.go | 78 +++++++ modules/queue/queue_batch_test.go | 46 +++++ modules/queue/queue_channel.go | 75 +++++++ modules/queue/queue_channel_test.go | 38 ++++ modules/queue/queue_disk.go | 158 +++++++++++++++ modules/queue/queue_disk_channel.go | 160 +++++++++++++++ modules/queue/queue_disk_channel_test.go | 105 ++++++++++ modules/queue/queue_disk_test.go | 99 +++++++++ modules/queue/queue_redis.go | 190 ++++++++++++++++++ modules/queue/queue_test.go | 42 ++++ modules/queue/queue_wrapped.go | 183 +++++++++++++++++ modules/setting/queue.go | 143 +++++++++++++ 15 files changed, 1466 insertions(+) create mode 100644 modules/queue/queue.go create mode 100644 modules/queue/queue_batch.go create mode 100644 modules/queue/queue_batch_test.go create mode 100644 modules/queue/queue_channel.go create mode 100644 modules/queue/queue_channel_test.go create mode 100644 modules/queue/queue_disk.go create mode 100644 modules/queue/queue_disk_channel.go create mode 100644 modules/queue/queue_disk_channel_test.go create mode 100644 modules/queue/queue_disk_test.go create mode 100644 modules/queue/queue_redis.go create mode 100644 modules/queue/queue_test.go create mode 100644 modules/queue/queue_wrapped.go create mode 100644 modules/setting/queue.go diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index c9ca821280b1..4b810f91f766 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -371,6 +371,19 @@ REPO_INDEXER_INCLUDE = ; A comma separated list of glob patterns to exclude from the index; ; default is empty REPO_INDEXER_EXCLUDE = +[queue] +; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy +; default to persistable-channel +TYPE = persistable-channel +; data-dir for storing persistable queues and level queues, individual queues will be named by their type +DATADIR = queues/ +; Default queue length before a channel queue will block +LENGTH = 20 +; Batch size to send for batched queues +BATCH_LENGTH = 20 +; Connection string for redis queues this will store the redis connection string. +CONN_STR = "addrs=127.0.0.1:6379 db=0" + [admin] ; Disallow regular (non-admin) users from creating organizations. DISABLE_REGULAR_ORG_CREATION = false diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index e71fb1b3bc3e..2db543f5e630 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -234,6 +234,14 @@ relation to port exhaustion. - `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed. - `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to zero to never timeout. +## Queue (`queue`) + +- `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel`, `batched-channel`, `channel`, `level`, `redis`, `dummy` +- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. +- `LENGTH`: **20**: Maximal queue size before channel queues block +- `BATCH_LENGTH`: **20**: Batch data before passing to the handler +- `CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Connection string for the redis queue type. + ## Admin (`admin`) - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled diff --git a/modules/queue/queue.go b/modules/queue/queue.go new file mode 100644 index 000000000000..1220db5c03bb --- /dev/null +++ b/modules/queue/queue.go @@ -0,0 +1,128 @@ +// 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" + "encoding/json" + "fmt" + "reflect" +) + +// ErrInvalidConfiguration is called when there is invalid configuration for a queue +type ErrInvalidConfiguration struct { + cfg interface{} + err error +} + +func (err ErrInvalidConfiguration) Error() string { + if err.err != nil { + return fmt.Sprintf("Invalid Configuration Argument: %v: Error: %v", err.cfg, err.err) + } + return fmt.Sprintf("Invalid Configuration Argument: %v", err.cfg) +} + +// IsErrInvalidConfiguration checks if an error is an ErrInvalidConfiguration +func IsErrInvalidConfiguration(err error) bool { + _, ok := err.(ErrInvalidConfiguration) + return ok +} + +// Type is a type of Queue +type Type string + +// Data defines an type of queuable data +type Data interface{} + +// HandlerFunc is a function that takes a variable amount of data and processes it +type HandlerFunc func(...Data) + +// NewQueueFunc is a function that creates a queue +type NewQueueFunc func(handler HandlerFunc, config interface{}, exemplar interface{}) (Queue, error) + +// Shutdownable represents a queue that can be shutdown +type Shutdownable interface { + Shutdown() + Terminate() +} + +// Queue defines an interface to save an issue indexer queue +type Queue interface { + Run(atShutdown, atTerminate func(context.Context, func())) + Push(Data) error +} + +// DummyQueueType is the type for the dummy queue +const DummyQueueType Type = "dummy" + +// NewDummyQueue creates a new DummyQueue +func NewDummyQueue(handler HandlerFunc, opts, exemplar interface{}) (Queue, error) { + return &DummyQueue{}, nil +} + +// DummyQueue represents an empty queue +type DummyQueue struct { +} + +// Run starts to run the queue +func (b *DummyQueue) Run(_, _ func(context.Context, func())) {} + +// Push pushes data to the queue +func (b *DummyQueue) Push(Data) error { + return nil +} + +func toConfig(exemplar, cfg interface{}) (interface{}, error) { + if reflect.TypeOf(cfg).AssignableTo(reflect.TypeOf(exemplar)) { + return cfg, nil + } + + configBytes, ok := cfg.([]byte) + if !ok { + configStr, ok := cfg.(string) + if !ok { + return nil, ErrInvalidConfiguration{cfg: cfg} + } + configBytes = []byte(configStr) + } + newVal := reflect.New(reflect.TypeOf(exemplar)) + if err := json.Unmarshal(configBytes, newVal.Interface()); err != nil { + return nil, ErrInvalidConfiguration{cfg: cfg, err: err} + } + return newVal.Elem().Interface(), nil +} + +var queuesMap = map[Type]NewQueueFunc{DummyQueueType: NewDummyQueue} + +// RegisteredTypes provides the list of requested types of queues +func RegisteredTypes() []Type { + types := make([]Type, len(queuesMap)) + i := 0 + for key := range queuesMap { + types[i] = key + i++ + } + return types +} + +// RegisteredTypesAsString provides the list of requested types of queues +func RegisteredTypesAsString() []string { + types := make([]string, len(queuesMap)) + i := 0 + for key := range queuesMap { + types[i] = string(key) + i++ + } + return types +} + +// CreateQueue takes a queue Type and HandlerFunc some options and possibly an exemplar and returns a Queue or an error +func CreateQueue(queueType Type, handlerFunc HandlerFunc, opts, exemplar interface{}) (Queue, error) { + newFn, ok := queuesMap[queueType] + if !ok { + return nil, fmt.Errorf("Unsupported queue type: %v", queueType) + } + return newFn(handlerFunc, opts, exemplar) +} diff --git a/modules/queue/queue_batch.go b/modules/queue/queue_batch.go new file mode 100644 index 000000000000..07166441e6df --- /dev/null +++ b/modules/queue/queue_batch.go @@ -0,0 +1,78 @@ +// 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 +} + +// 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, + }, + 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!") + }) + 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 +} diff --git a/modules/queue/queue_batch_test.go b/modules/queue/queue_batch_test.go new file mode 100644 index 000000000000..08d3641da123 --- /dev/null +++ b/modules/queue/queue_batch_test.go @@ -0,0 +1,46 @@ +// 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}, &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) +} diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go new file mode 100644 index 000000000000..e0cba2db01dd --- /dev/null +++ b/modules/queue/queue_channel.go @@ -0,0 +1,75 @@ +// 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" + "fmt" + "reflect" + + "code.gitea.io/gitea/modules/log" +) + +// ChannelQueueType is the type for channel queue +const ChannelQueueType Type = "channel" + +// ChannelQueueConfiguration is the configuration for a ChannelQueue +type ChannelQueueConfiguration struct { + QueueLength int +} + +// ChannelQueue implements +type ChannelQueue struct { + queue chan Data + handle HandlerFunc + exemplar interface{} +} + +// NewChannelQueue create a memory channel queue +func NewChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { + configInterface, err := toConfig(ChannelQueueConfiguration{}, cfg) + if err != nil { + return nil, err + } + config := configInterface.(ChannelQueueConfiguration) + return &ChannelQueue{ + queue: make(chan Data, config.QueueLength), + handle: handle, + exemplar: exemplar, + }, nil +} + +// Run starts to run the queue +func (c *ChannelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { + atShutdown(context.Background(), func() { + log.Warn("ChannelQueue is not shutdownable!") + }) + atTerminate(context.Background(), func() { + log.Warn("ChannelQueue is not terminatable!") + }) + go func() { + for data := range c.queue { + c.handle(data) + } + }() +} + +// Push will push the indexer data to queue +func (c *ChannelQueue) Push(data Data) error { + if c.exemplar != nil { + // Assert data is of same type as r.exemplar + t := reflect.TypeOf(data) + exemplarType := reflect.TypeOf(c.exemplar) + if !t.AssignableTo(exemplarType) || data == nil { + return fmt.Errorf("Unable to assign data: %v to same type as exemplar: %v in queue: %s", data, c.exemplar, c.name) + } + } + c.queue <- data + return nil +} + +func init() { + queuesMap[ChannelQueueType] = NewChannelQueue +} diff --git a/modules/queue/queue_channel_test.go b/modules/queue/queue_channel_test.go new file mode 100644 index 000000000000..77f4a8fe8f59 --- /dev/null +++ b/modules/queue/queue_channel_test.go @@ -0,0 +1,38 @@ +// 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" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChannelQueue(t *testing.T) { + handleChan := make(chan *testData) + handle := func(data ...Data) { + for _, datum := range data { + testDatum := datum.(*testData) + handleChan <- testDatum + } + } + + nilFn := func(_ context.Context, _ func()) {} + + queue, err := NewChannelQueue(handle, ChannelQueueConfiguration{QueueLength: 20}, &testData{}) + assert.NoError(t, err) + + go queue.Run(nilFn, nilFn) + + test1 := testData{"A", 1} + go queue.Push(&test1) + result1 := <-handleChan + assert.Equal(t, test1.TestString, result1.TestString) + assert.Equal(t, test1.TestInt, result1.TestInt) + + err = queue.Push(test1) + assert.Error(t, err) +} diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go new file mode 100644 index 000000000000..dafff5c21c8e --- /dev/null +++ b/modules/queue/queue_disk.go @@ -0,0 +1,158 @@ +// 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" + "encoding/json" + "fmt" + "reflect" + "time" + + "code.gitea.io/gitea/modules/log" + + "gitea.com/lunny/levelqueue" +) + +// LevelQueueType is the type for level queue +const LevelQueueType Type = "level" + +// LevelQueueConfiguration is the configuration for a LevelQueue +type LevelQueueConfiguration struct { + DataDir string + BatchLength int +} + +// LevelQueue implements a disk library queue +type LevelQueue struct { + handle HandlerFunc + queue *levelqueue.Queue + batchLength int + closed chan struct{} + exemplar interface{} +} + +// NewLevelQueue creates a ledis local queue +func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { + configInterface, err := toConfig(LevelQueueConfiguration{}, cfg) + if err != nil { + return nil, err + } + config := configInterface.(LevelQueueConfiguration) + + queue, err := levelqueue.Open(config.DataDir) + if err != nil { + return nil, err + } + + return &LevelQueue{ + handle: handle, + queue: queue, + batchLength: config.BatchLength, + exemplar: exemplar, + closed: make(chan struct{}), + }, nil +} + +// Run starts to run the queue +func (l *LevelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { + atShutdown(context.Background(), l.Shutdown) + atTerminate(context.Background(), l.Terminate) + var i int + var datas = make([]Data, 0, l.batchLength) + for { + select { + case <-l.closed: + if len(datas) > 0 { + log.Trace("Handling: %d data, %v", len(datas), datas) + l.handle(datas...) + } + return + default: + } + i++ + if len(datas) > l.batchLength || (len(datas) > 0 && i > 3) { + log.Trace("Handling: %d data, %v", len(datas), datas) + l.handle(datas...) + datas = make([]Data, 0, l.batchLength) + i = 0 + continue + } + + bs, err := l.queue.RPop() + if err != nil { + if err != levelqueue.ErrNotFound { + log.Error("RPop: %v", err) + } + time.Sleep(time.Millisecond * 100) + continue + } + + if len(bs) == 0 { + time.Sleep(time.Millisecond * 100) + continue + } + + var data Data + if l.exemplar != nil { + t := reflect.TypeOf(l.exemplar) + n := reflect.New(t) + ne := n.Elem() + err = json.Unmarshal(bs, ne.Addr().Interface()) + data = ne.Interface().(Data) + } else { + err = json.Unmarshal(bs, &data) + } + if err != nil { + log.Error("Unmarshal: %v", err) + time.Sleep(time.Millisecond * 10) + continue + } + + log.Trace("LevelQueue: task found: %#v", data) + + datas = append(datas, data) + } +} + +// Push will push the indexer data to queue +func (l *LevelQueue) Push(data Data) error { + if l.exemplar != nil { + // Assert data is of same type as r.exemplar + value := reflect.ValueOf(data) + t := value.Type() + exemplarType := reflect.ValueOf(l.exemplar).Type() + if !t.AssignableTo(exemplarType) || data == nil { + return fmt.Errorf("Unable to assign data: %v to same type as exemplar: %v in %s", data, l.exemplar, l.name) + } + } + bs, err := json.Marshal(data) + if err != nil { + return err + } + return l.queue.LPush(bs) +} + +// Shutdown this queue and stop processing +func (l *LevelQueue) Shutdown() { + select { + case <-l.closed: + default: + close(l.closed) + } +} + +// Terminate this queue and close the queue +func (l *LevelQueue) Terminate() { + l.Shutdown() + if err := l.queue.Close(); err != nil && err.Error() != "leveldb: closed" { + log.Error("Error whilst closing internal queue: %v", err) + } + +} + +func init() { + queuesMap[LevelQueueType] = NewLevelQueue +} diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go new file mode 100644 index 000000000000..b13f1b9603de --- /dev/null +++ b/modules/queue/queue_disk_channel.go @@ -0,0 +1,160 @@ +// 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" +) + +// PersistableChannelQueueType is the type for persistable queue +const PersistableChannelQueueType Type = "persistable-channel" + +// PersistableChannelQueueConfiguration is the configuration for a PersistableChannelQueue +type PersistableChannelQueueConfiguration struct { + DataDir string + BatchLength int + QueueLength int + Timeout time.Duration + MaxAttempts int +} + +// PersistableChannelQueue wraps a channel queue and level queue together +type PersistableChannelQueue struct { + *BatchedChannelQueue + delayedStarter + closed chan struct{} +} + +// NewPersistableChannelQueue creates a wrapped batched channel queue with persistable level queue backend when shutting down +// This differs from a wrapped queue in that the persistent queue is only used to persist at shutdown/terminate +func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { + configInterface, err := toConfig(PersistableChannelQueueConfiguration{}, cfg) + if err != nil { + return nil, err + } + config := configInterface.(PersistableChannelQueueConfiguration) + + batchChannelQueue, err := NewBatchedChannelQueue(handle, BatchedChannelQueueConfiguration{ + QueueLength: config.QueueLength, + BatchLength: config.BatchLength, + }, exemplar) + if err != nil { + return nil, err + } + + levelCfg := LevelQueueConfiguration{ + DataDir: config.DataDir, + BatchLength: config.BatchLength, + } + + levelQueue, err := NewLevelQueue(handle, levelCfg, exemplar) + if err == nil { + return &PersistableChannelQueue{ + BatchedChannelQueue: batchChannelQueue.(*BatchedChannelQueue), + delayedStarter: delayedStarter{ + internal: levelQueue.(*LevelQueue), + }, + closed: make(chan struct{}), + }, nil + } + if IsErrInvalidConfiguration(err) { + // Retrying ain't gonna make this any better... + return nil, ErrInvalidConfiguration{cfg: cfg} + } + + return &PersistableChannelQueue{ + BatchedChannelQueue: batchChannelQueue.(*BatchedChannelQueue), + delayedStarter: delayedStarter{ + cfg: levelCfg, + underlying: LevelQueueType, + timeout: config.Timeout, + maxAttempts: config.MaxAttempts, + }, + closed: make(chan struct{}), + }, nil +} + +// Push will push the indexer data to queue +func (p *PersistableChannelQueue) Push(data Data) error { + select { + case <-p.closed: + return p.internal.Push(data) + default: + return p.BatchedChannelQueue.Push(data) + } +} + +// Run starts to run the queue +func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { + p.lock.Lock() + if p.internal == nil { + p.setInternal(atShutdown, p.handle, p.exemplar) + } else { + p.lock.Unlock() + } + atShutdown(context.Background(), p.Shutdown) + atTerminate(context.Background(), p.Terminate) + + // Just run the level queue - we shut it down later + go p.internal.Run(func(_ context.Context, _ func()) {}, func(_ context.Context, _ func()) {}) + delay := time.Millisecond * 300 + var datas = make([]Data, 0, p.batchLength) +loop: + for { + select { + case data := <-p.queue: + datas = append(datas, data) + if len(datas) >= p.batchLength { + p.handle(datas...) + datas = make([]Data, 0, p.batchLength) + } + case <-time.After(delay): + delay = time.Millisecond * 100 + if len(datas) > 0 { + p.handle(datas...) + datas = make([]Data, 0, p.batchLength) + } + case <-p.closed: + if len(datas) > 0 { + p.handle(datas...) + } + break loop + } + } + go func() { + for data := range p.queue { + _ = p.internal.Push(data) + } + }() +} + +// Shutdown processing this queue +func (p *PersistableChannelQueue) Shutdown() { + select { + case <-p.closed: + default: + close(p.closed) + p.lock.Lock() + defer p.lock.Unlock() + if p.internal != nil { + p.internal.(*LevelQueue).Shutdown() + } + } +} + +// Terminate this queue and close the queue +func (p *PersistableChannelQueue) Terminate() { + p.Shutdown() + p.lock.Lock() + defer p.lock.Unlock() + if p.internal != nil { + p.internal.(*LevelQueue).Terminate() + } +} + +func init() { + queuesMap[PersistableChannelQueueType] = NewPersistableChannelQueue +} diff --git a/modules/queue/queue_disk_channel_test.go b/modules/queue/queue_disk_channel_test.go new file mode 100644 index 000000000000..66c90f3bc3f3 --- /dev/null +++ b/modules/queue/queue_disk_channel_test.go @@ -0,0 +1,105 @@ +// 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" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPersistableChannelQueue(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 + } + } + + var queueShutdown func() + var queueTerminate func() + + tmpDir, err := ioutil.TempDir("", "persistable-channel-queue-test-data") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + queue, err := NewPersistableChannelQueue(handle, PersistableChannelQueueConfiguration{ + DataDir: tmpDir, + BatchLength: 2, + QueueLength: 20, + }, &testData{}) + assert.NoError(t, err) + + go queue.Run(func(_ context.Context, shutdown func()) { + queueShutdown = shutdown + }, func(_ context.Context, terminate func()) { + queueTerminate = terminate + }) + + test1 := testData{"A", 1} + test2 := testData{"B", 2} + + err = queue.Push(&test1) + assert.NoError(t, err) + go func() { + err = queue.Push(&test2) + assert.NoError(t, err) + }() + + 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) + + queueShutdown() + time.Sleep(200 * time.Millisecond) + err = queue.Push(&test1) + assert.NoError(t, err) + err = queue.Push(&test2) + assert.NoError(t, err) + select { + case <-handleChan: + assert.Fail(t, "Handler processing should have stopped") + default: + } + queueTerminate() + + // Reopen queue + queue, err = NewPersistableChannelQueue(handle, PersistableChannelQueueConfiguration{ + DataDir: tmpDir, + BatchLength: 2, + QueueLength: 20, + }, &testData{}) + assert.NoError(t, err) + + go queue.Run(func(_ context.Context, shutdown func()) { + queueShutdown = shutdown + }, func(_ context.Context, terminate func()) { + queueTerminate = terminate + }) + + result3 := <-handleChan + assert.Equal(t, test1.TestString, result3.TestString) + assert.Equal(t, test1.TestInt, result3.TestInt) + + result4 := <-handleChan + assert.Equal(t, test2.TestString, result4.TestString) + assert.Equal(t, test2.TestInt, result4.TestInt) + queueShutdown() + queueTerminate() + +} diff --git a/modules/queue/queue_disk_test.go b/modules/queue/queue_disk_test.go new file mode 100644 index 000000000000..9bc689b5f060 --- /dev/null +++ b/modules/queue/queue_disk_test.go @@ -0,0 +1,99 @@ +// 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" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestLevelQueue(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 + } + } + + var queueShutdown func() + var queueTerminate func() + + queue, err := NewLevelQueue(handle, LevelQueueConfiguration{ + DataDir: "level-queue-test-data", + BatchLength: 2, + }, &testData{}) + assert.NoError(t, err) + + go queue.Run(func(_ context.Context, shutdown func()) { + queueShutdown = shutdown + }, func(_ context.Context, terminate func()) { + queueTerminate = terminate + }) + + test1 := testData{"A", 1} + test2 := testData{"B", 2} + + err = queue.Push(&test1) + assert.NoError(t, err) + go func() { + err = queue.Push(&test2) + assert.NoError(t, err) + }() + + 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) + + queueShutdown() + time.Sleep(200 * time.Millisecond) + err = queue.Push(&test1) + assert.NoError(t, err) + err = queue.Push(&test2) + assert.NoError(t, err) + select { + case <-handleChan: + assert.Fail(t, "Handler processing should have stopped") + default: + } + queueTerminate() + + // Reopen queue + queue, err = NewLevelQueue(handle, LevelQueueConfiguration{ + DataDir: "level-queue-test-data", + BatchLength: 2, + }, &testData{}) + assert.NoError(t, err) + + go queue.Run(func(_ context.Context, shutdown func()) { + queueShutdown = shutdown + }, func(_ context.Context, terminate func()) { + queueTerminate = terminate + }) + + result3 := <-handleChan + assert.Equal(t, test1.TestString, result3.TestString) + assert.Equal(t, test1.TestInt, result3.TestInt) + + result4 := <-handleChan + assert.Equal(t, test2.TestString, result4.TestString) + assert.Equal(t, test2.TestInt, result4.TestInt) + queueShutdown() + queueTerminate() + + os.RemoveAll("level-queue-test-data") +} diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go new file mode 100644 index 000000000000..b785f0073f79 --- /dev/null +++ b/modules/queue/queue_redis.go @@ -0,0 +1,190 @@ +// 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" + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" + + "github.com/go-redis/redis" +) + +// RedisQueueType is the type for redis queue +const RedisQueueType Type = "redis" + +type redisClient interface { + RPush(key string, args ...interface{}) *redis.IntCmd + LPop(key string) *redis.StringCmd + Ping() *redis.StatusCmd + Close() error +} + +// RedisQueue redis queue +type RedisQueue struct { + client redisClient + queueName string + handle HandlerFunc + batchLength int + closed chan struct{} + exemplar interface{} +} + +// RedisQueueConfiguration is the configuration for the redis queue +type RedisQueueConfiguration struct { + Addresses string + Password string + DBIndex int + BatchLength int + QueueName string +} + +// NewRedisQueue creates single redis or cluster redis queue +func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { + configInterface, err := toConfig(RedisQueueConfiguration{}, cfg) + if err != nil { + return nil, err + } + config := configInterface.(RedisQueueConfiguration) + + dbs := strings.Split(config.Addresses, ",") + var queue = RedisQueue{ + queueName: config.QueueName, + handle: handle, + batchLength: config.BatchLength, + exemplar: exemplar, + closed: make(chan struct{}), + } + if len(dbs) == 0 { + return nil, errors.New("no redis host found") + } else if len(dbs) == 1 { + queue.client = redis.NewClient(&redis.Options{ + Addr: strings.TrimSpace(dbs[0]), // use default Addr + Password: config.Password, // no password set + DB: config.DBIndex, // use default DB + }) + } else { + queue.client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: dbs, + }) + } + if err := queue.client.Ping().Err(); err != nil { + return nil, err + } + return &queue, nil +} + +// Run runs the redis queue +func (r *RedisQueue) Run(atShutdown, atTerminate func(context.Context, func())) { + atShutdown(context.Background(), r.Shutdown) + atTerminate(context.Background(), r.Terminate) + var i int + var datas = make([]Data, 0, r.batchLength) + for { + select { + case <-r.closed: + if len(datas) > 0 { + log.Trace("Handling: %d data, %v", len(datas), datas) + r.handle(datas...) + } + return + default: + } + bs, err := r.client.LPop(r.queueName).Bytes() + if err != nil && err != redis.Nil { + log.Error("LPop failed: %v", err) + time.Sleep(time.Millisecond * 100) + continue + } + + i++ + if len(datas) > r.batchLength || (len(datas) > 0 && i > 3) { + log.Trace("Handling: %d data, %v", len(datas), datas) + r.handle(datas...) + datas = make([]Data, 0, r.batchLength) + i = 0 + } + + if len(bs) == 0 { + time.Sleep(time.Millisecond * 100) + continue + } + + var data Data + if r.exemplar != nil { + t := reflect.TypeOf(r.exemplar) + n := reflect.New(t) + ne := n.Elem() + err = json.Unmarshal(bs, ne.Addr().Interface()) + data = ne.Interface().(Data) + } else { + err = json.Unmarshal(bs, &data) + } + if err != nil { + log.Error("Unmarshal: %v", err) + time.Sleep(time.Millisecond * 100) + continue + } + + log.Trace("RedisQueue: task found: %#v", data) + + datas = append(datas, data) + select { + case <-r.closed: + if len(datas) > 0 { + log.Trace("Handling: %d data, %v", len(datas), datas) + r.handle(datas...) + } + return + default: + } + time.Sleep(time.Millisecond * 100) + } +} + +// Push implements Queue +func (r *RedisQueue) Push(data Data) error { + if r.exemplar != nil { + // Assert data is of same type as r.exemplar + value := reflect.ValueOf(data) + t := value.Type() + exemplarType := reflect.ValueOf(r.exemplar).Type() + if !t.AssignableTo(exemplarType) || data == nil { + return fmt.Errorf("Unable to assign data: %v to same type as exemplar: %v in %s", data, r.exemplar, r.name) + } + } + bs, err := json.Marshal(data) + if err != nil { + return err + } + return r.client.RPush(r.queueName, bs).Err() +} + +// Shutdown processing from this queue +func (r *RedisQueue) Shutdown() { + select { + case <-r.closed: + default: + close(r.closed) + } +} + +// Terminate this queue and close the queue +func (r *RedisQueue) Terminate() { + r.Shutdown() + if err := r.client.Close(); err != nil { + log.Error("Error whilst closing internal redis client: %v", err) + } +} + +func init() { + queuesMap[RedisQueueType] = NewRedisQueue +} diff --git a/modules/queue/queue_test.go b/modules/queue/queue_test.go new file mode 100644 index 000000000000..e41643da211c --- /dev/null +++ b/modules/queue/queue_test.go @@ -0,0 +1,42 @@ +// 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 "encoding/json" + +type testData struct { + TestString string + TestInt int +} + +func TestToConfig(t *testing.T) { + cfg := testData{ + TestString: "Config", + TestInt: 10, + } + exemplar := testData{} + + cfg2I, err := toConfig(exemplar, cfg) + assert.NoError(t, err) + cfg2, ok := (cfg2I).(testData) + assert.True(t, ok) + assert.NotEqual(t, cfg2, exemplar) + assert.Equal(t, &cfg, &cfg2) + + cfgString, err := json.Marshal(cfg) + assert.NoError(t, err) + + cfg3I, err := toConfig(exemplar, cfgString) + assert.NoError(t, err) + cfg3, ok := (cfg3I).(testData) + assert.True(t, ok) + assert.Equal(t, cfg.TestString, cfg3.TestString) + assert.Equal(t, cfg.TestInt, cfg3.TestInt) + assert.NotEqual(t, cfg3, exemplar) +} diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go new file mode 100644 index 000000000000..f99675a9f913 --- /dev/null +++ b/modules/queue/queue_wrapped.go @@ -0,0 +1,183 @@ +// 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" + "fmt" + "reflect" + "sync" + "time" + + "code.gitea.io/gitea/modules/log" +) + +// WrappedQueueType is the type for a wrapped delayed starting queue +const WrappedQueueType Type = "wrapped" + +// WrappedQueueConfiguration is the configuration for a WrappedQueue +type WrappedQueueConfiguration struct { + Underlying Type + Timeout time.Duration + MaxAttempts int + Config interface{} + QueueLength int +} + +type delayedStarter struct { + lock sync.Mutex + internal Queue + underlying Type + cfg interface{} + timeout time.Duration + maxAttempts int +} + +func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), handle HandlerFunc, exemplar interface{}) { + var ctx context.Context + var cancel context.CancelFunc + if q.timeout > 0 { + ctx, cancel = context.WithTimeout(context.Background(), q.timeout) + } else { + ctx, cancel = context.WithCancel(context.Background()) + } + + defer cancel() + // Ensure we also stop at shutdown + atShutdown(ctx, func() { + cancel() + }) + + i := 1 + for q.internal == nil { + select { + case <-ctx.Done(): + q.lock.Unlock() + log.Fatal("Timedout creating queue %v with cfg %v ", q.underlying, q.cfg) + default: + queue, err := CreateQueue(q.underlying, handle, q.cfg, exemplar) + if err == nil { + q.internal = queue + q.lock.Unlock() + break + } + if err.Error() != "resource temporarily unavailable" { + log.Warn("[Attempt: %d] Failed to create queue: %v cfg: %v error: %v", i, q.underlying, q.cfg, err) + } + i++ + if q.maxAttempts > 0 && i > q.maxAttempts { + q.lock.Unlock() + log.Fatal("Unable to create queue %v with cfg %v by max attempts: error: %v", q.underlying, q.cfg, err) + } + sleepTime := 100 * time.Millisecond + if q.timeout > 0 && q.maxAttempts > 0 { + sleepTime = (q.timeout - 200*time.Millisecond) / time.Duration(q.maxAttempts) + } + time.Sleep(sleepTime) + } + } +} + +// WrappedQueue wraps a delayed starting queue +type WrappedQueue struct { + delayedStarter + handle HandlerFunc + exemplar interface{} + channel chan Data +} + +// NewWrappedQueue will attempt to create a queue of the provided type, +// but if there is a problem creating this queue it will instead create +// a WrappedQueue with delayed the startup of the queue instead and a +// channel which will be redirected to the queue +func NewWrappedQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { + configInterface, err := toConfig(WrappedQueueConfiguration{}, cfg) + if err != nil { + return nil, err + } + config := configInterface.(WrappedQueueConfiguration) + + queue, err := CreateQueue(config.Underlying, handle, config.Config, exemplar) + if err == nil { + // Just return the queue there is no need to wrap + return queue, nil + } + if IsErrInvalidConfiguration(err) { + // Retrying ain't gonna make this any better... + return nil, ErrInvalidConfiguration{cfg: cfg} + } + + return &WrappedQueue{ + handle: handle, + channel: make(chan Data, config.QueueLength), + exemplar: exemplar, + delayedStarter: delayedStarter{ + cfg: config.Config, + underlying: config.Underlying, + timeout: config.Timeout, + maxAttempts: config.MaxAttempts, + }, + }, nil +} + +// Push will push the data to the internal channel checking it against the exemplar +func (q *WrappedQueue) Push(data Data) error { + if q.exemplar != nil { + // Assert data is of same type as r.exemplar + value := reflect.ValueOf(data) + t := value.Type() + exemplarType := reflect.ValueOf(q.exemplar).Type() + if !t.AssignableTo(exemplarType) || data == nil { + return fmt.Errorf("Unable to assign data: %v to same type as exemplar: %v in %s", data, q.exemplar, q.name) + } + } + q.channel <- data + return nil +} + +// Run starts to run the queue and attempts to create the internal queue +func (q *WrappedQueue) Run(atShutdown, atTerminate func(context.Context, func())) { + q.lock.Lock() + if q.internal == nil { + q.setInternal(atShutdown, q.handle, q.exemplar) + go func() { + for data := range q.channel { + _ = q.internal.Push(data) + } + }() + } else { + q.lock.Unlock() + } + + q.internal.Run(atShutdown, atTerminate) +} + +// Shutdown this queue and stop processing +func (q *WrappedQueue) Shutdown() { + q.lock.Lock() + defer q.lock.Unlock() + if q.internal == nil { + return + } + if shutdownable, ok := q.internal.(Shutdownable); ok { + shutdownable.Shutdown() + } +} + +// Terminate this queue and close the queue +func (q *WrappedQueue) Terminate() { + q.lock.Lock() + defer q.lock.Unlock() + if q.internal == nil { + return + } + if shutdownable, ok := q.internal.(Shutdownable); ok { + shutdownable.Terminate() + } +} + +func init() { + queuesMap[WrappedQueueType] = NewWrappedQueue +} diff --git a/modules/setting/queue.go b/modules/setting/queue.go new file mode 100644 index 000000000000..4c80c79079e1 --- /dev/null +++ b/modules/setting/queue.go @@ -0,0 +1,143 @@ +// 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 setting + +import ( + "encoding/json" + "path" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" +) + +type queueSettings struct { + DataDir string + Length int + BatchLength int + ConnectionString string + Type string + Addresses string + Password string + DBIndex int + WrapIfNecessary bool + MaxAttempts int + Timeout time.Duration + Workers int +} + +// Queue settings +var Queue = queueSettings{} + +// CreateQueue for name with provided handler and exemplar +func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) queue.Queue { + q := getQueueSettings(name) + opts := make(map[string]interface{}) + opts["QueueLength"] = q.Length + opts["BatchLength"] = q.BatchLength + opts["DataDir"] = q.DataDir + opts["Addresses"] = q.Addresses + opts["Password"] = q.Password + opts["DBIndex"] = q.DBIndex + opts["QueueName"] = name + + cfg, err := json.Marshal(opts) + if err != nil { + log.Error("Unable to marshall generic options: %v Error: %v", opts, err) + log.Error("Unable to create queue for %s", name, err) + return nil + } + + returnable, err := queue.CreateQueue(queue.Type(q.Type), handle, cfg, exemplar) + if q.WrapIfNecessary && err != nil { + log.Warn("Unable to create queue for %s: %v", name, err) + log.Warn("Attempting to create wrapped queue") + returnable, err = queue.CreateQueue(queue.WrappedQueueType, handle, queue.WrappedQueueConfiguration{ + Underlying: queue.Type(q.Type), + Timeout: q.Timeout, + MaxAttempts: q.MaxAttempts, + Config: cfg, + QueueLength: q.Length, + }, exemplar) + } + if err != nil { + log.Error("Unable to create queue for %s: %v", name, err) + return nil + } + return returnable +} + +func getQueueSettings(name string) queueSettings { + q := queueSettings{} + sec := Cfg.Section("queue." + name) + // DataDir is not directly inheritable + q.DataDir = path.Join(Queue.DataDir, name) + for _, key := range sec.Keys() { + switch key.Name() { + case "DATADIR": + q.DataDir = key.MustString(q.DataDir) + } + } + if !path.IsAbs(q.DataDir) { + q.DataDir = path.Join(AppDataPath, q.DataDir) + } + sec.Key("DATADIR").SetValue(q.DataDir) + // The rest are... + q.Length = sec.Key("LENGTH").MustInt(Queue.Length) + q.BatchLength = sec.Key("BATCH_LENGTH").MustInt(Queue.BatchLength) + q.ConnectionString = sec.Key("CONN_STR").MustString(Queue.ConnectionString) + validTypes := queue.RegisteredTypesAsString() + q.Type = sec.Key("TYPE").In(Queue.Type, validTypes) + q.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(Queue.WrapIfNecessary) + q.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(Queue.MaxAttempts) + q.Timeout = sec.Key("TIMEOUT").MustDuration(Queue.Timeout) + q.Workers = sec.Key("WORKER").MustInt(Queue.Workers) + + q.Addresses, q.Password, q.DBIndex, _ = ParseQueueConnStr(q.ConnectionString) + return q +} + +func newQueueService() { + sec := Cfg.Section("queue") + Queue.DataDir = sec.Key("DATADIR").MustString("queues/") + if !path.IsAbs(Queue.DataDir) { + Queue.DataDir = path.Join(AppDataPath, Queue.DataDir) + } + Queue.Length = sec.Key("LENGTH").MustInt(20) + Queue.BatchLength = sec.Key("BATCH_LENGTH").MustInt(20) + Queue.ConnectionString = sec.Key("CONN_STR").MustString(path.Join(AppDataPath, "")) + validTypes := queue.RegisteredTypesAsString() + Queue.Type = sec.Key("TYPE").In(string(queue.PersistableChannelQueueType), validTypes) + Queue.Addresses, Queue.Password, Queue.DBIndex, _ = ParseQueueConnStr(Queue.ConnectionString) + Queue.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(true) + Queue.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(10) + Queue.Timeout = sec.Key("TIMEOUT").MustDuration(GracefulHammerTime + 30*time.Second) + Queue.Workers = sec.Key("WORKER").MustInt(1) +} + +// ParseQueueConnStr parses a queue connection string +func ParseQueueConnStr(connStr string) (addrs, password string, dbIdx int, err error) { + fields := strings.Fields(connStr) + for _, f := range fields { + items := strings.SplitN(f, "=", 2) + if len(items) < 2 { + continue + } + switch strings.ToLower(items[0]) { + case "addrs": + addrs = items[1] + case "password": + password = items[1] + case "db": + dbIdx, err = strconv.Atoi(items[1]) + if err != nil { + return + } + } + } + return +} From 9fb051654a026d8745fb4049be916438d5d79f52 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 20 Nov 2019 21:31:39 +0000 Subject: [PATCH 02/35] Queue & Setting: Add worker pool implementation --- modules/queue/queue_batch.go | 40 +++++++++++++----------- modules/queue/queue_batch_test.go | 2 +- modules/queue/queue_channel.go | 15 ++++++--- modules/queue/queue_channel_test.go | 2 +- modules/queue/queue_disk.go | 17 ++++++++++ modules/queue/queue_disk_channel.go | 18 +++++++++++ modules/queue/queue_disk_channel_test.go | 2 ++ modules/queue/queue_disk_test.go | 2 ++ modules/queue/queue_redis.go | 16 ++++++++++ modules/setting/queue.go | 3 ++ 10 files changed, 92 insertions(+), 25 deletions(-) diff --git a/modules/queue/queue_batch.go b/modules/queue/queue_batch.go index 07166441e6df..2731ac5e23c9 100644 --- a/modules/queue/queue_batch.go +++ b/modules/queue/queue_batch.go @@ -18,6 +18,7 @@ const BatchedChannelQueueType Type = "batched-channel" type BatchedChannelQueueConfiguration struct { QueueLength int BatchLength int + Workers int } // BatchedChannelQueue implements @@ -38,6 +39,7 @@ func NewBatchedChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queu queue: make(chan Data, config.QueueLength), handle: handle, exemplar: exemplar, + workers: config.Workers, }, config.BatchLength, }, nil @@ -51,26 +53,28 @@ func (c *BatchedChannelQueue) Run(atShutdown, atTerminate func(context.Context, atTerminate(context.Background(), func() { log.Warn("BatchedChannelQueue is not terminatable!") }) - 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) + 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() { diff --git a/modules/queue/queue_batch_test.go b/modules/queue/queue_batch_test.go index 08d3641da123..13a85a0aadaf 100644 --- a/modules/queue/queue_batch_test.go +++ b/modules/queue/queue_batch_test.go @@ -22,7 +22,7 @@ func TestBatchedChannelQueue(t *testing.T) { nilFn := func(_ context.Context, _ func()) {} - queue, err := NewBatchedChannelQueue(handle, BatchedChannelQueueConfiguration{QueueLength: 20, BatchLength: 2}, &testData{}) + queue, err := NewBatchedChannelQueue(handle, BatchedChannelQueueConfiguration{QueueLength: 20, BatchLength: 2, Workers: 1}, &testData{}) assert.NoError(t, err) go queue.Run(nilFn, nilFn) diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go index e0cba2db01dd..9d0ab11d21c0 100644 --- a/modules/queue/queue_channel.go +++ b/modules/queue/queue_channel.go @@ -18,6 +18,7 @@ const ChannelQueueType Type = "channel" // ChannelQueueConfiguration is the configuration for a ChannelQueue type ChannelQueueConfiguration struct { QueueLength int + Workers int } // ChannelQueue implements @@ -25,6 +26,7 @@ type ChannelQueue struct { queue chan Data handle HandlerFunc exemplar interface{} + workers int } // NewChannelQueue create a memory channel queue @@ -38,6 +40,7 @@ func NewChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro queue: make(chan Data, config.QueueLength), handle: handle, exemplar: exemplar, + workers: config.Workers, }, nil } @@ -49,11 +52,13 @@ func (c *ChannelQueue) Run(atShutdown, atTerminate func(context.Context, func()) atTerminate(context.Background(), func() { log.Warn("ChannelQueue is not terminatable!") }) - go func() { - for data := range c.queue { - c.handle(data) - } - }() + for i := 0; i < c.workers; i++ { + go func() { + for data := range c.queue { + c.handle(data) + } + }() + } } // Push will push the indexer data to queue diff --git a/modules/queue/queue_channel_test.go b/modules/queue/queue_channel_test.go index 77f4a8fe8f59..9e72bed85d7a 100644 --- a/modules/queue/queue_channel_test.go +++ b/modules/queue/queue_channel_test.go @@ -22,7 +22,7 @@ func TestChannelQueue(t *testing.T) { nilFn := func(_ context.Context, _ func()) {} - queue, err := NewChannelQueue(handle, ChannelQueueConfiguration{QueueLength: 20}, &testData{}) + queue, err := NewChannelQueue(handle, ChannelQueueConfiguration{QueueLength: 20, Workers: 1}, &testData{}) assert.NoError(t, err) go queue.Run(nilFn, nilFn) diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index dafff5c21c8e..799bc98046fb 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "reflect" + "sync" "time" "code.gitea.io/gitea/modules/log" @@ -23,6 +24,7 @@ const LevelQueueType Type = "level" type LevelQueueConfiguration struct { DataDir string BatchLength int + Workers int } // LevelQueue implements a disk library queue @@ -32,6 +34,7 @@ type LevelQueue struct { batchLength int closed chan struct{} exemplar interface{} + workers int } // NewLevelQueue creates a ledis local queue @@ -53,6 +56,7 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) batchLength: config.BatchLength, exemplar: exemplar, closed: make(chan struct{}), + workers: config.Workers, }, nil } @@ -60,6 +64,19 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) func (l *LevelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { atShutdown(context.Background(), l.Shutdown) atTerminate(context.Background(), l.Terminate) + + wg := sync.WaitGroup{} + for i := 0; i < l.workers; i++ { + wg.Add(1) + go func() { + l.worker() + wg.Done() + }() + } + wg.Wait() +} + +func (l *LevelQueue) worker() { var i int var datas = make([]Data, 0, l.batchLength) for { diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index b13f1b9603de..428e104fb5ac 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -6,6 +6,7 @@ package queue import ( "context" + "sync" "time" ) @@ -19,6 +20,7 @@ type PersistableChannelQueueConfiguration struct { QueueLength int Timeout time.Duration MaxAttempts int + Workers int } // PersistableChannelQueue wraps a channel queue and level queue together @@ -40,14 +42,17 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( batchChannelQueue, err := NewBatchedChannelQueue(handle, BatchedChannelQueueConfiguration{ QueueLength: config.QueueLength, BatchLength: config.BatchLength, + Workers: config.Workers, }, exemplar) if err != nil { return nil, err } + // the level backend only needs one worker to catch up with the previously dropped work levelCfg := LevelQueueConfiguration{ DataDir: config.DataDir, BatchLength: config.BatchLength, + Workers: 1, } levelQueue, err := NewLevelQueue(handle, levelCfg, exemplar) @@ -100,6 +105,19 @@ func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Conte // Just run the level queue - we shut it down later go p.internal.Run(func(_ context.Context, _ func()) {}, func(_ context.Context, _ func()) {}) + + wg := sync.WaitGroup{} + for i := 0; i < p.workers; i++ { + wg.Add(1) + go func() { + p.worker() + wg.Done() + }() + } + wg.Wait() +} + +func (p *PersistableChannelQueue) worker() { delay := time.Millisecond * 300 var datas = make([]Data, 0, p.batchLength) loop: diff --git a/modules/queue/queue_disk_channel_test.go b/modules/queue/queue_disk_channel_test.go index 66c90f3bc3f3..5f6f614bd8c8 100644 --- a/modules/queue/queue_disk_channel_test.go +++ b/modules/queue/queue_disk_channel_test.go @@ -35,6 +35,7 @@ func TestPersistableChannelQueue(t *testing.T) { DataDir: tmpDir, BatchLength: 2, QueueLength: 20, + Workers: 1, }, &testData{}) assert.NoError(t, err) @@ -83,6 +84,7 @@ func TestPersistableChannelQueue(t *testing.T) { DataDir: tmpDir, BatchLength: 2, QueueLength: 20, + Workers: 1, }, &testData{}) assert.NoError(t, err) diff --git a/modules/queue/queue_disk_test.go b/modules/queue/queue_disk_test.go index 9bc689b5f060..7033fc6a34a4 100644 --- a/modules/queue/queue_disk_test.go +++ b/modules/queue/queue_disk_test.go @@ -29,6 +29,7 @@ func TestLevelQueue(t *testing.T) { queue, err := NewLevelQueue(handle, LevelQueueConfiguration{ DataDir: "level-queue-test-data", BatchLength: 2, + Workers: 1, }, &testData{}) assert.NoError(t, err) @@ -76,6 +77,7 @@ func TestLevelQueue(t *testing.T) { queue, err = NewLevelQueue(handle, LevelQueueConfiguration{ DataDir: "level-queue-test-data", BatchLength: 2, + Workers: 1, }, &testData{}) assert.NoError(t, err) diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index b785f0073f79..80ce67233c3c 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -11,6 +11,7 @@ import ( "fmt" "reflect" "strings" + "sync" "time" "code.gitea.io/gitea/modules/log" @@ -36,6 +37,7 @@ type RedisQueue struct { batchLength int closed chan struct{} exemplar interface{} + workers int } // RedisQueueConfiguration is the configuration for the redis queue @@ -45,6 +47,7 @@ type RedisQueueConfiguration struct { DBIndex int BatchLength int QueueName string + Workers int } // NewRedisQueue creates single redis or cluster redis queue @@ -62,6 +65,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) batchLength: config.BatchLength, exemplar: exemplar, closed: make(chan struct{}), + workers: config.Workers, } if len(dbs) == 0 { return nil, errors.New("no redis host found") @@ -86,6 +90,18 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) func (r *RedisQueue) Run(atShutdown, atTerminate func(context.Context, func())) { atShutdown(context.Background(), r.Shutdown) atTerminate(context.Background(), r.Terminate) + wg := sync.WaitGroup{} + for i := 0; i < r.workers; i++ { + wg.Add(1) + go func() { + r.worker() + wg.Done() + }() + } + wg.Wait() +} + +func (r *RedisQueue) worker() { var i int var datas = make([]Data, 0, r.batchLength) for { diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 4c80c79079e1..4f7da32ce916 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -44,6 +44,7 @@ func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) qu opts["Password"] = q.Password opts["DBIndex"] = q.DBIndex opts["QueueName"] = name + opts["Workers"] = q.Workers cfg, err := json.Marshal(opts) if err != nil { @@ -117,6 +118,8 @@ func newQueueService() { Queue.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(10) Queue.Timeout = sec.Key("TIMEOUT").MustDuration(GracefulHammerTime + 30*time.Second) Queue.Workers = sec.Key("WORKER").MustInt(1) + + Cfg.Section("queue.notification").Key("WORKER").MustInt(5) } // ParseQueueConnStr parses a queue connection string From 0edb70a099262efe9bf4521dc4d5ae4ce71622c4 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 7 Dec 2019 16:41:53 +0000 Subject: [PATCH 03/35] Queue: Add worker settings --- modules/setting/queue.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 4f7da32ce916..1a33e232c334 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -119,7 +119,16 @@ func newQueueService() { Queue.Timeout = sec.Key("TIMEOUT").MustDuration(GracefulHammerTime + 30*time.Second) Queue.Workers = sec.Key("WORKER").MustInt(1) - Cfg.Section("queue.notification").Key("WORKER").MustInt(5) + hasWorkers := false + for _, key := range Cfg.Section("queue.notification").Keys() { + if key.Name() == "WORKERS" { + hasWorkers = true + break + } + } + if !hasWorkers { + Cfg.Section("queue.notification").Key("WORKERS").SetValue("5") + } } // ParseQueueConnStr parses a queue connection string From e6ebb47299a596416318a61a767dc79ee6209446 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 7 Dec 2019 16:44:37 +0000 Subject: [PATCH 04/35] Queue: Make resizing worker pools --- .../doc/advanced/config-cheat-sheet.en-us.md | 2 +- modules/queue/queue_batch.go | 82 ------ modules/queue/queue_batch_test.go | 46 ---- modules/queue/queue_channel.go | 40 ++- modules/queue/queue_channel_test.go | 53 +++- modules/queue/queue_disk.go | 148 ++++++----- modules/queue/queue_disk_channel.go | 98 +++---- modules/queue/queue_disk_test.go | 20 +- modules/queue/queue_redis.go | 156 ++++++------ modules/queue/workerpool.go | 239 ++++++++++++++++++ modules/setting/queue.go | 16 +- 11 files changed, 540 insertions(+), 360 deletions(-) delete mode 100644 modules/queue/queue_batch.go delete mode 100644 modules/queue/queue_batch_test.go create mode 100644 modules/queue/workerpool.go diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 2db543f5e630..6ffb43fcd89e 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -236,7 +236,7 @@ relation to port exhaustion. ## Queue (`queue`) -- `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel`, `batched-channel`, `channel`, `level`, `redis`, `dummy` +- `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel`, `channel`, `level`, `redis`, `dummy` - `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. - `LENGTH`: **20**: Maximal queue size before channel queues block - `BATCH_LENGTH`: **20**: Batch data before passing to the handler diff --git a/modules/queue/queue_batch.go b/modules/queue/queue_batch.go deleted file mode 100644 index 2731ac5e23c9..000000000000 --- a/modules/queue/queue_batch.go +++ /dev/null @@ -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 -} diff --git a/modules/queue/queue_batch_test.go b/modules/queue/queue_batch_test.go deleted file mode 100644 index 13a85a0aadaf..000000000000 --- a/modules/queue/queue_batch_test.go +++ /dev/null @@ -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) -} diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go index 9d0ab11d21c0..ebcf22ef7932 100644 --- a/modules/queue/queue_channel.go +++ b/modules/queue/queue_channel.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "reflect" + "time" "code.gitea.io/gitea/modules/log" ) @@ -17,14 +18,17 @@ const ChannelQueueType Type = "channel" // ChannelQueueConfiguration is the configuration for a ChannelQueue type ChannelQueueConfiguration struct { - QueueLength int - Workers int + QueueLength int + BatchLength int + Workers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int } // ChannelQueue implements type ChannelQueue struct { - queue chan Data - handle HandlerFunc + pool *WorkerPool exemplar interface{} workers int } @@ -36,9 +40,23 @@ func NewChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro return nil, err } config := configInterface.(ChannelQueueConfiguration) + if config.BatchLength == 0 { + config.BatchLength = 1 + } + dataChan := make(chan Data, config.QueueLength) + + ctx, cancel := context.WithCancel(context.Background()) return &ChannelQueue{ - queue: make(chan Data, config.QueueLength), - handle: handle, + pool: &WorkerPool{ + baseCtx: ctx, + cancel: cancel, + batchLength: config.BatchLength, + handle: handle, + dataChan: dataChan, + blockTimeout: config.BlockTimeout, + boostTimeout: config.BoostTimeout, + boostWorkers: config.BoostWorkers, + }, exemplar: exemplar, workers: config.Workers, }, nil @@ -52,13 +70,7 @@ func (c *ChannelQueue) Run(atShutdown, atTerminate func(context.Context, func()) atTerminate(context.Background(), func() { log.Warn("ChannelQueue is not terminatable!") }) - for i := 0; i < c.workers; i++ { - go func() { - for data := range c.queue { - c.handle(data) - } - }() - } + c.pool.addWorkers(c.pool.baseCtx, c.workers) } // Push will push the indexer data to queue @@ -71,7 +83,7 @@ func (c *ChannelQueue) Push(data Data) error { return fmt.Errorf("Unable to assign data: %v to same type as exemplar: %v in queue: %s", data, c.exemplar, c.name) } } - c.queue <- data + c.pool.Push(data) return nil } diff --git a/modules/queue/queue_channel_test.go b/modules/queue/queue_channel_test.go index 9e72bed85d7a..c04407aa243f 100644 --- a/modules/queue/queue_channel_test.go +++ b/modules/queue/queue_channel_test.go @@ -7,6 +7,7 @@ package queue import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -22,7 +23,14 @@ func TestChannelQueue(t *testing.T) { nilFn := func(_ context.Context, _ func()) {} - queue, err := NewChannelQueue(handle, ChannelQueueConfiguration{QueueLength: 20, Workers: 1}, &testData{}) + queue, err := NewChannelQueue(handle, + ChannelQueueConfiguration{ + QueueLength: 20, + Workers: 1, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, + }, &testData{}) assert.NoError(t, err) go queue.Run(nilFn, nilFn) @@ -36,3 +44,46 @@ func TestChannelQueue(t *testing.T) { err = queue.Push(test1) assert.Error(t, err) } + +func TestChannelQueue_Batch(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 := NewChannelQueue(handle, + ChannelQueueConfiguration{ + QueueLength: 20, + BatchLength: 2, + Workers: 1, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, + }, &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) +} diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index 799bc98046fb..50e49f3a29ef 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "reflect" - "sync" "time" "code.gitea.io/gitea/modules/log" @@ -22,19 +21,23 @@ const LevelQueueType Type = "level" // LevelQueueConfiguration is the configuration for a LevelQueue type LevelQueueConfiguration struct { - DataDir string - BatchLength int - Workers int + DataDir string + QueueLength int + BatchLength int + Workers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int } // LevelQueue implements a disk library queue type LevelQueue struct { - handle HandlerFunc - queue *levelqueue.Queue - batchLength int - closed chan struct{} - exemplar interface{} - workers int + pool *WorkerPool + queue *levelqueue.Queue + closed chan struct{} + terminated chan struct{} + exemplar interface{} + workers int } // NewLevelQueue creates a ledis local queue @@ -50,13 +53,25 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) return nil, err } + dataChan := make(chan Data, config.QueueLength) + ctx, cancel := context.WithCancel(context.Background()) + return &LevelQueue{ - handle: handle, - queue: queue, - batchLength: config.BatchLength, - exemplar: exemplar, - closed: make(chan struct{}), - workers: config.Workers, + pool: &WorkerPool{ + baseCtx: ctx, + cancel: cancel, + batchLength: config.BatchLength, + handle: handle, + dataChan: dataChan, + blockTimeout: config.BlockTimeout, + boostTimeout: config.BoostTimeout, + boostWorkers: config.BoostWorkers, + }, + queue: queue, + exemplar: exemplar, + closed: make(chan struct{}), + terminated: make(chan struct{}), + workers: config.Workers, }, nil } @@ -65,72 +80,66 @@ func (l *LevelQueue) Run(atShutdown, atTerminate func(context.Context, func())) atShutdown(context.Background(), l.Shutdown) atTerminate(context.Background(), l.Terminate) - wg := sync.WaitGroup{} - for i := 0; i < l.workers; i++ { - wg.Add(1) - go func() { - l.worker() - wg.Done() - }() - } - wg.Wait() + go l.pool.addWorkers(l.pool.baseCtx, l.workers) + + go l.readToChan() + + log.Trace("Waiting til closed") + <-l.closed + + log.Trace("Waiting til done") + l.pool.Wait() + // FIXME: graceful: Needs HammerContext + log.Trace("Waiting til cleaned") + + l.pool.CleanUp(context.TODO()) + log.Trace("cleaned") + } -func (l *LevelQueue) worker() { - var i int - var datas = make([]Data, 0, l.batchLength) +func (l *LevelQueue) readToChan() { for { select { case <-l.closed: - if len(datas) > 0 { - log.Trace("Handling: %d data, %v", len(datas), datas) - l.handle(datas...) - } + // tell the pool to shutdown. + l.pool.cancel() return default: - } - i++ - if len(datas) > l.batchLength || (len(datas) > 0 && i > 3) { - log.Trace("Handling: %d data, %v", len(datas), datas) - l.handle(datas...) - datas = make([]Data, 0, l.batchLength) - i = 0 - continue - } + bs, err := l.queue.RPop() + if err != nil { + if err != levelqueue.ErrNotFound { + log.Error("RPop: %v", err) + } + time.Sleep(time.Millisecond * 100) + continue + } - bs, err := l.queue.RPop() - if err != nil { - if err != levelqueue.ErrNotFound { - log.Error("RPop: %v", err) + if len(bs) == 0 { + time.Sleep(time.Millisecond * 100) + continue } - time.Sleep(time.Millisecond * 100) - continue - } - if len(bs) == 0 { - time.Sleep(time.Millisecond * 100) - continue - } + var data Data + if l.exemplar != nil { + t := reflect.TypeOf(l.exemplar) + n := reflect.New(t) + ne := n.Elem() + err = json.Unmarshal(bs, ne.Addr().Interface()) + data = ne.Interface().(Data) + } else { + err = json.Unmarshal(bs, &data) + } + if err != nil { + log.Error("LevelQueue failed to unmarshal: %v", err) + time.Sleep(time.Millisecond * 10) + continue + } - var data Data - if l.exemplar != nil { - t := reflect.TypeOf(l.exemplar) - n := reflect.New(t) - ne := n.Elem() - err = json.Unmarshal(bs, ne.Addr().Interface()) - data = ne.Interface().(Data) - } else { - err = json.Unmarshal(bs, &data) - } - if err != nil { - log.Error("Unmarshal: %v", err) + log.Trace("LevelQueue: task found: %#v", data) + l.pool.Push(data) time.Sleep(time.Millisecond * 10) - continue - } - log.Trace("LevelQueue: task found: %#v", data) - - datas = append(datas, data) + } } } @@ -163,6 +172,7 @@ func (l *LevelQueue) Shutdown() { // Terminate this queue and close the queue func (l *LevelQueue) Terminate() { + log.Trace("Terminating") l.Shutdown() if err := l.queue.Close(); err != nil && err.Error() != "leveldb: closed" { log.Error("Error whilst closing internal queue: %v", err) diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index 428e104fb5ac..f3278271527c 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -6,8 +6,9 @@ package queue import ( "context" - "sync" "time" + + "code.gitea.io/gitea/modules/log" ) // PersistableChannelQueueType is the type for persistable queue @@ -15,17 +16,20 @@ const PersistableChannelQueueType Type = "persistable-channel" // PersistableChannelQueueConfiguration is the configuration for a PersistableChannelQueue type PersistableChannelQueueConfiguration struct { - DataDir string - BatchLength int - QueueLength int - Timeout time.Duration - MaxAttempts int - Workers int + DataDir string + BatchLength int + QueueLength int + Timeout time.Duration + MaxAttempts int + Workers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int } // PersistableChannelQueue wraps a channel queue and level queue together type PersistableChannelQueue struct { - *BatchedChannelQueue + *ChannelQueue delayedStarter closed chan struct{} } @@ -39,26 +43,33 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( } config := configInterface.(PersistableChannelQueueConfiguration) - batchChannelQueue, err := NewBatchedChannelQueue(handle, BatchedChannelQueueConfiguration{ - QueueLength: config.QueueLength, - BatchLength: config.BatchLength, - Workers: config.Workers, + channelQueue, err := NewChannelQueue(handle, ChannelQueueConfiguration{ + QueueLength: config.QueueLength, + BatchLength: config.BatchLength, + Workers: config.Workers, + BlockTimeout: config.BlockTimeout, + BoostTimeout: config.BoostTimeout, + BoostWorkers: config.BoostWorkers, }, exemplar) if err != nil { return nil, err } - // the level backend only needs one worker to catch up with the previously dropped work + // the level backend only needs temporary workrers to catch up with the previously dropped work levelCfg := LevelQueueConfiguration{ - DataDir: config.DataDir, - BatchLength: config.BatchLength, - Workers: 1, + DataDir: config.DataDir, + QueueLength: config.QueueLength, + BatchLength: config.BatchLength, + Workers: 1, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, } levelQueue, err := NewLevelQueue(handle, levelCfg, exemplar) if err == nil { return &PersistableChannelQueue{ - BatchedChannelQueue: batchChannelQueue.(*BatchedChannelQueue), + ChannelQueue: channelQueue.(*ChannelQueue), delayedStarter: delayedStarter{ internal: levelQueue.(*LevelQueue), }, @@ -71,7 +82,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( } return &PersistableChannelQueue{ - BatchedChannelQueue: batchChannelQueue.(*BatchedChannelQueue), + ChannelQueue: channelQueue.(*ChannelQueue), delayedStarter: delayedStarter{ cfg: levelCfg, underlying: LevelQueueType, @@ -88,7 +99,7 @@ func (p *PersistableChannelQueue) Push(data Data) error { case <-p.closed: return p.internal.Push(data) default: - return p.BatchedChannelQueue.Push(data) + return p.ChannelQueue.Push(data) } } @@ -96,7 +107,7 @@ func (p *PersistableChannelQueue) Push(data Data) error { func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { p.lock.Lock() if p.internal == nil { - p.setInternal(atShutdown, p.handle, p.exemplar) + p.setInternal(atShutdown, p.ChannelQueue.pool.handle, p.exemplar) } else { p.lock.Unlock() } @@ -106,44 +117,16 @@ func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Conte // Just run the level queue - we shut it down later go p.internal.Run(func(_ context.Context, _ func()) {}, func(_ context.Context, _ func()) {}) - wg := sync.WaitGroup{} - for i := 0; i < p.workers; i++ { - wg.Add(1) - go func() { - p.worker() - wg.Done() - }() - } - wg.Wait() -} + go p.ChannelQueue.pool.addWorkers(p.ChannelQueue.pool.baseCtx, p.workers) -func (p *PersistableChannelQueue) worker() { - delay := time.Millisecond * 300 - var datas = make([]Data, 0, p.batchLength) -loop: - for { - select { - case data := <-p.queue: - datas = append(datas, data) - if len(datas) >= p.batchLength { - p.handle(datas...) - datas = make([]Data, 0, p.batchLength) - } - case <-time.After(delay): - delay = time.Millisecond * 100 - if len(datas) > 0 { - p.handle(datas...) - datas = make([]Data, 0, p.batchLength) - } - case <-p.closed: - if len(datas) > 0 { - p.handle(datas...) - } - break loop - } - } + <-p.closed + p.ChannelQueue.pool.cancel() + p.internal.(*LevelQueue).pool.cancel() + p.ChannelQueue.pool.Wait() + p.internal.(*LevelQueue).pool.Wait() + // Redirect all remaining data in the chan to the internal channel go func() { - for data := range p.queue { + for data := range p.ChannelQueue.pool.dataChan { _ = p.internal.Push(data) } }() @@ -154,17 +137,18 @@ func (p *PersistableChannelQueue) Shutdown() { select { case <-p.closed: default: - close(p.closed) p.lock.Lock() defer p.lock.Unlock() if p.internal != nil { p.internal.(*LevelQueue).Shutdown() } + close(p.closed) } } // Terminate this queue and close the queue func (p *PersistableChannelQueue) Terminate() { + log.Trace("Terminating") p.Shutdown() p.lock.Lock() defer p.lock.Unlock() diff --git a/modules/queue/queue_disk_test.go b/modules/queue/queue_disk_test.go index 7033fc6a34a4..b9c6f278ef57 100644 --- a/modules/queue/queue_disk_test.go +++ b/modules/queue/queue_disk_test.go @@ -27,9 +27,13 @@ func TestLevelQueue(t *testing.T) { var queueTerminate func() queue, err := NewLevelQueue(handle, LevelQueueConfiguration{ - DataDir: "level-queue-test-data", - BatchLength: 2, - Workers: 1, + DataDir: "level-queue-test-data", + BatchLength: 2, + Workers: 1, + QueueLength: 20, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, }, &testData{}) assert.NoError(t, err) @@ -75,9 +79,13 @@ func TestLevelQueue(t *testing.T) { // Reopen queue queue, err = NewLevelQueue(handle, LevelQueueConfiguration{ - DataDir: "level-queue-test-data", - BatchLength: 2, - Workers: 1, + DataDir: "level-queue-test-data", + BatchLength: 2, + Workers: 1, + QueueLength: 20, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, }, &testData{}) assert.NoError(t, err) diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 80ce67233c3c..acc6feeb95ed 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -11,7 +11,6 @@ import ( "fmt" "reflect" "strings" - "sync" "time" "code.gitea.io/gitea/modules/log" @@ -31,23 +30,26 @@ type redisClient interface { // RedisQueue redis queue type RedisQueue struct { - client redisClient - queueName string - handle HandlerFunc - batchLength int - closed chan struct{} - exemplar interface{} - workers int + pool *WorkerPool + client redisClient + queueName string + closed chan struct{} + exemplar interface{} + workers int } // RedisQueueConfiguration is the configuration for the redis queue type RedisQueueConfiguration struct { - Addresses string - Password string - DBIndex int - BatchLength int - QueueName string - Workers int + Addresses string + Password string + DBIndex int + BatchLength int + QueueLength int + QueueName string + Workers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int } // NewRedisQueue creates single redis or cluster redis queue @@ -59,13 +61,25 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) config := configInterface.(RedisQueueConfiguration) dbs := strings.Split(config.Addresses, ",") + + dataChan := make(chan Data, config.QueueLength) + ctx, cancel := context.WithCancel(context.Background()) + var queue = RedisQueue{ - queueName: config.QueueName, - handle: handle, - batchLength: config.BatchLength, - exemplar: exemplar, - closed: make(chan struct{}), - workers: config.Workers, + pool: &WorkerPool{ + baseCtx: ctx, + cancel: cancel, + batchLength: config.BatchLength, + handle: handle, + dataChan: dataChan, + blockTimeout: config.BlockTimeout, + boostTimeout: config.BoostTimeout, + boostWorkers: config.BoostWorkers, + }, + queueName: config.QueueName, + exemplar: exemplar, + closed: make(chan struct{}), + workers: config.Workers, } if len(dbs) == 0 { return nil, errors.New("no redis host found") @@ -90,79 +104,57 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) func (r *RedisQueue) Run(atShutdown, atTerminate func(context.Context, func())) { atShutdown(context.Background(), r.Shutdown) atTerminate(context.Background(), r.Terminate) - wg := sync.WaitGroup{} - for i := 0; i < r.workers; i++ { - wg.Add(1) - go func() { - r.worker() - wg.Done() - }() - } - wg.Wait() + + go r.pool.addWorkers(r.pool.baseCtx, r.workers) + + go r.readToChan() + + <-r.closed + r.pool.Wait() + // FIXME: graceful: Needs HammerContext + r.pool.CleanUp(context.TODO()) } -func (r *RedisQueue) worker() { - var i int - var datas = make([]Data, 0, r.batchLength) +func (r *RedisQueue) readToChan() { for { select { case <-r.closed: - if len(datas) > 0 { - log.Trace("Handling: %d data, %v", len(datas), datas) - r.handle(datas...) - } + // tell the pool to shutdown + r.pool.cancel() return default: - } - bs, err := r.client.LPop(r.queueName).Bytes() - if err != nil && err != redis.Nil { - log.Error("LPop failed: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } - - i++ - if len(datas) > r.batchLength || (len(datas) > 0 && i > 3) { - log.Trace("Handling: %d data, %v", len(datas), datas) - r.handle(datas...) - datas = make([]Data, 0, r.batchLength) - i = 0 - } - - if len(bs) == 0 { - time.Sleep(time.Millisecond * 100) - continue - } - - var data Data - if r.exemplar != nil { - t := reflect.TypeOf(r.exemplar) - n := reflect.New(t) - ne := n.Elem() - err = json.Unmarshal(bs, ne.Addr().Interface()) - data = ne.Interface().(Data) - } else { - err = json.Unmarshal(bs, &data) - } - if err != nil { - log.Error("Unmarshal: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } + bs, err := r.client.LPop(r.queueName).Bytes() + if err != nil && err != redis.Nil { + log.Error("LPop failed: %v", err) + time.Sleep(time.Millisecond * 100) + continue + } - log.Trace("RedisQueue: task found: %#v", data) + if len(bs) == 0 { + time.Sleep(time.Millisecond * 100) + continue + } - datas = append(datas, data) - select { - case <-r.closed: - if len(datas) > 0 { - log.Trace("Handling: %d data, %v", len(datas), datas) - r.handle(datas...) + var data Data + if r.exemplar != nil { + t := reflect.TypeOf(r.exemplar) + n := reflect.New(t) + ne := n.Elem() + err = json.Unmarshal(bs, ne.Addr().Interface()) + data = ne.Interface().(Data) + } else { + err = json.Unmarshal(bs, &data) } - return - default: + if err != nil { + log.Error("Unmarshal: %v", err) + time.Sleep(time.Millisecond * 100) + continue + } + + log.Trace("RedisQueue: task found: %#v", data) + r.pool.Push(data) + time.Sleep(time.Millisecond * 10) } - time.Sleep(time.Millisecond * 100) } } diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go new file mode 100644 index 000000000000..02e053a427be --- /dev/null +++ b/modules/queue/workerpool.go @@ -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) + } + + } + } + } +} diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 1a33e232c334..b619c9855a72 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -28,6 +28,9 @@ type queueSettings struct { MaxAttempts int Timeout time.Duration Workers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int } // Queue settings @@ -45,6 +48,9 @@ func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) qu opts["DBIndex"] = q.DBIndex opts["QueueName"] = name opts["Workers"] = q.Workers + opts["BlockTimeout"] = q.BlockTimeout + opts["BoostTimeout"] = q.BoostTimeout + opts["BoostWorkers"] = q.BoostWorkers cfg, err := json.Marshal(opts) if err != nil { @@ -96,7 +102,10 @@ func getQueueSettings(name string) queueSettings { q.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(Queue.WrapIfNecessary) q.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(Queue.MaxAttempts) q.Timeout = sec.Key("TIMEOUT").MustDuration(Queue.Timeout) - q.Workers = sec.Key("WORKER").MustInt(Queue.Workers) + q.Workers = sec.Key("WORKERS").MustInt(Queue.Workers) + q.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(Queue.BlockTimeout) + q.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(Queue.BoostTimeout) + q.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(Queue.BoostWorkers) q.Addresses, q.Password, q.DBIndex, _ = ParseQueueConnStr(q.ConnectionString) return q @@ -117,7 +126,10 @@ func newQueueService() { Queue.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(true) Queue.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(10) Queue.Timeout = sec.Key("TIMEOUT").MustDuration(GracefulHammerTime + 30*time.Second) - Queue.Workers = sec.Key("WORKER").MustInt(1) + Queue.Workers = sec.Key("WORKERS").MustInt(1) + Queue.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(1 * time.Second) + Queue.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(5 * time.Minute) + Queue.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(5) hasWorkers := false for _, key := range Cfg.Section("queue.notification").Keys() { From 85d1a7f7d22f21ed341d538388b3894b54947be0 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 7 Dec 2019 16:46:36 +0000 Subject: [PATCH 05/35] Queue: Add name variable to queues --- modules/queue/queue_channel.go | 7 +++++-- modules/queue/queue_disk.go | 23 +++++++++++++---------- modules/queue/queue_disk_channel.go | 13 ++++++++++++- modules/queue/queue_redis.go | 13 +++++++++---- modules/queue/queue_wrapped.go | 11 ++++++++--- modules/setting/queue.go | 6 +++++- 6 files changed, 52 insertions(+), 21 deletions(-) diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go index ebcf22ef7932..90ec52347def 100644 --- a/modules/queue/queue_channel.go +++ b/modules/queue/queue_channel.go @@ -24,6 +24,7 @@ type ChannelQueueConfiguration struct { BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int + Name string } // ChannelQueue implements @@ -31,6 +32,7 @@ type ChannelQueue struct { pool *WorkerPool exemplar interface{} workers int + name string } // NewChannelQueue create a memory channel queue @@ -59,16 +61,17 @@ func NewChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro }, exemplar: exemplar, workers: config.Workers, + name: config.Name, }, nil } // Run starts to run the queue func (c *ChannelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { atShutdown(context.Background(), func() { - log.Warn("ChannelQueue is not shutdownable!") + log.Warn("ChannelQueue: %s is not shutdownable!", c.name) }) atTerminate(context.Background(), func() { - log.Warn("ChannelQueue is not terminatable!") + log.Warn("ChannelQueue: %s is not terminatable!", c.name) }) c.pool.addWorkers(c.pool.baseCtx, c.workers) } diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index 50e49f3a29ef..cb95b9611902 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -28,6 +28,7 @@ type LevelQueueConfiguration struct { BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int + Name string } // LevelQueue implements a disk library queue @@ -38,6 +39,7 @@ type LevelQueue struct { terminated chan struct{} exemplar interface{} workers int + name string } // NewLevelQueue creates a ledis local queue @@ -72,6 +74,7 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) closed: make(chan struct{}), terminated: make(chan struct{}), workers: config.Workers, + name: config.Name, }, nil } @@ -84,16 +87,16 @@ func (l *LevelQueue) Run(atShutdown, atTerminate func(context.Context, func())) go l.readToChan() - log.Trace("Waiting til closed") + log.Trace("%s Waiting til closed", l.name) <-l.closed - log.Trace("Waiting til done") + log.Trace("%s Waiting til done", l.name) l.pool.Wait() // FIXME: graceful: Needs HammerContext - log.Trace("Waiting til cleaned") + log.Trace("%s Waiting til cleaned", l.name) l.pool.CleanUp(context.TODO()) - log.Trace("cleaned") + log.Trace("%s cleaned", l.name) } @@ -108,7 +111,7 @@ func (l *LevelQueue) readToChan() { bs, err := l.queue.RPop() if err != nil { if err != levelqueue.ErrNotFound { - log.Error("RPop: %v", err) + log.Error("%s RPop: %v", l.name, err) } time.Sleep(time.Millisecond * 100) continue @@ -130,12 +133,12 @@ func (l *LevelQueue) readToChan() { err = json.Unmarshal(bs, &data) } if err != nil { - log.Error("LevelQueue failed to unmarshal: %v", err) + log.Error("LevelQueue: %s failed to unmarshal: %v", l.name, err) time.Sleep(time.Millisecond * 10) continue } - log.Trace("LevelQueue: task found: %#v", data) + log.Trace("LevelQueue %s: task found: %#v", l.name, data) l.pool.Push(data) time.Sleep(time.Millisecond * 10) @@ -163,6 +166,7 @@ func (l *LevelQueue) Push(data Data) error { // Shutdown this queue and stop processing func (l *LevelQueue) Shutdown() { + log.Trace("Shutdown: %s", l.name) select { case <-l.closed: default: @@ -172,12 +176,11 @@ func (l *LevelQueue) Shutdown() { // Terminate this queue and close the queue func (l *LevelQueue) Terminate() { - log.Trace("Terminating") + log.Trace("Terminating: %s", l.name) l.Shutdown() if err := l.queue.Close(); err != nil && err.Error() != "leveldb: closed" { - log.Error("Error whilst closing internal queue: %v", err) + log.Error("Error whilst closing internal queue in %s: %v", l.name, err) } - } func init() { diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index f3278271527c..fc186b3bb985 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -16,6 +16,7 @@ const PersistableChannelQueueType Type = "persistable-channel" // PersistableChannelQueueConfiguration is the configuration for a PersistableChannelQueue type PersistableChannelQueueConfiguration struct { + Name string DataDir string BatchLength int QueueLength int @@ -50,6 +51,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( BlockTimeout: config.BlockTimeout, BoostTimeout: config.BoostTimeout, BoostWorkers: config.BoostWorkers, + Name: config.Name + "-channel", }, exemplar) if err != nil { return nil, err @@ -64,6 +66,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( BlockTimeout: 1 * time.Second, BoostTimeout: 5 * time.Minute, BoostWorkers: 5, + Name: config.Name + "-level", } levelQueue, err := NewLevelQueue(handle, levelCfg, exemplar) @@ -72,6 +75,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( ChannelQueue: channelQueue.(*ChannelQueue), delayedStarter: delayedStarter{ internal: levelQueue.(*LevelQueue), + name: config.Name, }, closed: make(chan struct{}), }, nil @@ -88,11 +92,17 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( underlying: LevelQueueType, timeout: config.Timeout, maxAttempts: config.MaxAttempts, + name: config.Name, }, closed: make(chan struct{}), }, nil } +// Name returns the name of this queue +func (p *PersistableChannelQueue) Name() string { + return p.delayedStarter.name +} + // Push will push the indexer data to queue func (p *PersistableChannelQueue) Push(data Data) error { select { @@ -134,6 +144,7 @@ func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Conte // Shutdown processing this queue func (p *PersistableChannelQueue) Shutdown() { + log.Trace("Shutdown: %s", p.delayedStarter.name) select { case <-p.closed: default: @@ -148,7 +159,7 @@ func (p *PersistableChannelQueue) Shutdown() { // Terminate this queue and close the queue func (p *PersistableChannelQueue) Terminate() { - log.Trace("Terminating") + log.Trace("Terminating: %s", p.delayedStarter.name) p.Shutdown() p.lock.Lock() defer p.lock.Unlock() diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index acc6feeb95ed..ebcba683cb15 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -36,6 +36,7 @@ type RedisQueue struct { closed chan struct{} exemplar interface{} workers int + name string } // RedisQueueConfiguration is the configuration for the redis queue @@ -50,6 +51,7 @@ type RedisQueueConfiguration struct { BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int + Name string } // NewRedisQueue creates single redis or cluster redis queue @@ -80,6 +82,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) exemplar: exemplar, closed: make(chan struct{}), workers: config.Workers, + name: config.Name, } if len(dbs) == 0 { return nil, errors.New("no redis host found") @@ -125,7 +128,7 @@ func (r *RedisQueue) readToChan() { default: bs, err := r.client.LPop(r.queueName).Bytes() if err != nil && err != redis.Nil { - log.Error("LPop failed: %v", err) + log.Error("RedisQueue: %s LPop failed: %v", r.name, err) time.Sleep(time.Millisecond * 100) continue } @@ -146,12 +149,12 @@ func (r *RedisQueue) readToChan() { err = json.Unmarshal(bs, &data) } if err != nil { - log.Error("Unmarshal: %v", err) + log.Error("RedisQueue: %s Unmarshal: %v", r.name, err) time.Sleep(time.Millisecond * 100) continue } - log.Trace("RedisQueue: task found: %#v", data) + log.Trace("RedisQueue: %s task found: %#v", r.name, data) r.pool.Push(data) time.Sleep(time.Millisecond * 10) } @@ -178,6 +181,7 @@ func (r *RedisQueue) Push(data Data) error { // Shutdown processing from this queue func (r *RedisQueue) Shutdown() { + log.Trace("Shutdown: %s", r.name) select { case <-r.closed: default: @@ -187,9 +191,10 @@ func (r *RedisQueue) Shutdown() { // Terminate this queue and close the queue func (r *RedisQueue) Terminate() { + log.Trace("Terminating: %s", r.name) r.Shutdown() if err := r.client.Close(); err != nil { - log.Error("Error whilst closing internal redis client: %v", err) + log.Error("Error whilst closing internal redis client in %s: %v", r.name, err) } } diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go index f99675a9f913..229332734802 100644 --- a/modules/queue/queue_wrapped.go +++ b/modules/queue/queue_wrapped.go @@ -24,6 +24,7 @@ type WrappedQueueConfiguration struct { MaxAttempts int Config interface{} QueueLength int + Name string } type delayedStarter struct { @@ -33,6 +34,7 @@ type delayedStarter struct { cfg interface{} timeout time.Duration maxAttempts int + name string } func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), handle HandlerFunc, exemplar interface{}) { @@ -55,7 +57,7 @@ func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), h select { case <-ctx.Done(): q.lock.Unlock() - log.Fatal("Timedout creating queue %v with cfg %v ", q.underlying, q.cfg) + log.Fatal("Timedout creating queue %v with cfg %v in %s", q.underlying, q.cfg, q.name) default: queue, err := CreateQueue(q.underlying, handle, q.cfg, exemplar) if err == nil { @@ -64,12 +66,12 @@ func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), h break } if err.Error() != "resource temporarily unavailable" { - log.Warn("[Attempt: %d] Failed to create queue: %v cfg: %v error: %v", i, q.underlying, q.cfg, err) + log.Warn("[Attempt: %d] Failed to create queue: %v for %s cfg: %v error: %v", i, q.underlying, q.name, q.cfg, err) } i++ if q.maxAttempts > 0 && i > q.maxAttempts { q.lock.Unlock() - log.Fatal("Unable to create queue %v with cfg %v by max attempts: error: %v", q.underlying, q.cfg, err) + log.Fatal("Unable to create queue %v for %s with cfg %v by max attempts: error: %v", q.underlying, q.name, q.cfg, err) } sleepTime := 100 * time.Millisecond if q.timeout > 0 && q.maxAttempts > 0 { @@ -118,6 +120,7 @@ func NewWrappedQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro underlying: config.Underlying, timeout: config.Timeout, maxAttempts: config.MaxAttempts, + name: config.Name, }, }, nil } @@ -156,6 +159,7 @@ func (q *WrappedQueue) Run(atShutdown, atTerminate func(context.Context, func()) // Shutdown this queue and stop processing func (q *WrappedQueue) Shutdown() { + log.Trace("Shutdown: %s", q.name) q.lock.Lock() defer q.lock.Unlock() if q.internal == nil { @@ -168,6 +172,7 @@ func (q *WrappedQueue) Shutdown() { // Terminate this queue and close the queue func (q *WrappedQueue) Terminate() { + log.Trace("Terminating: %s", q.name) q.lock.Lock() defer q.lock.Unlock() if q.internal == nil { diff --git a/modules/setting/queue.go b/modules/setting/queue.go index b619c9855a72..0066d5a9467a 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -23,6 +23,7 @@ type queueSettings struct { Type string Addresses string Password string + QueueName string DBIndex int WrapIfNecessary bool MaxAttempts int @@ -40,13 +41,14 @@ var Queue = queueSettings{} func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) queue.Queue { q := getQueueSettings(name) opts := make(map[string]interface{}) + opts["Name"] = name opts["QueueLength"] = q.Length opts["BatchLength"] = q.BatchLength opts["DataDir"] = q.DataDir opts["Addresses"] = q.Addresses opts["Password"] = q.Password opts["DBIndex"] = q.DBIndex - opts["QueueName"] = name + opts["QueueName"] = q.QueueName opts["Workers"] = q.Workers opts["BlockTimeout"] = q.BlockTimeout opts["BoostTimeout"] = q.BoostTimeout @@ -106,6 +108,7 @@ func getQueueSettings(name string) queueSettings { q.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(Queue.BlockTimeout) q.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(Queue.BoostTimeout) q.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(Queue.BoostWorkers) + q.QueueName = sec.Key("QUEUE_NAME").MustString(Queue.QueueName) q.Addresses, q.Password, q.DBIndex, _ = ParseQueueConnStr(q.ConnectionString) return q @@ -130,6 +133,7 @@ func newQueueService() { Queue.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(1 * time.Second) Queue.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(5 * time.Minute) Queue.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(5) + Queue.QueueName = sec.Key("QUEUE_NAME").MustString(Queue.QueueName) hasWorkers := false for _, key := range Cfg.Section("queue.notification").Keys() { From 2927bc6fe56ed76d015371b1a87832fd25fea520 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 7 Dec 2019 16:48:21 +0000 Subject: [PATCH 06/35] Queue: Add monitoring --- modules/queue/manager.go | 211 ++++++++++++++++++++++++++++ modules/queue/queue.go | 5 + modules/queue/queue_channel.go | 15 +- modules/queue/queue_disk.go | 21 ++- modules/queue/queue_disk_channel.go | 10 +- modules/queue/queue_redis.go | 15 +- modules/queue/queue_wrapped.go | 11 +- modules/queue/workerpool.go | 30 +++- options/locale/locale_en-US.ini | 28 ++++ routers/admin/admin.go | 59 ++++++++ routers/routes/routes.go | 11 +- templates/admin/monitor.tmpl | 28 ++++ templates/admin/queue.tmpl | 117 +++++++++++++++ 13 files changed, 541 insertions(+), 20 deletions(-) create mode 100644 modules/queue/manager.go create mode 100644 templates/admin/queue.tmpl diff --git a/modules/queue/manager.go b/modules/queue/manager.go new file mode 100644 index 000000000000..100780c70628 --- /dev/null +++ b/modules/queue/manager.go @@ -0,0 +1,211 @@ +// 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" + "encoding/json" + "fmt" + "reflect" + "sort" + "sync" + "time" +) + +var manager *Manager + +// Manager is a queue manager +type Manager struct { + mutex sync.Mutex + + counter int64 + Queues map[int64]*Description +} + +// Description represents a working queue inheriting from Gitea. +type Description struct { + mutex sync.Mutex + QID int64 + Queue Queue + Type Type + Name string + Configuration interface{} + ExemplarType string + addWorkers func(number int, timeout time.Duration) context.CancelFunc + numberOfWorkers func() int + counter int64 + PoolWorkers map[int64]*PoolWorkers +} + +// DescriptionList implements the sort.Interface +type DescriptionList []*Description + +// PoolWorkers represents a working queue inheriting from Gitea. +type PoolWorkers struct { + PID int64 + Workers int + Start time.Time + Timeout time.Time + HasTimeout bool + Cancel context.CancelFunc +} + +// PoolWorkersList implements the sort.Interface +type PoolWorkersList []*PoolWorkers + +func init() { + _ = GetManager() +} + +// GetManager returns a Manager and initializes one as singleton if there's none yet +func GetManager() *Manager { + if manager == nil { + manager = &Manager{ + Queues: make(map[int64]*Description), + } + } + return manager +} + +// Add adds a queue to this manager +func (m *Manager) Add(queue Queue, + t Type, + configuration, + exemplar interface{}, + addWorkers func(number int, timeout time.Duration) context.CancelFunc, + numberOfWorkers func() int) int64 { + + cfg, _ := json.Marshal(configuration) + desc := &Description{ + Queue: queue, + Type: t, + Configuration: string(cfg), + ExemplarType: reflect.TypeOf(exemplar).String(), + PoolWorkers: make(map[int64]*PoolWorkers), + addWorkers: addWorkers, + numberOfWorkers: numberOfWorkers, + } + m.mutex.Lock() + m.counter++ + desc.QID = m.counter + desc.Name = fmt.Sprintf("queue-%d", desc.QID) + if named, ok := queue.(Named); ok { + desc.Name = named.Name() + } + m.Queues[desc.QID] = desc + m.mutex.Unlock() + return desc.QID +} + +// Remove a queue from the Manager +func (m *Manager) Remove(qid int64) { + m.mutex.Lock() + delete(m.Queues, qid) + m.mutex.Unlock() +} + +// GetDescription by qid +func (m *Manager) GetDescription(qid int64) *Description { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.Queues[qid] +} + +// Descriptions returns the queue descriptions +func (m *Manager) Descriptions() []*Description { + m.mutex.Lock() + descs := make([]*Description, 0, len(m.Queues)) + for _, desc := range m.Queues { + descs = append(descs, desc) + } + m.mutex.Unlock() + sort.Sort(DescriptionList(descs)) + return descs +} + +// Workers returns the poolworkers +func (q *Description) Workers() []*PoolWorkers { + q.mutex.Lock() + workers := make([]*PoolWorkers, 0, len(q.PoolWorkers)) + for _, worker := range q.PoolWorkers { + workers = append(workers, worker) + } + q.mutex.Unlock() + sort.Sort(PoolWorkersList(workers)) + return workers +} + +// RegisterWorkers registers workers to this queue +func (q *Description) RegisterWorkers(number int, start time.Time, hasTimeout bool, timeout time.Time, cancel context.CancelFunc) int64 { + q.mutex.Lock() + defer q.mutex.Unlock() + q.counter++ + q.PoolWorkers[q.counter] = &PoolWorkers{ + PID: q.counter, + Workers: number, + Start: start, + Timeout: timeout, + HasTimeout: hasTimeout, + Cancel: cancel, + } + return q.counter +} + +// CancelWorkers cancels pooled workers with pid +func (q *Description) CancelWorkers(pid int64) { + q.mutex.Lock() + pw, ok := q.PoolWorkers[pid] + q.mutex.Unlock() + if !ok { + return + } + pw.Cancel() +} + +// RemoveWorkers deletes pooled workers with pid +func (q *Description) RemoveWorkers(pid int64) { + q.mutex.Lock() + delete(q.PoolWorkers, pid) + q.mutex.Unlock() +} + +// AddWorkers adds workers to the queue if it has registered an add worker function +func (q *Description) AddWorkers(number int, timeout time.Duration) { + if q.addWorkers != nil { + _ = q.addWorkers(number, timeout) + } +} + +// NumberOfWorkers returns the number of workers in the queue +func (q *Description) NumberOfWorkers() int { + if q.numberOfWorkers != nil { + return q.numberOfWorkers() + } + return -1 +} + +func (l DescriptionList) Len() int { + return len(l) +} + +func (l DescriptionList) Less(i, j int) bool { + return l[i].Name < l[j].Name +} + +func (l DescriptionList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func (l PoolWorkersList) Len() int { + return len(l) +} + +func (l PoolWorkersList) Less(i, j int) bool { + return l[i].Start.Before(l[j].Start) +} + +func (l PoolWorkersList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} diff --git a/modules/queue/queue.go b/modules/queue/queue.go index 1220db5c03bb..464e16dab130 100644 --- a/modules/queue/queue.go +++ b/modules/queue/queue.go @@ -48,6 +48,11 @@ type Shutdownable interface { Terminate() } +// Named represents a queue with a name +type Named interface { + Name() string +} + // Queue defines an interface to save an issue indexer queue type Queue interface { Run(atShutdown, atTerminate func(context.Context, func())) diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go index 90ec52347def..265a5c88f10e 100644 --- a/modules/queue/queue_channel.go +++ b/modules/queue/queue_channel.go @@ -48,7 +48,7 @@ func NewChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro dataChan := make(chan Data, config.QueueLength) ctx, cancel := context.WithCancel(context.Background()) - return &ChannelQueue{ + queue := &ChannelQueue{ pool: &WorkerPool{ baseCtx: ctx, cancel: cancel, @@ -62,7 +62,9 @@ func NewChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro exemplar: exemplar, workers: config.Workers, name: config.Name, - }, nil + } + queue.pool.qid = GetManager().Add(queue, ChannelQueueType, config, exemplar, queue.pool.AddWorkers, queue.pool.NumberOfWorkers) + return queue, nil } // Run starts to run the queue @@ -73,7 +75,9 @@ func (c *ChannelQueue) Run(atShutdown, atTerminate func(context.Context, func()) atTerminate(context.Background(), func() { log.Warn("ChannelQueue: %s is not terminatable!", c.name) }) - c.pool.addWorkers(c.pool.baseCtx, c.workers) + go func() { + _ = c.pool.AddWorkers(c.workers, 0) + }() } // Push will push the indexer data to queue @@ -90,6 +94,11 @@ func (c *ChannelQueue) Push(data Data) error { return nil } +// Name returns the name of this queue +func (c *ChannelQueue) Name() string { + return c.name +} + func init() { queuesMap[ChannelQueueType] = NewChannelQueue } diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index cb95b9611902..f18f3c5f8edc 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -50,7 +50,7 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) } config := configInterface.(LevelQueueConfiguration) - queue, err := levelqueue.Open(config.DataDir) + internal, err := levelqueue.Open(config.DataDir) if err != nil { return nil, err } @@ -58,7 +58,7 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) dataChan := make(chan Data, config.QueueLength) ctx, cancel := context.WithCancel(context.Background()) - return &LevelQueue{ + queue := &LevelQueue{ pool: &WorkerPool{ baseCtx: ctx, cancel: cancel, @@ -69,13 +69,15 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) boostTimeout: config.BoostTimeout, boostWorkers: config.BoostWorkers, }, - queue: queue, + queue: internal, exemplar: exemplar, closed: make(chan struct{}), terminated: make(chan struct{}), workers: config.Workers, name: config.Name, - }, nil + } + queue.pool.qid = GetManager().Add(queue, LevelQueueType, config, exemplar, queue.pool.AddWorkers, queue.pool.NumberOfWorkers) + return queue, nil } // Run starts to run the queue @@ -83,7 +85,9 @@ func (l *LevelQueue) Run(atShutdown, atTerminate func(context.Context, func())) atShutdown(context.Background(), l.Shutdown) atTerminate(context.Background(), l.Terminate) - go l.pool.addWorkers(l.pool.baseCtx, l.workers) + go func() { + _ = l.pool.AddWorkers(l.workers, 0) + }() go l.readToChan() @@ -140,7 +144,7 @@ func (l *LevelQueue) readToChan() { log.Trace("LevelQueue %s: task found: %#v", l.name, data) l.pool.Push(data) - time.Sleep(time.Millisecond * 10) + time.Sleep(time.Millisecond * 100) } } @@ -183,6 +187,11 @@ func (l *LevelQueue) Terminate() { } } +// Name returns the name of this queue +func (l *LevelQueue) Name() string { + return l.name +} + func init() { queuesMap[LevelQueueType] = NewLevelQueue } diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index fc186b3bb985..3bf39b9fa594 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -85,7 +85,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( return nil, ErrInvalidConfiguration{cfg: cfg} } - return &PersistableChannelQueue{ + queue := &PersistableChannelQueue{ ChannelQueue: channelQueue.(*ChannelQueue), delayedStarter: delayedStarter{ cfg: levelCfg, @@ -95,7 +95,9 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( name: config.Name, }, closed: make(chan struct{}), - }, nil + } + _ = GetManager().Add(queue, PersistableChannelQueueType, config, exemplar, nil, nil) + return queue, nil } // Name returns the name of this queue @@ -127,7 +129,9 @@ func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Conte // Just run the level queue - we shut it down later go p.internal.Run(func(_ context.Context, _ func()) {}, func(_ context.Context, _ func()) {}) - go p.ChannelQueue.pool.addWorkers(p.ChannelQueue.pool.baseCtx, p.workers) + go func() { + _ = p.ChannelQueue.pool.AddWorkers(p.workers, 0) + }() <-p.closed p.ChannelQueue.pool.cancel() diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index ebcba683cb15..88794428a857 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -67,7 +67,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) dataChan := make(chan Data, config.QueueLength) ctx, cancel := context.WithCancel(context.Background()) - var queue = RedisQueue{ + var queue = &RedisQueue{ pool: &WorkerPool{ baseCtx: ctx, cancel: cancel, @@ -100,7 +100,9 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) if err := queue.client.Ping().Err(); err != nil { return nil, err } - return &queue, nil + queue.pool.qid = GetManager().Add(queue, RedisQueueType, config, exemplar, queue.pool.AddWorkers, queue.pool.NumberOfWorkers) + + return queue, nil } // Run runs the redis queue @@ -108,7 +110,9 @@ func (r *RedisQueue) Run(atShutdown, atTerminate func(context.Context, func())) atShutdown(context.Background(), r.Shutdown) atTerminate(context.Background(), r.Terminate) - go r.pool.addWorkers(r.pool.baseCtx, r.workers) + go func() { + _ = r.pool.AddWorkers(r.workers, 0) + }() go r.readToChan() @@ -198,6 +202,11 @@ func (r *RedisQueue) Terminate() { } } +// Name returns the name of this queue +func (r *RedisQueue) Name() string { + return r.name +} + func init() { queuesMap[RedisQueueType] = NewRedisQueue } diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go index 229332734802..57f19f63127d 100644 --- a/modules/queue/queue_wrapped.go +++ b/modules/queue/queue_wrapped.go @@ -111,7 +111,7 @@ func NewWrappedQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro return nil, ErrInvalidConfiguration{cfg: cfg} } - return &WrappedQueue{ + queue = &WrappedQueue{ handle: handle, channel: make(chan Data, config.QueueLength), exemplar: exemplar, @@ -122,7 +122,14 @@ func NewWrappedQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro maxAttempts: config.MaxAttempts, name: config.Name, }, - }, nil + } + _ = GetManager().Add(queue, WrappedQueueType, config, exemplar, nil, nil) + return queue, nil +} + +// Name returns the name of the queue +func (q *WrappedQueue) Name() string { + return q.name + "-wrapper" } // Push will push the data to the internal channel checking it against the exemplar diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go index 02e053a427be..bf3a15c00ed3 100644 --- a/modules/queue/workerpool.go +++ b/modules/queue/workerpool.go @@ -18,6 +18,7 @@ type WorkerPool struct { baseCtx context.Context cancel context.CancelFunc cond *sync.Cond + qid int64 numberOfWorkers int batchLength int handle HandlerFunc @@ -68,8 +69,21 @@ func (p *WorkerPool) pushBoost(data 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) + desc := GetManager().GetDescription(p.qid) + if desc != nil { + log.Warn("Worker Channel for %v blocked for %v - adding %d temporary workers for %s, block timeout now %v", desc.Name, ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) + + start := time.Now() + pid := desc.RegisterWorkers(p.boostWorkers, start, false, start, cancel) + go func() { + <-ctx.Done() + desc.RemoveWorkers(pid) + cancel() + }() + } else { + log.Warn("Worker Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) + } go func() { <-time.After(p.boostTimeout) cancel() @@ -95,12 +109,26 @@ func (p *WorkerPool) NumberOfWorkers() int { func (p *WorkerPool) AddWorkers(number int, timeout time.Duration) context.CancelFunc { var ctx context.Context var cancel context.CancelFunc + start := time.Now() + end := start + hasTimeout := false if timeout > 0 { ctx, cancel = context.WithTimeout(p.baseCtx, timeout) + end = start.Add(timeout) + hasTimeout = true } else { ctx, cancel = context.WithCancel(p.baseCtx) } + desc := GetManager().GetDescription(p.qid) + if desc != nil { + pid := desc.RegisterWorkers(number, start, hasTimeout, end, cancel) + go func() { + <-ctx.Done() + desc.RemoveWorkers(pid) + cancel() + }() + } p.addWorkers(ctx, number) return cancel } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 691190427148..d6a96b55a5dd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2022,6 +2022,34 @@ monitor.execute_time = Execution Time monitor.process.cancel = Cancel process monitor.process.cancel_desc = Cancelling a process may cause data loss monitor.process.cancel_notices = Cancel: %s? +monitor.queues = Queues +monitor.queue = Queue: %s +monitor.queue.name = Name +monitor.queue.type = Type +monitor.queue.exemplar = Exemplar Type +monitor.queue.numberworkers = Number of Workers +monitor.queue.review = Review Config +monitor.queue.review_add = Review/Add Workers +monitor.queue.configuration = Initial Configuration +monitor.queue.nopool.title = No Worker Pool +monitor.queue.nopool.desc = This queue wraps other queues and does not itself have a worker pool. +monitor.queue.wrapped.desc = A wrapped queue wraps a slow starting queue, buffering queued requests in a channel. It does not have a worker pool itself. +monitor.queue.persistable-channel.desc = A persistable-channel wraps two queues, a channel queue that has its own worker pool and a level queue for persisted requests from previous shutdowns. It does not have a worker pool itself. +monitor.queue.pool.timeout = Timeout +monitor.queue.pool.addworkers.title = Add Workers +monitor.queue.pool.addworkers.submit = Add Workers +monitor.queue.pool.addworkers.desc = Add Workers to this pool with or without a timeout. If you set a timeout these workers will be removed from the pool after the timeout has lapsed. +monitor.queue.pool.addworkers.numberworkers.placeholder = Number of Workers +monitor.queue.pool.addworkers.timeout.placeholder = Set to 0 for no timeout +monitor.queue.pool.addworkers.mustnumbergreaterzero = Number of Workers to add must be greater than zero +monitor.queue.pool.addworkers.musttimeoutduration = Timeout must be a golang duration eg. 5m or be 0 +monitor.queue.pool.added = Worker Group Added +monitor.queue.pool.workers.title = Active Worker Groups +monitor.queue.pool.workers.none = No worker groups. +monitor.queue.pool.cancel = Shutdown Worker Group +monitor.queue.pool.cancelling = Worker Group shutting down +monitor.queue.pool.cancel_notices = Shutdown this group of %s workers? +monitor.queue.pool.cancel_desc = Leaving a queue without any worker groups may cause requests may block indefinitely. notices.system_notice_list = System Notices notices.view_detail_header = View Notice Details diff --git a/routers/admin/admin.go b/routers/admin/admin.go index ccedcaf8a62e..7fc57edf312a 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/services/mailer" @@ -35,6 +36,7 @@ const ( tplDashboard base.TplName = "admin/dashboard" tplConfig base.TplName = "admin/config" tplMonitor base.TplName = "admin/monitor" + tplQueue base.TplName = "admin/queue" ) var ( @@ -355,6 +357,7 @@ func Monitor(ctx *context.Context) { ctx.Data["PageIsAdminMonitor"] = true ctx.Data["Processes"] = process.GetManager().Processes() ctx.Data["Entries"] = cron.ListTasks() + ctx.Data["Queues"] = queue.GetManager().Descriptions() ctx.HTML(200, tplMonitor) } @@ -366,3 +369,59 @@ func MonitorCancel(ctx *context.Context) { "redirect": ctx.Repo.RepoLink + "/admin/monitor", }) } + +// Queue shows details for a specific queue +func Queue(ctx *context.Context) { + qid := ctx.ParamsInt64("qid") + desc := queue.GetManager().GetDescription(qid) + if desc == nil { + ctx.Status(404) + return + } + ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", desc.Name) + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminMonitor"] = true + ctx.Data["Queue"] = desc + ctx.HTML(200, tplQueue) +} + +// WorkerCancel cancels a worker group +func WorkerCancel(ctx *context.Context) { + qid := ctx.ParamsInt64("qid") + desc := queue.GetManager().GetDescription(qid) + if desc == nil { + ctx.Status(404) + return + } + pid := ctx.ParamsInt64("pid") + desc.CancelWorkers(pid) + ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.cancelling")) + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid), + }) +} + +// AddWorkers adds workers to a worker group +func AddWorkers(ctx *context.Context) { + qid := ctx.ParamsInt64("qid") + desc := queue.GetManager().GetDescription(qid) + if desc == nil { + ctx.Status(404) + return + } + number := ctx.QueryInt("number") + if number < 1 { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.mustnumbergreaterzero")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + timeout, err := time.ParseDuration(ctx.Query("timeout")) + if err != nil { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.musttimeoutduration")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + desc.AddWorkers(number, timeout) + ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index c8351f312bb3..e97a932692ab 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -411,8 +411,15 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("", adminReq, admin.Dashboard) m.Get("/config", admin.Config) m.Post("/config/test_mail", admin.SendTestMail) - m.Get("/monitor", admin.Monitor) - m.Post("/monitor/cancel/:pid", admin.MonitorCancel) + m.Group("/monitor", func() { + m.Get("", admin.Monitor) + m.Post("/cancel/:pid", admin.MonitorCancel) + m.Group("/queue/:qid", func() { + m.Get("", admin.Queue) + m.Post("/add", admin.AddWorkers) + m.Post("/cancel/:pid", admin.WorkerCancel) + }) + }) m.Group("/users", func() { m.Get("", admin.Users) diff --git a/templates/admin/monitor.tmpl b/templates/admin/monitor.tmpl index 38402fece2be..0f9c2150b647 100644 --- a/templates/admin/monitor.tmpl +++ b/templates/admin/monitor.tmpl @@ -31,6 +31,34 @@ +

+ {{.i18n.Tr "admin.monitor.queues"}} +

+
+ + + + + + + + + + + + {{range .Queues}} + + + + + + + {{end}} + +
{{.i18n.Tr "admin.monitor.queue.name"}}{{.i18n.Tr "admin.monitor.queue.type"}}{{.i18n.Tr "admin.monitor.queue.exemplar"}}{{.i18n.Tr "admin.monitor.queue.numberworkers"}}
{{.Name}}{{.Type}}{{.ExemplarType}}{{$sum := .NumberOfWorkers}}{{if lt $sum 0}}-{{else}}{{$sum}}{{end}}{{if lt $sum 0}}{{$.i18n.Tr "admin.monitor.queue.review"}}{{else}}{{$.i18n.Tr "admin.monitor.queue.review_add"}}{{end}} +
+
+

{{.i18n.Tr "admin.monitor.process"}}

diff --git a/templates/admin/queue.tmpl b/templates/admin/queue.tmpl new file mode 100644 index 000000000000..ab8422824361 --- /dev/null +++ b/templates/admin/queue.tmpl @@ -0,0 +1,117 @@ +{{template "base/head" .}} +
+ {{template "admin/navbar" .}} +
+ {{template "base/alert" .}} +

+ {{.i18n.Tr "admin.monitor.queue" .Queue.Name}} +

+
+ + + + + + + + + + + + + + + + + +
{{.i18n.Tr "admin.monitor.queue.name"}}{{.i18n.Tr "admin.monitor.queue.type"}}{{.i18n.Tr "admin.monitor.queue.exemplar"}}{{.i18n.Tr "admin.monitor.queue.numberworkers"}}
{{.Queue.Name}}{{.Queue.Type}}{{.Queue.ExemplarType}}{{$sum := .Queue.NumberOfWorkers}}{{if lt $sum 0}}-{{else}}{{$sum}}{{end}}
+
+ {{if lt $sum 0 }} +

+ {{.i18n.Tr "admin.monitor.queue.nopool.title"}} +

+
+ {{if eq .Queue.Type "wrapped" }} +

{{.i18n.Tr "admin.monitor.queue.wrapped.desc"}}

+ {{else if eq .Queue.Type "persistable-channel"}} +

{{.i18n.Tr "admin.monitor.queue.persistable-channel.desc"}}

+ {{else}} +

{{.i18n.Tr "admin.monitor.queue.nopool.desc"}}

+ {{end}} +
+ {{else}} +

+ {{.i18n.Tr "admin.monitor.queue.pool.addworkers.title"}} +

+
+

{{.i18n.Tr "admin.monitor.queue.pool.addworkers.desc"}}

+
+ {{$.CsrfTokenHtml}} +
+
+
+ + +
+
+ + +
+
+ +
+
+
+

+ {{.i18n.Tr "admin.monitor.queue.pool.workers.title"}} +

+
+ + + + + + + + + + + {{range .Queue.Workers}} + + + + + + + {{else}} + + + {{end}} + +
{{.i18n.Tr "admin.monitor.queue.numberworkers"}}{{.i18n.Tr "admin.monitor.start"}}{{.i18n.Tr "admin.monitor.queue.pool.timeout"}}
{{.Workers}}{{DateFmtLong .Start}}{{if .HasTimeout}}{{DateFmtLong .Timeout}}{{else}}-{{end}} + +
{{.i18n.Tr "admin.monitor.queue.pool.workers.none" }} +
+
+ {{end}} +

+ {{.i18n.Tr "admin.monitor.queue.configuration"}} +

+
+
{{.Queue.Configuration | JsonPrettyPrint}}
+		
+
+
+ + +{{template "base/footer" .}} From 9ad9070555868e472c80ff4b5c27041ab49892c7 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 11 Dec 2019 20:34:05 +0000 Subject: [PATCH 07/35] Queue: Improve logging --- modules/queue/manager.go | 5 ++ modules/queue/queue_disk.go | 38 +++++++++------ modules/queue/queue_disk_channel.go | 10 +++- modules/queue/queue_disk_channel_test.go | 28 +++++++---- modules/queue/queue_disk_test.go | 59 +++++++++++++++--------- modules/queue/queue_redis.go | 16 +++++-- modules/queue/queue_wrapped.go | 7 +-- modules/queue/workerpool.go | 14 ++++-- 8 files changed, 115 insertions(+), 62 deletions(-) diff --git a/modules/queue/manager.go b/modules/queue/manager.go index 100780c70628..81478019e533 100644 --- a/modules/queue/manager.go +++ b/modules/queue/manager.go @@ -12,6 +12,8 @@ import ( "sort" "sync" "time" + + "code.gitea.io/gitea/modules/log" ) var manager *Manager @@ -96,6 +98,7 @@ func (m *Manager) Add(queue Queue, } m.Queues[desc.QID] = desc m.mutex.Unlock() + log.Trace("Queue Manager registered: %s (QID: %d)", desc.Name, desc.QID) return desc.QID } @@ -104,6 +107,8 @@ func (m *Manager) Remove(qid int64) { m.mutex.Lock() delete(m.Queues, qid) m.mutex.Unlock() + log.Trace("Queue Manager removed: QID: %d", qid) + } // GetDescription by qid diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index f18f3c5f8edc..41e8a9e7c0b7 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -91,16 +91,18 @@ func (l *LevelQueue) Run(atShutdown, atTerminate func(context.Context, func())) go l.readToChan() - log.Trace("%s Waiting til closed", l.name) + log.Trace("LevelQueue: %s Waiting til closed", l.name) <-l.closed - log.Trace("%s Waiting til done", l.name) + log.Trace("LevelQueue: %s Waiting til done", l.name) l.pool.Wait() - // FIXME: graceful: Needs HammerContext - log.Trace("%s Waiting til cleaned", l.name) - l.pool.CleanUp(context.TODO()) - log.Trace("%s cleaned", l.name) + log.Trace("LevelQueue: %s Waiting til cleaned", l.name) + ctx, cancel := context.WithCancel(context.Background()) + atTerminate(ctx, cancel) + l.pool.CleanUp(ctx) + cancel() + log.Trace("LevelQueue: %s Cleaned", l.name) } @@ -115,7 +117,7 @@ func (l *LevelQueue) readToChan() { bs, err := l.queue.RPop() if err != nil { if err != levelqueue.ErrNotFound { - log.Error("%s RPop: %v", l.name, err) + log.Error("LevelQueue: %s Error on RPop: %v", l.name, err) } time.Sleep(time.Millisecond * 100) continue @@ -137,14 +139,14 @@ func (l *LevelQueue) readToChan() { err = json.Unmarshal(bs, &data) } if err != nil { - log.Error("LevelQueue: %s failed to unmarshal: %v", l.name, err) - time.Sleep(time.Millisecond * 10) + log.Error("LevelQueue: %s Failed to unmarshal with error: %v", l.name, err) + time.Sleep(time.Millisecond * 100) continue } - log.Trace("LevelQueue %s: task found: %#v", l.name, data) + log.Trace("LevelQueue %s: Task found: %#v", l.name, data) l.pool.Push(data) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 10) } } @@ -170,7 +172,7 @@ func (l *LevelQueue) Push(data Data) error { // Shutdown this queue and stop processing func (l *LevelQueue) Shutdown() { - log.Trace("Shutdown: %s", l.name) + log.Trace("LevelQueue: %s Shutdown", l.name) select { case <-l.closed: default: @@ -180,10 +182,16 @@ func (l *LevelQueue) Shutdown() { // Terminate this queue and close the queue func (l *LevelQueue) Terminate() { - log.Trace("Terminating: %s", l.name) + log.Trace("LevelQueue: %s Terminating", l.name) l.Shutdown() - if err := l.queue.Close(); err != nil && err.Error() != "leveldb: closed" { - log.Error("Error whilst closing internal queue in %s: %v", l.name, err) + select { + case <-l.terminated: + default: + close(l.terminated) + if err := l.queue.Close(); err != nil && err.Error() != "leveldb: closed" { + log.Error("Error whilst closing internal queue in %s: %v", l.name, err) + } + } } diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index 3bf39b9fa594..884fc410df92 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -133,22 +133,28 @@ func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Conte _ = p.ChannelQueue.pool.AddWorkers(p.workers, 0) }() + log.Trace("PersistableChannelQueue: %s Waiting til closed", p.delayedStarter.name) <-p.closed + log.Trace("PersistableChannelQueue: %s Cancelling pools", p.delayedStarter.name) p.ChannelQueue.pool.cancel() p.internal.(*LevelQueue).pool.cancel() + log.Trace("PersistableChannelQueue: %s Waiting til done", p.delayedStarter.name) p.ChannelQueue.pool.Wait() p.internal.(*LevelQueue).pool.Wait() // Redirect all remaining data in the chan to the internal channel go func() { + log.Trace("PersistableChannelQueue: %s Redirecting remaining data", p.delayedStarter.name) for data := range p.ChannelQueue.pool.dataChan { _ = p.internal.Push(data) } + log.Trace("PersistableChannelQueue: %s Done Redirecting remaining data", p.delayedStarter.name) }() + log.Trace("PersistableChannelQueue: %s Done main loop", p.delayedStarter.name) } // Shutdown processing this queue func (p *PersistableChannelQueue) Shutdown() { - log.Trace("Shutdown: %s", p.delayedStarter.name) + log.Trace("PersistableChannelQueue: %s Shutdown", p.delayedStarter.name) select { case <-p.closed: default: @@ -163,7 +169,7 @@ func (p *PersistableChannelQueue) Shutdown() { // Terminate this queue and close the queue func (p *PersistableChannelQueue) Terminate() { - log.Trace("Terminating: %s", p.delayedStarter.name) + log.Trace("PersistableChannelQueue: %s Terminating", p.delayedStarter.name) p.Shutdown() p.lock.Lock() defer p.lock.Unlock() diff --git a/modules/queue/queue_disk_channel_test.go b/modules/queue/queue_disk_channel_test.go index 5f6f614bd8c8..01a90ebcfb8a 100644 --- a/modules/queue/queue_disk_channel_test.go +++ b/modules/queue/queue_disk_channel_test.go @@ -24,8 +24,8 @@ func TestPersistableChannelQueue(t *testing.T) { } } - var queueShutdown func() - var queueTerminate func() + queueShutdown := []func(){} + queueTerminate := []func(){} tmpDir, err := ioutil.TempDir("", "persistable-channel-queue-test-data") assert.NoError(t, err) @@ -40,9 +40,9 @@ func TestPersistableChannelQueue(t *testing.T) { assert.NoError(t, err) go queue.Run(func(_ context.Context, shutdown func()) { - queueShutdown = shutdown + queueShutdown = append(queueShutdown, shutdown) }, func(_ context.Context, terminate func()) { - queueTerminate = terminate + queueTerminate = append(queueTerminate, terminate) }) test1 := testData{"A", 1} @@ -66,7 +66,9 @@ func TestPersistableChannelQueue(t *testing.T) { err = queue.Push(test1) assert.Error(t, err) - queueShutdown() + for _, callback := range queueShutdown { + callback() + } time.Sleep(200 * time.Millisecond) err = queue.Push(&test1) assert.NoError(t, err) @@ -77,7 +79,9 @@ func TestPersistableChannelQueue(t *testing.T) { assert.Fail(t, "Handler processing should have stopped") default: } - queueTerminate() + for _, callback := range queueTerminate { + callback() + } // Reopen queue queue, err = NewPersistableChannelQueue(handle, PersistableChannelQueueConfiguration{ @@ -89,9 +93,9 @@ func TestPersistableChannelQueue(t *testing.T) { assert.NoError(t, err) go queue.Run(func(_ context.Context, shutdown func()) { - queueShutdown = shutdown + queueShutdown = append(queueShutdown, shutdown) }, func(_ context.Context, terminate func()) { - queueTerminate = terminate + queueTerminate = append(queueTerminate, terminate) }) result3 := <-handleChan @@ -101,7 +105,11 @@ func TestPersistableChannelQueue(t *testing.T) { result4 := <-handleChan assert.Equal(t, test2.TestString, result4.TestString) assert.Equal(t, test2.TestInt, result4.TestInt) - queueShutdown() - queueTerminate() + for _, callback := range queueShutdown { + callback() + } + for _, callback := range queueTerminate { + callback() + } } diff --git a/modules/queue/queue_disk_test.go b/modules/queue/queue_disk_test.go index b9c6f278ef57..03de451760a0 100644 --- a/modules/queue/queue_disk_test.go +++ b/modules/queue/queue_disk_test.go @@ -6,6 +6,7 @@ package queue import ( "context" + "io/ioutil" "os" "testing" "time" @@ -23,11 +24,15 @@ func TestLevelQueue(t *testing.T) { } } - var queueShutdown func() - var queueTerminate func() + queueShutdown := []func(){} + queueTerminate := []func(){} + + tmpDir, err := ioutil.TempDir("", "level-queue-test-data") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) queue, err := NewLevelQueue(handle, LevelQueueConfiguration{ - DataDir: "level-queue-test-data", + DataDir: tmpDir, BatchLength: 2, Workers: 1, QueueLength: 20, @@ -38,9 +43,9 @@ func TestLevelQueue(t *testing.T) { assert.NoError(t, err) go queue.Run(func(_ context.Context, shutdown func()) { - queueShutdown = shutdown + queueShutdown = append(queueShutdown, shutdown) }, func(_ context.Context, terminate func()) { - queueTerminate = terminate + queueTerminate = append(queueTerminate, terminate) }) test1 := testData{"A", 1} @@ -64,7 +69,9 @@ func TestLevelQueue(t *testing.T) { err = queue.Push(test1) assert.Error(t, err) - queueShutdown() + for _, callback := range queueShutdown { + callback() + } time.Sleep(200 * time.Millisecond) err = queue.Push(&test1) assert.NoError(t, err) @@ -75,24 +82,30 @@ func TestLevelQueue(t *testing.T) { assert.Fail(t, "Handler processing should have stopped") default: } - queueTerminate() + for _, callback := range queueTerminate { + callback() + } // Reopen queue - queue, err = NewLevelQueue(handle, LevelQueueConfiguration{ - DataDir: "level-queue-test-data", - BatchLength: 2, - Workers: 1, - QueueLength: 20, - BlockTimeout: 1 * time.Second, - BoostTimeout: 5 * time.Minute, - BoostWorkers: 5, - }, &testData{}) + queue, err = NewWrappedQueue(handle, + WrappedQueueConfiguration{ + Underlying: LevelQueueType, + Config: LevelQueueConfiguration{ + DataDir: tmpDir, + BatchLength: 2, + Workers: 1, + QueueLength: 20, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, + }, + }, &testData{}) assert.NoError(t, err) go queue.Run(func(_ context.Context, shutdown func()) { - queueShutdown = shutdown + queueShutdown = append(queueShutdown, shutdown) }, func(_ context.Context, terminate func()) { - queueTerminate = terminate + queueTerminate = append(queueTerminate, terminate) }) result3 := <-handleChan @@ -102,8 +115,10 @@ func TestLevelQueue(t *testing.T) { result4 := <-handleChan assert.Equal(t, test2.TestString, result4.TestString) assert.Equal(t, test2.TestInt, result4.TestInt) - queueShutdown() - queueTerminate() - - os.RemoveAll("level-queue-test-data") + for _, callback := range queueShutdown { + callback() + } + for _, callback := range queueTerminate { + callback() + } } diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 88794428a857..4f2ceec029f0 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -116,10 +116,16 @@ func (r *RedisQueue) Run(atShutdown, atTerminate func(context.Context, func())) go r.readToChan() + log.Trace("RedisQueue: %s Waiting til closed", r.name) <-r.closed + log.Trace("RedisQueue: %s Waiting til done", r.name) r.pool.Wait() - // FIXME: graceful: Needs HammerContext - r.pool.CleanUp(context.TODO()) + + log.Trace("RedisQueue: %s Waiting til cleaned", r.name) + ctx, cancel := context.WithCancel(context.Background()) + atTerminate(ctx, cancel) + r.pool.CleanUp(ctx) + cancel() } func (r *RedisQueue) readToChan() { @@ -132,7 +138,7 @@ func (r *RedisQueue) readToChan() { default: bs, err := r.client.LPop(r.queueName).Bytes() if err != nil && err != redis.Nil { - log.Error("RedisQueue: %s LPop failed: %v", r.name, err) + log.Error("RedisQueue: %s Error on LPop: %v", r.name, err) time.Sleep(time.Millisecond * 100) continue } @@ -153,12 +159,12 @@ func (r *RedisQueue) readToChan() { err = json.Unmarshal(bs, &data) } if err != nil { - log.Error("RedisQueue: %s Unmarshal: %v", r.name, err) + log.Error("RedisQueue: %s Error on Unmarshal: %v", r.name, err) time.Sleep(time.Millisecond * 100) continue } - log.Trace("RedisQueue: %s task found: %#v", r.name, data) + log.Trace("RedisQueue: %s Task found: %#v", r.name, data) r.pool.Push(data) time.Sleep(time.Millisecond * 10) } diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go index 57f19f63127d..46557ea31899 100644 --- a/modules/queue/queue_wrapped.go +++ b/modules/queue/queue_wrapped.go @@ -92,7 +92,7 @@ type WrappedQueue struct { // NewWrappedQueue will attempt to create a queue of the provided type, // but if there is a problem creating this queue it will instead create -// a WrappedQueue with delayed the startup of the queue instead and a +// a WrappedQueue with delayed startup of the queue instead and a // channel which will be redirected to the queue func NewWrappedQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { configInterface, err := toConfig(WrappedQueueConfiguration{}, cfg) @@ -162,11 +162,12 @@ func (q *WrappedQueue) Run(atShutdown, atTerminate func(context.Context, func()) } q.internal.Run(atShutdown, atTerminate) + log.Trace("WrappedQueue: %s Done", q.name) } // Shutdown this queue and stop processing func (q *WrappedQueue) Shutdown() { - log.Trace("Shutdown: %s", q.name) + log.Trace("WrappedQueue: %s Shutdown", q.name) q.lock.Lock() defer q.lock.Unlock() if q.internal == nil { @@ -179,7 +180,7 @@ func (q *WrappedQueue) Shutdown() { // Terminate this queue and close the queue func (q *WrappedQueue) Terminate() { - log.Trace("Terminating: %s", q.name) + log.Trace("WrappedQueue: %s Terminating", q.name) q.lock.Lock() defer q.lock.Unlock() if q.internal == nil { diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go index bf3a15c00ed3..fe05e7fe6ec2 100644 --- a/modules/queue/workerpool.go +++ b/modules/queue/workerpool.go @@ -72,7 +72,7 @@ func (p *WorkerPool) pushBoost(data Data) { ctx, cancel := context.WithCancel(p.baseCtx) desc := GetManager().GetDescription(p.qid) if desc != nil { - log.Warn("Worker Channel for %v blocked for %v - adding %d temporary workers for %s, block timeout now %v", desc.Name, ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) + log.Warn("WorkerPool: %d (for %s) Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", p.qid, desc.Name, ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) start := time.Now() pid := desc.RegisterWorkers(p.boostWorkers, start, false, start, cancel) @@ -82,7 +82,7 @@ func (p *WorkerPool) pushBoost(data Data) { cancel() }() } else { - log.Warn("Worker Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) + log.Warn("WorkerPool: %d Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", p.qid, ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) } go func() { <-time.After(p.boostTimeout) @@ -128,6 +128,10 @@ func (p *WorkerPool) AddWorkers(number int, timeout time.Duration) context.Cance desc.RemoveWorkers(pid) cancel() }() + log.Trace("WorkerPool: %d (for %s) adding %d workers with group id: %d", p.qid, desc.Name, number, pid) + } else { + log.Trace("WorkerPool: %d adding %d workers (no group id)", p.qid, number) + } p.addWorkers(ctx, number) return cancel @@ -173,18 +177,18 @@ func (p *WorkerPool) 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") + log.Trace("WorkerPool: %d CleanUp", p.qid) close(p.dataChan) for data := range p.dataChan { p.handle(data) select { case <-ctx.Done(): - log.Warn("Cleanup context closed before finishing clean-up") + log.Warn("WorkerPool: %d Cleanup context closed before finishing clean-up", p.qid) return default: } } - log.Trace("CleanUp done") + log.Trace("WorkerPool: %d CleanUp Done", p.qid) } func (p *WorkerPool) doWork(ctx context.Context) { From d6b540475f2a63fe34e38e842e8d6b6ce4520875 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Fri, 15 Nov 2019 19:16:40 +0000 Subject: [PATCH 08/35] Issues: Gracefulise the issues indexer Remove the old now unused specific queues --- integrations/issue_test.go | 9 +- modules/indexer/issues/bleve.go | 5 + modules/indexer/issues/db.go | 5 + modules/indexer/issues/indexer.go | 216 ++++++++++++++++-------- modules/indexer/issues/queue.go | 25 --- modules/indexer/issues/queue_channel.go | 62 ------- modules/indexer/issues/queue_disk.go | 104 ------------ modules/indexer/issues/queue_redis.go | 146 ---------------- 8 files changed, 166 insertions(+), 406 deletions(-) delete mode 100644 modules/indexer/issues/queue.go delete mode 100644 modules/indexer/issues/queue_channel.go delete mode 100644 modules/indexer/issues/queue_disk.go delete mode 100644 modules/indexer/issues/queue_redis.go diff --git a/integrations/issue_test.go b/integrations/issue_test.go index fe66a005047f..1454d7588501 100644 --- a/integrations/issue_test.go +++ b/integrations/issue_test.go @@ -11,8 +11,10 @@ import ( "strconv" "strings" "testing" + "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" @@ -87,7 +89,12 @@ func TestViewIssuesKeyword(t *testing.T) { defer prepareTestEnv(t)() repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) - + issue := models.AssertExistsAndLoadBean(t, &models.Issue{ + RepoID: repo.ID, + Index: 1, + }).(*models.Issue) + issues.UpdateIssueIndexer(issue) + time.Sleep(time.Second * 1) const keyword = "first" req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.RelLink(), keyword) resp := MakeRequest(t, req, http.StatusOK) diff --git a/modules/indexer/issues/bleve.go b/modules/indexer/issues/bleve.go index 787ff0dec5a1..b9f505e4bfe0 100644 --- a/modules/indexer/issues/bleve.go +++ b/modules/indexer/issues/bleve.go @@ -266,3 +266,8 @@ func (b *BleveIndexer) Search(keyword string, repoIDs []int64, limit, start int) } return &ret, nil } + +// Close the Index +func (b *BleveIndexer) Close() error { + return b.indexer.Close() +} diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go index a758cfeaeebd..2a5df80fac2e 100644 --- a/modules/indexer/issues/db.go +++ b/modules/indexer/issues/db.go @@ -25,6 +25,11 @@ func (db *DBIndexer) Delete(ids ...int64) error { return nil } +// Close dummy function +func (db *DBIndexer) Close() error { + return nil +} + // Search dummy function func (db *DBIndexer) Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error) { total, ids, err := models.SearchIssueIDsByKeyword(kw, repoIDs, limit, start) diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index ebcd3f68dd51..1fcef59f34f4 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -5,12 +5,16 @@ package issues import ( + "context" + "encoding/json" + "os" "sync" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -44,6 +48,7 @@ type Indexer interface { Index(issue []*IndexerData) error Delete(ids ...int64) error Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error) + Close() error } type indexerHolder struct { @@ -75,9 +80,8 @@ func (h *indexerHolder) get() Indexer { } var ( - issueIndexerChannel = make(chan *IndexerData, setting.Indexer.UpdateQueueLength) // issueIndexerQueue queue of issue ids to be updated - issueIndexerQueue Queue + issueIndexerQueue queue.Queue holder = newIndexerHolder() ) @@ -85,88 +89,142 @@ var ( // all issue index done. func InitIssueIndexer(syncReindex bool) { waitChannel := make(chan time.Duration) + + // Create the Queue + switch setting.Indexer.IssueType { + case "bleve": + handler := func(data ...queue.Data) { + iData := make([]*IndexerData, 0, setting.Indexer.IssueQueueBatchNumber) + for _, datum := range data { + indexerData, ok := datum.(*IndexerData) + if !ok { + log.Error("Unable to process provided datum: %v - not possible to cast to IndexerData", datum) + continue + } + log.Trace("IndexerData Process: %d %v %t", indexerData.ID, indexerData.IDs, indexerData.IsDelete) + if indexerData.IsDelete { + _ = holder.get().Delete(indexerData.IDs...) + continue + } + iData = append(iData, indexerData) + } + if err := holder.get().Index(iData); err != nil { + log.Error("Error whilst indexing: %v Error: %v", iData, err) + } + } + + queueType := queue.PersistableChannelQueueType + switch setting.Indexer.IssueQueueType { + case setting.LevelQueueType: + queueType = queue.LevelQueueType + case setting.ChannelQueueType: + queueType = queue.PersistableChannelQueueType + case setting.RedisQueueType: + queueType = queue.RedisQueueType + default: + log.Fatal("Unsupported indexer queue type: %v", + setting.Indexer.IssueQueueType) + } + + name := "issue_indexer_queue" + opts := make(map[string]interface{}) + opts["QueueLength"] = setting.Indexer.UpdateQueueLength + opts["BatchLength"] = setting.Indexer.IssueQueueBatchNumber + opts["DataDir"] = setting.Indexer.IssueQueueDir + + addrs, password, dbIdx, err := setting.ParseQueueConnStr(setting.Indexer.IssueQueueConnStr) + if queueType == queue.RedisQueueType && err != nil { + log.Fatal("Unable to parse connection string for RedisQueueType: %s : %v", + setting.Indexer.IssueQueueConnStr, + err) + } + opts["Addresses"] = addrs + opts["Password"] = password + opts["DBIndex"] = dbIdx + opts["QueueName"] = name + opts["Name"] = name + opts["Workers"] = 1 + opts["BlockTimeout"] = 1 * time.Second + opts["BoostTimeout"] = 5 * time.Minute + opts["BoostWorkers"] = 5 + cfg, err := json.Marshal(opts) + if err != nil { + log.Error("Unable to marshall generic options: %v Error: %v", opts, err) + log.Fatal("Unable to create issue indexer queue with type %s: %v", + queueType, + err) + } + log.Debug("Creating issue indexer queue with type %s: configuration: %s", queueType, string(cfg)) + issueIndexerQueue, err = queue.CreateQueue(queueType, handler, cfg, &IndexerData{}) + if err != nil { + issueIndexerQueue, err = queue.CreateQueue(queue.WrappedQueueType, handler, queue.WrappedQueueConfiguration{ + Underlying: queueType, + Timeout: setting.GracefulHammerTime + 30*time.Second, + MaxAttempts: 10, + Config: cfg, + QueueLength: setting.Indexer.UpdateQueueLength, + Name: name, + }, &IndexerData{}) + } + if err != nil { + log.Fatal("Unable to create issue indexer queue with type %s: %v : %v", + queueType, + string(cfg), + err) + } + default: + issueIndexerQueue = &queue.DummyQueue{} + } + + // Create the Indexer go func() { start := time.Now() - log.Info("Initializing Issue Indexer") + log.Info("PID %d: Initializing Issue Indexer: %s", os.Getpid(), setting.Indexer.IssueType) var populate bool - var dummyQueue bool switch setting.Indexer.IssueType { case "bleve": - issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath) - exist, err := issueIndexer.Init() - if err != nil { - log.Fatal("Unable to initialize Bleve Issue Indexer: %v", err) - } - populate = !exist - holder.set(issueIndexer) + graceful.GetManager().RunWithShutdownFns(func(_, atTerminate func(context.Context, func())) { + issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath) + exist, err := issueIndexer.Init() + if err != nil { + log.Fatal("Unable to initialize Bleve Issue Indexer: %v", err) + } + populate = !exist + holder.set(issueIndexer) + atTerminate(context.Background(), func() { + log.Debug("Closing issue indexer") + issueIndexer := holder.get() + if issueIndexer != nil { + err := issueIndexer.Close() + if err != nil { + log.Error("Error whilst closing the issue indexer: %v", err) + } + } + log.Info("PID: %d Issue Indexer closed", os.Getpid()) + }) + log.Debug("Created Bleve Indexer") + }) case "db": issueIndexer := &DBIndexer{} holder.set(issueIndexer) - dummyQueue = true default: log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) } - if dummyQueue { - issueIndexerQueue = &DummyQueue{} - } else { - var err error - switch setting.Indexer.IssueQueueType { - case setting.LevelQueueType: - issueIndexerQueue, err = NewLevelQueue( - holder.get(), - setting.Indexer.IssueQueueDir, - setting.Indexer.IssueQueueBatchNumber) - if err != nil { - log.Fatal( - "Unable create level queue for issue queue dir: %s batch number: %d : %v", - setting.Indexer.IssueQueueDir, - setting.Indexer.IssueQueueBatchNumber, - err) - } - case setting.ChannelQueueType: - issueIndexerQueue = NewChannelQueue(holder.get(), setting.Indexer.IssueQueueBatchNumber) - case setting.RedisQueueType: - addrs, pass, idx, err := parseConnStr(setting.Indexer.IssueQueueConnStr) - if err != nil { - log.Fatal("Unable to parse connection string for RedisQueueType: %s : %v", - setting.Indexer.IssueQueueConnStr, - err) - } - issueIndexerQueue, err = NewRedisQueue(addrs, pass, idx, holder.get(), setting.Indexer.IssueQueueBatchNumber) - if err != nil { - log.Fatal("Unable to create RedisQueue: %s : %v", - setting.Indexer.IssueQueueConnStr, - err) - } - default: - log.Fatal("Unsupported indexer queue type: %v", - setting.Indexer.IssueQueueType) - } - - go func() { - err = issueIndexerQueue.Run() - if err != nil { - log.Error("issueIndexerQueue.Run: %v", err) - } - }() - } - - go func() { - for data := range issueIndexerChannel { - _ = issueIndexerQueue.Push(data) - } - }() + // Start processing the queue + go graceful.GetManager().RunWithShutdownFns(issueIndexerQueue.Run) + // Populate the index if populate { if syncReindex { - populateIssueIndexer() + graceful.GetManager().RunWithShutdownContext(populateIssueIndexer) } else { - go populateIssueIndexer() + go graceful.GetManager().RunWithShutdownContext(populateIssueIndexer) } } waitChannel <- time.Since(start) }() + if syncReindex { <-waitChannel } else if setting.Indexer.StartupTimeout > 0 { @@ -179,6 +237,9 @@ func InitIssueIndexer(syncReindex bool) { case duration := <-waitChannel: log.Info("Issue Indexer Initialization took %v", duration) case <-time.After(timeout): + if shutdownable, ok := issueIndexerQueue.(queue.Shutdownable); ok { + shutdownable.Terminate() + } log.Fatal("Issue Indexer Initialization timed-out after: %v", timeout) } }() @@ -186,8 +247,14 @@ func InitIssueIndexer(syncReindex bool) { } // populateIssueIndexer populate the issue indexer with issue data -func populateIssueIndexer() { +func populateIssueIndexer(ctx context.Context) { for page := 1; ; page++ { + select { + case <-ctx.Done(): + log.Warn("Issue Indexer population shutdown before completion") + return + default: + } repos, _, err := models.SearchRepositoryByName(&models.SearchRepoOptions{ Page: page, PageSize: models.RepositoryListDefaultPageSize, @@ -200,10 +267,17 @@ func populateIssueIndexer() { continue } if len(repos) == 0 { + log.Debug("Issue Indexer population complete") return } for _, repo := range repos { + select { + case <-ctx.Done(): + log.Info("Issue Indexer population shutdown before completion") + return + default: + } UpdateRepoIndexer(repo) } } @@ -237,13 +311,17 @@ func UpdateIssueIndexer(issue *models.Issue) { comments = append(comments, comment.Content) } } - issueIndexerChannel <- &IndexerData{ + indexerData := &IndexerData{ ID: issue.ID, RepoID: issue.RepoID, Title: issue.Title, Content: issue.Content, Comments: comments, } + log.Debug("Adding to channel: %v", indexerData) + if err := issueIndexerQueue.Push(indexerData); err != nil { + log.Error("Unable to push to issue indexer: %v: Error: %v", indexerData, err) + } } // DeleteRepoIssueIndexer deletes repo's all issues indexes @@ -258,11 +336,13 @@ func DeleteRepoIssueIndexer(repo *models.Repository) { if len(ids) == 0 { return } - - issueIndexerChannel <- &IndexerData{ + indexerData := &IndexerData{ IDs: ids, IsDelete: true, } + if err := issueIndexerQueue.Push(indexerData); err != nil { + log.Error("Unable to push to issue indexer: %v: Error: %v", indexerData, err) + } } // SearchIssuesByKeyword search issue ids by keywords and repo id diff --git a/modules/indexer/issues/queue.go b/modules/indexer/issues/queue.go deleted file mode 100644 index f93e5c47a40a..000000000000 --- a/modules/indexer/issues/queue.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018 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 issues - -// Queue defines an interface to save an issue indexer queue -type Queue interface { - Run() error - Push(*IndexerData) error -} - -// DummyQueue represents an empty queue -type DummyQueue struct { -} - -// Run starts to run the queue -func (b *DummyQueue) Run() error { - return nil -} - -// Push pushes data to indexer -func (b *DummyQueue) Push(*IndexerData) error { - return nil -} diff --git a/modules/indexer/issues/queue_channel.go b/modules/indexer/issues/queue_channel.go deleted file mode 100644 index b6458d3eb53d..000000000000 --- a/modules/indexer/issues/queue_channel.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2018 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 issues - -import ( - "time" - - "code.gitea.io/gitea/modules/setting" -) - -// ChannelQueue implements -type ChannelQueue struct { - queue chan *IndexerData - indexer Indexer - batchNumber int -} - -// NewChannelQueue create a memory channel queue -func NewChannelQueue(indexer Indexer, batchNumber int) *ChannelQueue { - return &ChannelQueue{ - queue: make(chan *IndexerData, setting.Indexer.UpdateQueueLength), - indexer: indexer, - batchNumber: batchNumber, - } -} - -// Run starts to run the queue -func (c *ChannelQueue) Run() error { - var i int - var datas = make([]*IndexerData, 0, c.batchNumber) - for { - select { - case data := <-c.queue: - if data.IsDelete { - _ = c.indexer.Delete(data.IDs...) - continue - } - - datas = append(datas, data) - if len(datas) >= c.batchNumber { - _ = c.indexer.Index(datas) - // TODO: save the point - datas = make([]*IndexerData, 0, c.batchNumber) - } - case <-time.After(time.Millisecond * 100): - i++ - if i >= 3 && len(datas) > 0 { - _ = c.indexer.Index(datas) - // TODO: save the point - datas = make([]*IndexerData, 0, c.batchNumber) - } - } - } -} - -// Push will push the indexer data to queue -func (c *ChannelQueue) Push(data *IndexerData) error { - c.queue <- data - return nil -} diff --git a/modules/indexer/issues/queue_disk.go b/modules/indexer/issues/queue_disk.go deleted file mode 100644 index d6187f2acbd0..000000000000 --- a/modules/indexer/issues/queue_disk.go +++ /dev/null @@ -1,104 +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 issues - -import ( - "encoding/json" - "time" - - "code.gitea.io/gitea/modules/log" - - "gitea.com/lunny/levelqueue" -) - -var ( - _ Queue = &LevelQueue{} -) - -// LevelQueue implements a disk library queue -type LevelQueue struct { - indexer Indexer - queue *levelqueue.Queue - batchNumber int -} - -// NewLevelQueue creates a ledis local queue -func NewLevelQueue(indexer Indexer, dataDir string, batchNumber int) (*LevelQueue, error) { - queue, err := levelqueue.Open(dataDir) - if err != nil { - return nil, err - } - - return &LevelQueue{ - indexer: indexer, - queue: queue, - batchNumber: batchNumber, - }, nil -} - -// Run starts to run the queue -func (l *LevelQueue) Run() error { - var i int - var datas = make([]*IndexerData, 0, l.batchNumber) - for { - i++ - if len(datas) > l.batchNumber || (len(datas) > 0 && i > 3) { - _ = l.indexer.Index(datas) - datas = make([]*IndexerData, 0, l.batchNumber) - i = 0 - continue - } - - bs, err := l.queue.RPop() - if err != nil { - if err != levelqueue.ErrNotFound { - log.Error("RPop: %v", err) - } - time.Sleep(time.Millisecond * 100) - continue - } - - if len(bs) == 0 { - time.Sleep(time.Millisecond * 100) - continue - } - - var data IndexerData - err = json.Unmarshal(bs, &data) - if err != nil { - log.Error("Unmarshal: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } - - log.Trace("LevelQueue: task found: %#v", data) - - if data.IsDelete { - if data.ID > 0 { - if err = l.indexer.Delete(data.ID); err != nil { - log.Error("indexer.Delete: %v", err) - } - } else if len(data.IDs) > 0 { - if err = l.indexer.Delete(data.IDs...); err != nil { - log.Error("indexer.Delete: %v", err) - } - } - time.Sleep(time.Millisecond * 10) - continue - } - - datas = append(datas, &data) - time.Sleep(time.Millisecond * 10) - } -} - -// Push will push the indexer data to queue -func (l *LevelQueue) Push(data *IndexerData) error { - bs, err := json.Marshal(data) - if err != nil { - return err - } - return l.queue.LPush(bs) -} diff --git a/modules/indexer/issues/queue_redis.go b/modules/indexer/issues/queue_redis.go deleted file mode 100644 index 0344d3c87a0f..000000000000 --- a/modules/indexer/issues/queue_redis.go +++ /dev/null @@ -1,146 +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 issues - -import ( - "encoding/json" - "errors" - "strconv" - "strings" - "time" - - "code.gitea.io/gitea/modules/log" - - "github.com/go-redis/redis" -) - -var ( - _ Queue = &RedisQueue{} -) - -type redisClient interface { - RPush(key string, args ...interface{}) *redis.IntCmd - LPop(key string) *redis.StringCmd - Ping() *redis.StatusCmd -} - -// RedisQueue redis queue -type RedisQueue struct { - client redisClient - queueName string - indexer Indexer - batchNumber int -} - -func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { - fields := strings.Fields(connStr) - for _, f := range fields { - items := strings.SplitN(f, "=", 2) - if len(items) < 2 { - continue - } - switch strings.ToLower(items[0]) { - case "addrs": - addrs = items[1] - case "password": - password = items[1] - case "db": - dbIdx, err = strconv.Atoi(items[1]) - if err != nil { - return - } - } - } - return -} - -// NewRedisQueue creates single redis or cluster redis queue -func NewRedisQueue(addrs string, password string, dbIdx int, indexer Indexer, batchNumber int) (*RedisQueue, error) { - dbs := strings.Split(addrs, ",") - var queue = RedisQueue{ - queueName: "issue_indexer_queue", - indexer: indexer, - batchNumber: batchNumber, - } - if len(dbs) == 0 { - return nil, errors.New("no redis host found") - } else if len(dbs) == 1 { - queue.client = redis.NewClient(&redis.Options{ - Addr: strings.TrimSpace(dbs[0]), // use default Addr - Password: password, // no password set - DB: dbIdx, // use default DB - }) - } else { - queue.client = redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: dbs, - }) - } - if err := queue.client.Ping().Err(); err != nil { - return nil, err - } - return &queue, nil -} - -// Run runs the redis queue -func (r *RedisQueue) Run() error { - var i int - var datas = make([]*IndexerData, 0, r.batchNumber) - for { - bs, err := r.client.LPop(r.queueName).Bytes() - if err != nil && err != redis.Nil { - log.Error("LPop faile: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } - - i++ - if len(datas) > r.batchNumber || (len(datas) > 0 && i > 3) { - _ = r.indexer.Index(datas) - datas = make([]*IndexerData, 0, r.batchNumber) - i = 0 - } - - if len(bs) == 0 { - time.Sleep(time.Millisecond * 100) - continue - } - - var data IndexerData - err = json.Unmarshal(bs, &data) - if err != nil { - log.Error("Unmarshal: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } - - log.Trace("RedisQueue: task found: %#v", data) - - if data.IsDelete { - if data.ID > 0 { - if err = r.indexer.Delete(data.ID); err != nil { - log.Error("indexer.Delete: %v", err) - } - } else if len(data.IDs) > 0 { - if err = r.indexer.Delete(data.IDs...); err != nil { - log.Error("indexer.Delete: %v", err) - } - } - time.Sleep(time.Millisecond * 100) - continue - } - - datas = append(datas, &data) - time.Sleep(time.Millisecond * 100) - } -} - -// Push implements Queue -func (r *RedisQueue) Push(data *IndexerData) error { - bs, err := json.Marshal(data) - if err != nil { - return err - } - return r.client.RPush(r.queueName, bs).Err() -} From 4658b2f35e8f9058c9d0521910b784243a30b70e Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 16 Nov 2019 18:11:34 +0000 Subject: [PATCH 09/35] Task: Move to generic queue and gracefulise --- modules/setting/setting.go | 1 + modules/setting/task.go | 27 +++---- modules/task/queue.go | 14 ---- modules/task/queue_channel.go | 48 ------------- modules/task/queue_redis.go | 130 ---------------------------------- modules/task/task.go | 40 +++++------ 6 files changed, 30 insertions(+), 230 deletions(-) delete mode 100644 modules/task/queue.go delete mode 100644 modules/task/queue_channel.go delete mode 100644 modules/task/queue_redis.go diff --git a/modules/setting/setting.go b/modules/setting/setting.go index dbf43f31ee25..a7a916e9c2ea 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1090,4 +1090,5 @@ func NewServices() { newMigrationsService() newIndexerService() newTaskService() + newQueueService() } diff --git a/modules/setting/task.go b/modules/setting/task.go index 97704d4a4da6..fa63c669c662 100644 --- a/modules/setting/task.go +++ b/modules/setting/task.go @@ -4,22 +4,17 @@ package setting -var ( - // Task settings - Task = struct { - QueueType string - QueueLength int - QueueConnStr string - }{ - QueueType: ChannelQueueType, - QueueLength: 1000, - QueueConnStr: "addrs=127.0.0.1:6379 db=0", - } -) +import "code.gitea.io/gitea/modules/queue" func newTaskService() { - sec := Cfg.Section("task") - Task.QueueType = sec.Key("QUEUE_TYPE").MustString(ChannelQueueType) - Task.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) - Task.QueueConnStr = sec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0") + taskSec := Cfg.Section("task") + queueTaskSec := Cfg.Section("queue.task") + switch taskSec.Key("QUEUE_TYPE").MustString(ChannelQueueType) { + case ChannelQueueType: + queueTaskSec.Key("TYPE").MustString(string(queue.PersistableChannelQueueType)) + case RedisQueueType: + queueTaskSec.Key("TYPE").MustString(string(queue.RedisQueueType)) + } + queueTaskSec.Key("LENGTH").MustInt(taskSec.Key("QUEUE_LENGTH").MustInt(1000)) + queueTaskSec.Key("CONN_STR").MustString(taskSec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0")) } diff --git a/modules/task/queue.go b/modules/task/queue.go deleted file mode 100644 index ddee0b3d4627..000000000000 --- a/modules/task/queue.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2019 Gitea. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package task - -import "code.gitea.io/gitea/models" - -// Queue defines an interface to run task queue -type Queue interface { - Run() error - Push(*models.Task) error - Stop() -} diff --git a/modules/task/queue_channel.go b/modules/task/queue_channel.go deleted file mode 100644 index da541f47551f..000000000000 --- a/modules/task/queue_channel.go +++ /dev/null @@ -1,48 +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 task - -import ( - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" -) - -var ( - _ Queue = &ChannelQueue{} -) - -// ChannelQueue implements -type ChannelQueue struct { - queue chan *models.Task -} - -// NewChannelQueue create a memory channel queue -func NewChannelQueue(queueLen int) *ChannelQueue { - return &ChannelQueue{ - queue: make(chan *models.Task, queueLen), - } -} - -// Run starts to run the queue -func (c *ChannelQueue) Run() error { - for task := range c.queue { - err := Run(task) - if err != nil { - log.Error("Run task failed: %s", err.Error()) - } - } - return nil -} - -// Push will push the task ID to queue -func (c *ChannelQueue) Push(task *models.Task) error { - c.queue <- task - return nil -} - -// Stop stop the queue -func (c *ChannelQueue) Stop() { - close(c.queue) -} diff --git a/modules/task/queue_redis.go b/modules/task/queue_redis.go deleted file mode 100644 index 127de0cdbf1d..000000000000 --- a/modules/task/queue_redis.go +++ /dev/null @@ -1,130 +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 task - -import ( - "encoding/json" - "errors" - "strconv" - "strings" - "time" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - - "github.com/go-redis/redis" -) - -var ( - _ Queue = &RedisQueue{} -) - -type redisClient interface { - RPush(key string, args ...interface{}) *redis.IntCmd - LPop(key string) *redis.StringCmd - Ping() *redis.StatusCmd -} - -// RedisQueue redis queue -type RedisQueue struct { - client redisClient - queueName string - closeChan chan bool -} - -func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { - fields := strings.Fields(connStr) - for _, f := range fields { - items := strings.SplitN(f, "=", 2) - if len(items) < 2 { - continue - } - switch strings.ToLower(items[0]) { - case "addrs": - addrs = items[1] - case "password": - password = items[1] - case "db": - dbIdx, err = strconv.Atoi(items[1]) - if err != nil { - return - } - } - } - return -} - -// NewRedisQueue creates single redis or cluster redis queue -func NewRedisQueue(addrs string, password string, dbIdx int) (*RedisQueue, error) { - dbs := strings.Split(addrs, ",") - var queue = RedisQueue{ - queueName: "task_queue", - closeChan: make(chan bool), - } - if len(dbs) == 0 { - return nil, errors.New("no redis host found") - } else if len(dbs) == 1 { - queue.client = redis.NewClient(&redis.Options{ - Addr: strings.TrimSpace(dbs[0]), // use default Addr - Password: password, // no password set - DB: dbIdx, // use default DB - }) - } else { - // cluster will ignore db - queue.client = redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: dbs, - Password: password, - }) - } - if err := queue.client.Ping().Err(); err != nil { - return nil, err - } - return &queue, nil -} - -// Run starts to run the queue -func (r *RedisQueue) Run() error { - for { - select { - case <-r.closeChan: - return nil - case <-time.After(time.Millisecond * 100): - } - - bs, err := r.client.LPop(r.queueName).Bytes() - if err != nil { - if err != redis.Nil { - log.Error("LPop failed: %v", err) - } - time.Sleep(time.Millisecond * 100) - continue - } - - var task models.Task - err = json.Unmarshal(bs, &task) - if err != nil { - log.Error("Unmarshal task failed: %s", err.Error()) - } else { - err = Run(&task) - if err != nil { - log.Error("Run task failed: %s", err.Error()) - } - } - } -} - -// Push implements Queue -func (r *RedisQueue) Push(task *models.Task) error { - bs, err := json.Marshal(task) - if err != nil { - return err - } - return r.client.RPush(r.queueName, bs).Err() -} - -// Stop stop the queue -func (r *RedisQueue) Stop() { - r.closeChan <- true -} diff --git a/modules/task/task.go b/modules/task/task.go index 64744afe7a4c..852319d406fb 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -8,14 +8,16 @@ import ( "fmt" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" ) // taskQueue is a global queue of tasks -var taskQueue Queue +var taskQueue queue.Queue // Run a task func Run(t *models.Task) error { @@ -23,38 +25,32 @@ func Run(t *models.Task) error { case structs.TaskTypeMigrateRepo: return runMigrateTask(t) default: - return fmt.Errorf("Unknow task type: %d", t.Type) + return fmt.Errorf("Unknown task type: %d", t.Type) } } // Init will start the service to get all unfinished tasks and run them func Init() error { - switch setting.Task.QueueType { - case setting.ChannelQueueType: - taskQueue = NewChannelQueue(setting.Task.QueueLength) - case setting.RedisQueueType: - var err error - addrs, pass, idx, err := parseConnStr(setting.Task.QueueConnStr) - if err != nil { - return err - } - taskQueue, err = NewRedisQueue(addrs, pass, idx) - if err != nil { - return err - } - default: - return fmt.Errorf("Unsupported task queue type: %v", setting.Task.QueueType) + taskQueue = setting.CreateQueue("task", handle, &models.Task{}) + + if taskQueue == nil { + return fmt.Errorf("Unable to create Task Queue") } - go func() { - if err := taskQueue.Run(); err != nil { - log.Error("taskQueue.Run end failed: %v", err) - } - }() + go graceful.GetManager().RunWithShutdownFns(taskQueue.Run) return nil } +func handle(data ...queue.Data) { + for _, datum := range data { + task := datum.(*models.Task) + if err := Run(task); err != nil { + log.Error("Run task failed: %v", err) + } + } +} + // MigrateRepository add migration repository to task func MigrateRepository(doer, u *models.User, opts base.MigrateOptions) error { task, err := models.CreateMigrateTask(doer, u, opts) From cc123c3448fef93ccffb3457e5932e45bd5b7194 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 16 Dec 2019 20:51:28 +0000 Subject: [PATCH 10/35] Issues: Standardise the issues indexer queue settings --- modules/indexer/issues/indexer.go | 61 ++----------------------------- modules/setting/queue.go | 33 +++++++++++++++++ 2 files changed, 36 insertions(+), 58 deletions(-) diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 1fcef59f34f4..8f0593acfff1 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -6,7 +6,6 @@ package issues import ( "context" - "encoding/json" "os" "sync" "time" @@ -113,64 +112,10 @@ func InitIssueIndexer(syncReindex bool) { } } - queueType := queue.PersistableChannelQueueType - switch setting.Indexer.IssueQueueType { - case setting.LevelQueueType: - queueType = queue.LevelQueueType - case setting.ChannelQueueType: - queueType = queue.PersistableChannelQueueType - case setting.RedisQueueType: - queueType = queue.RedisQueueType - default: - log.Fatal("Unsupported indexer queue type: %v", - setting.Indexer.IssueQueueType) - } - - name := "issue_indexer_queue" - opts := make(map[string]interface{}) - opts["QueueLength"] = setting.Indexer.UpdateQueueLength - opts["BatchLength"] = setting.Indexer.IssueQueueBatchNumber - opts["DataDir"] = setting.Indexer.IssueQueueDir + issueIndexerQueue = setting.CreateQueue("issue_indexer", handler, &IndexerData{}) - addrs, password, dbIdx, err := setting.ParseQueueConnStr(setting.Indexer.IssueQueueConnStr) - if queueType == queue.RedisQueueType && err != nil { - log.Fatal("Unable to parse connection string for RedisQueueType: %s : %v", - setting.Indexer.IssueQueueConnStr, - err) - } - opts["Addresses"] = addrs - opts["Password"] = password - opts["DBIndex"] = dbIdx - opts["QueueName"] = name - opts["Name"] = name - opts["Workers"] = 1 - opts["BlockTimeout"] = 1 * time.Second - opts["BoostTimeout"] = 5 * time.Minute - opts["BoostWorkers"] = 5 - cfg, err := json.Marshal(opts) - if err != nil { - log.Error("Unable to marshall generic options: %v Error: %v", opts, err) - log.Fatal("Unable to create issue indexer queue with type %s: %v", - queueType, - err) - } - log.Debug("Creating issue indexer queue with type %s: configuration: %s", queueType, string(cfg)) - issueIndexerQueue, err = queue.CreateQueue(queueType, handler, cfg, &IndexerData{}) - if err != nil { - issueIndexerQueue, err = queue.CreateQueue(queue.WrappedQueueType, handler, queue.WrappedQueueConfiguration{ - Underlying: queueType, - Timeout: setting.GracefulHammerTime + 30*time.Second, - MaxAttempts: 10, - Config: cfg, - QueueLength: setting.Indexer.UpdateQueueLength, - Name: name, - }, &IndexerData{}) - } - if err != nil { - log.Fatal("Unable to create issue indexer queue with type %s: %v : %v", - queueType, - string(cfg), - err) + if issueIndexerQueue == nil { + log.Fatal("Unable to create issue indexer queue") } default: issueIndexerQueue = &queue.DummyQueue{} diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 0066d5a9467a..08f6eaf3ee57 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -6,6 +6,7 @@ package setting import ( "encoding/json" + "fmt" "path" "strconv" "strings" @@ -145,6 +146,38 @@ func newQueueService() { if !hasWorkers { Cfg.Section("queue.notification").Key("WORKERS").SetValue("5") } + + // Now handle the old issue_indexer configuration + section := Cfg.Section("queue.issue_indexer") + issueIndexerSectionMap := map[string]string{} + for _, key := range section.Keys() { + issueIndexerSectionMap[key.Name()] = key.Value() + } + if _, ok := issueIndexerSectionMap["TYPE"]; !ok { + switch Indexer.IssueQueueType { + case LevelQueueType: + section.Key("TYPE").SetValue("level") + case ChannelQueueType: + section.Key("TYPE").SetValue("persistable-channel") + case RedisQueueType: + section.Key("TYPE").SetValue("redis") + default: + log.Fatal("Unsupported indexer queue type: %v", + Indexer.IssueQueueType) + } + } + if _, ok := issueIndexerSectionMap["LENGTH"]; !ok { + section.Key("LENGTH").SetValue(fmt.Sprintf("%d", Indexer.UpdateQueueLength)) + } + if _, ok := issueIndexerSectionMap["BATCH_LENGTH"]; !ok { + section.Key("BATCH_LENGTH").SetValue(fmt.Sprintf("%d", Indexer.IssueQueueBatchNumber)) + } + if _, ok := issueIndexerSectionMap["DATADIR"]; !ok { + section.Key("DATADIR").SetValue(Indexer.IssueQueueDir) + } + if _, ok := issueIndexerSectionMap["CONN_STR"]; !ok { + section.Key("CONN_STR").SetValue(Indexer.IssueQueueConnStr) + } } // ParseQueueConnStr parses a queue connection string From 1013ced3267132d69d14b30b138c31cf1ce1866b Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 16 Dec 2019 22:05:31 +0000 Subject: [PATCH 11/35] Fix test --- modules/indexer/issues/indexer_test.go | 3 +++ modules/queue/queue_test.go | 9 +++++---- modules/setting/queue.go | 4 +++- modules/setting/setting.go | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index ca7ba29703fe..ecc12f79c8af 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/setting" + "gopkg.in/ini.v1" "github.com/stretchr/testify/assert" ) @@ -24,6 +25,7 @@ func TestMain(m *testing.M) { func TestBleveSearchIssues(t *testing.T) { assert.NoError(t, models.PrepareTestDatabase()) + setting.Cfg = ini.Empty() tmpIndexerDir, err := ioutil.TempDir("", "issues-indexer") if err != nil { @@ -41,6 +43,7 @@ func TestBleveSearchIssues(t *testing.T) { }() setting.Indexer.IssueType = "bleve" + setting.NewQueueService() InitIssueIndexer(true) defer func() { indexer := holder.get() diff --git a/modules/queue/queue_test.go b/modules/queue/queue_test.go index e41643da211c..3608f68d3d42 100644 --- a/modules/queue/queue_test.go +++ b/modules/queue/queue_test.go @@ -4,11 +4,12 @@ package queue -import "testing" +import ( + "encoding/json" + "testing" -import "github.com/stretchr/testify/assert" - -import "encoding/json" + "github.com/stretchr/testify/assert" +) type testData struct { TestString string diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 08f6eaf3ee57..5cbee851c64e 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -115,7 +115,9 @@ func getQueueSettings(name string) queueSettings { return q } -func newQueueService() { +// NewQueueService sets up the default settings for Queues +// This is exported for tests to be able to use the queue +func NewQueueService() { sec := Cfg.Section("queue") Queue.DataDir = sec.Key("DATADIR").MustString("queues/") if !path.IsAbs(Queue.DataDir) { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index a7a916e9c2ea..76609990892b 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1090,5 +1090,5 @@ func NewServices() { newMigrationsService() newIndexerService() newTaskService() - newQueueService() + NewQueueService() } From 1fb9104009ed8a14182e90b13022b86364553d05 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 17 Dec 2019 19:44:37 +0000 Subject: [PATCH 12/35] Queue: Allow Redis to connect to unix --- modules/queue/queue_redis.go | 2 ++ modules/setting/queue.go | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 4f2ceec029f0..724e22b7b5d5 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -41,6 +41,7 @@ type RedisQueue struct { // RedisQueueConfiguration is the configuration for the redis queue type RedisQueueConfiguration struct { + Network string Addresses string Password string DBIndex int @@ -88,6 +89,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) return nil, errors.New("no redis host found") } else if len(dbs) == 1 { queue.client = redis.NewClient(&redis.Options{ + Network: config.Network, Addr: strings.TrimSpace(dbs[0]), // use default Addr Password: config.Password, // no password set DB: config.DBIndex, // use default DB diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 5cbee851c64e..778ddeb217f3 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -22,6 +22,7 @@ type queueSettings struct { BatchLength int ConnectionString string Type string + Network string Addresses string Password string QueueName string @@ -47,6 +48,7 @@ func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) qu opts["BatchLength"] = q.BatchLength opts["DataDir"] = q.DataDir opts["Addresses"] = q.Addresses + opts["Network"] = q.Network opts["Password"] = q.Password opts["DBIndex"] = q.DBIndex opts["QueueName"] = q.QueueName @@ -111,7 +113,7 @@ func getQueueSettings(name string) queueSettings { q.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(Queue.BoostWorkers) q.QueueName = sec.Key("QUEUE_NAME").MustString(Queue.QueueName) - q.Addresses, q.Password, q.DBIndex, _ = ParseQueueConnStr(q.ConnectionString) + q.Network, q.Addresses, q.Password, q.DBIndex, _ = ParseQueueConnStr(q.ConnectionString) return q } @@ -128,7 +130,7 @@ func NewQueueService() { Queue.ConnectionString = sec.Key("CONN_STR").MustString(path.Join(AppDataPath, "")) validTypes := queue.RegisteredTypesAsString() Queue.Type = sec.Key("TYPE").In(string(queue.PersistableChannelQueueType), validTypes) - Queue.Addresses, Queue.Password, Queue.DBIndex, _ = ParseQueueConnStr(Queue.ConnectionString) + Queue.Network, Queue.Addresses, Queue.Password, Queue.DBIndex, _ = ParseQueueConnStr(Queue.ConnectionString) Queue.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(true) Queue.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(10) Queue.Timeout = sec.Key("TIMEOUT").MustDuration(GracefulHammerTime + 30*time.Second) @@ -183,7 +185,7 @@ func NewQueueService() { } // ParseQueueConnStr parses a queue connection string -func ParseQueueConnStr(connStr string) (addrs, password string, dbIdx int, err error) { +func ParseQueueConnStr(connStr string) (network, addrs, password string, dbIdx int, err error) { fields := strings.Fields(connStr) for _, f := range fields { items := strings.SplitN(f, "=", 2) @@ -191,6 +193,8 @@ func ParseQueueConnStr(connStr string) (addrs, password string, dbIdx int, err e continue } switch strings.ToLower(items[0]) { + case "network": + network = items[1] case "addrs": addrs = items[1] case "password": From a492b3071c7adc9d6999551c8a8425426ed1ead0 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 23 Dec 2019 10:29:36 +0000 Subject: [PATCH 13/35] Prevent deadlock during early shutdown of issue indexer --- modules/indexer/issues/indexer.go | 45 +++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 8f0593acfff1..8676561cf134 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -6,6 +6,7 @@ package issues import ( "context" + "fmt" "os" "sync" "time" @@ -51,9 +52,10 @@ type Indexer interface { } type indexerHolder struct { - indexer Indexer - mutex sync.RWMutex - cond *sync.Cond + indexer Indexer + mutex sync.RWMutex + cond *sync.Cond + cancelled bool } func newIndexerHolder() *indexerHolder { @@ -62,6 +64,13 @@ func newIndexerHolder() *indexerHolder { return h } +func (h *indexerHolder) cancel() { + h.mutex.Lock() + defer h.mutex.Unlock() + h.cancelled = true + h.cond.Broadcast() +} + func (h *indexerHolder) set(indexer Indexer) { h.mutex.Lock() defer h.mutex.Unlock() @@ -72,7 +81,7 @@ func (h *indexerHolder) set(indexer Indexer) { func (h *indexerHolder) get() Indexer { h.mutex.RLock() defer h.mutex.RUnlock() - if h.indexer == nil { + if h.indexer == nil && !h.cancelled { h.cond.Wait() } return h.indexer @@ -93,6 +102,12 @@ func InitIssueIndexer(syncReindex bool) { switch setting.Indexer.IssueType { case "bleve": handler := func(data ...queue.Data) { + indexer := holder.get() + if indexer == nil { + log.Error("Unable to get indexer!") + return + } + iData := make([]*IndexerData, 0, setting.Indexer.IssueQueueBatchNumber) for _, datum := range data { indexerData, ok := datum.(*IndexerData) @@ -102,12 +117,12 @@ func InitIssueIndexer(syncReindex bool) { } log.Trace("IndexerData Process: %d %v %t", indexerData.ID, indexerData.IDs, indexerData.IsDelete) if indexerData.IsDelete { - _ = holder.get().Delete(indexerData.IDs...) + _ = indexer.Delete(indexerData.IDs...) continue } iData = append(iData, indexerData) } - if err := holder.get().Index(iData); err != nil { + if err := indexer.Index(iData); err != nil { log.Error("Error whilst indexing: %v Error: %v", iData, err) } } @@ -132,6 +147,7 @@ func InitIssueIndexer(syncReindex bool) { issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath) exist, err := issueIndexer.Init() if err != nil { + holder.cancel() log.Fatal("Unable to initialize Bleve Issue Indexer: %v", err) } populate = !exist @@ -153,6 +169,7 @@ func InitIssueIndexer(syncReindex bool) { issueIndexer := &DBIndexer{} holder.set(issueIndexer) default: + holder.cancel() log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) } @@ -168,10 +185,14 @@ func InitIssueIndexer(syncReindex bool) { } } waitChannel <- time.Since(start) + close(waitChannel) }() if syncReindex { - <-waitChannel + select { + case <-waitChannel: + case <-graceful.GetManager().IsShutdown(): + } } else if setting.Indexer.StartupTimeout > 0 { go func() { timeout := setting.Indexer.StartupTimeout @@ -181,6 +202,8 @@ func InitIssueIndexer(syncReindex bool) { select { case duration := <-waitChannel: log.Info("Issue Indexer Initialization took %v", duration) + case <-graceful.GetManager().IsShutdown(): + log.Warn("Shutdown occurred before issue index initialisation was complete") case <-time.After(timeout): if shutdownable, ok := issueIndexerQueue.(queue.Shutdownable); ok { shutdownable.Terminate() @@ -293,7 +316,13 @@ func DeleteRepoIssueIndexer(repo *models.Repository) { // SearchIssuesByKeyword search issue ids by keywords and repo id func SearchIssuesByKeyword(repoIDs []int64, keyword string) ([]int64, error) { var issueIDs []int64 - res, err := holder.get().Search(keyword, repoIDs, 1000, 0) + indexer := holder.get() + + if indexer == nil { + log.Error("Unable to get indexer!") + return nil, fmt.Errorf("unable to get issue indexer") + } + res, err := indexer.Search(keyword, repoIDs, 1000, 0) if err != nil { return nil, err } From b1c9fa7f1a6f73982462fa99dd411fbf1dd14f04 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 30 Dec 2019 15:54:19 +0000 Subject: [PATCH 14/35] Add MaxWorker settings to queues --- modules/queue/manager.go | 100 +++++++++++++++++------ modules/queue/queue_channel.go | 20 +++-- modules/queue/queue_channel_test.go | 2 + modules/queue/queue_disk.go | 20 +++-- modules/queue/queue_disk_channel.go | 5 +- modules/queue/queue_disk_channel_test.go | 2 + modules/queue/queue_disk_test.go | 2 + modules/queue/queue_redis.go | 20 +++-- modules/queue/queue_wrapped.go | 2 +- modules/queue/workerpool.go | 87 ++++++++++++++++---- modules/setting/queue.go | 4 + options/locale/locale_en-US.ini | 22 ++++- routers/admin/admin.go | 68 +++++++++++++++ routers/routes/routes.go | 1 + templates/admin/queue.tmpl | 30 +++++++ 15 files changed, 312 insertions(+), 73 deletions(-) diff --git a/modules/queue/manager.go b/modules/queue/manager.go index 81478019e533..a2deb8ff7cfe 100644 --- a/modules/queue/manager.go +++ b/modules/queue/manager.go @@ -28,17 +28,28 @@ type Manager struct { // Description represents a working queue inheriting from Gitea. type Description struct { - mutex sync.Mutex - QID int64 - Queue Queue - Type Type - Name string - Configuration interface{} - ExemplarType string - addWorkers func(number int, timeout time.Duration) context.CancelFunc - numberOfWorkers func() int - counter int64 - PoolWorkers map[int64]*PoolWorkers + mutex sync.Mutex + QID int64 + Queue Queue + Type Type + Name string + Configuration interface{} + ExemplarType string + Pool PoolManager + counter int64 + PoolWorkers map[int64]*PoolWorkers +} + +// PoolManager is a simple interface to get certain details from a worker pool +type PoolManager interface { + AddWorkers(number int, timeout time.Duration) context.CancelFunc + NumberOfWorkers() int + MaxNumberOfWorkers() int + SetMaxNumberOfWorkers(int) + BoostTimeout() time.Duration + BlockTimeout() time.Duration + BoostWorkers() int + SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) } // DescriptionList implements the sort.Interface @@ -76,18 +87,16 @@ func (m *Manager) Add(queue Queue, t Type, configuration, exemplar interface{}, - addWorkers func(number int, timeout time.Duration) context.CancelFunc, - numberOfWorkers func() int) int64 { + pool PoolManager) int64 { cfg, _ := json.Marshal(configuration) desc := &Description{ - Queue: queue, - Type: t, - Configuration: string(cfg), - ExemplarType: reflect.TypeOf(exemplar).String(), - PoolWorkers: make(map[int64]*PoolWorkers), - addWorkers: addWorkers, - numberOfWorkers: numberOfWorkers, + Queue: queue, + Type: t, + Configuration: string(cfg), + ExemplarType: reflect.TypeOf(exemplar).String(), + PoolWorkers: make(map[int64]*PoolWorkers), + Pool: pool, } m.mutex.Lock() m.counter++ @@ -177,20 +186,61 @@ func (q *Description) RemoveWorkers(pid int64) { } // AddWorkers adds workers to the queue if it has registered an add worker function -func (q *Description) AddWorkers(number int, timeout time.Duration) { - if q.addWorkers != nil { - _ = q.addWorkers(number, timeout) +func (q *Description) AddWorkers(number int, timeout time.Duration) context.CancelFunc { + if q.Pool != nil { + // the cancel will be added to the pool workers description above + return q.Pool.AddWorkers(number, timeout) } + return nil } // NumberOfWorkers returns the number of workers in the queue func (q *Description) NumberOfWorkers() int { - if q.numberOfWorkers != nil { - return q.numberOfWorkers() + if q.Pool != nil { + return q.Pool.NumberOfWorkers() + } + return -1 +} + +// MaxNumberOfWorkers returns the maximum number of workers for the pool +func (q *Description) MaxNumberOfWorkers() int { + if q.Pool != nil { + return q.Pool.MaxNumberOfWorkers() + } + return 0 +} + +// BoostWorkers returns the number of workers for a boost +func (q *Description) BoostWorkers() int { + if q.Pool != nil { + return q.Pool.BoostWorkers() } return -1 } +// BoostTimeout returns the timeout of the next boost +func (q *Description) BoostTimeout() time.Duration { + if q.Pool != nil { + return q.Pool.BoostTimeout() + } + return 0 +} + +// BlockTimeout returns the timeout til the next boost +func (q *Description) BlockTimeout() time.Duration { + if q.Pool != nil { + return q.Pool.BlockTimeout() + } + return 0 +} + +// SetSettings sets the setable boost values +func (q *Description) SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) { + if q.Pool != nil { + q.Pool.SetSettings(maxNumberOfWorkers, boostWorkers, timeout) + } +} + func (l DescriptionList) Len() int { return len(l) } diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go index 265a5c88f10e..5f41ef75746f 100644 --- a/modules/queue/queue_channel.go +++ b/modules/queue/queue_channel.go @@ -21,6 +21,7 @@ type ChannelQueueConfiguration struct { QueueLength int BatchLength int Workers int + MaxWorkers int BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int @@ -50,20 +51,21 @@ func NewChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro ctx, cancel := context.WithCancel(context.Background()) queue := &ChannelQueue{ pool: &WorkerPool{ - baseCtx: ctx, - cancel: cancel, - batchLength: config.BatchLength, - handle: handle, - dataChan: dataChan, - blockTimeout: config.BlockTimeout, - boostTimeout: config.BoostTimeout, - boostWorkers: config.BoostWorkers, + baseCtx: ctx, + cancel: cancel, + batchLength: config.BatchLength, + handle: handle, + dataChan: dataChan, + blockTimeout: config.BlockTimeout, + boostTimeout: config.BoostTimeout, + boostWorkers: config.BoostWorkers, + maxNumberOfWorkers: config.MaxWorkers, }, exemplar: exemplar, workers: config.Workers, name: config.Name, } - queue.pool.qid = GetManager().Add(queue, ChannelQueueType, config, exemplar, queue.pool.AddWorkers, queue.pool.NumberOfWorkers) + queue.pool.qid = GetManager().Add(queue, ChannelQueueType, config, exemplar, queue.pool) return queue, nil } diff --git a/modules/queue/queue_channel_test.go b/modules/queue/queue_channel_test.go index c04407aa243f..fafc1e3303eb 100644 --- a/modules/queue/queue_channel_test.go +++ b/modules/queue/queue_channel_test.go @@ -27,6 +27,7 @@ func TestChannelQueue(t *testing.T) { ChannelQueueConfiguration{ QueueLength: 20, Workers: 1, + MaxWorkers: 10, BlockTimeout: 1 * time.Second, BoostTimeout: 5 * time.Minute, BoostWorkers: 5, @@ -62,6 +63,7 @@ func TestChannelQueue_Batch(t *testing.T) { QueueLength: 20, BatchLength: 2, Workers: 1, + MaxWorkers: 10, BlockTimeout: 1 * time.Second, BoostTimeout: 5 * time.Minute, BoostWorkers: 5, diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index 41e8a9e7c0b7..b74ce378b07b 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -25,6 +25,7 @@ type LevelQueueConfiguration struct { QueueLength int BatchLength int Workers int + MaxWorkers int BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int @@ -60,14 +61,15 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) queue := &LevelQueue{ pool: &WorkerPool{ - baseCtx: ctx, - cancel: cancel, - batchLength: config.BatchLength, - handle: handle, - dataChan: dataChan, - blockTimeout: config.BlockTimeout, - boostTimeout: config.BoostTimeout, - boostWorkers: config.BoostWorkers, + baseCtx: ctx, + cancel: cancel, + batchLength: config.BatchLength, + handle: handle, + dataChan: dataChan, + blockTimeout: config.BlockTimeout, + boostTimeout: config.BoostTimeout, + boostWorkers: config.BoostWorkers, + maxNumberOfWorkers: config.MaxWorkers, }, queue: internal, exemplar: exemplar, @@ -76,7 +78,7 @@ func NewLevelQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) workers: config.Workers, name: config.Name, } - queue.pool.qid = GetManager().Add(queue, LevelQueueType, config, exemplar, queue.pool.AddWorkers, queue.pool.NumberOfWorkers) + queue.pool.qid = GetManager().Add(queue, LevelQueueType, config, exemplar, queue.pool) return queue, nil } diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index 884fc410df92..e2ee0bda566f 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -23,6 +23,7 @@ type PersistableChannelQueueConfiguration struct { Timeout time.Duration MaxAttempts int Workers int + MaxWorkers int BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int @@ -48,6 +49,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( QueueLength: config.QueueLength, BatchLength: config.BatchLength, Workers: config.Workers, + MaxWorkers: config.MaxWorkers, BlockTimeout: config.BlockTimeout, BoostTimeout: config.BoostTimeout, BoostWorkers: config.BoostWorkers, @@ -63,6 +65,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( QueueLength: config.QueueLength, BatchLength: config.BatchLength, Workers: 1, + MaxWorkers: 6, BlockTimeout: 1 * time.Second, BoostTimeout: 5 * time.Minute, BoostWorkers: 5, @@ -96,7 +99,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( }, closed: make(chan struct{}), } - _ = GetManager().Add(queue, PersistableChannelQueueType, config, exemplar, nil, nil) + _ = GetManager().Add(queue, PersistableChannelQueueType, config, exemplar, nil) return queue, nil } diff --git a/modules/queue/queue_disk_channel_test.go b/modules/queue/queue_disk_channel_test.go index 01a90ebcfb8a..4ef68961c6fe 100644 --- a/modules/queue/queue_disk_channel_test.go +++ b/modules/queue/queue_disk_channel_test.go @@ -36,6 +36,7 @@ func TestPersistableChannelQueue(t *testing.T) { BatchLength: 2, QueueLength: 20, Workers: 1, + MaxWorkers: 10, }, &testData{}) assert.NoError(t, err) @@ -89,6 +90,7 @@ func TestPersistableChannelQueue(t *testing.T) { BatchLength: 2, QueueLength: 20, Workers: 1, + MaxWorkers: 10, }, &testData{}) assert.NoError(t, err) diff --git a/modules/queue/queue_disk_test.go b/modules/queue/queue_disk_test.go index 03de451760a0..c5959d606fdd 100644 --- a/modules/queue/queue_disk_test.go +++ b/modules/queue/queue_disk_test.go @@ -35,6 +35,7 @@ func TestLevelQueue(t *testing.T) { DataDir: tmpDir, BatchLength: 2, Workers: 1, + MaxWorkers: 10, QueueLength: 20, BlockTimeout: 1 * time.Second, BoostTimeout: 5 * time.Minute, @@ -94,6 +95,7 @@ func TestLevelQueue(t *testing.T) { DataDir: tmpDir, BatchLength: 2, Workers: 1, + MaxWorkers: 10, QueueLength: 20, BlockTimeout: 1 * time.Second, BoostTimeout: 5 * time.Minute, diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 724e22b7b5d5..21fa4462b8cb 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -49,6 +49,7 @@ type RedisQueueConfiguration struct { QueueLength int QueueName string Workers int + MaxWorkers int BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int @@ -70,14 +71,15 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) var queue = &RedisQueue{ pool: &WorkerPool{ - baseCtx: ctx, - cancel: cancel, - batchLength: config.BatchLength, - handle: handle, - dataChan: dataChan, - blockTimeout: config.BlockTimeout, - boostTimeout: config.BoostTimeout, - boostWorkers: config.BoostWorkers, + baseCtx: ctx, + cancel: cancel, + batchLength: config.BatchLength, + handle: handle, + dataChan: dataChan, + blockTimeout: config.BlockTimeout, + boostTimeout: config.BoostTimeout, + boostWorkers: config.BoostWorkers, + maxNumberOfWorkers: config.MaxWorkers, }, queueName: config.QueueName, exemplar: exemplar, @@ -102,7 +104,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) if err := queue.client.Ping().Err(); err != nil { return nil, err } - queue.pool.qid = GetManager().Add(queue, RedisQueueType, config, exemplar, queue.pool.AddWorkers, queue.pool.NumberOfWorkers) + queue.pool.qid = GetManager().Add(queue, RedisQueueType, config, exemplar, queue.pool) return queue, nil } diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go index 46557ea31899..c218749b6526 100644 --- a/modules/queue/queue_wrapped.go +++ b/modules/queue/queue_wrapped.go @@ -123,7 +123,7 @@ func NewWrappedQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro name: config.Name, }, } - _ = GetManager().Add(queue, WrappedQueueType, config, exemplar, nil, nil) + _ = GetManager().Add(queue, WrappedQueueType, config, exemplar, nil) return queue, nil } diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go index fe05e7fe6ec2..98a68cd0411e 100644 --- a/modules/queue/workerpool.go +++ b/modules/queue/workerpool.go @@ -14,24 +14,25 @@ import ( // WorkerPool takes type WorkerPool struct { - lock sync.Mutex - baseCtx context.Context - cancel context.CancelFunc - cond *sync.Cond - qid int64 - numberOfWorkers int - batchLength int - handle HandlerFunc - dataChan chan Data - blockTimeout time.Duration - boostTimeout time.Duration - boostWorkers int + lock sync.Mutex + baseCtx context.Context + cancel context.CancelFunc + cond *sync.Cond + qid int64 + maxNumberOfWorkers int + 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 { + if p.blockTimeout > 0 && p.boostTimeout > 0 && (p.numberOfWorkers <= p.maxNumberOfWorkers || p.maxNumberOfWorkers < 0) { p.lock.Unlock() p.pushBoost(data) } else { @@ -63,7 +64,7 @@ func (p *WorkerPool) pushBoost(data Data) { } case <-timer.C: p.lock.Lock() - if p.blockTimeout > ourTimeout { + if p.blockTimeout > ourTimeout || (p.numberOfWorkers > p.maxNumberOfWorkers && p.maxNumberOfWorkers >= 0) { p.lock.Unlock() p.dataChan <- data return @@ -71,11 +72,15 @@ func (p *WorkerPool) pushBoost(data Data) { p.blockTimeout *= 2 ctx, cancel := context.WithCancel(p.baseCtx) desc := GetManager().GetDescription(p.qid) + boost := p.boostWorkers + if (boost+p.numberOfWorkers) > p.maxNumberOfWorkers && p.maxNumberOfWorkers >= 0 { + boost = p.maxNumberOfWorkers - p.numberOfWorkers + } if desc != nil { - log.Warn("WorkerPool: %d (for %s) Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", p.qid, desc.Name, ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) + log.Warn("WorkerPool: %d (for %s) Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", p.qid, desc.Name, ourTimeout, boost, p.boostTimeout, p.blockTimeout) start := time.Now() - pid := desc.RegisterWorkers(p.boostWorkers, start, false, start, cancel) + pid := desc.RegisterWorkers(boost, start, false, start, cancel) go func() { <-ctx.Done() desc.RemoveWorkers(pid) @@ -91,7 +96,7 @@ func (p *WorkerPool) pushBoost(data Data) { p.blockTimeout /= 2 p.lock.Unlock() }() - p.addWorkers(ctx, p.boostWorkers) + p.addWorkers(ctx, boost) p.lock.Unlock() p.dataChan <- data } @@ -105,7 +110,53 @@ func (p *WorkerPool) NumberOfWorkers() int { return p.numberOfWorkers } -// AddWorkers adds workers to the pool +// MaxNumberOfWorkers returns the maximum number of workers automatically added to the pool +func (p *WorkerPool) MaxNumberOfWorkers() int { + p.lock.Lock() + defer p.lock.Unlock() + return p.maxNumberOfWorkers +} + +// BoostWorkers returns the number of workers for a boost +func (p *WorkerPool) BoostWorkers() int { + p.lock.Lock() + defer p.lock.Unlock() + return p.boostWorkers +} + +// BoostTimeout returns the timeout of the next boost +func (p *WorkerPool) BoostTimeout() time.Duration { + p.lock.Lock() + defer p.lock.Unlock() + return p.boostTimeout +} + +// BlockTimeout returns the timeout til the next boost +func (p *WorkerPool) BlockTimeout() time.Duration { + p.lock.Lock() + defer p.lock.Unlock() + return p.blockTimeout +} + +// SetSettings sets the setable boost values +func (p *WorkerPool) SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) { + p.lock.Lock() + defer p.lock.Unlock() + p.maxNumberOfWorkers = maxNumberOfWorkers + p.boostWorkers = boostWorkers + p.boostTimeout = timeout +} + +// SetMaxNumberOfWorkers sets the maximum number of workers automatically added to the pool +// Changing this number will not change the number of current workers but will change the limit +// for future additions +func (p *WorkerPool) SetMaxNumberOfWorkers(newMax int) { + p.lock.Lock() + defer p.lock.Unlock() + p.maxNumberOfWorkers = newMax +} + +// AddWorkers adds workers to the pool - this allows the number of workers to go above the limit func (p *WorkerPool) AddWorkers(number int, timeout time.Duration) context.CancelFunc { var ctx context.Context var cancel context.CancelFunc diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 778ddeb217f3..017083439138 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -31,6 +31,7 @@ type queueSettings struct { MaxAttempts int Timeout time.Duration Workers int + MaxWorkers int BlockTimeout time.Duration BoostTimeout time.Duration BoostWorkers int @@ -53,6 +54,7 @@ func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) qu opts["DBIndex"] = q.DBIndex opts["QueueName"] = q.QueueName opts["Workers"] = q.Workers + opts["MaxWorkers"] = q.MaxWorkers opts["BlockTimeout"] = q.BlockTimeout opts["BoostTimeout"] = q.BoostTimeout opts["BoostWorkers"] = q.BoostWorkers @@ -108,6 +110,7 @@ func getQueueSettings(name string) queueSettings { q.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(Queue.MaxAttempts) q.Timeout = sec.Key("TIMEOUT").MustDuration(Queue.Timeout) q.Workers = sec.Key("WORKERS").MustInt(Queue.Workers) + q.MaxWorkers = sec.Key("MAX_WORKERS").MustInt(Queue.MaxWorkers) q.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(Queue.BlockTimeout) q.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(Queue.BoostTimeout) q.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(Queue.BoostWorkers) @@ -135,6 +138,7 @@ func NewQueueService() { Queue.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(10) Queue.Timeout = sec.Key("TIMEOUT").MustDuration(GracefulHammerTime + 30*time.Second) Queue.Workers = sec.Key("WORKERS").MustInt(1) + Queue.MaxWorkers = sec.Key("MAX_WORKERS").MustInt(10) Queue.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(1 * time.Second) Queue.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(5 * time.Minute) Queue.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(5) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index d6a96b55a5dd..4e30cf08f66f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1408,7 +1408,7 @@ settings.protect_check_status_contexts_list = Status checks found in the last we settings.protect_required_approvals = Required approvals: settings.protect_required_approvals_desc = Allow only to merge pull request with enough positive reviews. settings.protect_approvals_whitelist_enabled = Restrict approvals to whitelisted users or teams -settings.protect_approvals_whitelist_enabled_desc = Only reviews from whitelisted users or teams will count to the required approvals. Without approval whitelist, reviews from anyone with write access count to the required approvals. +settings.protect_approvals_whitelist_enabled_desc = Only reviews from whitelisted users or teams will count to the required approvals. Without approval whitelist, reviews from anyone with write access count to the required approvals. settings.protect_approvals_whitelist_users = Whitelisted reviewers: settings.protect_approvals_whitelist_teams = Whitelisted teams for reviews: settings.add_protected_branch = Enable protection @@ -2028,6 +2028,7 @@ monitor.queue.name = Name monitor.queue.type = Type monitor.queue.exemplar = Exemplar Type monitor.queue.numberworkers = Number of Workers +monitor.queue.maxnumberworkers = Max Number of Workers monitor.queue.review = Review Config monitor.queue.review_add = Review/Add Workers monitor.queue.configuration = Initial Configuration @@ -2043,7 +2044,26 @@ monitor.queue.pool.addworkers.numberworkers.placeholder = Number of Workers monitor.queue.pool.addworkers.timeout.placeholder = Set to 0 for no timeout monitor.queue.pool.addworkers.mustnumbergreaterzero = Number of Workers to add must be greater than zero monitor.queue.pool.addworkers.musttimeoutduration = Timeout must be a golang duration eg. 5m or be 0 + +monitor.queue.settings.title = Pool Settings +monitor.queue.settings.desc = Pools dynamically grow with a boost in response to their worker queue blocking. These changes will not affect current worker groups. +monitor.queue.settings.timeout = Boost Timeout +monitor.queue.settings.timeout.placeholder = Currently %[1]v +monitor.queue.settings.timeout.error = Timeout must be a golang duration eg. 5m or be 0 +monitor.queue.settings.numberworkers = Boost Number of Workers +monitor.queue.settings.numberworkers.placeholder = Currently %[1]d +monitor.queue.settings.numberworkers.error = Number of Workers to add must be greater than or equal to zero +monitor.queue.settings.maxnumberworkers = Max Number of workers +monitor.queue.settings.maxnumberworkers.placeholder = Currently %[1]d +monitor.queue.settings.maxnumberworkers.error = Max number of workers must be a number +monitor.queue.settings.submit = Change Settings +monitor.queue.settings.changed = Settings Updated +monitor.queue.settings.blocktimeout = Current Block Timeout +monitor.queue.settings.blocktimeout.value = %[1]v + +monitor.queue.pool.none = This queue does not have a Pool monitor.queue.pool.added = Worker Group Added +monitor.queue.pool.max_changed = Maximum number of workers changed monitor.queue.pool.workers.title = Active Worker Groups monitor.queue.pool.workers.none = No worker groups. monitor.queue.pool.cancel = Shutdown Worker Group diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 7fc57edf312a..299da8b46c3e 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "runtime" + "strconv" "strings" "time" @@ -421,7 +422,74 @@ func AddWorkers(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) return } + if desc.Pool == nil { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } desc.AddWorkers(number, timeout) ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added")) ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) } + +// SetQueueSettings sets the maximum number of workers for this queue +func SetQueueSettings(ctx *context.Context) { + qid := ctx.ParamsInt64("qid") + desc := queue.GetManager().GetDescription(qid) + if desc == nil { + ctx.Status(404) + return + } + if desc.Pool == nil { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + + maxNumberStr := ctx.Query("max-number") + numberStr := ctx.Query("number") + timeoutStr := ctx.Query("timeout") + + var err error + var maxNumber, number int + var timeout time.Duration + if len(maxNumberStr) > 0 { + maxNumber, err = strconv.Atoi(maxNumberStr) + if err != nil { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.maxnumberworkers.error")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + if maxNumber < -1 { + maxNumber = -1 + } + } else { + maxNumber = desc.MaxNumberOfWorkers() + } + + if len(numberStr) > 0 { + number, err = strconv.Atoi(numberStr) + if err != nil || number < 0 { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.numberworkers.error")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + } else { + number = desc.BoostWorkers() + } + + if len(timeoutStr) > 0 { + timeout, err = time.ParseDuration(timeoutStr) + if err != nil { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.timeout.error")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + } else { + timeout = desc.Pool.BoostTimeout() + } + + desc.SetSettings(maxNumber, number, timeout) + ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index e97a932692ab..11cc2975808c 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -416,6 +416,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/cancel/:pid", admin.MonitorCancel) m.Group("/queue/:qid", func() { m.Get("", admin.Queue) + m.Post("/set", admin.SetQueueSettings) m.Post("/add", admin.AddWorkers) m.Post("/cancel/:pid", admin.WorkerCancel) }) diff --git a/templates/admin/queue.tmpl b/templates/admin/queue.tmpl index ab8422824361..4f422210e756 100644 --- a/templates/admin/queue.tmpl +++ b/templates/admin/queue.tmpl @@ -14,6 +14,7 @@ {{.i18n.Tr "admin.monitor.queue.type"}} {{.i18n.Tr "admin.monitor.queue.exemplar"}} {{.i18n.Tr "admin.monitor.queue.numberworkers"}} + {{.i18n.Tr "admin.monitor.queue.maxnumberworkers"}} @@ -22,6 +23,7 @@ {{.Queue.Type}} {{.Queue.ExemplarType}} {{$sum := .Queue.NumberOfWorkers}}{{if lt $sum 0}}-{{else}}{{$sum}}{{end}} + {{if lt $sum 0}}-{{else}}{{.Queue.MaxNumberOfWorkers}}{{end}} @@ -40,6 +42,34 @@ {{end}} {{else}} +

+ {{.i18n.Tr "admin.monitor.queue.settings.title"}} +

+
+

{{.i18n.Tr "admin.monitor.queue.settings.desc"}}

+
+ {{$.CsrfTokenHtml}} +
+
+ + +
+
+ + +
+
+ + +
+
+ + {{.i18n.Tr "admin.monitor.queue.settings.blocktimeout.value" .Queue.BlockTimeout}} +
+ +
+
+

{{.i18n.Tr "admin.monitor.queue.pool.addworkers.title"}}

From 4d8b8ed02ea56e7d8e3b1bd3972fb87b73492789 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 30 Dec 2019 17:56:12 +0000 Subject: [PATCH 15/35] Merge branch 'master' into graceful-queues --- modules/indexer/issues/bleve.go | 5 ----- modules/indexer/issues/db.go | 3 +-- modules/indexer/issues/indexer.go | 7 ++----- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/modules/indexer/issues/bleve.go b/modules/indexer/issues/bleve.go index b9f505e4bfe0..787ff0dec5a1 100644 --- a/modules/indexer/issues/bleve.go +++ b/modules/indexer/issues/bleve.go @@ -266,8 +266,3 @@ func (b *BleveIndexer) Search(keyword string, repoIDs []int64, limit, start int) } return &ret, nil } - -// Close the Index -func (b *BleveIndexer) Close() error { - return b.indexer.Close() -} diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go index 2a5df80fac2e..d0cca4fd1808 100644 --- a/modules/indexer/issues/db.go +++ b/modules/indexer/issues/db.go @@ -26,8 +26,7 @@ func (db *DBIndexer) Delete(ids ...int64) error { } // Close dummy function -func (db *DBIndexer) Close() error { - return nil +func (db *DBIndexer) Close() { } // Search dummy function diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 8676561cf134..34764b20f0b8 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -48,7 +48,7 @@ type Indexer interface { Index(issue []*IndexerData) error Delete(ids ...int64) error Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error) - Close() error + Close() } type indexerHolder struct { @@ -156,10 +156,7 @@ func InitIssueIndexer(syncReindex bool) { log.Debug("Closing issue indexer") issueIndexer := holder.get() if issueIndexer != nil { - err := issueIndexer.Close() - if err != nil { - log.Error("Error whilst closing the issue indexer: %v", err) - } + issueIndexer.Close() } log.Info("PID: %d Issue Indexer closed", os.Getpid()) }) From a763ccada1099a722351894df30153d2992e5522 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 31 Dec 2019 11:39:58 +0000 Subject: [PATCH 16/35] Update modules/indexer/issues/indexer.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- modules/indexer/issues/indexer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 34764b20f0b8..b765c03ccb20 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -104,7 +104,7 @@ func InitIssueIndexer(syncReindex bool) { handler := func(data ...queue.Data) { indexer := holder.get() if indexer == nil { - log.Error("Unable to get indexer!") + log.Error("Issue indexer handler: unable to get indexer!") return } From 632757bfa7b656e1d31dce3b229a5826f241795f Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 31 Dec 2019 12:16:39 +0000 Subject: [PATCH 17/35] Update modules/indexer/issues/indexer.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- modules/indexer/issues/indexer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index b765c03ccb20..6c89a9708a6c 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -316,7 +316,7 @@ func SearchIssuesByKeyword(repoIDs []int64, keyword string) ([]int64, error) { indexer := holder.get() if indexer == nil { - log.Error("Unable to get indexer!") + log.Error("SearchIssuesByKeyword(): unable to get indexer!") return nil, fmt.Errorf("unable to get issue indexer") } res, err := indexer.Search(keyword, repoIDs, 1000, 0) From 6c6d3eae764e5faf5bcae08ff54a6c80c95a4605 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 31 Dec 2019 12:17:18 +0000 Subject: [PATCH 18/35] Update modules/queue/queue_channel.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- modules/queue/queue_channel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go index 5f41ef75746f..c8f8a53804e7 100644 --- a/modules/queue/queue_channel.go +++ b/modules/queue/queue_channel.go @@ -82,7 +82,7 @@ func (c *ChannelQueue) Run(atShutdown, atTerminate func(context.Context, func()) }() } -// Push will push the indexer data to queue +// Push will push data into the queue func (c *ChannelQueue) Push(data Data) error { if c.exemplar != nil { // Assert data is of same type as r.exemplar From 402e4dfe9623f747aa815064a28eb58c4e885fb2 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 31 Dec 2019 12:27:04 +0000 Subject: [PATCH 19/35] Update modules/queue/queue_disk.go --- modules/queue/queue_disk.go | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index b74ce378b07b..550e78b97ee1 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -148,7 +148,6 @@ func (l *LevelQueue) readToChan() { log.Trace("LevelQueue %s: Task found: %#v", l.name, data) l.pool.Push(data) - time.Sleep(time.Millisecond * 10) } } From a10129f74c23258910203b2efa6658c20a48d616 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 31 Dec 2019 12:33:33 +0000 Subject: [PATCH 20/35] Update modules/queue/queue_disk_channel.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- modules/queue/queue_disk_channel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index e2ee0bda566f..209835387904 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -59,7 +59,7 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( return nil, err } - // the level backend only needs temporary workrers to catch up with the previously dropped work + // the level backend only needs temporary workers to catch up with the previously dropped work levelCfg := LevelQueueConfiguration{ DataDir: config.DataDir, QueueLength: config.QueueLength, From 6306cd42bcb645c5ecc30fb0e3d769fc80a201fa Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 31 Dec 2019 17:17:07 +0000 Subject: [PATCH 21/35] Rename queue.Description to queue.ManagedQueue as per @guillep2k --- modules/queue/manager.go | 80 ++++++++++++++++++------------------- modules/queue/workerpool.go | 20 +++++----- routers/admin/admin.go | 38 +++++++++--------- 3 files changed, 69 insertions(+), 69 deletions(-) diff --git a/modules/queue/manager.go b/modules/queue/manager.go index a2deb8ff7cfe..d26836e7c836 100644 --- a/modules/queue/manager.go +++ b/modules/queue/manager.go @@ -23,11 +23,11 @@ type Manager struct { mutex sync.Mutex counter int64 - Queues map[int64]*Description + Queues map[int64]*ManagedQueue } -// Description represents a working queue inheriting from Gitea. -type Description struct { +// ManagedQueue represents a working queue inheriting from Gitea. +type ManagedQueue struct { mutex sync.Mutex QID int64 Queue Queue @@ -35,13 +35,13 @@ type Description struct { Name string Configuration interface{} ExemplarType string - Pool PoolManager + Pool ManagedPool counter int64 PoolWorkers map[int64]*PoolWorkers } -// PoolManager is a simple interface to get certain details from a worker pool -type PoolManager interface { +// ManagedPool is a simple interface to get certain details from a worker pool +type ManagedPool interface { AddWorkers(number int, timeout time.Duration) context.CancelFunc NumberOfWorkers() int MaxNumberOfWorkers() int @@ -52,8 +52,8 @@ type PoolManager interface { SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) } -// DescriptionList implements the sort.Interface -type DescriptionList []*Description +// ManagedQueueList implements the sort.Interface +type ManagedQueueList []*ManagedQueue // PoolWorkers represents a working queue inheriting from Gitea. type PoolWorkers struct { @@ -76,7 +76,7 @@ func init() { func GetManager() *Manager { if manager == nil { manager = &Manager{ - Queues: make(map[int64]*Description), + Queues: make(map[int64]*ManagedQueue), } } return manager @@ -87,10 +87,10 @@ func (m *Manager) Add(queue Queue, t Type, configuration, exemplar interface{}, - pool PoolManager) int64 { + pool ManagedPool) int64 { cfg, _ := json.Marshal(configuration) - desc := &Description{ + mq := &ManagedQueue{ Queue: queue, Type: t, Configuration: string(cfg), @@ -100,15 +100,15 @@ func (m *Manager) Add(queue Queue, } m.mutex.Lock() m.counter++ - desc.QID = m.counter - desc.Name = fmt.Sprintf("queue-%d", desc.QID) + mq.QID = m.counter + mq.Name = fmt.Sprintf("queue-%d", mq.QID) if named, ok := queue.(Named); ok { - desc.Name = named.Name() + mq.Name = named.Name() } - m.Queues[desc.QID] = desc + m.Queues[mq.QID] = mq m.mutex.Unlock() - log.Trace("Queue Manager registered: %s (QID: %d)", desc.Name, desc.QID) - return desc.QID + log.Trace("Queue Manager registered: %s (QID: %d)", mq.Name, mq.QID) + return mq.QID } // Remove a queue from the Manager @@ -120,27 +120,27 @@ func (m *Manager) Remove(qid int64) { } -// GetDescription by qid -func (m *Manager) GetDescription(qid int64) *Description { +// GetManagedQueue by qid +func (m *Manager) GetManagedQueue(qid int64) *ManagedQueue { m.mutex.Lock() defer m.mutex.Unlock() return m.Queues[qid] } -// Descriptions returns the queue descriptions -func (m *Manager) Descriptions() []*Description { +// ManagedQueues returns the managed queues +func (m *Manager) ManagedQueues() []*ManagedQueue { m.mutex.Lock() - descs := make([]*Description, 0, len(m.Queues)) - for _, desc := range m.Queues { - descs = append(descs, desc) + mqs := make([]*ManagedQueue, 0, len(m.Queues)) + for _, mq := range m.Queues { + mqs = append(mqs, mq) } m.mutex.Unlock() - sort.Sort(DescriptionList(descs)) - return descs + sort.Sort(ManagedQueueList(mqs)) + return mqs } // Workers returns the poolworkers -func (q *Description) Workers() []*PoolWorkers { +func (q *ManagedQueue) Workers() []*PoolWorkers { q.mutex.Lock() workers := make([]*PoolWorkers, 0, len(q.PoolWorkers)) for _, worker := range q.PoolWorkers { @@ -152,7 +152,7 @@ func (q *Description) Workers() []*PoolWorkers { } // RegisterWorkers registers workers to this queue -func (q *Description) RegisterWorkers(number int, start time.Time, hasTimeout bool, timeout time.Time, cancel context.CancelFunc) int64 { +func (q *ManagedQueue) RegisterWorkers(number int, start time.Time, hasTimeout bool, timeout time.Time, cancel context.CancelFunc) int64 { q.mutex.Lock() defer q.mutex.Unlock() q.counter++ @@ -168,7 +168,7 @@ func (q *Description) RegisterWorkers(number int, start time.Time, hasTimeout bo } // CancelWorkers cancels pooled workers with pid -func (q *Description) CancelWorkers(pid int64) { +func (q *ManagedQueue) CancelWorkers(pid int64) { q.mutex.Lock() pw, ok := q.PoolWorkers[pid] q.mutex.Unlock() @@ -179,14 +179,14 @@ func (q *Description) CancelWorkers(pid int64) { } // RemoveWorkers deletes pooled workers with pid -func (q *Description) RemoveWorkers(pid int64) { +func (q *ManagedQueue) RemoveWorkers(pid int64) { q.mutex.Lock() delete(q.PoolWorkers, pid) q.mutex.Unlock() } // AddWorkers adds workers to the queue if it has registered an add worker function -func (q *Description) AddWorkers(number int, timeout time.Duration) context.CancelFunc { +func (q *ManagedQueue) AddWorkers(number int, timeout time.Duration) context.CancelFunc { if q.Pool != nil { // the cancel will be added to the pool workers description above return q.Pool.AddWorkers(number, timeout) @@ -195,7 +195,7 @@ func (q *Description) AddWorkers(number int, timeout time.Duration) context.Canc } // NumberOfWorkers returns the number of workers in the queue -func (q *Description) NumberOfWorkers() int { +func (q *ManagedQueue) NumberOfWorkers() int { if q.Pool != nil { return q.Pool.NumberOfWorkers() } @@ -203,7 +203,7 @@ func (q *Description) NumberOfWorkers() int { } // MaxNumberOfWorkers returns the maximum number of workers for the pool -func (q *Description) MaxNumberOfWorkers() int { +func (q *ManagedQueue) MaxNumberOfWorkers() int { if q.Pool != nil { return q.Pool.MaxNumberOfWorkers() } @@ -211,7 +211,7 @@ func (q *Description) MaxNumberOfWorkers() int { } // BoostWorkers returns the number of workers for a boost -func (q *Description) BoostWorkers() int { +func (q *ManagedQueue) BoostWorkers() int { if q.Pool != nil { return q.Pool.BoostWorkers() } @@ -219,7 +219,7 @@ func (q *Description) BoostWorkers() int { } // BoostTimeout returns the timeout of the next boost -func (q *Description) BoostTimeout() time.Duration { +func (q *ManagedQueue) BoostTimeout() time.Duration { if q.Pool != nil { return q.Pool.BoostTimeout() } @@ -227,7 +227,7 @@ func (q *Description) BoostTimeout() time.Duration { } // BlockTimeout returns the timeout til the next boost -func (q *Description) BlockTimeout() time.Duration { +func (q *ManagedQueue) BlockTimeout() time.Duration { if q.Pool != nil { return q.Pool.BlockTimeout() } @@ -235,21 +235,21 @@ func (q *Description) BlockTimeout() time.Duration { } // SetSettings sets the setable boost values -func (q *Description) SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) { +func (q *ManagedQueue) SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) { if q.Pool != nil { q.Pool.SetSettings(maxNumberOfWorkers, boostWorkers, timeout) } } -func (l DescriptionList) Len() int { +func (l ManagedQueueList) Len() int { return len(l) } -func (l DescriptionList) Less(i, j int) bool { +func (l ManagedQueueList) Less(i, j int) bool { return l[i].Name < l[j].Name } -func (l DescriptionList) Swap(i, j int) { +func (l ManagedQueueList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go index 98a68cd0411e..e92c1ec31555 100644 --- a/modules/queue/workerpool.go +++ b/modules/queue/workerpool.go @@ -71,19 +71,19 @@ func (p *WorkerPool) pushBoost(data Data) { } p.blockTimeout *= 2 ctx, cancel := context.WithCancel(p.baseCtx) - desc := GetManager().GetDescription(p.qid) + mq := GetManager().GetManagedQueue(p.qid) boost := p.boostWorkers if (boost+p.numberOfWorkers) > p.maxNumberOfWorkers && p.maxNumberOfWorkers >= 0 { boost = p.maxNumberOfWorkers - p.numberOfWorkers } - if desc != nil { - log.Warn("WorkerPool: %d (for %s) Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", p.qid, desc.Name, ourTimeout, boost, p.boostTimeout, p.blockTimeout) + if mq != nil { + log.Warn("WorkerPool: %d (for %s) Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", p.qid, mq.Name, ourTimeout, boost, p.boostTimeout, p.blockTimeout) start := time.Now() - pid := desc.RegisterWorkers(boost, start, false, start, cancel) + pid := mq.RegisterWorkers(boost, start, false, start, cancel) go func() { <-ctx.Done() - desc.RemoveWorkers(pid) + mq.RemoveWorkers(pid) cancel() }() } else { @@ -171,15 +171,15 @@ func (p *WorkerPool) AddWorkers(number int, timeout time.Duration) context.Cance ctx, cancel = context.WithCancel(p.baseCtx) } - desc := GetManager().GetDescription(p.qid) - if desc != nil { - pid := desc.RegisterWorkers(number, start, hasTimeout, end, cancel) + mq := GetManager().GetManagedQueue(p.qid) + if mq != nil { + pid := mq.RegisterWorkers(number, start, hasTimeout, end, cancel) go func() { <-ctx.Done() - desc.RemoveWorkers(pid) + mq.RemoveWorkers(pid) cancel() }() - log.Trace("WorkerPool: %d (for %s) adding %d workers with group id: %d", p.qid, desc.Name, number, pid) + log.Trace("WorkerPool: %d (for %s) adding %d workers with group id: %d", p.qid, mq.Name, number, pid) } else { log.Trace("WorkerPool: %d adding %d workers (no group id)", p.qid, number) diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 299da8b46c3e..5e8e0b746767 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -358,7 +358,7 @@ func Monitor(ctx *context.Context) { ctx.Data["PageIsAdminMonitor"] = true ctx.Data["Processes"] = process.GetManager().Processes() ctx.Data["Entries"] = cron.ListTasks() - ctx.Data["Queues"] = queue.GetManager().Descriptions() + ctx.Data["Queues"] = queue.GetManager().ManagedQueues() ctx.HTML(200, tplMonitor) } @@ -374,28 +374,28 @@ func MonitorCancel(ctx *context.Context) { // Queue shows details for a specific queue func Queue(ctx *context.Context) { qid := ctx.ParamsInt64("qid") - desc := queue.GetManager().GetDescription(qid) - if desc == nil { + mq := queue.GetManager().GetManagedQueue(qid) + if mq == nil { ctx.Status(404) return } - ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", desc.Name) + ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", mq.Name) ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminMonitor"] = true - ctx.Data["Queue"] = desc + ctx.Data["Queue"] = mq ctx.HTML(200, tplQueue) } // WorkerCancel cancels a worker group func WorkerCancel(ctx *context.Context) { qid := ctx.ParamsInt64("qid") - desc := queue.GetManager().GetDescription(qid) - if desc == nil { + mq := queue.GetManager().GetManagedQueue(qid) + if mq == nil { ctx.Status(404) return } pid := ctx.ParamsInt64("pid") - desc.CancelWorkers(pid) + mq.CancelWorkers(pid) ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.cancelling")) ctx.JSON(200, map[string]interface{}{ "redirect": setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid), @@ -405,8 +405,8 @@ func WorkerCancel(ctx *context.Context) { // AddWorkers adds workers to a worker group func AddWorkers(ctx *context.Context) { qid := ctx.ParamsInt64("qid") - desc := queue.GetManager().GetDescription(qid) - if desc == nil { + mq := queue.GetManager().GetManagedQueue(qid) + if mq == nil { ctx.Status(404) return } @@ -422,12 +422,12 @@ func AddWorkers(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) return } - if desc.Pool == nil { + if mq.Pool == nil { ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none")) ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) return } - desc.AddWorkers(number, timeout) + mq.AddWorkers(number, timeout) ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added")) ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) } @@ -435,12 +435,12 @@ func AddWorkers(ctx *context.Context) { // SetQueueSettings sets the maximum number of workers for this queue func SetQueueSettings(ctx *context.Context) { qid := ctx.ParamsInt64("qid") - desc := queue.GetManager().GetDescription(qid) - if desc == nil { + mq := queue.GetManager().GetManagedQueue(qid) + if mq == nil { ctx.Status(404) return } - if desc.Pool == nil { + if mq.Pool == nil { ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none")) ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) return @@ -464,7 +464,7 @@ func SetQueueSettings(ctx *context.Context) { maxNumber = -1 } } else { - maxNumber = desc.MaxNumberOfWorkers() + maxNumber = mq.MaxNumberOfWorkers() } if len(numberStr) > 0 { @@ -475,7 +475,7 @@ func SetQueueSettings(ctx *context.Context) { return } } else { - number = desc.BoostWorkers() + number = mq.BoostWorkers() } if len(timeoutStr) > 0 { @@ -486,10 +486,10 @@ func SetQueueSettings(ctx *context.Context) { return } } else { - timeout = desc.Pool.BoostTimeout() + timeout = mq.Pool.BoostTimeout() } - desc.SetSettings(maxNumber, number, timeout) + mq.SetSettings(maxNumber, number, timeout) ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed")) ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) } From 8798a61ba4c71614e82d6fd95ea23b121278143a Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 31 Dec 2019 17:48:26 +0000 Subject: [PATCH 22/35] Cancel pool workers when removed --- modules/queue/manager.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/queue/manager.go b/modules/queue/manager.go index d26836e7c836..88b264484867 100644 --- a/modules/queue/manager.go +++ b/modules/queue/manager.go @@ -181,8 +181,12 @@ func (q *ManagedQueue) CancelWorkers(pid int64) { // RemoveWorkers deletes pooled workers with pid func (q *ManagedQueue) RemoveWorkers(pid int64) { q.mutex.Lock() + pw, ok := q.PoolWorkers[pid] delete(q.PoolWorkers, pid) q.mutex.Unlock() + if ok && pw.Cancel != nil { + pw.Cancel() + } } // AddWorkers adds workers to the queue if it has registered an add worker function From 030b6d91c854ddf49d99dd5a9cf9ae5588fd8dc8 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 31 Dec 2019 18:18:03 +0000 Subject: [PATCH 23/35] Remove dependency on queue from setting --- modules/indexer/issues/indexer.go | 2 +- modules/queue/queue.go | 4 +- modules/queue/queue_wrapped.go | 4 +- modules/queue/setting.go | 75 +++++++++++++++++++++++++++++++ modules/setting/queue.go | 63 ++++---------------------- modules/setting/task.go | 6 +-- modules/task/task.go | 3 +- 7 files changed, 91 insertions(+), 66 deletions(-) create mode 100644 modules/queue/setting.go diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 6c89a9708a6c..894f37a96315 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -127,7 +127,7 @@ func InitIssueIndexer(syncReindex bool) { } } - issueIndexerQueue = setting.CreateQueue("issue_indexer", handler, &IndexerData{}) + issueIndexerQueue = queue.CreateQueue("issue_indexer", handler, &IndexerData{}) if issueIndexerQueue == nil { log.Fatal("Unable to create issue indexer queue") diff --git a/modules/queue/queue.go b/modules/queue/queue.go index 464e16dab130..d458a7d50627 100644 --- a/modules/queue/queue.go +++ b/modules/queue/queue.go @@ -123,8 +123,8 @@ func RegisteredTypesAsString() []string { return types } -// CreateQueue takes a queue Type and HandlerFunc some options and possibly an exemplar and returns a Queue or an error -func CreateQueue(queueType Type, handlerFunc HandlerFunc, opts, exemplar interface{}) (Queue, error) { +// NewQueue takes a queue Type and HandlerFunc some options and possibly an exemplar and returns a Queue or an error +func NewQueue(queueType Type, handlerFunc HandlerFunc, opts, exemplar interface{}) (Queue, error) { newFn, ok := queuesMap[queueType] if !ok { return nil, fmt.Errorf("Unsupported queue type: %v", queueType) diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go index c218749b6526..4578cd7250f3 100644 --- a/modules/queue/queue_wrapped.go +++ b/modules/queue/queue_wrapped.go @@ -59,7 +59,7 @@ func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), h q.lock.Unlock() log.Fatal("Timedout creating queue %v with cfg %v in %s", q.underlying, q.cfg, q.name) default: - queue, err := CreateQueue(q.underlying, handle, q.cfg, exemplar) + queue, err := NewQueue(q.underlying, handle, q.cfg, exemplar) if err == nil { q.internal = queue q.lock.Unlock() @@ -101,7 +101,7 @@ func NewWrappedQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, erro } config := configInterface.(WrappedQueueConfiguration) - queue, err := CreateQueue(config.Underlying, handle, config.Config, exemplar) + queue, err := NewQueue(config.Underlying, handle, config.Config, exemplar) if err == nil { // Just return the queue there is no need to wrap return queue, nil diff --git a/modules/queue/setting.go b/modules/queue/setting.go new file mode 100644 index 000000000000..d5a6b41882a0 --- /dev/null +++ b/modules/queue/setting.go @@ -0,0 +1,75 @@ +// 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 ( + "encoding/json" + "fmt" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +func validType(t string) (Type, error) { + if len(t) == 0 { + return PersistableChannelQueueType, nil + } + for _, typ := range RegisteredTypes() { + if t == string(typ) { + return typ, nil + } + } + return PersistableChannelQueueType, fmt.Errorf("Unknown queue type: %s defaulting to %s", t, string(PersistableChannelQueueType)) +} + +// CreateQueue for name with provided handler and exemplar +func CreateQueue(name string, handle HandlerFunc, exemplar interface{}) Queue { + q := setting.GetQueueSettings(name) + opts := make(map[string]interface{}) + opts["Name"] = name + opts["QueueLength"] = q.Length + opts["BatchLength"] = q.BatchLength + opts["DataDir"] = q.DataDir + opts["Addresses"] = q.Addresses + opts["Network"] = q.Network + opts["Password"] = q.Password + opts["DBIndex"] = q.DBIndex + opts["QueueName"] = q.QueueName + opts["Workers"] = q.Workers + opts["MaxWorkers"] = q.MaxWorkers + opts["BlockTimeout"] = q.BlockTimeout + opts["BoostTimeout"] = q.BoostTimeout + opts["BoostWorkers"] = q.BoostWorkers + + typ, err := validType(q.Type) + if err != nil { + log.Error("Invalid type %s provided for queue named %s defaulting to %s", q.Type, name, string(typ)) + } + + cfg, err := json.Marshal(opts) + if err != nil { + log.Error("Unable to marshall generic options: %v Error: %v", opts, err) + log.Error("Unable to create queue for %s", name, err) + return nil + } + + returnable, err := NewQueue(typ, handle, cfg, exemplar) + if q.WrapIfNecessary && err != nil { + log.Warn("Unable to create queue for %s: %v", name, err) + log.Warn("Attempting to create wrapped queue") + returnable, err = NewQueue(WrappedQueueType, handle, WrappedQueueConfiguration{ + Underlying: Type(q.Type), + Timeout: q.Timeout, + MaxAttempts: q.MaxAttempts, + Config: cfg, + QueueLength: q.Length, + }, exemplar) + } + if err != nil { + log.Error("Unable to create queue for %s: %v", name, err) + return nil + } + return returnable +} diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 017083439138..bb3c30126248 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -5,7 +5,6 @@ package setting import ( - "encoding/json" "fmt" "path" "strconv" @@ -13,10 +12,10 @@ import ( "time" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/queue" ) -type queueSettings struct { +// QueueSettings represent the settings for a queue from the ini +type QueueSettings struct { DataDir string Length int BatchLength int @@ -38,55 +37,11 @@ type queueSettings struct { } // Queue settings -var Queue = queueSettings{} +var Queue = QueueSettings{} -// CreateQueue for name with provided handler and exemplar -func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) queue.Queue { - q := getQueueSettings(name) - opts := make(map[string]interface{}) - opts["Name"] = name - opts["QueueLength"] = q.Length - opts["BatchLength"] = q.BatchLength - opts["DataDir"] = q.DataDir - opts["Addresses"] = q.Addresses - opts["Network"] = q.Network - opts["Password"] = q.Password - opts["DBIndex"] = q.DBIndex - opts["QueueName"] = q.QueueName - opts["Workers"] = q.Workers - opts["MaxWorkers"] = q.MaxWorkers - opts["BlockTimeout"] = q.BlockTimeout - opts["BoostTimeout"] = q.BoostTimeout - opts["BoostWorkers"] = q.BoostWorkers - - cfg, err := json.Marshal(opts) - if err != nil { - log.Error("Unable to marshall generic options: %v Error: %v", opts, err) - log.Error("Unable to create queue for %s", name, err) - return nil - } - - returnable, err := queue.CreateQueue(queue.Type(q.Type), handle, cfg, exemplar) - if q.WrapIfNecessary && err != nil { - log.Warn("Unable to create queue for %s: %v", name, err) - log.Warn("Attempting to create wrapped queue") - returnable, err = queue.CreateQueue(queue.WrappedQueueType, handle, queue.WrappedQueueConfiguration{ - Underlying: queue.Type(q.Type), - Timeout: q.Timeout, - MaxAttempts: q.MaxAttempts, - Config: cfg, - QueueLength: q.Length, - }, exemplar) - } - if err != nil { - log.Error("Unable to create queue for %s: %v", name, err) - return nil - } - return returnable -} - -func getQueueSettings(name string) queueSettings { - q := queueSettings{} +// GetQueueSettings returns the queue settings for the appropriately named queue +func GetQueueSettings(name string) QueueSettings { + q := QueueSettings{} sec := Cfg.Section("queue." + name) // DataDir is not directly inheritable q.DataDir = path.Join(Queue.DataDir, name) @@ -104,8 +59,7 @@ func getQueueSettings(name string) queueSettings { q.Length = sec.Key("LENGTH").MustInt(Queue.Length) q.BatchLength = sec.Key("BATCH_LENGTH").MustInt(Queue.BatchLength) q.ConnectionString = sec.Key("CONN_STR").MustString(Queue.ConnectionString) - validTypes := queue.RegisteredTypesAsString() - q.Type = sec.Key("TYPE").In(Queue.Type, validTypes) + q.Type = sec.Key("TYPE").MustString(Queue.Type) q.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(Queue.WrapIfNecessary) q.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(Queue.MaxAttempts) q.Timeout = sec.Key("TIMEOUT").MustDuration(Queue.Timeout) @@ -131,8 +85,7 @@ func NewQueueService() { Queue.Length = sec.Key("LENGTH").MustInt(20) Queue.BatchLength = sec.Key("BATCH_LENGTH").MustInt(20) Queue.ConnectionString = sec.Key("CONN_STR").MustString(path.Join(AppDataPath, "")) - validTypes := queue.RegisteredTypesAsString() - Queue.Type = sec.Key("TYPE").In(string(queue.PersistableChannelQueueType), validTypes) + Queue.Type = sec.Key("TYPE").MustString("") Queue.Network, Queue.Addresses, Queue.Password, Queue.DBIndex, _ = ParseQueueConnStr(Queue.ConnectionString) Queue.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(true) Queue.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(10) diff --git a/modules/setting/task.go b/modules/setting/task.go index fa63c669c662..81ed39a9fb90 100644 --- a/modules/setting/task.go +++ b/modules/setting/task.go @@ -4,16 +4,14 @@ package setting -import "code.gitea.io/gitea/modules/queue" - func newTaskService() { taskSec := Cfg.Section("task") queueTaskSec := Cfg.Section("queue.task") switch taskSec.Key("QUEUE_TYPE").MustString(ChannelQueueType) { case ChannelQueueType: - queueTaskSec.Key("TYPE").MustString(string(queue.PersistableChannelQueueType)) + queueTaskSec.Key("TYPE").MustString("persistable-channel") case RedisQueueType: - queueTaskSec.Key("TYPE").MustString(string(queue.RedisQueueType)) + queueTaskSec.Key("TYPE").MustString("redis") } queueTaskSec.Key("LENGTH").MustInt(taskSec.Key("QUEUE_LENGTH").MustInt(1000)) queueTaskSec.Key("CONN_STR").MustString(taskSec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0")) diff --git a/modules/task/task.go b/modules/task/task.go index 852319d406fb..416f0c696a99 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" "code.gitea.io/gitea/modules/queue" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" ) @@ -31,7 +30,7 @@ func Run(t *models.Task) error { // Init will start the service to get all unfinished tasks and run them func Init() error { - taskQueue = setting.CreateQueue("task", handle, &models.Task{}) + taskQueue = queue.CreateQueue("task", handle, &models.Task{}) if taskQueue == nil { return fmt.Errorf("Unable to create Task Queue") From 9941ae296648f10b5cd3e0853cc6821e1ac573d0 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 31 Dec 2019 18:18:55 +0000 Subject: [PATCH 24/35] Update modules/queue/queue_redis.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- modules/queue/queue_redis.go | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 21fa4462b8cb..de2ceca5e2dc 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -170,7 +170,6 @@ func (r *RedisQueue) readToChan() { log.Trace("RedisQueue: %s Task found: %#v", r.name, data) r.pool.Push(data) - time.Sleep(time.Millisecond * 10) } } } From e852cb620f8c12f9bd2f5976ce53126215626b1b Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 4 Jan 2020 14:59:39 +0000 Subject: [PATCH 25/35] As per @guillep2k add mutex locks on shutdown/terminate --- modules/queue/queue_disk.go | 7 +++++++ modules/queue/queue_redis.go | 31 ++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go index 550e78b97ee1..98e7b24e42fd 100644 --- a/modules/queue/queue_disk.go +++ b/modules/queue/queue_disk.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "reflect" + "sync" "time" "code.gitea.io/gitea/modules/log" @@ -38,6 +39,7 @@ type LevelQueue struct { queue *levelqueue.Queue closed chan struct{} terminated chan struct{} + lock sync.Mutex exemplar interface{} workers int name string @@ -173,6 +175,8 @@ func (l *LevelQueue) Push(data Data) error { // Shutdown this queue and stop processing func (l *LevelQueue) Shutdown() { + l.lock.Lock() + defer l.lock.Unlock() log.Trace("LevelQueue: %s Shutdown", l.name) select { case <-l.closed: @@ -185,10 +189,13 @@ func (l *LevelQueue) Shutdown() { func (l *LevelQueue) Terminate() { log.Trace("LevelQueue: %s Terminating", l.name) l.Shutdown() + l.lock.Lock() select { case <-l.terminated: + l.lock.Unlock() default: close(l.terminated) + l.lock.Unlock() if err := l.queue.Close(); err != nil && err.Error() != "leveldb: closed" { log.Error("Error whilst closing internal queue in %s: %v", l.name, err) } diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index de2ceca5e2dc..87a0ccd9321d 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -11,6 +11,7 @@ import ( "fmt" "reflect" "strings" + "sync" "time" "code.gitea.io/gitea/modules/log" @@ -30,13 +31,15 @@ type redisClient interface { // RedisQueue redis queue type RedisQueue struct { - pool *WorkerPool - client redisClient - queueName string - closed chan struct{} - exemplar interface{} - workers int - name string + pool *WorkerPool + client redisClient + queueName string + closed chan struct{} + terminated chan struct{} + exemplar interface{} + workers int + name string + lock sync.Mutex } // RedisQueueConfiguration is the configuration for the redis queue @@ -195,19 +198,29 @@ func (r *RedisQueue) Push(data Data) error { // Shutdown processing from this queue func (r *RedisQueue) Shutdown() { log.Trace("Shutdown: %s", r.name) + r.lock.Lock() select { case <-r.closed: default: close(r.closed) } + r.lock.Unlock() } // Terminate this queue and close the queue func (r *RedisQueue) Terminate() { log.Trace("Terminating: %s", r.name) r.Shutdown() - if err := r.client.Close(); err != nil { - log.Error("Error whilst closing internal redis client in %s: %v", r.name, err) + r.lock.Lock() + select { + case <-r.terminated: + r.lock.Unlock() + default: + close(r.terminated) + r.lock.Unlock() + if err := r.client.Close(); err != nil { + log.Error("Error whilst closing internal redis client in %s: %v", r.name, err) + } } } From 21b97781197b1f121e50604048cb6b6638f82fa5 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 4 Jan 2020 15:12:43 +0000 Subject: [PATCH 26/35] move unlocking out of setInternal --- modules/queue/queue_disk_channel.go | 7 ++++++- modules/queue/queue_wrapped.go | 24 +++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index 209835387904..46a097e84a9b 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -122,7 +122,12 @@ func (p *PersistableChannelQueue) Push(data Data) error { func (p *PersistableChannelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { p.lock.Lock() if p.internal == nil { - p.setInternal(atShutdown, p.ChannelQueue.pool.handle, p.exemplar) + err := p.setInternal(atShutdown, p.ChannelQueue.pool.handle, p.exemplar) + p.lock.Unlock() + if err != nil { + log.Fatal("Unable to create internal queue for %s Error: %v", p.Name(), err) + return + } } else { p.lock.Unlock() } diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go index 4578cd7250f3..d0b93b54d0c3 100644 --- a/modules/queue/queue_wrapped.go +++ b/modules/queue/queue_wrapped.go @@ -37,7 +37,8 @@ type delayedStarter struct { name string } -func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), handle HandlerFunc, exemplar interface{}) { +// setInternal must be called with the lock locked. +func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), handle HandlerFunc, exemplar interface{}) error { var ctx context.Context var cancel context.CancelFunc if q.timeout > 0 { @@ -56,8 +57,7 @@ func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), h for q.internal == nil { select { case <-ctx.Done(): - q.lock.Unlock() - log.Fatal("Timedout creating queue %v with cfg %v in %s", q.underlying, q.cfg, q.name) + return fmt.Errorf("Timedout creating queue %v with cfg %v in %s", q.underlying, q.cfg, q.name) default: queue, err := NewQueue(q.underlying, handle, q.cfg, exemplar) if err == nil { @@ -70,16 +70,21 @@ func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), h } i++ if q.maxAttempts > 0 && i > q.maxAttempts { - q.lock.Unlock() - log.Fatal("Unable to create queue %v for %s with cfg %v by max attempts: error: %v", q.underlying, q.name, q.cfg, err) + return fmt.Errorf("Unable to create queue %v for %s with cfg %v by max attempts: error: %v", q.underlying, q.name, q.cfg, err) } sleepTime := 100 * time.Millisecond if q.timeout > 0 && q.maxAttempts > 0 { sleepTime = (q.timeout - 200*time.Millisecond) / time.Duration(q.maxAttempts) } - time.Sleep(sleepTime) + t := time.NewTimer(sleepTime) + select { + case <-ctx.Done(): + t.Stop() + case <-t.C: + } } } + return nil } // WrappedQueue wraps a delayed starting queue @@ -151,7 +156,12 @@ func (q *WrappedQueue) Push(data Data) error { func (q *WrappedQueue) Run(atShutdown, atTerminate func(context.Context, func())) { q.lock.Lock() if q.internal == nil { - q.setInternal(atShutdown, q.handle, q.exemplar) + err := q.setInternal(atShutdown, q.handle, q.exemplar) + q.lock.Unlock() + if err != nil { + log.Fatal("Unable to set the internal queue for %s Error: %v", q.Name(), err) + return + } go func() { for data := range q.channel { _ = q.internal.Push(data) From 1cb7a86e37d579e9b23e9e5041ed03c5f5a3d4a5 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 4 Jan 2020 15:26:18 +0000 Subject: [PATCH 27/35] Add warning if number of workers < 0 --- modules/queue/workerpool.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go index e92c1ec31555..25fc7dd64425 100644 --- a/modules/queue/workerpool.go +++ b/modules/queue/workerpool.go @@ -202,8 +202,11 @@ func (p *WorkerPool) addWorkers(ctx context.Context, number int) { p.lock.Lock() p.numberOfWorkers-- - if p.numberOfWorkers <= 0 { + if p.numberOfWorkers == 0 { + p.cond.Broadcast() + } else if p.numberOfWorkers < 0 { // numberOfWorkers can't go negative but... + log.Warn("Number of Workers < 0 for QID %d - this shouldn't happen", p.qid) p.numberOfWorkers = 0 p.cond.Broadcast() } From e4ddaab70b7513b2b89a55ebde09be8f1379d10d Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 4 Jan 2020 15:44:28 +0000 Subject: [PATCH 28/35] Small changes as per @guillep2k --- options/locale/locale_en-US.ini | 4 ++-- routers/admin/admin.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4e30cf08f66f..81ec866a1fdd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2056,7 +2056,7 @@ monitor.queue.settings.numberworkers.error = Number of Workers to add must be gr monitor.queue.settings.maxnumberworkers = Max Number of workers monitor.queue.settings.maxnumberworkers.placeholder = Currently %[1]d monitor.queue.settings.maxnumberworkers.error = Max number of workers must be a number -monitor.queue.settings.submit = Change Settings +monitor.queue.settings.submit = Update Settings monitor.queue.settings.changed = Settings Updated monitor.queue.settings.blocktimeout = Current Block Timeout monitor.queue.settings.blocktimeout.value = %[1]v @@ -2069,7 +2069,7 @@ monitor.queue.pool.workers.none = No worker groups. monitor.queue.pool.cancel = Shutdown Worker Group monitor.queue.pool.cancelling = Worker Group shutting down monitor.queue.pool.cancel_notices = Shutdown this group of %s workers? -monitor.queue.pool.cancel_desc = Leaving a queue without any worker groups may cause requests may block indefinitely. +monitor.queue.pool.cancel_desc = Leaving a queue without any worker groups may cause requests to block indefinitely. notices.system_notice_list = System Notices notices.view_detail_header = View Notice Details diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 5e8e0b746767..055b8f5a5e9b 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -432,7 +432,7 @@ func AddWorkers(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) } -// SetQueueSettings sets the maximum number of workers for this queue +// SetQueueSettings sets the maximum number of workers and other settings for this queue func SetQueueSettings(ctx *context.Context) { qid := ctx.ParamsInt64("qid") mq := queue.GetManager().GetManagedQueue(qid) From c7550a4bddbb33a337c87d68a7d02d7e7bc854bf Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 4 Jan 2020 15:47:37 +0000 Subject: [PATCH 29/35] No redis host specified not found --- modules/queue/queue_redis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 87a0ccd9321d..14e68937a5b8 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -91,7 +91,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) name: config.Name, } if len(dbs) == 0 { - return nil, errors.New("no redis host found") + return nil, errors.New("no redis host specified") } else if len(dbs) == 1 { queue.client = redis.NewClient(&redis.Options{ Network: config.Network, From 90eeb1415aab31ae58c97997b15d11127dd9f814 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 4 Jan 2020 17:23:24 +0000 Subject: [PATCH 30/35] Clean up documentation for queues --- custom/conf/app.ini.sample | 20 +++++++++++++++++++ .../doc/advanced/config-cheat-sheet.en-us.md | 16 +++++++++++++-- modules/setting/queue.go | 18 +++++------------ 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 4b810f91f766..b86ace3b94f9 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -372,6 +372,8 @@ REPO_INDEXER_INCLUDE = REPO_INDEXER_EXCLUDE = [queue] +; Specific queues can be individually configured with [queue.name]. [queue] provides defaults +; ; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy ; default to persistable-channel TYPE = persistable-channel @@ -383,6 +385,24 @@ LENGTH = 20 BATCH_LENGTH = 20 ; Connection string for redis queues this will store the redis connection string. CONN_STR = "addrs=127.0.0.1:6379 db=0" +; Provide the suffix of the default redis queue name - specific queues can be overriden within in their [queue.name] sections. +QUEUE_NAME = "_queue" +; If the queue cannot be created at startup - level queues may need a timeout at startup - wrap the queue: +WRAP_IF_NECESSARY = true +; Attempt to create the wrapped queue at max +MAX_ATTEMPTS = 10 +; Timeout queue creation +TIMEOUT = 15m30s +; Create a pool with this many workers +WORKERS = 1 +; Dynamically scale the worker pool to at this many workers +MAX_WORKERS = 10 +; Add boost workers when the queue blocks for BLOCK_TIMEOUT +BLOCK_TIMEOUT = 1s +; Remove the boost workers after BOOST_TIMEOUT +BOOST_TIMEOUT = 5m +; During a boost add BOOST_WORKERS +BOOST_WORKERS = 5 [admin] ; Disallow regular (non-admin) users from creating organizations. diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 6ffb43fcd89e..9b9ad2b1833e 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -221,6 +221,7 @@ relation to port exhaustion. - `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently support: bleve or db, if it's db, below issue indexer item will be invalid. - `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search. +- The next 4 configuration values are deprecated and should be set in `queue.issue_indexer` however are kept for backwards compatibility: - `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: Issue indexer queue, currently supports:`channel`, `levelqueue`, `redis`. - `ISSUE_INDEXER_QUEUE_DIR`: **indexers/issues.queue**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the queue will be saved path. - `ISSUE_INDEXER_QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string. @@ -234,13 +235,23 @@ relation to port exhaustion. - `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed. - `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to zero to never timeout. -## Queue (`queue`) +## Queue (`queue` and `queue.*`) - `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel`, `channel`, `level`, `redis`, `dummy` -- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. +- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. `DATADIR` for inidividual queues can be set in `queue.name` sections but will default to `DATADIR/`**`name`**. - `LENGTH`: **20**: Maximal queue size before channel queues block - `BATCH_LENGTH`: **20**: Batch data before passing to the handler - `CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Connection string for the redis queue type. +- `QUEUE_NAME`: **""**: The suffix for default redis queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overriden in the specific `queue.name` section. +- `WRAP_IF_NECESSARY`: **true**: Will wrap queues with a timeoutable queue if the selected queue is not ready to be created - (Only relevant for the level queue.) +- `MAX_ATTEMPTS`: **10**: Maximum number of attempts to create the wrapped queue +- `TIMEOUT`: **GRACEFUL_HAMMER_TIME + 30s**: Timeout the creation of the wrapped queue if it takes longer than this to create. +- Queues by default come with a dynamically scaling worker pool. The following settings configure this: +- `WORKERS`: **1**: Number of initial workers for the queue. +- `MAX_WORKERS`: **10**: Maximum number of worker go-routines for the queue. +- `BLOCK_TIMEOUT`: **1s**: If the queue blocks for this time, boost the number of workers - the `BLOCK_TIMEOUT` will then be doubled before boosting again whilst the boost is ongoing. +- `BOOST_TIMEOUT`: **5m**: Boost workers will timeout after this long. +- `BOOST_WORKERS`: **5**: This many workers will be added to the worker pool if there is a boost. ## Admin (`admin`) - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled @@ -617,6 +628,7 @@ You may redefine `ELEMENT`, `ALLOW_ATTR`, and `REGEXP` multiple times; each time ## Task (`task`) +- Task queue configuration has been moved to `queue.task` however, the below configuration values are kept for backwards compatibilityx: - `QUEUE_TYPE`: **channel**: Task queue type, could be `channel` or `redis`. - `QUEUE_LENGTH`: **1000**: Task queue length, available only when `QUEUE_TYPE` is `channel`. - `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Task queue connection string, available only when `QUEUE_TYPE` is `redis`. If there redis needs a password, use `addrs=127.0.0.1:6379 password=123 db=0`. diff --git a/modules/setting/queue.go b/modules/setting/queue.go index bb3c30126248..546802715f14 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -45,10 +45,14 @@ func GetQueueSettings(name string) QueueSettings { sec := Cfg.Section("queue." + name) // DataDir is not directly inheritable q.DataDir = path.Join(Queue.DataDir, name) + // QueueName is not directly inheritable either + q.QueueName = name + Queue.QueueName for _, key := range sec.Keys() { switch key.Name() { case "DATADIR": q.DataDir = key.MustString(q.DataDir) + case "QUEUE_NAME": + q.QueueName = key.MustString(q.QueueName) } } if !path.IsAbs(q.DataDir) { @@ -68,7 +72,6 @@ func GetQueueSettings(name string) QueueSettings { q.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(Queue.BlockTimeout) q.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(Queue.BoostTimeout) q.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(Queue.BoostWorkers) - q.QueueName = sec.Key("QUEUE_NAME").MustString(Queue.QueueName) q.Network, q.Addresses, q.Password, q.DBIndex, _ = ParseQueueConnStr(q.ConnectionString) return q @@ -95,18 +98,7 @@ func NewQueueService() { Queue.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(1 * time.Second) Queue.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(5 * time.Minute) Queue.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(5) - Queue.QueueName = sec.Key("QUEUE_NAME").MustString(Queue.QueueName) - - hasWorkers := false - for _, key := range Cfg.Section("queue.notification").Keys() { - if key.Name() == "WORKERS" { - hasWorkers = true - break - } - } - if !hasWorkers { - Cfg.Section("queue.notification").Key("WORKERS").SetValue("5") - } + Queue.QueueName = sec.Key("QUEUE_NAME").MustString("_queue") // Now handle the old issue_indexer configuration section := Cfg.Section("queue.issue_indexer") From 8649dfa65e6d289afc026891d42cc4fdb32428ac Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 4 Jan 2020 18:27:04 +0000 Subject: [PATCH 31/35] Update docs/content/doc/advanced/config-cheat-sheet.en-us.md --- docs/content/doc/advanced/config-cheat-sheet.en-us.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 9b9ad2b1833e..b2cb81c5cd3c 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -242,7 +242,7 @@ relation to port exhaustion. - `LENGTH`: **20**: Maximal queue size before channel queues block - `BATCH_LENGTH`: **20**: Batch data before passing to the handler - `CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Connection string for the redis queue type. -- `QUEUE_NAME`: **""**: The suffix for default redis queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overriden in the specific `queue.name` section. +- `QUEUE_NAME`: **_queue**: The suffix for default redis queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overriden in the specific `queue.name` section. - `WRAP_IF_NECESSARY`: **true**: Will wrap queues with a timeoutable queue if the selected queue is not ready to be created - (Only relevant for the level queue.) - `MAX_ATTEMPTS`: **10**: Maximum number of attempts to create the wrapped queue - `TIMEOUT`: **GRACEFUL_HAMMER_TIME + 30s**: Timeout the creation of the wrapped queue if it takes longer than this to create. From 99a6f484fdd00673e488cb65099800d03bb30042 Mon Sep 17 00:00:00 2001 From: zeripath Date: Mon, 6 Jan 2020 07:25:20 +0000 Subject: [PATCH 32/35] Update modules/indexer/issues/indexer_test.go --- modules/indexer/issues/indexer_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index ecc12f79c8af..4028a6c8b518 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/setting" + "gopkg.in/ini.v1" "github.com/stretchr/testify/assert" From 52ee212c952ca15aac7eae0dfdb0a95d7d32d400 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 6 Jan 2020 17:27:30 +0000 Subject: [PATCH 33/35] Ensure that persistable channel queue is added to manager --- modules/queue/queue_disk_channel.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go index 46a097e84a9b..895c8ce918f3 100644 --- a/modules/queue/queue_disk_channel.go +++ b/modules/queue/queue_disk_channel.go @@ -74,14 +74,16 @@ func NewPersistableChannelQueue(handle HandlerFunc, cfg, exemplar interface{}) ( levelQueue, err := NewLevelQueue(handle, levelCfg, exemplar) if err == nil { - return &PersistableChannelQueue{ + queue := &PersistableChannelQueue{ ChannelQueue: channelQueue.(*ChannelQueue), delayedStarter: delayedStarter{ internal: levelQueue.(*LevelQueue), name: config.Name, }, closed: make(chan struct{}), - }, nil + } + _ = GetManager().Add(queue, PersistableChannelQueueType, config, exemplar, nil) + return queue, nil } if IsErrInvalidConfiguration(err) { // Retrying ain't gonna make this any better... From 1f83b4fc9b9dabda186257b38c265fe7012f90df Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 7 Jan 2020 09:08:29 +0000 Subject: [PATCH 34/35] Rename QUEUE_NAME REDIS_QUEUE_NAME --- .../doc/advanced/config-cheat-sheet.en-us.md | 2 +- modules/queue/queue_redis.go | 28 +++++++++---------- modules/queue/setting.go | 2 +- modules/setting/queue.go | 12 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index dc6a1ba34697..a92682c9c396 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -247,7 +247,7 @@ relation to port exhaustion. - `LENGTH`: **20**: Maximal queue size before channel queues block - `BATCH_LENGTH`: **20**: Batch data before passing to the handler - `CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Connection string for the redis queue type. -- `QUEUE_NAME`: **_queue**: The suffix for default redis queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overriden in the specific `queue.name` section. +- `REDIS_QUEUE_NAME`: **_queue**: The suffix for default redis queue name. Individual queues will default to **`name`**`REDIS_QUEUE_NAME` but can be overriden in the specific `queue.name` section. - `WRAP_IF_NECESSARY`: **true**: Will wrap queues with a timeoutable queue if the selected queue is not ready to be created - (Only relevant for the level queue.) - `MAX_ATTEMPTS`: **10**: Maximum number of attempts to create the wrapped queue - `TIMEOUT`: **GRACEFUL_HAMMER_TIME + 30s**: Timeout the creation of the wrapped queue if it takes longer than this to create. diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index 14e68937a5b8..a9406864a35a 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -44,19 +44,19 @@ type RedisQueue struct { // RedisQueueConfiguration is the configuration for the redis queue type RedisQueueConfiguration struct { - Network string - Addresses string - Password string - DBIndex int - BatchLength int - QueueLength int - QueueName string - Workers int - MaxWorkers int - BlockTimeout time.Duration - BoostTimeout time.Duration - BoostWorkers int - Name string + Network string + Addresses string + Password string + DBIndex int + BatchLength int + QueueLength int + RedisQueueName string + Workers int + MaxWorkers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int + Name string } // NewRedisQueue creates single redis or cluster redis queue @@ -84,7 +84,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) boostWorkers: config.BoostWorkers, maxNumberOfWorkers: config.MaxWorkers, }, - queueName: config.QueueName, + queueName: config.RedisQueueName, exemplar: exemplar, closed: make(chan struct{}), workers: config.Workers, diff --git a/modules/queue/setting.go b/modules/queue/setting.go index d5a6b41882a0..9ae8cf0dd712 100644 --- a/modules/queue/setting.go +++ b/modules/queue/setting.go @@ -36,7 +36,7 @@ func CreateQueue(name string, handle HandlerFunc, exemplar interface{}) Queue { opts["Network"] = q.Network opts["Password"] = q.Password opts["DBIndex"] = q.DBIndex - opts["QueueName"] = q.QueueName + opts["RedisQueueName"] = q.RedisQueueName opts["Workers"] = q.Workers opts["MaxWorkers"] = q.MaxWorkers opts["BlockTimeout"] = q.BlockTimeout diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 546802715f14..639e6d7c1e4c 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -24,7 +24,7 @@ type QueueSettings struct { Network string Addresses string Password string - QueueName string + RedisQueueName string DBIndex int WrapIfNecessary bool MaxAttempts int @@ -45,14 +45,14 @@ func GetQueueSettings(name string) QueueSettings { sec := Cfg.Section("queue." + name) // DataDir is not directly inheritable q.DataDir = path.Join(Queue.DataDir, name) - // QueueName is not directly inheritable either - q.QueueName = name + Queue.QueueName + // RedisQueueName is not directly inheritable either + q.RedisQueueName = name + Queue.RedisQueueName for _, key := range sec.Keys() { switch key.Name() { case "DATADIR": q.DataDir = key.MustString(q.DataDir) - case "QUEUE_NAME": - q.QueueName = key.MustString(q.QueueName) + case "REDIS_QUEUE_NAME": + q.RedisQueueName = key.MustString(q.RedisQueueName) } } if !path.IsAbs(q.DataDir) { @@ -98,7 +98,7 @@ func NewQueueService() { Queue.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(1 * time.Second) Queue.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(5 * time.Minute) Queue.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(5) - Queue.QueueName = sec.Key("QUEUE_NAME").MustString("_queue") + Queue.RedisQueueName = sec.Key("REDIS_QUEUE_NAME").MustString("_queue") // Now handle the old issue_indexer configuration section := Cfg.Section("queue.issue_indexer") From 0c345db8b2a3d68e27bdf08f92bcde3dbd58f21a Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 7 Jan 2020 09:53:26 +0000 Subject: [PATCH 35/35] Revert "Rename QUEUE_NAME REDIS_QUEUE_NAME" This reverts commit 1f83b4fc9b9dabda186257b38c265fe7012f90df. --- .../doc/advanced/config-cheat-sheet.en-us.md | 2 +- modules/queue/queue_redis.go | 28 +++++++++---------- modules/queue/setting.go | 2 +- modules/setting/queue.go | 12 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index a92682c9c396..dc6a1ba34697 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -247,7 +247,7 @@ relation to port exhaustion. - `LENGTH`: **20**: Maximal queue size before channel queues block - `BATCH_LENGTH`: **20**: Batch data before passing to the handler - `CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Connection string for the redis queue type. -- `REDIS_QUEUE_NAME`: **_queue**: The suffix for default redis queue name. Individual queues will default to **`name`**`REDIS_QUEUE_NAME` but can be overriden in the specific `queue.name` section. +- `QUEUE_NAME`: **_queue**: The suffix for default redis queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overriden in the specific `queue.name` section. - `WRAP_IF_NECESSARY`: **true**: Will wrap queues with a timeoutable queue if the selected queue is not ready to be created - (Only relevant for the level queue.) - `MAX_ATTEMPTS`: **10**: Maximum number of attempts to create the wrapped queue - `TIMEOUT`: **GRACEFUL_HAMMER_TIME + 30s**: Timeout the creation of the wrapped queue if it takes longer than this to create. diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go index a9406864a35a..14e68937a5b8 100644 --- a/modules/queue/queue_redis.go +++ b/modules/queue/queue_redis.go @@ -44,19 +44,19 @@ type RedisQueue struct { // RedisQueueConfiguration is the configuration for the redis queue type RedisQueueConfiguration struct { - Network string - Addresses string - Password string - DBIndex int - BatchLength int - QueueLength int - RedisQueueName string - Workers int - MaxWorkers int - BlockTimeout time.Duration - BoostTimeout time.Duration - BoostWorkers int - Name string + Network string + Addresses string + Password string + DBIndex int + BatchLength int + QueueLength int + QueueName string + Workers int + MaxWorkers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int + Name string } // NewRedisQueue creates single redis or cluster redis queue @@ -84,7 +84,7 @@ func NewRedisQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) boostWorkers: config.BoostWorkers, maxNumberOfWorkers: config.MaxWorkers, }, - queueName: config.RedisQueueName, + queueName: config.QueueName, exemplar: exemplar, closed: make(chan struct{}), workers: config.Workers, diff --git a/modules/queue/setting.go b/modules/queue/setting.go index 9ae8cf0dd712..d5a6b41882a0 100644 --- a/modules/queue/setting.go +++ b/modules/queue/setting.go @@ -36,7 +36,7 @@ func CreateQueue(name string, handle HandlerFunc, exemplar interface{}) Queue { opts["Network"] = q.Network opts["Password"] = q.Password opts["DBIndex"] = q.DBIndex - opts["RedisQueueName"] = q.RedisQueueName + opts["QueueName"] = q.QueueName opts["Workers"] = q.Workers opts["MaxWorkers"] = q.MaxWorkers opts["BlockTimeout"] = q.BlockTimeout diff --git a/modules/setting/queue.go b/modules/setting/queue.go index 639e6d7c1e4c..546802715f14 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -24,7 +24,7 @@ type QueueSettings struct { Network string Addresses string Password string - RedisQueueName string + QueueName string DBIndex int WrapIfNecessary bool MaxAttempts int @@ -45,14 +45,14 @@ func GetQueueSettings(name string) QueueSettings { sec := Cfg.Section("queue." + name) // DataDir is not directly inheritable q.DataDir = path.Join(Queue.DataDir, name) - // RedisQueueName is not directly inheritable either - q.RedisQueueName = name + Queue.RedisQueueName + // QueueName is not directly inheritable either + q.QueueName = name + Queue.QueueName for _, key := range sec.Keys() { switch key.Name() { case "DATADIR": q.DataDir = key.MustString(q.DataDir) - case "REDIS_QUEUE_NAME": - q.RedisQueueName = key.MustString(q.RedisQueueName) + case "QUEUE_NAME": + q.QueueName = key.MustString(q.QueueName) } } if !path.IsAbs(q.DataDir) { @@ -98,7 +98,7 @@ func NewQueueService() { Queue.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(1 * time.Second) Queue.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(5 * time.Minute) Queue.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(5) - Queue.RedisQueueName = sec.Key("REDIS_QUEUE_NAME").MustString("_queue") + Queue.QueueName = sec.Key("QUEUE_NAME").MustString("_queue") // Now handle the old issue_indexer configuration section := Cfg.Section("queue.issue_indexer")