robig/cron源码解析

1、背景

本文旨在走读cron源码来更好的理解时间表达式中各项字符串的意思。
本文基于 github.com/robfig/cron@v1.2.0 (公司项目用的这个版本,理解一下)
cron满足标准语义https://zh.wikipedia.org/wiki/Cron

2、简单的使用方式

func TestCron(t *testing.T){
	// 新建实例
	cronTimer := cron.New()
	// 添加定时任务
	_=cronTimer.AddFunc("0 0 0 0 * *",PrintSth)
	// 开启定时任务
	cronTimer.Start()
	// 停止定时任务
	cronTimer.Stop()
}

func PrintSth(){
	fmt.Println("1")
}

3、New() *Cron

创建一个调度任务管理器实例
如下是执行New()方法后返回的结构体

如下是该结构体对外提供的方法

4、cron.AddFunc

添加调度任务

// AddFunc adds a func to the Cron to be run on the given schedule.
func (c *Cron) AddFunc(spec string, cmd func()) error {
	return c.AddJob(spec, FuncJob(cmd))
}

// AddJob adds a Job to the Cron to be run on the given schedule.
func (c *Cron) AddJob(spec string, cmd Job) error {
	// 解析cron表达式
	schedule, err := Parse(spec)
	if err != nil {
		return err
	}
	c.Schedule(schedule, cmd)
	return nil
}

// Schedule adds a Job to the Cron to be run on the given schedule.
func (c *Cron) Schedule(schedule Schedule, cmd Job) {
	// 封装下次执行时间,需要执行的方法
	entry := &Entry{
		Schedule: schedule,
		Job:      cmd,
	}
  	// 非运行状态 直接添加到调度任务列表里
    if !c.running {
 	   c.entries = append(c.entries, entry)
		return
	}
	// 运行状态,添加到任务管道,随后会找个机会加入调度任务列表
	c.add <- entry
}

5、cron.Start()

启动调度任务管理器
在这里插入图片描述

// Start the cron scheduler in its own go-routine, or no-op if already started.
func (c *Cron) Start() {
	if c.running {
		return
	}
	c.running = true
	go c.run()
}

// Run the cron scheduler, or no-op if already running.
func (c *Cron) Run() {
	if c.running {
		return
	}
	c.running = true
	c.run()
}

// Run the scheduler. this is private just due to the need to synchronize
// access to the 'running' state variable.
func (c *Cron) run() {
	// Figure out the next activation times for each entry.
	now := c.now()
	for _, entry := range c.entries {
		entry.Next = entry.Schedule.Next(now)
	}

	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)
				// Run every entry whose next time was less than now
				for _, e := range c.entries {
					if e.Next.After(now) || e.Next.IsZero() {
						break
					}
					go c.runWithRecovery(e.Job)
					e.Prev = e.Next
					e.Next = e.Schedule.Next(now)
				}
			// 新增调度任务
			case newEntry := <-c.add:
				timer.Stop()
				now = c.now()
				newEntry.Next = newEntry.Schedule.Next(now)
				c.entries = append(c.entries, newEntry)
			// 创建调度任务列表的副本
			case <-c.snapshot:
				c.snapshot <- c.entrySnapshot()
				continue
			// 管理器停止的信号
			case <-c.stop:
				timer.Stop()
				return
			}

			break
		}
	}
}

6、Parser.parse(spec string)

解析cron表达式为便于计算的数据结构

1、调度任务的数据结构

Schedule:cron表达式解析后的数据结构
Next: 该调度任务的下次执行时间
Prev:该调度任务的上次执行时间
Job:封装了调度任务实际要执行的方法
在这里插入图片描述
Schedule的默认实现结构体,对应cron表达式中的6个表达式:
在这里插入图片描述

2、如何使用uint64来存储复杂的cron表达式

由常识已知:
Second (秒)的范围是 [0, 59]
Minute (分钟)的范围是 [0, 59]
Hour (时)的范围是 [0, 23]
Dom (Day of month 一个月中的第几天)的范围是 [1, 31]
Month (月)的范围是 [1, 12]
Dow (Day of week 一星期的第几天)的范围是**[0, 6]**,0为星期日

因此若使用长度>60的数组来存储cron表达式的各个值,即可满足存储需求。

这里robig/cron使用64位int类型的二进制表示各值是否存在。1表示有值,0表示无值,首bit位用来表示是否使用了*

举例:

表达式: 1,55 20-26/2 12 * * *

含义: 每天的 12:20:01 ,12:20:55, 12:22:01 ,12:22:55 ,12:24:01, 12:24:55 ,12:26:01 ,12:26:55 执行一次任务

存储结构:
在这里插入图片描述
表达式解析流程:

  1. 初始化用来存储的uint64数值为0
  2. , 符号 为分隔符 分割为一个数组,遍历此数组
    1. 计算可取数值的最小值和最大值,若有/则已左侧字符串来取值
      • 若此值为 * ,最小值和最大值为默认,将最大bit位置为1
      • 若此值为单个数值 x, 最小值和最大值都置为x
      • 若此值为范围值 x-y,最小值置为x,最大值为y
    2. 计算可取数值的最小值和最大值,若有/则已左侧字符串来取值
      • 若有/,则步长为右侧数值
      • 若无/,则步长为1
    3. 从最小值到最大值,每隔一个步长,对初始值进行运算

7、Entry.Schedule.Next(t time.Time)

计算调度任务的下次执行时间

从上述流程可以总结出几点

  • 下次调度时间计算顺序为 月->日->时->分→秒,且有年限制(5年),防止进入无限循环
  • Dom和Dow满足https://zh.wikipedia.org/wiki/Cron的语义,即Dom和Dow互斥(OR的关系),有其中一个满足条件即可

8、v1.2.0较严重的bug

表象: dom和dow若使用*进行大于1的步长运算,那么它们俩将会变成(AND的关系),不符合标准cron的语义

解决方式: 升级到V3,相关issue: https://github.com/robfig/cron/issues/70

解析cron表达式的相关代码:

// 获取逗号分隔后的某个字符串对应的uint64值,秒/分/时/day of month/月/day of week 都会调用这个函数
// 例如 1-5/2 * * * * *的秒位 会被解析成 0101010
func getRange(expr string, r bounds) (uint64, error) {

	var (
		start, end, step uint
		rangeAndStep     = strings.Split(expr, "/")
		lowAndHigh       = strings.Split(rangeAndStep[0], "-")
		singleDigit      = len(lowAndHigh) == 1
		err              error
	)
	var extra uint64
	if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
		start = r.min
		end = r.max
		extra = starBit
	} else {
		//...斜杆左侧值不为 * 号时,解析 start 和end的值
	}

	// ...解析斜杆右侧值为步长 step
	// ...校验解析结果是否合法
	 return getBits(start, end, step) | extra, nil
}

// getBits sets all bits in the range [min, max], modulo the given step size.
func getBits(min, max, step uint) uint64 {
	var bits uint64

	// If step is 1, use shifts.
	if step == 1 {
		return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
	}

	// Else, use a simple loop.
	for i := min; i <= max; i += step {
		bits |= 1 << i
	}
	return bits
}

判断当天(Day)是否满足cron表达式:

// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
// s是配置好的表达式,t是当前时间
func dayMatches(s *SpecSchedule, t time.Time) bool {
	var (
		domMatch bool = 1<<uint(t.Day())&s.Dom > 0
		dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
	)
	// 只有一个变量不为*,则满足此变量的条件就返回true
	if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
		return domMatch && dowMatch
	}
	// 都指定变量,满足其中一个就返回true
	return domMatch || dowMatch
}

这里存在一个bug,“只有一个变量不为*” 和"则满足此变量的条件就返回true"的执行行为不一致

即代码不满足 若s.Dow&starBit > 0成立 , dowMatch必须为true

解决方式:“只有一个变量不为*” → “只有一个变量没赋全部值”

如果有帮助到您,欢迎点赞
如果有任何疑问,欢迎评论

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

热心小伙chj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值