Repository: rfyiamcool/cronlib Branch: master Commit: eeb0ee00ee37 Files: 11 Total size: 31.5 KB Directory structure: gitextract_wqk3jgme/ ├── README.md ├── cron_parser.go ├── cron_parser_test.go ├── example/ │ ├── easy/ │ │ └── easy.go │ ├── hard/ │ │ └── hard.go │ ├── multi/ │ │ └── multi.go │ └── parse_time/ │ └── parse_next_time.go ├── go.mod ├── go.sum ├── scheduler.go └── scheduler_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # cronlib Cronlib is easy golang crontab library, support parse crontab and schedule cron jobs. cron_parser.go import `https://github.com/robfig/cron/blob/master/parser.go`, thank @robfig ## Feature * thread safe * add try catch mode * dynamic modify job cron * dynamic add job * stop service job * add Wait method for waiting all job exit * async & sync mode ## Usage see more [example](github.com/rfyiamcool/example) ### quick run ```go package main import ( "log" "github.com/rfyiamcool/cronlib" ) var ( cron = cronlib.New() ) func main() { handleClean() go start() // cron already start, dynamic add job handleBackup() select {} } func start() { cron.Start() cron.Wait() } func handleClean() { job, err := cronlib.NewJobModel( "*/5 * * * * *", func() { pstdout("do clean gc action") }, ) if err != nil { panic(err.Error()) } err = cron.Register("clean", job) if err != nil { panic(err.Error()) } } func handleBackup() { job, err := cronlib.NewJobModel( "*/5 * * * * *", func() { pstdout("do backup action") }, ) if err != nil { panic(err.Error()) } err = cron.DynamicRegister("backup", job) if err != nil { panic(err.Error()) } } func pstdout(srv string) { log.Println(srv) } ``` ### set job attr open async mode and try catch mode ```go func run() error { cron := cronlib.New() // set async mode job, err = cronlib.NewJobModel( "0 * * * * *", func(), cronlib.AsyncMode(), cronlib.TryCatchMode(), ) ... } ``` other method ```go func run() error { cron := cronlib.New() // set async mode job, err = cronlib.NewJobModel( "0 * * * * *", func(), ) ... job.SetTryCatch(cronlib.OnMode) job.SetAsyncMode(cronlib.OnMode) ... } ``` ### stop job ```go cron := cronlib.New() ... cron.StopService(srvName) ``` ### update job ```go spec := "*/3 * * * * *" srv := "risk.scan.total.5s.to.3s" job, _ := cronlib.NewJobModel( spec, func() { stdout(srv, spec) }, ) err := cron.UpdateJobModel(srv, job) ... ``` ## Example ```go package main // test for crontab spec import ( "log" "time" "github.com/rfyiamcool/cronlib" ) func main() { cron := cronlib.New() specList := map[string]string{ "risk.scan.total.1s": "*/1 * * * * *", "risk.scan.total.2s": "*/2 * * * * *", "risk.scan.total.3s": "*/3 * * * * *", "risk.scan.total.4s": "*/4 * * * * *", "risk.scan.total.5s.to.3s": "*/5 * * * * *", } for srv, spec := range specList { tspec := spec // copy ssrv := srv // copy job, err := cronlib.NewJobModel( spec, func() { stdout(ssrv, tspec) }, ) if err != nil { panic(err.Error()) } err = cron.Register(srv, job) if err != nil { panic(err.Error()) } } // update test time.AfterFunc(10*time.Second, func() { spec := "*/3 * * * * *" srv := "risk.scan.total.5s.to.3s" log.Println("reset 5s to 3s", srv) job, _ := cronlib.NewJobModel( spec, func() { stdout(srv, spec) }, ) cron.UpdateJobModel(srv, job) log.Println("reset finish", srv) }) // kill test time.AfterFunc(3*time.Second, func() { srv := "risk.scan.total.1s" log.Println("stoping", srv) cron.StopService(srv) log.Println("stop finish", srv) }) time.AfterFunc(11*time.Second, func() { srvPrefix := "risk" log.Println("stoping srv prefix", srvPrefix) cron.StopServicePrefix(srvPrefix) }) cron.Start() cron.Wait() } func stdout(srv, spec string) { log.Println(srv, spec) } ``` ## Time Format Usage: **cronlib has second field, cronlibs contains six fields, first field is second than linux crontab** every 2 seconds ``` */2 * * * * * ``` every hour on the half hour ``` 0 30 * * * * ``` detail field desc ``` Field name | Mandatory? | Allowed values | Allowed special characters ---------- | ---------- | -------------- | -------------------------- Seconds | Yes | 0-59 | * / , - Minutes | Yes | 0-59 | * / , - Hours | Yes | 0-23 | * / , - Day of month | Yes | 1-31 | * / , - ? Month | Yes | 1-12 or JAN-DEC | * / , - Day of week | Yes | 0-6 or SUN-SAT | * / , - ? ``` cron parse doc: https://github.com/robfig/cron ================================================ FILE: cron_parser.go ================================================ package cronlib // // crontab parser import from https://github.com/robfig/cron/blob/master/parser.go // import ( "fmt" "math" "strconv" "strings" "time" ) // ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". // It does not support jobs more frequent than once a second. type ConstantDelaySchedule struct { Delay time.Duration } // Every returns a crontab Schedule that activates once every duration. // Delays of less than a second are not supported (will round up to 1 second). // Any fields less than a Second are truncated. func Every(duration time.Duration) ConstantDelaySchedule { if duration < time.Second { duration = time.Second } return ConstantDelaySchedule{ Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, } } // Next returns the next time this should be run. // This rounds so that the next activation time will be on the second. func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) } // The Schedule describes a job's duty cycle. type TimeRunner interface { // Return the next activation time, later than the given time. // Next is invoked initially, and then each time the job is run. Next(time.Time) time.Time } // SpecSchedule specifies a duty cycle (to the second granularity), based on a // traditional crontab specification. It is computed initially and stored as bit sets. type SpecSchedule struct { Second, Minute, Hour, Dom, Month, Dow uint64 } // bounds provides a range of acceptable values (plus a map of name to value). 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, }} ) const ( // Set the top bit if a star was included in the expression. starBit = 1 << 63 ) // Next returns the next time this schedule is activated, greater than the given // time. If no time can be found to satisfy the schedule, return the zero time. func (s *SpecSchedule) Next(t time.Time) time.Time { // General approach: // For Month, Day, Hour, Minute, Second: // Check if the time value matches. If yes, continue to the next field. // If the field doesn't match the schedule, then increment the field until it matches. // While incrementing the field, a wrap-around brings it back to the beginning // of the field list (since it is necessary to re-verify previous field // values) // Start at the earliest possible time (the upcoming second). t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) // This flag indicates whether a field has been incremented. added := false // If no time is found within five years, return zero. yearLimit := t.Year() + 5 WRAP: if t.Year() > yearLimit { return time.Time{} } // Find the first applicable month. // If it's this month, then do nothing. for 1< 0 dowMatch bool = 1< 0 ) if s.Dom&starBit > 0 || s.Dow&starBit > 0 { return domMatch && dowMatch } return domMatch || dowMatch } // 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 const ( Second ParseOption = 1 << iota // 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. ) var places = []ParseOption{ Second, Minute, Hour, Dom, Month, Dow, } var defaults = []string{ "0", "0", "0", "*", "*", "*", } // A custom Parser that can be configured. type Parser struct { options ParseOption optionals int } // Creates a custom Parser with custom options. // // // 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 // subsParser := NewParser(Dom | Month | Dow) // sched, err := specParser.Parse("15 */3 *") // // // Same as above, just makes Dow optional // subsParser := NewParser(Dom | Month | DowOptional) // sched, err := specParser.Parse("15 */3") // func NewParser(options ParseOption) Parser { optionals := 0 if options&DowOptional > 0 { options |= Dow optionals++ } return Parser{options, optionals} } // Parse returns a new crontab schedule representing the given spec. // It returns a descriptive error if the spec is not valid. // It accepts crontab specs and features configured by NewParser. func (p Parser) Parse(spec string) (TimeRunner, error) { if len(spec) == 0 { return nil, fmt.Errorf("Empty spec string") } if spec[0] == '@' && p.options&Descriptor > 0 { return parseDescriptor(spec) } // Figure out how many fields we need max := 0 for _, place := range places { if p.options&place > 0 { max++ } } min := max - p.optionals // Split fields on whitespace fields := strings.Fields(spec) // Validate number of 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, spec) } return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec) } // Fill in missing fields fields = expandFields(fields, p.options) var err error 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, }, nil } func expandFields(fields []string, options ParseOption) []string { n := 0 count := len(fields) expFields := make([]string, len(places)) copy(expFields, defaults) for i, place := range places { if options&place > 0 { expFields[i] = fields[n] n++ } if n == count { break } } return expFields } 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 differs from Parse requiring to always // pass 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) (TimeRunner, error) { return standardParser.Parse(standardSpec) } var defaultParser = NewParser( Second | Minute | Hour | Dom | Month | DowOptional | Descriptor, ) // Parse returns a new crontab schedule representing the given spec. // It returns a descriptive error if the spec is not valid. // // It accepts // - Full crontab specs, e.g. "* * * * * ?" // - Descriptors, e.g. "@midnight", "@every 1h30m" func Parse(spec string) (TimeRunner, error) { return defaultParser.Parse(spec) } // getField returns an Int with the bits set representing all of the times that // the field represents or error parsing field value. A "field" is a comma-separated // list of "ranges". 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 } // getRange returns the bits indicated by the given expression: // number | number "-" number [ "/" number ] // or error parsing range. 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, 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) if err != nil { return 0, err } default: return 0, fmt.Errorf("Too many hyphens: %s", expr) } } switch len(rangeAndStep) { 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 } 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 } // parseIntOrName returns the (possibly-named) integer contained in expr. func parseIntOrName(expr string, names map[string]uint) (uint, error) { if names != nil { if namedInt, ok := names[strings.ToLower(expr)]; ok { return namedInt, nil } } return mustParseInt(expr) } // mustParseInt parses the given expression as an int or returns an error. 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 } // 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 } // all returns all bits within the given bounds. (plus the star bit) func all(r bounds) uint64 { return getBits(r.min, r.max, 1) | starBit } // parseDescriptor returns a predefined schedule for the expression, or error if none matches. func parseDescriptor(descriptor string) (TimeRunner, 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), }, 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), }, 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, }, 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), }, 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), }, nil } 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) } ================================================ FILE: cron_parser_test.go ================================================ package cronlib import ( "testing" "time" ) func TestParse(t *testing.T) { cron, err := Parse("1 0 0 */1 * *") if err != nil { t.Error(err) } now1 := time.Date(2018, 12, 11, 05, 22, 33, 0, time.UTC) nowAt := cron.Next(now1) if nowAt != time.Date(2018, 12, 12, 0, 0, 1, 0, time.UTC) { t.Error("err") } } ================================================ FILE: example/easy/easy.go ================================================ package main import ( "log" "time" "github.com/rfyiamcool/cronlib" ) var ( cron = cronlib.New() ) func main() { handleClean() time.AfterFunc(time.Duration(2*time.Second), func() { // dynamic add handleBackup() }) time.AfterFunc( 12*time.Second, func() { log.Println("stop clean") cron.StopService("clean") log.Println("stop backup") cron.StopService("backup") }, ) cron.Start() cron.Wait() } func handleClean() { job, err := cronlib.NewJobModel( "*/5 * * * * *", func() { pstdout("do clean gc action") }, ) if err != nil { panic(err.Error()) } err = cron.Register("clean", job) if err != nil { panic(err.Error()) } } func handleBackup() { job, err := cronlib.NewJobModel( "*/5 * * * * *", func() { pstdout("do backup action") }, ) if err != nil { panic(err.Error()) } err = cron.DynamicRegister("backup", job) if err != nil { panic(err.Error()) } } func pstdout(srv string) { log.Println(srv) } ================================================ FILE: example/hard/hard.go ================================================ package main import ( "log" "time" "github.com/rfyiamcool/cronlib" ) // start multi job func main() { cron := cronlib.New() specList := map[string]string{ "risk.scan.total.1s": "*/1 * * * * *", "risk.scan.total.2s": "*/2 * * * * *", "risk.scan.total.3s": "*/3 * * * * *", "risk.scan.total.4s": "*/4 * * * * *", "risk.scan.total.5s.to.3s": "*/5 * * * * *", } for srv, spec := range specList { tspec := spec // copy ssrv := srv // copy job, err := cronlib.NewJobModel( spec, func() { stdout(ssrv, tspec) }, ) if err != nil { panic(err.Error()) } err = cron.Register(srv, job) if err != nil { panic(err.Error()) } } // add job test time.AfterFunc(5*time.Second, func() { spec := "*/1 * * * * *" srv := "risk.scan.total.new_add.1s" job, _ := cronlib.NewJobModel( spec, func() { stdout(srv, spec) }, ) cron.UpdateJobModel(srv, job) log.Println("reset finish", srv) }) // update job test time.AfterFunc(10*time.Second, func() { spec := "*/3 * * * * *" srv := "risk.scan.total.5s.to.3s" log.Println("reset 5s to 3s", srv) job, _ := cronlib.NewJobModel( spec, func() { stdout(srv, spec) }, ) cron.UpdateJobModel(srv, job) log.Println("reset finish", srv) }) // kill job test time.AfterFunc(15*time.Second, func() { srv := "risk.scan.total.1s" log.Println("stoping", srv) cron.StopService(srv) log.Println("stop finish", srv) }) // stop cron time.AfterFunc(25*time.Second, func() { srvPrefix := "risk" log.Println("stoping srv prefix", srvPrefix) cron.StopServicePrefix(srvPrefix) }) cron.Start() log.Println("cron start") cron.Wait() } func stdout(srv, spec string) { log.Println(srv, spec) } ================================================ FILE: example/multi/multi.go ================================================ package main import ( "log" "github.com/rfyiamcool/cronlib" ) // start multi job func main() { cron := cronlib.New() specList := map[string]string{ "risk.scan.total.per.5s": "*/5 * * * * *", "risk.scan.total.min.0s": "0 * * * * *", "risk.scan.total.per.30s": "*/30 * * * * *", } for srv, spec := range specList { tspec := spec // copy ssrv := srv // copy job, err := cronlib.NewJobModel( spec, func() { stdout(ssrv, tspec) }, ) if err != nil { panic(err.Error()) } err = cron.Register(srv, job) if err != nil { panic(err.Error()) } } cron.Start() log.Println("cron start") cron.Wait() } func stdout(srv, spec string) { log.Println(srv, spec) } ================================================ FILE: example/parse_time/parse_next_time.go ================================================ package main import ( "fmt" "time" "github.com/rfyiamcool/cronlib" ) func main() { t, err := cronlib.Parse("0 0 0 */1 * *") if err != nil { panic(err.Error()) } fmt.Println(" now: ", time.Now()) next := t.Next(time.Now()) fmt.Println("next: ", next) } ================================================ FILE: go.mod ================================================ module github.com/rfyiamcool/cronlib go 1.14 require github.com/stretchr/testify v1.6.1 ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: scheduler.go ================================================ package cronlib import ( "context" "errors" "fmt" "strings" "sync" "time" ) // copy robfig/cron's crontab parser to cronlib.cron_parser.go // "github.com/robfig/cron" const ( OnMode = true OffMode = false ) var ( ErrNotFoundJob = errors.New("not found job") ErrAlreadyRegister = errors.New("the job already in pool") ErrJobDOFuncNil = errors.New("callback func is nil") ErrCronSpecInvalid = errors.New("crontab spec is invalid") ) // null logger var defualtLogger = func(level, s string) {} type loggerType func(level, s string) func SetLogger(logger loggerType) { defualtLogger = logger } // panic call var panicCaller = func(srv, err string) { } type panicType func(srv, err string) func SetPanicCaller(p panicType) { panicCaller = p } // New - create CronSchduler func New() *CronSchduler { ctx, cancel := context.WithCancel(context.Background()) return &CronSchduler{ tasks: make(map[string]*JobModel), ctx: ctx, cancel: cancel, wg: &sync.WaitGroup{}, once: &sync.Once{}, } } // CronSchduler type CronSchduler struct { tasks map[string]*JobModel ctx context.Context cancel context.CancelFunc wg *sync.WaitGroup once *sync.Once sync.RWMutex } // Register - only register srv's job model, don't start auto. func (c *CronSchduler) Register(srv string, model *JobModel) error { return c.reset(srv, model, true, false) } // UpdateJobModel - stop old job, update srv's job model func (c *CronSchduler) UpdateJobModel(srv string, model *JobModel) error { return c.reset(srv, model, false, true) } // DynamicRegister - after cronlib already run, dynamic add a job, the job autostart by cronlib. func (c *CronSchduler) DynamicRegister(srv string, model *JobModel) error { return c.reset(srv, model, false, true) } // reset - reset srv model func (c *CronSchduler) reset(srv string, model *JobModel, denyReplace, autoStart bool) error { c.Lock() defer c.Unlock() // validate model err := model.validate() if err != nil { return err } cctx, cancel := context.WithCancel(c.ctx) model.ctx = cctx model.cancel = cancel model.srv = srv oldModel, ok := c.tasks[srv] if denyReplace && ok { return ErrAlreadyRegister } if ok { oldModel.kill() } c.tasks[srv] = model if autoStart { c.wg.Add(1) go c.tasks[srv].runLoop(c.wg) } return nil } // UnRegister - stop and delete srv func (c *CronSchduler) UnRegister(srv string) error { c.Lock() defer c.Unlock() oldModel, ok := c.tasks[srv] if !ok { return ErrNotFoundJob } oldModel.kill() delete(c.tasks, srv) return nil } // Stop - stop all cron job func (c *CronSchduler) Stop() { c.Lock() defer c.Unlock() for srv, job := range c.tasks { job.kill() delete(c.tasks, srv) } c.cancel() } // StopService - stop job by serviceName func (c *CronSchduler) StopService(srv string) { c.Lock() defer c.Unlock() job, ok := c.tasks[srv] if !ok { return } job.kill() delete(c.tasks, srv) } // StopServicePrefix - stop job by srv regex prefix. // if regex = "risk.scan", stop risk.scan.total, risk.scan.user at the same time func (c *CronSchduler) StopServicePrefix(regex string) { c.Lock() defer c.Unlock() // regex match for srv, job := range c.tasks { if !strings.HasPrefix(srv, regex) { continue } job.kill() delete(c.tasks, srv) } } func validateSpec(spec string) bool { _, err := Parse(spec) if err != nil { return false } return true } func getNextDue(spec string) (time.Time, error) { sc, err := Parse(spec) if err != nil { return time.Now(), err } // avoid time.sub time.Sleep(10 * time.Millisecond) due := sc.Next(time.Now()) return due, err } func getNextDueSafe(spec string, last time.Time) (time.Time, error) { var ( due time.Time err error ) for { due, err = getNextDue(spec) if err != nil { return due, err } if last.Equal(due) { // avoid time.sub lost some accuracy, repeat do job. time.Sleep(100 * time.Millisecond) continue } break } return due, err } func (c *CronSchduler) Start() { // only once call c.once.Do(func() { for _, job := range c.tasks { c.wg.Add(1) job.runLoop(c.wg) } }) } // Wait - if all jobs is exited, return. func (c *CronSchduler) Wait() { c.wg.Wait() } // WaitStop - when stop cronlib controller, return. func (c *CronSchduler) WaitStop() { select { case <-c.ctx.Done(): } } func (c *CronSchduler) GetServiceCron(srv string) (*JobModel, error) { c.RLock() defer c.RUnlock() oldModel, ok := c.tasks[srv] if !ok { return nil, ErrNotFoundJob } return oldModel, nil } // NewJobModel - defualt block sync callfunc func NewJobModel(spec string, f func(), options ...JobOption) (*JobModel, error) { var err error job := &JobModel{ running: true, async: false, do: f, spec: spec, notifyChan: make(chan int, 1), // avoid block } for _, opt := range options { if opt != nil { if err := opt(job); err != nil { return nil, err } } } err = job.validate() if err != nil { return job, err } return job, nil } type JobOption func(*JobModel) error func AsyncMode() JobOption { return func(o *JobModel) error { o.async = true return nil } } func TryCatchMode() JobOption { return func(o *JobModel) error { o.tryCatch = true return nil } } type JobModel struct { // srv name srv string // callfunc do func() // if async = true; go func() { do() } async bool // try catch panic tryCatch bool // cron spec spec string // for control ctx context.Context cancel context.CancelFunc notifyChan chan int // break for { ... } loop running bool // ensure job worker is exited already exited bool sync.RWMutex } func (j *JobModel) SetTryCatch(b bool) { j.tryCatch = b } func (j *JobModel) SetAsyncMode(b bool) { j.async = b } func (j *JobModel) validate() error { if j.do == nil { return ErrJobDOFuncNil } if _, err := getNextDue(j.spec); err != nil { return err } return nil } func (j *JobModel) runLoop(wg *sync.WaitGroup) { go j.run(wg) } func (j *JobModel) run(wg *sync.WaitGroup) { var ( // stdout do time cost doTimeCostFunc = func() { startTS := time.Now() defualtLogger("info", fmt.Sprintf("scheduler service: %s begin run", j.srv, ), ) if j.tryCatch { tryCatch(j) } else { j.do() } defualtLogger("info", fmt.Sprintf("scheduler service: %s has been finished, time cost: %s, spec: %s", j.srv, time.Since(startTS).String(), j.spec, ), ) } timer *time.Timer lastNextTime time.Time due time.Time interval time.Duration err error ) // parse crontab spec due, err = getNextDue(j.spec) interval = due.Sub(time.Now()) if err != nil { panic(err.Error()) } lastNextTime = due defualtLogger("info", fmt.Sprintf("scheduler service: %s next time is %s, sub: %s", j.srv, due.String(), interval.String(), ), ) // int timer timer = time.NewTimer(interval) // release join counter defer func() { timer.Stop() wg.Done() j.exited = true }() for j.running { select { case <-timer.C: if time.Now().Before(due) { timer.Reset( due.Sub(time.Now()) + 50*time.Millisecond, ) continue } due, _ := getNextDueSafe(j.spec, lastNextTime) lastNextTime = due interval := due.Sub(time.Now()) timer.Reset(interval) if j.async { go doTimeCostFunc() // goroutine for per job } else { doTimeCostFunc() } defualtLogger("info", fmt.Sprintf("scheduler service: %s next time is %s, sub: %s", j.srv, due.String(), interval.String(), ), ) case <-j.notifyChan: // parse crontab spec again ! continue case <-j.ctx.Done(): return } } } func (j *JobModel) kill() { j.running = false j.cancel() } func (j *JobModel) workerExited() bool { return j.exited } func (j *JobModel) notifySig() { select { case j.notifyChan <- 1: default: // avoid block return } } func tryCatch(job *JobModel) { defer func() { if e := recover(); e != nil { panicCaller( job.srv, fmt.Sprintf("%v", e), ) defualtLogger( "error", fmt.Sprintf("srv: %s, trycatch panicing %v", job.srv, e), ) } }() job.do() } ================================================ FILE: scheduler_test.go ================================================ package cronlib import ( "testing" "github.com/stretchr/testify/assert" ) func TestToDO(t *testing.T) { assert.Equal(t, 0, 0) }