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  }