[
  {
    "path": "README.md",
    "content": "# cronlib\n\nCronlib is easy golang crontab library, support parse crontab and schedule cron jobs.\n\ncron_parser.go import `https://github.com/robfig/cron/blob/master/parser.go`, thank @robfig\n\n## Feature\n\n* thread safe\n* add try catch mode\n* dynamic modify job cron\n* dynamic add job\n* stop service job\n* add Wait method for waiting all job exit\n* async & sync mode\n\n## Usage\n\nsee more [example](github.com/rfyiamcool/example)\n\n### quick run\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\n\t\"github.com/rfyiamcool/cronlib\"\n)\n\nvar (\n\tcron = cronlib.New()\n)\n\nfunc main() {\n\thandleClean()\n\tgo start()\n\n\t// cron already start, dynamic add job\n\thandleBackup()\n\n\tselect {}\n}\n\nfunc start() {\n\tcron.Start()\n\tcron.Wait()\n}\n\nfunc handleClean() {\n\tjob, err := cronlib.NewJobModel(\n\t\t\"*/5 * * * * *\",\n\t\tfunc() {\n\t\t\tpstdout(\"do clean gc action\")\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\terr = cron.Register(\"clean\", job)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\nfunc handleBackup() {\n\tjob, err := cronlib.NewJobModel(\n\t\t\"*/5 * * * * *\",\n\t\tfunc() {\n\t\t\tpstdout(\"do backup action\")\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\terr = cron.DynamicRegister(\"backup\", job)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\nfunc pstdout(srv string) {\n\tlog.Println(srv)\n}\n```\n\n### set job attr\n\nopen async mode and try catch mode\n\n```go\nfunc run() error {\n\tcron := cronlib.New()\n\n\t// set async mode\n\tjob, err = cronlib.NewJobModel(\n\t\t\"0 * * * * *\",\n\t\tfunc(),\n\t\tcronlib.AsyncMode(),\n\t\tcronlib.TryCatchMode(),\n\t)\n\n\t...\n}\n```\n\nother method\n\n```go\nfunc run() error {\n\tcron := cronlib.New()\n\n\t// set async mode\n\tjob, err = cronlib.NewJobModel(\n\t\t\"0 * * * * *\",\n\t\tfunc(),\n\t)\n\n\t...\n\n\tjob.SetTryCatch(cronlib.OnMode)\n\tjob.SetAsyncMode(cronlib.OnMode)\n\n\t...\n}\n\n```\n\n### stop job\n\n```go\ncron := cronlib.New()\n...\ncron.StopService(srvName)\n```\n\n### update job\n\n```go\nspec := \"*/3 * * * * *\"\nsrv := \"risk.scan.total.5s.to.3s\"\n\njob, _ := cronlib.NewJobModel(\n\tspec,\n\tfunc() {\n\t\tstdout(srv, spec)\n\t},\n)\n\nerr := cron.UpdateJobModel(srv, job)\n...\n```\n\n## Example\n\n```go\npackage main\n\n// test for crontab spec\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/rfyiamcool/cronlib\"\n)\n\nfunc main() {\n\tcron := cronlib.New()\n\n\tspecList := map[string]string{\n\t\t\"risk.scan.total.1s\":       \"*/1 * * * * *\",\n\t\t\"risk.scan.total.2s\":       \"*/2 * * * * *\",\n\t\t\"risk.scan.total.3s\":       \"*/3 * * * * *\",\n\t\t\"risk.scan.total.4s\":       \"*/4 * * * * *\",\n\t\t\"risk.scan.total.5s.to.3s\": \"*/5 * * * * *\",\n\t}\n\n\tfor srv, spec := range specList {\n\t\ttspec := spec // copy\n\t\tssrv := srv   // copy\n\t\tjob, err := cronlib.NewJobModel(\n\t\t\tspec,\n\t\t\tfunc() {\n\t\t\t\tstdout(ssrv, tspec)\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\n\t\terr = cron.Register(srv, job)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\t}\n\n\t// update test\n\ttime.AfterFunc(10*time.Second, func() {\n\t\tspec := \"*/3 * * * * *\"\n\t\tsrv := \"risk.scan.total.5s.to.3s\"\n\t\tlog.Println(\"reset 5s to 3s\", srv)\n\t\tjob, _ := cronlib.NewJobModel(\n\t\t\tspec,\n\t\t\tfunc() {\n\t\t\t\tstdout(srv, spec)\n\t\t\t},\n\t\t)\n\t\tcron.UpdateJobModel(srv, job)\n\t\tlog.Println(\"reset finish\", srv)\n\n\t})\n\n\t// kill test\n\ttime.AfterFunc(3*time.Second, func() {\n\n\t\tsrv := \"risk.scan.total.1s\"\n\t\tlog.Println(\"stoping\", srv)\n\t\tcron.StopService(srv)\n\t\tlog.Println(\"stop finish\", srv)\n\n\t})\n\n\ttime.AfterFunc(11*time.Second, func() {\n\n\t\tsrvPrefix := \"risk\"\n\t\tlog.Println(\"stoping srv prefix\", srvPrefix)\n\t\tcron.StopServicePrefix(srvPrefix)\n\n\t})\n\n\tcron.Start()\n\tcron.Wait()\n}\n\nfunc stdout(srv, spec string) {\n\tlog.Println(srv, spec)\n}\n\n```\n\n## Time Format Usage:\n\n**cronlib has second field, cronlibs contains six fields, first field is second than linux crontab**\n\nevery 2 seconds\n\n```\n*/2 * * * * *\n```\n\nevery hour on the half hour\n\n```\n0 30 * * * *\n```\n\ndetail field desc\n\n```\n\nField name   | Mandatory? | Allowed values  | Allowed special characters\n----------   | ---------- | --------------  | --------------------------\nSeconds      | Yes        | 0-59            | * / , -\nMinutes      | Yes        | 0-59            | * / , -\nHours        | Yes        | 0-23            | * / , -\nDay of month | Yes        | 1-31            | * / , - ?\nMonth        | Yes        | 1-12 or JAN-DEC | * / , -\nDay of week  | Yes        | 0-6 or SUN-SAT  | * / , - ?\n\n```\n\ncron parse doc: https://github.com/robfig/cron"
  },
  {
    "path": "cron_parser.go",
    "content": "package cronlib\n\n//\n// crontab parser import from https://github.com/robfig/cron/blob/master/parser.go\n//\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. \"Every 5 minutes\".\n// It does not support jobs more frequent than once a second.\ntype ConstantDelaySchedule struct {\n\tDelay time.Duration\n}\n\n// Every returns a crontab Schedule that activates once every duration.\n// Delays of less than a second are not supported (will round up to 1 second).\n// Any fields less than a Second are truncated.\nfunc Every(duration time.Duration) ConstantDelaySchedule {\n\tif duration < time.Second {\n\t\tduration = time.Second\n\t}\n\treturn ConstantDelaySchedule{\n\t\tDelay: duration - time.Duration(duration.Nanoseconds())%time.Second,\n\t}\n}\n\n// Next returns the next time this should be run.\n// This rounds so that the next activation time will be on the second.\nfunc (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {\n\treturn t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)\n}\n\n// The Schedule describes a job's duty cycle.\ntype TimeRunner interface {\n\t// Return the next activation time, later than the given time.\n\t// Next is invoked initially, and then each time the job is run.\n\tNext(time.Time) time.Time\n}\n\n// SpecSchedule specifies a duty cycle (to the second granularity), based on a\n// traditional crontab specification. It is computed initially and stored as bit sets.\ntype SpecSchedule struct {\n\tSecond, Minute, Hour, Dom, Month, Dow uint64\n}\n\n// bounds provides a range of acceptable values (plus a map of name to value).\ntype bounds struct {\n\tmin, max uint\n\tnames    map[string]uint\n}\n\n// The bounds for each field.\nvar (\n\tseconds = bounds{0, 59, nil}\n\tminutes = bounds{0, 59, nil}\n\thours   = bounds{0, 23, nil}\n\tdom     = bounds{1, 31, nil}\n\tmonths  = bounds{1, 12, map[string]uint{\n\t\t\"jan\": 1,\n\t\t\"feb\": 2,\n\t\t\"mar\": 3,\n\t\t\"apr\": 4,\n\t\t\"may\": 5,\n\t\t\"jun\": 6,\n\t\t\"jul\": 7,\n\t\t\"aug\": 8,\n\t\t\"sep\": 9,\n\t\t\"oct\": 10,\n\t\t\"nov\": 11,\n\t\t\"dec\": 12,\n\t}}\n\tdow = bounds{0, 6, map[string]uint{\n\t\t\"sun\": 0,\n\t\t\"mon\": 1,\n\t\t\"tue\": 2,\n\t\t\"wed\": 3,\n\t\t\"thu\": 4,\n\t\t\"fri\": 5,\n\t\t\"sat\": 6,\n\t}}\n)\n\nconst (\n\t// Set the top bit if a star was included in the expression.\n\tstarBit = 1 << 63\n)\n\n// Next returns the next time this schedule is activated, greater than the given\n// time.  If no time can be found to satisfy the schedule, return the zero time.\nfunc (s *SpecSchedule) Next(t time.Time) time.Time {\n\t// General approach:\n\t// For Month, Day, Hour, Minute, Second:\n\t// Check if the time value matches.  If yes, continue to the next field.\n\t// If the field doesn't match the schedule, then increment the field until it matches.\n\t// While incrementing the field, a wrap-around brings it back to the beginning\n\t// of the field list (since it is necessary to re-verify previous field\n\t// values)\n\n\t// Start at the earliest possible time (the upcoming second).\n\tt = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)\n\n\t// This flag indicates whether a field has been incremented.\n\tadded := false\n\n\t// If no time is found within five years, return zero.\n\tyearLimit := t.Year() + 5\n\nWRAP:\n\tif t.Year() > yearLimit {\n\t\treturn time.Time{}\n\t}\n\n\t// Find the first applicable month.\n\t// If it's this month, then do nothing.\n\tfor 1<<uint(t.Month())&s.Month == 0 {\n\t\t// If we have to add a month, reset the other parts to 0.\n\t\tif !added {\n\t\t\tadded = true\n\t\t\t// Otherwise, set the date at the beginning (since the current time is irrelevant).\n\t\t\tt = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())\n\t\t}\n\t\tt = t.AddDate(0, 1, 0)\n\n\t\t// Wrapped around.\n\t\tif t.Month() == time.January {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\t// Now get a day in that month.\n\tfor !dayMatches(s, t) {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())\n\t\t}\n\t\tt = t.AddDate(0, 0, 1)\n\n\t\tif t.Day() == 1 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\tfor 1<<uint(t.Hour())&s.Hour == 0 {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())\n\t\t}\n\t\tt = t.Add(1 * time.Hour)\n\n\t\tif t.Hour() == 0 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\tfor 1<<uint(t.Minute())&s.Minute == 0 {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = t.Truncate(time.Minute)\n\t\t}\n\t\tt = t.Add(1 * time.Minute)\n\n\t\tif t.Minute() == 0 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\tfor 1<<uint(t.Second())&s.Second == 0 {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = t.Truncate(time.Second)\n\t\t}\n\t\tt = t.Add(1 * time.Second)\n\n\t\tif t.Second() == 0 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\treturn t\n}\n\n// dayMatches returns true if the schedule's day-of-week and day-of-month\n// restrictions are satisfied by the given time.\nfunc dayMatches(s *SpecSchedule, t time.Time) bool {\n\tvar (\n\t\tdomMatch bool = 1<<uint(t.Day())&s.Dom > 0\n\t\tdowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0\n\t)\n\tif s.Dom&starBit > 0 || s.Dow&starBit > 0 {\n\t\treturn domMatch && dowMatch\n\t}\n\treturn domMatch || dowMatch\n}\n\n// Configuration options for creating a parser. Most options specify which\n// fields should be included, while others enable features. If a field is not\n// included the parser will assume a default value. These options do not change\n// the order fields are parse in.\ntype ParseOption int\n\nconst (\n\tSecond      ParseOption = 1 << iota // Seconds field, default 0\n\tMinute                              // Minutes field, default 0\n\tHour                                // Hours field, default 0\n\tDom                                 // Day of month field, default *\n\tMonth                               // Month field, default *\n\tDow                                 // Day of week field, default *\n\tDowOptional                         // Optional day of week field, default *\n\tDescriptor                          // Allow descriptors such as @monthly, @weekly, etc.\n)\n\nvar places = []ParseOption{\n\tSecond,\n\tMinute,\n\tHour,\n\tDom,\n\tMonth,\n\tDow,\n}\n\nvar defaults = []string{\n\t\"0\",\n\t\"0\",\n\t\"0\",\n\t\"*\",\n\t\"*\",\n\t\"*\",\n}\n\n// A custom Parser that can be configured.\ntype Parser struct {\n\toptions   ParseOption\n\toptionals int\n}\n\n// Creates a custom Parser with custom options.\n//\n//  // Standard parser without descriptors\n//  specParser := NewParser(Minute | Hour | Dom | Month | Dow)\n//  sched, err := specParser.Parse(\"0 0 15 */3 *\")\n//\n//  // Same as above, just excludes time fields\n//  subsParser := NewParser(Dom | Month | Dow)\n//  sched, err := specParser.Parse(\"15 */3 *\")\n//\n//  // Same as above, just makes Dow optional\n//  subsParser := NewParser(Dom | Month | DowOptional)\n//  sched, err := specParser.Parse(\"15 */3\")\n//\nfunc NewParser(options ParseOption) Parser {\n\toptionals := 0\n\tif options&DowOptional > 0 {\n\t\toptions |= Dow\n\t\toptionals++\n\t}\n\treturn Parser{options, optionals}\n}\n\n// Parse returns a new crontab schedule representing the given spec.\n// It returns a descriptive error if the spec is not valid.\n// It accepts crontab specs and features configured by NewParser.\nfunc (p Parser) Parse(spec string) (TimeRunner, error) {\n\tif len(spec) == 0 {\n\t\treturn nil, fmt.Errorf(\"Empty spec string\")\n\t}\n\tif spec[0] == '@' && p.options&Descriptor > 0 {\n\t\treturn parseDescriptor(spec)\n\t}\n\n\t// Figure out how many fields we need\n\tmax := 0\n\tfor _, place := range places {\n\t\tif p.options&place > 0 {\n\t\t\tmax++\n\t\t}\n\t}\n\tmin := max - p.optionals\n\n\t// Split fields on whitespace\n\tfields := strings.Fields(spec)\n\n\t// Validate number of fields\n\tif count := len(fields); count < min || count > max {\n\t\tif min == max {\n\t\t\treturn nil, fmt.Errorf(\"Expected exactly %d fields, found %d: %s\", min, count, spec)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"Expected %d to %d fields, found %d: %s\", min, max, count, spec)\n\t}\n\n\t// Fill in missing fields\n\tfields = expandFields(fields, p.options)\n\n\tvar err error\n\tfield := func(field string, r bounds) uint64 {\n\t\tif err != nil {\n\t\t\treturn 0\n\t\t}\n\t\tvar bits uint64\n\t\tbits, err = getField(field, r)\n\t\treturn bits\n\t}\n\n\tvar (\n\t\tsecond     = field(fields[0], seconds)\n\t\tminute     = field(fields[1], minutes)\n\t\thour       = field(fields[2], hours)\n\t\tdayofmonth = field(fields[3], dom)\n\t\tmonth      = field(fields[4], months)\n\t\tdayofweek  = field(fields[5], dow)\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &SpecSchedule{\n\t\tSecond: second,\n\t\tMinute: minute,\n\t\tHour:   hour,\n\t\tDom:    dayofmonth,\n\t\tMonth:  month,\n\t\tDow:    dayofweek,\n\t}, nil\n}\n\nfunc expandFields(fields []string, options ParseOption) []string {\n\tn := 0\n\tcount := len(fields)\n\texpFields := make([]string, len(places))\n\tcopy(expFields, defaults)\n\tfor i, place := range places {\n\t\tif options&place > 0 {\n\t\t\texpFields[i] = fields[n]\n\t\t\tn++\n\t\t}\n\t\tif n == count {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn expFields\n}\n\nvar standardParser = NewParser(\n\tMinute | Hour | Dom | Month | Dow | Descriptor,\n)\n\n// ParseStandard returns a new crontab schedule representing the given standardSpec\n// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always\n// pass 5 entries representing: minute, hour, day of month, month and day of week,\n// in that order. It returns a descriptive error if the spec is not valid.\n//\n// It accepts\n//   - Standard crontab specs, e.g. \"* * * * ?\"\n//   - Descriptors, e.g. \"@midnight\", \"@every 1h30m\"\nfunc ParseStandard(standardSpec string) (TimeRunner, error) {\n\treturn standardParser.Parse(standardSpec)\n}\n\nvar defaultParser = NewParser(\n\tSecond | Minute | Hour | Dom | Month | DowOptional | Descriptor,\n)\n\n// Parse returns a new crontab schedule representing the given spec.\n// It returns a descriptive error if the spec is not valid.\n//\n// It accepts\n//   - Full crontab specs, e.g. \"* * * * * ?\"\n//   - Descriptors, e.g. \"@midnight\", \"@every 1h30m\"\nfunc Parse(spec string) (TimeRunner, error) {\n\treturn defaultParser.Parse(spec)\n}\n\n// getField returns an Int with the bits set representing all of the times that\n// the field represents or error parsing field value.  A \"field\" is a comma-separated\n// list of \"ranges\".\nfunc getField(field string, r bounds) (uint64, error) {\n\tvar bits uint64\n\tranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })\n\tfor _, expr := range ranges {\n\t\tbit, err := getRange(expr, r)\n\t\tif err != nil {\n\t\t\treturn bits, err\n\t\t}\n\t\tbits |= bit\n\t}\n\treturn bits, nil\n}\n\n// getRange returns the bits indicated by the given expression:\n//   number | number \"-\" number [ \"/\" number ]\n// or error parsing range.\nfunc getRange(expr string, r bounds) (uint64, error) {\n\tvar (\n\t\tstart, end, step uint\n\t\trangeAndStep     = strings.Split(expr, \"/\")\n\t\tlowAndHigh       = strings.Split(rangeAndStep[0], \"-\")\n\t\tsingleDigit      = len(lowAndHigh) == 1\n\t\terr              error\n\t)\n\n\tvar extra uint64\n\tif lowAndHigh[0] == \"*\" || lowAndHigh[0] == \"?\" {\n\t\tstart = r.min\n\t\tend = r.max\n\t\textra = starBit\n\t} else {\n\t\tstart, err = parseIntOrName(lowAndHigh[0], r.names)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tswitch len(lowAndHigh) {\n\t\tcase 1:\n\t\t\tend = start\n\t\tcase 2:\n\t\t\tend, err = parseIntOrName(lowAndHigh[1], r.names)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn 0, fmt.Errorf(\"Too many hyphens: %s\", expr)\n\t\t}\n\t}\n\n\tswitch len(rangeAndStep) {\n\tcase 1:\n\t\tstep = 1\n\tcase 2:\n\t\tstep, err = mustParseInt(rangeAndStep[1])\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\t// Special handling: \"N/step\" means \"N-max/step\".\n\t\tif singleDigit {\n\t\t\tend = r.max\n\t\t}\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"Too many slashes: %s\", expr)\n\t}\n\n\tif start < r.min {\n\t\treturn 0, fmt.Errorf(\"Beginning of range (%d) below minimum (%d): %s\", start, r.min, expr)\n\t}\n\tif end > r.max {\n\t\treturn 0, fmt.Errorf(\"End of range (%d) above maximum (%d): %s\", end, r.max, expr)\n\t}\n\tif start > end {\n\t\treturn 0, fmt.Errorf(\"Beginning of range (%d) beyond end of range (%d): %s\", start, end, expr)\n\t}\n\tif step == 0 {\n\t\treturn 0, fmt.Errorf(\"Step of range should be a positive number: %s\", expr)\n\t}\n\n\treturn getBits(start, end, step) | extra, nil\n}\n\n// parseIntOrName returns the (possibly-named) integer contained in expr.\nfunc parseIntOrName(expr string, names map[string]uint) (uint, error) {\n\tif names != nil {\n\t\tif namedInt, ok := names[strings.ToLower(expr)]; ok {\n\t\t\treturn namedInt, nil\n\t\t}\n\t}\n\treturn mustParseInt(expr)\n}\n\n// mustParseInt parses the given expression as an int or returns an error.\nfunc mustParseInt(expr string) (uint, error) {\n\tnum, err := strconv.Atoi(expr)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"Failed to parse int from %s: %s\", expr, err)\n\t}\n\tif num < 0 {\n\t\treturn 0, fmt.Errorf(\"Negative number (%d) not allowed: %s\", num, expr)\n\t}\n\n\treturn uint(num), nil\n}\n\n// getBits sets all bits in the range [min, max], modulo the given step size.\nfunc getBits(min, max, step uint) uint64 {\n\tvar bits uint64\n\n\t// If step is 1, use shifts.\n\tif step == 1 {\n\t\treturn ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)\n\t}\n\n\t// Else, use a simple loop.\n\tfor i := min; i <= max; i += step {\n\t\tbits |= 1 << i\n\t}\n\treturn bits\n}\n\n// all returns all bits within the given bounds.  (plus the star bit)\nfunc all(r bounds) uint64 {\n\treturn getBits(r.min, r.max, 1) | starBit\n}\n\n// parseDescriptor returns a predefined schedule for the expression, or error if none matches.\nfunc parseDescriptor(descriptor string) (TimeRunner, error) {\n\tswitch descriptor {\n\tcase \"@yearly\", \"@annually\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond: 1 << seconds.min,\n\t\t\tMinute: 1 << minutes.min,\n\t\t\tHour:   1 << hours.min,\n\t\t\tDom:    1 << dom.min,\n\t\t\tMonth:  1 << months.min,\n\t\t\tDow:    all(dow),\n\t\t}, nil\n\n\tcase \"@monthly\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond: 1 << seconds.min,\n\t\t\tMinute: 1 << minutes.min,\n\t\t\tHour:   1 << hours.min,\n\t\t\tDom:    1 << dom.min,\n\t\t\tMonth:  all(months),\n\t\t\tDow:    all(dow),\n\t\t}, nil\n\n\tcase \"@weekly\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond: 1 << seconds.min,\n\t\t\tMinute: 1 << minutes.min,\n\t\t\tHour:   1 << hours.min,\n\t\t\tDom:    all(dom),\n\t\t\tMonth:  all(months),\n\t\t\tDow:    1 << dow.min,\n\t\t}, nil\n\n\tcase \"@daily\", \"@midnight\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond: 1 << seconds.min,\n\t\t\tMinute: 1 << minutes.min,\n\t\t\tHour:   1 << hours.min,\n\t\t\tDom:    all(dom),\n\t\t\tMonth:  all(months),\n\t\t\tDow:    all(dow),\n\t\t}, nil\n\n\tcase \"@hourly\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond: 1 << seconds.min,\n\t\t\tMinute: 1 << minutes.min,\n\t\t\tHour:   all(hours),\n\t\t\tDom:    all(dom),\n\t\t\tMonth:  all(months),\n\t\t\tDow:    all(dow),\n\t\t}, nil\n\t}\n\n\tconst every = \"@every \"\n\tif strings.HasPrefix(descriptor, every) {\n\t\tduration, err := time.ParseDuration(descriptor[len(every):])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Failed to parse duration %s: %s\", descriptor, err)\n\t\t}\n\t\treturn Every(duration), nil\n\t}\n\n\treturn nil, fmt.Errorf(\"Unrecognized descriptor: %s\", descriptor)\n}\n\n"
  },
  {
    "path": "cron_parser_test.go",
    "content": "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 err != nil {\n\t\tt.Error(err)\n\t}\n\n\tnow1 := time.Date(2018, 12, 11, 05, 22, 33, 0, time.UTC)\n\tnowAt := cron.Next(now1)\n\tif nowAt != time.Date(2018, 12, 12, 0, 0, 1, 0, time.UTC) {\n\t\tt.Error(\"err\")\n\t}\n}\n"
  },
  {
    "path": "example/easy/easy.go",
    "content": "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\thandleClean()\n\n\ttime.AfterFunc(time.Duration(2*time.Second), func() {\n\t\t// dynamic add\n\t\thandleBackup()\n\t})\n\n\ttime.AfterFunc(\n\t\t12*time.Second,\n\t\tfunc() {\n\t\t\tlog.Println(\"stop clean\")\n\t\t\tcron.StopService(\"clean\")\n\n\t\t\tlog.Println(\"stop backup\")\n\t\t\tcron.StopService(\"backup\")\n\t\t},\n\t)\n\n\tcron.Start()\n\tcron.Wait()\n}\n\nfunc handleClean() {\n\tjob, err := cronlib.NewJobModel(\n\t\t\"*/5 * * * * *\",\n\t\tfunc() {\n\t\t\tpstdout(\"do clean gc action\")\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\terr = cron.Register(\"clean\", job)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\nfunc handleBackup() {\n\tjob, err := cronlib.NewJobModel(\n\t\t\"*/5 * * * * *\",\n\t\tfunc() {\n\t\t\tpstdout(\"do backup action\")\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\terr = cron.DynamicRegister(\"backup\", job)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\nfunc pstdout(srv string) {\n\tlog.Println(srv)\n}\n"
  },
  {
    "path": "example/hard/hard.go",
    "content": "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 := cronlib.New()\n\n\tspecList := map[string]string{\n\t\t\"risk.scan.total.1s\":       \"*/1 * * * * *\",\n\t\t\"risk.scan.total.2s\":       \"*/2 * * * * *\",\n\t\t\"risk.scan.total.3s\":       \"*/3 * * * * *\",\n\t\t\"risk.scan.total.4s\":       \"*/4 * * * * *\",\n\t\t\"risk.scan.total.5s.to.3s\": \"*/5 * * * * *\",\n\t}\n\n\tfor srv, spec := range specList {\n\t\ttspec := spec // copy\n\t\tssrv := srv   // copy\n\t\tjob, err := cronlib.NewJobModel(\n\t\t\tspec,\n\t\t\tfunc() {\n\t\t\t\tstdout(ssrv, tspec)\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\n\t\terr = cron.Register(srv, job)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\t}\n\n\t// add job test\n\ttime.AfterFunc(5*time.Second, func() {\n\t\tspec := \"*/1 * * * * *\"\n\t\tsrv := \"risk.scan.total.new_add.1s\"\n\t\tjob, _ := cronlib.NewJobModel(\n\t\t\tspec,\n\t\t\tfunc() {\n\t\t\t\tstdout(srv, spec)\n\t\t\t},\n\t\t)\n\t\tcron.UpdateJobModel(srv, job)\n\t\tlog.Println(\"reset finish\", srv)\n\t})\n\n\t// update job test\n\ttime.AfterFunc(10*time.Second, func() {\n\t\tspec := \"*/3 * * * * *\"\n\t\tsrv := \"risk.scan.total.5s.to.3s\"\n\t\tlog.Println(\"reset 5s to 3s\", srv)\n\t\tjob, _ := cronlib.NewJobModel(\n\t\t\tspec,\n\t\t\tfunc() {\n\t\t\t\tstdout(srv, spec)\n\t\t\t},\n\t\t)\n\t\tcron.UpdateJobModel(srv, job)\n\t\tlog.Println(\"reset finish\", srv)\n\n\t})\n\n\t// kill job test\n\ttime.AfterFunc(15*time.Second, func() {\n\t\tsrv := \"risk.scan.total.1s\"\n\t\tlog.Println(\"stoping\", srv)\n\t\tcron.StopService(srv)\n\t\tlog.Println(\"stop finish\", srv)\n\t})\n\n\t// stop cron\n\ttime.AfterFunc(25*time.Second, func() {\n\t\tsrvPrefix := \"risk\"\n\t\tlog.Println(\"stoping srv prefix\", srvPrefix)\n\t\tcron.StopServicePrefix(srvPrefix)\n\t})\n\n\tcron.Start()\n\tlog.Println(\"cron start\")\n\tcron.Wait()\n}\n\nfunc stdout(srv, spec string) {\n\tlog.Println(srv, spec)\n}\n"
  },
  {
    "path": "example/multi/multi.go",
    "content": "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()\n\n\tspecList := map[string]string{\n\t\t\"risk.scan.total.per.5s\":  \"*/5 * * * * *\",\n\t\t\"risk.scan.total.min.0s\":  \"0 * * * * *\",\n\t\t\"risk.scan.total.per.30s\": \"*/30 * * * * *\",\n\t}\n\n\tfor srv, spec := range specList {\n\t\ttspec := spec // copy\n\t\tssrv := srv   // copy\n\t\tjob, err := cronlib.NewJobModel(\n\t\t\tspec,\n\t\t\tfunc() {\n\t\t\t\tstdout(ssrv, tspec)\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\n\t\terr = cron.Register(srv, job)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\t}\n\n\tcron.Start()\n\tlog.Println(\"cron start\")\n\tcron.Wait()\n}\n\nfunc stdout(srv, spec string) {\n\tlog.Println(srv, spec)\n}\n"
  },
  {
    "path": "example/parse_time/parse_next_time.go",
    "content": "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 */1 * *\")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tfmt.Println(\" now: \", time.Now())\n\tnext := t.Next(time.Now())\n\tfmt.Println(\"next: \", next)\n}\n\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/rfyiamcool/cronlib\n\ngo 1.14\n\nrequire github.com/stretchr/testify v1.6.1\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "scheduler.go",
    "content": "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 to cronlib.cron_parser.go\n// \"github.com/robfig/cron\"\n\nconst (\n\tOnMode  = true\n\tOffMode = false\n)\n\nvar (\n\tErrNotFoundJob     = errors.New(\"not found job\")\n\tErrAlreadyRegister = errors.New(\"the job already in pool\")\n\tErrJobDOFuncNil    = errors.New(\"callback func is nil\")\n\tErrCronSpecInvalid = errors.New(\"crontab spec is invalid\")\n)\n\n// null logger\nvar defualtLogger = func(level, s string) {}\n\ntype loggerType func(level, s string)\n\nfunc SetLogger(logger loggerType) {\n\tdefualtLogger = logger\n}\n\n// panic call\nvar panicCaller = func(srv, err string) {\n}\n\ntype panicType func(srv, err string)\n\nfunc SetPanicCaller(p panicType) {\n\tpanicCaller = p\n}\n\n// New - create CronSchduler\nfunc New() *CronSchduler {\n\tctx, cancel := context.WithCancel(context.Background())\n\treturn &CronSchduler{\n\t\ttasks:  make(map[string]*JobModel),\n\t\tctx:    ctx,\n\t\tcancel: cancel,\n\t\twg:     &sync.WaitGroup{},\n\t\tonce:   &sync.Once{},\n\t}\n}\n\n// CronSchduler\ntype CronSchduler struct {\n\ttasks  map[string]*JobModel\n\tctx    context.Context\n\tcancel context.CancelFunc\n\n\twg   *sync.WaitGroup\n\tonce *sync.Once\n\n\tsync.RWMutex\n}\n\n// Register - only register srv's job model, don't start auto.\nfunc (c *CronSchduler) Register(srv string, model *JobModel) error {\n\treturn c.reset(srv, model, true, false)\n}\n\n// UpdateJobModel - stop old job, update srv's job model\nfunc (c *CronSchduler) UpdateJobModel(srv string, model *JobModel) error {\n\treturn c.reset(srv, model, false, true)\n}\n\n// DynamicRegister - after cronlib already run, dynamic add a job, the job autostart by cronlib.\nfunc (c *CronSchduler) DynamicRegister(srv string, model *JobModel) error {\n\treturn c.reset(srv, model, false, true)\n}\n\n// reset - reset srv model\nfunc (c *CronSchduler) reset(srv string, model *JobModel, denyReplace, autoStart bool) error {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\t// validate model\n\terr := model.validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcctx, cancel := context.WithCancel(c.ctx)\n\tmodel.ctx = cctx\n\tmodel.cancel = cancel\n\tmodel.srv = srv\n\n\toldModel, ok := c.tasks[srv]\n\tif denyReplace && ok {\n\t\treturn ErrAlreadyRegister\n\t}\n\n\tif ok {\n\t\toldModel.kill()\n\t}\n\n\tc.tasks[srv] = model\n\tif autoStart {\n\t\tc.wg.Add(1)\n\t\tgo c.tasks[srv].runLoop(c.wg)\n\t}\n\n\treturn nil\n}\n\n// UnRegister - stop and delete srv\nfunc (c *CronSchduler) UnRegister(srv string) error {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\toldModel, ok := c.tasks[srv]\n\tif !ok {\n\t\treturn ErrNotFoundJob\n\t}\n\n\toldModel.kill()\n\tdelete(c.tasks, srv)\n\treturn nil\n}\n\n// Stop - stop all cron job\nfunc (c *CronSchduler) Stop() {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tfor srv, job := range c.tasks {\n\t\tjob.kill()\n\t\tdelete(c.tasks, srv)\n\t}\n\tc.cancel()\n}\n\n// StopService - stop job by serviceName\nfunc (c *CronSchduler) StopService(srv string) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tjob, ok := c.tasks[srv]\n\tif !ok {\n\t\treturn\n\t}\n\n\tjob.kill()\n\tdelete(c.tasks, srv)\n}\n\n// StopServicePrefix - stop job by srv regex prefix.\n// if regex = \"risk.scan\", stop risk.scan.total, risk.scan.user at the same time\nfunc (c *CronSchduler) StopServicePrefix(regex string) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\t// regex match\n\tfor srv, job := range c.tasks {\n\t\tif !strings.HasPrefix(srv, regex) {\n\t\t\tcontinue\n\t\t}\n\n\t\tjob.kill()\n\t\tdelete(c.tasks, srv)\n\t}\n}\n\nfunc validateSpec(spec string) bool {\n\t_, err := Parse(spec)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc getNextDue(spec string) (time.Time, error) {\n\tsc, err := Parse(spec)\n\tif err != nil {\n\t\treturn time.Now(), err\n\t}\n\n\t// avoid time.sub\n\ttime.Sleep(10 * time.Millisecond)\n\tdue := sc.Next(time.Now())\n\treturn due, err\n}\n\nfunc getNextDueSafe(spec string, last time.Time) (time.Time, error) {\n\tvar (\n\t\tdue time.Time\n\t\terr error\n\t)\n\n\tfor {\n\t\tdue, err = getNextDue(spec)\n\t\tif err != nil {\n\t\t\treturn due, err\n\t\t}\n\n\t\tif last.Equal(due) {\n\t\t\t// avoid time.sub lost some accuracy, repeat do job.\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\n\t\tbreak\n\t}\n\n\treturn due, err\n}\n\nfunc (c *CronSchduler) Start() {\n\t// only once call\n\tc.once.Do(func() {\n\n\t\tfor _, job := range c.tasks {\n\t\t\tc.wg.Add(1)\n\t\t\tjob.runLoop(c.wg)\n\t\t}\n\n\t})\n}\n\n// Wait - if all jobs is exited, return.\nfunc (c *CronSchduler) Wait() {\n\tc.wg.Wait()\n}\n\n// WaitStop - when stop cronlib controller, return.\nfunc (c *CronSchduler) WaitStop() {\n\tselect {\n\tcase <-c.ctx.Done():\n\t}\n}\n\nfunc (c *CronSchduler) GetServiceCron(srv string) (*JobModel, error) {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\toldModel, ok := c.tasks[srv]\n\tif !ok {\n\t\treturn nil, ErrNotFoundJob\n\t}\n\n\treturn oldModel, nil\n}\n\n// NewJobModel - defualt block sync callfunc\nfunc NewJobModel(spec string, f func(), options ...JobOption) (*JobModel, error) {\n\tvar err error\n\tjob := &JobModel{\n\t\trunning:    true,\n\t\tasync:      false,\n\t\tdo:         f,\n\t\tspec:       spec,\n\t\tnotifyChan: make(chan int, 1), // avoid block\n\t}\n\n\tfor _, opt := range options {\n\t\tif opt != nil {\n\t\t\tif err := opt(job); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\terr = job.validate()\n\tif err != nil {\n\t\treturn job, err\n\t}\n\n\treturn job, nil\n}\n\ntype JobOption func(*JobModel) error\n\nfunc AsyncMode() JobOption {\n\treturn func(o *JobModel) error {\n\t\to.async = true\n\t\treturn nil\n\t}\n}\n\nfunc TryCatchMode() JobOption {\n\treturn func(o *JobModel) error {\n\t\to.tryCatch = true\n\t\treturn nil\n\t}\n}\n\ntype JobModel struct {\n\t// srv name\n\tsrv string\n\n\t// callfunc\n\tdo func()\n\n\t// if async = true; go func() { do() }\n\tasync bool\n\n\t// try catch panic\n\ttryCatch bool\n\n\t// cron spec\n\tspec string\n\n\t// for control\n\tctx        context.Context\n\tcancel     context.CancelFunc\n\tnotifyChan chan int\n\n\t// break for { ... } loop\n\trunning bool\n\n\t// ensure job worker is exited already\n\texited bool\n\n\tsync.RWMutex\n}\n\nfunc (j *JobModel) SetTryCatch(b bool) {\n\tj.tryCatch = b\n}\n\nfunc (j *JobModel) SetAsyncMode(b bool) {\n\tj.async = b\n}\n\nfunc (j *JobModel) validate() error {\n\tif j.do == nil {\n\t\treturn ErrJobDOFuncNil\n\t}\n\n\tif _, err := getNextDue(j.spec); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (j *JobModel) runLoop(wg *sync.WaitGroup) {\n\tgo j.run(wg)\n}\n\nfunc (j *JobModel) run(wg *sync.WaitGroup) {\n\tvar (\n\t\t// stdout do time cost\n\t\tdoTimeCostFunc = func() {\n\t\t\tstartTS := time.Now()\n\t\t\tdefualtLogger(\"info\",\n\t\t\t\tfmt.Sprintf(\"scheduler service: %s begin run\",\n\t\t\t\t\tj.srv,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tif j.tryCatch {\n\t\t\t\ttryCatch(j)\n\t\t\t} else {\n\t\t\t\tj.do()\n\t\t\t}\n\n\t\t\tdefualtLogger(\"info\",\n\t\t\t\tfmt.Sprintf(\"scheduler service: %s has been finished, time cost: %s, spec: %s\",\n\t\t\t\t\tj.srv,\n\t\t\t\t\ttime.Since(startTS).String(),\n\t\t\t\t\tj.spec,\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\ttimer        *time.Timer\n\t\tlastNextTime time.Time\n\t\tdue          time.Time\n\t\tinterval     time.Duration\n\n\t\terr error\n\t)\n\n\t// parse crontab spec\n\tdue, err = getNextDue(j.spec)\n\tinterval = due.Sub(time.Now())\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tlastNextTime = due\n\tdefualtLogger(\"info\",\n\t\tfmt.Sprintf(\"scheduler service: %s next time is %s, sub: %s\",\n\t\t\tj.srv,\n\t\t\tdue.String(),\n\t\t\tinterval.String(),\n\t\t),\n\t)\n\n\t// int timer\n\ttimer = time.NewTimer(interval)\n\n\t// release join counter\n\tdefer func() {\n\t\ttimer.Stop()\n\t\twg.Done()\n\t\tj.exited = true\n\t}()\n\n\tfor j.running {\n\t\tselect {\n\t\tcase <-timer.C:\n\t\t\tif time.Now().Before(due) {\n\t\t\t\ttimer.Reset(\n\t\t\t\t\tdue.Sub(time.Now()) + 50*time.Millisecond,\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdue, _ := getNextDueSafe(j.spec, lastNextTime)\n\t\t\tlastNextTime = due\n\t\t\tinterval := due.Sub(time.Now())\n\t\t\ttimer.Reset(interval)\n\n\t\t\tif j.async {\n\t\t\t\tgo doTimeCostFunc() // goroutine for per job\n\t\t\t} else {\n\t\t\t\tdoTimeCostFunc()\n\t\t\t}\n\n\t\t\tdefualtLogger(\"info\",\n\t\t\t\tfmt.Sprintf(\"scheduler service: %s next time is %s, sub: %s\",\n\t\t\t\t\tj.srv,\n\t\t\t\t\tdue.String(),\n\t\t\t\t\tinterval.String(),\n\t\t\t\t),\n\t\t\t)\n\n\t\tcase <-j.notifyChan:\n\t\t\t// parse crontab spec again !\n\t\t\tcontinue\n\n\t\tcase <-j.ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (j *JobModel) kill() {\n\tj.running = false\n\tj.cancel()\n}\n\nfunc (j *JobModel) workerExited() bool {\n\treturn j.exited\n}\n\nfunc (j *JobModel) notifySig() {\n\tselect {\n\tcase j.notifyChan <- 1:\n\tdefault:\n\t\t// avoid block\n\t\treturn\n\t}\n}\n\nfunc tryCatch(job *JobModel) {\n\tdefer func() {\n\t\tif e := recover(); e != nil {\n\t\t\tpanicCaller(\n\t\t\t\tjob.srv,\n\t\t\t\tfmt.Sprintf(\"%v\", e),\n\t\t\t)\n\n\t\t\tdefualtLogger(\n\t\t\t\t\"error\",\n\t\t\t\tfmt.Sprintf(\"srv: %s, trycatch panicing %v\", job.srv, e),\n\t\t\t)\n\t\t}\n\t}()\n\n\tjob.do()\n}\n"
  },
  {
    "path": "scheduler_test.go",
    "content": "package cronlib\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestToDO(t *testing.T) {\n\tassert.Equal(t, 0, 0)\n}\n"
  }
]