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  }