go-cron源码分析(二、Parser解析器)

news/2024/6/29 12:02:42 标签: golang, go-cron, cron, parser, 秒定时器

文章目录

    • 2.1 Parser解析器
    • 2.2 NewParser()解析
      • 2.2.1 ParseOption类型
      • 2.2.2 Parser解析类
    • 2.3 func (p Parser) Parse(spec string)(Schedule, error)
    • 2.4 parseDescriptor(descriptor string, loc *time.Location)
      • 2.4.1 all()
      • 2.4.2 bounds
      • 2.4.3 getBits()
    • 2.5 normalizeFields()
      • 2.5.1 field匿名函数
      • 2.5.2 getField()
      • 2.5.3 getRange()
      • 2.5.4 其他工具函数
    • 2.6 standardParser
    • 2.7 总结

2.1 Parser解析器

接下来我们先来看看解析器,先挑软柿子捏,哈哈哈。

还从昨天修改Parser解析器开始讲起:

func TestWithParser(t *testing.T) {
	var parser = NewParser(Dow)			// 这里就new了一个解析器
	c := New(WithParser(parser))		// 这里用新的解析器替换了缺省的解析器
	if c.parser != parser {
		t.Error("expected provided parser")
	}
}

2.2 NewParser()解析

接下来我们分析一下NewParser()函数

// NewParser creates a Parser with custom options.
// NewParser 创建一个 Parser with 自定义选项
//
// It panics if more than one Optional is given, since it would be impossible to
// correctly infer which optional is provided or missing in general.
// 如果给出了多个可选项,则会出现恐慌,因为一般情况下不可能正确地推断哪些可选选项被提供或缺失
//
// Examples
//
//  // Standard parser without descriptors
//  没有描述符的标准解析器
//  specParser := NewParser(Minute | Hour | Dom | Month | Dow)
//  sched, err := specParser.Parse("0 0 15 */3 *")
//
//  // Same as above, just excludes time fields
//	和上面一样,只是不包括时间字段
//  specParser := NewParser(Dom | Month | Dow)
//  sched, err := specParser.Parse("15 */3 *")
//
//  // Same as above, just makes Dow optional
//  和上面一样,只是包括了 Dow optional
//  specParser := NewParser(Dom | Month | DowOptional)
//  sched, err := specParser.Parse("15 */3")
//
func NewParser(options ParseOption) Parser {
	optionals := 0
	if options&DowOptional > 0 {
		optionals++
	}
	if options&SecondOptional > 0 {
		optionals++
	}
	if optionals > 1 {			// 这个限制只能是 DowOptional 或者是 SecondOptional 或者没有
		panic("multiple optionals may not be configured")
	}
	return Parser{options}   // 生成一个解析类对象
}

2.2.1 ParseOption类型

我们先来分析这个类型:

其实这个类型是int型,我们看看源码:

// Configuration options for creating a parser. Most options specify which
// fields should be included, while others enable features. If a field is not
// included the parser will assume a default value. These options do not change
// the order fields are parse in.
// 用于创建解析器的配置选项。大多数选项指定应该包括哪些字段,而其他选项启用特性.
// 如果没有包含某个字段,解析器将假定一个默认值。这些选项不会改变解析字段的顺序。
type ParseOption int   // 其实是int类型

// 取值,就是每个宏占一个位,实际传参:
// NewParser(Minute | Hour | Dom | Month | Dow)
const (
	Second         ParseOption = 1 << iota // Seconds field, default 0
	SecondOptional                         // Optional seconds field, default 0
	Minute                                 // Minutes field, default 0
	Hour                                   // Hours field, default 0
	Dom                                    // Day of month field, default *
	Month                                  // Month field, default *
	Dow                                    // Day of week field, default *
	DowOptional                            // Optional day of week field, default *
	Descriptor                             // Allow descriptors such as @monthly, @weekly, etc.
)

2.2.2 Parser解析类

代码最后一句话是生成一个Parser类的对象:

// A custom Parser that can be configured.
type Parser struct {
	options ParseOption			// 成员就只有,选项,其他的就是成员函数了
}

2.3 func (p Parser) Parse(spec string)(Schedule, error)

// Parse returns a new crontab schedule representing the given spec.
// Parse返回一个新的crontab计划,表示给定的规范
// It returns a descriptive error if the spec is not valid.
// 如果规范无效,它将返回一个描述性错误。
// It accepts crontab specs and features configured by NewParser.
// 它接受NewParser配置的crontab规范和特性。
func (p Parser) Parse(spec string) (Schedule, error) {
	if len(spec) == 0 {
		return nil, fmt.Errorf("empty spec string")
	}

	// Extract timezone if present
	var loc = time.Local
	// TZ是世界标准时间
    // 例子:TZ=Asia/Tokyo
	if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") {
		var err error
		i := strings.Index(spec, " ")
		eq := strings.Index(spec, "=")
		// 转为对应的时间
		if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil {
			return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err)
		}
		spec = strings.TrimSpace(spec[i:])
	}

	// Handle named schedules (descriptors), if configured
	// 处理已配置的命名计划(描述符)
	if strings.HasPrefix(spec, "@") {
		if p.options&Descriptor == 0 {		// 解析器需要支持描述
			return nil, fmt.Errorf("parser does not accept descriptors: %v", spec)
		}
		return parseDescriptor(spec, loc)
	}

	// Split on whitespace.
	fields := strings.Fields(spec)   // 对字符串以空格分割 例:5 * * * *

	// Validate & fill in any omitted or optional fields
	// 验证并填写任何省略或可选字段
	var err error
	fields, err = normalizeFields(fields, p.options)   // 验证对应的参数,并返回真正的参数,如果参数不全,会自动补全
	if err != nil {
		return nil, err
	}

	field := func(field string, r bounds) uint64 {
		if err != nil {
			return 0
		}
		var bits uint64
		bits, err = getField(field, r)
		return bits
	}

	var (
		second     = field(fields[0], seconds)
		minute     = field(fields[1], minutes)
		hour       = field(fields[2], hours)
		dayofmonth = field(fields[3], dom)
		month      = field(fields[4], months)
		dayofweek  = field(fields[5], dow)
	)
	if err != nil {
		return nil, err
	}

	return &SpecSchedule{		// 构造一个调度器的对象
		Second:   second,
		Minute:   minute,
		Hour:     hour,
		Dom:      dayofmonth,
		Month:    month,
		Dow:      dayofweek,
		Location: loc,
	}, nil
}

这个函数是完整的解析函数,里面包含了TZ世界标准时间和@前缀的描述进行解析。

最后还调用了normalizeFields()这个函数来解析参数,最后的最后生成了一个调度器的对象。

2.4 parseDescriptor(descriptor string, loc *time.Location)

解析@前缀的时间,我们来看看这么解析的:

// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
// parseDescriptor返回表达式的预定义调度,如果没有匹配则会出错
func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) {
	switch descriptor {
	case "@yearly", "@annually":
		return &SpecSchedule{				//	调度器
			Second:   1 << seconds.min,
			Minute:   1 << minutes.min,
			Hour:     1 << hours.min,
			Dom:      1 << dom.min,
			Month:    1 << months.min,
			Dow:      all(dow),				// 这个要好好分析一下下
			Location: loc,
		}, nil

	case "@monthly":					// 每月
		return &SpecSchedule{
			Second:   1 << seconds.min,
			Minute:   1 << minutes.min,
			Hour:     1 << hours.min,
			Dom:      1 << dom.min,
			Month:    all(months),
			Dow:      all(dow),
			Location: loc,
		}, nil

	case "@weekly":						// 每周
		return &SpecSchedule{
			Second:   1 << seconds.min,
			Minute:   1 << minutes.min,
			Hour:     1 << hours.min,
			Dom:      all(dom),
			Month:    all(months),
			Dow:      1 << dow.min,
			Location: loc,
		}, nil

	case "@daily", "@midnight":				// 每天
		return &SpecSchedule{
			Second:   1 << seconds.min,
			Minute:   1 << minutes.min,
			Hour:     1 << hours.min,
			Dom:      all(dom),
			Month:    all(months),
			Dow:      all(dow),
			Location: loc,
		}, nil

	case "@hourly":					// 每小时
		return &SpecSchedule{
			Second:   1 << seconds.min,
			Minute:   1 << minutes.min,
			Hour:     all(hours),
			Dom:      all(dom),
			Month:    all(months),
			Dow:      all(dow),
			Location: loc,
		}, nil

	}

    // {secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}},
	const every = "@every "					// 这个是定时
	if strings.HasPrefix(descriptor, every) {
		duration, err := time.ParseDuration(descriptor[len(every):])	// 字符串转成 时间间隔
		if err != nil {
			return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err)
		}
		return Every(duration), nil		// 这个函数要好好分析,延时调度器,下一节分析
	}

	return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor)
}

2.4.1 all()

all函数这么多,我们来看看里面是啥意思。

// all returns all bits within the given bounds.  (plus the star bit)
// All返回给定边界内的所有位。(加上星号位)
func all(r bounds) uint64 {					// 这个初看还是有点懵逼的,这个bounds其实也是一个结构体,描述秒,分,时的范围的,我们可以来看看	
	return getBits(r.min, r.max, 1) | starBit	// startBit是一个常量,目前也不知道有啥用	
}

// starBit
const (
	// Set the top bit if a star was included in the expression.
	// 如果表达式中包含星号,则设置顶部位。
	starBit = 1 << 63
)

2.4.2 bounds

// bounds provides a range of acceptable values (plus a map of name to value).
// Bounds提供可接受值的范围(加上名称到值的映射)。
type bounds struct {			// 这个一看也是懵逼状态,不知道这个结构的意义,下面我们来看看用这个结构体定义的变量就明白了
	min, max uint
	names    map[string]uint
}

// The bounds for each field.
var (
	seconds = bounds{0, 59, nil}		// 这个就是秒的范围
	minutes = bounds{0, 59, nil}		// 分的范围
	hours   = bounds{0, 23, nil}		// 时的范围
	dom     = bounds{1, 31, nil}		// 一个月第几天的范围
	months  = bounds{1, 12, map[string]uint{	// 月
		"jan": 1,
		"feb": 2,
		"mar": 3,
		"apr": 4,
		"may": 5,
		"jun": 6,
		"jul": 7,
		"aug": 8,
		"sep": 9,
		"oct": 10,
		"nov": 11,
		"dec": 12,
	}}
	dow = bounds{0, 6, map[string]uint{		// 一个星期的第几天
		"sun": 0,
		"mon": 1,
		"tue": 2,
		"wed": 3,
		"thu": 4,
		"fri": 5,
		"sat": 6,
	}}
)

看到这里就好奇了,这么数据怎么表示我们定时的时间呢?我们继续分析

2.4.3 getBits()

// getBits sets all bits in the range [min, max], modulo the given step size.
// getBits设置范围[min, max]内的所有位,取给定步长的模。
func getBits(min, max, step uint) uint64 {
	var bits uint64

	// If step is 1, use shifts.
	// 如果step为1,则使用移位。
	// 我们现在传参是1
	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
}

这一个流程分析完了,我们还留下了疑问,SpecSchedule类的分析,getBits()返回的这些位是有什么用?还有Every()函数返回ConstantDelaySchedule类对象?

这个我们下节带着这些问题好好分析,接下来,我们看看传统配置的处理方式。

2.5 normalizeFields()

这个函数主要是判断传参是否正确,并且把参数解析成为代码默认的参数(因为有些配置只需要年月日,所以把时分秒补上)

// normalizeFields takes a subset set of the time fields and returns the full set
// with defaults (zeroes) populated for unset fields.
// normalizeFields接受时间字段的子集,并返回完整的时间字段集,为未设置的字段填充默认值(零)。
//
// As part of performing this function, it also validates that the provided
// 作为执行这个功能的一部分,它还验证所提供的字段是否与配置的选项兼容。
// fields are compatible with the configured options.
func normalizeFields(fields []string, options ParseOption) ([]string, error) {
	// Validate optionals & add their field to options
	optionals := 0
	if options&SecondOptional > 0 {  // 不能很明白SecondOptional 和 Second的区别
		options |= Second
		optionals++
	}
	if options&DowOptional > 0 {
		options |= Dow
		optionals++
	}
    // 这里再次判断两个不能一起出现
	if optionals > 1 {
		return nil, fmt.Errorf("multiple optionals may not be configured")
	}

	// Figure out how many fields we need
	// 算出我们需要多少字段
	max := 0
	for _, place := range places {    // place 是自定义的值
		if options&place > 0 {
			max++						// 计算最大的字段
		}
	}
	min := max - optionals

	// Validate number of fields
	// 验证fields是否正确
	if count := len(fields); count < min || count > max {
		if min == max {
			return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields)
		}
		return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields)
	}

	// Populate the optional field if not provided
	// 如果没有提供,填充可选字段。正常填充是不会进来
	if min < max && len(fields) == min {
		switch {
		case options&DowOptional > 0:
			fields = append(fields, defaults[5]) // TODO: improve access to default
		case options&SecondOptional > 0:
			fields = append([]string{defaults[0]}, fields...)
		default:
			return nil, fmt.Errorf("unknown optional field")
		}
	}

	// Populate all fields not part of options with their defaults
	// 用默认值填充所有不属于选项的字段
	n := 0
	expandedFields := make([]string, len(places))
	copy(expandedFields, defaults)		//先以缺省的字段填充
	for i, place := range places {
		if options&place > 0 {  // 如果没有这个字段,expandedFields就是默认值
			expandedFields[i] = fields[n]   // 填充匹配的字段的值
			n++
		}
	}
	return expandedFields, nil
}

// places就是平常的6个
var places = []ParseOption{
	Second,
	Minute,
	Hour,
	Dom,
	Month,
	Dow,
}

// defaults对应默认值
var defaults = []string{
	"0",
	"0",
	"0",
	"*",
	"*",
	"*",
}

2.5.1 field匿名函数

我们接着来分析这个匿名函数,

field := func(field string, r bounds) uint64 {
		if err != nil {
			return 0
		}
		var bits uint64
		bits, err = getField(field, r)   // 看看这个函数里面做了啥
		return bits
	}

	var (
		second     = field(fields[0], seconds)
		minute     = field(fields[1], minutes)
		hour       = field(fields[2], hours)
		dayofmonth = field(fields[3], dom)
		month      = field(fields[4], months)
		dayofweek  = field(fields[5], dow)
	)

2.5.2 getField()

func getField(field string, r bounds) (uint64, error) {
	var bits uint64
	ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
	for _, expr := range ranges {
		bit, err := getRange(expr, r)    // 返回对应的 位数据
		if err != nil {
			return bits, err
		}
		bits |= bit				// 把所有的 位数据都或起来
	}
	return bits, nil
}

2.5.3 getRange()

分析传参,并且通过参数返回对应的位数据

// getRange returns the bits indicated by the given expression:
// getRange返回由给定表达式指定的位:
//   number | number "-" number [ "/" number ]
// or error parsing range.
func getRange(expr string, r bounds) (uint64, error) {
	var (
		start, end, step uint
		// */2		需要解析分号情况
		rangeAndStep     = strings.Split(expr, "/")
		// 5-6/2    需要直接-
		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, err = parseIntOrName(lowAndHigh[0], r.names)
		if err != nil {
			return 0, err
		}
		switch len(lowAndHigh) {
		case 1:
			end = start		// 如果是一个值,范围就是 一个
		case 2:
			end, err = parseIntOrName(lowAndHigh[1], r.names)		// 如果是 5-6这种, 范围就是 5 到 6
			if err != nil {
				return 0, err
			}
		default:
			return 0, fmt.Errorf("too many hyphens: %s", expr)
		}
	}

	switch len(rangeAndStep) {		// 看看有没有 5/6 这种情况
	case 1:
		step = 1
	case 2:
		step, err = mustParseInt(rangeAndStep[1])
		if err != nil {
			return 0, err
		}

		// Special handling: "N/step" means "N-max/step".
		if singleDigit {
			end = r.max
		}
		if step > 1 {
			extra = 0
		}
	default:
		return 0, fmt.Errorf("too many slashes: %s", expr)
	}

	if start < r.min {
		return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
	}
	if end > r.max {
		return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
	}
	if start > end {
		return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
	}
	if step == 0 {
		return 0, fmt.Errorf("step of range should be a positive number: %s", expr)
	}

	return getBits(start, end, step) | extra, nil    // 这个步进在这里用的
}

2.5.4 其他工具函数

// parseIntOrName returns the (possibly-named) integer contained in expr.
// 解析字符串for name,如果有names这个字段,就直接在map中取数据
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
	if names != nil {
		if namedInt, ok := names[strings.ToLower(expr)]; ok {   // 如果是有names的,就直接在map,取值
			return namedInt, nil
		}
	}
	return mustParseInt(expr)   // 剩下的就只有秒 分 时
}

// mustParseInt parses the given expression as an int or returns an error.
// 这个就直接字符串转int了
func mustParseInt(expr string) (uint, error) {
	num, err := strconv.Atoi(expr)			// 直接这个转
	if err != nil {
		return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err)
	}
	if num < 0 {
		return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr)
	}

	return uint(num), nil
}

2.6 standardParser

其实源码中默认定义了一个标准解析器,如果我们没有传新的解析器,系统就会默认使用这个。

var standardParser = NewParser(
	Minute | Hour | Dom | Month | Dow | Descriptor,
)

// ParseStandard returns a new crontab schedule representing the given
// standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries
// representing: minute, hour, day of month, month and day of week, in that
// order. It returns a descriptive error if the spec is not valid.
//
// It accepts
//   - Standard crontab specs, e.g. "* * * * ?"
//   - Descriptors, e.g. "@midnight", "@every 1h30m"
func ParseStandard(standardSpec string) (Schedule, error) {
	return standardParser.Parse(standardSpec)
}

默认解析器是没有秒的,这也是linux的cron的定时,也是没有秒的,所以我们如果需要秒的定时,就需要自定义解析器。这个看上一节就明白了。

2.7 总结

这一节主要介绍了解析器,可以自定义解析器。

  1. 系统始终是6位的解析器,如果自定义解析器减少了某一个,系统会自动补成缺省值。
  2. 解析器有两种格式,一种是@xxx 一种是 x x x x x。两种解析走不同分支
  3. 不管走那个分支,最后都是生成一个SpecSchedule对象或者ConstantDelaySchedule对象,这个这一节暂不分析
  4. 生成SpecSchedule对象,都需要转参,并且参数是一堆按位操作的数,这些数,这一节也暂不分析。

带着这两个问题,期待下一节分析。


http://www.niftyadmin.cn/n/4396.html

相关文章

web前端课程设计——动漫网页2个网页HTML+CSS web前端开发技术 web课程设计 网页规划与设计

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 ⚽精彩专栏推荐&#x1…

一名程序员的电脑桌面

配置&#xff1a; 酷呆桌面注册表隐藏快捷方式箭头图标开启桌面模式自动隐藏任务栏 酷呆桌面 在选择酷呆之前&#xff0c;一直是使用的Fences&#xff0c;他的桌面切换功能非常赞&#xff0c;适合划分工作区。但由于强迫症实在是忍受不了肉眼可见的掉帧、黑背景bug&#xff0…

JAVA--枚举类

一、定义&#xff1a; 类的对象是有限个、确定的。如&#xff1a; 性别&#xff1a;男、女&#xff1b; 线程状态&#xff1a;创建、就绪、运行、堵塞、死亡&#xff1b; 当需要定义一组常量时&#xff0c;强烈建议使用枚举类。 二、属性&#xff1a; 1、枚举类对象的属性…

便宜又大碗!AI将画廊轻松搬到自家墙壁;用隐写术在图像中存储文件;免费书·算法高维鲁棒统计;关节式手部模型数据集;前沿论文 | ShowMeAI资讯日报

&#x1f440;日报合辑 | &#x1f4c6;电子月刊 | &#x1f514;公众号下载资料 | &#x1f369;韩信子 &#x1f4e2; Mixtiles&#xff1a;将画廊搬到自家墙壁&#xff0c;“便宜又大碗”的艺术平替 https://www.mixtiles.com/ Mixtiles 是一家快速发展的照片创业公司&…

网络协议:透彻解析HTTPS协议

本篇内容包括&#xff1a;HTTPS 协议概述&#xff0c;SSL 与 TLS&#xff08;SSL 与 TLS 概述、SSL证书 和 SSL 与 TLS 二者之间关系&#xff09;&#xff0c;以及 HTTPS 下浏览器访问一个网站的全过程的内容&#xff01; 文章目录一、HTTPS 协议概述二、SSL 与 TLS1、SSL 与 T…

电脑技巧:Win10自带的6个实用功能,你都会用吗

目录 1、系统自带计算器 2、Win10自带截图功能 3、 Win10自带的手写输入 4、历史剪贴板 5、定时提醒 6、自带录屏功能 1、系统自带计算器 系统里自带了一个计算器大家都知道&#xff0c;打开是这个样子。 但当你点击计算器左上角的三个横杠图标后会发现这个计算器功能非常强大。…

【RHCE】作业:ansible playbook中结合yaml运用各种模块

目录 1.创建一个play, 用来创建用户和用户组 第一个任务&#xff1a;创建用户usertest并指定uid为2000&#xff0c;shell类型为/bin/sh 第二个任务&#xff1a;创建用户组grouptest 并指定gid为1999 2.创建一个play&#xff0c;用来创建文件并往文件中写入内容, 并压缩 第一个…

智能运维应用之道,告别企业数字化转型危机

面临的问题及挑战 数据中心发展历程 2000 年中国数据中心始建&#xff0c;至今已经历以下 3 大阶段。早期&#xff1a;离散型数据中心 IT 因以项目建设为导向&#xff0c;故缺乏规划且无专门运维管理体系&#xff0c;此外&#xff0c;开发建设完的项目均是独立运维维护&#…