Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(153)

Side by Side Diff: scheduler/appengine/engine/cron/machine.go

Issue 2980943002: Add cron.Machine state machine. (Closed)
Patch Set: add-cron-machine-construct Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698