go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/cron/machine.go (about) 1 // Copyright 2017 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 cron 16 17 import ( 18 "fmt" 19 "time" 20 21 "go.chromium.org/luci/scheduler/appengine/schedule" 22 ) 23 24 // State stores serializable state of the cron machine. 25 // 26 // Whoever hosts the cron machine is supposed to store this state in some 27 // persistent store between events. It's mutated by Machine. So the usage 28 // pattern is: 29 // - Deserialize State, construct Machine instance with it. 30 // - Invoke some Machine method (e.g. Enable()) to advance the state. 31 // - Acknowledge all actions emitted by the machine (see Machine.Actions). 32 // - Serialize the mutated state (available in Machine.State). 33 // 34 // If appropriate, all of the above should be done in a transaction. 35 // 36 // Machine assumes that whoever hosts it handles TickLaterAction with following 37 // semantics: 38 // - A scheduled tick can't be "unscheduled". 39 // - A scheduled tick may come more than one time. 40 // 41 // So the machine just ignores ticks it doesn't expect. 42 // 43 // It supports "absolute" and "relative" schedules, see 'schedule' package for 44 // definitions. 45 type State struct { 46 // Enabled is true if the cron machine is running. 47 // 48 // A disabled cron machine ignores all events except 'Enable'. 49 Enabled bool 50 51 // Generation is increased each time state mutates. 52 // 53 // Monotonic, never resets. Should not be assumed sequential: some calls 54 // mutate the state multiple times during one transition. 55 // 56 // Used to deduplicate StartInvocationAction in case of retries of cron state 57 // transitions. 58 Generation int64 59 60 // LastRewind is a time when the cron machine was restarted last time. 61 // 62 // For relative schedules, it's a time RewindIfNecessary() was called. For 63 // absolute schedules it is last time invocation happened (cron machines on 64 // absolute schedules auto-rewind themselves). 65 LastRewind time.Time 66 67 // LastTick is last emitted tick request (or empty struct). 68 // 69 // It may be scheduled for "distant future" for paused cron machines. 70 LastTick TickLaterAction 71 } 72 73 // Equal reports whether two structs are equal. 74 func (s *State) Equal(o *State) bool { 75 return s.Enabled == o.Enabled && 76 s.Generation == o.Generation && 77 s.LastRewind.Equal(o.LastRewind) && 78 s.LastTick.Equal(&o.LastTick) 79 } 80 81 // IsSuspended returns true if the cron machine is not waiting for a tick. 82 // 83 // This happens for paused cron machines (they technically are scheduled for 84 // a tick in a distant future) and for cron machines on relative schedule that 85 // wait for 'RewindIfNecessary' to be called to start ticking again. 86 // 87 // A disabled cron machine is also considered suspended. 88 func (s *State) IsSuspended() bool { 89 return !s.Enabled || s.LastTick.When.IsZero() || s.LastTick.When == schedule.DistantFuture 90 } 91 92 //////////////////////////////////////////////////////////////////////////////// 93 94 // Action is a particular action to perform when switching the state. 95 // 96 // Can be type cast to some concrete *Action struct. Intended to be handled by 97 // whoever hosts the cron machine. 98 type Action interface { 99 IsAction() bool 100 } 101 102 // TickLaterAction schedules an OnTimerTick call at given moment in time. 103 // 104 // TickNonce is used by cron machine to skip canceled or repeated ticks. 105 type TickLaterAction struct { 106 When time.Time 107 TickNonce int64 108 } 109 110 // Equal reports whether two structs are equal. 111 func (a *TickLaterAction) Equal(o *TickLaterAction) bool { 112 return a.TickNonce == o.TickNonce && a.When.Equal(o.When) 113 } 114 115 // IsAction makes TickLaterAction implement Action interface. 116 func (a TickLaterAction) IsAction() bool { return true } 117 118 // StartInvocationAction is emitted when the scheduled moment comes. 119 // 120 // A handler is expected to call RewindIfNecessary() at some later time to 121 // restart the cron machine if it's running on a relative schedule (e.g. "with 122 // 10 sec interval"). Cron machines on relative schedules are "one shot". They 123 // need to be rewound to start counting time again. 124 // 125 // Cron machines on absolute schedules (regular crons, like "at 12 AM every 126 // day") don't need rewinding, they'll start counting time until next invocation 127 // automatically. Calling RewindIfNecessary() for them won't hurt though, it 128 // will be noop. 129 type StartInvocationAction struct { 130 Generation int64 // value of state.Generation when the action was emitted 131 } 132 133 // IsAction makes StartInvocationAction implement Action interface. 134 func (a StartInvocationAction) IsAction() bool { return true } 135 136 //////////////////////////////////////////////////////////////////////////////// 137 138 // Machine advances the state of the cron machine. 139 // 140 // It gracefully handles various kinds of external events (like pauses and 141 // schedule changes) and emits actions that's supposed to handled by whoever 142 // hosts it. 143 type Machine struct { 144 // Inputs. 145 Now time.Time // current time 146 Schedule *schedule.Schedule // knows when to emit invocation action 147 Nonce func() int64 // produces nonces on demand 148 149 // Mutated. 150 State State // state of the cron machine, mutated by its methods 151 Actions []Action // all emitted actions (if any) 152 } 153 154 // Enable makes the cron machine start counting time. 155 // 156 // Does nothing if already enabled. 157 func (m *Machine) Enable() { 158 if !m.State.Enabled { 159 m.State = State{ 160 Enabled: true, 161 Generation: m.nextGen(), 162 LastRewind: m.Now, 163 } 164 m.scheduleTick(m.Now) 165 } 166 } 167 168 // Disable stops any pending timer ticks, resets state. 169 // 170 // The cron machine will ignore any events until Enable is called to turn it on. 171 func (m *Machine) Disable() { 172 m.State = State{Enabled: false, Generation: m.nextGen()} 173 } 174 175 // RewindIfNecessary is called to restart the cron after it has fired the 176 // invocation action. 177 // 178 // Does nothing if the cron is disabled or already ticking. 179 func (m *Machine) RewindIfNecessary() { 180 m.rewindIfNecessary(m.Now) 181 } 182 183 // OnScheduleChange happens when cron's schedule changes. 184 // 185 // In particular, it handles switches between absolute and relative schedules. 186 func (m *Machine) OnScheduleChange() { 187 // Do not touch timers on disabled cron machines. 188 if !m.State.Enabled { 189 return 190 } 191 192 // The following condition is true for cron machines on a relative schedule 193 // that have already "fired", and currently wait for manual RewindIfNecessary 194 // call to start ticking again. When such cron machines switch to an absolute 195 // schedule, we need to rewind them right away (since machines on absolute 196 // schedules always tick!). If the new schedule is also relative, do nothing: 197 // RewindIfNecessary() should be called manually by the host at some later 198 // time (as usual for relative schedules). 199 if m.State.LastTick.When.IsZero() { 200 if m.Schedule.IsAbsolute() { 201 m.RewindIfNecessary() 202 } 203 } else { 204 // In this branch, the cron machine has a timer tick scheduled. It means it 205 // is either in a relative or absolute schedule, and this schedule may have 206 // changed, so we may need to move the tick to reflect the change. Note that 207 // we are not resetting LastRewind here, since we want the new schedule to 208 // take into account real last RewindIfNecessary call. For example, if the 209 // last rewind happened at moment X, current time is Now, and the new 210 // schedule is "with 10s interval", we want the tick to happen at "X+10", 211 // not "Now+10". 212 m.scheduleTick(m.Now) 213 } 214 } 215 216 // OnTimerTick happens when a scheduled timer tick (added with TickLaterAction) 217 // occurs. 218 // 219 // Returns an error if the tick happened too soon. 220 func (m *Machine) OnTimerTick(tickNonce int64) error { 221 // Silently skip unexpected, late or canceled ticks. This is fine. 222 switch { 223 case m.State.IsSuspended(): 224 return nil 225 case m.State.LastTick.TickNonce != tickNonce: 226 return nil 227 } 228 229 // For ticks in the future more than Task Queue limit, OnTimerTick will be 230 // called much earlier than the actual tick, so emit a new TickLaterAction 231 // with a new Nonce. 232 // 233 // Report error (to trigger a retry) if the tick happened unexpectedly soon. 234 // Allow up to 50ms clock drift, but correct it to be 0. This is important for 235 // getting the correct next tick time from an absolute schedule. If we pass 236 // m.Now to the cron schedule uncorrected, we'll just get the time of the 237 // already scheduled tick (the one we are handling now, since uncorrected 238 // m.Now is before it). 239 // 240 // Note that m.Now is part of inputs and must not be mutated. We propagate 241 // the corrected time via the call stack. 242 now := m.Now 243 switch remaining := m.State.LastTick.When.Sub(now); { 244 case remaining > time.Second: 245 m.State.Generation = m.nextGen() 246 m.State.LastTick.TickNonce = m.Nonce() 247 m.Actions = append(m.Actions, m.State.LastTick) 248 return nil 249 case remaining > 50*time.Millisecond: 250 return fmt.Errorf("tick happened %s before it was expected", remaining) 251 case remaining > 0: 252 now = m.State.LastTick.When 253 } 254 255 // The scheduled time has come! 256 m.State.Generation = m.nextGen() 257 m.Actions = append(m.Actions, StartInvocationAction{ 258 Generation: m.State.Generation, 259 }) 260 m.State.LastTick = TickLaterAction{} 261 262 // Start waiting for a new tick right away if on an absolute schedule or just 263 // keep the tick state clear for relative schedules: new tick will be set when 264 // RewindIfNecessary() is manually called by whoever handles the cron. 265 if m.Schedule.IsAbsolute() { 266 m.rewindIfNecessary(now) 267 } 268 269 return nil 270 } 271 272 // scheduleTick emits TickLaterAction action according to the schedule, current 273 // time, and last time RewindIfNecessary was called. 274 // 275 // Does nothing if such tick has already been scheduled. 276 func (m *Machine) scheduleTick(now time.Time) { 277 nextTickTime := m.Schedule.Next(now, m.State.LastRewind) 278 if nextTickTime != m.State.LastTick.When { 279 m.State.Generation = m.nextGen() 280 m.State.LastTick = TickLaterAction{ 281 When: nextTickTime, 282 TickNonce: m.Nonce(), 283 } 284 if nextTickTime != schedule.DistantFuture { 285 m.Actions = append(m.Actions, m.State.LastTick) 286 } 287 } 288 } 289 290 // nextGen returns the next generation number. 291 // 292 // It does NOT update Generation in-place, just produces the next number. 293 func (m *Machine) nextGen() int64 { 294 return m.State.Generation + 1 295 } 296 297 // rewindIfNecessary implements RewindIfNecessary, accepting corrected 'now'. 298 // 299 // This is important for OnTimerTick. Note that m.Now is part of inputs and must 300 // not be mutated. 301 func (m *Machine) rewindIfNecessary(now time.Time) { 302 if m.State.Enabled && m.State.LastTick.When.IsZero() { 303 m.State.LastRewind = now 304 m.State.Generation = m.nextGen() 305 m.scheduleTick(now) 306 } 307 }