github.com/isyscore/isc-gobase@v1.5.3-0.20231218061332-cbc7451899e9/cron/cron.go (about)

     1  package cron
     2  
     3  import (
     4  	"log"
     5  	"runtime"
     6  	"sort"
     7  	"time"
     8  )
     9  
    10  // Cron keeps track of any number of entries, invoking the associated func as
    11  // specified by the schedule. It may be started, stopped, and the entries may
    12  // be inspected while running.
    13  type Cron struct {
    14  	entries  []*Entry
    15  	stop     chan struct{}
    16  	add      chan *Entry
    17  	snapshot chan []*Entry
    18  	running  bool
    19  	ErrorLog *log.Logger
    20  	location *time.Location
    21  }
    22  
    23  // Job is an interface for submitted cron jobs.
    24  type Job interface {
    25  	Run()
    26  }
    27  
    28  // The Schedule describes a job's duty cycle.
    29  type Schedule interface {
    30  	// Next Return the next activation time, later than the given time.
    31  	// Next is invoked initially, and then each time the job is run.
    32  	Next(time.Time) time.Time
    33  }
    34  
    35  // Entry consists of a schedule and the func to execute on that schedule.
    36  type Entry struct {
    37  	// The schedule on which this job should be run.
    38  	Schedule Schedule
    39  
    40  	// The next time the job will run. This is the zero time if Cron has not been
    41  	// started or this entry's schedule is unsatisfiable
    42  	Next time.Time
    43  
    44  	// The last time this job was run. This is the zero time if the job has never
    45  	// been run.
    46  	Prev time.Time
    47  
    48  	// The Job to run.
    49  	Job Job
    50  }
    51  
    52  // byTime is a wrapper for sorting the entry array by time
    53  // (with zero time at the end).
    54  type byTime []*Entry
    55  
    56  func (s byTime) Len() int      { return len(s) }
    57  func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    58  func (s byTime) Less(i, j int) bool {
    59  	// Two zero times should return false.
    60  	// Otherwise, zero is "greater" than any other time.
    61  	// (To sort it at the end of the list.)
    62  	if s[i].Next.IsZero() {
    63  		return false
    64  	}
    65  	if s[j].Next.IsZero() {
    66  		return true
    67  	}
    68  	return s[i].Next.Before(s[j].Next)
    69  }
    70  
    71  // New returns a new Cron job runner, in the Local time zone.
    72  func New() *Cron {
    73  	return NewWithLocation(time.Now().Location())
    74  }
    75  
    76  // NewWithLocation returns a new Cron job runner.
    77  func NewWithLocation(location *time.Location) *Cron {
    78  	return &Cron{
    79  		entries:  nil,
    80  		add:      make(chan *Entry),
    81  		stop:     make(chan struct{}),
    82  		snapshot: make(chan []*Entry),
    83  		running:  false,
    84  		ErrorLog: nil,
    85  		location: location,
    86  	}
    87  }
    88  
    89  // FuncJob A wrapper that turns a func() into a cron.Job
    90  type FuncJob func()
    91  
    92  func (f FuncJob) Run() { f() }
    93  
    94  // AddFunc adds a func to the Cron to be run on the given schedule.
    95  func (c *Cron) AddFunc(spec string, cmd func()) error {
    96  	return c.AddJob(spec, FuncJob(cmd))
    97  }
    98  
    99  // AddJob adds a Job to the Cron to be run on the given schedule.
   100  func (c *Cron) AddJob(spec string, cmd Job) error {
   101  	schedule, err := Parse(spec)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	c.Schedule(schedule, cmd)
   106  	return nil
   107  }
   108  
   109  // Schedule adds a Job to the Cron to be run on the given schedule.
   110  func (c *Cron) Schedule(schedule Schedule, cmd Job) {
   111  	entry := &Entry{
   112  		Schedule: schedule,
   113  		Job:      cmd,
   114  	}
   115  	if !c.running {
   116  		c.entries = append(c.entries, entry)
   117  		return
   118  	}
   119  
   120  	c.add <- entry
   121  }
   122  
   123  // Entries returns a snapshot of the cron entries.
   124  func (c *Cron) Entries() []*Entry {
   125  	if c.running {
   126  		c.snapshot <- nil
   127  		x := <-c.snapshot
   128  		return x
   129  	}
   130  	return c.entrySnapshot()
   131  }
   132  
   133  // Location gets the time zone location
   134  func (c *Cron) Location() *time.Location {
   135  	return c.location
   136  }
   137  
   138  // Start the cron scheduler in its own go-routine, or no-op if already started.
   139  func (c *Cron) Start() {
   140  	if c.running {
   141  		return
   142  	}
   143  	c.running = true
   144  	go c.run()
   145  }
   146  
   147  // Run the cron scheduler, or no-op if already running.
   148  func (c *Cron) Run() {
   149  	if c.running {
   150  		return
   151  	}
   152  	c.running = true
   153  	c.run()
   154  }
   155  
   156  func (c *Cron) runWithRecovery(j Job) {
   157  	defer func() {
   158  		if r := recover(); r != nil {
   159  			const size = 64 << 10
   160  			buf := make([]byte, size)
   161  			buf = buf[:runtime.Stack(buf, false)]
   162  			c.logf("cron: panic running job: %v\n%s", r, buf)
   163  		}
   164  	}()
   165  	j.Run()
   166  }
   167  
   168  // Run the scheduler. this is private just due to the need to synchronize
   169  // access to the 'running' state variable.
   170  func (c *Cron) run() {
   171  	// Figure out the next activation times for each entry.
   172  	now := c.now()
   173  	for _, entry := range c.entries {
   174  		entry.Next = entry.Schedule.Next(now)
   175  	}
   176  
   177  	for {
   178  		// Determine the next entry to run.
   179  		sort.Sort(byTime(c.entries))
   180  
   181  		var timer *time.Timer
   182  		if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
   183  			// If there are no entries yet, just sleep - it still handles new entries
   184  			// and stop requests.
   185  			timer = time.NewTimer(100000 * time.Hour)
   186  		} else {
   187  			timer = time.NewTimer(c.entries[0].Next.Sub(now))
   188  		}
   189  
   190  		for {
   191  			select {
   192  			case now = <-timer.C:
   193  				now = now.In(c.location)
   194  				// Run every entry whose next time was less than now
   195  				for _, e := range c.entries {
   196  					if e.Next.After(now) || e.Next.IsZero() {
   197  						break
   198  					}
   199  					go c.runWithRecovery(e.Job)
   200  					e.Prev = e.Next
   201  					e.Next = e.Schedule.Next(now)
   202  				}
   203  
   204  			case newEntry := <-c.add:
   205  				timer.Stop()
   206  				now = c.now()
   207  				newEntry.Next = newEntry.Schedule.Next(now)
   208  				c.entries = append(c.entries, newEntry)
   209  
   210  			case <-c.snapshot:
   211  				c.snapshot <- c.entrySnapshot()
   212  				continue
   213  
   214  			case <-c.stop:
   215  				timer.Stop()
   216  				return
   217  			}
   218  
   219  			break
   220  		}
   221  	}
   222  }
   223  
   224  // Logs an error to stderr or to the configured error log
   225  func (c *Cron) logf(format string, args ...any) {
   226  	if c.ErrorLog != nil {
   227  		c.ErrorLog.Printf(format, args...)
   228  	} else {
   229  		log.Printf(format, args...)
   230  	}
   231  }
   232  
   233  // Stop stops the cron scheduler if it is running; otherwise it does nothing.
   234  func (c *Cron) Stop() {
   235  	if !c.running {
   236  		return
   237  	}
   238  	c.stop <- struct{}{}
   239  	c.running = false
   240  }
   241  
   242  // entrySnapshot returns a copy of the current cron entry list.
   243  func (c *Cron) entrySnapshot() []*Entry {
   244  	var entries []*Entry
   245  	for _, e := range c.entries {
   246  		entries = append(entries, &Entry{
   247  			Schedule: e.Schedule,
   248  			Next:     e.Next,
   249  			Prev:     e.Prev,
   250  			Job:      e.Job,
   251  		})
   252  	}
   253  	return entries
   254  }
   255  
   256  // now returns current time in c location
   257  func (c *Cron) now() time.Time {
   258  	return time.Now().In(c.location)
   259  }
   260  
   261  // ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
   262  // It does not support jobs more frequent than once a second.
   263  type ConstantDelaySchedule struct {
   264  	Delay time.Duration
   265  }
   266  
   267  // Every returns a crontab Schedule that activates once every duration.
   268  // Delays of less than a second are not supported (will round up to 1 second).
   269  // Any fields less than a Second are truncated.
   270  func Every(duration time.Duration) ConstantDelaySchedule {
   271  	if duration < time.Second {
   272  		duration = time.Second
   273  	}
   274  	return ConstantDelaySchedule{
   275  		Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
   276  	}
   277  }
   278  
   279  // Next returns the next time this should be run.
   280  // This rounds so that the next activation time will be on the second.
   281  func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
   282  	return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
   283  }