go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/baselineupdater/baselineupdater.go (about) 1 // Copyright 2023 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 baselineupdater marks test variants from an invocation as submitted by 16 // adding the test variants to the set of tests for its baseline identifier. 17 package baselineupdater 18 19 import ( 20 "context" 21 22 "cloud.google.com/go/spanner" 23 24 "google.golang.org/grpc/status" 25 "google.golang.org/protobuf/proto" 26 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/common/logging" 29 "go.chromium.org/luci/common/proto/mask" 30 "go.chromium.org/luci/common/retry/transient" 31 32 "go.chromium.org/luci/resultdb/internal/baselines" 33 btv "go.chromium.org/luci/resultdb/internal/baselines/testvariants" 34 "go.chromium.org/luci/resultdb/internal/invocations" 35 "go.chromium.org/luci/resultdb/internal/invocations/graph" 36 "go.chromium.org/luci/resultdb/internal/tasks/taskspb" 37 38 tr "go.chromium.org/luci/resultdb/internal/testresults" 39 pb "go.chromium.org/luci/resultdb/proto/v1" 40 41 "go.chromium.org/luci/server" 42 "go.chromium.org/luci/server/auth/realms" 43 "go.chromium.org/luci/server/span" 44 "go.chromium.org/luci/server/tq" 45 ) 46 47 // BaselineUpdaterTasks describes how to route mark submitted tasks. 48 var BaselineUpdaterTasks = tq.RegisterTaskClass(tq.TaskClass{ 49 ID: "update-baseline", 50 Prototype: &taskspb.MarkInvocationSubmitted{}, 51 Kind: tq.Transactional, 52 Queue: "baselineupdater", // use a dedicated queue 53 RoutingPrefix: "/internal/tasks/baselineupdater", // for routing to "baselineupdater" service 54 }) 55 56 // TransactionLimit is set to 8000 because Cloud Spanner limits 40k mutations per transaction. 57 // We have 5 columns to write to per row, and 40,000/5 = 8000. 58 var TransactionLimit = 8000 59 60 func Schedule(ctx context.Context, invID string) { 61 tq.MustAddTask(ctx, &tq.Task{ 62 Payload: &taskspb.MarkInvocationSubmitted{InvocationId: invID}, 63 Title: invID, 64 }) 65 } 66 67 // InitServer initializes a baselineupdator server. 68 func InitServer(srv *server.Server) { 69 // init() below takes care of everything. 70 } 71 72 func init() { 73 BaselineUpdaterTasks.AttachHandler(func(ctx context.Context, msg proto.Message) error { 74 task := msg.(*taskspb.MarkInvocationSubmitted) 75 err := tryMarkInvocationSubmitted(ctx, invocations.ID(task.InvocationId)) 76 if _, ok := status.FromError(errors.Unwrap(err)); ok { 77 // Spanner gRPC error. 78 return transient.Tag.Apply(err) 79 } 80 return err 81 }) 82 } 83 84 // Marking an invocation is asynchronous. Invocations must be finalized prior 85 // to it being marked submitted. Non-finalized invocations are marked as submitted 86 // and will be scheduled by the finalizer. 87 // 88 // When an invocation is marked submitted, all test variants from that invocation 89 // are added to the set of test variants for the invocation's baseline. In other 90 // words, the set of tests expected to run for a baseline are updated with the 91 // test variants from the provided invocation. Adding all test variants 92 // from an invocation to its baseline are done recursively. 93 // 94 // For example, if invocation A for baseline "try:linux-rel" is finalized with 95 // test A and B and "try:linux-rel" has A and C in its set of test variants, 96 // this call would update the final set of test variants to (A, B and C). 97 // 98 // Marking an invocation submitted also updates the Baselines table with new 99 // baselines. 100 func tryMarkInvocationSubmitted(ctx context.Context, invID invocations.ID) error { 101 inv, err := invocations.Read(span.Single(ctx), invID) 102 if err != nil { 103 return errors.Annotate(err, "read invocation").Err() 104 } 105 106 if inv.BaselineId == "" { 107 // It's valid for a baseline to not be specified, so this workflow 108 // will terminate early. 109 return nil 110 } 111 112 if err := shouldMarkSubmitted(inv); err != nil { 113 return errors.Annotate(err, "mark invocation submitted").Err() 114 } 115 116 if err = ensureBaselineExists(ctx, inv); err != nil { 117 return errors.Annotate(err, "mark invocation submitted").Err() 118 } 119 120 return markInvocationSubmitted(ctx, inv) 121 } 122 123 // shouldMarkSubmitted returns an error if the invocation is ready to be marked 124 // as submitted. 125 func shouldMarkSubmitted(inv *pb.Invocation) error { 126 // all sub invocations should be finalized if the parent invocation is finalized. 127 if inv.State != pb.Invocation_FINALIZED { 128 return errors.Reason("the invocation is not yet finalized").Err() 129 } 130 131 return nil 132 } 133 134 // markBaselineNew adds the baseline to the Baselines table if it's not present 135 // in the BaselineTestVariants table. Baselines present will have the LastUpdated 136 // time reset to the commit timestamp. 137 func ensureBaselineExists(ctx context.Context, inv *pb.Invocation) error { 138 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 139 project, _ := realms.Split(inv.Realm) 140 baselineID := inv.BaselineId 141 142 _, err := baselines.Read(ctx, project, baselineID) 143 if err != nil { 144 if err == baselines.NotFound { 145 // if it isn't found, we can create the baseline in the table and terminate. 146 span.BufferWrite(ctx, baselines.Create(project, baselineID)) 147 return nil 148 } else { 149 return errors.Annotate(err, "read baseline").Err() 150 } 151 } 152 153 // If the baseline exists, we'll update LastUpdatedTime so that it is not 154 // automatically ejected from the table. 155 span.BufferWrite(ctx, baselines.UpdateLastUpdatedTime(project, baselineID)) 156 return nil 157 }) 158 if err != nil { 159 return errors.Annotate(err, "ensure baseline").Err() 160 } 161 162 return nil 163 } 164 165 func markInvocationSubmitted(ctx context.Context, inv *pb.Invocation) error { 166 invID := invocations.MustParseName(inv.Name) 167 baselineID := inv.BaselineId 168 project, _ := realms.Split(inv.Realm) 169 170 masks := mask.MustFromReadMask(&pb.TestResult{}, 171 "test_id", 172 "variant_hash", 173 "status", 174 ) 175 176 rCtx, cancel := span.ReadOnlyTransaction(ctx) 177 defer cancel() 178 179 idSet := make([]invocations.ID, 0) 180 idSet = append(idSet, invocations.ID(invID)) 181 invs, err := graph.Reachable(rCtx, invocations.NewIDSet(invID)) 182 if err != nil { 183 return errors.Annotate(err, "discover reachable invocations").Err() 184 } 185 for invID, reachableInv := range invs.Invocations { 186 if !reachableInv.HasTestResults { 187 continue 188 } 189 idSet = append(idSet, invID) 190 } 191 q := &tr.Query{ 192 Predicate: &pb.TestResultPredicate{}, 193 PageSize: 0, 194 InvocationIDs: invocations.NewIDSet(idSet...), 195 Mask: masks, 196 } 197 198 // This will sequentially load results and process them when we reach mutation 199 // limits. This is not the quickest way to process the results and has room 200 // for optimization. 201 ms := make([]*spanner.Mutation, 0) 202 err = q.Run(rCtx, func(tr *pb.TestResult) error { 203 if tr.Status == pb.TestStatus_SKIP { 204 // We'll ignore SKIPPED from being BaselineTestVariants. This allows 205 // it to be verified for flakiness when it no longer becomes skipped. 206 logging.Debugf(ctx, "Skipped adding %s for baselineID %s", tr.TestId, baselineID) 207 return nil 208 } 209 ms = append(ms, btv.InsertOrUpdate(project, baselineID, tr.TestId, tr.VariantHash)) 210 211 if len(ms) >= TransactionLimit { 212 _, err := span.ReadWriteTransaction(ctx, func(rwCtx context.Context) error { 213 span.BufferWrite(rwCtx, ms...) 214 return nil 215 }) 216 if err != nil { 217 return errors.Annotate(err, "write baseline test variants").Err() 218 } 219 ms = make([]*spanner.Mutation, 0) 220 } 221 return nil 222 }) 223 if err != nil { 224 return errors.Annotate(err, "query test variants").Err() 225 } 226 227 // Insert remaining test variants as a final write transaction. 228 if len(ms) > 0 { 229 _, err := span.ReadWriteTransaction(ctx, func(rwCtx context.Context) error { 230 span.BufferWrite(rwCtx, ms...) 231 return nil 232 }) 233 if err != nil { 234 return errors.Annotate(err, "write baseline test variants").Err() 235 } 236 } 237 238 return nil 239 }