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 }