code.gitea.io/gitea@v1.22.3/modules/indexer/issues/util.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package issues
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	issue_model "code.gitea.io/gitea/models/issues"
    13  	"code.gitea.io/gitea/modules/container"
    14  	"code.gitea.io/gitea/modules/indexer/issues/internal"
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/queue"
    17  )
    18  
    19  // getIssueIndexerData returns the indexer data of an issue and a bool value indicating whether the issue exists.
    20  func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerData, bool, error) {
    21  	issue, err := issue_model.GetIssueByID(ctx, issueID)
    22  	if err != nil {
    23  		if issue_model.IsErrIssueNotExist(err) {
    24  			return nil, false, nil
    25  		}
    26  		return nil, false, err
    27  	}
    28  
    29  	// FIXME: what if users want to search for a review comment of a pull request?
    30  	//        The comment type is CommentTypeCode or CommentTypeReview.
    31  	//        But LoadDiscussComments only loads CommentTypeComment.
    32  	if err := issue.LoadDiscussComments(ctx); err != nil {
    33  		return nil, false, err
    34  	}
    35  
    36  	comments := make([]string, 0, len(issue.Comments))
    37  	for _, comment := range issue.Comments {
    38  		if comment.Content != "" {
    39  			// what ever the comment type is, index the content if it is not empty.
    40  			comments = append(comments, comment.Content)
    41  		}
    42  	}
    43  
    44  	if err := issue.LoadAttributes(ctx); err != nil {
    45  		return nil, false, err
    46  	}
    47  
    48  	labels := make([]int64, 0, len(issue.Labels))
    49  	for _, label := range issue.Labels {
    50  		labels = append(labels, label.ID)
    51  	}
    52  
    53  	mentionIDs, err := issue_model.GetIssueMentionIDs(ctx, issueID)
    54  	if err != nil {
    55  		return nil, false, err
    56  	}
    57  
    58  	var (
    59  		reviewedIDs        []int64
    60  		reviewRequestedIDs []int64
    61  	)
    62  	{
    63  		reviews, err := issue_model.FindReviews(ctx, issue_model.FindReviewOptions{
    64  			ListOptions:  db.ListOptionsAll,
    65  			IssueID:      issueID,
    66  			OfficialOnly: false,
    67  		})
    68  		if err != nil {
    69  			return nil, false, err
    70  		}
    71  
    72  		reviewedIDsSet := make(container.Set[int64], len(reviews))
    73  		reviewRequestedIDsSet := make(container.Set[int64], len(reviews))
    74  		for _, review := range reviews {
    75  			if review.Type == issue_model.ReviewTypeRequest {
    76  				reviewRequestedIDsSet.Add(review.ReviewerID)
    77  			} else {
    78  				reviewedIDsSet.Add(review.ReviewerID)
    79  			}
    80  		}
    81  		reviewedIDs = reviewedIDsSet.Values()
    82  		reviewRequestedIDs = reviewRequestedIDsSet.Values()
    83  	}
    84  
    85  	subscriberIDs, err := issue_model.GetIssueWatchersIDs(ctx, issue.ID, true)
    86  	if err != nil {
    87  		return nil, false, err
    88  	}
    89  
    90  	var projectID int64
    91  	if issue.Project != nil {
    92  		projectID = issue.Project.ID
    93  	}
    94  
    95  	return &internal.IndexerData{
    96  		ID:                 issue.ID,
    97  		RepoID:             issue.RepoID,
    98  		IsPublic:           !issue.Repo.IsPrivate,
    99  		Title:              issue.Title,
   100  		Content:            issue.Content,
   101  		Comments:           comments,
   102  		IsPull:             issue.IsPull,
   103  		IsClosed:           issue.IsClosed,
   104  		LabelIDs:           labels,
   105  		NoLabel:            len(labels) == 0,
   106  		MilestoneID:        issue.MilestoneID,
   107  		ProjectID:          projectID,
   108  		ProjectBoardID:     issue.ProjectBoardID(ctx),
   109  		PosterID:           issue.PosterID,
   110  		AssigneeID:         issue.AssigneeID,
   111  		MentionIDs:         mentionIDs,
   112  		ReviewedIDs:        reviewedIDs,
   113  		ReviewRequestedIDs: reviewRequestedIDs,
   114  		SubscriberIDs:      subscriberIDs,
   115  		UpdatedUnix:        issue.UpdatedUnix,
   116  		CreatedUnix:        issue.CreatedUnix,
   117  		DeadlineUnix:       issue.DeadlineUnix,
   118  		CommentCount:       int64(len(issue.Comments)),
   119  	}, true, nil
   120  }
   121  
   122  func updateRepoIndexer(ctx context.Context, repoID int64) error {
   123  	ids, err := issue_model.GetIssueIDsByRepoID(ctx, repoID)
   124  	if err != nil {
   125  		return fmt.Errorf("issue_model.GetIssueIDsByRepoID: %w", err)
   126  	}
   127  	for _, id := range ids {
   128  		if err := updateIssueIndexer(ctx, id); err != nil {
   129  			return err
   130  		}
   131  	}
   132  	return nil
   133  }
   134  
   135  func updateIssueIndexer(ctx context.Context, issueID int64) error {
   136  	return pushIssueIndexerQueue(ctx, &IndexerMetadata{ID: issueID})
   137  }
   138  
   139  func deleteRepoIssueIndexer(ctx context.Context, repoID int64) error {
   140  	var ids []int64
   141  	ids, err := issue_model.GetIssueIDsByRepoID(ctx, repoID)
   142  	if err != nil {
   143  		return fmt.Errorf("issue_model.GetIssueIDsByRepoID: %w", err)
   144  	}
   145  
   146  	if len(ids) == 0 {
   147  		return nil
   148  	}
   149  	return pushIssueIndexerQueue(ctx, &IndexerMetadata{
   150  		IDs:      ids,
   151  		IsDelete: true,
   152  	})
   153  }
   154  
   155  type keepRetryKey struct{}
   156  
   157  // contextWithKeepRetry returns a context with a key indicating that the indexer should keep retrying.
   158  // Please note that it's for background tasks only, and it should not be used for user requests, or it may cause blocking.
   159  func contextWithKeepRetry(ctx context.Context) context.Context {
   160  	return context.WithValue(ctx, keepRetryKey{}, true)
   161  }
   162  
   163  func pushIssueIndexerQueue(ctx context.Context, data *IndexerMetadata) error {
   164  	if issueIndexerQueue == nil {
   165  		// Some unit tests will trigger indexing, but the queue is not initialized.
   166  		// It's OK to ignore it, but log a warning message in case it's not a unit test.
   167  		log.Warn("Trying to push %+v to issue indexer queue, but the queue is not initialized, it's OK if it's a unit test", data)
   168  		return nil
   169  	}
   170  
   171  	for {
   172  		select {
   173  		case <-ctx.Done():
   174  			return ctx.Err()
   175  		default:
   176  		}
   177  		err := issueIndexerQueue.Push(data)
   178  		if errors.Is(err, queue.ErrAlreadyInQueue) {
   179  			return nil
   180  		}
   181  		if errors.Is(err, context.DeadlineExceeded) { // the queue is full
   182  			log.Warn("It seems that issue indexer is slow and the queue is full. Please check the issue indexer or increase the queue size.")
   183  			if ctx.Value(keepRetryKey{}) == nil {
   184  				return err
   185  			}
   186  			// It will be better to increase the queue size instead of retrying, but users may ignore the previous warning message.
   187  			// However, even it retries, it may still cause index loss when there's a deadline in the context.
   188  			log.Debug("Retry to push %+v to issue indexer queue", data)
   189  			continue
   190  		}
   191  		return err
   192  	}
   193  }