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

     1  // Copyright 2022 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 revertculprit contains the logic to revert culprits
    16  package revertculprit
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/grpc/status"
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"go.chromium.org/luci/bisection/internal/config"
    28  	"go.chromium.org/luci/bisection/internal/gerrit"
    29  	"go.chromium.org/luci/bisection/internal/lucianalysis"
    30  	"go.chromium.org/luci/bisection/model"
    31  	configpb "go.chromium.org/luci/bisection/proto/config"
    32  	pb "go.chromium.org/luci/bisection/proto/v1"
    33  	taskpb "go.chromium.org/luci/bisection/task/proto"
    34  	"go.chromium.org/luci/bisection/util"
    35  	"go.chromium.org/luci/bisection/util/datastoreutil"
    36  	"go.chromium.org/luci/bisection/util/loggingutil"
    37  	"go.chromium.org/luci/server"
    38  
    39  	"go.chromium.org/luci/common/clock"
    40  	"go.chromium.org/luci/common/errors"
    41  	"go.chromium.org/luci/common/logging"
    42  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    43  	"go.chromium.org/luci/common/retry/transient"
    44  	"go.chromium.org/luci/gae/service/datastore"
    45  	"go.chromium.org/luci/server/tq"
    46  )
    47  
    48  var CompileFailureTasks = tq.RegisterTaskClass(tq.TaskClass{
    49  	ID:        "revert-culprit-action",
    50  	Prototype: (*taskpb.RevertCulpritTask)(nil),
    51  	Queue:     "revert-culprit-action",
    52  	Kind:      tq.NonTransactional,
    53  })
    54  
    55  var TestFailureTasks = tq.RegisterTaskClass(tq.TaskClass{
    56  	ID:        "test-failure-culprit-action",
    57  	Prototype: (*taskpb.TestFailureCulpritActionTask)(nil),
    58  	Queue:     "test-failure-culprit-action",
    59  	Kind:      tq.NonTransactional,
    60  })
    61  
    62  // RegisterTaskClass registers the task class for tq dispatcher
    63  func RegisterTaskClass(srv *server.Server, luciAnalysisProjectFunc func(luciProject string) string) error {
    64  	client, err := lucianalysis.NewClient(srv.Context, srv.Options.CloudProject, luciAnalysisProjectFunc)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	srv.RegisterCleanup(func(context.Context) {
    69  		client.Close()
    70  	})
    71  
    72  	CompileFailureTasks.AttachHandler(processRevertCulpritTask)
    73  	TestFailureTasks.AttachHandler(func(ctx context.Context, payload proto.Message) error {
    74  		task := payload.(*taskpb.TestFailureCulpritActionTask)
    75  		if err := processTestFailureCulpritTask(ctx, task.AnalysisId, client); err != nil {
    76  			err := errors.Annotate(err, "run test failure culprit action").Err()
    77  			logging.Errorf(ctx, err.Error())
    78  
    79  			// Return nil so the task will not be retried.
    80  			// If in the future, task retry is required, remember to update HasTakenActions
    81  			// field to false before retry.
    82  			return nil
    83  		}
    84  		return nil
    85  	})
    86  	return nil
    87  }
    88  
    89  func processRevertCulpritTask(ctx context.Context, payload proto.Message) error {
    90  	task := payload.(*taskpb.RevertCulpritTask)
    91  
    92  	analysisID := task.GetAnalysisId()
    93  	culpritID := task.GetCulpritId()
    94  
    95  	ctx, err := loggingutil.UpdateLoggingWithAnalysisID(ctx, analysisID)
    96  	if err != nil {
    97  		// not critical, just log
    98  		err := errors.Annotate(err, "failed UpdateLoggingWithAnalysisID %d", analysisID)
    99  		logging.Errorf(ctx, "%v", err)
   100  	}
   101  
   102  	logging.Infof(ctx,
   103  		"Processing revert culprit task for analysis ID=%d, culprit ID=%d",
   104  		analysisID, culpritID)
   105  
   106  	cfa, err := datastoreutil.GetCompileFailureAnalysis(ctx, analysisID)
   107  	if err != nil {
   108  		// failed getting the CompileFailureAnalysis, so no point retrying
   109  		err = errors.Annotate(err,
   110  			"failed getting CompileFailureAnalysis when processing culprit revert task").Err()
   111  		logging.Errorf(ctx, err.Error())
   112  		return nil
   113  	}
   114  
   115  	var culprit *model.Suspect
   116  	for _, verifiedCulprit := range cfa.VerifiedCulprits {
   117  		if verifiedCulprit.IntID() == culpritID {
   118  			culprit, err = datastoreutil.GetSuspect(ctx, culpritID, verifiedCulprit.Parent())
   119  			if err != nil {
   120  				// failed getting the Suspect, so no point retrying
   121  				err = errors.Annotate(err,
   122  					"failed getting Suspect when processing culprit revert task").Err()
   123  				logging.Errorf(ctx, err.Error())
   124  				return nil
   125  			}
   126  			break
   127  		}
   128  	}
   129  
   130  	if culprit == nil {
   131  		// culprit is not within the analysis' verified culprits, so no point retrying
   132  		logging.Errorf(ctx, "failed to find the culprit within the analysis' verified culprits")
   133  		return nil
   134  	}
   135  
   136  	// The analysis should be canceled. We should not do any gerrit actions.
   137  	if cfa.ShouldCancel {
   138  		logging.Infof(ctx, "Analysis %d was canceled. No gerrit action required.", analysisID)
   139  		saveInactionReason(ctx, culprit, pb.CulpritInactionReason_ANALYSIS_CANCELED)
   140  		return nil
   141  	}
   142  
   143  	// Revert culprit
   144  	err = TakeCulpritAction(ctx, culprit)
   145  	if err != nil {
   146  		// If the error is transient, return err to retry
   147  		if transient.Tag.In(err) {
   148  			return err
   149  		}
   150  
   151  		// non-transient error, so do not retry
   152  		logging.Errorf(ctx, err.Error())
   153  		return nil
   154  	}
   155  	return nil
   156  }
   157  
   158  func isSuspectGerritActionReady(ctx context.Context, culpritModel *model.Suspect, gerritConfig *configpb.GerritConfig) (bool, error) {
   159  	// We only proceed with heuristic culprit if it is a confirmed culprit
   160  	if culpritModel.Type == model.SuspectType_Heuristic {
   161  		if culpritModel.VerificationStatus == model.SuspectVerificationStatus_ConfirmedCulprit {
   162  			return true, nil
   163  		}
   164  		return false, fmt.Errorf("suspect (commit %s) has verification status %s and should not be reverted",
   165  			culpritModel.GitilesCommit.Id, culpritModel.VerificationStatus)
   166  	}
   167  	// Nthsection
   168  	if culpritModel.Type == model.SuspectType_NthSection {
   169  		settings := gerritConfig.NthsectionSettings
   170  		if !settings.Enabled {
   171  			logging.Infof(ctx, "Nthsection settings is disabled")
   172  			return false, nil
   173  		}
   174  		if culpritModel.VerificationStatus == model.SuspectVerificationStatus_ConfirmedCulprit || (settings.ActionWhenVerificationError && culpritModel.VerificationStatus == model.SuspectVerificationStatus_VerificationError) {
   175  			return true, nil
   176  		}
   177  		return false, fmt.Errorf("suspect (commit %s) has verification status %s and should not be reverted",
   178  			culpritModel.GitilesCommit.Id, culpritModel.VerificationStatus)
   179  	}
   180  	return false, fmt.Errorf("unsupported suspect type: %s", culpritModel.Type)
   181  }
   182  
   183  // TakeCulpritAction attempts to comment culprit, comment revert, create revert and commit revert for a culprit
   184  // when the culprit satisfies the critieria of the action.
   185  // A culprit is identified as a result of a heuristic analysis or an nthsection analysis.
   186  func TakeCulpritAction(ctx context.Context, culpritModel *model.Suspect) error {
   187  	project, err := datastoreutil.GetProjectForSuspect(ctx, culpritModel)
   188  	if err != nil {
   189  		return errors.Annotate(err, "get project for suspect").Err()
   190  	}
   191  	// Get gerrit config.
   192  	gerritConfig, err := config.GetGerritCfgForSuspect(ctx, culpritModel, project)
   193  	if err != nil {
   194  		return errors.Annotate(err, "get gerrit config for suspect").Err()
   195  	}
   196  	// Check if Gerrit actions are disabled
   197  	if !gerritConfig.ActionsEnabled {
   198  		logging.Infof(ctx, "Gerrit actions have been disabled")
   199  		saveInactionReason(ctx, culpritModel, pb.CulpritInactionReason_ACTIONS_DISABLED)
   200  		return nil
   201  	}
   202  
   203  	// Check the culprit verification status
   204  	shouldTakeAction, err := isSuspectGerritActionReady(ctx, culpritModel, gerritConfig)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	if !shouldTakeAction {
   209  		return nil
   210  	}
   211  
   212  	// Make Gerrit client
   213  	gerritHost, err := gerrit.GetHost(ctx, culpritModel.ReviewUrl)
   214  	if err != nil {
   215  		logging.Errorf(ctx, err.Error())
   216  		return err
   217  	}
   218  	gerritClient, err := gerrit.NewClient(ctx, gerritHost)
   219  	if err != nil {
   220  		logging.Errorf(ctx, err.Error())
   221  		return err
   222  	}
   223  
   224  	// Get the culprit's Gerrit change
   225  	culprit, err := gerritClient.GetChange(ctx,
   226  		culpritModel.GitilesCommit.Project, culpritModel.GitilesCommit.Id)
   227  	if err != nil {
   228  		logging.Errorf(ctx, err.Error())
   229  		return err
   230  	}
   231  
   232  	// Check for existing reverts
   233  	existingRevert, err := getMostRelevantRevert(ctx, gerritClient, culprit)
   234  	if err != nil {
   235  		logging.Errorf(ctx, err.Error())
   236  		return err
   237  	}
   238  	if existingRevert != nil {
   239  		err = saveRevertURL(ctx, gerritClient, culpritModel, existingRevert)
   240  		if err != nil {
   241  			// not critical - just log the error
   242  			logging.Errorf(ctx, err.Error())
   243  		}
   244  
   245  		switch existingRevert.Status {
   246  		case gerritpb.ChangeStatus_MERGED:
   247  			// There is a merged revert - no further action required.
   248  
   249  			// Update the inaction reason based on whether the revert was created by
   250  			// LUCI Bisection.
   251  			lbOwned, err := gerrit.IsOwnedByLUCIBisection(ctx, existingRevert)
   252  			if err != nil {
   253  				// Not critical - just log the error and skip updating the
   254  				// inaction reason.
   255  				err = errors.Annotate(err,
   256  					"no action required but failed to find owner of existing revert").Err()
   257  				logging.Errorf(ctx, err.Error())
   258  			} else {
   259  				reason := pb.CulpritInactionReason_REVERTED_MANUALLY
   260  				if lbOwned {
   261  					reason = pb.CulpritInactionReason_REVERTED_BY_BISECTION
   262  				}
   263  				saveInactionReason(ctx, culpritModel, reason)
   264  			}
   265  
   266  			return nil
   267  		case gerritpb.ChangeStatus_NEW:
   268  			// add a supporting comment to the first revert
   269  			err = commentSupportOnExistingRevert(ctx, gerritClient, culpritModel, existingRevert)
   270  			if err != nil {
   271  				logging.Errorf(ctx, err.Error())
   272  				return err
   273  			}
   274  			return nil
   275  		case gerritpb.ChangeStatus_ABANDONED:
   276  			// add a comment on the culprit since the revert has been abandoned
   277  			err = commentReasonOnCulprit(ctx, gerritClient, culpritModel, culprit,
   278  				"an abandoned revert already exists")
   279  			if err != nil {
   280  				logging.Errorf(ctx, err.Error())
   281  				return err
   282  			}
   283  			return nil
   284  		default:
   285  			logging.Errorf(ctx,
   286  				"status was not recognized for existing revert %s~%d [status='%v']",
   287  				existingRevert.Project, existingRevert.Number, existingRevert.Status)
   288  		}
   289  
   290  		return nil
   291  	}
   292  
   293  	shouldCreateRevert, reason, err := isCulpritRevertible(ctx, gerritClient, culprit, culpritModel, project)
   294  	if err != nil {
   295  		logging.Errorf(ctx, err.Error())
   296  		return err
   297  	}
   298  	if !shouldCreateRevert {
   299  		// Add a comment on the culprit CL to explain why a revert was not created
   300  		err = commentReasonOnCulprit(ctx, gerritClient, culpritModel, culprit,
   301  			reason)
   302  		if err != nil {
   303  			logging.Errorf(ctx, err.Error())
   304  			return err
   305  		}
   306  
   307  		return nil
   308  	}
   309  
   310  	// Create revert
   311  	revert, err := createRevert(ctx, gerritClient, culpritModel, culprit)
   312  	if err != nil {
   313  		logging.Errorf(ctx, err.Error())
   314  
   315  		if status.Convert(errors.Unwrap(err)).Code() == codes.DeadlineExceeded {
   316  			// Workaround for Gerrit performance issue with revert creations
   317  			// (see b/261896675). The request may have timed out but the revert may
   318  			// have been successfully created, so look for the newly created revert
   319  			createdRevert, searchErr := searchForCreatedRevert(ctx, gerritClient,
   320  				culpritModel, culprit)
   321  			if searchErr != nil {
   322  				logging.Errorf(ctx, searchErr.Error())
   323  				return searchErr
   324  			}
   325  
   326  			if createdRevert != nil {
   327  				logging.Debugf(ctx, "continuing revert process; found created revert")
   328  				revert = createdRevert
   329  			} else {
   330  				logging.Debugf(ctx, "could not find the revert created by LUCI Bisection")
   331  				return err
   332  			}
   333  		} else {
   334  			return err
   335  		}
   336  	}
   337  
   338  	err = saveCreationDetails(ctx, gerritClient, culpritModel, revert)
   339  	if err != nil {
   340  		logging.Errorf(ctx, err.Error())
   341  
   342  		// a revert was created by LUCI Bisection - add reviewers to it
   343  		shouldReview, reviewErr := isRevertActive(ctx, gerritClient, revert)
   344  		if reviewErr != nil {
   345  			logging.Errorf(ctx, reviewErr.Error())
   346  			return reviewErr
   347  		}
   348  		if shouldReview {
   349  			reviewErr = sendRevertForReview(ctx, gerritClient, culpritModel, revert,
   350  				"an unexpected error occurred after LUCI Bisection created this revert")
   351  			if reviewErr != nil {
   352  				logging.Errorf(ctx, reviewErr.Error())
   353  				return reviewErr
   354  			}
   355  		}
   356  
   357  		return err
   358  	}
   359  
   360  	// Check again for merged reverts for the culprit, in case
   361  	// another revert was manually created and merged while waiting for Gerrit
   362  	// to finish creating the revert.
   363  	existingReverts, err := gerritClient.GetReverts(ctx, culprit)
   364  	if err != nil {
   365  		logging.Errorf(ctx, err.Error())
   366  		return err
   367  	}
   368  	for _, existingRevert := range existingReverts {
   369  		if existingRevert.Status == gerritpb.ChangeStatus_MERGED {
   370  			// A revert has already been merged, so there is no need to commit the
   371  			// revert created by LUCI Bisection
   372  			logging.Debugf(ctx, "existing revert %s~%d already merged for culprit %s~%d",
   373  				existingRevert.Project, existingRevert.Number,
   374  				culprit.Project, culprit.Number)
   375  
   376  			// TODO (nqmtuan): Automatically abandon the revert created by
   377  			// LUCI Bisection if this merged revert is different. Currently, the
   378  			// created revert will be left open until manually abandoned.
   379  
   380  			return nil
   381  		}
   382  	}
   383  
   384  	// Check the revert is still active, as creation can take a long time so
   385  	// it may have been manually updated
   386  	isActive, err := isRevertActive(ctx, gerritClient, revert)
   387  	if err != nil {
   388  		logging.Errorf(ctx, err.Error())
   389  		return err
   390  	}
   391  	if !isActive {
   392  		// revert has been manually updated, so no further action is required
   393  		return nil
   394  	}
   395  
   396  	shouldCommit, reason, err := canCommit(ctx, culprit, culpritModel, project)
   397  	if err != nil {
   398  		logging.Errorf(ctx, err.Error())
   399  		return err
   400  	}
   401  	if !shouldCommit {
   402  		// Send the revert for manual review and add a comment to explain why the
   403  		// revert was not automatically submitted
   404  		err = sendRevertForReview(ctx, gerritClient, culpritModel, revert,
   405  			reason)
   406  		if err != nil {
   407  			logging.Errorf(ctx, err.Error())
   408  			return err
   409  		}
   410  
   411  		return nil
   412  	}
   413  
   414  	// Commit revert
   415  	err = commitRevert(ctx, gerritClient, culpritModel, revert)
   416  	if err != nil {
   417  		logging.Errorf(ctx, err.Error())
   418  
   419  		// Send the revert to be manually reviewed
   420  		reviewErr := sendRevertForReview(ctx, gerritClient, culpritModel, revert,
   421  			"an error occurred when attempting to submit it")
   422  		if reviewErr != nil {
   423  			logging.Errorf(ctx, reviewErr.Error())
   424  			return reviewErr
   425  		}
   426  
   427  		return err
   428  	}
   429  	err = saveCommitDetails(ctx, culpritModel)
   430  	if err != nil {
   431  		logging.Errorf(ctx, err.Error())
   432  		return err
   433  	}
   434  
   435  	return nil
   436  }
   437  
   438  // getMostRelevantRevert returns the most relevant revert based on the
   439  // revert change's status, in the order of merged > active > abandoned > nil.
   440  func getMostRelevantRevert(ctx context.Context, gerritClient *gerrit.Client,
   441  	culprit *gerritpb.ChangeInfo) (*gerritpb.ChangeInfo, error) {
   442  	// Check for existing reverts
   443  	reverts, err := gerritClient.GetReverts(ctx, culprit)
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  
   448  	var activeRevert *gerritpb.ChangeInfo = nil
   449  	var abandonedRevert *gerritpb.ChangeInfo = nil
   450  	for _, revert := range reverts {
   451  		logging.Debugf(ctx, "Existing revert found for culprit %s~%d - revert is %s~%d",
   452  			culprit.Project, culprit.Number, revert.Project, revert.Number)
   453  
   454  		switch revert.Status {
   455  		case gerritpb.ChangeStatus_MERGED:
   456  			return revert, nil
   457  		case gerritpb.ChangeStatus_ABANDONED:
   458  			if abandonedRevert == nil {
   459  				abandonedRevert = revert
   460  			}
   461  		case gerritpb.ChangeStatus_NEW:
   462  			if activeRevert == nil {
   463  				activeRevert = revert
   464  			}
   465  		default:
   466  			logging.Debugf(ctx, "ignoring revert %s~%d due to its unrecognized status %v",
   467  				revert.Project, revert.Number, revert.Status)
   468  		}
   469  	}
   470  
   471  	if activeRevert != nil {
   472  		// there is an existing revert yet to be merged
   473  		return activeRevert, nil
   474  	}
   475  
   476  	if abandonedRevert != nil {
   477  		// there is an abandoned revert
   478  		return abandonedRevert, nil
   479  	}
   480  
   481  	return nil, nil
   482  }
   483  
   484  // searchForCreatedRevert returns the revert CL created by LUCI Bisection
   485  // when processing the given Suspect, if it exists.
   486  func searchForCreatedRevert(ctx context.Context, gerritClient *gerrit.Client,
   487  	culpritModel *model.Suspect, culprit *gerritpb.ChangeInfo) (*gerritpb.ChangeInfo, error) {
   488  	// Construct the revert description to use for comparison since different
   489  	// analyses can result in the same culprit CL. The revert description
   490  	// similarity can be used to ascertain whether a revert CL was created for
   491  	// this specific Suspect
   492  	generatedRevertDescription, err := generateRevertDescription(ctx, culpritModel, culprit)
   493  	if err != nil {
   494  		return nil, errors.Annotate(err, "failed generating revert description"+
   495  			" for comparison when searching for created revert").Err()
   496  	}
   497  	// Drop the last paragraph for the comparison as Gerrit may have inserted
   498  	// its own values in the footer
   499  	paragraphs := strings.Split(generatedRevertDescription, "\n\n")
   500  	descriptionStart := strings.Join(paragraphs[:len(paragraphs)-1], "\n\n")
   501  
   502  	// Check for existing reverts
   503  	reverts, err := gerritClient.GetReverts(ctx, culprit)
   504  	if err != nil {
   505  		return nil, errors.Annotate(err,
   506  			"failed getting existing reverts when searching for created revert").Err()
   507  	}
   508  
   509  	var createdRevert *gerritpb.ChangeInfo = nil
   510  	for _, revert := range reverts {
   511  		lbOwned, err := gerrit.IsOwnedByLUCIBisection(ctx, revert)
   512  		if err != nil {
   513  			// non-critical - log the error and move on
   514  			err = errors.Annotate(err,
   515  				"error searching for created revert when checking owner").Err()
   516  			logging.Errorf(ctx, err.Error())
   517  			continue
   518  		}
   519  
   520  		// Check if the revert was created by LUCI Bisection
   521  		if lbOwned {
   522  			revertDescription, err := gerrit.CommitMessage(ctx, revert)
   523  			if err != nil {
   524  				// non-critical - log the error and move on
   525  				err = errors.Annotate(err,
   526  					"error searching for created revert when getting commit message").Err()
   527  				logging.Errorf(ctx, err.Error())
   528  				continue
   529  			}
   530  
   531  			// Check if the description starts as expected, to confirm this revert CL
   532  			// was the newly-created one for this specific Suspect and not from
   533  			// another analysis
   534  			if strings.HasPrefix(revertDescription, descriptionStart) {
   535  				createdRevert = revert
   536  				break
   537  			}
   538  		}
   539  	}
   540  
   541  	return createdRevert, nil
   542  }
   543  
   544  func isRevertActive(ctx context.Context, gerritClient *gerrit.Client,
   545  	revert *gerritpb.ChangeInfo) (bool, error) {
   546  	// Refetch the created revert to get its latest status
   547  	revert, err := gerritClient.RefetchChange(ctx, revert)
   548  	if err != nil {
   549  		return false, errors.Annotate(err,
   550  			"error refetching revert created by LUCI Bisection").Err()
   551  	}
   552  
   553  	if revert.Status == gerritpb.ChangeStatus_NEW {
   554  		return true, nil
   555  	} else {
   556  		// the revert created by LUCI Bisection has been manually updated
   557  		logging.Debugf(ctx, "revert %s~%d created by LUCI Bisection was updated"+
   558  			" manually [status=%v]", revert.Project, revert.Number, revert.Status)
   559  		return false, nil
   560  	}
   561  }
   562  
   563  // saveRevertURL updates the revert URL for the given Suspect
   564  func saveRevertURL(ctx context.Context, gerritClient *gerrit.Client,
   565  	culpritModel *model.Suspect, revert *gerritpb.ChangeInfo) error {
   566  	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   567  		e := datastore.Get(ctx, culpritModel)
   568  		if e != nil {
   569  			return e
   570  		}
   571  		// set the revert CL URL
   572  		culpritModel.RevertURL = util.ConstructGerritCodeReviewURL(ctx, gerritClient, revert)
   573  		return datastore.Put(ctx, culpritModel)
   574  	}, nil)
   575  
   576  	if err != nil {
   577  		err = errors.Annotate(err,
   578  			"couldn't update suspect details for culprit with existing revert").Err()
   579  		return err
   580  	}
   581  
   582  	return nil
   583  }
   584  
   585  func saveCreationDetails(ctx context.Context, gerritClient *gerrit.Client,
   586  	culpritModel *model.Suspect, revert *gerritpb.ChangeInfo) error {
   587  	// Update tsmon metrics
   588  	err := updateCulpritActionCounter(ctx, culpritModel, ActionTypeCreateRevert)
   589  	if err != nil {
   590  		logging.Errorf(ctx, errors.Annotate(err, "updateCulpritActionCounter").Err().Error())
   591  	}
   592  
   593  	// Update revert details for creation
   594  	err = datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   595  		e := datastore.Get(ctx, culpritModel)
   596  		if e != nil {
   597  			return e
   598  		}
   599  
   600  		culpritModel.RevertURL = util.ConstructGerritCodeReviewURL(ctx, gerritClient, revert)
   601  		culpritModel.IsRevertCreated = true
   602  		culpritModel.RevertCreateTime = clock.Now(ctx)
   603  
   604  		return datastore.Put(ctx, culpritModel)
   605  	}, nil)
   606  	if err != nil {
   607  		return errors.Annotate(err,
   608  			"couldn't update suspect revert creation details").Err()
   609  	}
   610  	return nil
   611  }
   612  
   613  func saveCommitDetails(ctx context.Context, culpritModel *model.Suspect) error {
   614  	// Update tsmon metrics
   615  	err := updateCulpritActionCounter(ctx, culpritModel, ActionTypeSubmitRevert)
   616  	if err != nil {
   617  		logging.Errorf(ctx, errors.Annotate(err, "updateCulpritActionCounter").Err().Error())
   618  	}
   619  
   620  	// Update revert details for commit action
   621  	err = datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   622  		e := datastore.Get(ctx, culpritModel)
   623  		if e != nil {
   624  			return e
   625  		}
   626  
   627  		culpritModel.IsRevertCommitted = true
   628  		culpritModel.RevertCommitTime = clock.Now(ctx)
   629  
   630  		return datastore.Put(ctx, culpritModel)
   631  	}, nil)
   632  	if err != nil {
   633  		return errors.Annotate(err,
   634  			"couldn't update suspect revert commit details").Err()
   635  	}
   636  	return nil
   637  }
   638  
   639  // saveInactionReason updates the inaction reason for the given Suspect.
   640  func saveInactionReason(ctx context.Context, culpritModel *model.Suspect,
   641  	reason pb.CulpritInactionReason) {
   642  	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   643  		e := datastore.Get(ctx, culpritModel)
   644  		if e != nil {
   645  			return e
   646  		}
   647  		// Set the inaction reason
   648  		culpritModel.InactionReason = reason
   649  		return datastore.Put(ctx, culpritModel)
   650  	}, nil)
   651  
   652  	if err != nil {
   653  		// not critical - just log the error
   654  		err = errors.Annotate(err,
   655  			"couldn't update suspect inaction reason").Err()
   656  		logging.Errorf(ctx, err.Error())
   657  	}
   658  }
   659  
   660  func ScheduleTestFailureTask(ctx context.Context, analysisID int64) error {
   661  	return tq.AddTask(ctx, &tq.Task{
   662  		Payload: &taskpb.TestFailureCulpritActionTask{
   663  			AnalysisId: analysisID,
   664  		},
   665  		Title: fmt.Sprintf("test_failure_culprit_action_%d", analysisID),
   666  	})
   667  }