code.gitea.io/gitea@v1.22.3/modules/indexer/code/indexer.go (about) 1 // Copyright 2016 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package code 5 6 import ( 7 "context" 8 "os" 9 "runtime/pprof" 10 "slices" 11 "sync/atomic" 12 "time" 13 14 "code.gitea.io/gitea/models/db" 15 repo_model "code.gitea.io/gitea/models/repo" 16 "code.gitea.io/gitea/modules/graceful" 17 "code.gitea.io/gitea/modules/indexer/code/bleve" 18 "code.gitea.io/gitea/modules/indexer/code/elasticsearch" 19 "code.gitea.io/gitea/modules/indexer/code/internal" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/process" 22 "code.gitea.io/gitea/modules/queue" 23 "code.gitea.io/gitea/modules/setting" 24 ) 25 26 var ( 27 indexerQueue *queue.WorkerPoolQueue[*internal.IndexerData] 28 // globalIndexer is the global indexer, it cannot be nil. 29 // When the real indexer is not ready, it will be a dummy indexer which will return error to explain it's not ready. 30 // So it's always safe use it as *globalIndexer.Load() and call its methods. 31 globalIndexer atomic.Pointer[internal.Indexer] 32 dummyIndexer *internal.Indexer 33 ) 34 35 func init() { 36 i := internal.NewDummyIndexer() 37 dummyIndexer = &i 38 globalIndexer.Store(dummyIndexer) 39 } 40 41 func index(ctx context.Context, indexer internal.Indexer, repoID int64) error { 42 repo, err := repo_model.GetRepositoryByID(ctx, repoID) 43 if repo_model.IsErrRepoNotExist(err) { 44 return indexer.Delete(ctx, repoID) 45 } 46 if err != nil { 47 return err 48 } 49 50 repoTypes := setting.Indexer.RepoIndexerRepoTypes 51 52 if len(repoTypes) == 0 { 53 repoTypes = []string{"sources"} 54 } 55 56 // skip forks from being indexed if unit is not present 57 if !slices.Contains(repoTypes, "forks") && repo.IsFork { 58 return nil 59 } 60 61 // skip mirrors from being indexed if unit is not present 62 if !slices.Contains(repoTypes, "mirrors") && repo.IsMirror { 63 return nil 64 } 65 66 // skip templates from being indexed if unit is not present 67 if !slices.Contains(repoTypes, "templates") && repo.IsTemplate { 68 return nil 69 } 70 71 // skip regular repos from being indexed if unit is not present 72 if !slices.Contains(repoTypes, "sources") && !repo.IsFork && !repo.IsMirror && !repo.IsTemplate { 73 return nil 74 } 75 76 sha, err := getDefaultBranchSha(ctx, repo) 77 if err != nil { 78 return err 79 } 80 changes, err := getRepoChanges(ctx, repo, sha) 81 if err != nil { 82 return err 83 } else if changes == nil { 84 return nil 85 } 86 87 if err := indexer.Index(ctx, repo, sha, changes); err != nil { 88 return err 89 } 90 91 return repo_model.UpdateIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode, sha) 92 } 93 94 // Init initialize the repo indexer 95 func Init() { 96 if !setting.Indexer.RepoIndexerEnabled { 97 (*globalIndexer.Load()).Close() 98 return 99 } 100 101 ctx, cancel, finished := process.GetManager().AddTypedContext(context.Background(), "Service: CodeIndexer", process.SystemProcessType, false) 102 103 graceful.GetManager().RunAtTerminate(func() { 104 select { 105 case <-ctx.Done(): 106 return 107 default: 108 } 109 cancel() 110 log.Debug("Closing repository indexer") 111 (*globalIndexer.Load()).Close() 112 log.Info("PID: %d Repository Indexer closed", os.Getpid()) 113 finished() 114 }) 115 116 waitChannel := make(chan time.Duration, 1) 117 118 // Create the Queue 119 switch setting.Indexer.RepoType { 120 case "bleve", "elasticsearch": 121 handler := func(items ...*internal.IndexerData) (unhandled []*internal.IndexerData) { 122 indexer := *globalIndexer.Load() 123 for _, indexerData := range items { 124 log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) 125 if err := index(ctx, indexer, indexerData.RepoID); err != nil { 126 unhandled = append(unhandled, indexerData) 127 if !setting.IsInTesting { 128 log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) 129 } 130 } 131 } 132 return unhandled 133 } 134 135 indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler) 136 if indexerQueue == nil { 137 log.Fatal("Unable to create codes indexer queue") 138 } 139 default: 140 log.Fatal("Unknown codes indexer type; %s", setting.Indexer.RepoType) 141 } 142 143 go func() { 144 pprof.SetGoroutineLabels(ctx) 145 start := time.Now() 146 var ( 147 rIndexer internal.Indexer 148 existed bool 149 err error 150 ) 151 switch setting.Indexer.RepoType { 152 case "bleve": 153 log.Info("PID: %d Initializing Repository Indexer at: %s", os.Getpid(), setting.Indexer.RepoPath) 154 defer func() { 155 if err := recover(); err != nil { 156 log.Error("PANIC whilst initializing repository indexer: %v\nStacktrace: %s", err, log.Stack(2)) 157 log.Error("The indexer files are likely corrupted and may need to be deleted") 158 log.Error("You can completely remove the \"%s\" directory to make Gitea recreate the indexes", setting.Indexer.RepoPath) 159 } 160 }() 161 162 rIndexer = bleve.NewIndexer(setting.Indexer.RepoPath) 163 existed, err = rIndexer.Init(ctx) 164 if err != nil { 165 cancel() 166 (*globalIndexer.Load()).Close() 167 close(waitChannel) 168 log.Fatal("PID: %d Unable to initialize the bleve Repository Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.RepoPath, err) 169 } 170 case "elasticsearch": 171 log.Info("PID: %d Initializing Repository Indexer at: %s", os.Getpid(), setting.Indexer.RepoConnStr) 172 defer func() { 173 if err := recover(); err != nil { 174 log.Error("PANIC whilst initializing repository indexer: %v\nStacktrace: %s", err, log.Stack(2)) 175 log.Error("The indexer files are likely corrupted and may need to be deleted") 176 log.Error("You can completely remove the \"%s\" index to make Gitea recreate the indexes", setting.Indexer.RepoConnStr) 177 } 178 }() 179 180 rIndexer = elasticsearch.NewIndexer(setting.Indexer.RepoConnStr, setting.Indexer.RepoIndexerName) 181 if err != nil { 182 cancel() 183 (*globalIndexer.Load()).Close() 184 close(waitChannel) 185 log.Fatal("PID: %d Unable to create the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err) 186 } 187 existed, err = rIndexer.Init(ctx) 188 if err != nil { 189 cancel() 190 (*globalIndexer.Load()).Close() 191 close(waitChannel) 192 log.Fatal("PID: %d Unable to initialize the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err) 193 } 194 195 default: 196 log.Fatal("PID: %d Unknown Indexer type: %s", os.Getpid(), setting.Indexer.RepoType) 197 } 198 199 globalIndexer.Store(&rIndexer) 200 201 // Start processing the queue 202 go graceful.GetManager().RunWithCancel(indexerQueue) 203 204 if !existed { // populate the index because it's created for the first time 205 go graceful.GetManager().RunWithShutdownContext(populateRepoIndexer) 206 } 207 select { 208 case waitChannel <- time.Since(start): 209 case <-graceful.GetManager().IsShutdown(): 210 } 211 212 close(waitChannel) 213 }() 214 215 if setting.Indexer.StartupTimeout > 0 { 216 go func() { 217 pprof.SetGoroutineLabels(ctx) 218 timeout := setting.Indexer.StartupTimeout 219 if graceful.GetManager().IsChild() && setting.GracefulHammerTime > 0 { 220 timeout += setting.GracefulHammerTime 221 } 222 select { 223 case <-graceful.GetManager().IsShutdown(): 224 log.Warn("Shutdown before Repository Indexer completed initialization") 225 cancel() 226 (*globalIndexer.Load()).Close() 227 case duration, ok := <-waitChannel: 228 if !ok { 229 log.Warn("Repository Indexer Initialization failed") 230 cancel() 231 (*globalIndexer.Load()).Close() 232 return 233 } 234 log.Info("Repository Indexer Initialization took %v", duration) 235 case <-time.After(timeout): 236 cancel() 237 (*globalIndexer.Load()).Close() 238 log.Fatal("Repository Indexer Initialization Timed-Out after: %v", timeout) 239 } 240 }() 241 } 242 } 243 244 // UpdateRepoIndexer update a repository's entries in the indexer 245 func UpdateRepoIndexer(repo *repo_model.Repository) { 246 indexData := &internal.IndexerData{RepoID: repo.ID} 247 if err := indexerQueue.Push(indexData); err != nil { 248 log.Error("Update repo index data %v failed: %v", indexData, err) 249 } 250 } 251 252 // IsAvailable checks if issue indexer is available 253 func IsAvailable(ctx context.Context) bool { 254 return (*globalIndexer.Load()).Ping(ctx) == nil 255 } 256 257 // populateRepoIndexer populate the repo indexer with pre-existing data. This 258 // should only be run when the indexer is created for the first time. 259 func populateRepoIndexer(ctx context.Context) { 260 log.Info("Populating the repo indexer with existing repositories") 261 262 exist, err := db.IsTableNotEmpty("repository") 263 if err != nil { 264 log.Fatal("System error: %v", err) 265 } else if !exist { 266 return 267 } 268 269 // if there is any existing repo indexer metadata in the DB, delete it 270 // since we are starting afresh. Also, xorm requires deletes to have a 271 // condition, and we want to delete everything, thus 1=1. 272 if err := db.DeleteAllRecords("repo_indexer_status"); err != nil { 273 log.Fatal("System error: %v", err) 274 } 275 276 var maxRepoID int64 277 if maxRepoID, err = db.GetMaxID("repository"); err != nil { 278 log.Fatal("System error: %v", err) 279 } 280 281 // start with the maximum existing repo ID and work backwards, so that we 282 // don't include repos that are created after gitea starts; such repos will 283 // already be added to the indexer, and we don't need to add them again. 284 for maxRepoID > 0 { 285 select { 286 case <-ctx.Done(): 287 log.Info("Repository Indexer population shutdown before completion") 288 return 289 default: 290 } 291 ids, err := repo_model.GetUnindexedRepos(ctx, repo_model.RepoIndexerTypeCode, maxRepoID, 0, 50) 292 if err != nil { 293 log.Error("populateRepoIndexer: %v", err) 294 return 295 } else if len(ids) == 0 { 296 break 297 } 298 for _, id := range ids { 299 select { 300 case <-ctx.Done(): 301 log.Info("Repository Indexer population shutdown before completion") 302 return 303 default: 304 } 305 if err := indexerQueue.Push(&internal.IndexerData{RepoID: id}); err != nil { 306 log.Error("indexerQueue.Push: %v", err) 307 return 308 } 309 maxRepoID = id - 1 310 } 311 } 312 log.Info("Done (re)populating the repo indexer with existing repositories") 313 }