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  }