go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/model/op_end.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 model 16 17 import ( 18 "context" 19 "time" 20 21 statuspb "google.golang.org/genproto/googleapis/rpc/status" 22 "google.golang.org/grpc/codes" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/logging" 27 "go.chromium.org/luci/gae/service/datastore" 28 29 "go.chromium.org/luci/deploy/api/modelpb" 30 "go.chromium.org/luci/deploy/api/rpcpb" 31 ) 32 33 // ActuationEndOp collects changes to transactionally apply to the datastore 34 // to end an existing actuation. 35 type ActuationEndOp struct { 36 actuation *Actuation 37 assets map[string]*Asset 38 history *historyRecorder 39 now time.Time 40 } 41 42 // NewActuationEndOp starts a datastore operation to finish an actuation. 43 // 44 // Takes ownership of `actuation` mutating it. 45 func NewActuationEndOp(ctx context.Context, actuation *Actuation) (*ActuationEndOp, error) { 46 // Fetch all assets associated with this actuation in BeginActuation. We'll 47 // need to update their `Actuation` field with the final status of the 48 // actuation. 49 assets, err := fetchAssets(ctx, actuation.AssetIDs(), true) 50 if err != nil { 51 return nil, err 52 } 53 54 // Filter out assets that are already handled by another actuation. Can happen 55 // due to races, crashes, expirations, etc. 56 active := make(map[string]*Asset, len(assets)) 57 for assetID, ent := range assets { 58 if ent.Asset.LastActuation.GetId() == actuation.ID { 59 active[assetID] = ent 60 } else { 61 logging.Warningf(ctx, "Skipping asset %q: it was already handled by another actuation %q", assetID, ent.Asset.LastActuation.GetId()) 62 } 63 } 64 65 return &ActuationEndOp{ 66 actuation: actuation, 67 assets: active, 68 history: &historyRecorder{actuation: actuation.Actuation}, 69 now: clock.Now(ctx), 70 }, nil 71 } 72 73 // UpdateActuationStatus is called to report the status of the actuation. 74 func (op *ActuationEndOp) UpdateActuationStatus(ctx context.Context, status *statuspb.Status, logURL string) { 75 op.actuation.Actuation.Finished = timestamppb.New(op.now) 76 if status.GetCode() == int32(codes.OK) { 77 op.actuation.Actuation.State = modelpb.Actuation_SUCCEEDED 78 } else { 79 op.actuation.Actuation.State = modelpb.Actuation_FAILED 80 op.actuation.Actuation.Status = status 81 } 82 if logURL != "" { 83 op.actuation.Actuation.LogUrl = logURL 84 } 85 } 86 87 // HandleActuatedState is called to report the post-actuation asset state. 88 // 89 // Ignores assets not associated with this actuation anymore. Takes ownership 90 // of `asset` mutating it. 91 func (op *ActuationEndOp) HandleActuatedState(ctx context.Context, assetID string, asset *rpcpb.ActuatedAsset) { 92 // If the asset is not in `op.assets`, then it is already handled by another 93 // actuation and we should not modify it. Such assets were already logged in 94 // NewActuationEndOp. 95 ent, ok := op.assets[assetID] 96 if !ok { 97 return 98 } 99 100 reported := asset.State 101 if reported.Timestamp == nil { 102 reported.Timestamp = timestamppb.New(op.now) 103 } 104 reported.Deployment = op.actuation.Actuation.Deployment 105 reported.Actuator = op.actuation.Actuation.Actuator 106 107 ent.Asset.PostActuationStatus = reported.Status 108 if reported.Status.GetCode() == int32(codes.OK) { 109 ent.Asset.ReportedState = reported 110 ent.Asset.ActuatedState = reported 111 ent.ConsecutiveFailures = 0 112 } else { 113 ent.ConsecutiveFailures += 1 114 } 115 116 // If was recording a history entry, close and commit it. 117 if ent.IsRecordingHistoryEntry() { 118 ent.HistoryEntry.Actuation = op.actuation.Actuation 119 ent.HistoryEntry.PostActuationState = reported 120 op.history.recordAndNotify(ent.finalizeHistoryEntry()) 121 } 122 } 123 124 // Expire marks the actuation as expired. 125 func (op *ActuationEndOp) Expire(ctx context.Context) { 126 op.actuation.Actuation.Finished = timestamppb.New(op.now) 127 op.actuation.Actuation.State = modelpb.Actuation_EXPIRED 128 op.actuation.Actuation.Status = &statuspb.Status{ 129 Code: int32(codes.DeadlineExceeded), 130 Message: "the actuation didn't finish before its expiration timeout", 131 } 132 133 // Append historic records to all assets that were being actuated. 134 for _, asset := range op.assets { 135 asset.ConsecutiveFailures += 1 136 if asset.IsRecordingHistoryEntry() { 137 asset.HistoryEntry.Actuation = op.actuation.Actuation 138 op.history.recordAndNotify(asset.finalizeHistoryEntry()) 139 } 140 } 141 } 142 143 // Apply stores all updated or created datastore entities. 144 func (op *ActuationEndOp) Apply(ctx context.Context) error { 145 var toPut []any 146 147 // Embed the up-to-date Actuation snapshot into Asset entities. 148 for _, ent := range op.assets { 149 ent.Asset.LastActuation = op.actuation.Actuation 150 if ent.Asset.LastActuateActuation.GetId() == op.actuation.ID { 151 ent.Asset.LastActuateActuation = op.actuation.Actuation 152 } 153 if op.actuation.Actuation.State == modelpb.Actuation_SUCCEEDED { 154 ent.Asset.AppliedState = ent.Asset.IntendedState 155 } 156 toPut = append(toPut, ent) 157 } 158 159 // Update the Actuation entity to match the Actuation proto. 160 op.actuation.State = op.actuation.Actuation.State 161 toPut = append(toPut, op.actuation) 162 163 // Prepare AssetHistory entities. Note they refer to op.actuation by pointer 164 // inside already and will pick up all changes made to the Actuation proto. 165 history, err := op.history.commit(ctx) 166 if err != nil { 167 return err 168 } 169 toPut = append(toPut, history...) 170 171 return datastore.Put(ctx, toPut...) 172 }