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 |