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  }