go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/build_status.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 tasks
    16  
    17  import (
    18  	"context"
    19  	"strings"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/types/known/timestamppb"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/logging"
    27  	"go.chromium.org/luci/common/retry/transient"
    28  	"go.chromium.org/luci/common/sync/parallel"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  
    31  	"go.chromium.org/luci/buildbucket"
    32  	"go.chromium.org/luci/buildbucket/appengine/internal/buildstatus"
    33  	"go.chromium.org/luci/buildbucket/appengine/internal/metrics"
    34  	"go.chromium.org/luci/buildbucket/appengine/model"
    35  	taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs"
    36  	pb "go.chromium.org/luci/buildbucket/proto"
    37  	"go.chromium.org/luci/buildbucket/protoutil"
    38  )
    39  
    40  // sendOnBuildCompletion sends a bunch of related events when build is reaching
    41  // to an end status, e.g. finalizing the resultdb invocation, exporting to Bq,
    42  // and notify pubsub topics.
    43  func sendOnBuildCompletion(ctx context.Context, bld *model.Build) error {
    44  	bld.ClearLease()
    45  
    46  	return parallel.FanOutIn(func(tks chan<- func() error) {
    47  		tks <- func() error {
    48  			return errors.Annotate(NotifyPubSub(ctx, bld), "failed to enqueue pubsub notification task: %d", bld.ID).Err()
    49  		}
    50  		tks <- func() error {
    51  			return errors.Annotate(ExportBigQuery(ctx, bld.ID, strings.Contains(bld.ExperimentsString(), buildbucket.ExperimentBqExporterGo)), "failed to enqueue bigquery export task: %d", bld.ID).Err()
    52  		}
    53  		tks <- func() error {
    54  			return errors.Annotate(FinalizeResultDB(ctx, &taskdefs.FinalizeResultDBGo{BuildId: bld.ID}), "failed to enqueue resultDB finalization task: %d", bld.ID).Err()
    55  		}
    56  	})
    57  }
    58  
    59  // SendOnBuildStatusChange sends cloud tasks if a build's top level status changes.
    60  //
    61  // It's the default PostProcess func for buildstatus.Updater.
    62  //
    63  // Must run in a datastore transaction.
    64  func SendOnBuildStatusChange(ctx context.Context, bld *model.Build) error {
    65  	if datastore.Raw(ctx) == nil || datastore.CurrentTransaction(ctx) == nil {
    66  		return errors.Reason("must enqueue cloud tasks that are triggered by build status update in a transaction").Err()
    67  	}
    68  	switch {
    69  	case bld.Proto.Status == pb.Status_STARTED:
    70  		if err := NotifyPubSub(ctx, bld); err != nil {
    71  			logging.Debugf(ctx, "failed to notify pubsub about starting %d: %s", bld.ID, err)
    72  		}
    73  	case protoutil.IsEnded(bld.Proto.Status):
    74  		return sendOnBuildCompletion(ctx, bld)
    75  	}
    76  	return nil
    77  }
    78  
    79  // failBuild fails the given build with INFRA_FAILURE status.
    80  func failBuild(ctx context.Context, buildID int64, msg string) error {
    81  	bld := &model.Build{
    82  		ID: buildID,
    83  	}
    84  
    85  	statusUpdated := false
    86  	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
    87  		switch err := datastore.Get(ctx, bld); {
    88  		case err == datastore.ErrNoSuchEntity:
    89  			logging.Warningf(ctx, "build %d not found: %s", buildID, err)
    90  			return nil
    91  		case err != nil:
    92  			return errors.Annotate(err, "failed to fetch build: %d", bld.ID).Err()
    93  		}
    94  
    95  		if protoutil.IsEnded(bld.Proto.Status) {
    96  			// Build already ended, no more change to it.
    97  			return nil
    98  		}
    99  
   100  		statusUpdated = true
   101  		bld.Proto.SummaryMarkdown = msg
   102  		st := &buildstatus.StatusWithDetails{Status: pb.Status_INFRA_FAILURE}
   103  		bs, steps, err := updateBuildStatusOnTaskStatusChange(ctx, bld, st, st, clock.Now(ctx))
   104  		if err != nil {
   105  			return err
   106  		}
   107  
   108  		toSave := []any{bld}
   109  		if bs != nil {
   110  			toSave = append(toSave, bs)
   111  		}
   112  		if steps != nil {
   113  			toSave = append(toSave, steps)
   114  		}
   115  		return datastore.Put(ctx, toSave)
   116  	}, nil)
   117  	if err != nil {
   118  		return transient.Tag.Apply(errors.Annotate(err, "failed to terminate build: %d", buildID).Err())
   119  	}
   120  	if statusUpdated {
   121  		metrics.BuildCompleted(ctx, bld)
   122  	}
   123  	return nil
   124  }
   125  
   126  // updateBuildStatusOnTaskStatusChange updates build's top level status based on
   127  // task status change.
   128  func updateBuildStatusOnTaskStatusChange(ctx context.Context, bld *model.Build, buildStatus, taskStatus *buildstatus.StatusWithDetails, updateTime time.Time) (*model.BuildStatus, *model.BuildSteps, error) {
   129  	var steps *model.BuildSteps
   130  	statusUpdater := buildstatus.Updater{
   131  		Build:       bld,
   132  		BuildStatus: buildStatus,
   133  		TaskStatus:  taskStatus,
   134  		UpdateTime:  updateTime,
   135  		PostProcess: func(c context.Context, bld *model.Build) error {
   136  			// Besides the post process cloud tasks, we also need to update
   137  			// steps, in case the build task ends before the build does.
   138  			if protoutil.IsEnded(bld.Proto.Status) {
   139  				steps = &model.BuildSteps{Build: datastore.KeyForObj(ctx, bld)}
   140  				// If the build has no steps, CancelIncomplete will return false.
   141  				if err := model.GetIgnoreMissing(ctx, steps); err != nil {
   142  					return errors.Annotate(err, "failed to fetch steps for build %d", bld.ID).Err()
   143  				}
   144  				switch _, err := steps.CancelIncomplete(ctx, timestamppb.New(updateTime.UTC())); {
   145  				case err != nil:
   146  					// The steps are fetched from datastore and should always be valid in
   147  					// CancelIncomplete. But in case of any errors, we can just log it here
   148  					// instead of rethrowing it to make the entire flow fail or retry.
   149  					logging.Errorf(ctx, "failed to mark steps cancelled for build %d: %s", bld.ID, err)
   150  				}
   151  			}
   152  			return SendOnBuildStatusChange(ctx, bld)
   153  		},
   154  	}
   155  	bs, err := statusUpdater.Do(ctx)
   156  	if err != nil {
   157  		return nil, nil, err
   158  	}
   159  	return bs, steps, err
   160  }