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 }