package crontab import ( "errors" "fmt" "log" "reflect" "regexp" "strings" "time" "github.com/satori/go.uuid" "474420502.top/eson/structure/circular_linked" "github.com/Pallinder/go-randomdata" "github.com/davecgh/go-spew/spew" ) // StatusType 设置状态的类型 type StatusType int const ( _ StatusType = iota // SExecuteOK 设置这个次成功或者失败的状态 SExecuteOK // SExecuteSleep 设置下次Sleep时间, 只影响一次 SExecuteSleep // SExecuteCrontab 设置改写Crontab, 覆盖以前的Crontab SExecuteCrontab ) // Force 强制下次执行 type Force struct { sleep time.Duration } // NextTime 下次执行时间 func (force *Force) NextTime() time.Time { return time.Now().Add(force.sleep) } // Crontab 的string解析 type Crontab struct { crontab string uid uuid.UUID force *Force min []timePointer hour []timePointer day []timePointer month []timePointer week []timePointer WillPlans []time.Time SkipPlans []time.Time YearPlan *trieYear interval *clinked.CircularLinked lastStatus bool isCalculated bool trueCount int failCount int nextTime time.Time } // NewCrontab create 一个crontab func NewCrontab(crontab string) *Crontab { cron := &Crontab{} cron.crontab = strings.TrimSpace(crontab) cron.FromString(cron.crontab) return cron } // UnmarshalYAML 添加序列化接口 Marshal没实现, 需要的时候可以自己添加 func (cron *Crontab) UnmarshalYAML(unmarshal func(interface{}) error) error { var buf string err := unmarshal(&buf) if err != nil { return nil } if err := cron.FromString(buf); err != nil { return err } return nil } // SetStatus 设置上次状态 true false func (cron *Crontab) SetStatus(statusType StatusType, statusValue ...interface{}) { switch statusType { case SExecuteOK: if cron.interval != nil { cron.lastStatus = statusValue[0].(bool) } case SExecuteSleep: force := new(Force) ivalue := statusValue[0] switch value := ivalue.(type) { case int: force.sleep = time.Duration(value) * time.Second case int64: force.sleep = time.Duration(value) * time.Second case time.Duration: force.sleep = value default: panic(errors.New("statusValue type is error, the type is" + reflect.TypeOf(ivalue).String())) } cron.force = force case SExecuteCrontab: crontab := statusValue[0].(string) cron.crontab = strings.TrimSpace(crontab) cron.FromString(cron.crontab) default: panic(errors.New("StatusType is unknown, the type " + reflect.TypeOf(statusType).String())) } } // // GetStatus 获取上次状态 true false // func (cron *Crontab) GetStatus() (status bool) { // return cron.lastStatus // } // TimeUp 是否时间快到, 时间间隔调用完TimeUp后必须调用NextTime, 为了精准控制时间 func (cron *Crontab) TimeUp() bool { if cron.interval != nil { return cron.intervalTimeUp() } return cron.linuxTimeUp() } // NextTime 返回下次任务的时间 func (cron *Crontab) NextTime() time.Time { if cron.force != nil { nt := cron.force.NextTime() cron.force = nil return nt } if cron.interval != nil { if !cron.isCalculated { now := time.Now() cron.intervalCalculateNextTime(now) } return cron.nextTime } if len(cron.WillPlans) > 0 { return cron.WillPlans[0] } return time.Now().Add(time.Second * 2) } func (cron *Crontab) String() string { return fmt.Sprintf("min:%s\nhour:%s\nday:%s\nmonth:%s\nweek:%s\n", spew.Sdump(cron.min), spew.Sdump(cron.hour), spew.Sdump(cron.day), spew.Sdump(cron.month), spew.Sdump(cron.week)) } // FromString 解析crontab 的 表达式 func (cron *Crontab) FromString(crontab string) error { crontab = cron.crontab uid, err := uuid.NewV4() if err != nil { panic(err) } cron.uid = uid cron.interval = nil cron.min = nil cron.hour = nil cron.day = nil cron.month = nil cron.week = nil cron.WillPlans = nil cron.SkipPlans = nil cron.YearPlan = nil matches := regexp.MustCompile("[^ ]+").FindAllString(crontab, -1) mlen := len(matches) switch mlen { case 1: // "f1-2|5-10x5,f1|10m,10-15,f1" cron.lastStatus = true cron.isCalculated = false cron.interval = clinked.NewCircularLinked() var intervalList []interface{} intervalList = parseIntervalString(matches[0]) cron.interval.Append(intervalList...) cron.TimeUp() case 5: cron.min = createTimePointer(matches[0], 0, 59, true) cron.hour = createTimePointer(matches[1], 0, 23, true) cron.day = createTimePointer(matches[2], 1, 31, false) cron.month = createTimePointer(matches[3], 1, 12, true) cron.week = createTimePointer(matches[4], 0, 6, true) cron.createYearPlan() cron.TimeUp() default: return errors.New("mathches len != want, check crontab string") } return nil } // createYearPlan 创建年度计划 func (cron *Crontab) createYearPlan() { cron.YearPlan = newTrieYear() cron.YearPlan.FromCrontab(cron) } func (cron *Crontab) linuxTimeUp() bool { now := time.Now() maxlen := 1000 createlen := 500 plen := len(cron.WillPlans) if plen <= createlen { // 如果当前生成的计划表少于 限制的500. 就生成新的计划表 var lastplan time.Time if plen == 0 { lastplan = now } else { lastplan = cron.WillPlans[plen-1].Add(time.Minute) } if !cron.YearPlan.CheckYear() { cron.createYearPlan() } timeplans := cron.YearPlan.GetPlanTime(cron, lastplan, uint(maxlen-plen)) cron.WillPlans = append(cron.WillPlans, timeplans...) } if len(cron.WillPlans) > 0 { istimeup := false for i := 0; i < maxlen; i++ { // 统计过了多少计划任务时间表 i - 1 if now.Unix() >= cron.WillPlans[i].Unix() { istimeup = true } else { // 清除过时的计划任务时间表 if istimeup { if i-1 > 0 { cron.SkipPlans = append(cron.SkipPlans, cron.WillPlans[0:i-1]...) if len(cron.SkipPlans) >= maxlen+200 { cron.SkipPlans = cron.SkipPlans[200:] } } cron.WillPlans = cron.WillPlans[i:] return istimeup } return istimeup } } // 如果所有计划表都不符合, 全部放到忽略的计划表上, 这个表方便打印查看, 因为程序执行超时过了多少计划任务 cron.SkipPlans = append(cron.SkipPlans, cron.WillPlans...) cron.WillPlans = nil return istimeup } log.Panicln("error willplans range") return false } // IntervalCalculateNextTime 计算时间间隔的下次时间 func (cron *Crontab) intervalCalculateNextTime(now time.Time) { iv := cron.interval.Cursor().GetValue().(*hInterval) isecond := 0 if iv.PlanFailCount.Size() == 0 && len(iv.PlanFail) == 0 { cron.lastStatus = true } if cron.lastStatus { cron.trueCount++ cron.failCount = 0 isecond = intervalPriorityListISecond(&iv.PlanTrueCount, cron.trueCount) if isecond == -1 { if len(iv.PlanTrue) > 0 { idx := randomdata.Number(len(iv.PlanTrue)) lr := iv.PlanTrue[idx] isecond = randomdata.Number(lr.left, lr.right+1) } else { isecond = 0 } } fmt.Println(time.Now().Format("01-02 15:04:05"), cron.uid.String(), "success:", cron.trueCount, " wait:", isecond) } else { cron.failCount++ cron.trueCount = 0 isecond = intervalPriorityListISecond(&iv.PlanFailCount, cron.failCount) if isecond == -1 { if len(iv.PlanFail) > 0 { idx := randomdata.Number(len(iv.PlanFail)) lr := iv.PlanFail[idx] isecond = randomdata.Number(lr.left, lr.right+1) } else { isecond = 0 } } fmt.Println(time.Now().Format("01-02 15:04:05"), cron.uid.String(), "fail:", cron.failCount, " wait:", isecond) } iv.Count-- if iv.Count <= 0 { iv.reset() cron.interval.MoveNext() } cron.isCalculated = true cron.nextTime = now.Add(time.Duration(isecond) * time.Second) } func (cron *Crontab) intervalTimeUp() bool { if cron.isCalculated != false { now := time.Now() if now.Unix() >= cron.nextTime.Unix() { cron.isCalculated = false return true } } return false }