go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/runquery/project.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 runquery
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  
    21  	"go.chromium.org/luci/common/errors"
    22  	"go.chromium.org/luci/common/retry/transient"
    23  	"go.chromium.org/luci/gae/service/datastore"
    24  
    25  	"go.chromium.org/luci/cv/internal/common"
    26  	"go.chromium.org/luci/cv/internal/run"
    27  )
    28  
    29  var endedStatuses []run.Status
    30  
    31  func init() {
    32  	for s := range run.Status_name {
    33  		status := run.Status(s)
    34  		if status != run.Status_ENDED_MASK && run.IsEnded(status) {
    35  			endedStatuses = append(endedStatuses, status)
    36  		}
    37  	}
    38  }
    39  
    40  // ProjectQueryBuilder builds datastore.Query for searching Runs scoped to a
    41  // LUCI project.
    42  type ProjectQueryBuilder struct {
    43  	// Project is the LUCI project. Required.
    44  	Project string
    45  	// Status optionally restricts query to Runs with this status.
    46  	Status run.Status
    47  	// MaxExcl restricts query to Runs with ID lexicographically smaller. Optional.
    48  	//
    49  	// This means query is restricted to Runs created after this Run.
    50  	//
    51  	// This Run must belong to the same LUCI project.
    52  	MaxExcl common.RunID
    53  	// MinExcl restricts query to Runs with ID lexicographically larger. Optional.
    54  	//
    55  	// This means query is restricted to Runs created before this Run.
    56  	//
    57  	// This Run must belong to the same LUCI project.
    58  	MinExcl common.RunID
    59  
    60  	// Limit limits the number of results if positive. Ignored otherwise.
    61  	Limit int32
    62  }
    63  
    64  // isSatisfied returns whether the given Run satisfies the query.
    65  func (b ProjectQueryBuilder) isSatisfied(r *run.Run) bool {
    66  	switch {
    67  	case r == nil:
    68  	case r.ID.LUCIProject() != b.Project:
    69  	case b.Status == run.Status_ENDED_MASK && !run.IsEnded(r.Status):
    70  	case b.Status != run.Status_ENDED_MASK && b.Status != run.Status_STATUS_UNSPECIFIED && r.Status != b.Status:
    71  	case b.MinExcl != "" && r.ID <= b.MinExcl:
    72  	case b.MaxExcl != "" && r.ID >= b.MaxExcl:
    73  	default:
    74  		return true
    75  	}
    76  	return false
    77  }
    78  
    79  // After restricts the query to Runs created after the given Run.
    80  //
    81  // Panics if ProjectQueryBuilder is already constrained to a different Project.
    82  func (b ProjectQueryBuilder) After(id common.RunID) ProjectQueryBuilder {
    83  	if p := id.LUCIProject(); p != b.Project {
    84  		if b.Project != "" {
    85  			panic(fmt.Errorf("invalid ProjectQueryBuilder.After(%q): .Project is already set to %q", id, b.Project))
    86  		}
    87  		b.Project = p
    88  	}
    89  	b.MaxExcl = id
    90  	return b
    91  }
    92  
    93  // Before restricts the query to Runs created before the given Run.
    94  //
    95  // Panics if ProjectQueryBuilder is already constrained to a different Project.
    96  func (b ProjectQueryBuilder) Before(id common.RunID) ProjectQueryBuilder {
    97  	if p := id.LUCIProject(); p != b.Project {
    98  		if b.Project != "" {
    99  			panic(fmt.Errorf("invalid ProjectQueryBuilder.Before(%q): .Project is already set to %q", id, b.Project))
   100  		}
   101  		b.Project = p
   102  	}
   103  	b.MinExcl = id
   104  	return b
   105  }
   106  
   107  // PageToken constraints ProjectQueryBuilder to continue searching from the
   108  // prior search.
   109  func (b ProjectQueryBuilder) PageToken(pt *PageToken) ProjectQueryBuilder {
   110  	if pt != nil {
   111  		b.MinExcl = common.RunID(pt.GetRun())
   112  	}
   113  	return b
   114  }
   115  
   116  // BuildKeysOnly returns keys-only query on Run entities.
   117  //
   118  // It's exposed primarily for debugging reasons.
   119  //
   120  // WARNING: panics if Status is magic Status_ENDED_MASK,
   121  // as it's not feasible to perform this as 1 query.
   122  func (b ProjectQueryBuilder) BuildKeysOnly(ctx context.Context) *datastore.Query {
   123  	q := datastore.NewQuery(common.RunKind).KeysOnly(true)
   124  
   125  	switch b.Status {
   126  	case run.Status_ENDED_MASK:
   127  		panic(fmt.Errorf("Status=Status_ENDED_MASK is not yet supported"))
   128  	case run.Status_STATUS_UNSPECIFIED:
   129  	default:
   130  		q = q.Eq("Status", int(b.Status))
   131  	}
   132  
   133  	if b.Limit > 0 {
   134  		q = q.Limit(b.Limit)
   135  	}
   136  
   137  	if b.Project == "" {
   138  		panic(fmt.Errorf("Project is not set"))
   139  	}
   140  	min, max := rangeOfProjectIDs(b.Project)
   141  
   142  	switch {
   143  	case b.MinExcl == "":
   144  	case b.MinExcl.LUCIProject() != b.Project:
   145  		panic(fmt.Errorf("MinExcl %q doesn't match Project %q", b.MinExcl, b.Project))
   146  	default:
   147  		min = string(b.MinExcl)
   148  	}
   149  	q = q.Gt("__key__", datastore.MakeKey(ctx, common.RunKind, min))
   150  
   151  	switch {
   152  	case b.MaxExcl == "":
   153  	case b.MaxExcl.LUCIProject() != b.Project:
   154  		panic(fmt.Errorf("MaxExcl %q doesn't match Project %q", b.MaxExcl, b.Project))
   155  	default:
   156  		max = string(b.MaxExcl)
   157  	}
   158  	q = q.Lt("__key__", datastore.MakeKey(ctx, common.RunKind, max))
   159  
   160  	return q
   161  }
   162  
   163  // GetAllRunKeys runs the query and returns Datastore keys to Run entities.
   164  func (b ProjectQueryBuilder) GetAllRunKeys(ctx context.Context) ([]*datastore.Key, error) {
   165  	var keys []*datastore.Key
   166  
   167  	if b.Status != run.Status_ENDED_MASK {
   168  		if err := datastore.GetAll(ctx, b.BuildKeysOnly(ctx), &keys); err != nil {
   169  			return nil, errors.Annotate(err, "failed to fetch Runs IDs").Tag(transient.Tag).Err()
   170  		}
   171  		return keys, nil
   172  	}
   173  
   174  	// Status_ENDED_MASK requires several dedicated queries.
   175  	queries := make([]*datastore.Query, len(endedStatuses))
   176  	for i, s := range endedStatuses {
   177  		cpy := b
   178  		cpy.Status = s
   179  		queries[i] = cpy.BuildKeysOnly(ctx)
   180  	}
   181  	err := datastore.RunMulti(ctx, queries, func(k *datastore.Key) error {
   182  		keys = append(keys, k)
   183  		if b.Limit > 0 && len(keys) == int(b.Limit) {
   184  			return datastore.Stop
   185  		}
   186  		return nil
   187  	})
   188  	if err != nil {
   189  		return nil, errors.Annotate(err, "failed to fetch Runs IDs").Tag(transient.Tag).Err()
   190  	}
   191  	return keys, err
   192  }
   193  
   194  // LoadRuns returns matched Runs and the page token to continue search later.
   195  func (b ProjectQueryBuilder) LoadRuns(ctx context.Context, checkers ...run.LoadRunChecker) ([]*run.Run, *PageToken, error) {
   196  	return loadRunsFromQuery(ctx, b, checkers...)
   197  }
   198  
   199  // qLimit implements runKeysQuery interface.
   200  func (b ProjectQueryBuilder) qLimit() int32 { return b.Limit }
   201  
   202  // qPageToken implements runKeysQuery interface.
   203  func (b ProjectQueryBuilder) qPageToken(pt *PageToken) runKeysQuery { return b.PageToken(pt) }