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 "testing" |
| 19 "time" |
| 20 |
| 21 "github.com/luci/luci-go/scheduler/appengine/schedule" |
| 22 . "github.com/smartystreets/goconvey/convey" |
| 23 ) |
| 24 |
| 25 func TestMachine(t *testing.T) { |
| 26 t.Parallel() |
| 27 |
| 28 at0min, _ := schedule.Parse("0 * * * *", 0) |
| 29 at45min, _ := schedule.Parse("45 * * * *", 0) |
| 30 each30min, _ := schedule.Parse("with 30m interval", 0) |
| 31 each10min, _ := schedule.Parse("with 10m interval", 0) |
| 32 never, _ := schedule.Parse("triggered", 0) |
| 33 |
| 34 Convey("Absolute schedule", t, func() { |
| 35 tm := testMachine{ |
| 36 Now: parseTime("00:15"), |
| 37 Schedule: at0min, |
| 38 } |
| 39 |
| 40 // Enabling the job schedules the first tick based on the schedu
le. |
| 41 err := tm.roll(func(m *Machine) error { |
| 42 m.Enable() |
| 43 return nil |
| 44 }) |
| 45 So(err, ShouldBeNil) |
| 46 So(tm.Actions, ShouldResemble, []Action{ |
| 47 TickLaterAction{ |
| 48 When: parseTime("01:00"), |
| 49 TickNonce: 1, |
| 50 }, |
| 51 }) |
| 52 |
| 53 // RewindIfNecessary does nothing, the tick is already set. |
| 54 err = tm.roll(func(m *Machine) error { |
| 55 m.RewindIfNecessary() |
| 56 return nil |
| 57 }) |
| 58 So(err, ShouldBeNil) |
| 59 So(tm.Actions, ShouldBeNil) |
| 60 |
| 61 // Early tick is ignored with an error. |
| 62 tm.Now = parseTime("00:59") |
| 63 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }
) |
| 64 So(err.Error(), ShouldEqual, "tick happened 60.0 sec before it w
as expected") |
| 65 So(tm.Actions, ShouldEqual, nil) |
| 66 |
| 67 tm.Now = parseTime("01:01") // acceptable tick time (slightly la
te)! |
| 68 |
| 69 // A tick with wrong nonce is silently skipped. |
| 70 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(123)
}) |
| 71 So(err, ShouldBeNil) |
| 72 So(tm.Actions, ShouldEqual, nil) |
| 73 |
| 74 // The correct tick comes. Invocation is started and new tick is
scheduled. |
| 75 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }
) |
| 76 So(err, ShouldBeNil) |
| 77 So(tm.Actions, ShouldResemble, []Action{ |
| 78 StartInvocationAction{}, |
| 79 TickLaterAction{ |
| 80 When: parseTime("02:00"), |
| 81 TickNonce: 2, |
| 82 }, |
| 83 }) |
| 84 |
| 85 // Disabling the job. |
| 86 err = tm.roll(func(m *Machine) error { |
| 87 m.Disable() |
| 88 return nil |
| 89 }) |
| 90 So(err, ShouldBeNil) |
| 91 So(tm.Actions, ShouldBeNil) |
| 92 |
| 93 // It silently skips the tick now. |
| 94 tm.Now = parseTime("02:00") |
| 95 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(2) }
) |
| 96 So(err, ShouldBeNil) |
| 97 So(tm.Actions, ShouldEqual, nil) |
| 98 }) |
| 99 |
| 100 Convey("Relative schedule", t, func() { |
| 101 tm := testMachine{ |
| 102 Now: parseTime("00:00"), |
| 103 Schedule: each30min, |
| 104 } |
| 105 |
| 106 // Enabling the job schedules the first tick based on the schedu
le. |
| 107 err := tm.roll(func(m *Machine) error { |
| 108 m.Enable() |
| 109 return nil |
| 110 }) |
| 111 So(err, ShouldBeNil) |
| 112 So(tm.Actions, ShouldResemble, []Action{ |
| 113 TickLaterAction{ |
| 114 When: parseTime("00:30"), |
| 115 TickNonce: 1, |
| 116 }, |
| 117 }) |
| 118 |
| 119 // RewindIfNecessary does nothing, the tick is already set. |
| 120 err = tm.roll(func(m *Machine) error { |
| 121 m.RewindIfNecessary() |
| 122 return nil |
| 123 }) |
| 124 So(err, ShouldBeNil) |
| 125 So(tm.Actions, ShouldBeNil) |
| 126 |
| 127 // Tick arrives (slightly late). The invocation is started, but
the next |
| 128 // tick is _not_ set. |
| 129 tm.Now = parseTime("00:31") |
| 130 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }
) |
| 131 So(err, ShouldBeNil) |
| 132 So(tm.Actions, ShouldResemble, []Action{ |
| 133 StartInvocationAction{}, |
| 134 }) |
| 135 |
| 136 // Some time later (when invocation has presumably finished), re
wind the |
| 137 // clock. It sets a new tick 30min from now. |
| 138 tm.Now = parseTime("00:40") |
| 139 err = tm.roll(func(m *Machine) error { |
| 140 m.RewindIfNecessary() |
| 141 return nil |
| 142 }) |
| 143 So(err, ShouldBeNil) |
| 144 So(tm.Actions, ShouldResemble, []Action{ |
| 145 TickLaterAction{ |
| 146 When: parseTime("01:10"), // 40min + 30min |
| 147 TickNonce: 2, |
| 148 }, |
| 149 }) |
| 150 }) |
| 151 |
| 152 Convey("Relative schedule, distant future", t, func() { |
| 153 tm := testMachine{ |
| 154 Now: parseTime("00:00"), |
| 155 Schedule: never, |
| 156 } |
| 157 |
| 158 // Enabling the job does nothing. |
| 159 err := tm.roll(func(m *Machine) error { |
| 160 m.Enable() |
| 161 return nil |
| 162 }) |
| 163 So(err, ShouldBeNil) |
| 164 So(tm.Actions, ShouldBeNil) |
| 165 |
| 166 // Rewinding does nothing. |
| 167 err = tm.roll(func(m *Machine) error { |
| 168 m.RewindIfNecessary() |
| 169 return nil |
| 170 }) |
| 171 So(err, ShouldBeNil) |
| 172 So(tm.Actions, ShouldBeNil) |
| 173 |
| 174 // Ticking does nothing. |
| 175 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(1) }
) |
| 176 So(err, ShouldBeNil) |
| 177 So(tm.Actions, ShouldBeNil) |
| 178 }) |
| 179 |
| 180 Convey("Schedule changes", t, func() { |
| 181 // Start with absolute. |
| 182 tm := testMachine{ |
| 183 Now: parseTime("00:00"), |
| 184 Schedule: at0min, |
| 185 } |
| 186 |
| 187 // The first tick is scheduled to 1h from now. |
| 188 err := tm.roll(func(m *Machine) error { |
| 189 m.Enable() |
| 190 return nil |
| 191 }) |
| 192 So(err, ShouldBeNil) |
| 193 So(tm.Actions, ShouldResemble, []Action{ |
| 194 TickLaterAction{ |
| 195 When: parseTime("01:00"), |
| 196 TickNonce: 1, |
| 197 }, |
| 198 }) |
| 199 |
| 200 // 10 min later switch to the relative schedule. It reschedules
the tick |
| 201 // to 30 min since the _previous action_ (which was 'Enable'). |
| 202 tm.Now = parseTime("00:10") |
| 203 tm.Schedule = each30min |
| 204 err = tm.roll(func(m *Machine) error { |
| 205 m.OnScheduleChange() |
| 206 return nil |
| 207 }) |
| 208 So(err, ShouldBeNil) |
| 209 So(tm.Actions, ShouldResemble, []Action{ |
| 210 TickLaterAction{ |
| 211 When: parseTime("00:30"), |
| 212 TickNonce: 2, |
| 213 }, |
| 214 }) |
| 215 |
| 216 // The operation is idempotent. No new tick is scheduled when we
try again. |
| 217 tm.Now = parseTime("00:15") |
| 218 err = tm.roll(func(m *Machine) error { |
| 219 m.OnScheduleChange() |
| 220 return nil |
| 221 }) |
| 222 So(err, ShouldBeNil) |
| 223 So(tm.Actions, ShouldBeNil) |
| 224 |
| 225 // The scheduled tick comes. Since it is a relative schedule, no
new tick |
| 226 // is scheduled. |
| 227 tm.Now = parseTime("00:30") |
| 228 err = tm.roll(func(m *Machine) error { return m.OnTimerTick(2) }
) |
| 229 So(err, ShouldBeNil) |
| 230 So(tm.Actions, ShouldResemble, []Action{ |
| 231 StartInvocationAction{}, |
| 232 }) |
| 233 |
| 234 // Some time later we switch it to another relative schedule. No
thing |
| 235 // happens, since we are waiting for a rewind now anyway. |
| 236 tm.Now = parseTime("00:40") |
| 237 tm.Schedule = each10min |
| 238 err = tm.roll(func(m *Machine) error { |
| 239 m.OnScheduleChange() |
| 240 return nil |
| 241 }) |
| 242 So(err, ShouldBeNil) |
| 243 So(tm.Actions, ShouldBeNil) |
| 244 |
| 245 // Now we switch back to the absolute schedule. It schedules a n
ew tick. |
| 246 tm.Now = parseTime("01:30") |
| 247 tm.Schedule = at0min |
| 248 err = tm.roll(func(m *Machine) error { |
| 249 m.OnScheduleChange() |
| 250 return nil |
| 251 }) |
| 252 So(err, ShouldBeNil) |
| 253 So(tm.Actions, ShouldResemble, []Action{ |
| 254 TickLaterAction{ |
| 255 When: parseTime("02:00"), |
| 256 TickNonce: 3, |
| 257 }, |
| 258 }) |
| 259 |
| 260 // Changing the absolute schedule moves the tick accordingly. |
| 261 tm.Schedule = at45min |
| 262 err = tm.roll(func(m *Machine) error { |
| 263 m.OnScheduleChange() |
| 264 return nil |
| 265 }) |
| 266 So(err, ShouldBeNil) |
| 267 So(tm.Actions, ShouldResemble, []Action{ |
| 268 TickLaterAction{ |
| 269 When: parseTime("01:45"), |
| 270 TickNonce: 4, |
| 271 }, |
| 272 }) |
| 273 |
| 274 // Switching to 'triggered' schedule "disarms" the current tick
by replacing |
| 275 // it with "tick in the distant future". This doesn't emit any a
ctions, |
| 276 // since we can't actually schedule tick in the distant future. |
| 277 tm.Schedule = never |
| 278 err = tm.roll(func(m *Machine) error { |
| 279 m.OnScheduleChange() |
| 280 return nil |
| 281 }) |
| 282 So(err, ShouldBeNil) |
| 283 So(tm.Actions, ShouldBeNil) |
| 284 So(tm.State.LastTick, ShouldResemble, TickLaterAction{ |
| 285 When: schedule.DistantFuture, |
| 286 TickNonce: 5, |
| 287 }) |
| 288 |
| 289 // Enabling back absolute schedule places a new tick. |
| 290 tm.Now = parseTime("01:30") |
| 291 tm.Schedule = at0min |
| 292 err = tm.roll(func(m *Machine) error { |
| 293 m.OnScheduleChange() |
| 294 return nil |
| 295 }) |
| 296 So(err, ShouldBeNil) |
| 297 So(tm.Actions, ShouldResemble, []Action{ |
| 298 TickLaterAction{ |
| 299 When: parseTime("02:00"), |
| 300 TickNonce: 6, |
| 301 }, |
| 302 }) |
| 303 |
| 304 // Schedule changes do nothing to disabled jobs. |
| 305 tm.Schedule = at45min |
| 306 err = tm.roll(func(m *Machine) error { |
| 307 m.Disable() |
| 308 m.OnScheduleChange() |
| 309 return nil |
| 310 }) |
| 311 So(err, ShouldBeNil) |
| 312 So(tm.Actions, ShouldBeNil) |
| 313 }) |
| 314 |
| 315 Convey("Petty code coverage", t, func() { |
| 316 // Just to get 100% code coverage... |
| 317 So((StartInvocationAction{}).IsAction(), ShouldBeTrue) |
| 318 So((TickLaterAction{}).IsAction(), ShouldBeTrue) |
| 319 }) |
| 320 } |
| 321 |
| 322 func parseTime(str string) time.Time { |
| 323 t, err := time.Parse(time.RFC822, "01 Jan 17 "+str+" UTC") |
| 324 if err != nil { |
| 325 panic(err) |
| 326 } |
| 327 return t |
| 328 } |
| 329 |
| 330 type testMachine struct { |
| 331 State State |
| 332 Schedule *schedule.Schedule |
| 333 Now time.Time |
| 334 Nonces int64 |
| 335 Actions []Action |
| 336 } |
| 337 |
| 338 func (t *testMachine) roll(cb func(*Machine) error) error { |
| 339 m := Machine{ |
| 340 Now: t.Now, |
| 341 Schedule: t.Schedule, |
| 342 Nonce: func() int64 { |
| 343 t.Nonces++ |
| 344 return t.Nonces |
| 345 }, |
| 346 State: t.State, |
| 347 } |
| 348 |
| 349 if err := cb(&m); err != nil { |
| 350 return err |
| 351 } |
| 352 |
| 353 t.State = m.State |
| 354 t.Actions = m.Actions |
| 355 return nil |
| 356 } |
OLD | NEW |