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<<uint(t.Month())&s.Month == 0 {
// If we have to add a month, reset the other parts to 0.
if !added {
added = true
// Otherwise, set the date at the beginning (since the current time is irrelevant).
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
}
t = t.AddDate(0, 1, 0)
// Wrapped around.
if t.Month() == time.January {
goto WRAP
}
}
// Now get a day in that month.
for !dayMatches(s, t) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
t = t.AddDate(0, 0, 1)
if t.Day() == 1 {
goto WRAP
}
}
for 1<<uint(t.Hour())&s.Hour == 0 {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())
}
t = t.Add(1 * time.Hour)
if t.Hour() == 0 {
goto WRAP
}
}
for 1<<uint(t.Minute())&s.Minute == 0 {
if !added {
added = true
t = t.Truncate(time.Minute)
}
t = t.Add(1 * time.Minute)
if t.Minute() == 0 {
goto WRAP
}
}
for 1<<uint(t.Second())&s.Second == 0 {
if !added {
added = true
t = t.Truncate(time.Second)
}
t = t.Add(1 * time.Second)
if t.Second() == 0 {
goto WRAP
}
}
return t
}
// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
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
)
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)
}
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
SYMBOL INDEX (79 symbols across 8 files)
FILE: cron_parser.go
type ConstantDelaySchedule (line 17) | type ConstantDelaySchedule struct
method Next (line 35) | func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
function Every (line 24) | func Every(duration time.Duration) ConstantDelaySchedule {
type TimeRunner (line 40) | type TimeRunner interface
type SpecSchedule (line 48) | type SpecSchedule struct
method Next (line 96) | func (s *SpecSchedule) Next(t time.Time) time.Time {
type bounds (line 53) | type bounds struct
constant starBit (line 91) | starBit = 1 << 63
function dayMatches (line 190) | func dayMatches(s *SpecSchedule, t time.Time) bool {
type ParseOption (line 205) | type ParseOption
constant Second (line 208) | Second ParseOption = 1 << iota
constant Minute (line 209) | Minute
constant Hour (line 210) | Hour
constant Dom (line 211) | Dom
constant Month (line 212) | Month
constant Dow (line 213) | Dow
constant DowOptional (line 214) | DowOptional
constant Descriptor (line 215) | Descriptor
type Parser (line 237) | type Parser struct
method Parse (line 268) | func (p Parser) Parse(spec string) (TimeRunner, error) {
function NewParser (line 256) | func NewParser(options ParseOption) Parser {
function expandFields (line 331) | func expandFields(fields []string, options ParseOption) []string {
function ParseStandard (line 360) | func ParseStandard(standardSpec string) (TimeRunner, error) {
function Parse (line 374) | func Parse(spec string) (TimeRunner, error) {
function getField (line 381) | func getField(field string, r bounds) (uint64, error) {
function getRange (line 397) | func getRange(expr string, r bounds) (uint64, error) {
function parseIntOrName (line 463) | func parseIntOrName(expr string, names map[string]uint) (uint, error) {
function mustParseInt (line 473) | func mustParseInt(expr string) (uint, error) {
function getBits (line 486) | func getBits(min, max, step uint) uint64 {
function all (line 502) | func all(r bounds) uint64 {
function parseDescriptor (line 507) | func parseDescriptor(descriptor string) (TimeRunner, error) {
FILE: cron_parser_test.go
function TestParse (line 8) | func TestParse(t *testing.T) {
FILE: example/easy/easy.go
function main (line 14) | func main() {
function handleClean (line 37) | func handleClean() {
function handleBackup (line 54) | func handleBackup() {
function pstdout (line 71) | func pstdout(srv string) {
FILE: example/hard/hard.go
function main (line 11) | func main() {
function stdout (line 91) | func stdout(srv, spec string) {
FILE: example/multi/multi.go
function main (line 10) | func main() {
function stdout (line 43) | func stdout(srv, spec string) {
FILE: example/parse_time/parse_next_time.go
function main (line 10) | func main() {
FILE: scheduler.go
constant OnMode (line 16) | OnMode = true
constant OffMode (line 17) | OffMode = false
type loggerType (line 30) | type loggerType
function SetLogger (line 32) | func SetLogger(logger loggerType) {
type panicType (line 40) | type panicType
function SetPanicCaller (line 42) | func SetPanicCaller(p panicType) {
function New (line 47) | func New() *CronSchduler {
type CronSchduler (line 59) | type CronSchduler struct
method Register (line 71) | func (c *CronSchduler) Register(srv string, model *JobModel) error {
method UpdateJobModel (line 76) | func (c *CronSchduler) UpdateJobModel(srv string, model *JobModel) err...
method DynamicRegister (line 81) | func (c *CronSchduler) DynamicRegister(srv string, model *JobModel) er...
method reset (line 86) | func (c *CronSchduler) reset(srv string, model *JobModel, denyReplace,...
method UnRegister (line 120) | func (c *CronSchduler) UnRegister(srv string) error {
method Stop (line 135) | func (c *CronSchduler) Stop() {
method StopService (line 147) | func (c *CronSchduler) StopService(srv string) {
method StopServicePrefix (line 162) | func (c *CronSchduler) StopServicePrefix(regex string) {
method Start (line 222) | func (c *CronSchduler) Start() {
method Wait (line 235) | func (c *CronSchduler) Wait() {
method WaitStop (line 240) | func (c *CronSchduler) WaitStop() {
method GetServiceCron (line 246) | func (c *CronSchduler) GetServiceCron(srv string) (*JobModel, error) {
function validateSpec (line 177) | func validateSpec(spec string) bool {
function getNextDue (line 186) | func getNextDue(spec string) (time.Time, error) {
function getNextDueSafe (line 198) | func getNextDueSafe(spec string, last time.Time) (time.Time, error) {
function NewJobModel (line 259) | func NewJobModel(spec string, f func(), options ...JobOption) (*JobModel...
type JobOption (line 285) | type JobOption
function AsyncMode (line 287) | func AsyncMode() JobOption {
function TryCatchMode (line 294) | func TryCatchMode() JobOption {
type JobModel (line 301) | type JobModel struct
method SetTryCatch (line 331) | func (j *JobModel) SetTryCatch(b bool) {
method SetAsyncMode (line 335) | func (j *JobModel) SetAsyncMode(b bool) {
method validate (line 339) | func (j *JobModel) validate() error {
method runLoop (line 351) | func (j *JobModel) runLoop(wg *sync.WaitGroup) {
method run (line 355) | func (j *JobModel) run(wg *sync.WaitGroup) {
method kill (line 454) | func (j *JobModel) kill() {
method workerExited (line 459) | func (j *JobModel) workerExited() bool {
method notifySig (line 463) | func (j *JobModel) notifySig() {
function tryCatch (line 472) | func tryCatch(job *JobModel) {
FILE: scheduler_test.go
function TestToDO (line 9) | func TestToDO(t *testing.T) {
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (36K chars).
[
{
"path": "README.md",
"chars": 4248,
"preview": "# cronlib\n\nCronlib is easy golang crontab library, support parse crontab and schedule cron jobs.\n\ncron_parser.go import "
},
{
"path": "cron_parser.go",
"chars": 14460,
"preview": "package cronlib\n\n//\n// crontab parser import from https://github.com/robfig/cron/blob/master/parser.go\n//\n\nimport (\n\t\"fm"
},
{
"path": "cron_parser_test.go",
"chars": 319,
"preview": "package cronlib\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestParse(t *testing.T) {\n\tcron, err := Parse(\"1 0 0 */1 * *\")\n\tif "
},
{
"path": "example/easy/easy.go",
"chars": 982,
"preview": "package main\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/rfyiamcool/cronlib\"\n)\n\nvar (\n\tcron = cronlib.New()\n)\n\nfunc main() {\n"
},
{
"path": "example/hard/hard.go",
"chars": 1761,
"preview": "package main\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/rfyiamcool/cronlib\"\n)\n\n// start multi job\nfunc main() {\n\tcron := cro"
},
{
"path": "example/multi/multi.go",
"chars": 710,
"preview": "package main\n\nimport (\n\t\"log\"\n\n\t\"github.com/rfyiamcool/cronlib\"\n)\n\n// start multi job\nfunc main() {\n\tcron := cronlib.New"
},
{
"path": "example/parse_time/parse_next_time.go",
"chars": 268,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/rfyiamcool/cronlib\"\n)\n\nfunc main() {\n\tt, err := cronlib.Parse(\"0 0 0"
},
{
"path": "go.mod",
"chars": 90,
"preview": "module github.com/rfyiamcool/cronlib\n\ngo 1.14\n\nrequire github.com/stretchr/testify v1.6.1\n"
},
{
"path": "go.sum",
"chars": 1024,
"preview": "github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=\ngithub.com/davecgh/go-spew v1.1.0/go.m"
},
{
"path": "scheduler.go",
"chars": 8269,
"preview": "package cronlib\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// copy robfig/cron's crontab parser"
},
{
"path": "scheduler_test.go",
"chars": 134,
"preview": "package cronlib\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestToDO(t *testing.T) {\n\tassert.Equ"
}
]
About this extraction
This page contains the full source code of the rfyiamcool/cronlib GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (31.5 KB), approximately 10.3k tokens, and a symbol index with 79 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.