golang的定时任务

199 阅读3分钟

问题:

  1. 执行时间太长,是否影响第二次执行?
  2. 一个任务如果还在运行,是否可以跳过下一次到来的执行?
  3. 一个任务如果还在运行,下一次的执行是否可以延迟执行?
  4. 一个任务,一个出现panic是否会影响其他任务?
  5. 是否可以做到优雅关闭,不影响正在执行的业务?

简单版:

使用ticker实现

defer func() {
   if err := recover(); err != nil {
      logger.Error("panic RobotZAdd recovery msg", zap.Any("err", err), zap.String("stack", string(debug.Stack())))
   }
}()
ctx := context.Background()
logic := robotlogic.NewRobotZAdd()
ticker := time.NewTicker(10 * time.Second)
for {
   select {
   case t := <-ticker.C:
      logger.Info("RobotZAdd start", zap.Reflect("time", t))
      logic.HandleRobotZAdd(ctx)
   }
}
  • 答案1:
    • 不影响,依赖每次ticker
  • 答案2:
    • 可以使用redis的setNx和golang的sync.Mutex。但是setnx不能完全保证
  • 答案3:
    • 不可以
  • 答案4:
    • 可以每个业务逻辑都增加recover
  • 答案5:
    • 不可以

进化版 cron包:

git地址:github.com/robfig/cron

  • 答案1:
    • 每次执行,都会新起一个协程执行,所以执行时间太长,会出现并行执行
  • 答案2:
    • 可以使用func SkipIfStillRunning(logger Logger) JobWrapper 装饰器模式
  • 答案3:
    • 可以使用func DelayIfStillRunning(logger Logger) JobWrapper 装饰器模式
  • 答案4:
    • 可以使用func Recover(logger Logger) JobWrapper 装饰器模式
  • 答案5:
    • 可以通过func (c *Cron) Stop() context.Context和context的done channel来优雅关闭

cron表达式

  1. 带秒:自定义解析模式
func TestWithSeconds(t *testing.T) {
  wg := &sync.WaitGroup{}
  wg.Add(1)

  cron := newWithSeconds()
  cron.AddFunc("0/2 * * * * ?", func() {
     fmt.Println("hello done")
     wg.Done()
  })
  cron.Start()
  defer cron.Stop()

  // Give cron 2 seconds to run our job (which is always activated).
  select {
  case <-time.After(5 * OneSecond):
     t.Fatal("expected job runs")
  case <-wait(wg):
  }
}
var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor)

// newWithSeconds returns a Cron with the seconds field enabled.
func newWithSeconds() *Cron {
   return New(WithParser(secondParser), WithChain())
}
  1. 带秒:@every
cron.AddFunc("@every 2s", func() { wg.Done() })
  1. 不带秒:解析方式通过标准的standardParser
var standardParser = NewParser(
	Minute | Hour | Dom | Month | Dow | Descriptor,
)
// Start cron, add a job, expect it runs.
func TestWithNoSeconds(t *testing.T) {
   wg := &sync.WaitGroup{}
   wg.Add(1)

   cron := New()
   cron.Start()
   defer cron.Stop()
   cron.AddFunc("0/1 * * * ?", func() {
      fmt.Println("hello done")
      wg.Done()
   })

   select {
   case <-time.After(2 * time.Minute):
      t.Fatal("expected job runs")
   case <-wait(wg):
   }
}

优雅关闭

// startJob runs the given job in a new goroutine.
func (c *Cron) startJob(j Job) {
   c.jobWaiter.Add(1)
   go func() {
      defer c.jobWaiter.Done()
      j.Run()
   }()
}
func (c *Cron) Stop() context.Context {
   c.runningMu.Lock()
   defer c.runningMu.Unlock()
   if c.running {
      c.stop <- struct{}{}
      c.running = false
   }
   ctx, cancel := context.WithCancel(context.Background())
   // 异步停止
   go func() {
      c.jobWaiter.Wait()
      cancel()
   }()
   return ctx
}
ctx := cron.Stop()
select {
case <-ctx.Done():
}

两种执行模式,通过函数和interface

func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) 
func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) 

job执行之前和之后的装饰器

使用方法:
cron := New(WithChain(SkipIfStillRunning(DiscardLogger),Recover(newBufLogger(&buf))))

调用过程:
func New(opts ...Option) *Cron 
func WithChain(wrappers ...JobWrapper) Option
NewChain(c ...JobWrapper) Chain
type Chain struct {
    wrappers []JobWrapper
}
func (c *Cron) Start()
func (c *Cron) run()
func (c *Cron) AddFunc(spec string, cmd func()) 
func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) 
func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID
func (c Chain) Then(j Job) Job
func (c *Cron) startJob(j Job)

核心函数的实现

// run the scheduler.. this is private just due to the need to synchronize
// access to the 'running' state variable.
// 运行调度器.. 这是私有的只是因为需要同步
// 访问 'running' 状态变量。
func (c *Cron) run() {
   c.logger.Info("start")

   // Figure out the next activation times for each entry.
   now := c.now()
   for _, entry := range c.entries {
      entry.Next = entry.Schedule.Next(now)
      c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next)
   }

   for {
      // Determine the next entry to run.
      sort.Sort(byTime(c.entries))

      var timer *time.Timer
      if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
         // If there are no entries yet, just sleep - it still handles new entries
         // and stop requests.
         timer = time.NewTimer(100000 * time.Hour)
      } else {
         timer = time.NewTimer(c.entries[0].Next.Sub(now))
      }

      for {
         select {
         case now = <-timer.C:
            now = now.In(c.location)
            c.logger.Info("wake", "now", now)

            // Run every entry whose next time was less than now
            for i, e := range c.entries {
               fmt.Println("第几次", i)
               if e.Next.After(now) || e.Next.IsZero() {
                  break
               }
               c.startJob(e.WrappedJob)
               e.Prev = e.Next
               e.Next = e.Schedule.Next(now)
               c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next)
            }

         case newEntry := <-c.add:
            timer.Stop()
            now = c.now()
            newEntry.Next = newEntry.Schedule.Next(now)
            c.entries = append(c.entries, newEntry)
            c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next)

         case replyChan := <-c.snapshot:
            replyChan <- c.entrySnapshot()
            continue

         case <-c.stop:
            timer.Stop()
            c.logger.Info("stop")
            return

         case id := <-c.remove:
            timer.Stop()
            now = c.now()
            c.removeEntry(id)
            c.logger.Info("removed", "entry", id)
         }

         break
      }
   }
}

使用timer而不是ticker的好处,使用多个ticker有可能会被覆盖,比如case 1秒,2秒。那么第二秒的时候有可能执行case 1,也有可能执行case 2