go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/load.go (about) 1 // Copyright 2021 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 run 16 17 import ( 18 "context" 19 "fmt" 20 21 "google.golang.org/grpc/codes" 22 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/common/retry/transient" 25 "go.chromium.org/luci/gae/service/datastore" 26 "go.chromium.org/luci/grpc/appstatus" 27 28 "go.chromium.org/luci/cv/internal/common" 29 ) 30 31 // LoadRunChecker allows to plug ACL checking when loading Run from Datastore. 32 // 33 // See LoadRun(). 34 type LoadRunChecker interface { 35 // Before is called by LoadRun before attempting to load Run from Datastore. 36 // 37 // If Before returns an error, it's returned as is to the caller of LoadRun. 38 Before(ctx context.Context, id common.RunID) error 39 // After is called by LoadRun after loading Run from Datastore. 40 // 41 // If Run wasn't found, nil is passed. 42 // 43 // If After returns an error, it's returned as is to the caller of LoadRun. 44 After(ctx context.Context, runIfFound *Run) error 45 } 46 47 // LoadRun returns Run from Datastore, optionally performing before/after 48 // checks, typically used for checking read permissions. 49 // 50 // If Run isn't found, returns (nil, nil), unless optional checker returns an 51 // error. 52 func LoadRun(ctx context.Context, id common.RunID, checkers ...LoadRunChecker) (*Run, error) { 53 var checker LoadRunChecker = nullRunChecker{} 54 switch l := len(checkers); { 55 case l > 1: 56 panic(fmt.Errorf("at most 1 LoadRunChecker allowed, %d given", l)) 57 case l == 1: 58 checker = checkers[0] 59 } 60 61 if err := checker.Before(ctx, id); err != nil { 62 return nil, err 63 } 64 65 r := &Run{ID: id} 66 switch err := datastore.Get(ctx, r); { 67 case err == datastore.ErrNoSuchEntity: 68 r = nil 69 case err != nil: 70 return nil, errors.Annotate(err, "failed to fetch Run").Tag(transient.Tag).Err() 71 } 72 73 if err := checker.After(ctx, r); err != nil { 74 return nil, err 75 } 76 77 return r, nil 78 } 79 80 // LoadRunsFromKeys prepares loading a Run for each given Datastore Key. 81 func LoadRunsFromKeys(keys ...*datastore.Key) LoadRunsBuilder { 82 return LoadRunsBuilder{keys: keys} 83 } 84 85 // LoadRunsFromIDs prepares loading a Run for each given Run ID. 86 func LoadRunsFromIDs(ids ...common.RunID) LoadRunsBuilder { 87 return LoadRunsBuilder{ids: ids} 88 } 89 90 // LoadRunsBuilder implements builder pattern for loading Runs. 91 type LoadRunsBuilder struct { 92 // one of these must be set. 93 ids common.RunIDs 94 keys []*datastore.Key 95 96 checker LoadRunChecker 97 } 98 99 // Checker installs LoadRunChecker to perform checks before/after loading each 100 // Run, typically used for checking read permission. 101 func (b LoadRunsBuilder) Checker(c LoadRunChecker) LoadRunsBuilder { 102 b.checker = c 103 return b 104 } 105 106 // DoIgnoreNotFound loads and returns Runs in the same order as the input, but 107 // omitting not found ones. 108 // 109 // If used together with Checker: 110 // - if Checker.Before returns error with NotFound code, treats such Run as 111 // not found. 112 // - if Run is not found in Datastore, Checker.After isn't called on it. 113 // - if Checker.After returns error with NotFound code, treats such Run as 114 // not found. 115 // 116 // Returns a singular first encountered error. 117 func (b LoadRunsBuilder) DoIgnoreNotFound(ctx context.Context) ([]*Run, error) { 118 runs, errs := b.Do(ctx) 119 out := runs[:0] 120 for i, r := range runs { 121 switch err := errs[i]; { 122 case err == nil: 123 out = append(out, r) 124 case err == datastore.ErrNoSuchEntity: 125 // Skip. 126 default: 127 if st, ok := appstatus.Get(err); !ok || st.Code() != codes.NotFound { 128 return nil, err 129 } 130 // Also skip due to NotFound error. 131 } 132 } 133 if len(out) == 0 { 134 // Free memory immediately. 135 return nil, nil 136 } 137 return out, nil 138 } 139 140 // Do loads Runs returning an error per each Run. 141 // 142 // If Run doesn't exist, the corresponding error is datastore.ErrNoSuchEntity or 143 // whatever Checker.After() returned if Checker is given. 144 // 145 // This is useful if you need to collate each loaded Run and its error with 146 // another original slice from which Run's keys or IDs were derived, e.g. an API 147 // request. 148 // 149 // ids := make(common.RunIDs, len(batchReq)) 150 // for i, req := range batchReq { 151 // ids[i] = common.RunID(req.GetRunID()) 152 // } 153 // runs, errs := run.LoadRunsFromIDs(ids...).Checker(acls.NewRunReadChecker()).Do(ctx) 154 // respBatch := ... 155 // for i := range ids { 156 // switch id, r, err := ids[i], runs[i], errs[i];{ 157 // case err != nil: 158 // respBatch[i] = &respOne{Error: ...} 159 // default: 160 // respBatch[i] = &respOne{Run: ...} 161 // } 162 // } 163 func (b LoadRunsBuilder) Do(ctx context.Context) ([]*Run, errors.MultiError) { 164 loadFromDS := func(runs []*Run) errors.MultiError { 165 totalErr := datastore.Get(ctx, runs) 166 if totalErr == nil { 167 return make(errors.MultiError, len(runs)) 168 } 169 errs, ok := totalErr.(errors.MultiError) 170 if !ok { 171 // Assign the same error to each Run we tried to load. 172 totalErr = errors.Annotate(totalErr, "failed to load Runs").Tag(transient.Tag).Err() 173 errs = make(errors.MultiError, len(runs)) 174 for i := range errs { 175 errs[i] = totalErr 176 } 177 return errs 178 } 179 return errs 180 } 181 182 runs := b.prepareRunObjects() 183 if b.checker == nil { 184 // Without checker, can load all the Runs immediately. 185 return runs, loadFromDS(runs) 186 } 187 188 // Call checker.Before() on each Run ID, recording non-nil errors and skipping 189 // such Runs from the list of Runs to load. 190 errs := make(errors.MultiError, len(runs)) 191 entities := make([]*Run, 0, len(runs)) 192 indexes := make([]int, 0, len(runs)) 193 for i, r := range runs { 194 if err := b.checker.Before(ctx, r.ID); err != nil { 195 errs[i] = err 196 } else { 197 entities = append(entities, r) 198 indexes = append(indexes, i) 199 } 200 } 201 202 loadErrs := loadFromDS(entities) 203 for i, err := range loadErrs { 204 switch { 205 case err == nil: 206 err = b.checker.After(ctx, entities[i]) 207 case err == datastore.ErrNoSuchEntity: 208 err = b.checker.After(ctx, nil) 209 } 210 211 if err != nil { 212 idx := indexes[i] 213 errs[idx] = err 214 } 215 } 216 return runs, errs 217 } 218 219 func (b LoadRunsBuilder) prepareRunObjects() []*Run { 220 switch { 221 case len(b.ids) > 0: 222 out := make([]*Run, len(b.ids)) 223 for i, id := range b.ids { 224 out[i] = &Run{ID: id} 225 } 226 return out 227 case len(b.keys) > 0: 228 out := make([]*Run, len(b.keys)) 229 for i, k := range b.keys { 230 out[i] = &Run{ID: common.RunID(k.StringID())} 231 } 232 return out 233 default: 234 return nil 235 } 236 } 237 238 // LoadRunCLs loads `RunCL` entities of the provided cls in the Run. 239 func LoadRunCLs(ctx context.Context, runID common.RunID, clids common.CLIDs) ([]*RunCL, error) { 240 runCLs := make([]*RunCL, len(clids)) 241 runKey := datastore.MakeKey(ctx, common.RunKind, string(runID)) 242 for i, clID := range clids { 243 runCLs[i] = &RunCL{ 244 ID: clID, 245 Run: runKey, 246 } 247 } 248 err := datastore.Get(ctx, runCLs) 249 switch merr, ok := err.(errors.MultiError); { 250 case ok: 251 for i, err := range merr { 252 if err == datastore.ErrNoSuchEntity { 253 return nil, errors.Reason("RunCL %d not found in Datastore", runCLs[i].ID).Err() 254 } 255 } 256 count, err := merr.Summary() 257 return nil, errors.Annotate(err, "failed to load %d out of %d RunCLs", count, len(runCLs)).Tag(transient.Tag).Err() 258 case err != nil: 259 return nil, errors.Annotate(err, "failed to load %d RunCLs", len(runCLs)).Tag(transient.Tag).Err() 260 } 261 return runCLs, nil 262 } 263 264 // LoadRunLogEntries loads all log entries of a given Run. 265 // 266 // Ordered from logically oldest to newest. 267 func LoadRunLogEntries(ctx context.Context, runID common.RunID) ([]*LogEntry, error) { 268 // Since RunLog entities are immutable, it's cheapest to load them from 269 // DS cache. So, perform KeysOnly query first, which is cheap & fast, and then 270 // additional multi-Get which will go via DS cache. 271 272 var keys []*datastore.Key 273 runKey := datastore.MakeKey(ctx, common.RunKind, string(runID)) 274 q := datastore.NewQuery(RunLogKind).KeysOnly(true).Ancestor(runKey) 275 if err := datastore.GetAll(ctx, q, &keys); err != nil { 276 return nil, errors.Annotate(err, "failed to fetch keys of RunLog entities").Tag(transient.Tag).Err() 277 } 278 279 entities := make([]*RunLog, len(keys)) 280 for i, key := range keys { 281 entities[i] = &RunLog{ 282 Run: runKey, 283 ID: key.IntID(), 284 } 285 } 286 if err := datastore.Get(ctx, entities); err != nil { 287 // It's possible to get EntityNotExists, it may only happen if data 288 // retention enforcement is deleting old entities at the same time. 289 // Thus, treat all errors as transient. 290 return nil, errors.Annotate(common.MostSevereError(err), "failed to fetch RunLog entities").Tag(transient.Tag).Err() 291 } 292 293 // Each RunLog entity contains at least 1 LogEntry. 294 out := make([]*LogEntry, 0, len(entities)) 295 for _, e := range entities { 296 out = append(out, e.Entries.GetEntries()...) 297 } 298 return out, nil 299 } 300 301 // LoadChildRuns loads all Runs with the given Run in their dep_runs. 302 func LoadChildRuns(ctx context.Context, runID common.RunID) ([]*Run, error) { 303 q := datastore.NewQuery(common.RunKind).Eq("DepRuns", runID) 304 var runs []*Run 305 if err := datastore.GetAll(ctx, q, &runs); err != nil { 306 return nil, errors.Annotate(err, "failed to fetch dependency Run entities").Tag(transient.Tag).Err() 307 } 308 return runs, nil 309 } 310 311 type nullRunChecker struct{} 312 313 func (n nullRunChecker) Before(ctx context.Context, id common.RunID) error { return nil } 314 func (n nullRunChecker) After(ctx context.Context, runIfFound *Run) error { return nil }