go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/batch_create_test_results.go (about) 1 // Copyright 2019 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 recorder 16 17 import ( 18 "context" 19 "time" 20 21 "cloud.google.com/go/spanner" 22 "golang.org/x/sync/errgroup" 23 "google.golang.org/protobuf/proto" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/grpc/appstatus" 29 "go.chromium.org/luci/resultdb/internal/invocations" 30 "go.chromium.org/luci/resultdb/internal/resultcount" 31 "go.chromium.org/luci/resultdb/internal/spanutil" 32 "go.chromium.org/luci/resultdb/pbutil" 33 pb "go.chromium.org/luci/resultdb/proto/v1" 34 "go.chromium.org/luci/server/span" 35 ) 36 37 func emptyOrEqual(name, actual, expected string) error { 38 switch actual { 39 case "", expected: 40 return nil 41 } 42 return errors.Reason("%s must be either empty or equal to %q, but %q", name, expected, actual).Err() 43 } 44 45 func validateBatchCreateTestResultsRequest(req *pb.BatchCreateTestResultsRequest, now time.Time) error { 46 if err := pbutil.ValidateInvocationName(req.Invocation); err != nil { 47 return errors.Annotate(err, "invocation").Err() 48 } 49 50 if err := pbutil.ValidateRequestID(req.RequestId); err != nil { 51 return errors.Annotate(err, "request_id").Err() 52 } 53 54 if err := pbutil.ValidateBatchRequestCount(len(req.Requests)); err != nil { 55 return err 56 } 57 58 type Key struct { 59 testID string 60 resultID string 61 } 62 keySet := map[Key]struct{}{} 63 64 for i, r := range req.Requests { 65 if err := emptyOrEqual("invocation", r.Invocation, req.Invocation); err != nil { 66 return errors.Annotate(err, "requests: %d", i).Err() 67 } 68 if err := emptyOrEqual("request_id", r.RequestId, req.RequestId); err != nil { 69 return errors.Annotate(err, "requests: %d", i).Err() 70 } 71 if err := pbutil.ValidateTestResult(now, r.TestResult); err != nil { 72 return errors.Annotate(err, "requests: %d: test_result", i).Err() 73 } 74 75 key := Key{ 76 testID: r.TestResult.TestId, 77 resultID: r.TestResult.ResultId, 78 } 79 if _, ok := keySet[key]; ok { 80 // Duplicated results. 81 return errors.Reason("duplicate test results in request: testID %q, resultID %q", key.testID, key.resultID).Err() 82 } 83 keySet[key] = struct{}{} 84 } 85 return nil 86 } 87 88 // BatchCreateTestResults implements pb.RecorderServer. 89 func (s *recorderServer) BatchCreateTestResults(ctx context.Context, in *pb.BatchCreateTestResultsRequest) (*pb.BatchCreateTestResultsResponse, error) { 90 now := clock.Now(ctx).UTC() 91 if err := validateBatchCreateTestResultsRequest(in, now); err != nil { 92 return nil, appstatus.BadRequest(err) 93 } 94 95 invID := invocations.MustParseName(in.Invocation) 96 ret := &pb.BatchCreateTestResultsResponse{ 97 TestResults: make([]*pb.TestResult, len(in.Requests)), 98 } 99 ms := make([]*spanner.Mutation, len(in.Requests)) 100 var commonPrefix string 101 varUnion := stringset.New(0) 102 for i, r := range in.Requests { 103 ret.TestResults[i], ms[i] = insertTestResult(ctx, invID, in.RequestId, r.TestResult) 104 if i == 0 { 105 commonPrefix = r.TestResult.TestId 106 } else { 107 commonPrefix = longestCommonPrefix(commonPrefix, r.TestResult.TestId) 108 } 109 varUnion.AddAll(pbutil.VariantToStrings(r.TestResult.GetVariant())) 110 } 111 112 var realm string 113 err := mutateInvocation(ctx, invID, func(ctx context.Context) error { 114 span.BufferWrite(ctx, ms...) 115 eg, ctx := errgroup.WithContext(ctx) 116 eg.Go(func() (err error) { 117 var invCommonTestIdPrefix spanner.NullString 118 var invVars []string 119 if err = invocations.ReadColumns(ctx, invID, map[string]any{ 120 "Realm": &realm, 121 "CommonTestIDPrefix": &invCommonTestIdPrefix, 122 "TestResultVariantUnion": &invVars, 123 }); err != nil { 124 return 125 } 126 127 newPrefix := commonPrefix 128 if !invCommonTestIdPrefix.IsNull() { 129 newPrefix = longestCommonPrefix(invCommonTestIdPrefix.String(), commonPrefix) 130 } 131 varUnion.AddAll(invVars) 132 133 if invCommonTestIdPrefix.String() != newPrefix || varUnion.Len() > len(invVars) { 134 span.BufferWrite(ctx, spanutil.UpdateMap("Invocations", map[string]any{ 135 "InvocationId": invID, 136 "CommonTestIDPrefix": newPrefix, 137 "TestResultVariantUnion": varUnion.ToSortedSlice(), 138 })) 139 } 140 141 return 142 }) 143 eg.Go(func() error { 144 return resultcount.IncrementTestResultCount(ctx, invID, int64(len(in.Requests))) 145 }) 146 return eg.Wait() 147 }) 148 if err != nil { 149 return nil, err 150 } 151 152 spanutil.IncRowCount(ctx, len(in.Requests), spanutil.TestResults, spanutil.Inserted, realm) 153 return ret, nil 154 } 155 156 func insertTestResult(ctx context.Context, invID invocations.ID, requestID string, body *pb.TestResult) (*pb.TestResult, *spanner.Mutation) { 157 // create a copy of the input message with the OUTPUT_ONLY field(s) to be used in 158 // the response 159 ret := proto.Clone(body).(*pb.TestResult) 160 ret.Name = pbutil.TestResultName(string(invID), ret.TestId, ret.ResultId) 161 ret.VariantHash = pbutil.VariantHash(ret.Variant) 162 163 // handle values for nullable columns 164 var runDuration spanner.NullInt64 165 if ret.Duration != nil { 166 runDuration.Int64 = pbutil.MustDuration(ret.Duration).Microseconds() 167 runDuration.Valid = true 168 } 169 170 row := map[string]any{ 171 "InvocationId": invID, 172 "TestId": ret.TestId, 173 "ResultId": ret.ResultId, 174 "Variant": ret.Variant, 175 "VariantHash": ret.VariantHash, 176 "CommitTimestamp": spanner.CommitTimestamp, 177 "IsUnexpected": spanner.NullBool{Bool: true, Valid: !body.Expected}, 178 "Status": ret.Status, 179 "SummaryHTML": spanutil.Compressed(ret.SummaryHtml), 180 "StartTime": ret.StartTime, 181 "RunDurationUsec": runDuration, 182 "Tags": ret.Tags, 183 } 184 if ret.SkipReason != pb.SkipReason_SKIP_REASON_UNSPECIFIED { 185 // Unspecified is mapped to NULL, so only write if we have some other value. 186 row["SkipReason"] = ret.SkipReason 187 } 188 if ret.TestMetadata != nil { 189 row["TestMetadata"] = spanutil.Compressed(pbutil.MustMarshal(ret.TestMetadata)) 190 } 191 if ret.FailureReason != nil { 192 row["FailureReason"] = spanutil.Compressed(pbutil.MustMarshal(ret.FailureReason)) 193 } 194 if ret.Properties != nil { 195 row["Properties"] = spanutil.Compressed(pbutil.MustMarshal(ret.Properties)) 196 } 197 mutation := spanner.InsertOrUpdateMap("TestResults", spanutil.ToSpannerMap(row)) 198 return ret, mutation 199 } 200 201 func longestCommonPrefix(str1, str2 string) string { 202 for i := 0; i < len(str1) && i < len(str2); i++ { 203 if str1[i] != str2[i] { 204 return str1[:i] 205 } 206 } 207 if len(str1) <= len(str2) { 208 return str1 209 } 210 return str2 211 }