go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/engine/cron/machine.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  	"fmt"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/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  	// Generation is increased each time state mutates.
    52  	//
    53  	// Monotonic, never resets. Should not be assumed sequential: some calls
    54  	// mutate the state multiple times during one transition.
    55  	//
    56  	// Used to deduplicate StartInvocationAction in case of retries of cron state
    57  	// transitions.
    58  	Generation int64
    59  
    60  	// LastRewind is a time when the cron machine was restarted last time.
    61  	//
    62  	// For relative schedules, it's a time RewindIfNecessary() was called. For
    63  	// absolute schedules it is last time invocation happened (cron machines on
    64  	// absolute schedules auto-rewind themselves).
    65  	LastRewind time.Time
    66  
    67  	// LastTick is last emitted tick request (or empty struct).
    68  	//
    69  	// It may be scheduled for "distant future" for paused cron machines.
    70  	LastTick TickLaterAction
    71  }
    72  
    73  // Equal reports whether two structs are equal.
    74  func (s *State) Equal(o *State) bool {
    75  	return s.Enabled == o.Enabled &&
    76  		s.Generation == o.Generation &&
    77  		s.LastRewind.Equal(o.LastRewind) &&
    78  		s.LastTick.Equal(&o.LastTick)
    79  }
    80  
    81  // IsSuspended returns true if the cron machine is not waiting for a tick.
    82  //
    83  // This happens for paused cron machines (they technically are scheduled for
    84  // a tick in a distant future) and for cron machines on relative schedule that
    85  // wait for 'RewindIfNecessary' to be called to start ticking again.
    86  //
    87  // A disabled cron machine is also considered suspended.
    88  func (s *State) IsSuspended() bool {
    89  	return !s.Enabled || s.LastTick.When.IsZero() || s.LastTick.When == schedule.DistantFuture
    90  }
    91  
    92  ////////////////////////////////////////////////////////////////////////////////
    93  
    94  // Action is a particular action to perform when switching the state.
    95  //
    96  // Can be type cast to some concrete *Action struct. Intended to be handled by
    97  // whoever hosts the cron machine.
    98  type Action interface {
    99  	IsAction() bool
   100  }
   101  
   102  // TickLaterAction schedules an OnTimerTick call at given moment in time.
   103  //
   104  // TickNonce is used by cron machine to skip canceled or repeated ticks.
   105  type TickLaterAction struct {
   106  	When      time.Time
   107  	TickNonce int64
   108  }
   109  
   110  // Equal reports whether two structs are equal.
   111  func (a *TickLaterAction) Equal(o *TickLaterAction) bool {
   112  	return a.TickNonce == o.TickNonce && a.When.Equal(o.When)
   113  }
   114  
   115  // IsAction makes TickLaterAction implement Action interface.
   116  func (a TickLaterAction) IsAction() bool { return true }
   117  
   118  // StartInvocationAction is emitted when the scheduled moment comes.
   119  //
   120  // A handler is expected to call RewindIfNecessary() at some later time to
   121  // restart the cron machine if it's running on a relative schedule (e.g. "with
   122  // 10 sec interval"). Cron machines on relative schedules are "one shot". They
   123  // need to be rewound to start counting time again.
   124  //
   125  // Cron machines on absolute schedules (regular crons, like "at 12 AM every
   126  // day") don't need rewinding, they'll start counting time until next invocation
   127  // automatically. Calling RewindIfNecessary() for them won't hurt though, it
   128  // will be noop.
   129  type StartInvocationAction struct {
   130  	Generation int64 // value of state.Generation when the action was emitted
   131  }
   132  
   133  // IsAction makes StartInvocationAction implement Action interface.
   134  func (a StartInvocationAction) IsAction() bool { return true }
   135  
   136  ////////////////////////////////////////////////////////////////////////////////
   137  
   138  // Machine advances the state of the cron machine.
   139  //
   140  // It gracefully handles various kinds of external events (like pauses and
   141  // schedule changes) and emits actions that's supposed to handled by whoever
   142  // hosts it.
   143  type Machine struct {
   144  	// Inputs.
   145  	Now      time.Time          // current time
   146  	Schedule *schedule.Schedule // knows when to emit invocation action
   147  	Nonce    func() int64       // produces nonces on demand
   148  
   149  	// Mutated.
   150  	State   State    // state of the cron machine, mutated by its methods
   151  	Actions []Action // all emitted actions (if any)
   152  }
   153  
   154  // Enable makes the cron machine start counting time.
   155  //
   156  // Does nothing if already enabled.
   157  func (m *Machine) Enable() {
   158  	if !m.State.Enabled {
   159  		m.State = State{
   160  			Enabled:    true,
   161  			Generation: m.nextGen(),
   162  			LastRewind: m.Now,
   163  		}
   164  		m.scheduleTick(m.Now)
   165  	}
   166  }
   167  
   168  // Disable stops any pending timer ticks, resets state.
   169  //
   170  // The cron machine will ignore any events until Enable is called to turn it on.
   171  func (m *Machine) Disable() {
   172  	m.State = State{Enabled: false, Generation: m.nextGen()}
   173  }
   174  
   175  // RewindIfNecessary is called to restart the cron after it has fired the
   176  // invocation action.
   177  //
   178  // Does nothing if the cron is disabled or already ticking.
   179  func (m *Machine) RewindIfNecessary() {
   180  	m.rewindIfNecessary(m.Now)
   181  }
   182  
   183  // OnScheduleChange happens when cron's schedule changes.
   184  //
   185  // In particular, it handles switches between absolute and relative schedules.
   186  func (m *Machine) OnScheduleChange() {
   187  	// Do not touch timers on disabled cron machines.
   188  	if !m.State.Enabled {
   189  		return
   190  	}
   191  
   192  	// The following condition is true for cron machines on a relative schedule
   193  	// that have already "fired", and currently wait for manual RewindIfNecessary
   194  	// call to start ticking again. When such cron machines switch to an absolute
   195  	// schedule, we need to rewind them right away (since machines on absolute
   196  	// schedules always tick!). If the new schedule is also relative, do nothing:
   197  	// RewindIfNecessary() should be called manually by the host at some later
   198  	// time (as usual for relative schedules).
   199  	if m.State.LastTick.When.IsZero() {
   200  		if m.Schedule.IsAbsolute() {
   201  			m.RewindIfNecessary()
   202  		}
   203  	} else {
   204  		// In this branch, the cron machine has a timer tick scheduled. It means it
   205  		// is either in a relative or absolute schedule, and this schedule may have
   206  		// changed, so we may need to move the tick to reflect the change. Note that
   207  		// we are not resetting LastRewind here, since we want the new schedule to
   208  		// take into account real last RewindIfNecessary call. For example, if the
   209  		// last rewind happened at moment X, current time is Now, and the new
   210  		// schedule is "with 10s interval", we want the tick to happen at "X+10",
   211  		// not "Now+10".
   212  		m.scheduleTick(m.Now)
   213  	}
   214  }
   215  
   216  // OnTimerTick happens when a scheduled timer tick (added with TickLaterAction)
   217  // occurs.
   218  //
   219  // Returns an error if the tick happened too soon.
   220  func (m *Machine) OnTimerTick(tickNonce int64) error {
   221  	// Silently skip unexpected, late or canceled ticks. This is fine.
   222  	switch {
   223  	case m.State.IsSuspended():
   224  		return nil
   225  	case m.State.LastTick.TickNonce != tickNonce:
   226  		return nil
   227  	}
   228  
   229  	// For ticks in the future more than Task Queue limit, OnTimerTick will be
   230  	// called much earlier than the actual tick, so emit a new TickLaterAction
   231  	// with a new Nonce.
   232  	//
   233  	// Report error (to trigger a retry) if the tick happened unexpectedly soon.
   234  	// Allow up to 50ms clock drift, but correct it to be 0. This is important for
   235  	// getting the correct next tick time from an absolute schedule. If we pass
   236  	// m.Now to the cron schedule uncorrected, we'll just get the time of the
   237  	// already scheduled tick (the one we are handling now, since uncorrected
   238  	// m.Now is before it).
   239  	//
   240  	// Note that m.Now is part of inputs and must not be mutated. We propagate
   241  	// the corrected time via the call stack.
   242  	now := m.Now
   243  	switch remaining := m.State.LastTick.When.Sub(now); {
   244  	case remaining > time.Second:
   245  		m.State.Generation = m.nextGen()
   246  		m.State.LastTick.TickNonce = m.Nonce()
   247  		m.Actions = append(m.Actions, m.State.LastTick)
   248  		return nil
   249  	case remaining > 50*time.Millisecond:
   250  		return fmt.Errorf("tick happened %s before it was expected", remaining)
   251  	case remaining > 0:
   252  		now = m.State.LastTick.When
   253  	}
   254  
   255  	// The scheduled time has come!
   256  	m.State.Generation = m.nextGen()
   257  	m.Actions = append(m.Actions, StartInvocationAction{
   258  		Generation: m.State.Generation,
   259  	})
   260  	m.State.LastTick = TickLaterAction{}
   261  
   262  	// Start waiting for a new tick right away if on an absolute schedule or just
   263  	// keep the tick state clear for relative schedules: new tick will be set when
   264  	// RewindIfNecessary() is manually called by whoever handles the cron.
   265  	if m.Schedule.IsAbsolute() {
   266  		m.rewindIfNecessary(now)
   267  	}
   268  
   269  	return nil
   270  }
   271  
   272  // scheduleTick emits TickLaterAction action according to the schedule, current
   273  // time, and last time RewindIfNecessary was called.
   274  //
   275  // Does nothing if such tick has already been scheduled.
   276  func (m *Machine) scheduleTick(now time.Time) {
   277  	nextTickTime := m.Schedule.Next(now, m.State.LastRewind)
   278  	if nextTickTime != m.State.LastTick.When {
   279  		m.State.Generation = m.nextGen()
   280  		m.State.LastTick = TickLaterAction{
   281  			When:      nextTickTime,
   282  			TickNonce: m.Nonce(),
   283  		}
   284  		if nextTickTime != schedule.DistantFuture {
   285  			m.Actions = append(m.Actions, m.State.LastTick)
   286  		}
   287  	}
   288  }
   289  
   290  // nextGen returns the next generation number.
   291  //
   292  // It does NOT update Generation in-place, just produces the next number.
   293  func (m *Machine) nextGen() int64 {
   294  	return m.State.Generation + 1
   295  }
   296  
   297  // rewindIfNecessary implements RewindIfNecessary, accepting corrected 'now'.
   298  //
   299  // This is important for OnTimerTick. Note that m.Now is part of inputs and must
   300  // not be mutated.
   301  func (m *Machine) rewindIfNecessary(now time.Time) {
   302  	if m.State.Enabled && m.State.LastTick.When.IsZero() {
   303  		m.State.LastRewind = now
   304  		m.State.Generation = m.nextGen()
   305  		m.scheduleTick(now)
   306  	}
   307  }