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  }