diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index c9ca821280b1b..4b810f91f7668 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -371,6 +371,19 @@ REPO_INDEXER_INCLUDE = ; A comma separated list of glob patterns to exclude from the index; ; default is empty REPO_INDEXER_EXCLUDE = +[queue] +; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy +; default to persistable-channel +TYPE = persistable-channel +; data-dir for storing persistable queues and level queues, individual queues will be named by their type +DATADIR = queues/ +; Default queue length before a channel queue will block +LENGTH = 20 +; Batch size to send for batched queues +BATCH_LENGTH = 20 +; Connection string for redis queues this will store the redis connection string. +CONN_STR = "addrs=127.0.0.1:6379 db=0" + [admin] ; Disallow regular (non-admin) users from creating organizations. DISABLE_REGULAR_ORG_CREATION = false diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index e71fb1b3bc3e1..6ffb43fcd89e4 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -234,6 +234,14 @@ relation to port exhaustion. - `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed. - `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to zero to never timeout. +## Queue (`queue`) + +- `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel`, `channel`, `level`, `redis`, `dummy` +- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. +- `LENGTH`: **20**: Maximal queue size before channel queues block +- `BATCH_LENGTH`: **20**: Batch data before passing to the handler +- `CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Connection string for the redis queue type. + ## Admin (`admin`) - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled diff --git a/integrations/integration_test.go b/integrations/integration_test.go index bf363f3b4ddc3..98759675b2202 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -178,6 +178,7 @@ func initIntegrationTest() { defer db.Close() } routers.GlobalInit(graceful.GetManager().HammerContext()) + NotifierListenerInit() } func prepareTestEnv(t testing.TB, skip ...int) func() { diff --git a/integrations/issue_test.go b/integrations/issue_test.go index fe66a005047fe..1454d75885019 100644 --- a/integrations/issue_test.go +++ b/integrations/issue_test.go @@ -11,8 +11,10 @@ import ( "strconv" "strings" "testing" + "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" @@ -87,7 +89,12 @@ func TestViewIssuesKeyword(t *testing.T) { defer prepareTestEnv(t)() repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) - + issue := models.AssertExistsAndLoadBean(t, &models.Issue{ + RepoID: repo.ID, + Index: 1, + }).(*models.Issue) + issues.UpdateIssueIndexer(issue) + time.Sleep(time.Second * 1) const keyword = "first" req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.RelLink(), keyword) resp := MakeRequest(t, req, http.StatusOK) diff --git a/integrations/notification_helper_test.go b/integrations/notification_helper_test.go new file mode 100644 index 0000000000000..d7ef5f74678d2 --- /dev/null +++ b/integrations/notification_helper_test.go @@ -0,0 +1,121 @@ +// 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 integrations + +import ( + "encoding/json" + "reflect" + "sync" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/notification/base" + "code.gitea.io/gitea/modules/queue" +) + +var notifierListener *NotifierListener + +var once = sync.Once{} + +type NotifierListener struct { + lock sync.RWMutex + callbacks map[string][]*func(string, [][]byte) + notifier base.Notifier +} + +func NotifierListenerInit() { + once.Do(func() { + notifierListener = &NotifierListener{ + callbacks: map[string][]*func(string, [][]byte){}, + } + notifierListener.notifier = base.NewQueueNotifierWithHandle("test-notifier", notifierListener.handle) + notification.RegisterNotifier(notifierListener.notifier) + }) +} + +// Register will register a callback with the provided notifier function +func (n *NotifierListener) Register(functionName string, callback *func(string, [][]byte)) { + n.lock.Lock() + n.callbacks[functionName] = append(n.callbacks[functionName], callback) + n.lock.Unlock() +} + +// Deregister will remove the provided callback from the provided notifier function +func (n *NotifierListener) Deregister(functionName string, callback *func(string, [][]byte)) { + n.lock.Lock() + found := -1 + for i, callbackPtr := range n.callbacks[functionName] { + if callbackPtr == callback { + found = i + break + } + } + if found > -1 { + n.callbacks[functionName] = append(n.callbacks[functionName][0:found], n.callbacks[functionName][found+1:]...) + } + n.lock.Unlock() +} + +// RegisterChannel will return a registered channel with function name and return a function to deregister it and close the channel at the end +func (n *NotifierListener) RegisterChannel(name string, argNumber int, exemplar interface{}) (<-chan interface{}, func()) { + t := reflect.TypeOf(exemplar) + channel := make(chan interface{}, 10) + callback := func(_ string, args [][]byte) { + n := reflect.New(t).Elem() + err := json.Unmarshal(args[argNumber], n.Addr().Interface()) + if err != nil { + log.Error("Wrong Argument passed to register channel: %v ", err) + } + channel <- n.Interface() + } + n.Register(name, &callback) + + return channel, func() { + n.Deregister(name, &callback) + close(channel) + } +} + +func (n *NotifierListener) handle(data ...queue.Data) { + n.lock.RLock() + defer n.lock.RUnlock() + for _, datum := range data { + call := datum.(*base.FunctionCall) + callbacks, ok := n.callbacks[call.Name] + if ok && len(callbacks) > 0 { + for _, callback := range callbacks { + (*callback)(call.Name, call.Args) + } + } + } +} + +func TestNotifierListener(t *testing.T) { + defer prepareTestEnv(t)() + + createPullNotified, deregister := notifierListener.RegisterChannel("NotifyNewPullRequest", 0, &models.PullRequest{}) + + bs, _ := json.Marshal(&models.PullRequest{}) + notifierListener.handle(&base.FunctionCall{ + Name: "NotifyNewPullRequest", + Args: [][]byte{ + bs, + }, + }) + <-createPullNotified + + notifierListener.notifier.NotifyNewPullRequest(&models.PullRequest{}) + <-createPullNotified + + notification.NotifyNewPullRequest(&models.PullRequest{}) + <-createPullNotified + + deregister() + + notification.NotifyNewPullRequest(&models.PullRequest{}) + // would panic if not deregistered +} diff --git a/integrations/pull_merge_test.go b/integrations/pull_merge_test.go index 218f0e4da66ad..c38a5cd1af8a0 100644 --- a/integrations/pull_merge_test.go +++ b/integrations/pull_merge_test.go @@ -61,31 +61,75 @@ func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum str func TestPullMerge(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number - assert.NoError(t, err) - hookTasksLenBefore := len(hookTasks) + createPullNotified, deferableCreate := notifierListener.RegisterChannel("NotifyNewPullRequest", 0, &models.PullRequest{}) + defer deferableCreate() + + mergePullNotified, deferableMerge := notifierListener.RegisterChannel("NotifyMergePullRequest", 0, &models.PullRequest{}) + defer deferableMerge() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + var prInterface interface{} + resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title") + select { + case prInterface = <-createPullNotified: + case <-time.After(500 * time.Millisecond): + assert.Fail(t, "Took too long to notify!") + } + pr := prInterface.(*models.PullRequest) + pr.LoadBaseRepo() + pr.LoadHeadRepo() + pr.BaseRepo.MustOwner() + pr.HeadRepo.MustOwner() + + assert.EqualValues(t, "user1", pr.HeadRepo.Owner.Name) + assert.EqualValues(t, "repo1", pr.HeadRepo.Name) + assert.EqualValues(t, "user2", pr.BaseRepo.Owner.Name) + assert.EqualValues(t, "repo1", pr.BaseRepo.Name) elem := strings.Split(test.RedirectURL(resp), "/") assert.EqualValues(t, "pulls", elem[3]) + testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleMerge) - hookTasks, err = models.HookTasks(1, 1) - assert.NoError(t, err) - assert.Len(t, hookTasks, hookTasksLenBefore+1) + select { + case prInterface = <-mergePullNotified: + case <-time.After(500 * time.Millisecond): + assert.Fail(t, "Took too long to notify!") + } + + pr = prInterface.(*models.PullRequest) + pr.LoadBaseRepo() + pr.LoadHeadRepo() + pr.BaseRepo.MustOwner() + pr.HeadRepo.MustOwner() + + assert.EqualValues(t, "user1", pr.HeadRepo.Owner.Name) + assert.EqualValues(t, "repo1", pr.HeadRepo.Name) + assert.EqualValues(t, "user2", pr.BaseRepo.Owner.Name) + assert.EqualValues(t, "repo1", pr.BaseRepo.Name) + + time.Sleep(100 * time.Millisecond) + select { + case prInterface = <-createPullNotified: + assert.Fail(t, "Should only have one pull create notification: %v", prInterface) + default: + } + select { + case prInterface = <-mergePullNotified: + assert.Fail(t, "Should only have one pull merge notification: %v", prInterface) + default: + } }) } func TestPullRebase(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number - assert.NoError(t, err) - hookTasksLenBefore := len(hookTasks) + mergePullNotified, deferable := notifierListener.RegisterChannel("NotifyMergePullRequest", 0, &models.PullRequest{}) + defer deferable() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") @@ -96,20 +140,18 @@ func TestPullRebase(t *testing.T) { elem := strings.Split(test.RedirectURL(resp), "/") assert.EqualValues(t, "pulls", elem[3]) testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleRebase) - - hookTasks, err = models.HookTasks(1, 1) - assert.NoError(t, err) - assert.Len(t, hookTasks, hookTasksLenBefore+1) + select { + case <-mergePullNotified: + case <-time.After(500 * time.Millisecond): + assert.Fail(t, "Took too long to notify!") + } }) } func TestPullRebaseMerge(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - defer prepareTestEnv(t)() - - hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number - assert.NoError(t, err) - hookTasksLenBefore := len(hookTasks) + mergePullNotified, deferable := notifierListener.RegisterChannel("NotifyMergePullRequest", 0, &models.PullRequest{}) + defer deferable() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") @@ -121,19 +163,18 @@ func TestPullRebaseMerge(t *testing.T) { assert.EqualValues(t, "pulls", elem[3]) testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleRebaseMerge) - hookTasks, err = models.HookTasks(1, 1) - assert.NoError(t, err) - assert.Len(t, hookTasks, hookTasksLenBefore+1) + select { + case <-mergePullNotified: + case <-time.After(500 * time.Millisecond): + assert.Fail(t, "Took too long to notify!") + } }) } func TestPullSquash(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - defer prepareTestEnv(t)() - - hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number - assert.NoError(t, err) - hookTasksLenBefore := len(hookTasks) + mergePullNotified, deferable := notifierListener.RegisterChannel("NotifyMergePullRequest", 0, &models.PullRequest{}) + defer deferable() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") @@ -146,15 +187,16 @@ func TestPullSquash(t *testing.T) { assert.EqualValues(t, "pulls", elem[3]) testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleSquash) - hookTasks, err = models.HookTasks(1, 1) - assert.NoError(t, err) - assert.Len(t, hookTasks, hookTasksLenBefore+1) + select { + case <-mergePullNotified: + case <-time.After(500 * time.Millisecond): + assert.Fail(t, "Took too long to notify!") + } }) } func TestPullCleanUpAfterMerge(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - defer prepareTestEnv(t)() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") @@ -190,7 +232,6 @@ func TestPullCleanUpAfterMerge(t *testing.T) { func TestCantMergeWorkInProgress(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - defer prepareTestEnv(t)() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") @@ -212,7 +253,6 @@ func TestCantMergeWorkInProgress(t *testing.T) { func TestCantMergeConflict(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - defer prepareTestEnv(t)() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") @@ -258,7 +298,6 @@ func TestCantMergeConflict(t *testing.T) { func TestCantMergeUnrelated(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - defer prepareTestEnv(t)() session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") diff --git a/integrations/sqlite.ini b/integrations/sqlite.ini index de3355c166b03..b27b123957d3e 100644 --- a/integrations/sqlite.ini +++ b/integrations/sqlite.ini @@ -81,3 +81,10 @@ INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTI3OTU5ODN9.O [oauth2] JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko +[queue] +TYPE=channel + +[queue.test-notifier] +BATCH_LENGTH=1 +LENGTH=20 + diff --git a/models/issue.go b/models/issue.go index 75f7bd818aa6e..e7a31b62ae92c 100644 --- a/models/issue.go +++ b/models/issue.go @@ -47,7 +47,7 @@ type Issue struct { IsClosed bool `xorm:"INDEX"` IsRead bool `xorm:"-"` IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. - PullRequest *PullRequest `xorm:"-"` + PullRequest *PullRequest `xorm:"-" json:"-"` NumComments int Ref string diff --git a/models/pull.go b/models/pull.go index ba9c575775c05..89bd1825521c4 100644 --- a/models/pull.go +++ b/models/pull.go @@ -210,6 +210,9 @@ func (pr *PullRequest) apiFormat(e Engine) *api.PullRequest { log.Error("loadRepo[%d]: %v", pr.ID, err) return nil } + if pr.Issue.PullRequest == nil { + pr.Issue.PullRequest = pr + } apiIssue := pr.Issue.apiFormat(e) if pr.BaseRepo == nil { pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID) diff --git a/models/repo_unit.go b/models/repo_unit.go index a6162a65e516b..dbf198e6b31af 100644 --- a/models/repo_unit.go +++ b/models/repo_unit.go @@ -6,6 +6,8 @@ package models import ( "encoding/json" + "fmt" + "reflect" "code.gitea.io/gitea/modules/timeutil" @@ -169,6 +171,57 @@ func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig { return r.Config.(*ExternalTrackerConfig) } +// MarshalJSON implements json.Marshaler +func (r *RepoUnit) MarshalJSON() ([]byte, error) { + tmp := map[string]interface{}{} + tmp["ID"] = r.ID + tmp["RepoID"] = r.RepoID + var err error + tmp["Config"], err = r.Config.ToDB() + if err != nil { + return nil, err + } + tmp["CreatedUnix"] = r.CreatedUnix + bs, err := json.Marshal(tmp) + return bs, err +} + +// UnmarshalJSON implements json.UnMarshaler +func (r *RepoUnit) UnmarshalJSON(bs []byte) (err error) { + tmp := struct { + ID int64 + RepoID int64 + Type UnitType + Config []byte + CreatedUnix timeutil.TimeStamp + }{} + err = json.Unmarshal(bs, &tmp) + if err != nil { + return err + } + + r.ID = tmp.ID + r.RepoID = tmp.RepoID + r.Type = tmp.Type + if r.Type != 0 { + defer func() { + panicked := recover() + if panicked == nil { + return + } + // Panicing is not very nice... + err = fmt.Errorf("%v", panicked) + r.Config = new(UnitConfig) + }() + typeInt64 := int64(r.Type) + typeInterface := reflect.ValueOf(typeInt64).Interface() + r.BeforeSet("type", xorm.Cell(&typeInterface)) + return json.Unmarshal(tmp.Config, &(r.Config)) + } + r.Config = new(UnitConfig) + return nil +} + func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) { return units, e.Where("repo_id = ?", repoID).Find(&units) } diff --git a/models/repo_watch.go b/models/repo_watch.go index 9b3659dbf5901..9d15fc9b5d05a 100644 --- a/models/repo_watch.go +++ b/models/repo_watch.go @@ -184,6 +184,7 @@ func notifyWatchers(e Engine, actions ...*Action) error { } // Add feed for actioner. + act.ID = 0 act.UserID = act.ActUserID if _, err = e.InsertOne(act); err != nil { return fmt.Errorf("insert new actioner: %v", err) diff --git a/models/user.go b/models/user.go index 0454158de6863..32ca0b95df6c2 100644 --- a/models/user.go +++ b/models/user.go @@ -110,9 +110,9 @@ type User struct { LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` LoginName string Type UserType - OwnedOrgs []*User `xorm:"-"` - Orgs []*User `xorm:"-"` - Repos []*Repository `xorm:"-"` + OwnedOrgs []*User `xorm:"-" json:"-"` + Orgs []*User `xorm:"-" json:"-"` + Repos []*Repository `xorm:"-" json:"-"` Location string Website string Rands string `xorm:"VARCHAR(10)"` @@ -151,9 +151,9 @@ type User struct { // For organization NumTeams int NumMembers int - Teams []*Team `xorm:"-"` - Members UserList `xorm:"-"` - MembersIsPublic map[int64]bool `xorm:"-"` + Teams []*Team `xorm:"-" json:"-"` + Members UserList `xorm:"-" json:"-"` + MembersIsPublic map[int64]bool `xorm:"-" json:"-"` Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"` diff --git a/modules/indexer/issues/bleve.go b/modules/indexer/issues/bleve.go index 24443e54a343b..7878d39c75bd0 100644 --- a/modules/indexer/issues/bleve.go +++ b/modules/indexer/issues/bleve.go @@ -256,3 +256,8 @@ func (b *BleveIndexer) Search(keyword string, repoIDs []int64, limit, start int) } return &ret, nil } + +// Close the Index +func (b *BleveIndexer) Close() error { + return b.indexer.Close() +} diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go index a758cfeaeebdd..2a5df80fac2ee 100644 --- a/modules/indexer/issues/db.go +++ b/modules/indexer/issues/db.go @@ -25,6 +25,11 @@ func (db *DBIndexer) Delete(ids ...int64) error { return nil } +// Close dummy function +func (db *DBIndexer) Close() error { + return nil +} + // Search dummy function func (db *DBIndexer) Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error) { total, ids, err := models.SearchIssueIDsByKeyword(kw, repoIDs, limit, start) diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index ebcd3f68dd51c..8676561cf134c 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -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() error } 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,102 @@ 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("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 = setting.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 { + err := issueIndexer.Close() + if err != nil { + log.Error("Error whilst closing the issue indexer: %v", err) + } + } + log.Info("PID: %d Issue Indexer closed", os.Getpid()) + }) + log.Debug("Created Bleve Indexer") + }) case "db": issueIndexer := &DBIndexer{} holder.set(issueIndexer) - dummyQueue = true default: + 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 +202,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 +215,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 +235,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 +279,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 +304,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("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 } diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index a45fede9ac073..379f1c58905e9 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/setting" + "gopkg.in/ini.v1" "github.com/stretchr/testify/assert" ) @@ -22,10 +23,12 @@ func TestMain(m *testing.M) { func TestBleveSearchIssues(t *testing.T) { assert.NoError(t, models.PrepareTestDatabase()) + setting.Cfg = ini.Empty() os.RemoveAll(setting.Indexer.IssueQueueDir) os.RemoveAll(setting.Indexer.IssuePath) setting.Indexer.IssueType = "bleve" + setting.NewQueueService() InitIssueIndexer(true) time.Sleep(5 * time.Second) diff --git a/modules/indexer/issues/queue.go b/modules/indexer/issues/queue.go deleted file mode 100644 index f93e5c47a40a5..0000000000000 --- a/modules/indexer/issues/queue.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package issues - -// Queue defines an interface to save an issue indexer queue -type Queue interface { - Run() error - Push(*IndexerData) error -} - -// DummyQueue represents an empty queue -type DummyQueue struct { -} - -// Run starts to run the queue -func (b *DummyQueue) Run() error { - return nil -} - -// Push pushes data to indexer -func (b *DummyQueue) Push(*IndexerData) error { - return nil -} diff --git a/modules/indexer/issues/queue_channel.go b/modules/indexer/issues/queue_channel.go deleted file mode 100644 index b6458d3eb53db..0000000000000 --- a/modules/indexer/issues/queue_channel.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package issues - -import ( - "time" - - "code.gitea.io/gitea/modules/setting" -) - -// ChannelQueue implements -type ChannelQueue struct { - queue chan *IndexerData - indexer Indexer - batchNumber int -} - -// NewChannelQueue create a memory channel queue -func NewChannelQueue(indexer Indexer, batchNumber int) *ChannelQueue { - return &ChannelQueue{ - queue: make(chan *IndexerData, setting.Indexer.UpdateQueueLength), - indexer: indexer, - batchNumber: batchNumber, - } -} - -// Run starts to run the queue -func (c *ChannelQueue) Run() error { - var i int - var datas = make([]*IndexerData, 0, c.batchNumber) - for { - select { - case data := <-c.queue: - if data.IsDelete { - _ = c.indexer.Delete(data.IDs...) - continue - } - - datas = append(datas, data) - if len(datas) >= c.batchNumber { - _ = c.indexer.Index(datas) - // TODO: save the point - datas = make([]*IndexerData, 0, c.batchNumber) - } - case <-time.After(time.Millisecond * 100): - i++ - if i >= 3 && len(datas) > 0 { - _ = c.indexer.Index(datas) - // TODO: save the point - datas = make([]*IndexerData, 0, c.batchNumber) - } - } - } -} - -// Push will push the indexer data to queue -func (c *ChannelQueue) Push(data *IndexerData) error { - c.queue <- data - return nil -} diff --git a/modules/indexer/issues/queue_disk.go b/modules/indexer/issues/queue_disk.go deleted file mode 100644 index d6187f2acbd02..0000000000000 --- a/modules/indexer/issues/queue_disk.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package issues - -import ( - "encoding/json" - "time" - - "code.gitea.io/gitea/modules/log" - - "gitea.com/lunny/levelqueue" -) - -var ( - _ Queue = &LevelQueue{} -) - -// LevelQueue implements a disk library queue -type LevelQueue struct { - indexer Indexer - queue *levelqueue.Queue - batchNumber int -} - -// NewLevelQueue creates a ledis local queue -func NewLevelQueue(indexer Indexer, dataDir string, batchNumber int) (*LevelQueue, error) { - queue, err := levelqueue.Open(dataDir) - if err != nil { - return nil, err - } - - return &LevelQueue{ - indexer: indexer, - queue: queue, - batchNumber: batchNumber, - }, nil -} - -// Run starts to run the queue -func (l *LevelQueue) Run() error { - var i int - var datas = make([]*IndexerData, 0, l.batchNumber) - for { - i++ - if len(datas) > l.batchNumber || (len(datas) > 0 && i > 3) { - _ = l.indexer.Index(datas) - datas = make([]*IndexerData, 0, l.batchNumber) - i = 0 - continue - } - - bs, err := l.queue.RPop() - if err != nil { - if err != levelqueue.ErrNotFound { - log.Error("RPop: %v", err) - } - time.Sleep(time.Millisecond * 100) - continue - } - - if len(bs) == 0 { - time.Sleep(time.Millisecond * 100) - continue - } - - var data IndexerData - err = json.Unmarshal(bs, &data) - if err != nil { - log.Error("Unmarshal: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } - - log.Trace("LevelQueue: task found: %#v", data) - - if data.IsDelete { - if data.ID > 0 { - if err = l.indexer.Delete(data.ID); err != nil { - log.Error("indexer.Delete: %v", err) - } - } else if len(data.IDs) > 0 { - if err = l.indexer.Delete(data.IDs...); err != nil { - log.Error("indexer.Delete: %v", err) - } - } - time.Sleep(time.Millisecond * 10) - continue - } - - datas = append(datas, &data) - time.Sleep(time.Millisecond * 10) - } -} - -// Push will push the indexer data to queue -func (l *LevelQueue) Push(data *IndexerData) error { - bs, err := json.Marshal(data) - if err != nil { - return err - } - return l.queue.LPush(bs) -} diff --git a/modules/indexer/issues/queue_redis.go b/modules/indexer/issues/queue_redis.go deleted file mode 100644 index 0344d3c87a0f5..0000000000000 --- a/modules/indexer/issues/queue_redis.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package issues - -import ( - "encoding/json" - "errors" - "strconv" - "strings" - "time" - - "code.gitea.io/gitea/modules/log" - - "github.com/go-redis/redis" -) - -var ( - _ Queue = &RedisQueue{} -) - -type redisClient interface { - RPush(key string, args ...interface{}) *redis.IntCmd - LPop(key string) *redis.StringCmd - Ping() *redis.StatusCmd -} - -// RedisQueue redis queue -type RedisQueue struct { - client redisClient - queueName string - indexer Indexer - batchNumber int -} - -func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { - fields := strings.Fields(connStr) - for _, f := range fields { - items := strings.SplitN(f, "=", 2) - if len(items) < 2 { - continue - } - switch strings.ToLower(items[0]) { - case "addrs": - addrs = items[1] - case "password": - password = items[1] - case "db": - dbIdx, err = strconv.Atoi(items[1]) - if err != nil { - return - } - } - } - return -} - -// NewRedisQueue creates single redis or cluster redis queue -func NewRedisQueue(addrs string, password string, dbIdx int, indexer Indexer, batchNumber int) (*RedisQueue, error) { - dbs := strings.Split(addrs, ",") - var queue = RedisQueue{ - queueName: "issue_indexer_queue", - indexer: indexer, - batchNumber: batchNumber, - } - if len(dbs) == 0 { - return nil, errors.New("no redis host found") - } else if len(dbs) == 1 { - queue.client = redis.NewClient(&redis.Options{ - Addr: strings.TrimSpace(dbs[0]), // use default Addr - Password: password, // no password set - DB: dbIdx, // use default DB - }) - } else { - queue.client = redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: dbs, - }) - } - if err := queue.client.Ping().Err(); err != nil { - return nil, err - } - return &queue, nil -} - -// Run runs the redis queue -func (r *RedisQueue) Run() error { - var i int - var datas = make([]*IndexerData, 0, r.batchNumber) - for { - bs, err := r.client.LPop(r.queueName).Bytes() - if err != nil && err != redis.Nil { - log.Error("LPop faile: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } - - i++ - if len(datas) > r.batchNumber || (len(datas) > 0 && i > 3) { - _ = r.indexer.Index(datas) - datas = make([]*IndexerData, 0, r.batchNumber) - i = 0 - } - - if len(bs) == 0 { - time.Sleep(time.Millisecond * 100) - continue - } - - var data IndexerData - err = json.Unmarshal(bs, &data) - if err != nil { - log.Error("Unmarshal: %v", err) - time.Sleep(time.Millisecond * 100) - continue - } - - log.Trace("RedisQueue: task found: %#v", data) - - if data.IsDelete { - if data.ID > 0 { - if err = r.indexer.Delete(data.ID); err != nil { - log.Error("indexer.Delete: %v", err) - } - } else if len(data.IDs) > 0 { - if err = r.indexer.Delete(data.IDs...); err != nil { - log.Error("indexer.Delete: %v", err) - } - } - time.Sleep(time.Millisecond * 100) - continue - } - - datas = append(datas, &data) - time.Sleep(time.Millisecond * 100) - } -} - -// Push implements Queue -func (r *RedisQueue) Push(data *IndexerData) error { - bs, err := json.Marshal(data) - if err != nil { - return err - } - return r.client.RPush(r.queueName, bs).Err() -} diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index fb143f7e29e94..bc9adf4465b07 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -29,6 +29,7 @@ func RegisterDownloaderFactory(factory base.DownloaderFactory) { } // MigrateRepository migrate repository according MigrateOptions +// FIXME: graceful: ctx may need to be checked more often func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, opts base.MigrateOptions) (*models.Repository, error) { var ( downloader base.Downloader diff --git a/modules/notification/base/main.go b/modules/notification/base/main.go new file mode 100644 index 0000000000000..256566e7844fa --- /dev/null +++ b/modules/notification/base/main.go @@ -0,0 +1,247 @@ +// 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. + +// +build ignore + +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io/ioutil" + "strings" + "text/template" + "time" +) + +type funcDef struct { + Name string + Args []funcDefArg +} + +type funcDefArg struct { + Name string + Type string +} + +func main() { + fset := token.NewFileSet() // positions are relative to fset + f, err := parser.ParseFile(fset, "notifier.go", nil, 0) + if err != nil { + panic(err) + } + funcs := make([]funcDef, 0) + //currentFunc := funcDef{} + ast.Inspect(f, func(n ast.Node) bool { + spec, ok := n.(*ast.TypeSpec) + if !ok || spec.Name.Name != "Notifier" { + return true + } + child, ok := spec.Type.(*ast.InterfaceType) + if !ok { + return false + } + funcs = make([]funcDef, len(child.Methods.List)) + for i, method := range child.Methods.List { + methodFuncDef := method.Type.(*ast.FuncType) + def := funcDef{} + def.Name = method.Names[0].Name + def.Args = make([]funcDefArg, 0, len(methodFuncDef.Params.List)) + for j, param := range methodFuncDef.Params.List { + defaultName := fmt.Sprintf("unknown%d", j) + sb := strings.Builder{} + format.Node(&sb, fset, param.Type) + + if len(param.Names) == 0 { + def.Args = append(def.Args, funcDefArg{ + Name: defaultName, + Type: sb.String(), + }) + } else { + for _, ident := range param.Names { + def.Args = append(def.Args, funcDefArg{ + Name: ident.Name, + Type: sb.String(), + }) + } + } + } + funcs[i] = def + } + + return true + }) + + buf := bytes.Buffer{} + nullTemplate.Execute(&buf, struct { + Timestamp time.Time + Funcs []funcDef + }{ + Timestamp: time.Now(), + Funcs: funcs, + }) + + bs, err := format.Source(buf.Bytes()) + if err != nil { + panic(err) + } + + err = ioutil.WriteFile("null.go", bs, 0644) + if err != nil { + panic(err) + } + + buf = bytes.Buffer{} + queueTemplate.Execute(&buf, struct { + Timestamp time.Time + Funcs []funcDef + }{ + Timestamp: time.Now(), + Funcs: funcs, + }) + + bs, err = format.Source(buf.Bytes()) + if err != nil { + ioutil.WriteFile("queue.go", buf.Bytes(), 0644) + panic(err) + } + + err = ioutil.WriteFile("queue.go", bs, 0644) + if err != nil { + panic(err) + } + +} + +var queueTemplate = template.Must(template.New("").Parse(` +// Code generated by go generate; DO NOT EDIT. +package base + +import ( + "encoding/json" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/queue" +) + +// FunctionCall represents is function call with json.Marshaled arguments +type FunctionCall struct { + Name string + Args [][]byte +} + +type QueueNotifier struct { + name string + notifiers []Notifier + internal queue.Queue +} + +var ( + _ Notifier = &QueueNotifier{} +) + +func NewQueueNotifier(name string, notifiers []Notifier) Notifier { + q := &QueueNotifier{ + name: name, + notifiers: notifiers, + } + q.internal = setting.CreateQueue(name, q.handle, &FunctionCall{}) + return q +} + +func NewQueueNotifierWithHandle(name string, handle queue.HandlerFunc) Notifier { + q := &QueueNotifier{ + name: name, + } + q.internal = setting.CreateQueue(name, handle, &FunctionCall{}) + return q +} + +func (q *QueueNotifier) handle(data ...queue.Data) { + for _, datum := range data { + call := datum.(*FunctionCall) + var err error + switch call.Name { + {{- range .Funcs }} + case "{{.Name}}": + {{$p := .Name}} + {{- range $i, $e := .Args }} + var {{$e.Name}} {{$e.Type}} + err = json.Unmarshal(call.Args[{{$i}}], &{{$e.Name}}) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[{{$i}}]), "{{$e.Type}}", "{{$p}}", err) + continue + } + {{- end }} + for _, notifier := range q.notifiers { + notifier.{{.Name}}({{- range $i, $e := .Args}}{{ if $i }}, {{ end }}{{$e.Name}}{{end}}) + } + {{- end }} + default: + log.Error("Unknown notifier function %s with %d arguments", call.Name, len(call.Args)) + } + } +} + +func (q *QueueNotifier) Run() { + for _, notifier := range q.notifiers { + go notifier.Run() + } + graceful.GetManager().RunWithShutdownFns(q.internal.Run) +} +{{- range .Funcs}} +{{if ne .Name "Run"}} + +// {{ .Name }} is a placeholder function +func (q *QueueNotifier) {{ .Name }}({{ range $i, $e := .Args }}{{ if $i }}, {{ end }}{{$e.Name}} {{$e.Type}}{{end}}) { + args := make([][]byte, 0) + var err error + var bs []byte + {{- range .Args }} + bs, err = json.Marshal(&{{.Name}}) + if err != nil { + log.Error("Unable to marshall {{.Name}}: %v", err) + return + } + args = append(args, bs) + {{- end }} + + q.internal.Push(&FunctionCall{ + Name: "{{.Name}}", + Args: args, + }) +} +{{end}} +{{- end }} +`)) + +var nullTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. +package base + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" +) + +// NullNotifier implements a blank notifier +type NullNotifier struct { +} + +var ( + _ Notifier = &NullNotifier{} +) +{{- range .Funcs}} + +// {{ .Name }} is a placeholder function +func (*NullNotifier) {{ .Name }}({{ range $i, $e := .Args }}{{ if $i }}, {{ end }}{{$e.Name}} {{$e.Type}}{{end}}) {} +{{- end }} +`)) diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 48846b3446cea..c19b4fe5a720f 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -9,6 +9,8 @@ import ( "code.gitea.io/gitea/modules/git" ) +//go:generate go run -mod=vendor main.go + // Notifier defines an interface to notify receiver type Notifier interface { Run() diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index bea4e55277212..99fe789fe5dc4 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -1,7 +1,4 @@ -// 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. - +// Code generated by go generate; DO NOT EDIT. package base import ( @@ -17,132 +14,119 @@ var ( _ Notifier = &NullNotifier{} ) -// Run places a place holder function -func (*NullNotifier) Run() { -} +// Run is a placeholder function +func (*NullNotifier) Run() {} -// NotifyCreateIssueComment places a place holder function -func (*NullNotifier) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, - issue *models.Issue, comment *models.Comment) { +// NotifyCreateRepository is a placeholder function +func (*NullNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { } -// NotifyNewIssue places a place holder function -func (*NullNotifier) NotifyNewIssue(issue *models.Issue) { +// NotifyMigrateRepository is a placeholder function +func (*NullNotifier) NotifyMigrateRepository(doer *models.User, u *models.User, repo *models.Repository) { } -// NotifyIssueChangeStatus places a place holder function -func (*NullNotifier) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) { -} +// NotifyDeleteRepository is a placeholder function +func (*NullNotifier) NotifyDeleteRepository(doer *models.User, repo *models.Repository) {} -// NotifyNewPullRequest places a place holder function -func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest) { +// NotifyForkRepository is a placeholder function +func (*NullNotifier) NotifyForkRepository(doer *models.User, oldRepo *models.Repository, repo *models.Repository) { } -// NotifyPullRequestReview places a place holder function -func (*NullNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, comment *models.Comment) { +// NotifyRenameRepository is a placeholder function +func (*NullNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldRepoName string) { } -// NotifyMergePullRequest places a place holder function -func (*NullNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User, baseRepo *git.Repository) { +// NotifyTransferRepository is a placeholder function +func (*NullNotifier) NotifyTransferRepository(doer *models.User, repo *models.Repository, oldOwnerName string) { } -// NotifyPullRequestSynchronized places a place holder function -func (*NullNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) { -} +// NotifyNewIssue is a placeholder function +func (*NullNotifier) NotifyNewIssue(unknown0 *models.Issue) {} -// NotifyPullRequestChangeTargetBranch places a place holder function -func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string) { +// NotifyIssueChangeStatus is a placeholder function +func (*NullNotifier) NotifyIssueChangeStatus(unknown0 *models.User, unknown1 *models.Issue, unknown2 *models.Comment, unknown3 bool) { } -// NotifyUpdateComment places a place holder function -func (*NullNotifier) NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) { +// NotifyIssueChangeMilestone is a placeholder function +func (*NullNotifier) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue, oldMilestoneID int64) { } -// NotifyDeleteComment places a place holder function -func (*NullNotifier) NotifyDeleteComment(doer *models.User, c *models.Comment) { +// NotifyIssueChangeAssignee is a placeholder function +func (*NullNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) { } -// NotifyNewRelease places a place holder function -func (*NullNotifier) NotifyNewRelease(rel *models.Release) { +// NotifyIssueChangeContent is a placeholder function +func (*NullNotifier) NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) { } -// NotifyUpdateRelease places a place holder function -func (*NullNotifier) NotifyUpdateRelease(doer *models.User, rel *models.Release) { -} +// NotifyIssueClearLabels is a placeholder function +func (*NullNotifier) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) {} -// NotifyDeleteRelease places a place holder function -func (*NullNotifier) NotifyDeleteRelease(doer *models.User, rel *models.Release) { -} +// NotifyIssueChangeTitle is a placeholder function +func (*NullNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) {} -// NotifyIssueChangeMilestone places a place holder function -func (*NullNotifier) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue, oldMilestoneID int64) { +// NotifyIssueChangeLabels is a placeholder function +func (*NullNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, addedLabels []*models.Label, removedLabels []*models.Label) { } -// NotifyIssueChangeContent places a place holder function -func (*NullNotifier) NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) { -} +// NotifyNewPullRequest is a placeholder function +func (*NullNotifier) NotifyNewPullRequest(unknown0 *models.PullRequest) {} -// NotifyIssueChangeAssignee places a place holder function -func (*NullNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) { +// NotifyMergePullRequest is a placeholder function +func (*NullNotifier) NotifyMergePullRequest(unknown0 *models.PullRequest, unknown1 *models.User, unknown2 *git.Repository) { } -// NotifyIssueClearLabels places a place holder function -func (*NullNotifier) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) { -} +// NotifyPullRequestSynchronized is a placeholder function +func (*NullNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) {} -// NotifyIssueChangeTitle places a place holder function -func (*NullNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { +// NotifyPullRequestReview is a placeholder function +func (*NullNotifier) NotifyPullRequestReview(unknown0 *models.PullRequest, unknown1 *models.Review, unknown2 *models.Comment) { } -// NotifyIssueChangeLabels places a place holder function -func (*NullNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, - addedLabels []*models.Label, removedLabels []*models.Label) { +// NotifyPullRequestChangeTargetBranch is a placeholder function +func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string) { } -// NotifyCreateRepository places a place holder function -func (*NullNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { +// NotifyCreateIssueComment is a placeholder function +func (*NullNotifier) NotifyCreateIssueComment(unknown0 *models.User, unknown1 *models.Repository, unknown2 *models.Issue, unknown3 *models.Comment) { } -// NotifyDeleteRepository places a place holder function -func (*NullNotifier) NotifyDeleteRepository(doer *models.User, repo *models.Repository) { +// NotifyUpdateComment is a placeholder function +func (*NullNotifier) NotifyUpdateComment(unknown0 *models.User, unknown1 *models.Comment, unknown2 string) { } -// NotifyForkRepository places a place holder function -func (*NullNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { -} +// NotifyDeleteComment is a placeholder function +func (*NullNotifier) NotifyDeleteComment(unknown0 *models.User, unknown1 *models.Comment) {} -// NotifyMigrateRepository places a place holder function -func (*NullNotifier) NotifyMigrateRepository(doer *models.User, u *models.User, repo *models.Repository) { -} +// NotifyNewRelease is a placeholder function +func (*NullNotifier) NotifyNewRelease(rel *models.Release) {} -// NotifyPushCommits notifies commits pushed to notifiers -func (*NullNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *models.PushCommits) { -} +// NotifyUpdateRelease is a placeholder function +func (*NullNotifier) NotifyUpdateRelease(doer *models.User, rel *models.Release) {} -// NotifyCreateRef notifies branch or tag creation to notifiers -func (*NullNotifier) NotifyCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) { -} +// NotifyDeleteRelease is a placeholder function +func (*NullNotifier) NotifyDeleteRelease(doer *models.User, rel *models.Release) {} -// NotifyDeleteRef notifies branch or tag deleteion to notifiers -func (*NullNotifier) NotifyDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) { +// NotifyPushCommits is a placeholder function +func (*NullNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName string, oldCommitID string, newCommitID string, commits *models.PushCommits) { } -// NotifyRenameRepository places a place holder function -func (*NullNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldRepoName string) { +// NotifyCreateRef is a placeholder function +func (*NullNotifier) NotifyCreateRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { } -// NotifyTransferRepository places a place holder function -func (*NullNotifier) NotifyTransferRepository(doer *models.User, repo *models.Repository, oldOwnerName string) { +// NotifyDeleteRef is a placeholder function +func (*NullNotifier) NotifyDeleteRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { } -// NotifySyncPushCommits places a place holder function -func (*NullNotifier) NotifySyncPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *models.PushCommits) { +// NotifySyncPushCommits is a placeholder function +func (*NullNotifier) NotifySyncPushCommits(pusher *models.User, repo *models.Repository, refName string, oldCommitID string, newCommitID string, commits *models.PushCommits) { } -// NotifySyncCreateRef places a place holder function -func (*NullNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) { +// NotifySyncCreateRef is a placeholder function +func (*NullNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { } -// NotifySyncDeleteRef places a place holder function -func (*NullNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) { +// NotifySyncDeleteRef is a placeholder function +func (*NullNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { } diff --git a/modules/notification/base/queue.go b/modules/notification/base/queue.go new file mode 100644 index 0000000000000..5c476b0838a6f --- /dev/null +++ b/modules/notification/base/queue.go @@ -0,0 +1,1748 @@ +// Code generated by go generate; DO NOT EDIT. +package base + +import ( + "encoding/json" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" +) + +// FunctionCall represents is function call with json.Marshaled arguments +type FunctionCall struct { + Name string + Args [][]byte +} + +type QueueNotifier struct { + name string + notifiers []Notifier + internal queue.Queue +} + +var ( + _ Notifier = &QueueNotifier{} +) + +func NewQueueNotifier(name string, notifiers []Notifier) Notifier { + q := &QueueNotifier{ + name: name, + notifiers: notifiers, + } + q.internal = setting.CreateQueue(name, q.handle, &FunctionCall{}) + return q +} + +func NewQueueNotifierWithHandle(name string, handle queue.HandlerFunc) Notifier { + q := &QueueNotifier{ + name: name, + } + q.internal = setting.CreateQueue(name, handle, &FunctionCall{}) + return q +} + +func (q *QueueNotifier) handle(data ...queue.Data) { + for _, datum := range data { + call := datum.(*FunctionCall) + var err error + switch call.Name { + case "Run": + + for _, notifier := range q.notifiers { + notifier.Run() + } + case "NotifyCreateRepository": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyCreateRepository", err) + continue + } + var u *models.User + err = json.Unmarshal(call.Args[1], &u) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.User", "NotifyCreateRepository", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[2], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*models.Repository", "NotifyCreateRepository", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyCreateRepository(doer, u, repo) + } + case "NotifyMigrateRepository": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyMigrateRepository", err) + continue + } + var u *models.User + err = json.Unmarshal(call.Args[1], &u) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.User", "NotifyMigrateRepository", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[2], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*models.Repository", "NotifyMigrateRepository", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyMigrateRepository(doer, u, repo) + } + case "NotifyDeleteRepository": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyDeleteRepository", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyDeleteRepository", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyDeleteRepository(doer, repo) + } + case "NotifyForkRepository": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyForkRepository", err) + continue + } + var oldRepo *models.Repository + err = json.Unmarshal(call.Args[1], &oldRepo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyForkRepository", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[2], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*models.Repository", "NotifyForkRepository", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyForkRepository(doer, oldRepo, repo) + } + case "NotifyRenameRepository": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyRenameRepository", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyRenameRepository", err) + continue + } + var oldRepoName string + err = json.Unmarshal(call.Args[2], &oldRepoName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyRenameRepository", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyRenameRepository(doer, repo, oldRepoName) + } + case "NotifyTransferRepository": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyTransferRepository", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyTransferRepository", err) + continue + } + var oldOwnerName string + err = json.Unmarshal(call.Args[2], &oldOwnerName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyTransferRepository", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyTransferRepository(doer, repo, oldOwnerName) + } + case "NotifyNewIssue": + + var unknown0 *models.Issue + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.Issue", "NotifyNewIssue", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyNewIssue(unknown0) + } + case "NotifyIssueChangeStatus": + + var unknown0 *models.User + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyIssueChangeStatus", err) + continue + } + var unknown1 *models.Issue + err = json.Unmarshal(call.Args[1], &unknown1) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Issue", "NotifyIssueChangeStatus", err) + continue + } + var unknown2 *models.Comment + err = json.Unmarshal(call.Args[2], &unknown2) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*models.Comment", "NotifyIssueChangeStatus", err) + continue + } + var unknown3 bool + err = json.Unmarshal(call.Args[3], &unknown3) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "bool", "NotifyIssueChangeStatus", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyIssueChangeStatus(unknown0, unknown1, unknown2, unknown3) + } + case "NotifyIssueChangeMilestone": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyIssueChangeMilestone", err) + continue + } + var issue *models.Issue + err = json.Unmarshal(call.Args[1], &issue) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Issue", "NotifyIssueChangeMilestone", err) + continue + } + var oldMilestoneID int64 + err = json.Unmarshal(call.Args[2], &oldMilestoneID) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "int64", "NotifyIssueChangeMilestone", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyIssueChangeMilestone(doer, issue, oldMilestoneID) + } + case "NotifyIssueChangeAssignee": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyIssueChangeAssignee", err) + continue + } + var issue *models.Issue + err = json.Unmarshal(call.Args[1], &issue) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Issue", "NotifyIssueChangeAssignee", err) + continue + } + var assignee *models.User + err = json.Unmarshal(call.Args[2], &assignee) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*models.User", "NotifyIssueChangeAssignee", err) + continue + } + var removed bool + err = json.Unmarshal(call.Args[3], &removed) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "bool", "NotifyIssueChangeAssignee", err) + continue + } + var comment *models.Comment + err = json.Unmarshal(call.Args[4], &comment) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[4]), "*models.Comment", "NotifyIssueChangeAssignee", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyIssueChangeAssignee(doer, issue, assignee, removed, comment) + } + case "NotifyIssueChangeContent": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyIssueChangeContent", err) + continue + } + var issue *models.Issue + err = json.Unmarshal(call.Args[1], &issue) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Issue", "NotifyIssueChangeContent", err) + continue + } + var oldContent string + err = json.Unmarshal(call.Args[2], &oldContent) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyIssueChangeContent", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyIssueChangeContent(doer, issue, oldContent) + } + case "NotifyIssueClearLabels": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyIssueClearLabels", err) + continue + } + var issue *models.Issue + err = json.Unmarshal(call.Args[1], &issue) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Issue", "NotifyIssueClearLabels", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyIssueClearLabels(doer, issue) + } + case "NotifyIssueChangeTitle": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyIssueChangeTitle", err) + continue + } + var issue *models.Issue + err = json.Unmarshal(call.Args[1], &issue) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Issue", "NotifyIssueChangeTitle", err) + continue + } + var oldTitle string + err = json.Unmarshal(call.Args[2], &oldTitle) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyIssueChangeTitle", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyIssueChangeTitle(doer, issue, oldTitle) + } + case "NotifyIssueChangeLabels": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyIssueChangeLabels", err) + continue + } + var issue *models.Issue + err = json.Unmarshal(call.Args[1], &issue) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Issue", "NotifyIssueChangeLabels", err) + continue + } + var addedLabels []*models.Label + err = json.Unmarshal(call.Args[2], &addedLabels) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "[]*models.Label", "NotifyIssueChangeLabels", err) + continue + } + var removedLabels []*models.Label + err = json.Unmarshal(call.Args[3], &removedLabels) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "[]*models.Label", "NotifyIssueChangeLabels", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyIssueChangeLabels(doer, issue, addedLabels, removedLabels) + } + case "NotifyNewPullRequest": + + var unknown0 *models.PullRequest + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.PullRequest", "NotifyNewPullRequest", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyNewPullRequest(unknown0) + } + case "NotifyMergePullRequest": + + var unknown0 *models.PullRequest + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.PullRequest", "NotifyMergePullRequest", err) + continue + } + var unknown1 *models.User + err = json.Unmarshal(call.Args[1], &unknown1) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.User", "NotifyMergePullRequest", err) + continue + } + var unknown2 *git.Repository + err = json.Unmarshal(call.Args[2], &unknown2) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*git.Repository", "NotifyMergePullRequest", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyMergePullRequest(unknown0, unknown1, unknown2) + } + case "NotifyPullRequestSynchronized": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyPullRequestSynchronized", err) + continue + } + var pr *models.PullRequest + err = json.Unmarshal(call.Args[1], &pr) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.PullRequest", "NotifyPullRequestSynchronized", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyPullRequestSynchronized(doer, pr) + } + case "NotifyPullRequestReview": + + var unknown0 *models.PullRequest + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.PullRequest", "NotifyPullRequestReview", err) + continue + } + var unknown1 *models.Review + err = json.Unmarshal(call.Args[1], &unknown1) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Review", "NotifyPullRequestReview", err) + continue + } + var unknown2 *models.Comment + err = json.Unmarshal(call.Args[2], &unknown2) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*models.Comment", "NotifyPullRequestReview", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyPullRequestReview(unknown0, unknown1, unknown2) + } + case "NotifyPullRequestChangeTargetBranch": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyPullRequestChangeTargetBranch", err) + continue + } + var pr *models.PullRequest + err = json.Unmarshal(call.Args[1], &pr) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.PullRequest", "NotifyPullRequestChangeTargetBranch", err) + continue + } + var oldBranch string + err = json.Unmarshal(call.Args[2], &oldBranch) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyPullRequestChangeTargetBranch", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyPullRequestChangeTargetBranch(doer, pr, oldBranch) + } + case "NotifyCreateIssueComment": + + var unknown0 *models.User + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyCreateIssueComment", err) + continue + } + var unknown1 *models.Repository + err = json.Unmarshal(call.Args[1], &unknown1) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyCreateIssueComment", err) + continue + } + var unknown2 *models.Issue + err = json.Unmarshal(call.Args[2], &unknown2) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "*models.Issue", "NotifyCreateIssueComment", err) + continue + } + var unknown3 *models.Comment + err = json.Unmarshal(call.Args[3], &unknown3) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "*models.Comment", "NotifyCreateIssueComment", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyCreateIssueComment(unknown0, unknown1, unknown2, unknown3) + } + case "NotifyUpdateComment": + + var unknown0 *models.User + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyUpdateComment", err) + continue + } + var unknown1 *models.Comment + err = json.Unmarshal(call.Args[1], &unknown1) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Comment", "NotifyUpdateComment", err) + continue + } + var unknown2 string + err = json.Unmarshal(call.Args[2], &unknown2) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyUpdateComment", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyUpdateComment(unknown0, unknown1, unknown2) + } + case "NotifyDeleteComment": + + var unknown0 *models.User + err = json.Unmarshal(call.Args[0], &unknown0) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyDeleteComment", err) + continue + } + var unknown1 *models.Comment + err = json.Unmarshal(call.Args[1], &unknown1) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Comment", "NotifyDeleteComment", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyDeleteComment(unknown0, unknown1) + } + case "NotifyNewRelease": + + var rel *models.Release + err = json.Unmarshal(call.Args[0], &rel) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.Release", "NotifyNewRelease", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyNewRelease(rel) + } + case "NotifyUpdateRelease": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyUpdateRelease", err) + continue + } + var rel *models.Release + err = json.Unmarshal(call.Args[1], &rel) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Release", "NotifyUpdateRelease", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyUpdateRelease(doer, rel) + } + case "NotifyDeleteRelease": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyDeleteRelease", err) + continue + } + var rel *models.Release + err = json.Unmarshal(call.Args[1], &rel) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Release", "NotifyDeleteRelease", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyDeleteRelease(doer, rel) + } + case "NotifyPushCommits": + + var pusher *models.User + err = json.Unmarshal(call.Args[0], &pusher) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyPushCommits", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyPushCommits", err) + continue + } + var refName string + err = json.Unmarshal(call.Args[2], &refName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyPushCommits", err) + continue + } + var oldCommitID string + err = json.Unmarshal(call.Args[3], &oldCommitID) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "string", "NotifyPushCommits", err) + continue + } + var newCommitID string + err = json.Unmarshal(call.Args[4], &newCommitID) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[4]), "string", "NotifyPushCommits", err) + continue + } + var commits *models.PushCommits + err = json.Unmarshal(call.Args[5], &commits) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[5]), "*models.PushCommits", "NotifyPushCommits", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyPushCommits(pusher, repo, refName, oldCommitID, newCommitID, commits) + } + case "NotifyCreateRef": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyCreateRef", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyCreateRef", err) + continue + } + var refType string + err = json.Unmarshal(call.Args[2], &refType) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyCreateRef", err) + continue + } + var refFullName string + err = json.Unmarshal(call.Args[3], &refFullName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "string", "NotifyCreateRef", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyCreateRef(doer, repo, refType, refFullName) + } + case "NotifyDeleteRef": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifyDeleteRef", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifyDeleteRef", err) + continue + } + var refType string + err = json.Unmarshal(call.Args[2], &refType) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifyDeleteRef", err) + continue + } + var refFullName string + err = json.Unmarshal(call.Args[3], &refFullName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "string", "NotifyDeleteRef", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifyDeleteRef(doer, repo, refType, refFullName) + } + case "NotifySyncPushCommits": + + var pusher *models.User + err = json.Unmarshal(call.Args[0], &pusher) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifySyncPushCommits", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifySyncPushCommits", err) + continue + } + var refName string + err = json.Unmarshal(call.Args[2], &refName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifySyncPushCommits", err) + continue + } + var oldCommitID string + err = json.Unmarshal(call.Args[3], &oldCommitID) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "string", "NotifySyncPushCommits", err) + continue + } + var newCommitID string + err = json.Unmarshal(call.Args[4], &newCommitID) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[4]), "string", "NotifySyncPushCommits", err) + continue + } + var commits *models.PushCommits + err = json.Unmarshal(call.Args[5], &commits) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[5]), "*models.PushCommits", "NotifySyncPushCommits", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifySyncPushCommits(pusher, repo, refName, oldCommitID, newCommitID, commits) + } + case "NotifySyncCreateRef": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifySyncCreateRef", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifySyncCreateRef", err) + continue + } + var refType string + err = json.Unmarshal(call.Args[2], &refType) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifySyncCreateRef", err) + continue + } + var refFullName string + err = json.Unmarshal(call.Args[3], &refFullName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "string", "NotifySyncCreateRef", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifySyncCreateRef(doer, repo, refType, refFullName) + } + case "NotifySyncDeleteRef": + + var doer *models.User + err = json.Unmarshal(call.Args[0], &doer) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[0]), "*models.User", "NotifySyncDeleteRef", err) + continue + } + var repo *models.Repository + err = json.Unmarshal(call.Args[1], &repo) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[1]), "*models.Repository", "NotifySyncDeleteRef", err) + continue + } + var refType string + err = json.Unmarshal(call.Args[2], &refType) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[2]), "string", "NotifySyncDeleteRef", err) + continue + } + var refFullName string + err = json.Unmarshal(call.Args[3], &refFullName) + if err != nil { + log.Error("Unable to unmarshal %s to %s in call to %s: %v", string(call.Args[3]), "string", "NotifySyncDeleteRef", err) + continue + } + for _, notifier := range q.notifiers { + notifier.NotifySyncDeleteRef(doer, repo, refType, refFullName) + } + default: + log.Error("Unknown notifier function %s with %d arguments", call.Name, len(call.Args)) + } + } +} + +func (q *QueueNotifier) Run() { + for _, notifier := range q.notifiers { + go notifier.Run() + } + graceful.GetManager().RunWithShutdownFns(q.internal.Run) +} + +// NotifyCreateRepository is a placeholder function +func (q *QueueNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&u) + if err != nil { + log.Error("Unable to marshall u: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyCreateRepository", + Args: args, + }) +} + +// NotifyMigrateRepository is a placeholder function +func (q *QueueNotifier) NotifyMigrateRepository(doer *models.User, u *models.User, repo *models.Repository) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&u) + if err != nil { + log.Error("Unable to marshall u: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyMigrateRepository", + Args: args, + }) +} + +// NotifyDeleteRepository is a placeholder function +func (q *QueueNotifier) NotifyDeleteRepository(doer *models.User, repo *models.Repository) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyDeleteRepository", + Args: args, + }) +} + +// NotifyForkRepository is a placeholder function +func (q *QueueNotifier) NotifyForkRepository(doer *models.User, oldRepo *models.Repository, repo *models.Repository) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldRepo) + if err != nil { + log.Error("Unable to marshall oldRepo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyForkRepository", + Args: args, + }) +} + +// NotifyRenameRepository is a placeholder function +func (q *QueueNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldRepoName string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldRepoName) + if err != nil { + log.Error("Unable to marshall oldRepoName: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyRenameRepository", + Args: args, + }) +} + +// NotifyTransferRepository is a placeholder function +func (q *QueueNotifier) NotifyTransferRepository(doer *models.User, repo *models.Repository, oldOwnerName string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldOwnerName) + if err != nil { + log.Error("Unable to marshall oldOwnerName: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyTransferRepository", + Args: args, + }) +} + +// NotifyNewIssue is a placeholder function +func (q *QueueNotifier) NotifyNewIssue(unknown0 *models.Issue) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyNewIssue", + Args: args, + }) +} + +// NotifyIssueChangeStatus is a placeholder function +func (q *QueueNotifier) NotifyIssueChangeStatus(unknown0 *models.User, unknown1 *models.Issue, unknown2 *models.Comment, unknown3 bool) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown1) + if err != nil { + log.Error("Unable to marshall unknown1: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown2) + if err != nil { + log.Error("Unable to marshall unknown2: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown3) + if err != nil { + log.Error("Unable to marshall unknown3: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyIssueChangeStatus", + Args: args, + }) +} + +// NotifyIssueChangeMilestone is a placeholder function +func (q *QueueNotifier) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue, oldMilestoneID int64) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&issue) + if err != nil { + log.Error("Unable to marshall issue: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldMilestoneID) + if err != nil { + log.Error("Unable to marshall oldMilestoneID: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyIssueChangeMilestone", + Args: args, + }) +} + +// NotifyIssueChangeAssignee is a placeholder function +func (q *QueueNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&issue) + if err != nil { + log.Error("Unable to marshall issue: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&assignee) + if err != nil { + log.Error("Unable to marshall assignee: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&removed) + if err != nil { + log.Error("Unable to marshall removed: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&comment) + if err != nil { + log.Error("Unable to marshall comment: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyIssueChangeAssignee", + Args: args, + }) +} + +// NotifyIssueChangeContent is a placeholder function +func (q *QueueNotifier) NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&issue) + if err != nil { + log.Error("Unable to marshall issue: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldContent) + if err != nil { + log.Error("Unable to marshall oldContent: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyIssueChangeContent", + Args: args, + }) +} + +// NotifyIssueClearLabels is a placeholder function +func (q *QueueNotifier) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&issue) + if err != nil { + log.Error("Unable to marshall issue: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyIssueClearLabels", + Args: args, + }) +} + +// NotifyIssueChangeTitle is a placeholder function +func (q *QueueNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&issue) + if err != nil { + log.Error("Unable to marshall issue: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldTitle) + if err != nil { + log.Error("Unable to marshall oldTitle: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyIssueChangeTitle", + Args: args, + }) +} + +// NotifyIssueChangeLabels is a placeholder function +func (q *QueueNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, addedLabels []*models.Label, removedLabels []*models.Label) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&issue) + if err != nil { + log.Error("Unable to marshall issue: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&addedLabels) + if err != nil { + log.Error("Unable to marshall addedLabels: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&removedLabels) + if err != nil { + log.Error("Unable to marshall removedLabels: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyIssueChangeLabels", + Args: args, + }) +} + +// NotifyNewPullRequest is a placeholder function +func (q *QueueNotifier) NotifyNewPullRequest(unknown0 *models.PullRequest) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyNewPullRequest", + Args: args, + }) +} + +// NotifyMergePullRequest is a placeholder function +func (q *QueueNotifier) NotifyMergePullRequest(unknown0 *models.PullRequest, unknown1 *models.User, unknown2 *git.Repository) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown1) + if err != nil { + log.Error("Unable to marshall unknown1: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown2) + if err != nil { + log.Error("Unable to marshall unknown2: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyMergePullRequest", + Args: args, + }) +} + +// NotifyPullRequestSynchronized is a placeholder function +func (q *QueueNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&pr) + if err != nil { + log.Error("Unable to marshall pr: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyPullRequestSynchronized", + Args: args, + }) +} + +// NotifyPullRequestReview is a placeholder function +func (q *QueueNotifier) NotifyPullRequestReview(unknown0 *models.PullRequest, unknown1 *models.Review, unknown2 *models.Comment) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown1) + if err != nil { + log.Error("Unable to marshall unknown1: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown2) + if err != nil { + log.Error("Unable to marshall unknown2: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyPullRequestReview", + Args: args, + }) +} + +// NotifyPullRequestChangeTargetBranch is a placeholder function +func (q *QueueNotifier) NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&pr) + if err != nil { + log.Error("Unable to marshall pr: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldBranch) + if err != nil { + log.Error("Unable to marshall oldBranch: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyPullRequestChangeTargetBranch", + Args: args, + }) +} + +// NotifyCreateIssueComment is a placeholder function +func (q *QueueNotifier) NotifyCreateIssueComment(unknown0 *models.User, unknown1 *models.Repository, unknown2 *models.Issue, unknown3 *models.Comment) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown1) + if err != nil { + log.Error("Unable to marshall unknown1: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown2) + if err != nil { + log.Error("Unable to marshall unknown2: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown3) + if err != nil { + log.Error("Unable to marshall unknown3: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyCreateIssueComment", + Args: args, + }) +} + +// NotifyUpdateComment is a placeholder function +func (q *QueueNotifier) NotifyUpdateComment(unknown0 *models.User, unknown1 *models.Comment, unknown2 string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown1) + if err != nil { + log.Error("Unable to marshall unknown1: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown2) + if err != nil { + log.Error("Unable to marshall unknown2: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyUpdateComment", + Args: args, + }) +} + +// NotifyDeleteComment is a placeholder function +func (q *QueueNotifier) NotifyDeleteComment(unknown0 *models.User, unknown1 *models.Comment) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&unknown0) + if err != nil { + log.Error("Unable to marshall unknown0: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&unknown1) + if err != nil { + log.Error("Unable to marshall unknown1: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyDeleteComment", + Args: args, + }) +} + +// NotifyNewRelease is a placeholder function +func (q *QueueNotifier) NotifyNewRelease(rel *models.Release) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&rel) + if err != nil { + log.Error("Unable to marshall rel: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyNewRelease", + Args: args, + }) +} + +// NotifyUpdateRelease is a placeholder function +func (q *QueueNotifier) NotifyUpdateRelease(doer *models.User, rel *models.Release) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&rel) + if err != nil { + log.Error("Unable to marshall rel: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyUpdateRelease", + Args: args, + }) +} + +// NotifyDeleteRelease is a placeholder function +func (q *QueueNotifier) NotifyDeleteRelease(doer *models.User, rel *models.Release) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&rel) + if err != nil { + log.Error("Unable to marshall rel: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyDeleteRelease", + Args: args, + }) +} + +// NotifyPushCommits is a placeholder function +func (q *QueueNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName string, oldCommitID string, newCommitID string, commits *models.PushCommits) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&pusher) + if err != nil { + log.Error("Unable to marshall pusher: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refName) + if err != nil { + log.Error("Unable to marshall refName: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldCommitID) + if err != nil { + log.Error("Unable to marshall oldCommitID: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&newCommitID) + if err != nil { + log.Error("Unable to marshall newCommitID: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&commits) + if err != nil { + log.Error("Unable to marshall commits: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyPushCommits", + Args: args, + }) +} + +// NotifyCreateRef is a placeholder function +func (q *QueueNotifier) NotifyCreateRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refType) + if err != nil { + log.Error("Unable to marshall refType: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refFullName) + if err != nil { + log.Error("Unable to marshall refFullName: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyCreateRef", + Args: args, + }) +} + +// NotifyDeleteRef is a placeholder function +func (q *QueueNotifier) NotifyDeleteRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refType) + if err != nil { + log.Error("Unable to marshall refType: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refFullName) + if err != nil { + log.Error("Unable to marshall refFullName: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifyDeleteRef", + Args: args, + }) +} + +// NotifySyncPushCommits is a placeholder function +func (q *QueueNotifier) NotifySyncPushCommits(pusher *models.User, repo *models.Repository, refName string, oldCommitID string, newCommitID string, commits *models.PushCommits) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&pusher) + if err != nil { + log.Error("Unable to marshall pusher: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refName) + if err != nil { + log.Error("Unable to marshall refName: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&oldCommitID) + if err != nil { + log.Error("Unable to marshall oldCommitID: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&newCommitID) + if err != nil { + log.Error("Unable to marshall newCommitID: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&commits) + if err != nil { + log.Error("Unable to marshall commits: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifySyncPushCommits", + Args: args, + }) +} + +// NotifySyncCreateRef is a placeholder function +func (q *QueueNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refType) + if err != nil { + log.Error("Unable to marshall refType: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refFullName) + if err != nil { + log.Error("Unable to marshall refFullName: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifySyncCreateRef", + Args: args, + }) +} + +// NotifySyncDeleteRef is a placeholder function +func (q *QueueNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType string, refFullName string) { + args := make([][]byte, 0) + var err error + var bs []byte + bs, err = json.Marshal(&doer) + if err != nil { + log.Error("Unable to marshall doer: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&repo) + if err != nil { + log.Error("Unable to marshall repo: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refType) + if err != nil { + log.Error("Unable to marshall refType: %v", err) + return + } + args = append(args, bs) + bs, err = json.Marshal(&refFullName) + if err != nil { + log.Error("Unable to marshall refFullName: %v", err) + return + } + args = append(args, bs) + + q.internal.Push(&FunctionCall{ + Name: "NotifySyncDeleteRef", + Args: args, + }) +} diff --git a/modules/notification/indexer/indexer.go b/modules/notification/indexer/indexer.go index 4ca5e64c3e431..ffa3700b537a5 100644 --- a/modules/notification/indexer/indexer.go +++ b/modules/notification/indexer/indexer.go @@ -47,6 +47,10 @@ func (r *indexerNotifier) NotifyNewIssue(issue *models.Issue) { } func (r *indexerNotifier) NotifyNewPullRequest(pr *models.PullRequest) { + if err := pr.LoadIssue(); err != nil { + log.Error("Unable to load issue: %d for pr: %d, error: %v", pr.IssueID, pr.ID, err) + return + } issue_indexer.UpdateIssueIndexer(pr.Issue) } diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go index 5148434dca23e..e290c633c785e 100644 --- a/modules/notification/mail/mail.go +++ b/modules/notification/mail/mail.go @@ -46,6 +46,10 @@ func (m *mailNotifier) NotifyCreateIssueComment(doer *models.User, repo *models. } func (m *mailNotifier) NotifyNewIssue(issue *models.Issue) { + if err := issue.LoadPoster(); err != nil { + log.Error("Unable to load poster: %d for issue: %d: Error: %v", issue.PosterID, issue.ID, err) + return + } if err := mailer.MailParticipants(issue, issue.Poster, models.ActionCreateIssue); err != nil { log.Error("MailParticipants: %v", err) } @@ -73,6 +77,14 @@ func (m *mailNotifier) NotifyIssueChangeStatus(doer *models.User, issue *models. } func (m *mailNotifier) NotifyNewPullRequest(pr *models.PullRequest) { + if err := pr.LoadIssue(); err != nil { + log.Error("Unable to load issue: %d for pr: %d: Error: %v", pr.IssueID, pr.ID, err) + return + } + if err := pr.Issue.LoadPoster(); err != nil { + log.Error("Unable to load poster: %d for pr: %d, issue: %d: Error: %v", pr.Issue.PosterID, pr.ID, pr.IssueID, err) + return + } if err := mailer.MailParticipants(pr.Issue, pr.Issue.Poster, models.ActionCreatePullRequest); err != nil { log.Error("MailParticipants: %v", err) } diff --git a/modules/notification/notification.go b/modules/notification/notification.go index f567552df557b..2b3891645ee7e 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -5,6 +5,8 @@ package notification import ( + "sync" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/notification/action" @@ -18,6 +20,7 @@ import ( var ( notifiers []base.Notifier + once = sync.Once{} ) // RegisterNotifier providers method to receive notify messages @@ -28,13 +31,18 @@ func RegisterNotifier(notifier base.Notifier) { // NewContext registers notification handlers func NewContext() { - RegisterNotifier(ui.NewNotifier()) - if setting.Service.EnableNotifyMail { - RegisterNotifier(mail.NewNotifier()) - } - RegisterNotifier(indexer.NewNotifier()) - RegisterNotifier(webhook.NewNotifier()) - RegisterNotifier(action.NewNotifier()) + once.Do(func() { + var ns []base.Notifier + ns = append(ns, ui.NewNotifier()) + if setting.Service.EnableNotifyMail { + ns = append(ns, mail.NewNotifier()) + } + ns = append(ns, indexer.NewNotifier()) + ns = append(ns, webhook.NewNotifier()) + ns = append(ns, action.NewNotifier()) + + RegisterNotifier(base.NewQueueNotifier("notification", ns)) + }) } // NotifyCreateIssueComment notifies issue comment related message to notifiers diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go index f58ebce6d7a69..e6b0d6a85e7ed 100644 --- a/modules/notification/ui/ui.go +++ b/modules/notification/ui/ui.go @@ -7,14 +7,17 @@ package ui import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification/base" + "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" ) type ( notificationService struct { base.NullNotifier - issueQueue chan issueNotificationOpts + issueQueue queue.Queue } issueNotificationOpts struct { @@ -30,19 +33,24 @@ var ( // NewNotifier create a new notificationService notifier func NewNotifier() base.Notifier { - return ¬ificationService{ - issueQueue: make(chan issueNotificationOpts, 100), - } + ns := ¬ificationService{} + ns.issueQueue = setting.CreateQueue("notification-service", ns.handle, issueNotificationOpts{}) + return ns } -func (ns *notificationService) Run() { - for opts := range ns.issueQueue { +func (ns *notificationService) handle(data ...queue.Data) { + for _, datum := range data { + opts := datum.(issueNotificationOpts) if err := models.CreateOrUpdateIssueNotifications(opts.issueID, opts.commentID, opts.notificationAuthorID); err != nil { log.Error("Was unable to create issue notification: %v", err) } } } +func (ns *notificationService) Run() { + graceful.GetManager().RunWithShutdownFns(ns.issueQueue.Run) +} + func (ns *notificationService) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, issue *models.Issue, comment *models.Comment) { var opts = issueNotificationOpts{ @@ -52,44 +60,48 @@ func (ns *notificationService) NotifyCreateIssueComment(doer *models.User, repo if comment != nil { opts.commentID = comment.ID } - ns.issueQueue <- opts + _ = ns.issueQueue.Push(opts) } func (ns *notificationService) NotifyNewIssue(issue *models.Issue) { - ns.issueQueue <- issueNotificationOpts{ + _ = ns.issueQueue.Push(issueNotificationOpts{ issueID: issue.ID, notificationAuthorID: issue.Poster.ID, - } + }) } func (ns *notificationService) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) { - ns.issueQueue <- issueNotificationOpts{ + _ = ns.issueQueue.Push(issueNotificationOpts{ issueID: issue.ID, notificationAuthorID: doer.ID, - } + }) } func (ns *notificationService) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User, gitRepo *git.Repository) { - ns.issueQueue <- issueNotificationOpts{ - issueID: pr.Issue.ID, + _ = ns.issueQueue.Push(issueNotificationOpts{ + issueID: pr.IssueID, notificationAuthorID: doer.ID, - } + }) } func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest) { - ns.issueQueue <- issueNotificationOpts{ - issueID: pr.Issue.ID, - notificationAuthorID: pr.Issue.PosterID, + if err := pr.LoadIssue(); err != nil { + log.Error("Unable to load issue: %d for pr: %d: Error: %v", pr.IssueID, pr.ID, err) + return } + _ = ns.issueQueue.Push(issueNotificationOpts{ + issueID: pr.IssueID, + notificationAuthorID: pr.Issue.PosterID, + }) } func (ns *notificationService) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, c *models.Comment) { var opts = issueNotificationOpts{ - issueID: pr.Issue.ID, + issueID: pr.IssueID, notificationAuthorID: r.Reviewer.ID, } if c != nil { opts.commentID = c.ID } - ns.issueQueue <- opts + _ = ns.issueQueue.Push(opts) } diff --git a/modules/queue/manager.go b/modules/queue/manager.go new file mode 100644 index 0000000000000..81478019e5335 --- /dev/null +++ b/modules/queue/manager.go @@ -0,0 +1,216 @@ +// 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]*Description +} + +// Description represents a working queue inheriting from Gitea. +type Description struct { + mutex sync.Mutex + QID int64 + Queue Queue + Type Type + Name string + Configuration interface{} + ExemplarType string + addWorkers func(number int, timeout time.Duration) context.CancelFunc + numberOfWorkers func() int + counter int64 + PoolWorkers map[int64]*PoolWorkers +} + +// DescriptionList implements the sort.Interface +type DescriptionList []*Description + +// PoolWorkers represents a working queue inheriting from Gitea. +type PoolWorkers struct { + PID int64 + Workers int + Start time.Time + Timeout time.Time + HasTimeout bool + Cancel context.CancelFunc +} + +// PoolWorkersList implements the sort.Interface +type PoolWorkersList []*PoolWorkers + +func init() { + _ = GetManager() +} + +// GetManager returns a Manager and initializes one as singleton if there's none yet +func GetManager() *Manager { + if manager == nil { + manager = &Manager{ + Queues: make(map[int64]*Description), + } + } + return manager +} + +// Add adds a queue to this manager +func (m *Manager) Add(queue Queue, + t Type, + configuration, + exemplar interface{}, + addWorkers func(number int, timeout time.Duration) context.CancelFunc, + numberOfWorkers func() int) int64 { + + cfg, _ := json.Marshal(configuration) + desc := &Description{ + Queue: queue, + Type: t, + Configuration: string(cfg), + ExemplarType: reflect.TypeOf(exemplar).String(), + PoolWorkers: make(map[int64]*PoolWorkers), + addWorkers: addWorkers, + numberOfWorkers: numberOfWorkers, + } + m.mutex.Lock() + m.counter++ + desc.QID = m.counter + desc.Name = fmt.Sprintf("queue-%d", desc.QID) + if named, ok := queue.(Named); ok { + desc.Name = named.Name() + } + m.Queues[desc.QID] = desc + m.mutex.Unlock() + log.Trace("Queue Manager registered: %s (QID: %d)", desc.Name, desc.QID) + return desc.QID +} + +// Remove a queue from the Manager +func (m *Manager) Remove(qid int64) { + m.mutex.Lock() + delete(m.Queues, qid) + m.mutex.Unlock() + log.Trace("Queue Manager removed: QID: %d", qid) + +} + +// GetDescription by qid +func (m *Manager) GetDescription(qid int64) *Description { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.Queues[qid] +} + +// Descriptions returns the queue descriptions +func (m *Manager) Descriptions() []*Description { + m.mutex.Lock() + descs := make([]*Description, 0, len(m.Queues)) + for _, desc := range m.Queues { + descs = append(descs, desc) + } + m.mutex.Unlock() + sort.Sort(DescriptionList(descs)) + return descs +} + +// Workers returns the poolworkers +func (q *Description) Workers() []*PoolWorkers { + q.mutex.Lock() + workers := make([]*PoolWorkers, 0, len(q.PoolWorkers)) + for _, worker := range q.PoolWorkers { + workers = append(workers, worker) + } + q.mutex.Unlock() + sort.Sort(PoolWorkersList(workers)) + return workers +} + +// RegisterWorkers registers workers to this queue +func (q *Description) RegisterWorkers(number int, start time.Time, hasTimeout bool, timeout time.Time, cancel context.CancelFunc) int64 { + q.mutex.Lock() + defer q.mutex.Unlock() + q.counter++ + q.PoolWorkers[q.counter] = &PoolWorkers{ + PID: q.counter, + Workers: number, + Start: start, + Timeout: timeout, + HasTimeout: hasTimeout, + Cancel: cancel, + } + return q.counter +} + +// CancelWorkers cancels pooled workers with pid +func (q *Description) CancelWorkers(pid int64) { + q.mutex.Lock() + pw, ok := q.PoolWorkers[pid] + q.mutex.Unlock() + if !ok { + return + } + pw.Cancel() +} + +// RemoveWorkers deletes pooled workers with pid +func (q *Description) RemoveWorkers(pid int64) { + q.mutex.Lock() + delete(q.PoolWorkers, pid) + q.mutex.Unlock() +} + +// AddWorkers adds workers to the queue if it has registered an add worker function +func (q *Description) AddWorkers(number int, timeout time.Duration) { + if q.addWorkers != nil { + _ = q.addWorkers(number, timeout) + } +} + +// NumberOfWorkers returns the number of workers in the queue +func (q *Description) NumberOfWorkers() int { + if q.numberOfWorkers != nil { + return q.numberOfWorkers() + } + return -1 +} + +func (l DescriptionList) Len() int { + return len(l) +} + +func (l DescriptionList) Less(i, j int) bool { + return l[i].Name < l[j].Name +} + +func (l DescriptionList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func (l PoolWorkersList) Len() int { + return len(l) +} + +func (l PoolWorkersList) Less(i, j int) bool { + return l[i].Start.Before(l[j].Start) +} + +func (l PoolWorkersList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} diff --git a/modules/queue/queue.go b/modules/queue/queue.go new file mode 100644 index 0000000000000..464e16dab130d --- /dev/null +++ b/modules/queue/queue.go @@ -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 +} + +// CreateQueue takes a queue Type and HandlerFunc some options and possibly an exemplar and returns a Queue or an error +func CreateQueue(queueType Type, handlerFunc HandlerFunc, opts, exemplar interface{}) (Queue, error) { + newFn, ok := queuesMap[queueType] + if !ok { + return nil, fmt.Errorf("Unsupported queue type: %v", queueType) + } + return newFn(handlerFunc, opts, exemplar) +} diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go new file mode 100644 index 0000000000000..265a5c88f10e1 --- /dev/null +++ b/modules/queue/queue_channel.go @@ -0,0 +1,104 @@ +// 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 + 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, + }, + exemplar: exemplar, + workers: config.Workers, + name: config.Name, + } + queue.pool.qid = GetManager().Add(queue, ChannelQueueType, config, exemplar, queue.pool.AddWorkers, queue.pool.NumberOfWorkers) + 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 the indexer data to queue +func (c *ChannelQueue) Push(data Data) error { + if c.exemplar != nil { + // Assert data is of same type as r.exemplar + t := reflect.TypeOf(data) + exemplarType := reflect.TypeOf(c.exemplar) + if !t.AssignableTo(exemplarType) || data == nil { + return fmt.Errorf("Unable to assign data: %v to same type as exemplar: %v in queue: %s", data, c.exemplar, c.name) + } + } + c.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 +} diff --git a/modules/queue/queue_channel_test.go b/modules/queue/queue_channel_test.go new file mode 100644 index 0000000000000..c04407aa243f1 --- /dev/null +++ b/modules/queue/queue_channel_test.go @@ -0,0 +1,89 @@ +// 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, + 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, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, + }, &testData{}) + assert.NoError(t, err) + + go queue.Run(nilFn, nilFn) + + test1 := testData{"A", 1} + test2 := testData{"B", 2} + + queue.Push(&test1) + go queue.Push(&test2) + + result1 := <-handleChan + assert.Equal(t, test1.TestString, result1.TestString) + assert.Equal(t, test1.TestInt, result1.TestInt) + + result2 := <-handleChan + assert.Equal(t, test2.TestString, result2.TestString) + assert.Equal(t, test2.TestInt, result2.TestInt) + + err = queue.Push(test1) + assert.Error(t, err) +} diff --git a/modules/queue/queue_disk.go b/modules/queue/queue_disk.go new file mode 100644 index 0000000000000..41e8a9e7c0b71 --- /dev/null +++ b/modules/queue/queue_disk.go @@ -0,0 +1,205 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package queue + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "time" + + "code.gitea.io/gitea/modules/log" + + "gitea.com/lunny/levelqueue" +) + +// LevelQueueType is the type for level queue +const LevelQueueType Type = "level" + +// LevelQueueConfiguration is the configuration for a LevelQueue +type LevelQueueConfiguration struct { + DataDir string + QueueLength int + BatchLength int + Workers 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{} + 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, + }, + 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.AddWorkers, queue.pool.NumberOfWorkers) + 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) + time.Sleep(time.Millisecond * 10) + + } + } +} + +// 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() { + 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() + select { + case <-l.terminated: + default: + close(l.terminated) + if err := l.queue.Close(); err != nil && err.Error() != "leveldb: closed" { + log.Error("Error whilst closing internal queue in %s: %v", l.name, err) + } + + } +} + +// Name returns the name of this queue +func (l *LevelQueue) Name() string { + return l.name +} + +func init() { + queuesMap[LevelQueueType] = NewLevelQueue +} diff --git a/modules/queue/queue_disk_channel.go b/modules/queue/queue_disk_channel.go new file mode 100644 index 0000000000000..884fc410df922 --- /dev/null +++ b/modules/queue/queue_disk_channel.go @@ -0,0 +1,183 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package queue + +import ( + "context" + "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 + 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, + 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 workrers to catch up with the previously dropped work + levelCfg := LevelQueueConfiguration{ + DataDir: config.DataDir, + QueueLength: config.QueueLength, + BatchLength: config.BatchLength, + Workers: 1, + BlockTimeout: 1 * time.Second, + BoostTimeout: 5 * time.Minute, + BoostWorkers: 5, + Name: config.Name + "-level", + } + + levelQueue, err := NewLevelQueue(handle, levelCfg, exemplar) + if err == nil { + return &PersistableChannelQueue{ + ChannelQueue: channelQueue.(*ChannelQueue), + delayedStarter: delayedStarter{ + internal: levelQueue.(*LevelQueue), + name: config.Name, + }, + closed: make(chan struct{}), + }, 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, 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 { + p.setInternal(atShutdown, p.ChannelQueue.pool.handle, p.exemplar) + } else { + p.lock.Unlock() + } + atShutdown(context.Background(), p.Shutdown) + atTerminate(context.Background(), p.Terminate) + + // Just run the level queue - we shut it down later + go p.internal.Run(func(_ context.Context, _ func()) {}, func(_ context.Context, _ func()) {}) + + 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 +} diff --git a/modules/queue/queue_disk_channel_test.go b/modules/queue/queue_disk_channel_test.go new file mode 100644 index 0000000000000..01a90ebcfb8a6 --- /dev/null +++ b/modules/queue/queue_disk_channel_test.go @@ -0,0 +1,115 @@ +// 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, + }, &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, + }, &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() + } + +} diff --git a/modules/queue/queue_disk_test.go b/modules/queue/queue_disk_test.go new file mode 100644 index 0000000000000..03de451760a06 --- /dev/null +++ b/modules/queue/queue_disk_test.go @@ -0,0 +1,124 @@ +// 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, + 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, + 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() + } +} diff --git a/modules/queue/queue_redis.go b/modules/queue/queue_redis.go new file mode 100644 index 0000000000000..724e22b7b5d54 --- /dev/null +++ b/modules/queue/queue_redis.go @@ -0,0 +1,220 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package queue + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" + + "github.com/go-redis/redis" +) + +// RedisQueueType is the type for redis queue +const RedisQueueType Type = "redis" + +type redisClient interface { + RPush(key string, args ...interface{}) *redis.IntCmd + LPop(key string) *redis.StringCmd + Ping() *redis.StatusCmd + Close() error +} + +// RedisQueue redis queue +type RedisQueue struct { + pool *WorkerPool + client redisClient + queueName string + closed chan struct{} + exemplar interface{} + workers int + name string +} + +// 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 + 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, + }, + 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 found") + } else if len(dbs) == 1 { + queue.client = redis.NewClient(&redis.Options{ + Network: config.Network, + Addr: strings.TrimSpace(dbs[0]), // use default Addr + Password: config.Password, // no password set + DB: config.DBIndex, // use default DB + }) + } 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.AddWorkers, queue.pool.NumberOfWorkers) + + 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) + time.Sleep(time.Millisecond * 10) + } + } +} + +// 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) + select { + case <-r.closed: + default: + close(r.closed) + } +} + +// Terminate this queue and close the queue +func (r *RedisQueue) Terminate() { + log.Trace("Terminating: %s", r.name) + r.Shutdown() + if err := r.client.Close(); err != nil { + log.Error("Error whilst closing internal redis client in %s: %v", r.name, err) + } +} + +// Name returns the name of this queue +func (r *RedisQueue) Name() string { + return r.name +} + +func init() { + queuesMap[RedisQueueType] = NewRedisQueue +} diff --git a/modules/queue/queue_test.go b/modules/queue/queue_test.go new file mode 100644 index 0000000000000..3608f68d3d424 --- /dev/null +++ b/modules/queue/queue_test.go @@ -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) +} diff --git a/modules/queue/queue_wrapped.go b/modules/queue/queue_wrapped.go new file mode 100644 index 0000000000000..46557ea318991 --- /dev/null +++ b/modules/queue/queue_wrapped.go @@ -0,0 +1,196 @@ +// 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 +} + +func (q *delayedStarter) setInternal(atShutdown func(context.Context, func()), handle HandlerFunc, exemplar interface{}) { + var ctx context.Context + var cancel context.CancelFunc + if q.timeout > 0 { + ctx, cancel = context.WithTimeout(context.Background(), q.timeout) + } else { + ctx, cancel = context.WithCancel(context.Background()) + } + + defer cancel() + // Ensure we also stop at shutdown + atShutdown(ctx, func() { + cancel() + }) + + i := 1 + for q.internal == nil { + select { + case <-ctx.Done(): + q.lock.Unlock() + log.Fatal("Timedout creating queue %v with cfg %v in %s", q.underlying, q.cfg, q.name) + default: + queue, err := CreateQueue(q.underlying, handle, q.cfg, exemplar) + if err == nil { + q.internal = queue + q.lock.Unlock() + break + } + if err.Error() != "resource temporarily unavailable" { + log.Warn("[Attempt: %d] Failed to create queue: %v for %s cfg: %v error: %v", i, q.underlying, q.name, q.cfg, err) + } + i++ + if q.maxAttempts > 0 && i > q.maxAttempts { + q.lock.Unlock() + log.Fatal("Unable to create queue %v for %s with cfg %v by max attempts: error: %v", q.underlying, q.name, q.cfg, err) + } + sleepTime := 100 * time.Millisecond + if q.timeout > 0 && q.maxAttempts > 0 { + sleepTime = (q.timeout - 200*time.Millisecond) / time.Duration(q.maxAttempts) + } + time.Sleep(sleepTime) + } + } +} + +// 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 := CreateQueue(config.Underlying, handle, config.Config, exemplar) + if err == nil { + // Just return the queue there is no need to wrap + return queue, nil + } + if IsErrInvalidConfiguration(err) { + // Retrying ain't gonna make this any better... + return nil, ErrInvalidConfiguration{cfg: cfg} + } + + 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, 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 { + q.setInternal(atShutdown, q.handle, q.exemplar) + go func() { + for data := range q.channel { + _ = q.internal.Push(data) + } + }() + } else { + q.lock.Unlock() + } + + q.internal.Run(atShutdown, atTerminate) + 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 +} diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go new file mode 100644 index 0000000000000..fe05e7fe6ec2c --- /dev/null +++ b/modules/queue/workerpool.go @@ -0,0 +1,271 @@ +// 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 + numberOfWorkers int + batchLength int + handle HandlerFunc + dataChan chan Data + blockTimeout time.Duration + boostTimeout time.Duration + boostWorkers int +} + +// Push pushes the data to the internal channel +func (p *WorkerPool) Push(data Data) { + p.lock.Lock() + if p.blockTimeout > 0 && p.boostTimeout > 0 { + p.lock.Unlock() + p.pushBoost(data) + } else { + p.lock.Unlock() + p.dataChan <- data + } +} + +func (p *WorkerPool) pushBoost(data Data) { + select { + case p.dataChan <- data: + default: + p.lock.Lock() + if p.blockTimeout <= 0 { + p.lock.Unlock() + p.dataChan <- data + return + } + ourTimeout := p.blockTimeout + timer := time.NewTimer(p.blockTimeout) + p.lock.Unlock() + select { + case p.dataChan <- data: + if timer.Stop() { + select { + case <-timer.C: + default: + } + } + case <-timer.C: + p.lock.Lock() + if p.blockTimeout > ourTimeout { + p.lock.Unlock() + p.dataChan <- data + return + } + p.blockTimeout *= 2 + ctx, cancel := context.WithCancel(p.baseCtx) + desc := GetManager().GetDescription(p.qid) + if desc != nil { + log.Warn("WorkerPool: %d (for %s) Channel blocked for %v - adding %d temporary workers for %s, block timeout now %v", p.qid, desc.Name, ourTimeout, p.boostWorkers, p.boostTimeout, p.blockTimeout) + + start := time.Now() + pid := desc.RegisterWorkers(p.boostWorkers, start, false, start, cancel) + go func() { + <-ctx.Done() + desc.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, p.boostWorkers) + p.lock.Unlock() + p.dataChan <- data + } + } +} + +// NumberOfWorkers returns the number of current workers in the pool +func (p *WorkerPool) NumberOfWorkers() int { + p.lock.Lock() + defer p.lock.Unlock() + return p.numberOfWorkers +} + +// AddWorkers adds workers to the pool +func (p *WorkerPool) AddWorkers(number int, timeout time.Duration) context.CancelFunc { + var ctx context.Context + var cancel context.CancelFunc + start := time.Now() + end := start + hasTimeout := false + if timeout > 0 { + ctx, cancel = context.WithTimeout(p.baseCtx, timeout) + end = start.Add(timeout) + hasTimeout = true + } else { + ctx, cancel = context.WithCancel(p.baseCtx) + } + + desc := GetManager().GetDescription(p.qid) + if desc != nil { + pid := desc.RegisterWorkers(number, start, hasTimeout, end, cancel) + go func() { + <-ctx.Done() + desc.RemoveWorkers(pid) + cancel() + }() + log.Trace("WorkerPool: %d (for %s) adding %d workers with group id: %d", p.qid, desc.Name, number, pid) + } else { + log.Trace("WorkerPool: %d adding %d workers (no group id)", p.qid, number) + + } + p.addWorkers(ctx, number) + return cancel +} + +// addWorkers adds workers to the pool +func (p *WorkerPool) addWorkers(ctx context.Context, number int) { + for i := 0; i < number; i++ { + p.lock.Lock() + if p.cond == nil { + p.cond = sync.NewCond(&p.lock) + } + p.numberOfWorkers++ + p.lock.Unlock() + go func() { + p.doWork(ctx) + + p.lock.Lock() + p.numberOfWorkers-- + if p.numberOfWorkers <= 0 { + // numberOfWorkers can't go negative but... + p.numberOfWorkers = 0 + p.cond.Broadcast() + } + p.lock.Unlock() + }() + } +} + +// Wait for WorkerPool to finish +func (p *WorkerPool) Wait() { + p.lock.Lock() + defer p.lock.Unlock() + if p.cond == nil { + p.cond = sync.NewCond(&p.lock) + } + if p.numberOfWorkers <= 0 { + return + } + p.cond.Wait() +} + +// CleanUp will drain the remaining contents of the channel +// This should be called after AddWorkers context is closed +func (p *WorkerPool) CleanUp(ctx context.Context) { + log.Trace("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) + } + + } + } + } +} diff --git a/modules/setting/queue.go b/modules/setting/queue.go new file mode 100644 index 0000000000000..cd203322e2f24 --- /dev/null +++ b/modules/setting/queue.go @@ -0,0 +1,220 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "encoding/json" + "fmt" + "path" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" +) + +type queueSettings struct { + DataDir string + Length int + BatchLength int + ConnectionString string + Type string + Network string + Addresses string + Password string + QueueName string + DBIndex int + WrapIfNecessary bool + MaxAttempts int + Timeout time.Duration + Workers int + BlockTimeout time.Duration + BoostTimeout time.Duration + BoostWorkers int +} + +// Queue settings +var Queue = queueSettings{} + +// CreateQueue for name with provided handler and exemplar +func CreateQueue(name string, handle queue.HandlerFunc, exemplar interface{}) queue.Queue { + q := getQueueSettings(name) + opts := make(map[string]interface{}) + opts["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["BlockTimeout"] = q.BlockTimeout + opts["BoostTimeout"] = q.BoostTimeout + opts["BoostWorkers"] = q.BoostWorkers + + cfg, err := json.Marshal(opts) + if err != nil { + log.Error("Unable to marshall generic options: %v Error: %v", opts, err) + log.Error("Unable to create queue for %s", name, err) + return nil + } + returnable, err := queue.CreateQueue(queue.Type(q.Type), handle, cfg, exemplar) + if q.WrapIfNecessary && err != nil { + log.Warn("Unable to create queue for %s: %v", name, err) + log.Warn("Attempting to create wrapped queue") + returnable, err = queue.CreateQueue(queue.WrappedQueueType, handle, queue.WrappedQueueConfiguration{ + Underlying: queue.Type(q.Type), + Timeout: q.Timeout, + MaxAttempts: q.MaxAttempts, + Config: cfg, + QueueLength: q.Length, + }, exemplar) + } + if err != nil { + log.Error("Unable to create queue for %s: %v", name, err) + return nil + } + return returnable +} + +func getQueueSettings(name string) queueSettings { + q := queueSettings{} + sec := Cfg.Section("queue." + name) + // DataDir is not directly inheritable + q.DataDir = path.Join(Queue.DataDir, name) + for _, key := range sec.Keys() { + switch key.Name() { + case "DATADIR": + q.DataDir = key.MustString(q.DataDir) + } + } + if !path.IsAbs(q.DataDir) { + q.DataDir = path.Join(AppDataPath, q.DataDir) + } + sec.Key("DATADIR").SetValue(q.DataDir) + // The rest are... + q.Length = sec.Key("LENGTH").MustInt(Queue.Length) + q.BatchLength = sec.Key("BATCH_LENGTH").MustInt(Queue.BatchLength) + q.ConnectionString = sec.Key("CONN_STR").MustString(Queue.ConnectionString) + validTypes := queue.RegisteredTypesAsString() + q.Type = sec.Key("TYPE").In(Queue.Type, validTypes) + q.WrapIfNecessary = sec.Key("WRAP_IF_NECESSARY").MustBool(Queue.WrapIfNecessary) + q.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(Queue.MaxAttempts) + q.Timeout = sec.Key("TIMEOUT").MustDuration(Queue.Timeout) + q.Workers = sec.Key("WORKERS").MustInt(Queue.Workers) + q.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(Queue.BlockTimeout) + q.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(Queue.BoostTimeout) + q.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(Queue.BoostWorkers) + q.QueueName = sec.Key("QUEUE_NAME").MustString(Queue.QueueName) + + 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, "")) + validTypes := queue.RegisteredTypesAsString() + Queue.Type = sec.Key("TYPE").In(string(queue.PersistableChannelQueueType), validTypes) + 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.BlockTimeout = sec.Key("BLOCK_TIMEOUT").MustDuration(1 * time.Second) + Queue.BoostTimeout = sec.Key("BOOST_TIMEOUT").MustDuration(5 * time.Minute) + Queue.BoostWorkers = sec.Key("BOOST_WORKERS").MustInt(5) + Queue.QueueName = sec.Key("QUEUE_NAME").MustString(Queue.QueueName) + + hasWorkers := false + for _, key := range Cfg.Section("queue.notification").Keys() { + if key.Name() == "WORKERS" { + hasWorkers = true + break + } + } + if !hasWorkers { + Cfg.Section("queue.notification").Key("WORKERS").SetValue("5") + } + + // 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) + } + + hasLength := false + for _, key := range Cfg.Section("queue.mail").Keys() { + if key.Name() == "LENGTH" { + hasLength = true + break + } + } + if !hasLength { + Cfg.Section("queue.mail").Key("LENGTH").SetValue(fmt.Sprintf("%d", Cfg.Section("mailer").Key("SEND_BUFFER_LEN").MustInt(100))) + } +} + +// 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 +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index dbf43f31ee258..76609990892b2 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1090,4 +1090,5 @@ func NewServices() { newMigrationsService() newIndexerService() newTaskService() + NewQueueService() } diff --git a/modules/setting/task.go b/modules/setting/task.go index 97704d4a4da68..fa63c669c6629 100644 --- a/modules/setting/task.go +++ b/modules/setting/task.go @@ -4,22 +4,17 @@ package setting -var ( - // Task settings - Task = struct { - QueueType string - QueueLength int - QueueConnStr string - }{ - QueueType: ChannelQueueType, - QueueLength: 1000, - QueueConnStr: "addrs=127.0.0.1:6379 db=0", - } -) +import "code.gitea.io/gitea/modules/queue" func newTaskService() { - sec := Cfg.Section("task") - Task.QueueType = sec.Key("QUEUE_TYPE").MustString(ChannelQueueType) - Task.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) - Task.QueueConnStr = sec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0") + taskSec := Cfg.Section("task") + queueTaskSec := Cfg.Section("queue.task") + switch taskSec.Key("QUEUE_TYPE").MustString(ChannelQueueType) { + case ChannelQueueType: + queueTaskSec.Key("TYPE").MustString(string(queue.PersistableChannelQueueType)) + case RedisQueueType: + queueTaskSec.Key("TYPE").MustString(string(queue.RedisQueueType)) + } + queueTaskSec.Key("LENGTH").MustInt(taskSec.Key("QUEUE_LENGTH").MustInt(1000)) + queueTaskSec.Key("CONN_STR").MustString(taskSec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0")) } diff --git a/modules/task/queue.go b/modules/task/queue.go deleted file mode 100644 index ddee0b3d46274..0000000000000 --- a/modules/task/queue.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2019 Gitea. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package task - -import "code.gitea.io/gitea/models" - -// Queue defines an interface to run task queue -type Queue interface { - Run() error - Push(*models.Task) error - Stop() -} diff --git a/modules/task/queue_channel.go b/modules/task/queue_channel.go deleted file mode 100644 index da541f47551f5..0000000000000 --- a/modules/task/queue_channel.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package task - -import ( - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" -) - -var ( - _ Queue = &ChannelQueue{} -) - -// ChannelQueue implements -type ChannelQueue struct { - queue chan *models.Task -} - -// NewChannelQueue create a memory channel queue -func NewChannelQueue(queueLen int) *ChannelQueue { - return &ChannelQueue{ - queue: make(chan *models.Task, queueLen), - } -} - -// Run starts to run the queue -func (c *ChannelQueue) Run() error { - for task := range c.queue { - err := Run(task) - if err != nil { - log.Error("Run task failed: %s", err.Error()) - } - } - return nil -} - -// Push will push the task ID to queue -func (c *ChannelQueue) Push(task *models.Task) error { - c.queue <- task - return nil -} - -// Stop stop the queue -func (c *ChannelQueue) Stop() { - close(c.queue) -} diff --git a/modules/task/queue_redis.go b/modules/task/queue_redis.go deleted file mode 100644 index 127de0cdbf1d3..0000000000000 --- a/modules/task/queue_redis.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package task - -import ( - "encoding/json" - "errors" - "strconv" - "strings" - "time" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - - "github.com/go-redis/redis" -) - -var ( - _ Queue = &RedisQueue{} -) - -type redisClient interface { - RPush(key string, args ...interface{}) *redis.IntCmd - LPop(key string) *redis.StringCmd - Ping() *redis.StatusCmd -} - -// RedisQueue redis queue -type RedisQueue struct { - client redisClient - queueName string - closeChan chan bool -} - -func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { - fields := strings.Fields(connStr) - for _, f := range fields { - items := strings.SplitN(f, "=", 2) - if len(items) < 2 { - continue - } - switch strings.ToLower(items[0]) { - case "addrs": - addrs = items[1] - case "password": - password = items[1] - case "db": - dbIdx, err = strconv.Atoi(items[1]) - if err != nil { - return - } - } - } - return -} - -// NewRedisQueue creates single redis or cluster redis queue -func NewRedisQueue(addrs string, password string, dbIdx int) (*RedisQueue, error) { - dbs := strings.Split(addrs, ",") - var queue = RedisQueue{ - queueName: "task_queue", - closeChan: make(chan bool), - } - if len(dbs) == 0 { - return nil, errors.New("no redis host found") - } else if len(dbs) == 1 { - queue.client = redis.NewClient(&redis.Options{ - Addr: strings.TrimSpace(dbs[0]), // use default Addr - Password: password, // no password set - DB: dbIdx, // use default DB - }) - } else { - // cluster will ignore db - queue.client = redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: dbs, - Password: password, - }) - } - if err := queue.client.Ping().Err(); err != nil { - return nil, err - } - return &queue, nil -} - -// Run starts to run the queue -func (r *RedisQueue) Run() error { - for { - select { - case <-r.closeChan: - return nil - case <-time.After(time.Millisecond * 100): - } - - bs, err := r.client.LPop(r.queueName).Bytes() - if err != nil { - if err != redis.Nil { - log.Error("LPop failed: %v", err) - } - time.Sleep(time.Millisecond * 100) - continue - } - - var task models.Task - err = json.Unmarshal(bs, &task) - if err != nil { - log.Error("Unmarshal task failed: %s", err.Error()) - } else { - err = Run(&task) - if err != nil { - log.Error("Run task failed: %s", err.Error()) - } - } - } -} - -// Push implements Queue -func (r *RedisQueue) Push(task *models.Task) error { - bs, err := json.Marshal(task) - if err != nil { - return err - } - return r.client.RPush(r.queueName, bs).Err() -} - -// Stop stop the queue -func (r *RedisQueue) Stop() { - r.closeChan <- true -} diff --git a/modules/task/task.go b/modules/task/task.go index 64744afe7a4c7..852319d406fba 100644 --- a/modules/task/task.go +++ b/modules/task/task.go @@ -8,14 +8,16 @@ import ( "fmt" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" ) // taskQueue is a global queue of tasks -var taskQueue Queue +var taskQueue queue.Queue // Run a task func Run(t *models.Task) error { @@ -23,38 +25,32 @@ func Run(t *models.Task) error { case structs.TaskTypeMigrateRepo: return runMigrateTask(t) default: - return fmt.Errorf("Unknow task type: %d", t.Type) + return fmt.Errorf("Unknown task type: %d", t.Type) } } // Init will start the service to get all unfinished tasks and run them func Init() error { - switch setting.Task.QueueType { - case setting.ChannelQueueType: - taskQueue = NewChannelQueue(setting.Task.QueueLength) - case setting.RedisQueueType: - var err error - addrs, pass, idx, err := parseConnStr(setting.Task.QueueConnStr) - if err != nil { - return err - } - taskQueue, err = NewRedisQueue(addrs, pass, idx) - if err != nil { - return err - } - default: - return fmt.Errorf("Unsupported task queue type: %v", setting.Task.QueueType) + taskQueue = setting.CreateQueue("task", handle, &models.Task{}) + + if taskQueue == nil { + return fmt.Errorf("Unable to create Task Queue") } - go func() { - if err := taskQueue.Run(); err != nil { - log.Error("taskQueue.Run end failed: %v", err) - } - }() + go graceful.GetManager().RunWithShutdownFns(taskQueue.Run) return nil } +func handle(data ...queue.Data) { + for _, datum := range data { + task := datum.(*models.Task) + if err := Run(task); err != nil { + log.Error("Run task failed: %v", err) + } + } +} + // MigrateRepository add migration repository to task func MigrateRepository(doer, u *models.User, opts base.MigrateOptions) error { task, err := models.CreateMigrateTask(doer, u, opts) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c9416e727a2b8..54a2b630f5b15 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2021,6 +2021,34 @@ monitor.execute_time = Execution Time monitor.process.cancel = Cancel process monitor.process.cancel_desc = Cancelling a process may cause data loss monitor.process.cancel_notices = Cancel: %s? +monitor.queues = Queues +monitor.queue = Queue: %s +monitor.queue.name = Name +monitor.queue.type = Type +monitor.queue.exemplar = Exemplar Type +monitor.queue.numberworkers = Number of Workers +monitor.queue.review = Review Config +monitor.queue.review_add = Review/Add Workers +monitor.queue.configuration = Initial Configuration +monitor.queue.nopool.title = No Worker Pool +monitor.queue.nopool.desc = This queue wraps other queues and does not itself have a worker pool. +monitor.queue.wrapped.desc = A wrapped queue wraps a slow starting queue, buffering queued requests in a channel. It does not have a worker pool itself. +monitor.queue.persistable-channel.desc = A persistable-channel wraps two queues, a channel queue that has its own worker pool and a level queue for persisted requests from previous shutdowns. It does not have a worker pool itself. +monitor.queue.pool.timeout = Timeout +monitor.queue.pool.addworkers.title = Add Workers +monitor.queue.pool.addworkers.submit = Add Workers +monitor.queue.pool.addworkers.desc = Add Workers to this pool with or without a timeout. If you set a timeout these workers will be removed from the pool after the timeout has lapsed. +monitor.queue.pool.addworkers.numberworkers.placeholder = Number of Workers +monitor.queue.pool.addworkers.timeout.placeholder = Set to 0 for no timeout +monitor.queue.pool.addworkers.mustnumbergreaterzero = Number of Workers to add must be greater than zero +monitor.queue.pool.addworkers.musttimeoutduration = Timeout must be a golang duration eg. 5m or be 0 +monitor.queue.pool.added = Worker Group Added +monitor.queue.pool.workers.title = Active Worker Groups +monitor.queue.pool.workers.none = No worker groups. +monitor.queue.pool.cancel = Shutdown Worker Group +monitor.queue.pool.cancelling = Worker Group shutting down +monitor.queue.pool.cancel_notices = Shutdown this group of %s workers? +monitor.queue.pool.cancel_desc = Leaving a queue without any worker groups may cause requests may block indefinitely. notices.system_notice_list = System Notices notices.view_detail_header = View Notice Details diff --git a/routers/admin/admin.go b/routers/admin/admin.go index ccedcaf8a62e9..7fc57edf312af 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/services/mailer" @@ -35,6 +36,7 @@ const ( tplDashboard base.TplName = "admin/dashboard" tplConfig base.TplName = "admin/config" tplMonitor base.TplName = "admin/monitor" + tplQueue base.TplName = "admin/queue" ) var ( @@ -355,6 +357,7 @@ func Monitor(ctx *context.Context) { ctx.Data["PageIsAdminMonitor"] = true ctx.Data["Processes"] = process.GetManager().Processes() ctx.Data["Entries"] = cron.ListTasks() + ctx.Data["Queues"] = queue.GetManager().Descriptions() ctx.HTML(200, tplMonitor) } @@ -366,3 +369,59 @@ func MonitorCancel(ctx *context.Context) { "redirect": ctx.Repo.RepoLink + "/admin/monitor", }) } + +// Queue shows details for a specific queue +func Queue(ctx *context.Context) { + qid := ctx.ParamsInt64("qid") + desc := queue.GetManager().GetDescription(qid) + if desc == nil { + ctx.Status(404) + return + } + ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", desc.Name) + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminMonitor"] = true + ctx.Data["Queue"] = desc + ctx.HTML(200, tplQueue) +} + +// WorkerCancel cancels a worker group +func WorkerCancel(ctx *context.Context) { + qid := ctx.ParamsInt64("qid") + desc := queue.GetManager().GetDescription(qid) + if desc == nil { + ctx.Status(404) + return + } + pid := ctx.ParamsInt64("pid") + desc.CancelWorkers(pid) + ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.cancelling")) + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid), + }) +} + +// AddWorkers adds workers to a worker group +func AddWorkers(ctx *context.Context) { + qid := ctx.ParamsInt64("qid") + desc := queue.GetManager().GetDescription(qid) + if desc == nil { + ctx.Status(404) + return + } + number := ctx.QueryInt("number") + if number < 1 { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.mustnumbergreaterzero")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + timeout, err := time.ParseDuration(ctx.Query("timeout")) + if err != nil { + ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.musttimeoutduration")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) + return + } + desc.AddWorkers(number, timeout) + ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added")) + ctx.Redirect(setting.AppSubURL + fmt.Sprintf("/admin/monitor/queue/%d", qid)) +} diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index c13fc93cdfb73..49bce7e29262f 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -312,6 +312,8 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) ctx.Error(http.StatusInternalServerError, "UpdateComment", err) return } + _ = comment.LoadAssigneeUser() + _ = comment.LoadPoster() ctx.JSON(http.StatusOK, comment.APIFormat()) } diff --git a/routers/routes/routes.go b/routers/routes/routes.go index c434c42263557..aa98c2f44ed03 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -429,8 +429,15 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("", adminReq, admin.Dashboard) m.Get("/config", admin.Config) m.Post("/config/test_mail", admin.SendTestMail) - m.Get("/monitor", admin.Monitor) - m.Post("/monitor/cancel/:pid", admin.MonitorCancel) + m.Group("/monitor", func() { + m.Get("", admin.Monitor) + m.Post("/cancel/:pid", admin.MonitorCancel) + m.Group("/queue/:qid", func() { + m.Get("", admin.Queue) + m.Post("/add", admin.AddWorkers) + m.Post("/cancel/:pid", admin.WorkerCancel) + }) + }) m.Group("/users", func() { m.Get("", admin.Users) diff --git a/services/mailer/mail.go b/services/mailer/mail.go index a8768de6cdbde..bdbee403d54f1 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -52,7 +52,7 @@ func InitMailRender(subjectTpl *texttmpl.Template, bodyTpl *template.Template) { // SendTestMail sends a test mail func SendTestMail(email string) error { - return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").Message) + return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) } // SendUserMail sends a mail to the user diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 43e99c635e78a..d7d02d9dee822 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -61,11 +61,11 @@ func TestComposeIssueCommentMessage(t *testing.T) { msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue, Content: "test body", Comment: comment}, tos, false, "issue comment") assert.Len(t, msgs, 2) - - mailto := msgs[0].GetHeader("To") - subject := msgs[0].GetHeader("Subject") - inreplyTo := msgs[0].GetHeader("In-Reply-To") - references := msgs[0].GetHeader("References") + gomailMsg := msgs[0].ToMessage() + mailto := gomailMsg.GetHeader("To") + subject := gomailMsg.GetHeader("Subject") + inreplyTo := gomailMsg.GetHeader("In-Reply-To") + references := gomailMsg.GetHeader("References") assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field") assert.Equal(t, "Re: ", subject[0][:4], "Comment reply subject should contain Re:") @@ -96,14 +96,15 @@ func TestComposeIssueMessage(t *testing.T) { Content: "test body"}, tos, false, "issue create") assert.Len(t, msgs, 2) - mailto := msgs[0].GetHeader("To") - subject := msgs[0].GetHeader("Subject") - messageID := msgs[0].GetHeader("Message-ID") + gomailMsg := msgs[0].ToMessage() + mailto := gomailMsg.GetHeader("To") + subject := gomailMsg.GetHeader("Subject") + messageID := gomailMsg.GetHeader("Message-ID") assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field") assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0]) - assert.Nil(t, msgs[0].GetHeader("In-Reply-To")) - assert.Nil(t, msgs[0].GetHeader("References")) + assert.Nil(t, gomailMsg.GetHeader("In-Reply-To")) + assert.Nil(t, gomailMsg.GetHeader("References")) assert.Equal(t, messageID[0], "", "Message-ID header doesn't match") } @@ -134,9 +135,9 @@ func TestTemplateSelection(t *testing.T) { InitMailRender(stpl, btpl) expect := func(t *testing.T, msg *Message, expSubject, expBody string) { - subject := msg.GetHeader("Subject") + subject := msg.ToMessage().GetHeader("Subject") msgbuf := new(bytes.Buffer) - _, _ = msg.WriteTo(msgbuf) + _, _ = msg.ToMessage().WriteTo(msgbuf) wholemsg := msgbuf.String() assert.Equal(t, []string{expSubject}, subject) assert.Contains(t, wholemsg, expBody) @@ -188,9 +189,9 @@ func TestTemplateServices(t *testing.T) { msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: actionType, Content: "test body", Comment: comment}, tos, fromMention, "TestTemplateServices") - subject := msg.GetHeader("Subject") + subject := msg.ToMessage().GetHeader("Subject") msgbuf := new(bytes.Buffer) - _, _ = msg.WriteTo(msgbuf) + _, _ = msg.ToMessage().WriteTo(msgbuf) wholemsg := msgbuf.String() assert.Equal(t, []string{expSubject}, subject) diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index 2e4aa8d71b1c9..6a48ed8d95d5a 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -18,7 +18,9 @@ import ( "time" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "github.com/jaytaylor/html2text" @@ -27,38 +29,63 @@ import ( // Message mail body and log info type Message struct { - Info string // Message information for log purpose. - *gomail.Message + Info string // Message information for log purpose. + FromAddress string + FromDisplayName string + To []string + Subject string + Date time.Time + Body string + Headers map[string][]string } -// NewMessageFrom creates new mail message object with custom From header. -func NewMessageFrom(to []string, fromDisplayName, fromAddress, subject, body string) *Message { - log.Trace("NewMessageFrom (body):\n%s", body) - +// ToMessage converts a Message to gomail.Message +func (m *Message) ToMessage() *gomail.Message { msg := gomail.NewMessage() - msg.SetAddressHeader("From", fromAddress, fromDisplayName) - msg.SetHeader("To", to...) + msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) + msg.SetHeader("To", m.To...) + for header := range m.Headers { + msg.SetHeader(header, m.Headers[header]...) + } + if len(setting.MailService.SubjectPrefix) > 0 { - msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+subject) + msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) } else { - msg.SetHeader("Subject", subject) + msg.SetHeader("Subject", m.Subject) } - msg.SetDateHeader("Date", time.Now()) + msg.SetDateHeader("Date", m.Date) msg.SetHeader("X-Auto-Response-Suppress", "All") - plainBody, err := html2text.FromString(body) + plainBody, err := html2text.FromString(m.Body) if err != nil || setting.MailService.SendAsPlainText { - if strings.Contains(base.TruncateString(body, 100), "") { + if strings.Contains(base.TruncateString(m.Body, 100), "") { log.Warn("Mail contains HTML but configured to send as plain text.") } msg.SetBody("text/plain", plainBody) } else { msg.SetBody("text/plain", plainBody) - msg.AddAlternative("text/html", body) + msg.AddAlternative("text/html", m.Body) } + return msg +} + +// SetHeader adds additional headers to a message +func (m *Message) SetHeader(field string, value ...string) { + m.Headers[field] = value +} + +// NewMessageFrom creates new mail message object with custom From header. +func NewMessageFrom(to []string, fromDisplayName, fromAddress, subject, body string) *Message { + log.Trace("NewMessageFrom (body):\n%s", body) return &Message{ - Message: msg, + FromAddress: fromAddress, + FromDisplayName: fromDisplayName, + To: to, + Subject: subject, + Date: time.Now(), + Body: body, + Headers: map[string][]string{}, } } @@ -257,18 +284,7 @@ func (s *dummySender) Send(from string, to []string, msg io.WriterTo) error { return nil } -func processMailQueue() { - for msg := range mailQueue { - log.Trace("New e-mail sending request %s: %s", msg.GetHeader("To"), msg.Info) - if err := gomail.Send(Sender, msg.Message); err != nil { - log.Error("Failed to send emails %s: %s - %v", msg.GetHeader("To"), msg.Info, err) - } else { - log.Trace("E-mails sent %s: %s", msg.GetHeader("To"), msg.Info) - } - } -} - -var mailQueue chan *Message +var mailQueue queue.Queue // Sender sender for sending mail synchronously var Sender gomail.Sender @@ -291,14 +307,26 @@ func NewContext() { Sender = &dummySender{} } - mailQueue = make(chan *Message, setting.MailService.QueueLength) - go processMailQueue() + mailQueue = setting.CreateQueue("mail", func(data ...queue.Data) { + for _, datum := range data { + msg := datum.(*Message) + gomailMsg := msg.ToMessage() + log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info) + if err := gomail.Send(Sender, gomailMsg); err != nil { + log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err) + } else { + log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info) + } + } + }, &Message{}) + + go graceful.GetManager().RunWithShutdownFns(mailQueue.Run) } // SendAsync send mail asynchronously func SendAsync(msg *Message) { go func() { - mailQueue <- msg + _ = mailQueue.Push(msg) }() } @@ -306,7 +334,7 @@ func SendAsync(msg *Message) { func SendAsyncs(msgs []*Message) { go func() { for _, msg := range msgs { - mailQueue <- msg + _ = mailQueue.Push(msg) } }() } diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 7fc6e97b463a1..6ee0e8d5b4731 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -427,6 +427,7 @@ func syncMirror(repoID string) { } // InitSyncMirrors initializes a go routine to sync the mirrors +// FIXME: graceful: Needs to be a proper queue and graceful func InitSyncMirrors() { go graceful.GetManager().RunWithShutdownContext(SyncMirrors) } diff --git a/templates/admin/monitor.tmpl b/templates/admin/monitor.tmpl index 38402fece2be9..0f9c2150b647e 100644 --- a/templates/admin/monitor.tmpl +++ b/templates/admin/monitor.tmpl @@ -31,6 +31,34 @@ +

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

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

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

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

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

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

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

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

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

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

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

+ {{else}} +

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

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

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

+
+

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

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

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

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

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

+
+
{{.Queue.Configuration | JsonPrettyPrint}}
+		
+
+
+ + +{{template "base/footer" .}}