go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/policy/simulator.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 "container/heap" 19 "time" 20 21 "github.com/golang/protobuf/proto" 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 "go.chromium.org/luci/common/data/stringset" 25 26 "go.chromium.org/luci/scheduler/appengine/internal" 27 "go.chromium.org/luci/scheduler/appengine/task" 28 ) 29 30 // Simulator is used to test policies. 31 // 32 // It simulates the scheduler engine logic and passage of time. It takes a 33 // stream of triggers as input, passes them through the policy under the test, 34 // and collects the resulting invocation requests. 35 type Simulator struct { 36 // Policy is the policy function under test. 37 // 38 // Must be set by the caller. 39 Policy Func 40 41 // OnRequest is called whenever a new invocation request is emitted by the 42 // policy. 43 // 44 // It decides for how long the invocation will run. 45 // 46 // Must be set by the caller. 47 OnRequest func(s *Simulator, r task.Request) time.Duration 48 49 // OnDebugLog is called whenever the triggering policy logs something. 50 // 51 // May be set by the caller to collect the policy logs. 52 OnDebugLog func(format string, args ...any) 53 54 // Epoch is the timestamp of when the simulation started. 55 // 56 // Used to calculate SimulatedInvocation.Created. It is fine to leave it 57 // default if you aren't looking at absolute times (which will be weird with 58 // zero epoch time). 59 Epoch time.Time 60 61 // Now is the current time inside the simulation. 62 // 63 // It is advanced on various events (like new triggers or finishing 64 // invocations). Use AdvanceTime to move it manually. 65 Now time.Time 66 67 // PendingTriggers is a set of currently pending triggers, sorted by time 68 // (most recent last). 69 // 70 // Do not modify this list directly, use AddTrigger instead. 71 PendingTriggers []*internal.Trigger 72 73 // Invocations is a log of all produced invocations. 74 // 75 // They are ordered by the creation time. Contains invocations that are still 76 // running (based on Now). Use Last() as a shortcut to get the last item of 77 // this list. 78 Invocations []*SimulatedInvocation 79 80 // DiscardedTriggers is a log of all triggers that were discarded, sorted by time 81 // (most recent last). 82 DiscardedTriggers []*internal.Trigger 83 84 // Internals. 85 86 // events is a priority queue (heap) of future events. 87 events events 88 // seenTriggers is a set of IDs of all triggers ever seen, for deduplication. 89 seenTriggers stringset.Set 90 // nextInvID is used by handleRequest. 91 nextInvID int64 92 // invIDs is a set of running invocations. 93 invIDs map[int64]*SimulatedInvocation 94 } 95 96 // SimulatedInvocation contains details of an invocation. 97 type SimulatedInvocation struct { 98 // Request is the original invocation request as emitted by the policy. 99 Request task.Request 100 // Created is when the invocation was created, relative to the epoch. 101 Created time.Duration 102 // Duration of the invocation, as returned by OnRequest. 103 Duration time.Duration 104 // Running is true if the invocation is still running. 105 Running bool 106 } 107 108 // SimulatedEnvironment implements Environment interface for use by Simulator. 109 type SimulatedEnvironment struct { 110 OnDebugLog func(format string, args ...any) 111 } 112 113 // DebugLog is part of Environment interface. 114 func (s *SimulatedEnvironment) DebugLog(format string, args ...any) { 115 if s.OnDebugLog != nil { 116 s.OnDebugLog(format, args...) 117 } 118 } 119 120 //////////////////////////////////////////////////////////////////////////////// 121 // Triggering and invocations. 122 123 // Last returns the last invocation in Invocations list or nil if its empty. 124 func (s *Simulator) Last() *SimulatedInvocation { 125 if len(s.Invocations) == 0 { 126 return nil 127 } 128 return s.Invocations[len(s.Invocations)-1] 129 } 130 131 // AddTrigger submits a trigger (one or many) to the pending trigger set. 132 // 133 // This causes the execution of the policy function to decide what to do with 134 // the new triggers. 135 // 136 // 'delay' is time interval from the previously submitted trigger. It is used to 137 // advance time. The current simulation time will be used to populate trigger's 138 // Created field. 139 func (s *Simulator) AddTrigger(delay time.Duration, t ...internal.Trigger) { 140 s.AdvanceTime(delay) 141 142 if s.seenTriggers == nil { 143 s.seenTriggers = stringset.New(0) 144 } 145 146 ts := timestamppb.New(s.Now) 147 for _, tr := range t { 148 tr := proto.Clone(&tr).(*internal.Trigger) 149 tr.Created = ts 150 if s.seenTriggers.Add(tr.Id) { 151 s.PendingTriggers = append(s.PendingTriggers, tr) 152 } 153 } 154 155 s.triage() 156 } 157 158 // triage executes the triggering policy function. 159 func (s *Simulator) triage() { 160 // Collect the unordered list of currently running invocations. 161 invs := make([]int64, 0, len(s.invIDs)) 162 for id := range s.invIDs { 163 invs = append(invs, id) 164 } 165 166 // Clone pending triggers list since we don't want the policy to mutate them. 167 triggers := make([]*internal.Trigger, len(s.PendingTriggers)) 168 for i, t := range s.PendingTriggers { 169 triggers[i] = proto.Clone(t).(*internal.Trigger) 170 } 171 172 // Execute the policy function, collecting its log. 173 out := s.Policy(&SimulatedEnvironment{s.OnDebugLog}, In{ 174 Now: s.Now, 175 ActiveInvocations: invs, 176 Triggers: triggers, 177 }) 178 179 // Instantiate all new invocations and collect a set of consumed triggers. 180 consumed := stringset.New(0) 181 for _, r := range out.Requests { 182 s.handleRequest(r) 183 for _, t := range r.IncomingTriggers { 184 consumed.Add(t.Id) 185 } 186 } 187 188 // Collect a set of discarded triggers. 189 discarded := stringset.New(0) 190 for _, t := range out.Discard { 191 discarded.Add(t.Id) 192 } 193 194 // Pop all consumed or discarded triggers from PendingTriggers list (keeping it sorted). 195 if consumed.Len() != 0 || discarded.Len() != 0 { 196 filtered := make([]*internal.Trigger, 0, len(s.PendingTriggers)) 197 for _, t := range s.PendingTriggers { 198 if !consumed.Has(t.Id) && !discarded.Has(t.Id) { 199 filtered = append(filtered, t) 200 } 201 if discarded.Has(t.Id) { 202 s.DiscardedTriggers = append(s.DiscardedTriggers, t) 203 } 204 } 205 s.PendingTriggers = filtered 206 } 207 } 208 209 // handleRequest is called for each invocation request created by the policy. 210 // 211 // It adds new SimulatedInvocation to Invocations list. 212 func (s *Simulator) handleRequest(r task.Request) { 213 dur := s.OnRequest(s, r) 214 if dur <= 0 { 215 panic("the invocation duration should be positive") 216 } 217 218 inv := &SimulatedInvocation{ 219 Request: r, 220 Created: s.Now.Sub(s.Epoch), 221 Duration: dur, 222 Running: true, 223 } 224 s.Invocations = append(s.Invocations, inv) 225 226 s.nextInvID++ 227 id := s.nextInvID 228 if s.invIDs == nil { 229 s.invIDs = map[int64]*SimulatedInvocation{} 230 } 231 s.invIDs[id] = inv 232 233 s.scheduleEvent(event{ 234 eta: s.Now.Add(inv.Duration), 235 cb: func() { 236 // On invocation completion, kick it from the active invocations set and 237 // rerun the triggering policy function to decide what to do next. 238 inv.Running = false 239 delete(s.invIDs, id) 240 s.triage() 241 }, 242 }) 243 } 244 245 //////////////////////////////////////////////////////////////////////////////// 246 // Event reactor. 247 248 // event sits in a timeline and its callback is executed at moment 'eta'. 249 type event struct { 250 eta time.Time 251 cb func() 252 } 253 254 // events implements heap.Interface, smallest eta is on top of the heap. 255 type events []event 256 257 func (e events) Len() int { return len(e) } 258 func (e events) Less(i, j int) bool { return e[i].eta.Before(e[j].eta) } 259 func (e events) Swap(i, j int) { e[i], e[j] = e[j], e[i] } 260 func (e *events) Push(x any) { *e = append(*e, x.(event)) } 261 func (e *events) Pop() any { 262 old := *e 263 n := len(old) 264 x := old[n-1] 265 *e = old[0 : n-1] 266 return x 267 } 268 269 // AdvanceTime moves the simulated time, executing all events that happen. 270 func (s *Simulator) AdvanceTime(d time.Duration) { 271 switch { 272 case d == 0: 273 return 274 case d < 0: 275 panic("time must move forward only") 276 } 277 278 // First tick ever? Reset Now to Epoch, since Epoch is our beginning of times. 279 if s.Now.IsZero() { 280 s.Now = s.Epoch 281 } 282 283 deadline := s.Now.Add(d) 284 for { 285 // Nothing is happening at all or events happen later than we wish to go? 286 if ev := s.peekEvent(); ev == nil || ev.eta.After(deadline) { 287 s.Now = deadline 288 return 289 } 290 291 // Advance the time to the point when the event is happening and execute the 292 // event's callback. It may result in most stuff added to the timeline which 293 // we will discover on the next iteration of the loop. 294 ev := s.popEvent() 295 s.Now = ev.eta 296 ev.cb() 297 } 298 } 299 300 // scheduleEvent adds an event to the event queue. 301 // 302 // Panics if event's ETA is not in the future. 303 func (s *Simulator) scheduleEvent(e event) { 304 if !e.eta.After(s.Now) { 305 panic("event's ETA should be in the future") 306 } 307 heap.Push(&s.events, e) 308 } 309 310 // peekEvent peeks at the event that happens next. 311 // 312 // Returns nil if there are no pending events. 313 func (s *Simulator) peekEvent() *event { 314 if len(s.events) == 0 { 315 return nil 316 } 317 return &s.events[0] 318 } 319 320 // popEvent removes the event that happens next. 321 // 322 // Panics if there's no pending events. 323 func (s *Simulator) popEvent() event { 324 if len(s.events) == 0 { 325 panic("no events to pop") 326 } 327 return heap.Pop(&s.events).(event) 328 }