go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/policy/common.go (about)

     1  // Copyright 2018 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 policy
    16  
    17  import (
    18  	"fmt"
    19  
    20  	"go.chromium.org/luci/auth/identity"
    21  	"go.chromium.org/luci/common/data/stringset"
    22  	"go.chromium.org/luci/common/errors"
    23  	"go.chromium.org/luci/scheduler/appengine/internal"
    24  )
    25  
    26  // basePolicy returns a generic policy customizable by the `reduce` func.
    27  //
    28  // Parameters `maxConcurrentInvs` and `maxBatchSize` control how many
    29  // invocations can be running in parallel and maximum number of triggers that
    30  // can be collapsed into a single invocation correspondingly. The `reducer` func
    31  // allows to customize policy: it takes a list of outstanding triggers and must
    32  // return a number of triggers to be collapsed. The returned number must be >=
    33  // 0. The created invocation will collapse the specified number of triggers and
    34  // take the properties of the latest one. If the returned number is 0, no
    35  // invocation is created. If it is larger than `maxBatchSize`, the value is
    36  // ignored and invocation collapsing `maxBatchSize` triggers is created.
    37  func basePolicy(maxConcurrentInvs, maxBatchSize int, reducer func([]*internal.Trigger) int) (Func, error) {
    38  	switch {
    39  	case maxConcurrentInvs <= 0:
    40  		return nil, errors.Reason("max_concurrent_invocations should be positive").Err()
    41  	case maxBatchSize <= 0:
    42  		return nil, errors.Reason("max_batch_size should be positive").Err()
    43  	}
    44  
    45  	return func(env Environment, in In) (out Out) {
    46  		slots := maxConcurrentInvs - len(in.ActiveInvocations)
    47  		switch {
    48  		case len(in.Triggers) == 0:
    49  			return // nothing new to launch
    50  		case slots <= 0:
    51  			env.DebugLog(
    52  				"Max concurrent invocations is %d and there's %d running => refusing to launch more",
    53  				maxConcurrentInvs, len(in.ActiveInvocations))
    54  			return // maxed all available slots
    55  		}
    56  
    57  		triggers := in.Triggers
    58  		for slots > 0 && len(triggers) != 0 {
    59  			// Grab up to maxBatchSize triggers.
    60  			size := maxBatchSize
    61  			numTriggers := reducer(triggers)
    62  			if numTriggers == 0 {
    63  				break
    64  			}
    65  			if numTriggers > len(triggers) {
    66  				panic(fmt.Errorf("invalid policy implementation: returned %d which is more than %d triggers given", numTriggers, len(triggers)))
    67  			}
    68  			if size > numTriggers {
    69  				size = numTriggers
    70  			}
    71  			batch := triggers[:size]
    72  			triggers = triggers[size:]
    73  
    74  			// Put them into the new invocation, deriving its properties from the most
    75  			// recent trigger (which is last in the list, since triggers are sorted by
    76  			// time already)
    77  			req := RequestBuilder{env: env}
    78  			req.FromTrigger(batch[len(batch)-1])
    79  			req.IncomingTriggers = batch
    80  
    81  			// If all triggers were submitted by the same single identity (perhaps the
    82  			// scheduler service itself as indicated by empty EmittedByUser),
    83  			// attribute the invocation to this identity. This is mostly for UI,
    84  			// nothing really depends on TriggeredBy field.
    85  			idents := stringset.New(1)
    86  			for _, t := range batch {
    87  				idents.Add(t.EmittedByUser)
    88  			}
    89  			if idents.Len() == 1 {
    90  				req.TriggeredBy = identity.Identity(idents.ToSlice()[0])
    91  			}
    92  
    93  			out.Requests = append(out.Requests, req.Request)
    94  			slots--
    95  		}
    96  
    97  		return
    98  	}, nil
    99  }