go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/pbutil/query.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 pbutil 16 17 import ( 18 "context" 19 20 "github.com/golang/protobuf/proto" 21 "golang.org/x/sync/errgroup" 22 23 pb "go.chromium.org/luci/resultdb/proto/v1" 24 ) 25 26 // Query queries for results continuously, sending individual items to 27 // dest channel until the paging query is exhausted or the context is canceled. 28 // A request must be *pb.QueryTestResultRequest, pb.QueryTestExonerationsRequest 29 // or *pb.QueryArtifactsRequest. Messages sent to dest are *pb.TestResult, 30 // *pb.TestExoneration or *pb.Artifact respectively. 31 // 32 // Does not return the next page token because ctx can be canceled in the middle 33 // of a page. 34 // 35 // If there are multiple requests in reqs, then runs them all concurrently and 36 // sends all of their results to dest. 37 // This is useful to query items of different types, e.g. test results and 38 // test exonerations. 39 // Does not limit concurrency. 40 func Query(ctx context.Context, dest chan<- proto.Message, client pb.ResultDBClient, reqs ...proto.Message) error { 41 eg, ctx := errgroup.WithContext(ctx) 42 for _, req := range reqs { 43 // Check req type. 44 switch req.(type) { 45 case *pb.QueryTestResultsRequest: 46 case *pb.QueryTestExonerationsRequest: 47 case *pb.QueryArtifactsRequest: 48 default: 49 panic("req must be *QueryTestResultRequest, *QueryTestExonerationRequest or *QueryArtifactsRequest") 50 } 51 52 q := &queryResults{ 53 client: client, 54 // Make a copy because we will be modifying it. 55 req: proto.Clone(req), 56 dest: dest, 57 } 58 eg.Go(func() error { 59 return q.run(ctx) 60 }) 61 } 62 return eg.Wait() 63 } 64 65 // queryResults implements Query for one request. 66 type queryResults struct { 67 client pb.ResultDBClient 68 req proto.Message 69 dest chan<- proto.Message 70 } 71 72 func (q *queryResults) run(ctx context.Context) error { 73 // Prepare a channel for responses such that we can make an RPC as soon as we 74 // started consuming the response, as opposed to after the response is 75 // completely consumed. 76 batchC := make(chan []proto.Message) 77 errC := make(chan error, 1) 78 go func() { 79 defer close(batchC) 80 errC <- q.queryResponses(ctx, batchC) 81 }() 82 83 // Forward items to q.dest. 84 for batch := range batchC { 85 for _, item := range batch { 86 // Note: selecting on errC here would be a race because the batch 87 // goroutine might have been already done, but we still did not send all 88 // items to the caller. 89 select { 90 case <-ctx.Done(): 91 return <-errC 92 case q.dest <- item: 93 } 94 } 95 } 96 return <-errC 97 } 98 99 // queryResponses pages through query results and sends item batches to batchC. 100 func (q *queryResults) queryResponses(ctx context.Context, batchC chan<- []proto.Message) error { 101 // The initial token is the current token in the base request. 102 token := (q.req).(interface{ GetPageToken() string }).GetPageToken() 103 for { 104 var batch []proto.Message 105 var err error 106 batch, token, err = q.call(ctx, token) 107 if err != nil || len(batch) == 0 { 108 if ctx.Err() != nil { 109 return ctx.Err() 110 } 111 return err 112 } 113 114 select { 115 case batchC <- batch: 116 case <-ctx.Done(): 117 return ctx.Err() 118 } 119 120 if token == "" { 121 return nil 122 } 123 } 124 } 125 126 // call makes a request with a given page token. 127 func (q *queryResults) call(ctx context.Context, pageToken string) (batch []proto.Message, nextPageToken string, err error) { 128 // Make a call and convert the response to generic types. 129 switch req := q.req.(type) { 130 case *pb.QueryTestResultsRequest: 131 req.PageToken = pageToken 132 var res *pb.QueryTestResultsResponse 133 res, err = q.client.QueryTestResults(ctx, req) 134 if res != nil { 135 batch = make([]proto.Message, len(res.TestResults)) 136 for i, r := range res.TestResults { 137 batch[i] = r 138 } 139 nextPageToken = res.NextPageToken 140 } 141 142 case *pb.QueryTestExonerationsRequest: 143 req.PageToken = pageToken 144 var res *pb.QueryTestExonerationsResponse 145 res, err = q.client.QueryTestExonerations(ctx, req) 146 if res != nil { 147 batch = make([]proto.Message, len(res.TestExonerations)) 148 for i, r := range res.TestExonerations { 149 batch[i] = r 150 } 151 nextPageToken = res.NextPageToken 152 } 153 154 case *pb.QueryArtifactsRequest: 155 req.PageToken = pageToken 156 var res *pb.QueryArtifactsResponse 157 res, err = q.client.QueryArtifacts(ctx, req) 158 if res != nil { 159 batch = make([]proto.Message, len(res.Artifacts)) 160 for i, r := range res.Artifacts { 161 batch[i] = r 162 } 163 nextPageToken = res.NextPageToken 164 } 165 166 default: 167 panic("impossible") 168 } 169 170 return 171 }