go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/server/analyses.go (about)

     1  // Copyright 2023 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package server implements the LUCI Bisection servers to handle pRPC requests.
    16  package server
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"sort"
    22  	"strconv"
    23  
    24  	"go.chromium.org/luci/bisection/compilefailureanalysis/heuristic"
    25  	"go.chromium.org/luci/bisection/compilefailureanalysis/nthsection"
    26  	"go.chromium.org/luci/bisection/internal/lucianalysis"
    27  	"go.chromium.org/luci/bisection/model"
    28  	pb "go.chromium.org/luci/bisection/proto/v1"
    29  	"go.chromium.org/luci/bisection/util"
    30  	"go.chromium.org/luci/bisection/util/datastoreutil"
    31  	"go.chromium.org/luci/bisection/util/loggingutil"
    32  	"go.chromium.org/luci/bisection/util/protoutil"
    33  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    34  	"go.chromium.org/luci/common/errors"
    35  	"go.chromium.org/luci/common/logging"
    36  	"go.chromium.org/luci/common/pagination"
    37  	"go.chromium.org/luci/common/pagination/dscursor"
    38  	"go.chromium.org/luci/common/proto/mask"
    39  	"go.chromium.org/luci/common/sync/parallel"
    40  	"go.chromium.org/luci/gae/service/datastore"
    41  	rdbpbutil "go.chromium.org/luci/resultdb/pbutil"
    42  	"google.golang.org/grpc/codes"
    43  	"google.golang.org/grpc/status"
    44  	"google.golang.org/protobuf/types/known/fieldmaskpb"
    45  	"google.golang.org/protobuf/types/known/timestamppb"
    46  )
    47  
    48  var listAnalysesPageTokenVault = dscursor.NewVault([]byte("luci.bisection.v1.ListAnalyses"))
    49  var listTestAnalysesPageTokenVault = dscursor.NewVault([]byte("luci.bisection.v1.ListTestAnalyses"))
    50  
    51  // Max and default page sizes for ListAnalyses - the proto should be updated
    52  // to reflect any changes to these values.
    53  var listAnalysesPageSizeLimiter = PageSizeLimiter{
    54  	Max:     200,
    55  	Default: 50,
    56  }
    57  
    58  // Max and default page sizes for ListTestAnalyses - the proto should be updated
    59  // to reflect any changes to these values.
    60  var listTestAnalysesPageSizeLimiter = PageSizeLimiter{
    61  	Max:     200,
    62  	Default: 50,
    63  }
    64  
    65  type AnalysisClient interface {
    66  	ChangepointAnalysisForTestVariant(ctx context.Context, project string, keys []lucianalysis.TestVerdictKey) (map[lucianalysis.TestVerdictKey]*lucianalysis.ChangepointResult, error)
    67  }
    68  
    69  // AnalysesServer implements the LUCI Bisection proto service for Analyses.
    70  type AnalysesServer struct {
    71  	AnalysisClient AnalysisClient
    72  }
    73  
    74  // GetAnalysis returns the analysis given the analysis id
    75  func (server *AnalysesServer) GetAnalysis(c context.Context, req *pb.GetAnalysisRequest) (*pb.Analysis, error) {
    76  	c = loggingutil.SetAnalysisID(c, req.AnalysisId)
    77  	analysis := &model.CompileFailureAnalysis{
    78  		Id: req.AnalysisId,
    79  	}
    80  	switch err := datastore.Get(c, analysis); err {
    81  	case nil:
    82  		//continue
    83  	case datastore.ErrNoSuchEntity:
    84  		return nil, status.Errorf(codes.NotFound, "Analysis %d not found: %v", req.AnalysisId, err)
    85  	default:
    86  		return nil, status.Errorf(codes.Internal, "Error in retrieving analysis: %s", err)
    87  	}
    88  	result, err := GetAnalysisResult(c, analysis)
    89  	if err != nil {
    90  		return nil, status.Errorf(codes.Internal, "Error getting analysis result: %s", err)
    91  	}
    92  	return result, nil
    93  }
    94  
    95  // QueryAnalysis returns the analysis given a query
    96  func (server *AnalysesServer) QueryAnalysis(c context.Context, req *pb.QueryAnalysisRequest) (*pb.QueryAnalysisResponse, error) {
    97  	if err := validateQueryAnalysisRequest(req); err != nil {
    98  		return nil, err
    99  	}
   100  	if req.BuildFailure.FailedStepName != "compile" {
   101  		return nil, status.Errorf(codes.Unimplemented, "only compile failures are supported")
   102  	}
   103  	bbid := req.BuildFailure.GetBbid()
   104  	c = loggingutil.SetQueryBBID(c, bbid)
   105  	logging.Infof(c, "QueryAnalysis for build %d", bbid)
   106  
   107  	analysis, err := datastoreutil.GetAnalysisForBuild(c, bbid)
   108  	if err != nil {
   109  		logging.Errorf(c, "Could not query analysis for build %d: %s", bbid, err)
   110  		return nil, status.Errorf(codes.Internal, "failed to get analysis for build %d: %s", bbid, err)
   111  	}
   112  	if analysis == nil {
   113  		logging.Infof(c, "No analysis for build %d", bbid)
   114  		return nil, status.Errorf(codes.NotFound, "analysis not found for build %d", bbid)
   115  	}
   116  	c = loggingutil.SetAnalysisID(c, analysis.Id)
   117  	analysispb, err := GetAnalysisResult(c, analysis)
   118  	if err != nil {
   119  		logging.Errorf(c, "Could not get analysis data for build %d: %s", bbid, err)
   120  		return nil, status.Errorf(codes.Internal, "failed to get analysis data %s", err)
   121  	}
   122  
   123  	res := &pb.QueryAnalysisResponse{
   124  		Analyses: []*pb.Analysis{analysispb},
   125  	}
   126  	return res, nil
   127  }
   128  
   129  // TriggerAnalysis triggers an analysis for a failure
   130  func (server *AnalysesServer) TriggerAnalysis(c context.Context, req *pb.TriggerAnalysisRequest) (*pb.TriggerAnalysisResponse, error) {
   131  	// TODO(nqmtuan): Implement this
   132  	return nil, nil
   133  }
   134  
   135  // UpdateAnalysis updates the information of an analysis.
   136  // At the mean time, it is only used for update the bugs associated with an
   137  // analysis.
   138  func (server *AnalysesServer) UpdateAnalysis(c context.Context, req *pb.UpdateAnalysisRequest) (*pb.Analysis, error) {
   139  	// TODO(nqmtuan): Implement this
   140  	return nil, nil
   141  }
   142  
   143  func (server *AnalysesServer) ListTestAnalyses(ctx context.Context, req *pb.ListTestAnalysesRequest) (*pb.ListTestAnalysesResponse, error) {
   144  	logging.Infof(ctx, "ListTestAnalyses for project %s", req.Project)
   145  
   146  	// Validate the request.
   147  	if err := validateListTestAnalysesRequest(req); err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	// By default, returning all fields.
   152  	fieldMask := req.Fields
   153  	if fieldMask == nil {
   154  		fieldMask = defaultFieldMask()
   155  	}
   156  	mask, err := mask.FromFieldMask(fieldMask, &pb.TestAnalysis{}, false, false)
   157  	if err != nil {
   158  		return nil, errors.Annotate(err, "from field mask").Err()
   159  	}
   160  
   161  	// Decode cursor from page token.
   162  	cursor, err := listTestAnalysesPageTokenVault.Cursor(ctx, req.PageToken)
   163  	switch err {
   164  	case pagination.ErrInvalidPageToken:
   165  		return nil, status.Errorf(codes.InvalidArgument, "invalid page token")
   166  	case nil:
   167  		// Continue
   168  	default:
   169  		return nil, status.Errorf(codes.Internal, err.Error())
   170  	}
   171  
   172  	// Override the page size if necessary.
   173  	pageSize := int(listTestAnalysesPageSizeLimiter.Adjust(req.PageSize))
   174  
   175  	// Query datastore for test analyses.
   176  	q := datastore.NewQuery("TestFailureAnalysis").Eq("project", req.Project).Order("-create_time").Start(cursor)
   177  	tfas := make([]*model.TestFailureAnalysis, 0, pageSize)
   178  	var nextCursor datastore.Cursor
   179  	err = datastore.Run(ctx, q, func(tfa *model.TestFailureAnalysis, getCursor datastore.CursorCB) error {
   180  		tfas = append(tfas, tfa)
   181  
   182  		// Check whether the page size limit has been reached
   183  		if len(tfas) == pageSize {
   184  			nextCursor, err = getCursor()
   185  			if err != nil {
   186  				return err
   187  			}
   188  			return datastore.Stop
   189  		}
   190  		return nil
   191  	})
   192  	if err != nil {
   193  		logging.Errorf(ctx, err.Error())
   194  		return nil, status.Errorf(codes.Internal, err.Error())
   195  	}
   196  
   197  	// Construct the next page token.
   198  	nextPageToken, err := listTestAnalysesPageTokenVault.PageToken(ctx, nextCursor)
   199  	if err != nil {
   200  		return nil, status.Errorf(codes.Internal, err.Error())
   201  	}
   202  
   203  	// Get the result for each test failure analysis.
   204  	analyses := make([]*pb.TestAnalysis, len(tfas))
   205  	err = parallel.FanOutIn(func(workC chan<- func() error) {
   206  		for i, tfa := range tfas {
   207  			// Assign to local variables.
   208  			i := i
   209  			tfa := tfa
   210  			workC <- func() error {
   211  				analysis, err := protoutil.TestFailureAnalysisToPb(ctx, tfa, mask)
   212  				if err != nil {
   213  					err = errors.Annotate(err, "test failure analysis to pb").Err()
   214  					logging.Errorf(ctx, "Could not get analysis data for analysis %d: %s", tfa.ID, err)
   215  					return err
   216  				}
   217  				analyses[i] = analysis
   218  				return nil
   219  			}
   220  		}
   221  	})
   222  	if err != nil {
   223  		logging.Errorf(ctx, err.Error())
   224  		return nil, status.Errorf(codes.Internal, err.Error())
   225  	}
   226  
   227  	return &pb.ListTestAnalysesResponse{
   228  		Analyses:      analyses,
   229  		NextPageToken: nextPageToken,
   230  	}, nil
   231  }
   232  
   233  func (server *AnalysesServer) GetTestAnalysis(ctx context.Context, req *pb.GetTestAnalysisRequest) (*pb.TestAnalysis, error) {
   234  	ctx = loggingutil.SetAnalysisID(ctx, req.AnalysisId)
   235  
   236  	// By default, returning all fields.
   237  	fieldMask := req.Fields
   238  	if fieldMask == nil {
   239  		fieldMask = defaultFieldMask()
   240  	}
   241  	mask, err := mask.FromFieldMask(fieldMask, &pb.TestAnalysis{}, false, false)
   242  	if err != nil {
   243  		return nil, errors.Annotate(err, "from field mask").Err()
   244  	}
   245  
   246  	tfa, err := datastoreutil.GetTestFailureAnalysis(ctx, req.AnalysisId)
   247  	if err != nil {
   248  		if errors.Is(err, datastore.ErrNoSuchEntity) {
   249  			logging.Errorf(ctx, err.Error())
   250  			return nil, status.Errorf(codes.NotFound, "analysis not found: %v", err)
   251  		}
   252  		err = errors.Annotate(err, "get test failure analysis").Err()
   253  		logging.Errorf(ctx, err.Error())
   254  		return nil, status.Errorf(codes.Internal, err.Error())
   255  	}
   256  	result, err := protoutil.TestFailureAnalysisToPb(ctx, tfa, mask)
   257  	if err != nil {
   258  		err = errors.Annotate(err, "test failure analysis to pb").Err()
   259  		logging.Errorf(ctx, err.Error())
   260  		return nil, status.Errorf(codes.Internal, err.Error())
   261  	}
   262  	return result, nil
   263  }
   264  
   265  func (server *AnalysesServer) BatchGetTestAnalyses(ctx context.Context, req *pb.BatchGetTestAnalysesRequest) (*pb.BatchGetTestAnalysesResponse, error) {
   266  	// Validate request.
   267  	if err := validateBatchGetTestAnalysesRequest(req); err != nil {
   268  		return nil, status.Errorf(codes.InvalidArgument, err.Error())
   269  	}
   270  	// By default, returning all fields.
   271  	fieldMask := req.Fields
   272  	if fieldMask == nil {
   273  		fieldMask = defaultFieldMask()
   274  	}
   275  	tfamask, err := mask.FromFieldMask(fieldMask, &pb.TestAnalysis{}, false, false)
   276  	if err != nil {
   277  		return nil, errors.Annotate(err, "from field mask").Err()
   278  	}
   279  	// Query Changepoint analysis.
   280  	keys := []lucianalysis.TestVerdictKey{}
   281  	for _, tf := range req.TestFailures {
   282  		keys = append(keys, lucianalysis.TestVerdictKey{
   283  			TestID:      tf.TestId,
   284  			VariantHash: tf.VariantHash,
   285  			RefHash:     tf.RefHash,
   286  		})
   287  	}
   288  	changePointResults, err := server.AnalysisClient.ChangepointAnalysisForTestVariant(ctx, req.Project, keys)
   289  	if err != nil {
   290  		return nil, status.Errorf(codes.Internal, "read changepoint analysis %s", err)
   291  	}
   292  
   293  	result := make([]*pb.TestAnalysis, len(req.TestFailures))
   294  	err = parallel.FanOutIn(func(workC chan<- func() error) {
   295  		for i, tf := range req.TestFailures {
   296  			// Assign to local variables.
   297  			i := i
   298  			tf := tf
   299  			workC <- func() error {
   300  				tfs, err := datastoreutil.GetTestFailures(ctx, req.Project, tf.TestId, tf.RefHash, tf.VariantHash)
   301  				if err != nil {
   302  					return errors.Annotate(err, "get test failures").Err()
   303  				}
   304  				if len(tfs) == 0 {
   305  					return nil
   306  				}
   307  				sort.Slice(tfs, func(i, j int) bool {
   308  					return tfs[i].RegressionStartPosition > tfs[j].RegressionStartPosition
   309  				})
   310  				latestTestFailure := tfs[0]
   311  				if latestTestFailure.IsDiverged {
   312  					// Do not return test analysis if diverged.
   313  					// Because diverged test failure is considered excluded from the test analyses.
   314  					return nil
   315  				}
   316  				changepointResult, ok := changePointResults[lucianalysis.TestVerdictKey{
   317  					TestID:      tf.TestId,
   318  					VariantHash: tf.VariantHash,
   319  					RefHash:     tf.RefHash,
   320  				}]
   321  				if !ok {
   322  					logging.Infof(ctx, "no changepoint analysis for test %s %s %s", tf.TestId, tf.VariantHash, tf.RefHash)
   323  					return nil
   324  				}
   325  				ongoing, reason := isTestFailureDeterministicallyOngoing(latestTestFailure, changepointResult)
   326  				// Do not return the test analysis if the failure is not ongoing.
   327  				if !ongoing {
   328  					logging.Infof(ctx, "no bisection returned for test %s %s %s because %s", tf.TestId, tf.VariantHash, tf.RefHash, reason)
   329  					return nil
   330  				}
   331  				// Return the test analysis that analyze this test failure.
   332  				tfa, err := datastoreutil.GetTestFailureAnalysis(ctx, latestTestFailure.AnalysisKey.IntID())
   333  				if err != nil {
   334  					return errors.Annotate(err, "get test failure analysis").Err()
   335  				}
   336  				tfaProto, err := protoutil.TestFailureAnalysisToPb(ctx, tfa, tfamask)
   337  				if err != nil {
   338  					return errors.Annotate(err, "convert test failure analysis to protobuf").Err()
   339  				}
   340  				result[i] = tfaProto
   341  				return nil
   342  			}
   343  		}
   344  	})
   345  
   346  	if err != nil {
   347  		return nil, status.Errorf(codes.Internal, err.Error())
   348  	}
   349  
   350  	return &pb.BatchGetTestAnalysesResponse{
   351  		TestAnalyses: result,
   352  	}, nil
   353  }
   354  
   355  // IsTestFailureDeterministicallyOngoing returns a boolean which indicate whether
   356  // a test failure is still deterministically failing.
   357  // It also returns a string to explain why it is not deterministically failing.
   358  func isTestFailureDeterministicallyOngoing(tf *model.TestFailure, changepointResult *lucianalysis.ChangepointResult) (bool, string) {
   359  	segments := changepointResult.Segments
   360  	if len(segments) < 2 {
   361  		return false, "not deterministically failing"
   362  	}
   363  	curSegment := segments[0]
   364  	prevSegment := segments[1]
   365  	// The latest failure is not deterministically failing, return false.
   366  	if curSegment.CountTotalResults != curSegment.CountUnexpectedResults {
   367  		return false, "not deterministically failing"
   368  	}
   369  	// If the test failure is still ongoing, the regression range of the failure
   370  	// on record should equal or contain the current regression range of
   371  	// the latest segment in changepoint analysis.
   372  	// Because the regression range of deterministic failure obtained from changepoint analysis
   373  	// only shrinks or stays the same over time.
   374  	if (curSegment.StartPosition.Int64 <= tf.RegressionEndPosition) &&
   375  		(prevSegment.EndPosition.Int64 >= tf.RegressionStartPosition) {
   376  		return true, ""
   377  	}
   378  	return false, "latest bisected failure is not ongoing"
   379  }
   380  
   381  // GetAnalysisResult returns an analysis for pRPC from CompileFailureAnalysis
   382  func GetAnalysisResult(c context.Context, analysis *model.CompileFailureAnalysis) (*pb.Analysis, error) {
   383  	result := &pb.Analysis{
   384  		AnalysisId:      analysis.Id,
   385  		Status:          analysis.Status,
   386  		RunStatus:       analysis.RunStatus,
   387  		CreatedTime:     timestamppb.New(analysis.CreateTime),
   388  		FirstFailedBbid: analysis.FirstFailedBuildId,
   389  		LastPassedBbid:  analysis.LastPassedBuildId,
   390  	}
   391  
   392  	if analysis.HasEnded() {
   393  		result.EndTime = timestamppb.New(analysis.EndTime)
   394  	}
   395  
   396  	// Populate Builder and BuildFailureType data
   397  	if analysis.CompileFailure != nil && analysis.CompileFailure.Parent() != nil {
   398  		// Add details from associated compile failure
   399  		failedBuild, err := datastoreutil.GetBuild(c, analysis.CompileFailure.Parent().IntID())
   400  		if err != nil {
   401  			return nil, err
   402  		}
   403  		if failedBuild != nil {
   404  			result.Builder = &buildbucketpb.BuilderID{
   405  				Project: failedBuild.Project,
   406  				Bucket:  failedBuild.Bucket,
   407  				Builder: failedBuild.Builder,
   408  			}
   409  			result.BuildFailureType = failedBuild.BuildFailureType
   410  		}
   411  	}
   412  
   413  	heuristicAnalysis, err := datastoreutil.GetHeuristicAnalysis(c, analysis)
   414  	if err != nil {
   415  		return nil, err
   416  	}
   417  	if heuristicAnalysis != nil {
   418  		suspects, err := datastoreutil.GetSuspectsForHeuristicAnalysis(c, heuristicAnalysis)
   419  		if err != nil {
   420  			return nil, err
   421  		}
   422  
   423  		pbSuspects := make([]*pb.HeuristicSuspect, len(suspects))
   424  		for i, suspect := range suspects {
   425  			pbSuspects[i] = &pb.HeuristicSuspect{
   426  				GitilesCommit:   &suspect.GitilesCommit,
   427  				ReviewUrl:       suspect.ReviewUrl,
   428  				Score:           int32(suspect.Score),
   429  				Justification:   suspect.Justification,
   430  				ConfidenceLevel: heuristic.GetConfidenceLevel(suspect.Score),
   431  			}
   432  
   433  			verificationDetails, err := constructSuspectVerificationDetails(c, suspect)
   434  			if err != nil {
   435  				return nil, errors.Annotate(err, "couldn't constructSuspectVerificationDetails").Err()
   436  			}
   437  			pbSuspects[i].VerificationDetails = verificationDetails
   438  
   439  			// TODO: check access permissions before including the review title.
   440  			//       For now, we will include it by default as LUCI Bisection access
   441  			//       should already be restricted to internal users only.
   442  			pbSuspects[i].ReviewTitle = suspect.ReviewTitle
   443  		}
   444  		heuristicResult := &pb.HeuristicAnalysisResult{
   445  			Status:    heuristicAnalysis.Status,
   446  			StartTime: timestamppb.New(heuristicAnalysis.StartTime),
   447  			Suspects:  pbSuspects,
   448  		}
   449  		if heuristicAnalysis.HasEnded() {
   450  			heuristicResult.EndTime = timestamppb.New(heuristicAnalysis.EndTime)
   451  		}
   452  
   453  		result.HeuristicResult = heuristicResult
   454  	}
   455  
   456  	// Get culprits
   457  	culprits := make([]*pb.Culprit, len(analysis.VerifiedCulprits))
   458  	for i, culprit := range analysis.VerifiedCulprits {
   459  		suspect := &model.Suspect{
   460  			Id:             culprit.IntID(),
   461  			ParentAnalysis: culprit.Parent(),
   462  		}
   463  		err = datastore.Get(c, suspect)
   464  		if err != nil {
   465  			return nil, err
   466  		}
   467  
   468  		pbCulprit := &pb.Culprit{
   469  			Commit:      &suspect.GitilesCommit,
   470  			ReviewUrl:   suspect.ReviewUrl,
   471  			ReviewTitle: suspect.ReviewTitle,
   472  		}
   473  
   474  		// Add suspect verification details for the culprit
   475  		verificationDetails, err := constructSuspectVerificationDetails(c, suspect)
   476  		if err != nil {
   477  			return nil, err
   478  		}
   479  		pbCulprit.VerificationDetails = verificationDetails
   480  		pbCulprit.CulpritAction = protoutil.CulpritActionsForSuspect(suspect)
   481  		culprits[i] = pbCulprit
   482  	}
   483  	result.Culprits = culprits
   484  
   485  	nthSectionResult, err := getNthSectionResult(c, analysis)
   486  	if err != nil {
   487  		// If fetching nthSection analysis result failed for some reasons, print
   488  		// out the error, but we still continue.
   489  		err = errors.Annotate(err, "getNthSectionResult for analysis %d", analysis.Id).Err()
   490  		logging.Errorf(c, err.Error())
   491  	} else {
   492  		result.NthSectionResult = nthSectionResult
   493  	}
   494  
   495  	// TODO (nqmtuan): add culprit actions for:
   496  	//     * commenting on related bugs
   497  
   498  	return result, nil
   499  }
   500  
   501  func getNthSectionResult(c context.Context, cfa *model.CompileFailureAnalysis) (*pb.NthSectionAnalysisResult, error) {
   502  	nsa, err := datastoreutil.GetNthSectionAnalysis(c, cfa)
   503  	if err != nil {
   504  		return nil, errors.Annotate(err, "getting nthsection analysis").Err()
   505  	}
   506  	if nsa == nil {
   507  		return nil, nil
   508  	}
   509  	if nsa.BlameList == nil {
   510  		return nil, errors.Annotate(err, "couldn't find blamelist").Err()
   511  	}
   512  	result := &pb.NthSectionAnalysisResult{
   513  		Status:    nsa.Status,
   514  		StartTime: timestamppb.New(nsa.StartTime),
   515  		BlameList: nsa.BlameList,
   516  	}
   517  	if nsa.HasEnded() {
   518  		result.EndTime = timestamppb.New(nsa.EndTime)
   519  	}
   520  
   521  	// Get all reruns for the current analysis
   522  	// This should contain all reruns for nth section and culprit verification
   523  	reruns, err := datastoreutil.GetRerunsForAnalysis(c, cfa)
   524  	if err != nil {
   525  		return nil, err
   526  	}
   527  
   528  	for _, rerun := range reruns {
   529  		rerunResult := &pb.SingleRerun{
   530  			StartTime: timestamppb.New(rerun.StartTime),
   531  			RerunResult: &pb.RerunResult{
   532  				RerunStatus: rerun.Status,
   533  			},
   534  			Bbid:   rerun.RerunBuild.IntID(),
   535  			Commit: &rerun.GitilesCommit,
   536  			Type:   string(rerun.Type),
   537  		}
   538  		if rerun.HasEnded() {
   539  			rerunResult.EndTime = timestamppb.New(rerun.EndTime)
   540  		}
   541  		index, err := findRerunIndexInBlameList(rerun, nsa.BlameList)
   542  		if err != nil {
   543  			// There is only one case where we cannot find the rerun in blamelist
   544  			// It is when the rerun is part of the culprit verification and is
   545  			// the "last pass" revision.
   546  			// In this case, we should just log and continue, and the run will appear
   547  			// as part of culprit verification component.
   548  			logging.Warningf(c, errors.Annotate(err, "couldn't find index for rerun").Err().Error())
   549  			continue
   550  		}
   551  		rerunResult.Index = strconv.FormatInt(int64(index), 10)
   552  		result.Reruns = append(result.Reruns, rerunResult)
   553  	}
   554  
   555  	// Find remaining regression range
   556  	snapshot, err := nthsection.CreateSnapshot(c, nsa)
   557  	if err != nil {
   558  		return nil, errors.Annotate(err, "couldn't create snapshot").Err()
   559  	}
   560  
   561  	ff, lp, err := snapshot.GetCurrentRegressionRange()
   562  	// GetCurrentRegressionRange return error if the regression is invalid
   563  	// We don't want to return the error here, but just continue
   564  	if err != nil {
   565  		err = errors.Annotate(err, "getCurrentRegressionRange").Err()
   566  		// Log as Debugf because it is not exactly an error, but just a state of the analysis
   567  		logging.Debugf(c, err.Error())
   568  	} else {
   569  		result.RemainingNthSectionRange = &pb.RegressionRange{
   570  			FirstFailed: getCommitFromIndex(ff, nsa.BlameList, cfa),
   571  			LastPassed:  getCommitFromIndex(lp, nsa.BlameList, cfa),
   572  		}
   573  	}
   574  
   575  	// Find suspect
   576  	suspect, err := datastoreutil.GetSuspectForNthSectionAnalysis(c, nsa)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  	if suspect != nil {
   581  		pbSuspect := &pb.NthSectionSuspect{
   582  			GitilesCommit: &suspect.GitilesCommit,
   583  			ReviewUrl:     suspect.ReviewUrl,
   584  			ReviewTitle:   suspect.ReviewTitle,
   585  			Commit:        &suspect.GitilesCommit,
   586  		}
   587  
   588  		verificationDetails, err := constructSuspectVerificationDetails(c, suspect)
   589  		if err != nil {
   590  			return nil, errors.Annotate(err, "couldn't constructSuspectVerificationDetails").Err()
   591  		}
   592  		pbSuspect.VerificationDetails = verificationDetails
   593  		result.Suspect = pbSuspect
   594  	}
   595  
   596  	return result, nil
   597  }
   598  
   599  func findRerunIndexInBlameList(rerun *model.SingleRerun, blamelist *pb.BlameList) (int32, error) {
   600  	for i, commit := range blamelist.Commits {
   601  		if commit.Commit == rerun.GitilesCommit.Id {
   602  			return int32(i), nil
   603  		}
   604  	}
   605  	return -1, fmt.Errorf("couldn't find index for rerun %d", rerun.Id)
   606  }
   607  
   608  func getCommitFromIndex(index int, blamelist *pb.BlameList, cfa *model.CompileFailureAnalysis) *buildbucketpb.GitilesCommit {
   609  	return &buildbucketpb.GitilesCommit{
   610  		Id:      blamelist.Commits[index].Commit,
   611  		Host:    cfa.InitialRegressionRange.FirstFailed.Host,
   612  		Project: cfa.InitialRegressionRange.FirstFailed.Project,
   613  		Ref:     cfa.InitialRegressionRange.FirstFailed.Ref,
   614  	}
   615  }
   616  
   617  // constructSingleRerun constructs a pb.SingleRerun using the details from the
   618  // rerun build and latest single rerun
   619  func constructSingleRerun(c context.Context, rerunBBID int64) (*pb.SingleRerun, error) {
   620  	rerunBuild := &model.CompileRerunBuild{
   621  		Id: rerunBBID,
   622  	}
   623  	err := datastore.Get(c, rerunBuild)
   624  	if err != nil {
   625  		return nil, errors.Annotate(err, "failed getting rerun build").Err()
   626  	}
   627  
   628  	singleRerun, err := datastoreutil.GetLastRerunForRerunBuild(c, rerunBuild)
   629  	if err != nil {
   630  		return nil, errors.Annotate(err, "failed getting single rerun").Err()
   631  	}
   632  
   633  	result := &pb.SingleRerun{
   634  		StartTime: timestamppb.New(singleRerun.StartTime),
   635  		Bbid:      rerunBBID,
   636  		RerunResult: &pb.RerunResult{
   637  			RerunStatus: singleRerun.Status,
   638  		},
   639  		Commit: &singleRerun.GitilesCommit,
   640  	}
   641  	if singleRerun.HasEnded() {
   642  		result.EndTime = timestamppb.New(singleRerun.EndTime)
   643  	}
   644  	return result, nil
   645  }
   646  
   647  // constructSuspectVerificationDetails constructs a pb.SuspectVerificationDetails for the given suspect
   648  func constructSuspectVerificationDetails(c context.Context, suspect *model.Suspect) (*pb.SuspectVerificationDetails, error) {
   649  	// Add the current verification status
   650  	verificationDetails := &pb.SuspectVerificationDetails{
   651  		Status: string(suspect.VerificationStatus),
   652  	}
   653  
   654  	// Add rerun details for the suspect commit
   655  	if suspect.SuspectRerunBuild != nil {
   656  		singleRerun, err := constructSingleRerun(c, suspect.SuspectRerunBuild.IntID())
   657  		if err != nil {
   658  			return nil, errors.Annotate(err, "failed getting verification rerun for suspect commit").Err()
   659  		}
   660  		verificationDetails.SuspectRerun = singleRerun
   661  	}
   662  
   663  	// Add rerun details for the parent commit of suspect
   664  	if suspect.ParentRerunBuild != nil {
   665  		singleRerun, err := constructSingleRerun(c, suspect.ParentRerunBuild.IntID())
   666  		if err != nil {
   667  			return nil, errors.Annotate(err, "failed getting verification rerun for parent commit of suspect").Err()
   668  		}
   669  		verificationDetails.ParentRerun = singleRerun
   670  	}
   671  
   672  	return verificationDetails, nil
   673  }
   674  
   675  // validateQueryAnalysisRequest checks if the request is valid.
   676  func validateQueryAnalysisRequest(req *pb.QueryAnalysisRequest) error {
   677  	if req.BuildFailure == nil {
   678  		return status.Errorf(codes.InvalidArgument, "BuildFailure must not be empty")
   679  	}
   680  	if req.BuildFailure.GetBbid() == 0 {
   681  		return status.Errorf(codes.InvalidArgument, "BuildFailure bbid must not be empty")
   682  	}
   683  	return nil
   684  }
   685  
   686  // ListAnalyses returns existing analyses
   687  func (server *AnalysesServer) ListAnalyses(c context.Context, req *pb.ListAnalysesRequest) (*pb.ListAnalysesResponse, error) {
   688  	// Validate the request
   689  	if err := validateListAnalysesRequest(req); err != nil {
   690  		return nil, err
   691  	}
   692  
   693  	// Decode cursor from page token
   694  	cursor, err := listAnalysesPageTokenVault.Cursor(c, req.PageToken)
   695  	switch err {
   696  	case pagination.ErrInvalidPageToken:
   697  		return nil, status.Errorf(codes.InvalidArgument, "Invalid page token")
   698  	case nil:
   699  		// Continue
   700  	default:
   701  		return nil, status.Errorf(codes.Internal, err.Error())
   702  	}
   703  
   704  	// Override the page size if necessary
   705  	pageSize := int(listAnalysesPageSizeLimiter.Adjust(req.PageSize))
   706  
   707  	// Construct the query
   708  	q := datastore.NewQuery("CompileFailureAnalysis").Order("-create_time").Start(cursor)
   709  
   710  	// Query datastore for compile failure analyses
   711  	compileFailureAnalyses := make([]*model.CompileFailureAnalysis, 0, pageSize)
   712  	var nextCursor datastore.Cursor
   713  	err = datastore.Run(c, q, func(compileFailureAnalysis *model.CompileFailureAnalysis, getCursor datastore.CursorCB) error {
   714  		compileFailureAnalyses = append(compileFailureAnalyses, compileFailureAnalysis)
   715  
   716  		// Check whether the page size limit has been reached
   717  		if len(compileFailureAnalyses) == pageSize {
   718  			nextCursor, err = getCursor()
   719  			if err != nil {
   720  				return err
   721  			}
   722  			return datastore.Stop
   723  		}
   724  		return nil
   725  	})
   726  	if err != nil {
   727  		return nil, status.Errorf(codes.Internal, err.Error())
   728  	}
   729  
   730  	// Construct the next page token
   731  	nextPageToken, err := listAnalysesPageTokenVault.PageToken(c, nextCursor)
   732  	if err != nil {
   733  		return nil, status.Errorf(codes.Internal, err.Error())
   734  	}
   735  
   736  	// Get the result for each compile failure analysis
   737  	analyses := make([]*pb.Analysis, len(compileFailureAnalyses))
   738  	err = parallel.FanOutIn(func(workC chan<- func() error) {
   739  		for i, compileFailureAnalysis := range compileFailureAnalyses {
   740  			i := i
   741  			compileFailureAnalysis := compileFailureAnalysis
   742  			workC <- func() error {
   743  				analysis, err := GetAnalysisResult(c, compileFailureAnalysis)
   744  				if err != nil {
   745  					logging.Errorf(c, "Could not get analysis data for analysis %d: %s",
   746  						compileFailureAnalysis.Id, err)
   747  					return err
   748  				}
   749  				analyses[i] = analysis
   750  				return nil
   751  			}
   752  		}
   753  	})
   754  	if err != nil {
   755  		return nil, status.Errorf(codes.Internal, err.Error())
   756  	}
   757  
   758  	return &pb.ListAnalysesResponse{
   759  		Analyses:      analyses,
   760  		NextPageToken: nextPageToken,
   761  	}, nil
   762  }
   763  
   764  // validateListAnalysesRequest checks if the request is valid.
   765  func validateListAnalysesRequest(req *pb.ListAnalysesRequest) error {
   766  	if req.PageSize < 0 {
   767  		return status.Errorf(codes.InvalidArgument, "Page size can't be negative")
   768  	}
   769  
   770  	return nil
   771  }
   772  
   773  // validateListTestAnalysesRequest checks if the ListTestAnalysesRequest is valid.
   774  func validateListTestAnalysesRequest(req *pb.ListTestAnalysesRequest) error {
   775  	if req.Project == "" {
   776  		return status.Errorf(codes.InvalidArgument, "project must not be empty")
   777  	}
   778  	if req.PageSize < 0 {
   779  		return status.Errorf(codes.InvalidArgument, "page size must not be negative")
   780  	}
   781  	return nil
   782  }
   783  
   784  func validateBatchGetTestAnalysesRequest(req *pb.BatchGetTestAnalysesRequest) error {
   785  	// MaxTestFailures is the maximum number of test failures to be queried in one request.
   786  	const MaxTestFailures = 100
   787  	if err := util.ValidateProject(req.Project); err != nil {
   788  		return errors.Annotate(err, "project").Err()
   789  	}
   790  	if len(req.TestFailures) == 0 {
   791  		return errors.Reason("test_failures: unspecified").Err()
   792  	}
   793  	if len(req.TestFailures) > MaxTestFailures {
   794  		return errors.Reason("test_failures: no more than %v may be queried at a time", MaxTestFailures).Err()
   795  	}
   796  	for i, tf := range req.TestFailures {
   797  		if tf.GetTestId() == "" {
   798  			return errors.Reason("test_variants[%v]: test_id: unspecified", i).Err()
   799  		}
   800  		if tf.VariantHash == "" {
   801  			return errors.Reason("test_variants[%v]: variant_hash: unspecified", i).Err()
   802  		}
   803  		if tf.RefHash == "" {
   804  			return errors.Reason("test_variants[%v]: ref_hash: unspecified", i).Err()
   805  		}
   806  		if err := rdbpbutil.ValidateTestID(tf.TestId); err != nil {
   807  			return errors.Annotate(err, "test_variants[%v].test_id", i).Err()
   808  		}
   809  		if err := util.ValidateVariantHash(tf.VariantHash); err != nil {
   810  			return errors.Annotate(err, "test_variants[%v].variant_hash", i).Err()
   811  		}
   812  		if err := util.ValidateRefHash(tf.RefHash); err != nil {
   813  			return errors.Annotate(err, "test_variants[%v].ref_hash", i).Err()
   814  		}
   815  	}
   816  	return nil
   817  }
   818  
   819  func defaultFieldMask() *fieldmaskpb.FieldMask {
   820  	return &fieldmaskpb.FieldMask{
   821  		Paths: []string{"*"},
   822  	}
   823  }