go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/buildstatus/buildstatus.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 buildstatus provides the build status computation related functions. 16 package buildstatus 17 18 import ( 19 "context" 20 "time" 21 22 "go.chromium.org/luci/common/errors" 23 "go.chromium.org/luci/gae/service/datastore" 24 25 "go.chromium.org/luci/buildbucket/appengine/common" 26 "go.chromium.org/luci/buildbucket/appengine/model" 27 pb "go.chromium.org/luci/buildbucket/proto" 28 "go.chromium.org/luci/buildbucket/protoutil" 29 ) 30 31 type StatusWithDetails struct { 32 Status pb.Status 33 Details *pb.StatusDetails 34 } 35 36 func (sd *StatusWithDetails) isSet() bool { 37 return sd != nil && sd.Status != pb.Status_STATUS_UNSPECIFIED 38 } 39 40 type Updater struct { 41 Build *model.Build 42 43 BuildStatus *StatusWithDetails 44 OutputStatus *StatusWithDetails 45 TaskStatus *StatusWithDetails 46 47 UpdateTime time.Time 48 49 PostProcess func(c context.Context, bld *model.Build) error 50 } 51 52 // buildEndStatus calculates the final status of a build based on its output 53 // status and backend task status. 54 func buildEndStatus(outStatus, taskStatus *StatusWithDetails) *StatusWithDetails { 55 switch { 56 case !outStatus.isSet() || !protoutil.IsEnded(outStatus.Status): 57 if taskStatus.Status == pb.Status_SUCCESS { 58 // outStatus should have been an ended status since taskStatus is. 59 // Something must be wrong. 60 return &StatusWithDetails{Status: pb.Status_INFRA_FAILURE} 61 } else { 62 // This could happen if the task crashes when running the build, use the 63 // task status. 64 return taskStatus 65 } 66 case outStatus.Status == pb.Status_SUCCESS: 67 // Either taskStatus.Status is also SUCCESS or a failure, can use taskStatus 68 // as the final one. 69 return taskStatus 70 default: 71 // outStatus already contains failure, use outStatus as the final one. 72 return outStatus 73 } 74 } 75 func (u *Updater) calculateBuildStatus() *StatusWithDetails { 76 switch { 77 case u.BuildStatus != nil && u.BuildStatus.Status != pb.Status_STATUS_UNSPECIFIED: 78 // If top level status is provided, use that directly. 79 // TODO(crbug.com/1450399): remove this case after the callsites are updated 80 // to not set top level status directly. 81 return u.BuildStatus 82 case !u.OutputStatus.isSet() && !u.TaskStatus.isSet(): 83 return &StatusWithDetails{Status: pb.Status_STATUS_UNSPECIFIED} 84 case u.OutputStatus.isSet() && u.OutputStatus.Status == pb.Status_STARTED: 85 return &StatusWithDetails{Status: pb.Status_STARTED} 86 case u.TaskStatus.isSet() && protoutil.IsEnded(u.TaskStatus.Status): 87 return buildEndStatus(&StatusWithDetails{ 88 Status: u.Build.Proto.Output.GetStatus(), 89 Details: u.Build.Proto.Output.GetStatusDetails()}, 90 u.TaskStatus) 91 default: 92 // no change. 93 return &StatusWithDetails{Status: u.Build.Proto.Status, Details: u.Build.Proto.StatusDetails} 94 } 95 } 96 97 // Do updates the top level build status, and performs actions 98 // when build status changes: 99 // * it updates the corresponding BuildStatus entity for the build; 100 // * it triggers PubSub notify task; 101 // * if the build is ended, it triggers BQ export task. 102 // 103 // Note that pubsub notification on build start will not retry on failure. 104 // 105 // Must be run inside a transaction. 106 // 107 // The post-processes that should happen after the status update is committed 108 // is not included, and the callsite needs to handle them separately. These 109 // include: 110 // * update build event metrics 111 // * cancel descendent builds when this build is ended. 112 func (u *Updater) Do(ctx context.Context) (*model.BuildStatus, error) { 113 if datastore.Raw(ctx) == nil || datastore.CurrentTransaction(ctx) == nil { 114 return nil, errors.Reason("must update build status in a transaction").Err() 115 } 116 117 if protoutil.IsEnded(u.Build.Proto.Status) { 118 return nil, errors.Reason("cannot update status for an ended build").Err() 119 } 120 121 // Check the provided statuses. 122 if u.OutputStatus.isSet() && u.TaskStatus.isSet() { 123 return nil, errors.Reason("impossible: update build output status and task status at the same time").Err() 124 } 125 126 newBuildStatus := u.BuildStatus 127 if !newBuildStatus.isSet() { 128 newBuildStatus = u.calculateBuildStatus() 129 } 130 if !newBuildStatus.isSet() { 131 // Nothing provided to update. 132 return nil, errors.Reason("cannot set a build status to UNSPECIFIED").Err() 133 } 134 135 if newBuildStatus.Status == u.Build.Proto.Status { 136 // Nothing to update. 137 return nil, nil 138 } 139 140 protoutil.SetStatus(u.UpdateTime, u.Build.Proto, newBuildStatus.Status) 141 u.Build.Proto.StatusDetails = newBuildStatus.Details 142 143 // Update BuildStatus. 144 entities, err := common.GetBuildEntities(ctx, u.Build.ID, model.BuildStatusKind) 145 if err != nil { 146 return nil, err 147 } 148 bs := entities[0].(*model.BuildStatus) 149 bs.Status = newBuildStatus.Status 150 151 // post process after build status change. 152 if err := u.PostProcess(ctx, u.Build); err != nil { 153 return nil, err 154 } 155 156 return bs, nil 157 }