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) }