go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/testmetadataupdator/updator.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 testmetadataupdator implements a task which ingest test metadata from invocations. 16 package testmetadataupdator 17 18 import ( 19 "context" 20 "math" 21 "strings" 22 "time" 23 24 "cloud.google.com/go/spanner" 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/proto/mask" 27 "go.chromium.org/luci/resultdb/internal/invocations" 28 "go.chromium.org/luci/resultdb/internal/spanutil" 29 "go.chromium.org/luci/resultdb/internal/testmetadata" 30 "go.chromium.org/luci/resultdb/internal/testresults" 31 "go.chromium.org/luci/resultdb/pbutil" 32 pb "go.chromium.org/luci/resultdb/proto/v1" 33 "go.chromium.org/luci/server/auth/realms" 34 "go.chromium.org/luci/server/span" 35 "golang.org/x/sync/errgroup" 36 protoreflect "google.golang.org/protobuf/reflect/protoreflect" 37 ) 38 39 // Expiration time of a test metadata. 40 // After expiry, update to less metadata information is allowed. 41 var metadataExpiry = 48 * time.Hour 42 43 // Maximum number of incoming test metadata we hold in memory. 44 var batchSize = 1000 45 46 type testID string 47 48 // testMetadataUpdator updates TestMetadata rows with test results in a set of invocations. 49 type testMetadataUpdator struct { 50 sources *pb.Sources 51 invIDs invocations.IDSet 52 start time.Time // Used to determine if a test metadata is expired. 53 } 54 55 func newUpdator(sources *pb.Sources, invIDs invocations.IDSet, start time.Time) *testMetadataUpdator { 56 return &testMetadataUpdator{sources: sources, invIDs: invIDs, start: start} 57 } 58 59 func (u *testMetadataUpdator) run(ctx context.Context) error { 60 for invID := range u.invIDs { 61 // Query test results and use them to update test metadata. 62 batchC := make(chan map[testID]*pb.TestMetadata) 63 // Batch update test metadata. 64 eg, ctx := errgroup.WithContext(ctx) 65 66 eg.Go(func() error { 67 defer close(batchC) 68 if err := u.queryTestResultMetadata(ctx, invID, batchC); err != nil { 69 return errors.Annotate(err, "query test result metadata, invocation %s", invID).Err() 70 } 71 return nil 72 }) 73 74 eg.Go(func() error { 75 var realm string 76 if err := invocations.ReadColumns(span.Single(ctx), invID, map[string]any{"Realm": &realm}); err != nil { 77 return errors.Annotate(err, "read realm, invocation %s", invID).Err() 78 } 79 if err := u.batchCreateOrUpdateTestMetadata(ctx, realm, batchC); err != nil { 80 return errors.Annotate(err, "create or update test metadata, invocation %s", invID).Err() 81 } 82 return nil 83 }) 84 if err := eg.Wait(); err != nil { 85 return err 86 } 87 } 88 return nil 89 } 90 91 // queryTestResultMetadata visits all test results in the given invocations. 92 // and returns one test metadata with the most metadata information for each test. 93 func (u *testMetadataUpdator) queryTestResultMetadata(ctx context.Context, invID invocations.ID, batchC chan<- map[testID]*pb.TestMetadata) error { 94 ctx, cancel := span.ReadOnlyTransaction(ctx) 95 defer cancel() 96 97 q := testresults.Query{ 98 InvocationIDs: invocations.NewIDSet(invID), 99 Mask: mask.MustFromReadMask(&pb.TestResult{}, 100 "test_id", 101 "test_metadata", 102 ), 103 } 104 deduplicatedMetadata := make(map[testID]*pb.TestMetadata, batchSize) 105 err := q.Run(ctx, func(tr *pb.TestResult) error { 106 existing, ok := deduplicatedMetadata[testID(tr.TestId)] 107 if !ok { 108 if len(deduplicatedMetadata) >= batchSize { 109 select { 110 case <-ctx.Done(): 111 return ctx.Err() 112 case batchC <- deduplicatedMetadata: 113 } 114 deduplicatedMetadata = make(map[testID]*pb.TestMetadata, batchSize) 115 } 116 deduplicatedMetadata[testID(tr.TestId)] = tr.TestMetadata 117 return nil 118 } 119 if fieldExistenceBitField(existing) < fieldExistenceBitField(tr.TestMetadata) { 120 deduplicatedMetadata[testID(tr.TestId)] = tr.TestMetadata 121 } 122 return nil 123 }) 124 if err != nil { 125 return err 126 } 127 if len(deduplicatedMetadata) > 0 { 128 select { 129 case <-ctx.Done(): 130 return ctx.Err() 131 case batchC <- deduplicatedMetadata: 132 } 133 } 134 return nil 135 } 136 137 func (u *testMetadataUpdator) batchCreateOrUpdateTestMetadata(ctx context.Context, realm string, batchC <-chan map[testID]*pb.TestMetadata) error { 138 for testResults := range batchC { 139 if err := u.updateOrCreateRows(ctx, realm, testResults); err != nil { 140 return err 141 } 142 } 143 return nil 144 } 145 146 func (u *testMetadataUpdator) updateOrCreateRows(ctx context.Context, realm string, testMetadata map[testID]*pb.TestMetadata) error { 147 project, subRealm := realms.Split(realm) 148 testIDs := make([]string, 0, len(testMetadata)) 149 for k := range testMetadata { 150 testIDs = append(testIDs, string(k)) 151 } 152 f := func(ctx context.Context) error { 153 seen := make(map[string]bool) 154 ms := []*spanner.Mutation{} 155 opts := testmetadata.ReadTestMetadataOptions{ 156 Project: project, 157 TestIDs: testIDs, 158 SourceRef: pbutil.SourceRefFromSources(u.sources), 159 SubRealm: subRealm, 160 } 161 // Update existing metadata entries. 162 err := testmetadata.ReadTestMetadata(ctx, opts, func(tmd *testmetadata.TestMetadataRow) error { 163 seen[tmd.TestID] = true 164 // No update if test result from a lower commit position than existing test metadata. 165 if u.sources.GitilesCommit.Position < tmd.Position { 166 return nil 167 } 168 newMetadata := testMetadata[testID(tmd.TestID)] 169 newBitField := fieldExistenceBitField(newMetadata) 170 existingBitField := fieldExistenceBitField(tmd.TestMetadata) 171 // If test result from the same commit position, 172 // then only update when more metadata fields were found in the test results. 173 // If test result from a higher commit position, 174 // then update when more or equal number of metadata fields were found in the test results 175 // OR existing test metadata expired. 176 if (u.sources.GitilesCommit.Position == tmd.Position && newBitField > existingBitField) || 177 (u.sources.GitilesCommit.Position > tmd.Position && 178 (tmd.LastUpdated.Add(metadataExpiry).Before(u.start) || newBitField >= existingBitField)) { 179 mutation := u.testMetadataMutation(project, tmd.TestID, subRealm, testMetadata[testID(tmd.TestID)]) 180 ms = append(ms, mutation) 181 } 182 return nil 183 }) 184 if err != nil { 185 return err 186 } 187 // Create new metadata entries which aren't exist yet. 188 for testID, tm := range testMetadata { 189 if !seen[string(testID)] { 190 mutation := u.testMetadataMutation(project, string(testID), subRealm, tm) 191 ms = append(ms, mutation) 192 } 193 } 194 span.BufferWrite(ctx, ms...) 195 return nil 196 } 197 _, err := span.ReadWriteTransaction(ctx, f) 198 if err != nil { 199 return err 200 } 201 return nil 202 } 203 204 var saveCols = []string{ 205 "Project", 206 "TestId", 207 "RefHash", 208 "SubRealm", 209 "LastUpdated", 210 "TestMetadata", 211 "SourceRef", 212 "Position", 213 } 214 215 func (u *testMetadataUpdator) testMetadataMutation(project, testID, subRealm string, tm *pb.TestMetadata) *spanner.Mutation { 216 ref := pbutil.SourceRefFromSources(u.sources) 217 vals := []any{ 218 project, 219 testID, 220 pbutil.SourceRefHash(ref), 221 subRealm, 222 spanner.CommitTimestamp, 223 spanutil.Compressed(pbutil.MustMarshal(tm)).ToSpanner(), 224 spanutil.Compressed(pbutil.MustMarshal(ref)).ToSpanner(), 225 u.sources.GitilesCommit.Position, 226 } 227 return spanner.InsertOrUpdate("TestMetadata", saveCols, vals) 228 } 229 230 // fieldExistenceBitField returns bit fields contains 0 or 1 in each bit to indicate 231 // the existence of each field in the test metadata. 232 // Fields from the lowest bit to highest bit: 233 // TestMetadata.name 234 // TestMetadata.location.repo 235 // TestMetadata.location.file_name 236 // TestMetadata.location.line 237 // TestMetadata.bug_component 238 // TestMetadata.properties 239 func fieldExistenceBitField(metadata *pb.TestMetadata) uint8 { 240 bitField := uint8(0) 241 bitFieldOrder := []string{ 242 "name", 243 "location.repo", 244 "location.file_name", 245 "location.line", 246 "bug_component", 247 "properties", 248 } 249 for i, k := range bitFieldOrder { 250 if exist(strings.Split(k, "."), metadata.ProtoReflect()) { 251 bitField += uint8(math.Pow(2, float64(i))) 252 } 253 } 254 return bitField 255 } 256 257 func exist(fieldNameTokens []string, message protoreflect.Message) bool { 258 if len(fieldNameTokens) == 0 { 259 return true 260 } 261 curName := fieldNameTokens[0] 262 fd := message.Descriptor().Fields().ByTextName(curName) 263 if message.Has(fd) { 264 if fd.Kind() == protoreflect.MessageKind { 265 return exist(fieldNameTokens[1:], message.Get(fd).Message()) 266 } 267 return len(fieldNameTokens) == 1 268 } 269 return false 270 }