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