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 }