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 }