go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/changepoints/bqexporter/bqexporter.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 bqexporter handles the export of test variant analysis results 16 // to BigQuery. 17 package bqexporter 18 19 import ( 20 "context" 21 "encoding/hex" 22 "time" 23 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/errors" 28 29 "go.chromium.org/luci/analysis/internal/changepoints/inputbuffer" 30 cpb "go.chromium.org/luci/analysis/internal/changepoints/proto" 31 "go.chromium.org/luci/analysis/internal/changepoints/testvariantbranch" 32 "go.chromium.org/luci/analysis/pbutil" 33 bqpb "go.chromium.org/luci/analysis/proto/bq" 34 analysispb "go.chromium.org/luci/analysis/proto/v1" 35 ) 36 37 // If an unexpected test result is within 90 days, it is consider 38 // recently unexpected. 39 const recentUnexpectedResultThresholdHours = 90 * 24 40 41 // InsertClient defines an interface for inserting TestVariantBranchRow into BigQuery. 42 type InsertClient interface { 43 // Insert inserts the given rows into BigQuery. 44 Insert(ctx context.Context, rows []*bqpb.TestVariantBranchRow) error 45 } 46 47 // Exporter provides methods to export test variant branches to BigQuery. 48 type Exporter struct { 49 client InsertClient 50 } 51 52 // NewExporter instantiates a new Exporter. The given client is used 53 // to insert rows into BigQuery. 54 func NewExporter(client InsertClient) *Exporter { 55 return &Exporter{client: client} 56 } 57 58 // RowInputs is the contains the rows to be exported to BigQuery, together with 59 // the Spanner commit timestamp. 60 type RowInputs struct { 61 Rows []PartialBigQueryRow 62 CommitTimestamp time.Time 63 } 64 65 // ExportTestVariantBranches exports test variant branches to BigQuery. 66 func (e *Exporter) ExportTestVariantBranches(ctx context.Context, rowInputs RowInputs) error { 67 bqRows := make([]*bqpb.TestVariantBranchRow, len(rowInputs.Rows)) 68 for i, r := range rowInputs.Rows { 69 bqRows[i] = r.Complete(rowInputs.CommitTimestamp) 70 } 71 err := e.client.Insert(ctx, bqRows) 72 if err != nil { 73 return errors.Annotate(err, "insert rows").Err() 74 } 75 return nil 76 } 77 78 // PartialBigQueryRow represents a partially constructed BigQuery 79 // export row. Call Complete(...) on the row to finish its construction. 80 type PartialBigQueryRow struct { 81 // Field is private to avoid callers mistakenly exporting partially 82 // populated rows. 83 row *bqpb.TestVariantBranchRow 84 mostRecentUnexpectedResultHour time.Time 85 } 86 87 // ToPartialBigQueryRow starts building a BigQuery TestVariantBranchRow. 88 // All fields except those dependent on the commit timestamp, (i.e. 89 // Version and HasRecentUnexpectedResults) are populated. 90 // 91 // inputBufferSegments is the remaining segments in the input buffer of 92 // TestVariantBranch, after changepoint analysis and eviction process. 93 // Segments are sorted by commit position (lowest/oldest first). 94 // 95 // To support re-use of the *testvariantbranch.Entry buffer, no reference 96 // to tvb or inputBufferSegments or their fields (except immutable strings) 97 // will be retained by this method or its result. (All data will 98 // be copied.) 99 func ToPartialBigQueryRow(tvb *testvariantbranch.Entry, inputBufferSegments []*inputbuffer.Segment) (PartialBigQueryRow, error) { 100 row := &bqpb.TestVariantBranchRow{ 101 Project: tvb.Project, 102 TestId: tvb.TestID, 103 VariantHash: tvb.VariantHash, 104 RefHash: hex.EncodeToString(tvb.RefHash), 105 Ref: proto.Clone(tvb.SourceRef).(*analysispb.SourceRef), 106 } 107 108 // Variant. 109 variant, err := pbutil.VariantToJSON(tvb.Variant) 110 if err != nil { 111 return PartialBigQueryRow{}, errors.Annotate(err, "variant to json").Err() 112 } 113 row.Variant = variant 114 115 // Segments. 116 row.Segments = toSegments(tvb, inputBufferSegments) 117 118 // The row is partial because HasRecentUnexpectedResults and Version 119 // is not yet populated. Wrap it in another type to prevent export as-is. 120 return PartialBigQueryRow{ 121 row: row, 122 mostRecentUnexpectedResultHour: mostRecentUnexpectedResult(tvb, inputBufferSegments), 123 }, nil 124 } 125 126 // Complete finishes creating the BigQuery export row, returning it. 127 func (r PartialBigQueryRow) Complete(commitTimestamp time.Time) *bqpb.TestVariantBranchRow { 128 row := r.row 129 130 // Has recent unexpected result. 131 if commitTimestamp.Sub(r.mostRecentUnexpectedResultHour).Hours() <= recentUnexpectedResultThresholdHours { 132 row.HasRecentUnexpectedResults = 1 133 } 134 135 row.Version = timestamppb.New(commitTimestamp) 136 return row 137 } 138 139 // toSegments returns the segments for row input. 140 // The segments returned will be sorted, with the most recent segment 141 // comes first. 142 func toSegments(tvb *testvariantbranch.Entry, inputBufferSegments []*inputbuffer.Segment) []*bqpb.Segment { 143 results := []*bqpb.Segment{} 144 145 // The index where the active segments starts. 146 // If there is a finalizing segment, then the we need to first combine it will 147 // the first segment from the input buffer. 148 activeStartIndex := 0 149 if tvb.FinalizingSegment != nil { 150 activeStartIndex = 1 151 } 152 153 // Add the active segments. 154 for i := len(inputBufferSegments) - 1; i >= activeStartIndex; i-- { 155 inputSegment := inputBufferSegments[i] 156 bqSegment := inputSegmentToBQSegment(inputSegment) 157 results = append(results, bqSegment) 158 } 159 160 // Add the finalizing segment. 161 if tvb.FinalizingSegment != nil { 162 bqSegment := combineSegment(tvb.FinalizingSegment, inputBufferSegments[0]) 163 results = append(results, bqSegment) 164 } 165 166 // Add the finalized segments. 167 if tvb.FinalizedSegments != nil { 168 // More recent segments are on the back. 169 for i := len(tvb.FinalizedSegments.Segments) - 1; i >= 0; i-- { 170 segment := tvb.FinalizedSegments.Segments[i] 171 bqSegment := segmentToBQSegment(segment) 172 results = append(results, bqSegment) 173 } 174 } 175 176 return results 177 } 178 179 // combineSegment constructs a finalizing segment from its finalized part in 180 // the output buffer and its unfinalized part in the input buffer. 181 func combineSegment(finalizingSegment *cpb.Segment, inputSegment *inputbuffer.Segment) *bqpb.Segment { 182 return &bqpb.Segment{ 183 HasStartChangepoint: finalizingSegment.HasStartChangepoint, 184 StartPosition: finalizingSegment.StartPosition, 185 StartHour: timestamppb.New(finalizingSegment.StartHour.AsTime()), 186 StartPositionLowerBound_99Th: finalizingSegment.StartPositionLowerBound_99Th, 187 StartPositionUpperBound_99Th: finalizingSegment.StartPositionUpperBound_99Th, 188 EndPosition: inputSegment.EndPosition, 189 EndHour: timestamppb.New(inputSegment.EndHour.AsTime()), 190 Counts: countsToBQCounts(testvariantbranch.AddCounts(finalizingSegment.FinalizedCounts, inputSegment.Counts)), 191 } 192 } 193 194 func inputSegmentToBQSegment(segment *inputbuffer.Segment) *bqpb.Segment { 195 return &bqpb.Segment{ 196 HasStartChangepoint: segment.HasStartChangepoint, 197 StartPosition: segment.StartPosition, 198 StartPositionLowerBound_99Th: segment.StartPositionLowerBound99Th, 199 StartPositionUpperBound_99Th: segment.StartPositionUpperBound99Th, 200 StartHour: timestamppb.New(segment.StartHour.AsTime()), 201 EndPosition: segment.EndPosition, 202 EndHour: timestamppb.New(segment.EndHour.AsTime()), 203 Counts: countsToBQCounts(segment.Counts), 204 } 205 } 206 207 func segmentToBQSegment(segment *cpb.Segment) *bqpb.Segment { 208 return &bqpb.Segment{ 209 HasStartChangepoint: segment.HasStartChangepoint, 210 StartPosition: segment.StartPosition, 211 StartPositionLowerBound_99Th: segment.StartPositionLowerBound_99Th, 212 StartPositionUpperBound_99Th: segment.StartPositionUpperBound_99Th, 213 StartHour: timestamppb.New(segment.StartHour.AsTime()), 214 EndPosition: segment.EndPosition, 215 EndHour: timestamppb.New(segment.EndHour.AsTime()), 216 Counts: countsToBQCounts(segment.FinalizedCounts), 217 } 218 } 219 220 func countsToBQCounts(counts *cpb.Counts) *bqpb.Segment_Counts { 221 return &bqpb.Segment_Counts{ 222 TotalVerdicts: counts.TotalVerdicts, 223 UnexpectedVerdicts: counts.UnexpectedVerdicts, 224 FlakyVerdicts: counts.FlakyVerdicts, 225 TotalRuns: counts.TotalRuns, 226 FlakyRuns: counts.FlakyRuns, 227 UnexpectedUnretriedRuns: counts.UnexpectedUnretriedRuns, 228 UnexpectedAfterRetryRuns: counts.UnexpectedAfterRetryRuns, 229 TotalResults: counts.TotalResults, 230 UnexpectedResults: counts.UnexpectedResults, 231 ExpectedPassedResults: counts.ExpectedPassedResults, 232 ExpectedFailedResults: counts.ExpectedFailedResults, 233 ExpectedCrashedResults: counts.ExpectedCrashedResults, 234 ExpectedAbortedResults: counts.ExpectedAbortedResults, 235 UnexpectedPassedResults: counts.UnexpectedPassedResults, 236 UnexpectedFailedResults: counts.UnexpectedFailedResults, 237 UnexpectedCrashedResults: counts.UnexpectedCrashedResults, 238 UnexpectedAbortedResults: counts.UnexpectedAbortedResults, 239 } 240 } 241 242 // mostRecentUnexpectedResult returns the most recent unexpected result, 243 // if any. If there is no recent unexpected result, return the zero time 244 // (time.Time{}). 245 func mostRecentUnexpectedResult(tvb *testvariantbranch.Entry, inputBufferSegments []*inputbuffer.Segment) time.Time { 246 var mostRecentUnexpected time.Time 247 248 // Check input segments. 249 for _, segment := range inputBufferSegments { 250 if segment.MostRecentUnexpectedResultHourAllVerdicts != nil { 251 time := segment.MostRecentUnexpectedResultHourAllVerdicts.AsTime() 252 if time.After(mostRecentUnexpected) { 253 mostRecentUnexpected = time 254 } 255 } 256 257 } 258 259 // Check finalizing segment. 260 if tvb.FinalizingSegment != nil && tvb.FinalizingSegment.MostRecentUnexpectedResultHour != nil { 261 time := tvb.FinalizingSegment.MostRecentUnexpectedResultHour.AsTime() 262 if time.After(mostRecentUnexpected) { 263 mostRecentUnexpected = time 264 } 265 } 266 267 // Check finalized segments. 268 if tvb.FinalizedSegments != nil { 269 for _, segment := range tvb.FinalizedSegments.Segments { 270 if segment.MostRecentUnexpectedResultHour != nil { 271 time := segment.MostRecentUnexpectedResultHour.AsTime() 272 if time.After(mostRecentUnexpected) { 273 mostRecentUnexpected = time 274 } 275 } 276 } 277 } 278 279 return mostRecentUnexpected 280 }