go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/sink/test_result_channel.go (about)

     1  // Copyright 2020 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 sink
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"path"
    21  	"strings"
    22  	"sync"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"github.com/google/uuid"
    27  	"google.golang.org/protobuf/proto"
    28  
    29  	"go.chromium.org/luci/common/data/stringset"
    30  	"go.chromium.org/luci/common/sync/dispatcher"
    31  	"go.chromium.org/luci/common/sync/dispatcher/buffer"
    32  
    33  	"go.chromium.org/luci/resultdb/pbutil"
    34  	pb "go.chromium.org/luci/resultdb/proto/v1"
    35  	sinkpb "go.chromium.org/luci/resultdb/sink/proto/v1"
    36  )
    37  
    38  type testResultChannel struct {
    39  	ch  dispatcher.Channel
    40  	cfg *ServerConfig
    41  
    42  	// wgActive indicates if there are active goroutines invoking reportTestResults.
    43  	//
    44  	// reportTestResults can be invoked by multiple goroutines in parallel. wgActive is used
    45  	// to ensure that all active goroutines finish enqueuing messages to the channel before
    46  	// closeAndDrain closes and drains the channel.
    47  	wgActive sync.WaitGroup
    48  
    49  	// 1 indicates that testResultChannel started the process of closing and draining
    50  	// the channel. 0, otherwise.
    51  	closed int32
    52  }
    53  
    54  func newTestResultChannel(ctx context.Context, cfg *ServerConfig) *testResultChannel {
    55  	var err error
    56  	c := &testResultChannel{cfg: cfg}
    57  	opts := &dispatcher.Options{
    58  		Buffer: buffer.Options{
    59  			// BatchRequest can include up to 500 requests. KEEP BatchItemsMax <= 500
    60  			// to keep report() simple. For more details, visit
    61  			// https://godoc.org/go.chromium.org/luci/resultdb/proto/v1#BatchCreateTestResultsRequest
    62  			BatchItemsMax: 500,
    63  			MaxLeases:     int(cfg.TestResultChannelMaxLeases),
    64  			BatchAgeMax:   time.Second,
    65  			FullBehavior:  &buffer.BlockNewItems{MaxItems: 8000},
    66  		},
    67  	}
    68  	c.ch, err = dispatcher.NewChannel(ctx, opts, func(b *buffer.Batch) error {
    69  		return c.report(ctx, b)
    70  	})
    71  	if err != nil {
    72  		panic(fmt.Sprintf("failed to create a channel for TestResult: %s", err))
    73  	}
    74  	return c
    75  }
    76  
    77  func (c *testResultChannel) closeAndDrain(ctx context.Context) {
    78  	// announce that it is in the process of closeAndDrain.
    79  	if !atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
    80  		return
    81  	}
    82  	// wait for all the active sessions to finish enquing tests results to the channel
    83  	c.wgActive.Wait()
    84  	c.ch.CloseAndDrain(ctx)
    85  }
    86  
    87  func (c *testResultChannel) schedule(trs ...*sinkpb.TestResult) {
    88  	c.wgActive.Add(1)
    89  	defer c.wgActive.Done()
    90  	// if the channel already has been closed, drop the test results.
    91  	if atomic.LoadInt32(&c.closed) == 1 {
    92  		return
    93  	}
    94  	for _, tr := range trs {
    95  		c.ch.C <- tr
    96  	}
    97  }
    98  
    99  // setLocationSpecificFields sets the test tags and bug component in tr
   100  // by looking for the directory of tr.TestMetadata.Location.FileName
   101  // in the location tags file.
   102  func (c *testResultChannel) setLocationSpecificFields(tr *sinkpb.TestResult) {
   103  	if c.cfg.LocationTags == nil || tr.TestMetadata.GetLocation().GetFileName() == "" {
   104  		return
   105  	}
   106  	repo, ok := c.cfg.LocationTags.Repos[tr.TestMetadata.Location.Repo]
   107  	if !ok || (len(repo.GetDirs()) == 0 && len(repo.GetFiles()) == 0) {
   108  		return
   109  	}
   110  
   111  	tagKeySet := stringset.New(0)
   112  
   113  	var bugComponent *pb.BugComponent
   114  
   115  	// if a test result has a matching file location by file name, use the metadata
   116  	// associated with it first. Fill in the rest using directory metadata.
   117  	// fileName must start with "//" and it has been validated.
   118  	filePath := strings.TrimPrefix(tr.TestMetadata.Location.FileName, "//")
   119  	if f, ok := repo.Files[filePath]; ok {
   120  		for _, ft := range f.Tags {
   121  			if !tagKeySet.Has(ft.Key) {
   122  				tr.Tags = append(tr.Tags, ft)
   123  			}
   124  		}
   125  
   126  		// Fill in keys from file definition so that they are not repeated.
   127  		for _, ft := range f.Tags {
   128  			tagKeySet.Add(ft.Key)
   129  		}
   130  
   131  		if bugComponent == nil {
   132  			bugComponent = f.BugComponent
   133  		}
   134  	}
   135  
   136  	dir := path.Dir(filePath)
   137  	// Start from the directory of the file, then traverse to upper directories.
   138  	for {
   139  		if d, ok := repo.Dirs[dir]; ok {
   140  			for _, t := range d.Tags {
   141  				if !tagKeySet.Has(t.Key) {
   142  					tr.Tags = append(tr.Tags, t)
   143  				}
   144  			}
   145  
   146  			// Add new keys to tagKeySet.
   147  			// We cannot do this above because tag keys for this dir could be repeated.
   148  			for _, t := range d.Tags {
   149  				tagKeySet.Add(t.Key)
   150  			}
   151  
   152  			if bugComponent == nil {
   153  				bugComponent = d.BugComponent
   154  			}
   155  		}
   156  		if dir == "." {
   157  			// Have reached the root.
   158  			break
   159  		}
   160  		dir = path.Dir(dir)
   161  	}
   162  
   163  	// Use LocationTags-derived bug component if one is not already set.
   164  	if tr.TestMetadata.BugComponent == nil && bugComponent != nil {
   165  		tr.TestMetadata.BugComponent = proto.Clone(bugComponent).(*pb.BugComponent)
   166  	}
   167  }
   168  
   169  func (c *testResultChannel) report(ctx context.Context, b *buffer.Batch) error {
   170  	// retried batch?
   171  	if b.Meta == nil {
   172  		reqs := make([]*pb.CreateTestResultRequest, len(b.Data))
   173  		for i, d := range b.Data {
   174  			tr := d.Item.(*sinkpb.TestResult)
   175  			c.setLocationSpecificFields(tr)
   176  			tags := append(tr.GetTags(), c.cfg.BaseTags...)
   177  
   178  			// The test result variant will overwrite the value for the
   179  			// duplicate key in the base variant.
   180  			variant := pbutil.CombineVariant(c.cfg.BaseVariant, tr.GetVariant())
   181  
   182  			pbutil.SortStringPairs(tags)
   183  			reqs[i] = &pb.CreateTestResultRequest{
   184  				TestResult: &pb.TestResult{
   185  					TestId:        tr.GetTestId(),
   186  					ResultId:      tr.GetResultId(),
   187  					Variant:       variant,
   188  					Expected:      tr.GetExpected(),
   189  					Status:        tr.GetStatus(),
   190  					SummaryHtml:   tr.GetSummaryHtml(),
   191  					StartTime:     tr.GetStartTime(),
   192  					Duration:      tr.GetDuration(),
   193  					Tags:          tags,
   194  					TestMetadata:  tr.GetTestMetadata(),
   195  					FailureReason: tr.GetFailureReason(),
   196  					Properties:    tr.GetProperties(),
   197  				},
   198  			}
   199  		}
   200  		b.Meta = &pb.BatchCreateTestResultsRequest{
   201  			Invocation: c.cfg.Invocation,
   202  			// a random UUID
   203  			RequestId: uuid.New().String(),
   204  			Requests:  reqs,
   205  		}
   206  	}
   207  	_, err := c.cfg.Recorder.BatchCreateTestResults(ctx, b.Meta.(*pb.BatchCreateTestResultsRequest))
   208  	return err
   209  }