* Queue: Add generic graceful queues with settings
* Queue & Setting: Add worker pool implementation
* Queue: Add worker settings
* Queue: Make resizing worker pools
* Queue: Add name variable to queues
* Queue: Add monitoring
* Queue: Improve logging
* Issues: Gracefulise the issues indexer
Remove the old now unused specific queues
* Task: Move to generic queue and gracefulise
* Issues: Standardise the issues indexer queue settings
* Fix test
* Queue: Allow Redis to connect to unix
* Prevent deadlock during early shutdown of issue indexer
* Add MaxWorker settings to queues
* Merge branch 'master' into graceful-queues
* Update modules/indexer/issues/indexer.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Update modules/indexer/issues/indexer.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Update modules/queue/queue_channel.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Update modules/queue/queue_disk.go
* Update modules/queue/queue_disk_channel.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Rename queue.Description to queue.ManagedQueue as per @guillep2k
* Cancel pool workers when removed
* Remove dependency on queue from setting
* Update modules/queue/queue_redis.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* As per @guillep2k add mutex locks on shutdown/terminate
* move unlocking out of setInternal
* Add warning if number of workers < 0
* Small changes as per @guillep2k
* No redis host specified not found
* Clean up documentation for queues
* Update docs/content/doc/advanced/config-cheat-sheet.en-us.md
* Update modules/indexer/issues/indexer_test.go
* Ensure that persistable channel queue is added to manager
* Rename QUEUE_NAME REDIS_QUEUE_NAME
* Revert "Rename QUEUE_NAME REDIS_QUEUE_NAME"
This reverts commit 1f83b4fc9b
.
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: techknowlogick <matti@mdranta.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
tags/v1.21.12.1
@@ -382,6 +382,39 @@ REPO_INDEXER_INCLUDE = | |||
; A comma separated list of glob patterns to exclude from the index; ; default is empty | |||
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 | |||
; 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" | |||
; 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. | |||
DISABLE_REGULAR_ORG_CREATION = false | |||
@@ -226,6 +226,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. | |||
@@ -239,6 +240,24 @@ 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` 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` 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`: **_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. | |||
- 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 | |||
@@ -614,6 +633,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`. | |||
@@ -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) | |||
@@ -25,6 +25,10 @@ func (db *DBIndexer) Delete(ids ...int64) error { | |||
return nil | |||
} | |||
// Close dummy function | |||
func (db *DBIndexer) Close() { | |||
} | |||
// 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) | |||
@@ -5,12 +5,16 @@ | |||
package issues | |||
import ( | |||
"context" | |||
"fmt" | |||
"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,12 +48,14 @@ type Indexer interface { | |||
Index(issue []*IndexerData) error | |||
Delete(ids ...int64) error | |||
Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error) | |||
Close() | |||
} | |||
type indexerHolder struct { | |||
indexer Indexer | |||
mutex sync.RWMutex | |||
cond *sync.Cond | |||
indexer Indexer | |||
mutex sync.RWMutex | |||
cond *sync.Cond | |||
cancelled bool | |||
} | |||
func newIndexerHolder() *indexerHolder { | |||
@@ -58,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() | |||
@@ -68,16 +81,15 @@ 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 | |||
} | |||
var ( | |||
issueIndexerChannel = make(chan *IndexerData, setting.Indexer.UpdateQueueLength) | |||
// issueIndexerQueue queue of issue ids to be updated | |||
issueIndexerQueue Queue | |||
issueIndexerQueue queue.Queue | |||
holder = newIndexerHolder() | |||
) | |||
@@ -85,90 +97,99 @@ 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) { | |||
indexer := holder.get() | |||
if indexer == nil { | |||
log.Error("Issue indexer handler: unable to get indexer!") | |||
return | |||
} | |||
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 { | |||
_ = indexer.Delete(indexerData.IDs...) | |||
continue | |||
} | |||
iData = append(iData, indexerData) | |||
} | |||
if err := indexer.Index(iData); err != nil { | |||
log.Error("Error whilst indexing: %v Error: %v", iData, err) | |||
} | |||
} | |||
issueIndexerQueue = queue.CreateQueue("issue_indexer", handler, &IndexerData{}) | |||
if issueIndexerQueue == nil { | |||
log.Fatal("Unable to create issue indexer queue") | |||
} | |||
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 { | |||
holder.cancel() | |||
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 { | |||
issueIndexer.Close() | |||
} | |||
log.Info("PID: %d Issue Indexer closed", os.Getpid()) | |||
}) | |||
log.Debug("Created Bleve Indexer") | |||
}) | |||
case "db": | |||
issueIndexer := &DBIndexer{} | |||
holder.set(issueIndexer) | |||
dummyQueue = true | |||
default: | |||
holder.cancel() | |||
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) | |||
close(waitChannel) | |||
}() | |||
if syncReindex { | |||
<-waitChannel | |||
select { | |||
case <-waitChannel: | |||
case <-graceful.GetManager().IsShutdown(): | |||
} | |||
} else if setting.Indexer.StartupTimeout > 0 { | |||
go func() { | |||
timeout := setting.Indexer.StartupTimeout | |||
@@ -178,7 +199,12 @@ 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() | |||
} | |||
log.Fatal("Issue Indexer Initialization timed-out after: %v", timeout) | |||
} | |||
}() | |||
@@ -186,8 +212,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 +232,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 +276,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,17 +301,25 @@ 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 | |||
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("SearchIssuesByKeyword(): 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 | |||
} | |||
@@ -15,6 +15,8 @@ import ( | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/setting" | |||
"gopkg.in/ini.v1" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
@@ -24,6 +26,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 +44,7 @@ func TestBleveSearchIssues(t *testing.T) { | |||
}() | |||
setting.Indexer.IssueType = "bleve" | |||
setting.NewQueueService() | |||
InitIssueIndexer(true) | |||
defer func() { | |||
indexer := holder.get() | |||
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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) | |||
} |
@@ -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() | |||
} |
@@ -0,0 +1,270 @@ | |||
// 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" | |||
"code.gitea.io/gitea/modules/log" | |||
) | |||
var manager *Manager | |||
// Manager is a queue manager | |||
type Manager struct { | |||
mutex sync.Mutex | |||
counter int64 | |||
Queues map[int64]*ManagedQueue | |||
} | |||
// ManagedQueue represents a working queue inheriting from Gitea. | |||
type ManagedQueue struct { | |||
mutex sync.Mutex | |||
QID int64 | |||
Queue Queue | |||
Type Type | |||
Name string | |||
Configuration interface{} | |||
ExemplarType string | |||
Pool ManagedPool | |||
counter int64 | |||
PoolWorkers map[int64]*PoolWorkers | |||
} | |||
// 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 | |||
SetMaxNumberOfWorkers(int) | |||
BoostTimeout() time.Duration | |||
BlockTimeout() time.Duration | |||
BoostWorkers() int | |||
SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) | |||
} | |||
// ManagedQueueList implements the sort.Interface | |||
type ManagedQueueList []*ManagedQueue | |||
// 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]*ManagedQueue), | |||
} | |||
} | |||
return manager | |||
} | |||
// Add adds a queue to this manager | |||
func (m *Manager) Add(queue Queue, | |||
t Type, | |||
configuration, | |||
exemplar interface{}, | |||
pool ManagedPool) int64 { | |||
cfg, _ := json.Marshal(configuration) | |||
mq := &ManagedQueue{ | |||
Queue: queue, | |||
Type: t, | |||
Configuration: string(cfg), | |||
ExemplarType: reflect.TypeOf(exemplar).String(), | |||
PoolWorkers: make(map[int64]*PoolWorkers), | |||
Pool: pool, | |||
} | |||
m.mutex.Lock() | |||
m.counter++ | |||
mq.QID = m.counter | |||
mq.Name = fmt.Sprintf("queue-%d", mq.QID) | |||
if named, ok := queue.(Named); ok { | |||
mq.Name = named.Name() | |||
} | |||
m.Queues[mq.QID] = mq | |||
m.mutex.Unlock() | |||
log.Trace("Queue Manager registered: %s (QID: %d)", mq.Name, mq.QID) | |||
return mq.QID | |||
} | |||
// Remove a queue from the Manager | |||
func (m *Manager) Remove(qid int64) { | |||
m.mutex.Lock() | |||
delete(m.Queues, qid) | |||
m.mutex.Unlock() | |||
log.Trace("Queue Manager removed: QID: %d", qid) | |||
} | |||
// GetManagedQueue by qid | |||
func (m *Manager) GetManagedQueue(qid int64) *ManagedQueue { | |||
m.mutex.Lock() | |||
defer m.mutex.Unlock() | |||
return m.Queues[qid] | |||
} | |||
// ManagedQueues returns the managed queues | |||
func (m *Manager) ManagedQueues() []*ManagedQueue { | |||
m.mutex.Lock() | |||
mqs := make([]*ManagedQueue, 0, len(m.Queues)) | |||
for _, mq := range m.Queues { | |||
mqs = append(mqs, mq) | |||
} | |||
m.mutex.Unlock() | |||
sort.Sort(ManagedQueueList(mqs)) | |||
return mqs | |||
} | |||
// Workers returns the poolworkers | |||
func (q *ManagedQueue) 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 *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++ | |||
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 *ManagedQueue) 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 *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 | |||
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) | |||
} | |||
return nil | |||
} | |||
// NumberOfWorkers returns the number of workers in the queue | |||
func (q *ManagedQueue) NumberOfWorkers() int { | |||
if q.Pool != nil { | |||
return q.Pool.NumberOfWorkers() | |||
} | |||
return -1 | |||
} | |||
// MaxNumberOfWorkers returns the maximum number of workers for the pool | |||
func (q *ManagedQueue) MaxNumberOfWorkers() int { | |||
if q.Pool != nil { | |||
return q.Pool.MaxNumberOfWorkers() | |||
} | |||
return 0 | |||
} | |||
// BoostWorkers returns the number of workers for a boost | |||
func (q *ManagedQueue) BoostWorkers() int { | |||
if q.Pool != nil { | |||
return q.Pool.BoostWorkers() | |||
} | |||
return -1 | |||
} | |||
// BoostTimeout returns the timeout of the next boost | |||
func (q *ManagedQueue) BoostTimeout() time.Duration { | |||
if q.Pool != nil { | |||
return q.Pool.BoostTimeout() | |||
} | |||
return 0 | |||
} | |||
// BlockTimeout returns the timeout til the next boost | |||
func (q *ManagedQueue) BlockTimeout() time.Duration { | |||
if q.Pool != nil { | |||
return q.Pool.BlockTimeout() | |||
} | |||
return 0 | |||
} | |||
// SetSettings sets the setable boost values | |||
func (q *ManagedQueue) SetSettings(maxNumberOfWorkers, boostWorkers int, timeout time.Duration) { | |||
if q.Pool != nil { | |||
q.Pool.SetSettings(maxNumberOfWorkers, boostWorkers, timeout) | |||
} | |||
} | |||
func (l ManagedQueueList) Len() int { | |||
return len(l) | |||
} | |||
func (l ManagedQueueList) Less(i, j int) bool { | |||
return l[i].Name < l[j].Name | |||
} | |||
func (l ManagedQueueList) 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] | |||
} |
@@ -0,0 +1,133 @@ | |||
// 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() | |||
} | |||
// 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())) | |||
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 | |||
} | |||
// 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) | |||
} | |||
return newFn(handlerFunc, opts, exemplar) | |||
} |
@@ -0,0 +1,106 @@ | |||
// 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" | |||
"time" | |||
"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 | |||
BatchLength int | |||
Workers int | |||
MaxWorkers int | |||
BlockTimeout time.Duration | |||
BoostTimeout time.Duration | |||
BoostWorkers int | |||
Name string | |||
} | |||
// ChannelQueue implements | |||
type ChannelQueue struct { | |||
pool *WorkerPool | |||
exemplar interface{} | |||
workers int | |||
name string | |||
} | |||
// 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) | |||
if config.BatchLength == 0 { | |||
config.BatchLength = 1 | |||
} | |||
dataChan := make(chan Data, config.QueueLength) | |||
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, | |||
maxNumberOfWorkers: config.MaxWorkers, | |||
}, | |||
exemplar: exemplar, | |||
workers: config.Workers, | |||
name: config.Name, | |||
} | |||
queue.pool.qid = GetManager().Add(queue, ChannelQueueType, config, exemplar, queue.pool) | |||
return queue, nil | |||
} | |||
// Run starts to run the queue | |||
func (c *ChannelQueue) Run(atShutdown, atTerminate func(context.Context, func())) { | |||
atShutdown(context.Background(), func() { | |||
log.Warn("ChannelQueue: %s is not shutdownable!", c.name) | |||
}) | |||
atTerminate(context.Background(), func() { | |||
log.Warn("ChannelQueue: %s is not terminatable!", c.name) | |||
}) | |||
go func() { | |||
_ = c.pool.AddWorkers(c.workers, 0) | |||
}() | |||
} | |||
// 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 | |||
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.pool.Push(data) | |||
return nil | |||
} | |||
// Name returns the name of this queue | |||
func (c *ChannelQueue) Name() string { | |||
return c.name | |||
} | |||
func init() { | |||
queuesMap[ChannelQueueType] = NewChannelQueue | |||
} |
@@ -0,0 +1,91 @@ | |||
// 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" | |||
"time" | |||
"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, | |||
Workers: 1, | |||
MaxWorkers: 10, | |||
BlockTimeout: 1 * time.Second, | |||
BoostTimeout: 5 * time.Minute, | |||
BoostWorkers: 5, | |||
}, &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) | |||
} | |||
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, | |||
MaxWorkers: 10, | |||
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) | |||
} |
@@ -0,0 +1,213 @@ | |||
// 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" | |||
"sync" | |||
"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 | |||
QueueLength int | |||
BatchLength int | |||
Workers int | |||
MaxWorkers int | |||
BlockTimeout time.Duration | |||
BoostTimeout time.Duration | |||
BoostWorkers int | |||
Name string | |||
} | |||
// LevelQueue implements a disk library queue | |||
type LevelQueue struct { | |||
pool *WorkerPool | |||
queue *levelqueue.Queue | |||
closed chan struct{} | |||
terminated chan struct{} | |||
lock sync.Mutex | |||
exemplar interface{} | |||
workers int | |||
name string | |||
} | |||
// 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) | |||
internal, err := levelqueue.Open(config.DataDir) | |||
if err != nil { | |||
return nil, err | |||
} | |||
dataChan := make(chan Data, config.QueueLength) | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
queue := &LevelQueue{ | |||
pool: &WorkerPool{ | |||
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, | |||
closed: make(chan struct{}), | |||
terminated: make(chan struct{}), | |||
workers: config.Workers, | |||
name: config.Name, | |||
} | |||
queue.pool.qid = GetManager().Add(queue, LevelQueueType, config, exemplar, queue.pool) | |||
return queue, 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) | |||
go func() { | |||
_ = l.pool.AddWorkers(l.workers, 0) | |||
}() | |||
go l.readToChan() | |||
log.Trace("LevelQueue: %s Waiting til closed", l.name) | |||
<-l.closed | |||
log.Trace("LevelQueue: %s Waiting til done", l.name) | |||
l.pool.Wait() | |||
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) | |||
} | |||
func (l *LevelQueue) readToChan() { | |||
for { | |||
select { | |||
case <-l.closed: | |||
// tell the pool to shutdown. | |||
l.pool.cancel() | |||
return | |||
default: | |||
bs, err := l.queue.RPop() | |||
if err != nil { | |||
if err != levelqueue.ErrNotFound { | |||
log.Error("LevelQueue: %s Error on RPop: %v", l.name, 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("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) | |||
l.pool.Push(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() { | |||
l.lock.Lock() | |||
defer l.lock.Unlock() | |||
log.Trace("LevelQueue: %s Shutdown", l.name) | |||
select { | |||
case <-l.closed: | |||
default: | |||
close(l.closed) | |||
} | |||
} | |||
// Terminate this queue and close the queue | |||
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) | |||
} | |||
} | |||
} | |||
// Name returns the name of this queue | |||
func (l *LevelQueue) Name() string { | |||
return l.name | |||
} | |||
func init() { | |||
queuesMap[LevelQueueType] = NewLevelQueue | |||
} |
@@ -0,0 +1,193 @@ | |||
// 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" | |||
) | |||
// PersistableChannelQueueType is the type for persistable queue | |||
const PersistableChannelQueueType Type = "persistable-channel" | |||
// PersistableChannelQueueConfiguration is the configuration for a PersistableChannelQueue | |||
type PersistableChannelQueueConfiguration struct { | |||
Name string | |||
DataDir string | |||
BatchLength int | |||
QueueLength int | |||
Timeout time.Duration | |||
MaxAttempts int | |||
Workers int | |||
MaxWorkers int | |||
BlockTimeout time.Duration | |||
BoostTimeout time.Duration | |||
BoostWorkers int | |||
} | |||
// PersistableChannelQueue wraps a channel queue and level queue together | |||
type PersistableChannelQueue struct { | |||
*ChannelQueue | |||
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) | |||
channelQueue, err := NewChannelQueue(handle, ChannelQueueConfiguration{ | |||
QueueLength: config.QueueLength, | |||
BatchLength: config.BatchLength, | |||
Workers: config.Workers, | |||
MaxWorkers: config.MaxWorkers, | |||
BlockTimeout: config.BlockTimeout, | |||
BoostTimeout: config.BoostTimeout, | |||
BoostWorkers: config.BoostWorkers, | |||
Name: config.Name + "-channel", | |||
}, exemplar) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// the level backend only needs temporary workers to catch up with the previously dropped work | |||
levelCfg := LevelQueueConfiguration{ | |||
DataDir: config.DataDir, | |||
QueueLength: config.QueueLength, | |||
BatchLength: config.BatchLength, | |||
Workers: 1, | |||
MaxWorkers: 6, | |||
BlockTimeout: 1 * time.Second, | |||
BoostTimeout: 5 * time.Minute, | |||
BoostWorkers: 5, | |||
Name: config.Name + "-level", | |||
} | |||
levelQueue, err := NewLevelQueue(handle, levelCfg, exemplar) | |||
if err == nil { | |||
queue := &PersistableChannelQueue{ | |||
ChannelQueue: channelQueue.(*ChannelQueue), | |||
delayedStarter: delayedStarter{ | |||
internal: levelQueue.(*LevelQueue), | |||
name: config.Name, | |||
}, | |||
closed: make(chan struct{}), | |||
} | |||
_ = GetManager().Add(queue, PersistableChannelQueueType, config, exemplar, nil) | |||
return queue, nil | |||
} | |||
if IsErrInvalidConfiguration(err) { | |||
// Retrying ain't gonna make this any better... | |||
return nil, ErrInvalidConfiguration{cfg: cfg} | |||
} | |||
queue := &PersistableChannelQueue{ | |||
ChannelQueue: channelQueue.(*ChannelQueue), | |||
delayedStarter: delayedStarter{ | |||
cfg: levelCfg, | |||
underlying: LevelQueueType, | |||
timeout: config.Timeout, | |||
maxAttempts: config.MaxAttempts, | |||
name: config.Name, | |||
}, | |||
closed: make(chan struct{}), | |||
} | |||
_ = GetManager().Add(queue, PersistableChannelQueueType, config, exemplar, nil) | |||
return queue, 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 { | |||
case <-p.closed: | |||
return p.internal.Push(data) | |||
default: | |||
return p.ChannelQueue.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 { | |||
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() | |||
} | |||
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()) {}) | |||
go func() { | |||
_ = 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("PersistableChannelQueue: %s Shutdown", p.delayedStarter.name) | |||
select { | |||
case <-p.closed: | |||
default: | |||
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("PersistableChannelQueue: %s Terminating", p.delayedStarter.name) | |||
p.Shutdown() | |||
p.lock.Lock() | |||
defer p.lock.Unlock() | |||
if p.internal != nil { | |||
p.internal.(*LevelQueue).Terminate() | |||
} | |||
} | |||
func init() { | |||
queuesMap[PersistableChannelQueueType] = NewPersistableChannelQueue | |||
} |
@@ -0,0 +1,117 @@ | |||
// 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 | |||
} | |||
} | |||
queueShutdown := []func(){} | |||
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, | |||
Workers: 1, | |||
MaxWorkers: 10, | |||
}, &testData{}) | |||
assert.NoError(t, err) | |||
go queue.Run(func(_ context.Context, shutdown func()) { | |||
queueShutdown = append(queueShutdown, shutdown) | |||
}, func(_ context.Context, terminate func()) { | |||
queueTerminate = append(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) | |||
for _, callback := range queueShutdown { | |||
callback() | |||
} | |||
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: | |||
} | |||
for _, callback := range queueTerminate { | |||
callback() | |||
} | |||
// Reopen queue | |||
queue, err = NewPersistableChannelQueue(handle, PersistableChannelQueueConfiguration{ | |||
DataDir: tmpDir, | |||
BatchLength: 2, | |||
QueueLength: 20, | |||
Workers: 1, | |||
MaxWorkers: 10, | |||
}, &testData{}) | |||
assert.NoError(t, err) | |||
go queue.Run(func(_ context.Context, shutdown func()) { | |||
queueShutdown = append(queueShutdown, shutdown) | |||
}, func(_ context.Context, terminate func()) { | |||
queueTerminate = append(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) | |||
for _, callback := range queueShutdown { | |||
callback() | |||
} | |||
for _, callback := range queueTerminate { | |||
callback() | |||
} | |||
} |
@@ -0,0 +1,126 @@ | |||
// 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 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 | |||
} | |||
} | |||
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: tmpDir, | |||
BatchLength: 2, | |||
Workers: 1, | |||
MaxWorkers: 10, | |||
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 = append(queueShutdown, shutdown) | |||
}, func(_ context.Context, terminate func()) { | |||
queueTerminate = append(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) | |||
for _, callback := range queueShutdown { | |||
callback() | |||
} | |||
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: | |||
} | |||
for _, callback := range queueTerminate { | |||
callback() | |||
} | |||
// Reopen queue | |||
queue, err = NewWrappedQueue(handle, | |||
WrappedQueueConfiguration{ | |||
Underlying: LevelQueueType, | |||
Config: LevelQueueConfiguration{ | |||
DataDir: tmpDir, | |||
BatchLength: 2, | |||
Workers: 1, | |||
MaxWorkers: 10, | |||
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 = append(queueShutdown, shutdown) | |||
}, func(_ context.Context, terminate func()) { | |||
queueTerminate = append(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) | |||
for _, callback := range queueShutdown { | |||
callback() | |||
} | |||
for _, callback := range queueTerminate { | |||
callback() | |||
} | |||
} |
@@ -0,0 +1,234 @@ | |||
// 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" | |||
"sync" | |||
"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 { | |||
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 | |||
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 | |||
} | |||
// 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, ",") | |||
dataChan := make(chan Data, config.QueueLength) | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
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, | |||
maxNumberOfWorkers: config.MaxWorkers, | |||
}, | |||
queueName: config.QueueName, | |||
exemplar: exemplar, | |||
closed: make(chan struct{}), | |||
workers: config.Workers, | |||
name: config.Name, | |||
} | |||
if len(dbs) == 0 { | |||
return nil, errors.New("no redis host specified") | |||
} 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 | |||
}) | |||
} else { | |||
queue.client = redis.NewClusterClient(&redis.ClusterOptions{ | |||
Addrs: dbs, | |||
}) | |||
} | |||
if err := queue.client.Ping().Err(); err != nil { | |||
return nil, err | |||
} | |||
queue.pool.qid = GetManager().Add(queue, RedisQueueType, config, exemplar, queue.pool) | |||
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) | |||
go func() { | |||
_ = r.pool.AddWorkers(r.workers, 0) | |||
}() | |||
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() | |||
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() { | |||
for { | |||
select { | |||
case <-r.closed: | |||
// 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("RedisQueue: %s Error on LPop: %v", r.name, err) | |||
time.Sleep(time.Millisecond * 100) | |||
continue | |||
} | |||
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("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) | |||
r.pool.Push(data) | |||
} | |||
} | |||
} | |||
// 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() { | |||
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() | |||
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) | |||
} | |||
} | |||
} | |||
// Name returns the name of this queue | |||
func (r *RedisQueue) Name() string { | |||
return r.name | |||
} | |||
func init() { | |||
queuesMap[RedisQueueType] = NewRedisQueue | |||
} |
@@ -0,0 +1,43 @@ | |||
// 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" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
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) | |||
} |
@@ -0,0 +1,206 @@ | |||
// 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 | |||
Name string | |||
} | |||
type delayedStarter struct { | |||
lock sync.Mutex | |||
internal Queue | |||
underlying Type | |||
cfg interface{} | |||
timeout time.Duration | |||
maxAttempts int | |||
name string | |||
} | |||
// 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 { | |||
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(): | |||
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 { | |||
q.internal = queue | |||
q.lock.Unlock() | |||
break | |||
} | |||
if err.Error() != "resource temporarily unavailable" { | |||
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 { | |||
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) | |||
} | |||
t := time.NewTimer(sleepTime) | |||
select { | |||
case <-ctx.Done(): | |||
t.Stop() | |||
case <-t.C: | |||
} | |||
} | |||
} | |||
return nil | |||
} | |||
// 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 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 := NewQueue(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} | |||
} | |||
queue = &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, | |||
name: config.Name, | |||
}, | |||
} | |||
_ = GetManager().Add(queue, WrappedQueueType, config, exemplar, 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 | |||
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 { | |||
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) | |||
} | |||
}() | |||
} else { | |||
q.lock.Unlock() | |||
} | |||
q.internal.Run(atShutdown, atTerminate) | |||
log.Trace("WrappedQueue: %s Done", q.name) | |||
} | |||
// Shutdown this queue and stop processing | |||
func (q *WrappedQueue) Shutdown() { | |||
log.Trace("WrappedQueue: %s Shutdown", q.name) | |||
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() { | |||
log.Trace("WrappedQueue: %s Terminating", q.name) | |||
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 | |||
} |
@@ -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 | |||
} |
@@ -0,0 +1,325 @@ | |||
// 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 | |||
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 && (p.numberOfWorkers <= p.maxNumberOfWorkers || p.maxNumberOfWorkers < 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.numberOfWorkers > p.maxNumberOfWorkers && p.maxNumberOfWorkers >= 0) { | |||
p.lock.Unlock() | |||
p.dataChan <- data | |||
return | |||
} | |||
p.blockTimeout *= 2 | |||
ctx, cancel := context.WithCancel(p.baseCtx) | |||
mq := GetManager().GetManagedQueue(p.qid) | |||
boost := p.boostWorkers | |||
if (boost+p.numberOfWorkers) > p.maxNumberOfWorkers && p.maxNumberOfWorkers >= 0 { | |||
boost = p.maxNumberOfWorkers - p.numberOfWorkers | |||
} | |||
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 := mq.RegisterWorkers(boost, start, false, start, cancel) | |||
go func() { | |||
<-ctx.Done() | |||
mq.RemoveWorkers(pid) | |||
cancel() | |||
}() | |||
} else { | |||
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) | |||
cancel() | |||
p.lock.Lock() | |||
p.blockTimeout /= 2 | |||
p.lock.Unlock() | |||
}() | |||
p.addWorkers(ctx, boost) | |||
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 | |||
} | |||
// 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 | |||
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) | |||
} | |||
mq := GetManager().GetManagedQueue(p.qid) | |||
if mq != nil { | |||
pid := mq.RegisterWorkers(number, start, hasTimeout, end, cancel) | |||
go func() { | |||
<-ctx.Done() | |||
mq.RemoveWorkers(pid) | |||
cancel() | |||
}() | |||
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) | |||
} | |||
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 { | |||
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() | |||
} | |||
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("WorkerPool: %d CleanUp", p.qid) | |||
close(p.dataChan) | |||
for data := range p.dataChan { | |||
p.handle(data) | |||
select { | |||
case <-ctx.Done(): | |||
log.Warn("WorkerPool: %d Cleanup context closed before finishing clean-up", p.qid) | |||
return | |||
default: | |||
} | |||
} | |||
log.Trace("WorkerPool: %d CleanUp Done", p.qid) | |||
} | |||
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) | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,159 @@ | |||
// 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 ( | |||
"fmt" | |||
"path" | |||
"strconv" | |||
"strings" | |||
"time" | |||
"code.gitea.io/gitea/modules/log" | |||
) | |||
// QueueSettings represent the settings for a queue from the ini | |||
type QueueSettings struct { | |||
DataDir string | |||
Length int | |||
BatchLength int | |||
ConnectionString string | |||
Type string | |||
Network string | |||
Addresses string | |||
Password string | |||
QueueName string | |||
DBIndex int | |||
WrapIfNecessary bool | |||
MaxAttempts int | |||
Timeout time.Duration | |||
Workers int | |||
MaxWorkers int | |||
BlockTimeout time.Duration | |||
BoostTimeout time.Duration | |||
BoostWorkers int | |||
} | |||
// Queue settings | |||
var Queue = 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) | |||
// 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) { | |||
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) | |||
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) | |||
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) | |||
q.Network, q.Addresses, q.Password, q.DBIndex, _ = ParseQueueConnStr(q.ConnectionString) | |||
return q | |||
} | |||
// 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) { | |||
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, "")) | |||
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) | |||
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) | |||
Queue.QueueName = sec.Key("QUEUE_NAME").MustString("_queue") | |||
// 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 | |||
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) | |||
if len(items) < 2 { | |||
continue | |||
} | |||
switch strings.ToLower(items[0]) { | |||
case "network": | |||
network = items[1] | |||
case "addrs": | |||
addrs = items[1] | |||
case "password": | |||
password = items[1] | |||
case "db": | |||
dbIdx, err = strconv.Atoi(items[1]) | |||
if err != nil { | |||
return | |||
} | |||
} | |||
} | |||
return | |||
} |
@@ -1093,4 +1093,5 @@ func NewServices() { | |||
newMigrationsService() | |||
newIndexerService() | |||
newTaskService() | |||
NewQueueService() | |||
} |
@@ -4,22 +4,15 @@ | |||
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", | |||
} | |||
) | |||
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("persistable-channel") | |||
case 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")) | |||
} |
@@ -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() | |||
} |
@@ -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) | |||
} |
@@ -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 | |||
} |
@@ -8,14 +8,15 @@ 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/setting" | |||
"code.gitea.io/gitea/modules/queue" | |||
"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 +24,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 = queue.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) | |||
@@ -1410,7 +1410,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 | |||
@@ -2026,6 +2026,54 @@ 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: <strong>%s</strong>? | |||
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.maxnumberworkers = Max 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.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 = Update 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 | |||
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 to block indefinitely. | |||
notices.system_notice_list = System Notices | |||
notices.view_detail_header = View Notice Details | |||
@@ -11,6 +11,7 @@ import ( | |||
"net/url" | |||
"os" | |||
"runtime" | |||
"strconv" | |||
"strings" | |||
"time" | |||
@@ -22,6 +23,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 +37,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 +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().ManagedQueues() | |||
ctx.HTML(200, tplMonitor) | |||
} | |||
@@ -366,3 +370,126 @@ 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") | |||
mq := queue.GetManager().GetManagedQueue(qid) | |||
if mq == nil { | |||
ctx.Status(404) | |||
return | |||
} | |||
ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", mq.Name) | |||
ctx.Data["PageIsAdmin"] = true | |||
ctx.Data["PageIsAdminMonitor"] = true | |||
ctx.Data["Queue"] = mq | |||
ctx.HTML(200, tplQueue) | |||
} | |||
// WorkerCancel cancels a worker group | |||
func WorkerCancel(ctx *context.Context) { | |||
qid := ctx.ParamsInt64("qid") | |||
mq := queue.GetManager().GetManagedQueue(qid) | |||
if mq == nil { | |||
ctx.Status(404) | |||
return | |||
} | |||
pid := ctx.ParamsInt64("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), | |||
}) | |||
} | |||
// AddWorkers adds workers to a worker group | |||
func AddWorkers(ctx *context.Context) { | |||
qid := ctx.ParamsInt64("qid") | |||
mq := queue.GetManager().GetManagedQueue(qid) | |||
if mq == 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 | |||
} | |||
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 | |||
} | |||
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)) | |||
} | |||
// 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) | |||
if mq == nil { | |||
ctx.Status(404) | |||
return | |||
} | |||
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 | |||
} | |||
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 = mq.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 = mq.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 = mq.Pool.BoostTimeout() | |||
} | |||
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)) | |||
} |
@@ -410,8 +410,16 @@ 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("/set", admin.SetQueueSettings) | |||
m.Post("/add", admin.AddWorkers) | |||
m.Post("/cancel/:pid", admin.WorkerCancel) | |||
}) | |||
}) | |||
m.Group("/users", func() { | |||
m.Get("", admin.Users) | |||
@@ -31,6 +31,34 @@ | |||
</table> | |||
</div> | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.queues"}} | |||
</h4> | |||
<div class="ui attached table segment"> | |||
<table class="ui very basic striped table"> | |||
<thead> | |||
<tr> | |||
<th>{{.i18n.Tr "admin.monitor.queue.name"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.type"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.exemplar"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.numberworkers"}}</th> | |||
<th></th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{{range .Queues}} | |||
<tr> | |||
<td>{{.Name}}</td> | |||
<td>{{.Type}}</td> | |||
<td>{{.ExemplarType}}</td> | |||
<td>{{$sum := .NumberOfWorkers}}{{if lt $sum 0}}-{{else}}{{$sum}}{{end}}</td> | |||
<td><a href="{{$.Link}}/queue/{{.QID}}" class="button">{{if lt $sum 0}}{{$.i18n.Tr "admin.monitor.queue.review"}}{{else}}{{$.i18n.Tr "admin.monitor.queue.review_add"}}{{end}}</a> | |||
</tr> | |||
{{end}} | |||
</tbody> | |||
</table> | |||
</div> | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.process"}} | |||
</h4> | |||
@@ -0,0 +1,147 @@ | |||
{{template "base/head" .}} | |||
<div class="admin monitor"> | |||
{{template "admin/navbar" .}} | |||
<div class="ui container"> | |||
{{template "base/alert" .}} | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.queue" .Queue.Name}} | |||
</h4> | |||
<div class="ui attached table segment"> | |||
<table class="ui very basic striped table"> | |||
<thead> | |||
<tr> | |||
<th>{{.i18n.Tr "admin.monitor.queue.name"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.type"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.exemplar"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.numberworkers"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.maxnumberworkers"}}</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr> | |||
<td>{{.Queue.Name}}</td> | |||
<td>{{.Queue.Type}}</td> | |||
<td>{{.Queue.ExemplarType}}</td> | |||
<td>{{$sum := .Queue.NumberOfWorkers}}{{if lt $sum 0}}-{{else}}{{$sum}}{{end}}</td> | |||
<td>{{if lt $sum 0}}-{{else}}{{.Queue.MaxNumberOfWorkers}}{{end}}</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
{{if lt $sum 0 }} | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.queue.nopool.title"}} | |||
</h4> | |||
<div class="ui attached segment"> | |||
{{if eq .Queue.Type "wrapped" }} | |||
<p>{{.i18n.Tr "admin.monitor.queue.wrapped.desc"}}</p> | |||
{{else if eq .Queue.Type "persistable-channel"}} | |||
<p>{{.i18n.Tr "admin.monitor.queue.persistable-channel.desc"}}</p> | |||
{{else}} | |||
<p>{{.i18n.Tr "admin.monitor.queue.nopool.desc"}}</p> | |||
{{end}} | |||
</div> | |||
{{else}} | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.queue.settings.title"}} | |||
</h4> | |||
<div class="ui attached segment"> | |||
<p>{{.i18n.Tr "admin.monitor.queue.settings.desc"}}</p> | |||
<form method="POST" action="{{.Link}}/set"> | |||
{{$.CsrfTokenHtml}} | |||
<div class="ui form"> | |||
<div class="inline field"> | |||
<label for="max-number">{{.i18n.Tr "admin.monitor.queue.settings.maxnumberworkers"}}</label> | |||
<input name="max-number" type="text" placeholder="{{.i18n.Tr "admin.monitor.queue.settings.maxnumberworkers.placeholder" .Queue.MaxNumberOfWorkers}}"> | |||
</div> | |||
<div class="inline field"> | |||
<label for="timeout">{{.i18n.Tr "admin.monitor.queue.settings.timeout"}}</label> | |||
<input name="timeout" type="text" placeholder="{{.i18n.Tr "admin.monitor.queue.settings.timeout.placeholder" .Queue.BoostTimeout }}"> | |||
</div> | |||
<div class="inline field"> | |||
<label for="number">{{.i18n.Tr "admin.monitor.queue.settings.numberworkers"}}</label> | |||
<input name="number" type="text" placeholder="{{.i18n.Tr "admin.monitor.queue.settings.numberworkers.placeholder" .Queue.BoostWorkers}}"> | |||
</div> | |||
<div class="inline field"> | |||
<label>{{.i18n.Tr "admin.monitor.queue.settings.blocktimeout"}}</label> | |||
<span>{{.i18n.Tr "admin.monitor.queue.settings.blocktimeout.value" .Queue.BlockTimeout}}</span> | |||
</div> | |||
<button class="ui submit button">{{.i18n.Tr "admin.monitor.queue.settings.submit"}}</button> | |||
</div> | |||
</form> | |||
</div> | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.queue.pool.addworkers.title"}} | |||
</h4> | |||
<div class="ui attached segment"> | |||
<p>{{.i18n.Tr "admin.monitor.queue.pool.addworkers.desc"}}</p> | |||
<form method="POST" action="{{.Link}}/add"> | |||
{{$.CsrfTokenHtml}} | |||
<div class="ui form"> | |||
<div class="fields"> | |||
<div class="field"> | |||
<label>{{.i18n.Tr "admin.monitor.queue.numberworkers"}}</label> | |||
<input name="number" type="text" placeholder="{{.i18n.Tr "admin.monitor.queue.pool.addworkers.numberworkers.placeholder"}}"> | |||
</div> | |||
<div class="field"> | |||
<label>{{.i18n.Tr "admin.monitor.queue.pool.timeout"}}</label> | |||
<input name="timeout" type="text" placeholder="{{.i18n.Tr "admin.monitor.queue.pool.addworkers.timeout.placeholder"}}"> | |||
</div> | |||
</div> | |||
<button class="ui submit button">{{.i18n.Tr "admin.monitor.queue.pool.addworkers.submit"}}</button> | |||
</div> | |||
</form> | |||
</div> | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.queue.pool.workers.title"}} | |||
</h4> | |||
<div class="ui attached table segment"> | |||
<table class="ui very basic striped table"> | |||
<thead> | |||
<tr> | |||
<th>{{.i18n.Tr "admin.monitor.queue.numberworkers"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.start"}}</th> | |||
<th>{{.i18n.Tr "admin.monitor.queue.pool.timeout"}}</th> | |||
<th></th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{{range .Queue.Workers}} | |||
<tr> | |||
<td>{{.Workers}}</td> | |||
<td>{{DateFmtLong .Start}}</td> | |||
<td>{{if .HasTimeout}}{{DateFmtLong .Timeout}}{{else}}-{{end}}</td> | |||
<td> | |||
<a class="delete-button" href="" data-url="{{$.Link}}/cancel/{{.PID}}" data-id="{{.PID}}" data-name="{{.Workers}}"><i class="close icon text red" title="{{$.i18n.Tr "remove"}}"></i></a> | |||
</td> | |||
</tr> | |||
{{else}} | |||
<tr> | |||
<td colspan="4">{{.i18n.Tr "admin.monitor.queue.pool.workers.none" }} | |||
</tr> | |||
{{end}} | |||
</tbody> | |||
</table> | |||
</div> | |||
{{end}} | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "admin.monitor.queue.configuration"}} | |||
</h4> | |||
<div class="ui attached segment"> | |||
<pre>{{.Queue.Configuration | JsonPrettyPrint}} | |||
</div> | |||
</div> | |||
</div> | |||
<div class="ui small basic delete modal"> | |||
<div class="ui icon header"> | |||
<i class="close icon"></i> | |||
{{.i18n.Tr "admin.monitor.queue.pool.cancel"}} | |||
</div> | |||
<div class="content"> | |||
<p>{{$.i18n.Tr "admin.monitor.queue.pool.cancel_notices" `<span class="name"></span>` | Safe}}</p> | |||
<p>{{$.i18n.Tr "admin.monitor.queue.pool.cancel_desc"}}</p> | |||
</div> | |||
{{template "base/delete_modal_actions" .}} | |||
</div> | |||
{{template "base/footer" .}} |