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  }