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  }