go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/model/utils.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  	"strings"
    20  	"time"
    21  
    22  	"google.golang.org/protobuf/types/known/timestamppb"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/logging"
    26  	"go.chromium.org/luci/common/retry/transient"
    27  	"go.chromium.org/luci/gae/service/datastore"
    28  
    29  	"go.chromium.org/luci/deploy/api/modelpb"
    30  )
    31  
    32  // Txn runs the callback in a datastore transaction.
    33  //
    34  // Transient-tagged errors trigger a transaction retry. If all retries are
    35  // exhausted, returns a transient-tagged error itself.
    36  func Txn(ctx context.Context, cb func(context.Context) error) error {
    37  	var attempt int
    38  	var innerErr error
    39  
    40  	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
    41  		attempt++
    42  		if attempt != 1 {
    43  			if innerErr != nil {
    44  				logging.Warningf(ctx, "Retrying the transaction after the error: %s", innerErr)
    45  			} else {
    46  				logging.Warningf(ctx, "Retrying the transaction: failed to commit")
    47  			}
    48  		}
    49  		innerErr = cb(ctx)
    50  		if transient.Tag.In(innerErr) {
    51  			return datastore.ErrConcurrentTransaction // causes a retry
    52  		}
    53  		return innerErr
    54  	}, nil)
    55  
    56  	if err != nil {
    57  		// If the transaction callback failed, prefer its error.
    58  		if innerErr != nil {
    59  			return innerErr
    60  		}
    61  		// Here it can only be a commit error (i.e. produced by RunInTransaction
    62  		// itself, not by its callback). We treat them as transient.
    63  		return transient.Tag.Apply(err)
    64  	}
    65  
    66  	return nil
    67  }
    68  
    69  // EqualStrSlice compares two string slices.
    70  func EqualStrSlice(a, b []string) bool {
    71  	if len(a) != len(b) {
    72  		return false
    73  	}
    74  	for i, s := range a {
    75  		if b[i] != s {
    76  			return false
    77  		}
    78  	}
    79  	return true
    80  }
    81  
    82  // asTime converts a timestamp proto to optional time.Time for Datastore.
    83  func asTime(ts *timestamppb.Timestamp) time.Time {
    84  	if ts == nil {
    85  		return time.Time{}
    86  	}
    87  	return ts.AsTime().UTC()
    88  }
    89  
    90  // fetchAssets fetches a bunch of asset entities given their IDs.
    91  //
    92  // If shouldExist is true, fails if some of them do not exist. Otherwise
    93  // constructs new empty Asset structs in place of missing ones.
    94  func fetchAssets(ctx context.Context, assets []string, shouldExist bool) (map[string]*Asset, error) {
    95  	ents := make([]*Asset, len(assets))
    96  	for idx, id := range assets {
    97  		ents[idx] = &Asset{ID: id}
    98  	}
    99  
   100  	if err := datastore.Get(ctx, ents); err != nil {
   101  		merr, ok := err.(errors.MultiError)
   102  		if !ok {
   103  			return nil, transient.Tag.Apply(err)
   104  		}
   105  
   106  		var missing []string
   107  		for idx, err := range merr {
   108  			switch {
   109  			case err == datastore.ErrNoSuchEntity:
   110  				if shouldExist {
   111  					missing = append(missing, assets[idx])
   112  				} else {
   113  					ents[idx] = &Asset{
   114  						ID:    assets[idx],
   115  						Asset: &modelpb.Asset{Id: assets[idx]},
   116  					}
   117  				}
   118  			case err != nil:
   119  				return nil, transient.Tag.Apply(err)
   120  			}
   121  		}
   122  
   123  		if len(missing) != 0 {
   124  			return nil, errors.Reason("assets entities unexpectedly missing: %s", strings.Join(missing, ", ")).Err()
   125  		}
   126  	}
   127  
   128  	assetMap := make(map[string]*Asset, len(assets))
   129  	for _, ent := range ents {
   130  		assetMap[ent.ID] = ent
   131  	}
   132  
   133  	return assetMap, nil
   134  }
   135  
   136  // IsActuateDecision returns true for ACTUATE_* decisions.
   137  func IsActuateDecision(d modelpb.ActuationDecision_Decision) bool {
   138  	return d == modelpb.ActuationDecision_ACTUATE_STALE ||
   139  		d == modelpb.ActuationDecision_ACTUATE_FORCE
   140  }