gitee.com/quant1x/gox@v1.21.2/cron/cron.go (about)

     1  package cron
     2  
     3  import (
     4  	"context"
     5  	"gitee.com/quant1x/gox/logger"
     6  	"gitee.com/quant1x/gox/runtime"
     7  	"sort"
     8  	"sync"
     9  	"time"
    10  )
    11  
    12  // Cron keeps track of any number of entries, invoking the associated func as
    13  // specified by the schedule. It may be started, stopped, and the entries may
    14  // be inspected while running.
    15  type Cron struct {
    16  	entries   []*Entry
    17  	chain     Chain
    18  	stop      chan struct{}
    19  	add       chan *Entry
    20  	remove    chan EntryID
    21  	snapshot  chan chan []Entry
    22  	running   bool
    23  	logger    Logger
    24  	runningMu sync.Mutex
    25  	location  *time.Location
    26  	parser    ScheduleParser
    27  	nextID    EntryID
    28  	jobWaiter sync.WaitGroup
    29  }
    30  
    31  // ScheduleParser is an interface for schedule spec parsers that return a Schedule
    32  type ScheduleParser interface {
    33  	Parse(spec string) (Schedule, error)
    34  }
    35  
    36  // Job is an interface for submitted cron jobs.
    37  type Job interface {
    38  	Run()
    39  }
    40  
    41  // Schedule describes a job's duty cycle.
    42  type Schedule interface {
    43  	// Next returns the next activation time, later than the given time.
    44  	// Next is invoked initially, and then each time the job is run.
    45  	Next(time.Time) time.Time
    46  }
    47  
    48  // EntryID identifies an entry within a Cron instance
    49  type EntryID int
    50  
    51  // Entry consists of a schedule and the func to execute on that schedule.
    52  type Entry struct {
    53  	// ID is the cron-assigned ID of this entry, which may be used to look up a
    54  	// snapshot or remove it.
    55  	ID EntryID
    56  
    57  	// Schedule on which this job should be run.
    58  	Schedule Schedule
    59  
    60  	// Next time the job will run, or the zero time if Cron has not been
    61  	// started or this entry's schedule is unsatisfiable
    62  	Next time.Time
    63  
    64  	// Prev is the last time this job was run, or the zero time if never.
    65  	Prev time.Time
    66  
    67  	// WrappedJob is the thing to run when the Schedule is activated.
    68  	WrappedJob Job
    69  
    70  	// Job is the thing that was submitted to cron.
    71  	// It is kept around so that user code that needs to get at the job later,
    72  	// e.g. via Entries() can do so.
    73  	Job Job
    74  }
    75  
    76  // Valid returns true if this is not the zero entry.
    77  func (e Entry) Valid() bool { return e.ID != 0 }
    78  
    79  // byTime is a wrapper for sorting the entry array by time
    80  // (with zero time at the end).
    81  type byTime []*Entry
    82  
    83  func (s byTime) Len() int      { return len(s) }
    84  func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    85  func (s byTime) Less(i, j int) bool {
    86  	// Two zero times should return false.
    87  	// Otherwise, zero is "greater" than any other time.
    88  	// (To sort it at the end of the list.)
    89  	if s[i].Next.IsZero() {
    90  		return false
    91  	}
    92  	if s[j].Next.IsZero() {
    93  		return true
    94  	}
    95  	return s[i].Next.Before(s[j].Next)
    96  }
    97  
    98  // New returns a new Cron job runner, modified by the given options.
    99  //
   100  // Available Settings
   101  //
   102  //	Time Zone
   103  //	  Description: The time zone in which schedules are interpreted
   104  //	  Default:     time.Local
   105  //
   106  //	Parser
   107  //	  Description: Parser converts cron spec strings into cron.Schedules.
   108  //	  Default:     Accepts this spec: https://en.wikipedia.org/wiki/Cron
   109  //
   110  //	Chain
   111  //	  Description: Wrap submitted jobs to customize behavior.
   112  //	  Default:     A chain that recovers panics and logs them to stderr.
   113  //
   114  // See "cron.With*" to modify the default behavior.
   115  func New(opts ...Option) *Cron {
   116  	c := &Cron{
   117  		entries:   nil,
   118  		chain:     NewChain(),
   119  		add:       make(chan *Entry),
   120  		stop:      make(chan struct{}),
   121  		snapshot:  make(chan chan []Entry),
   122  		remove:    make(chan EntryID),
   123  		running:   false,
   124  		runningMu: sync.Mutex{},
   125  		logger:    DefaultLogger,
   126  		location:  time.Local,
   127  		parser:    standardParser,
   128  	}
   129  	for _, opt := range opts {
   130  		opt(c)
   131  	}
   132  	return c
   133  }
   134  
   135  // FuncJob is a wrapper that turns a func() into a cron.Job
   136  type FuncJob func()
   137  
   138  func (f FuncJob) Run() { f() }
   139  
   140  // AddFunc adds a func to the Cron to be run on the given schedule.
   141  // The spec is parsed using the time zone of this Cron instance as the default.
   142  // An opaque ID is returned that can be used to later remove it.
   143  func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
   144  	return c.AddJob(spec, FuncJob(cmd))
   145  }
   146  
   147  // AddJob adds a Job to the Cron to be run on the given schedule.
   148  // The spec is parsed using the time zone of this Cron instance as the default.
   149  // An opaque ID is returned that can be used to later remove it.
   150  func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) {
   151  	schedule, err := c.parser.Parse(spec)
   152  	if err != nil {
   153  		return 0, err
   154  	}
   155  	return c.Schedule(schedule, cmd), nil
   156  }
   157  
   158  func (c *Cron) AddFuncWithSkipIfStillRunning(spec string, cmd func()) (EntryID, error) {
   159  	job := SkipIfStillRunning()(FuncJob(cmd))
   160  	return c.AddJob(spec, job)
   161  }
   162  
   163  func (c *Cron) AddJobWithSkipIfStillRunning(spec string, cmd func()) (EntryID, error) {
   164  	var ch = make(chan struct{}, 1)
   165  	ch <- struct{}{}
   166  	funcName := runtime.FuncName(cmd)
   167  	jobFunc := func() {
   168  		select {
   169  		case v := <-ch:
   170  			defer func() { ch <- v }()
   171  			var tmStart time.Time
   172  			if runtime.Debug() {
   173  				tmStart = time.Now()
   174  			}
   175  			cmd()
   176  			if runtime.Debug() {
   177  				elapsedTime := time.Since(tmStart) / time.Millisecond
   178  				logger.Warnf("func: %s, 总耗时: %.3fs", funcName, float64(elapsedTime)/1000)
   179  			}
   180  		default:
   181  			if runtime.Debug() {
   182  				logger.Warnf("func: %s, skip", funcName)
   183  			}
   184  		}
   185  	}
   186  
   187  	return c.AddJob(spec, FuncJob(jobFunc))
   188  }
   189  
   190  // Schedule adds a Job to the Cron to be run on the given schedule.
   191  // The job is wrapped with the configured Chain.
   192  func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID {
   193  	c.runningMu.Lock()
   194  	defer c.runningMu.Unlock()
   195  	c.nextID++
   196  	entry := &Entry{
   197  		ID:         c.nextID,
   198  		Schedule:   schedule,
   199  		WrappedJob: c.chain.Then(cmd),
   200  		Job:        cmd,
   201  	}
   202  	if !c.running {
   203  		c.entries = append(c.entries, entry)
   204  	} else {
   205  		c.add <- entry
   206  	}
   207  	return entry.ID
   208  }
   209  
   210  // Entries returns a snapshot of the cron entries.
   211  func (c *Cron) Entries() []Entry {
   212  	c.runningMu.Lock()
   213  	defer c.runningMu.Unlock()
   214  	if c.running {
   215  		replyChan := make(chan []Entry, 1)
   216  		c.snapshot <- replyChan
   217  		return <-replyChan
   218  	}
   219  	return c.entrySnapshot()
   220  }
   221  
   222  // Location gets the time zone location
   223  func (c *Cron) Location() *time.Location {
   224  	return c.location
   225  }
   226  
   227  // Entry returns a snapshot of the given entry, or nil if it couldn't be found.
   228  func (c *Cron) Entry(id EntryID) Entry {
   229  	for _, entry := range c.Entries() {
   230  		if id == entry.ID {
   231  			return entry
   232  		}
   233  	}
   234  	return Entry{}
   235  }
   236  
   237  // Remove an entry from being run in the future.
   238  func (c *Cron) Remove(id EntryID) {
   239  	c.runningMu.Lock()
   240  	defer c.runningMu.Unlock()
   241  	if c.running {
   242  		c.remove <- id
   243  	} else {
   244  		c.removeEntry(id)
   245  	}
   246  }
   247  
   248  // Start the cron scheduler in its own goroutine, or no-op if already started.
   249  func (c *Cron) Start() {
   250  	c.runningMu.Lock()
   251  	defer c.runningMu.Unlock()
   252  	if c.running {
   253  		return
   254  	}
   255  	c.running = true
   256  	go c.run()
   257  }
   258  
   259  // Run the cron scheduler, or no-op if already running.
   260  func (c *Cron) Run() {
   261  	c.runningMu.Lock()
   262  	if c.running {
   263  		c.runningMu.Unlock()
   264  		return
   265  	}
   266  	c.running = true
   267  	c.runningMu.Unlock()
   268  	c.run()
   269  }
   270  
   271  // run the scheduler.. this is private just due to the need to synchronize
   272  // access to the 'running' state variable.
   273  func (c *Cron) run() {
   274  	c.logger.Info("start")
   275  
   276  	// Figure out the next activation times for each entry.
   277  	now := c.now()
   278  	for _, entry := range c.entries {
   279  		entry.Next = entry.Schedule.Next(now)
   280  		c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next)
   281  	}
   282  
   283  	for {
   284  		// Determine the next entry to run.
   285  		sort.Sort(byTime(c.entries))
   286  
   287  		var timer *time.Timer
   288  		if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
   289  			// If there are no entries yet, just sleep - it still handles new entries
   290  			// and stop requests.
   291  			timer = time.NewTimer(100000 * time.Hour)
   292  		} else {
   293  			timer = time.NewTimer(c.entries[0].Next.Sub(now))
   294  		}
   295  
   296  		for {
   297  			select {
   298  			case now = <-timer.C:
   299  				now = now.In(c.location)
   300  				c.logger.Info("wake", "now", now)
   301  
   302  				// Run every entry whose next time was less than now
   303  				for _, e := range c.entries {
   304  					if e.Next.After(now) || e.Next.IsZero() {
   305  						break
   306  					}
   307  					c.startJob(e.WrappedJob)
   308  					e.Prev = e.Next
   309  					e.Next = e.Schedule.Next(now)
   310  					c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next)
   311  				}
   312  
   313  			case newEntry := <-c.add:
   314  				timer.Stop()
   315  				now = c.now()
   316  				newEntry.Next = newEntry.Schedule.Next(now)
   317  				c.entries = append(c.entries, newEntry)
   318  				c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next)
   319  
   320  			case replyChan := <-c.snapshot:
   321  				replyChan <- c.entrySnapshot()
   322  				continue
   323  
   324  			case <-c.stop:
   325  				timer.Stop()
   326  				c.logger.Info("stop")
   327  				return
   328  
   329  			case id := <-c.remove:
   330  				timer.Stop()
   331  				now = c.now()
   332  				c.removeEntry(id)
   333  				c.logger.Info("removed", "entry", id)
   334  			}
   335  
   336  			break
   337  		}
   338  	}
   339  }
   340  
   341  // startJob runs the given job in a new goroutine.
   342  func (c *Cron) startJob(j Job) {
   343  	c.jobWaiter.Add(1)
   344  	go func() {
   345  		defer c.jobWaiter.Done()
   346  		j.Run()
   347  	}()
   348  }
   349  
   350  // now returns current time in c location
   351  func (c *Cron) now() time.Time {
   352  	return time.Now().In(c.location)
   353  }
   354  
   355  // Stop stops the cron scheduler if it is running; otherwise it does nothing.
   356  // A context is returned so the caller can wait for running jobs to complete.
   357  func (c *Cron) Stop() context.Context {
   358  	c.runningMu.Lock()
   359  	defer c.runningMu.Unlock()
   360  	if c.running {
   361  		c.stop <- struct{}{}
   362  		c.running = false
   363  	}
   364  	ctx, cancel := context.WithCancel(context.Background())
   365  	go func() {
   366  		c.jobWaiter.Wait()
   367  		cancel()
   368  	}()
   369  	return ctx
   370  }
   371  
   372  // entrySnapshot returns a copy of the current cron entry list.
   373  func (c *Cron) entrySnapshot() []Entry {
   374  	var entries = make([]Entry, len(c.entries))
   375  	for i, e := range c.entries {
   376  		entries[i] = *e
   377  	}
   378  	return entries
   379  }
   380  
   381  func (c *Cron) removeEntry(id EntryID) {
   382  	var entries []*Entry
   383  	for _, e := range c.entries {
   384  		if e.ID != id {
   385  			entries = append(entries, e)
   386  		}
   387  	}
   388  	c.entries = entries
   389  }