go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/execute/reuse_internal.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 execute
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  
    21  	"google.golang.org/protobuf/proto"
    22  
    23  	"go.chromium.org/luci/common/clock"
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/retry/transient"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  
    28  	"go.chromium.org/luci/cv/internal/common"
    29  	"go.chromium.org/luci/cv/internal/tryjob"
    30  )
    31  
    32  // findReuseInCV returns reusable Tryjob candidates from CV datastore.
    33  func (w *worker) findReuseInCV(ctx context.Context, definitions []*tryjob.Definition) (map[*tryjob.Definition]*tryjob.Tryjob, error) {
    34  	candidates, err := w.queryForCandidates(ctx, definitions)
    35  	switch {
    36  	case err != nil:
    37  		return nil, err
    38  	case len(candidates) == 0:
    39  		return nil, nil
    40  	}
    41  
    42  	defs := make([]*tryjob.Definition, 0, len(candidates))
    43  	tryjobIDs := make(common.TryjobIDs, 0, len(candidates))
    44  	for def, tj := range candidates {
    45  		defs = append(defs, def)
    46  		tryjobIDs = append(tryjobIDs, tj.ID)
    47  	}
    48  	tjs, err := w.addCurrentRunToReuse(ctx, tryjobIDs)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	ret := make(map[*tryjob.Definition]*tryjob.Tryjob, len(defs))
    53  	for i, def := range defs {
    54  		ret[def] = tjs[i]
    55  	}
    56  	return ret, nil
    57  }
    58  
    59  // queryForCandidates makes a DS query to find Tryjob candidates for reuse that
    60  // have a matching reuse key.
    61  func (w *worker) queryForCandidates(ctx context.Context, definitions []*tryjob.Definition) (map[*tryjob.Definition]*tryjob.Tryjob, error) {
    62  	q := datastore.NewQuery(tryjob.TryjobKind).Eq("ReuseKey", w.reuseKey)
    63  	luciProject := w.run.ID.LUCIProject()
    64  	mode := w.run.Mode
    65  	candidates := make(map[*tryjob.Definition]*tryjob.Tryjob)
    66  	err := datastore.Run(ctx, q, func(tj *tryjob.Tryjob) error {
    67  		switch def := matchDefinitions(tj, definitions); {
    68  		case def == nil:
    69  		case w.knownTryjobIDs.Has(tj.ID):
    70  		case tj.LUCIProject() != luciProject:
    71  			// Ensures Run only reuse the Tryjob that its belonging LUCI
    72  			// Project has access to. This check may give a false negative
    73  			// result but it's good enough, because currently it's very
    74  			// unlikely for a Run from Project A to reuse a Tryjob triggered by
    75  			// a Run from Project B. Project A and Project B should watch a
    76  			// disjoint set of Gerrit refs.
    77  		case canReuseTryjob(ctx, tj, mode) == reuseDenied:
    78  		case tj.EntityCreateTime.IsZero():
    79  			panic(fmt.Errorf("tryjob %d has zero entity create time", tj.ID))
    80  		default:
    81  			if existing, ok := candidates[def]; !ok || tj.EntityCreateTime.After(existing.EntityCreateTime) {
    82  				// Pick the latest one.
    83  				candidates[def] = tj
    84  			}
    85  		}
    86  		return nil
    87  	})
    88  	if err != nil {
    89  		return nil, errors.Annotate(err, "failed to query for reusable tryjobs").Tag(transient.Tag).Err()
    90  	}
    91  	return candidates, nil
    92  }
    93  
    94  // matchDefinitions returns a Definition that matches the given Tryjob from the
    95  // given list of Definitions.
    96  func matchDefinitions(tj *tryjob.Tryjob, definitions []*tryjob.Definition) *tryjob.Definition {
    97  	for _, def := range definitions {
    98  		switch {
    99  		case proto.Equal(tj.Definition, def):
   100  			return def
   101  		case def.GetBuildbucket() != nil:
   102  			switch builder := tj.Result.GetBuildbucket().GetBuilder(); {
   103  			case builder == nil:
   104  			case proto.Equal(builder, def.GetBuildbucket().GetBuilder()):
   105  				return def
   106  			case proto.Equal(builder, def.GetEquivalentTo().GetBuildbucket().GetBuilder()):
   107  				return def
   108  			}
   109  		default:
   110  			panic(fmt.Errorf("unknown backend: %T", def.GetBackend()))
   111  		}
   112  	}
   113  	return nil
   114  }
   115  
   116  func (w *worker) addCurrentRunToReuse(ctx context.Context, tjIDs common.TryjobIDs) ([]*tryjob.Tryjob, error) {
   117  	var tryjobs []*tryjob.Tryjob
   118  	var innerErr error
   119  	err := datastore.RunInTransaction(ctx, func(ctx context.Context) (err error) {
   120  		defer func() { innerErr = err }()
   121  		tryjobs = make([]*tryjob.Tryjob, len(tjIDs))
   122  		for i, id := range tjIDs {
   123  			tryjobs[i] = &tryjob.Tryjob{ID: id}
   124  		}
   125  		if err := datastore.Get(ctx, tryjobs); err != nil {
   126  			return errors.Annotate(err, "failed to load Tryjob entities").Tag(transient.Tag).Err()
   127  		}
   128  		var toSave []*tryjob.Tryjob
   129  		for _, tj := range tryjobs {
   130  			// Be defensive. Tryjob may already include this Run if previous request
   131  			// failed in the middle.
   132  			if tj.ReusedBy.Index(w.run.ID) < 0 {
   133  				tj.ReusedBy = append(tj.ReusedBy, w.run.ID)
   134  				tj.EVersion++
   135  				tj.EntityUpdateTime = clock.Now(ctx).UTC()
   136  				toSave = append(toSave, tj)
   137  			}
   138  		}
   139  		return tryjob.SaveTryjobs(ctx, toSave, w.rm.NotifyTryjobsUpdated)
   140  	}, nil)
   141  	switch {
   142  	case innerErr != nil:
   143  		return nil, innerErr
   144  	case err != nil:
   145  		return nil, errors.Annotate(err, "failed to commit transaction").Tag(transient.Tag).Err()
   146  	}
   147  	return tryjobs, nil
   148  }