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 }