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