Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 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 "github.com/luci/luci-go/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 // LastRewind is a time when the cron machine was restarted last time. | |
| 52 // | |
| 53 // For relative schedules, it's a time RewindIfNecessary() was called. F or | |
| 54 // absolute schedules it is last time invocation happened (cron machines on | |
| 55 // absolute schedules auto-rewind themselves). | |
| 56 LastRewind time.Time | |
| 57 | |
| 58 // LastTick is last emitted tick request (or empty struct). | |
| 59 // | |
| 60 // It may be scheduled for "distant future" for paused cron machines. | |
| 61 LastTick TickLaterAction | |
| 62 } | |
| 63 | |
| 64 // IsSuspended returns true if the cron machine is not waiting for a tick. | |
| 65 // | |
| 66 // This happens for paused cron machines (they technically are scheduled for | |
| 67 // a tick in a distant future) and for cron machines on relative schedule that | |
| 68 // wait for 'RewindIfNecessary' to be called to start ticking again. | |
| 69 // | |
| 70 // A disabled cron machine is also considered suspended. | |
| 71 func (s *State) IsSuspended() bool { | |
| 72 return !s.Enabled || s.LastTick.When.IsZero() || s.LastTick.When == sche dule.DistantFuture | |
| 73 } | |
| 74 | |
| 75 //////////////////////////////////////////////////////////////////////////////// | |
| 76 | |
| 77 // Action is a particular action to perform when switching the state. | |
| 78 // | |
| 79 // Can be type cast to some concrete *Action struct. Intended to be handled by | |
| 80 // whoever hosts the cron machine. | |
| 81 type Action interface { | |
| 82 IsAction() bool | |
| 83 } | |
| 84 | |
| 85 // TickLaterAction schedules an OnTimerTick call at given moment in time. | |
| 86 // | |
| 87 // TickNonce is used by cron machine to skip canceled or repeated ticks. | |
| 88 type TickLaterAction struct { | |
| 89 When time.Time | |
| 90 TickNonce int64 | |
| 91 } | |
| 92 | |
| 93 // IsAction makes TickLaterAction implement Action interface. | |
| 94 func (a TickLaterAction) IsAction() bool { return true } | |
| 95 | |
| 96 // StartInvocationAction is emitted when the scheduled moment comes. | |
| 97 // | |
| 98 // A handler is expected to call RewindIfNecessary() at some later time to | |
| 99 // restart the cron machine if it's running on a relative schedule (e.g. "with | |
| 100 // 10 sec interval"). Cron machines on relative schedules are "one shot". They | |
| 101 // need to be rewound to start counting time again. | |
| 102 // | |
| 103 // Cron machines on absolute schedules (regular crons, like "at 12 AM every | |
| 104 // day") don't need rewinding, they'll start counting time until next invocation | |
| 105 // automatically. | |
| 106 type StartInvocationAction struct{} | |
| 107 | |
| 108 // IsAction makes StartInvocationAction implement Action interface. | |
| 109 func (a StartInvocationAction) IsAction() bool { return true } | |
| 110 | |
| 111 //////////////////////////////////////////////////////////////////////////////// | |
| 112 | |
| 113 // Machine advances the state of the cron machine. | |
| 114 // | |
| 115 // It gracefully handles various kinds of external events (like pauses and | |
| 116 // schedule changes) and emits actions that's supposed to handled by whoever | |
| 117 // hosts it. | |
| 118 type Machine struct { | |
| 119 // Inputs. | |
| 120 Now time.Time // current time | |
| 121 Schedule *schedule.Schedule // knows when to emit invocation action | |
| 122 Nonce func() int64 // produces nonces on demand | |
| 123 | |
| 124 // Mutated. | |
| 125 State State // state of the cron machine, mutated by its methods | |
| 126 Actions []Action // all emitted actions (if any) | |
| 127 } | |
| 128 | |
| 129 // Enable makes the cron machine start counting time. | |
| 130 // | |
| 131 // Does nothing if already enabled. | |
| 132 func (m *Machine) Enable() { | |
| 133 if !m.State.Enabled { | |
| 134 m.State = State{Enabled: true, LastRewind: m.Now} // reset state | |
| 135 m.scheduleTick() | |
| 136 } | |
| 137 } | |
| 138 | |
| 139 // Disable stops any pending timer ticks, resets state. | |
| 140 // | |
| 141 // The cron machine will ignore any events until Enable is called to turn it on. | |
| 142 func (m *Machine) Disable() { | |
| 143 m.State = State{Enabled: false} | |
| 144 } | |
| 145 | |
| 146 // RewindIfNecessary is called to restart the cron after it has fired the | |
| 147 // invocation action. | |
| 148 // | |
| 149 // Does nothing if the cron is disabled or already ticking. | |
| 150 func (m *Machine) RewindIfNecessary() { | |
| 151 if m.State.Enabled && m.State.LastTick.When.IsZero() { | |
| 152 m.State.LastRewind = m.Now | |
| 153 m.scheduleTick() | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 // OnScheduleChange happens when cron's schedule changes. | |
| 158 // | |
| 159 // In particular, it handles switches between absolute and relative schedules. | |
| 160 func (m *Machine) OnScheduleChange() { | |
| 161 // Do not touch timers on disabled cron machines. | |
| 162 if !m.State.Enabled { | |
| 163 return | |
| 164 } | |
| 165 | |
| 166 // The following condition is true for cron machines on a relative sched ule | |
| 167 // that have already "fired", and currently wait for manual RewindIfNece ssary | |
| 168 // call to start ticking again. When such cron machines switch to an abs olute | |
| 169 // schedule, we need to rewind them right away (since machines on absolu te | |
| 170 // schedules always tick!). If the new schedule is also relative, do not hing: | |
| 171 // RewindIfNecessary() should be called manually by the host at some lat er | |
| 172 // time (as usual for relative schedules). | |
| 173 if m.State.LastTick.When.IsZero() { | |
| 174 if m.Schedule.IsAbsolute() { | |
| 175 m.RewindIfNecessary() | |
| 176 } | |
| 177 } else { | |
| 178 // In this branch, the cron machine has a timer tick scheduled. It means it | |
| 179 // is either in a relative or absolute schedule, and this schedu le may have | |
| 180 // changed, so we may need to move the tick to reflect the chang e. Note that | |
| 181 // we are not resetting LastRewind here, since we want the new s chedule to | |
| 182 // take into account real last RewindIfNecessary call. For examp le, if the | |
| 183 // last rewind happened at moment X, current time is Now, and th e new | |
| 184 // schedule is "with 10s interval", we want the tick to happen a t "X+10", | |
| 185 // not "Now+10". | |
| 186 m.scheduleTick() | |
| 187 } | |
| 188 } | |
| 189 | |
| 190 // OnTimerTick happens when a scheduled timer tick (added with TickLaterAction) | |
| 191 // occurs. | |
| 192 // | |
| 193 // Returns an error if the tick happened too soon. | |
| 194 func (m *Machine) OnTimerTick(tickNonce int64) error { | |
| 195 // Silently skip unexpected, late or canceled ticks. This is fine. | |
| 196 switch { | |
| 197 case m.State.IsSuspended(): | |
| 198 return nil | |
| 199 case m.State.LastTick.TickNonce != tickNonce: | |
| 200 return nil | |
| 201 } | |
| 202 | |
| 203 // Report error (to trigger a retry) if the tick happened unexpectedly s oon. | |
| 204 // Absolute schedules may report "wrong" next tick time if asked for a n ext | |
| 205 // tick before previous one has happened. | |
| 206 delay := m.Now.Sub(m.State.LastTick.When) | |
| 207 if delay < 0 { | |
|
tandrii(chromium)
2017/07/14 18:17:02
nit: combine two lines?
Vadim Sh.
2017/07/14 18:29:12
Ack, will do.
Vadim Sh.
2017/07/14 21:57:21
Done.
| |
| 208 return fmt.Errorf("tick happened %.1f sec before it was expected ", -delay.Seconds()) | |
| 209 } | |
| 210 | |
| 211 // The scheduled time has come! | |
| 212 m.Actions = append(m.Actions, StartInvocationAction{}) | |
| 213 m.State.LastTick = TickLaterAction{} | |
| 214 | |
| 215 // Start waiting for a new tick right away if on an absolute schedule or just | |
| 216 // keep the tick state clear for relative schedules: new tick will be se t when | |
| 217 // RewindIfNecessary() is manually called by whoever handles the cron. | |
| 218 if m.Schedule.IsAbsolute() { | |
| 219 m.RewindIfNecessary() | |
| 220 } | |
| 221 | |
| 222 return nil | |
| 223 } | |
| 224 | |
| 225 // scheduleTick emits TickLaterAction action according to the schedule, current | |
| 226 // time, and last time Rewind was called. | |
| 227 // | |
| 228 // Does nothing if such tick has already been scheduled. | |
| 229 func (m *Machine) scheduleTick() { | |
| 230 nextTickTime := m.Schedule.Next(m.Now, m.State.LastRewind) | |
| 231 if nextTickTime != m.State.LastTick.When { | |
| 232 m.State.LastTick = TickLaterAction{ | |
| 233 When: nextTickTime, | |
| 234 TickNonce: m.Nonce(), | |
| 235 } | |
| 236 if nextTickTime != schedule.DistantFuture { | |
|
tandrii(chromium)
2017/07/14 18:17:02
is this actually necessary for a cron job? AFAIR,
Vadim Sh.
2017/07/14 18:29:12
Maybe not... I'm not sure yet. I'm keeping it so f
tandrii(chromium)
2017/07/14 18:36:08
oops, that was my cache miss. Thanks for reminder.
| |
| 237 m.Actions = append(m.Actions, m.State.LastTick) | |
| 238 } | |
| 239 } | |
| 240 } | |
| OLD | NEW |