Full Code of rfyiamcool/cronlib for AI

master eeb0ee00ee37 cached
11 files
31.5 KB
10.3k tokens
79 symbols
1 requests
Download .txt
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)
}
Download .txt
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
Download .txt
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.

Copied to clipboard!