You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

indexer.go 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. // Copyright 2018 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package issues
  5. import (
  6. "context"
  7. "fmt"
  8. "os"
  9. "sync"
  10. "time"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/graceful"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/queue"
  15. "code.gitea.io/gitea/modules/setting"
  16. "code.gitea.io/gitea/modules/util"
  17. )
  18. // IndexerData data stored in the issue indexer
  19. type IndexerData struct {
  20. ID int64 `json:"id"`
  21. RepoID int64 `json:"repo_id"`
  22. Title string `json:"title"`
  23. Content string `json:"content"`
  24. Comments []string `json:"comments"`
  25. IsDelete bool `json:"is_delete"`
  26. IDs []int64 `json:"ids"`
  27. }
  28. // Match represents on search result
  29. type Match struct {
  30. ID int64 `json:"id"`
  31. Score float64 `json:"score"`
  32. }
  33. // SearchResult represents search results
  34. type SearchResult struct {
  35. Total int64
  36. Hits []Match
  37. }
  38. // Indexer defines an interface to indexer issues contents
  39. type Indexer interface {
  40. Init() (bool, error)
  41. Index(issue []*IndexerData) error
  42. Delete(ids ...int64) error
  43. Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error)
  44. Close()
  45. }
  46. type indexerHolder struct {
  47. indexer Indexer
  48. mutex sync.RWMutex
  49. cond *sync.Cond
  50. cancelled bool
  51. }
  52. func newIndexerHolder() *indexerHolder {
  53. h := &indexerHolder{}
  54. h.cond = sync.NewCond(h.mutex.RLocker())
  55. return h
  56. }
  57. func (h *indexerHolder) cancel() {
  58. h.mutex.Lock()
  59. defer h.mutex.Unlock()
  60. h.cancelled = true
  61. h.cond.Broadcast()
  62. }
  63. func (h *indexerHolder) set(indexer Indexer) {
  64. h.mutex.Lock()
  65. defer h.mutex.Unlock()
  66. h.indexer = indexer
  67. h.cond.Broadcast()
  68. }
  69. func (h *indexerHolder) get() Indexer {
  70. h.mutex.RLock()
  71. defer h.mutex.RUnlock()
  72. if h.indexer == nil && !h.cancelled {
  73. h.cond.Wait()
  74. }
  75. return h.indexer
  76. }
  77. var (
  78. // issueIndexerQueue queue of issue ids to be updated
  79. issueIndexerQueue queue.Queue
  80. holder = newIndexerHolder()
  81. )
  82. // InitIssueIndexer initialize issue indexer, syncReindex is true then reindex until
  83. // all issue index done.
  84. func InitIssueIndexer(syncReindex bool) {
  85. waitChannel := make(chan time.Duration)
  86. // Create the Queue
  87. switch setting.Indexer.IssueType {
  88. case "bleve", "elasticsearch":
  89. handler := func(data ...queue.Data) {
  90. indexer := holder.get()
  91. if indexer == nil {
  92. log.Error("Issue indexer handler: unable to get indexer!")
  93. return
  94. }
  95. iData := make([]*IndexerData, 0, setting.Indexer.IssueQueueBatchNumber)
  96. for _, datum := range data {
  97. indexerData, ok := datum.(*IndexerData)
  98. if !ok {
  99. log.Error("Unable to process provided datum: %v - not possible to cast to IndexerData", datum)
  100. continue
  101. }
  102. log.Trace("IndexerData Process: %d %v %t", indexerData.ID, indexerData.IDs, indexerData.IsDelete)
  103. if indexerData.IsDelete {
  104. _ = indexer.Delete(indexerData.IDs...)
  105. continue
  106. }
  107. iData = append(iData, indexerData)
  108. }
  109. if err := indexer.Index(iData); err != nil {
  110. log.Error("Error whilst indexing: %v Error: %v", iData, err)
  111. }
  112. }
  113. issueIndexerQueue = queue.CreateQueue("issue_indexer", handler, &IndexerData{})
  114. if issueIndexerQueue == nil {
  115. log.Fatal("Unable to create issue indexer queue")
  116. }
  117. default:
  118. issueIndexerQueue = &queue.DummyQueue{}
  119. }
  120. // Create the Indexer
  121. go func() {
  122. start := time.Now()
  123. log.Info("PID %d: Initializing Issue Indexer: %s", os.Getpid(), setting.Indexer.IssueType)
  124. var populate bool
  125. switch setting.Indexer.IssueType {
  126. case "bleve":
  127. defer func() {
  128. if err := recover(); err != nil {
  129. log.Error("PANIC whilst initializing issue indexer: %v\nStacktrace: %s", err, log.Stack(2))
  130. log.Error("The indexer files are likely corrupted and may need to be deleted")
  131. holder.cancel()
  132. log.Fatal("PID: %d Unable to initialize the Bleve Issue Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.IssuePath, err)
  133. }
  134. }()
  135. issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath)
  136. exist, err := issueIndexer.Init()
  137. if err != nil {
  138. holder.cancel()
  139. log.Fatal("Unable to initialize Bleve Issue Indexer: %v", err)
  140. }
  141. populate = !exist
  142. holder.set(issueIndexer)
  143. graceful.GetManager().RunAtTerminate(context.Background(), func() {
  144. log.Debug("Closing issue indexer")
  145. issueIndexer := holder.get()
  146. if issueIndexer != nil {
  147. issueIndexer.Close()
  148. }
  149. log.Info("PID: %d Issue Indexer closed", os.Getpid())
  150. })
  151. log.Debug("Created Bleve Indexer")
  152. case "elasticsearch":
  153. graceful.GetManager().RunWithShutdownFns(func(_, atTerminate func(context.Context, func())) {
  154. issueIndexer, err := NewElasticSearchIndexer(setting.Indexer.IssueConnStr, "gitea_issues")
  155. if err != nil {
  156. log.Fatal("Unable to initialize Elastic Search Issue Indexer: %v", err)
  157. }
  158. exist, err := issueIndexer.Init()
  159. if err != nil {
  160. log.Fatal("Unable to issueIndexer.Init: %v", err)
  161. }
  162. populate = !exist
  163. holder.set(issueIndexer)
  164. })
  165. case "db":
  166. issueIndexer := &DBIndexer{}
  167. holder.set(issueIndexer)
  168. default:
  169. holder.cancel()
  170. log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType)
  171. }
  172. // Start processing the queue
  173. go graceful.GetManager().RunWithShutdownFns(issueIndexerQueue.Run)
  174. // Populate the index
  175. if populate {
  176. if syncReindex {
  177. graceful.GetManager().RunWithShutdownContext(populateIssueIndexer)
  178. } else {
  179. go graceful.GetManager().RunWithShutdownContext(populateIssueIndexer)
  180. }
  181. }
  182. waitChannel <- time.Since(start)
  183. close(waitChannel)
  184. }()
  185. if syncReindex {
  186. select {
  187. case <-waitChannel:
  188. case <-graceful.GetManager().IsShutdown():
  189. }
  190. } else if setting.Indexer.StartupTimeout > 0 {
  191. go func() {
  192. timeout := setting.Indexer.StartupTimeout
  193. if graceful.GetManager().IsChild() && setting.GracefulHammerTime > 0 {
  194. timeout += setting.GracefulHammerTime
  195. }
  196. select {
  197. case duration := <-waitChannel:
  198. log.Info("Issue Indexer Initialization took %v", duration)
  199. case <-graceful.GetManager().IsShutdown():
  200. log.Warn("Shutdown occurred before issue index initialisation was complete")
  201. case <-time.After(timeout):
  202. if shutdownable, ok := issueIndexerQueue.(queue.Shutdownable); ok {
  203. shutdownable.Terminate()
  204. }
  205. log.Fatal("Issue Indexer Initialization timed-out after: %v", timeout)
  206. }
  207. }()
  208. }
  209. }
  210. // populateIssueIndexer populate the issue indexer with issue data
  211. func populateIssueIndexer(ctx context.Context) {
  212. for page := 1; ; page++ {
  213. select {
  214. case <-ctx.Done():
  215. log.Warn("Issue Indexer population shutdown before completion")
  216. return
  217. default:
  218. }
  219. repos, _, err := models.SearchRepositoryByName(&models.SearchRepoOptions{
  220. ListOptions: models.ListOptions{Page: page, PageSize: models.RepositoryListDefaultPageSize},
  221. OrderBy: models.SearchOrderByID,
  222. Private: true,
  223. Collaborate: util.OptionalBoolFalse,
  224. })
  225. if err != nil {
  226. log.Error("SearchRepositoryByName: %v", err)
  227. continue
  228. }
  229. if len(repos) == 0 {
  230. log.Debug("Issue Indexer population complete")
  231. return
  232. }
  233. for _, repo := range repos {
  234. select {
  235. case <-ctx.Done():
  236. log.Info("Issue Indexer population shutdown before completion")
  237. return
  238. default:
  239. }
  240. UpdateRepoIndexer(repo)
  241. }
  242. }
  243. }
  244. // UpdateRepoIndexer add/update all issues of the repositories
  245. func UpdateRepoIndexer(repo *models.Repository) {
  246. is, err := models.Issues(&models.IssuesOptions{
  247. RepoIDs: []int64{repo.ID},
  248. IsClosed: util.OptionalBoolNone,
  249. IsPull: util.OptionalBoolNone,
  250. })
  251. if err != nil {
  252. log.Error("Issues: %v", err)
  253. return
  254. }
  255. if err = models.IssueList(is).LoadDiscussComments(); err != nil {
  256. log.Error("LoadComments: %v", err)
  257. return
  258. }
  259. for _, issue := range is {
  260. UpdateIssueIndexer(issue)
  261. }
  262. }
  263. // UpdateIssueIndexer add/update an issue to the issue indexer
  264. func UpdateIssueIndexer(issue *models.Issue) {
  265. var comments []string
  266. for _, comment := range issue.Comments {
  267. if comment.Type == models.CommentTypeComment {
  268. comments = append(comments, comment.Content)
  269. }
  270. }
  271. indexerData := &IndexerData{
  272. ID: issue.ID,
  273. RepoID: issue.RepoID,
  274. Title: issue.Title,
  275. Content: issue.Content,
  276. Comments: comments,
  277. }
  278. log.Debug("Adding to channel: %v", indexerData)
  279. if err := issueIndexerQueue.Push(indexerData); err != nil {
  280. log.Error("Unable to push to issue indexer: %v: Error: %v", indexerData, err)
  281. }
  282. }
  283. // DeleteRepoIssueIndexer deletes repo's all issues indexes
  284. func DeleteRepoIssueIndexer(repo *models.Repository) {
  285. var ids []int64
  286. ids, err := models.GetIssueIDsByRepoID(repo.ID)
  287. if err != nil {
  288. log.Error("getIssueIDsByRepoID failed: %v", err)
  289. return
  290. }
  291. if len(ids) == 0 {
  292. return
  293. }
  294. indexerData := &IndexerData{
  295. IDs: ids,
  296. IsDelete: true,
  297. }
  298. if err := issueIndexerQueue.Push(indexerData); err != nil {
  299. log.Error("Unable to push to issue indexer: %v: Error: %v", indexerData, err)
  300. }
  301. }
  302. // SearchIssuesByKeyword search issue ids by keywords and repo id
  303. // WARNNING: You have to ensure user have permission to visit repoIDs' issues
  304. func SearchIssuesByKeyword(repoIDs []int64, keyword string) ([]int64, error) {
  305. var issueIDs []int64
  306. indexer := holder.get()
  307. if indexer == nil {
  308. log.Error("SearchIssuesByKeyword(): unable to get indexer!")
  309. return nil, fmt.Errorf("unable to get issue indexer")
  310. }
  311. res, err := indexer.Search(keyword, repoIDs, 50, 0)
  312. if err != nil {
  313. return nil, err
  314. }
  315. for _, r := range res.Hits {
  316. issueIDs = append(issueIDs, r.ID)
  317. }
  318. return issueIDs, nil
  319. }