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. Calling RewindIfNecessary() for them won't hurt though, it |
| 106 // will be noop. |
| 107 type StartInvocationAction struct{} |
| 108 |
| 109 // IsAction makes StartInvocationAction implement Action interface. |
| 110 func (a StartInvocationAction) IsAction() bool { return true } |
| 111 |
| 112 //////////////////////////////////////////////////////////////////////////////// |
| 113 |
| 114 // Machine advances the state of the cron machine. |
| 115 // |
| 116 // It gracefully handles various kinds of external events (like pauses and |
| 117 // schedule changes) and emits actions that's supposed to handled by whoever |
| 118 // hosts it. |
| 119 type Machine struct { |
| 120 // Inputs. |
| 121 Now time.Time // current time |
| 122 Schedule *schedule.Schedule // knows when to emit invocation action |
| 123 Nonce func() int64 // produces nonces on demand |
| 124 |
| 125 // Mutated. |
| 126 State State // state of the cron machine, mutated by its methods |
| 127 Actions []Action // all emitted actions (if any) |
| 128 } |
| 129 |
| 130 // Enable makes the cron machine start counting time. |
| 131 // |
| 132 // Does nothing if already enabled. |
| 133 func (m *Machine) Enable() { |
| 134 if !m.State.Enabled { |
| 135 m.State = State{Enabled: true, LastRewind: m.Now} // reset state |
| 136 m.scheduleTick() |
| 137 } |
| 138 } |
| 139 |
| 140 // Disable stops any pending timer ticks, resets state. |
| 141 // |
| 142 // The cron machine will ignore any events until Enable is called to turn it on. |
| 143 func (m *Machine) Disable() { |
| 144 m.State = State{Enabled: false} |
| 145 } |
| 146 |
| 147 // RewindIfNecessary is called to restart the cron after it has fired the |
| 148 // invocation action. |
| 149 // |
| 150 // Does nothing if the cron is disabled or already ticking. |
| 151 func (m *Machine) RewindIfNecessary() { |
| 152 if m.State.Enabled && m.State.LastTick.When.IsZero() { |
| 153 m.State.LastRewind = m.Now |
| 154 m.scheduleTick() |
| 155 } |
| 156 } |
| 157 |
| 158 // OnScheduleChange happens when cron's schedule changes. |
| 159 // |
| 160 // In particular, it handles switches between absolute and relative schedules. |
| 161 func (m *Machine) OnScheduleChange() { |
| 162 // Do not touch timers on disabled cron machines. |
| 163 if !m.State.Enabled { |
| 164 return |
| 165 } |
| 166 |
| 167 // The following condition is true for cron machines on a relative sched
ule |
| 168 // that have already "fired", and currently wait for manual RewindIfNece
ssary |
| 169 // call to start ticking again. When such cron machines switch to an abs
olute |
| 170 // schedule, we need to rewind them right away (since machines on absolu
te |
| 171 // schedules always tick!). If the new schedule is also relative, do not
hing: |
| 172 // RewindIfNecessary() should be called manually by the host at some lat
er |
| 173 // time (as usual for relative schedules). |
| 174 if m.State.LastTick.When.IsZero() { |
| 175 if m.Schedule.IsAbsolute() { |
| 176 m.RewindIfNecessary() |
| 177 } |
| 178 } else { |
| 179 // In this branch, the cron machine has a timer tick scheduled.
It means it |
| 180 // is either in a relative or absolute schedule, and this schedu
le may have |
| 181 // changed, so we may need to move the tick to reflect the chang
e. Note that |
| 182 // we are not resetting LastRewind here, since we want the new s
chedule to |
| 183 // take into account real last RewindIfNecessary call. For examp
le, if the |
| 184 // last rewind happened at moment X, current time is Now, and th
e new |
| 185 // schedule is "with 10s interval", we want the tick to happen a
t "X+10", |
| 186 // not "Now+10". |
| 187 m.scheduleTick() |
| 188 } |
| 189 } |
| 190 |
| 191 // OnTimerTick happens when a scheduled timer tick (added with TickLaterAction) |
| 192 // occurs. |
| 193 // |
| 194 // Returns an error if the tick happened too soon. |
| 195 func (m *Machine) OnTimerTick(tickNonce int64) error { |
| 196 // Silently skip unexpected, late or canceled ticks. This is fine. |
| 197 switch { |
| 198 case m.State.IsSuspended(): |
| 199 return nil |
| 200 case m.State.LastTick.TickNonce != tickNonce: |
| 201 return nil |
| 202 } |
| 203 |
| 204 // Report error (to trigger a retry) if the tick happened unexpectedly s
oon. |
| 205 // Absolute schedules may report "wrong" next tick time if asked for a n
ext |
| 206 // tick before previous one has happened. |
| 207 if delay := m.Now.Sub(m.State.LastTick.When); delay < 0 { |
| 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 RewindIfNecessary 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 { |
| 237 m.Actions = append(m.Actions, m.State.LastTick) |
| 238 } |
| 239 } |
| 240 } |
OLD | NEW |