mirror of
https://gitee.com/johng/gf.git
synced 2024-12-01 19:57:40 +08:00
add #
for cron pattern that can ignore seconds, which makes the cron pattern running in minimum minute like linux crontab pattern (#3306)
This commit is contained in:
parent
51326f3d02
commit
5307f0742e
@ -38,6 +38,20 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.6.2 h1:A9SPHECm3cu8Px+JmCfU7F9sUM00Azm086V/IGOTjyA=
|
||||
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.6.2/go.mod h1:uN8UUEk42sgxm7yPucxl94vOjlstJ4TDdmLvP+Ywqxo=
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.6.2 h1:jSeSz2m/8m9h6ijkOp8q10UNHTc+aoU1xhnFrdFsUxI=
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.6.2/go.mod h1:5f28iWJU/fqr9OH90sSV5WgsBYz4cIEYsNRKdCL75LI=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.6.2 h1:iCUoR8je08TehU633pj+vmNdQ/qmWLTpHYQx7yERTv8=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.6.2/go.mod h1:bPYIZ56MyKvLp1P+EWFpkyJ+wofFF9yxChgr/iScP8A=
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.6.2 h1:f4RcVDNRGH/aCGmnNv/CxZtwNOG1zxNSNoTuFBKPhAc=
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.6.2/go.mod h1:2okN4j0vs8fNAeYsTDYOhQclN0jkcFvv8LEzoKgwtSw=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.6.2 h1:qXk04hgkn8zPiuwK5EG/0oGf3Fh0MTkw7b5qwaR1+Yo=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.6.2/go.mod h1:T8ON3Jb2wZuC2qitj8QZSMGFbgLNk2ZQIFRSl1OC8qo=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.6.2 h1:IljLQPa+34RRHDO3+dOSzN1rUcEG8+MuJ3Zzq3Bvg08=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.6.2/go.mod h1:S2LEZGkPxA7ZXYDXNxjDPl8LcXdNcM7ODVUqgUz8zpg=
|
||||
github.com/gogf/gf/v2 v2.6.2 h1:TvI1UEH2RDbgFVlJJjkc/6ct6+5zjbOS5MiJ2ESG8qg=
|
||||
github.com/gogf/gf/v2 v2.6.2/go.mod h1:x2XONYcI4hRQ/4gMNbWHmZrNzSEIg20s2NULbzom5k0=
|
||||
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f h1:7xfXR/BhG3JDqO1s45n65Oyx9t4E/UqDOXep6jXdLCM=
|
||||
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f/go.mod h1:HnYoio6S7VaFJdryKcD/r9HgX+4QzYfr00XiXUo/xz0=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
|
39
example/os/cron/linux-crontab/main.go
Normal file
39
example/os/cron/linux-crontab/main.go
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gcron"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("start:", gtime.Now())
|
||||
var (
|
||||
err error
|
||||
pattern1 = "# * * * * *"
|
||||
pattern2 = "# */2 * * * *"
|
||||
)
|
||||
_, err = gcron.Add(gctx.New(), pattern1, func(ctx context.Context) {
|
||||
fmt.Println(pattern1, gtime.Now())
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = gcron.Add(gctx.New(), pattern2, func(ctx context.Context) {
|
||||
fmt.Println(pattern2, gtime.Now())
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/gogf/gf/contrib/registry/etcd/v2"
|
||||
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
|
||||
"github.com/gogf/gf/contrib/trace/otlpgrpc/v2"
|
||||
"github.com/gogf/gf/example/trace/grpc_with_db/protobuf/user"
|
||||
"github.com/gogf/gf/example/trace/grpc-with-db/protobuf/user"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gtrace"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
@ -11,7 +11,7 @@ import (
|
||||
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
|
||||
"github.com/gogf/gf/contrib/registry/etcd/v2"
|
||||
"github.com/gogf/gf/contrib/trace/otlpgrpc/v2"
|
||||
"github.com/gogf/gf/example/trace/grpc_with_db/protobuf/user"
|
||||
"github.com/gogf/gf/example/trace/grpc-with-db/protobuf/user"
|
||||
|
||||
"context"
|
||||
"fmt"
|
@ -27,12 +27,12 @@ var (
|
||||
defaultCron = New()
|
||||
)
|
||||
|
||||
// SetLogger sets the logger for cron.
|
||||
// SetLogger sets the global logger for cron.
|
||||
func SetLogger(logger glog.ILogger) {
|
||||
defaultCron.SetLogger(logger)
|
||||
}
|
||||
|
||||
// GetLogger returns the logger in the cron.
|
||||
// GetLogger returns the global logger in the cron.
|
||||
func GetLogger() glog.ILogger {
|
||||
return defaultCron.GetLogger()
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/gogf/gf/v2/os/gtimer"
|
||||
)
|
||||
|
||||
// Cron stores all the cron job entries.
|
||||
type Cron struct {
|
||||
idGen *gtype.Int64 // Used for unique name generation.
|
||||
status *gtype.Int // Timed task status(0: Not Start; 1: Running; 2: Stopped; -1: Closed)
|
||||
@ -44,7 +45,9 @@ func (c *Cron) GetLogger() glog.ILogger {
|
||||
}
|
||||
|
||||
// AddEntry creates and returns a new Entry object.
|
||||
func (c *Cron) AddEntry(ctx context.Context, pattern string, job JobFunc, times int, isSingleton bool, name ...string) (*Entry, error) {
|
||||
func (c *Cron) AddEntry(
|
||||
ctx context.Context, pattern string, job JobFunc, times int, isSingleton bool, name ...string,
|
||||
) (*Entry, error) {
|
||||
var (
|
||||
entryName = ""
|
||||
infinite = false
|
||||
|
@ -51,7 +51,11 @@ type doAddEntryInput struct {
|
||||
func (c *Cron) doAddEntry(in doAddEntryInput) (*Entry, error) {
|
||||
if in.Name != "" {
|
||||
if c.Search(in.Name) != nil {
|
||||
return nil, gerror.NewCodef(gcode.CodeInvalidOperation, `cron job "%s" already exists`, in.Name)
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeInvalidOperation,
|
||||
`duplicated cron job name "%s", already exists`,
|
||||
in.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
schedule, err := newSchedule(in.Pattern)
|
||||
@ -91,103 +95,104 @@ func (c *Cron) doAddEntry(in doAddEntryInput) (*Entry, error) {
|
||||
}
|
||||
|
||||
// IsSingleton return whether this entry is a singleton timed task.
|
||||
func (entry *Entry) IsSingleton() bool {
|
||||
return entry.timerEntry.IsSingleton()
|
||||
func (e *Entry) IsSingleton() bool {
|
||||
return e.timerEntry.IsSingleton()
|
||||
}
|
||||
|
||||
// SetSingleton sets the entry running in singleton mode.
|
||||
func (entry *Entry) SetSingleton(enabled bool) {
|
||||
entry.timerEntry.SetSingleton(enabled)
|
||||
func (e *Entry) SetSingleton(enabled bool) {
|
||||
e.timerEntry.SetSingleton(enabled)
|
||||
}
|
||||
|
||||
// SetTimes sets the times which the entry can run.
|
||||
func (entry *Entry) SetTimes(times int) {
|
||||
entry.times.Set(times)
|
||||
entry.infinite.Set(false)
|
||||
func (e *Entry) SetTimes(times int) {
|
||||
e.times.Set(times)
|
||||
e.infinite.Set(false)
|
||||
}
|
||||
|
||||
// Status returns the status of entry.
|
||||
func (entry *Entry) Status() int {
|
||||
return entry.timerEntry.Status()
|
||||
func (e *Entry) Status() int {
|
||||
return e.timerEntry.Status()
|
||||
}
|
||||
|
||||
// SetStatus sets the status of the entry.
|
||||
func (entry *Entry) SetStatus(status int) int {
|
||||
return entry.timerEntry.SetStatus(status)
|
||||
func (e *Entry) SetStatus(status int) int {
|
||||
return e.timerEntry.SetStatus(status)
|
||||
}
|
||||
|
||||
// Start starts running the entry.
|
||||
func (entry *Entry) Start() {
|
||||
entry.timerEntry.Start()
|
||||
func (e *Entry) Start() {
|
||||
e.timerEntry.Start()
|
||||
}
|
||||
|
||||
// Stop stops running the entry.
|
||||
func (entry *Entry) Stop() {
|
||||
entry.timerEntry.Stop()
|
||||
func (e *Entry) Stop() {
|
||||
e.timerEntry.Stop()
|
||||
}
|
||||
|
||||
// Close stops and removes the entry from cron.
|
||||
func (entry *Entry) Close() {
|
||||
entry.cron.entries.Remove(entry.Name)
|
||||
entry.timerEntry.Close()
|
||||
func (e *Entry) Close() {
|
||||
e.cron.entries.Remove(e.Name)
|
||||
e.timerEntry.Close()
|
||||
}
|
||||
|
||||
// checkAndRun is the core timing task check logic.
|
||||
func (entry *Entry) checkAndRun(ctx context.Context) {
|
||||
// This function is called every second.
|
||||
func (e *Entry) checkAndRun(ctx context.Context) {
|
||||
currentTime := time.Now()
|
||||
if !entry.schedule.checkMeetAndUpdateLastSeconds(ctx, currentTime) {
|
||||
if !e.schedule.checkMeetAndUpdateLastSeconds(ctx, currentTime) {
|
||||
return
|
||||
}
|
||||
switch entry.cron.status.Val() {
|
||||
switch e.cron.status.Val() {
|
||||
case StatusStopped:
|
||||
return
|
||||
|
||||
case StatusClosed:
|
||||
entry.logDebugf(ctx, `cron job "%s" is removed`, entry.getJobNameWithPattern())
|
||||
entry.Close()
|
||||
e.logDebugf(ctx, `cron job "%s" is removed`, e.getJobNameWithPattern())
|
||||
e.Close()
|
||||
|
||||
case StatusReady, StatusRunning:
|
||||
defer func() {
|
||||
if exception := recover(); exception != nil {
|
||||
// Exception caught, it logs the error content to logger in default behavior.
|
||||
entry.logErrorf(ctx,
|
||||
e.logErrorf(ctx,
|
||||
`cron job "%s(%s)" end with error: %+v`,
|
||||
entry.jobName, entry.schedule.pattern, exception,
|
||||
e.jobName, e.schedule.pattern, exception,
|
||||
)
|
||||
} else {
|
||||
entry.logDebugf(ctx, `cron job "%s" ends`, entry.getJobNameWithPattern())
|
||||
e.logDebugf(ctx, `cron job "%s" ends`, e.getJobNameWithPattern())
|
||||
}
|
||||
if entry.timerEntry.Status() == StatusClosed {
|
||||
entry.Close()
|
||||
if e.timerEntry.Status() == StatusClosed {
|
||||
e.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Running times check.
|
||||
if !entry.infinite.Val() {
|
||||
times := entry.times.Add(-1)
|
||||
if !e.infinite.Val() {
|
||||
times := e.times.Add(-1)
|
||||
if times <= 0 {
|
||||
if entry.timerEntry.SetStatus(StatusClosed) == StatusClosed || times < 0 {
|
||||
if e.timerEntry.SetStatus(StatusClosed) == StatusClosed || times < 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.logDebugf(ctx, `cron job "%s" starts`, entry.getJobNameWithPattern())
|
||||
entry.Job(ctx)
|
||||
e.logDebugf(ctx, `cron job "%s" starts`, e.getJobNameWithPattern())
|
||||
e.Job(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (entry *Entry) getJobNameWithPattern() string {
|
||||
return fmt.Sprintf(`%s(%s)`, entry.jobName, entry.schedule.pattern)
|
||||
func (e *Entry) getJobNameWithPattern() string {
|
||||
return fmt.Sprintf(`%s(%s)`, e.jobName, e.schedule.pattern)
|
||||
}
|
||||
|
||||
func (entry *Entry) logDebugf(ctx context.Context, format string, v ...interface{}) {
|
||||
if logger := entry.cron.GetLogger(); logger != nil {
|
||||
func (e *Entry) logDebugf(ctx context.Context, format string, v ...interface{}) {
|
||||
if logger := e.cron.GetLogger(); logger != nil {
|
||||
logger.Debugf(ctx, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (entry *Entry) logErrorf(ctx context.Context, format string, v ...interface{}) {
|
||||
logger := entry.cron.GetLogger()
|
||||
func (e *Entry) logErrorf(ctx context.Context, format string, v ...interface{}) {
|
||||
logger := e.cron.GetLogger()
|
||||
if logger == nil {
|
||||
logger = glog.DefaultLogger()
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
package gcron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -23,34 +22,48 @@ import (
|
||||
type cronSchedule struct {
|
||||
createTimestamp int64 // Created timestamp in seconds.
|
||||
everySeconds int64 // Running interval in seconds.
|
||||
pattern string // The raw cron pattern string.
|
||||
pattern string // The raw cron pattern string that is passed in cron job creation.
|
||||
ignoreSeconds bool // Mark the pattern is standard 5 parts crontab pattern instead 6 parts pattern.
|
||||
secondMap map[int]struct{} // Job can run in these second numbers.
|
||||
minuteMap map[int]struct{} // Job can run in these minute numbers.
|
||||
hourMap map[int]struct{} // Job can run in these hour numbers.
|
||||
dayMap map[int]struct{} // Job can run in these day numbers.
|
||||
weekMap map[int]struct{} // Job can run in these week numbers.
|
||||
monthMap map[int]struct{} // Job can run in these moth numbers.
|
||||
lastTimestamp *gtype.Int64 // Last timestamp number, for timestamp fix in some delay.
|
||||
|
||||
// This field stores the timestamp that meets schedule latest.
|
||||
lastMeetTimestamp *gtype.Int64
|
||||
|
||||
// Last timestamp number, for timestamp fix in some latency.
|
||||
lastCheckTimestamp *gtype.Int64
|
||||
}
|
||||
|
||||
type patternItemType int
|
||||
|
||||
const (
|
||||
patternItemTypeSecond patternItemType = iota
|
||||
patternItemTypeMinute
|
||||
patternItemTypeHour
|
||||
patternItemTypeDay
|
||||
patternItemTypeWeek
|
||||
patternItemTypeMonth
|
||||
)
|
||||
|
||||
const (
|
||||
// regular expression for cron pattern, which contains 6 parts of time units.
|
||||
regexForCron = `^([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,A-Za-z]+)\s+([\-/\d\*\?,A-Za-z]+)$`
|
||||
patternItemTypeUnknown = iota
|
||||
patternItemTypeWeek
|
||||
patternItemTypeMonth
|
||||
regexForCron = `^([\-/\d\*,#]+)\s+([\-/\d\*,]+)\s+([\-/\d\*,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*,A-Za-z]+)\s+([\-/\d\*\?,A-Za-z]+)$`
|
||||
)
|
||||
|
||||
var (
|
||||
// Predefined pattern map.
|
||||
predefinedPatternMap = map[string]string{
|
||||
"@yearly": "0 0 0 1 1 *",
|
||||
"@annually": "0 0 0 1 1 *",
|
||||
"@monthly": "0 0 0 1 * *",
|
||||
"@weekly": "0 0 0 * * 0",
|
||||
"@daily": "0 0 0 * * *",
|
||||
"@midnight": "0 0 0 * * *",
|
||||
"@hourly": "0 0 * * * *",
|
||||
"@yearly": "# 0 0 1 1 *",
|
||||
"@annually": "# 0 0 1 1 *",
|
||||
"@monthly": "# 0 0 1 * *",
|
||||
"@weekly": "# 0 0 * * 0",
|
||||
"@daily": "# 0 0 * * *",
|
||||
"@midnight": "# 0 0 * * *",
|
||||
"@hourly": "# 0 * * * *",
|
||||
}
|
||||
// Short month name to its number.
|
||||
monthShortNameMap = map[string]int{
|
||||
@ -107,7 +120,7 @@ var (
|
||||
// newSchedule creates and returns a schedule object for given cron pattern.
|
||||
func newSchedule(pattern string) (*cronSchedule, error) {
|
||||
var currentTimestamp = time.Now().Unix()
|
||||
// Check if the predefined patterns.
|
||||
// Check given `pattern` if the predefined patterns.
|
||||
if match, _ := gregex.MatchString(`(@\w+)\s*(\w*)\s*`, pattern); len(match) > 0 {
|
||||
key := strings.ToLower(match[1])
|
||||
if v, ok := predefinedPatternMap[key]; ok {
|
||||
@ -121,80 +134,90 @@ func newSchedule(pattern string) (*cronSchedule, error) {
|
||||
createTimestamp: currentTimestamp,
|
||||
everySeconds: int64(d.Seconds()),
|
||||
pattern: pattern,
|
||||
lastTimestamp: gtype.NewInt64(currentTimestamp),
|
||||
lastMeetTimestamp: gtype.NewInt64(currentTimestamp),
|
||||
lastCheckTimestamp: gtype.NewInt64(currentTimestamp),
|
||||
}, nil
|
||||
} else {
|
||||
return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
|
||||
}
|
||||
}
|
||||
// Handle the common cron pattern, like:
|
||||
// 0 0 0 1 1 2
|
||||
if match, _ := gregex.MatchString(regexForCron, pattern); len(match) == 7 {
|
||||
schedule := &cronSchedule{
|
||||
// Handle given `pattern` as common 6 parts pattern.
|
||||
match, _ := gregex.MatchString(regexForCron, pattern)
|
||||
if len(match) != 7 {
|
||||
return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
|
||||
}
|
||||
var (
|
||||
err error
|
||||
cs = &cronSchedule{
|
||||
createTimestamp: currentTimestamp,
|
||||
everySeconds: 0,
|
||||
pattern: pattern,
|
||||
lastTimestamp: gtype.NewInt64(currentTimestamp),
|
||||
lastMeetTimestamp: gtype.NewInt64(currentTimestamp),
|
||||
lastCheckTimestamp: gtype.NewInt64(currentTimestamp),
|
||||
}
|
||||
)
|
||||
|
||||
// Second.
|
||||
if m, err := parsePatternItem(match[1], 0, 59, false); err != nil {
|
||||
return nil, err
|
||||
if match[1] == "#" {
|
||||
cs.ignoreSeconds = true
|
||||
} else {
|
||||
schedule.secondMap = m
|
||||
cs.secondMap, err = parsePatternItem(match[1], 0, 59, false, patternItemTypeSecond)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Minute.
|
||||
if m, err := parsePatternItem(match[2], 0, 59, false); err != nil {
|
||||
cs.minuteMap, err = parsePatternItem(match[2], 0, 59, false, patternItemTypeMinute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
schedule.minuteMap = m
|
||||
}
|
||||
// Hour.
|
||||
if m, err := parsePatternItem(match[3], 0, 23, false); err != nil {
|
||||
cs.hourMap, err = parsePatternItem(match[3], 0, 23, false, patternItemTypeHour)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
schedule.hourMap = m
|
||||
}
|
||||
// Day.
|
||||
if m, err := parsePatternItem(match[4], 1, 31, true); err != nil {
|
||||
cs.dayMap, err = parsePatternItem(match[4], 1, 31, true, patternItemTypeDay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
schedule.dayMap = m
|
||||
}
|
||||
// Month.
|
||||
if m, err := parsePatternItem(match[5], 1, 12, false); err != nil {
|
||||
cs.monthMap, err = parsePatternItem(match[5], 1, 12, false, patternItemTypeMonth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
schedule.monthMap = m
|
||||
}
|
||||
// Week.
|
||||
if m, err := parsePatternItem(match[6], 0, 6, true); err != nil {
|
||||
cs.weekMap, err = parsePatternItem(match[6], 0, 6, true, patternItemTypeWeek)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
schedule.weekMap = m
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
// parsePatternItem parses every item in the pattern and returns the result as map, which is used for indexing.
|
||||
func parsePatternItem(item string, min int, max int, allowQuestionMark bool) (map[int]struct{}, error) {
|
||||
m := make(map[int]struct{}, max-min+1)
|
||||
func parsePatternItem(
|
||||
item string, min int, max int,
|
||||
allowQuestionMark bool, itemType patternItemType,
|
||||
) (itemMap map[int]struct{}, err error) {
|
||||
itemMap = make(map[int]struct{}, max-min+1)
|
||||
if item == "*" || (allowQuestionMark && item == "?") {
|
||||
for i := min; i <= max; i++ {
|
||||
m[i] = struct{}{}
|
||||
itemMap[i] = struct{}{}
|
||||
}
|
||||
return m, nil
|
||||
return itemMap, nil
|
||||
}
|
||||
// Like: MON,FRI
|
||||
// Example: 1-10/2,11-30/3
|
||||
var number int
|
||||
for _, itemElem := range strings.Split(item, ",") {
|
||||
var (
|
||||
interval = 1
|
||||
intervalArray = strings.Split(itemElem, "/")
|
||||
)
|
||||
if len(intervalArray) == 2 {
|
||||
if number, err := strconv.Atoi(intervalArray[1]); err != nil {
|
||||
return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem)
|
||||
if number, err = strconv.Atoi(intervalArray[1]); err != nil {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem,
|
||||
)
|
||||
} else {
|
||||
interval = number
|
||||
}
|
||||
@ -202,22 +225,14 @@ func parsePatternItem(item string, min int, max int, allowQuestionMark bool) (ma
|
||||
var (
|
||||
rangeMin = min
|
||||
rangeMax = max
|
||||
itemType = patternItemTypeUnknown
|
||||
rangeArray = strings.Split(intervalArray[0], "-") // Like: 1-30, JAN-DEC
|
||||
rangeArray = strings.Split(intervalArray[0], "-") // Example: 1-30, JAN-DEC
|
||||
)
|
||||
switch max {
|
||||
case 6:
|
||||
// It's checking week field.
|
||||
itemType = patternItemTypeWeek
|
||||
|
||||
case 12:
|
||||
// It's checking month field.
|
||||
itemType = patternItemTypeMonth
|
||||
}
|
||||
// Eg: */5
|
||||
// Example: 1-30/2
|
||||
if rangeArray[0] != "*" {
|
||||
if number, err := parsePatternItemValue(rangeArray[0], itemType); err != nil {
|
||||
return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem)
|
||||
if number, err = parseWeekAndMonthNameToInt(rangeArray[0], itemType); err != nil {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem,
|
||||
)
|
||||
} else {
|
||||
rangeMin = number
|
||||
if len(intervalArray) == 1 {
|
||||
@ -225,22 +240,25 @@ func parsePatternItem(item string, min int, max int, allowQuestionMark bool) (ma
|
||||
}
|
||||
}
|
||||
}
|
||||
// Example: 1-30/2
|
||||
if len(rangeArray) == 2 {
|
||||
if number, err := parsePatternItemValue(rangeArray[1], itemType); err != nil {
|
||||
return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem)
|
||||
if number, err = parseWeekAndMonthNameToInt(rangeArray[1], itemType); err != nil {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem,
|
||||
)
|
||||
} else {
|
||||
rangeMax = number
|
||||
}
|
||||
}
|
||||
for i := rangeMin; i <= rangeMax; i += interval {
|
||||
m[i] = struct{}{}
|
||||
itemMap[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
return
|
||||
}
|
||||
|
||||
// parsePatternItemValue parses the field value to a number according to its field type.
|
||||
func parsePatternItemValue(value string, itemType int) (int, error) {
|
||||
// parseWeekAndMonthNameToInt parses the field value to a number according to its field type.
|
||||
func parseWeekAndMonthNameToInt(value string, itemType patternItemType) (int, error) {
|
||||
if gregex.IsMatchString(`^\d+$`, value) {
|
||||
// It is pure number.
|
||||
if number, err := strconv.Atoi(value); err == nil {
|
||||
@ -268,145 +286,3 @@ func parsePatternItemValue(value string, itemType int) (int, error) {
|
||||
}
|
||||
return 0, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern value: "%s"`, value)
|
||||
}
|
||||
|
||||
// checkMeetAndUpdateLastSeconds checks if the given time `t` meets the runnable point for the job.
|
||||
func (s *cronSchedule) checkMeetAndUpdateLastSeconds(ctx context.Context, t time.Time) bool {
|
||||
var (
|
||||
lastTimestamp = s.getAndUpdateLastTimestamp(ctx, t)
|
||||
lastTime = gtime.NewFromTimeStamp(lastTimestamp)
|
||||
)
|
||||
|
||||
if s.everySeconds != 0 {
|
||||
// It checks using interval.
|
||||
secondsAfterCreated := lastTime.Timestamp() - s.createTimestamp
|
||||
if secondsAfterCreated > 0 {
|
||||
return secondsAfterCreated%s.everySeconds == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// It checks using normal cron pattern.
|
||||
if _, ok := s.secondMap[lastTime.Second()]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := s.minuteMap[lastTime.Minute()]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := s.hourMap[lastTime.Hour()]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := s.dayMap[lastTime.Day()]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := s.monthMap[lastTime.Month()]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := s.weekMap[int(lastTime.Weekday())]; !ok {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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 *cronSchedule) Next(t time.Time) time.Time {
|
||||
if s.everySeconds != 0 {
|
||||
var (
|
||||
diff = t.Unix() - s.createTimestamp
|
||||
count = diff/s.everySeconds + 1
|
||||
)
|
||||
return t.Add(time.Duration(count*s.everySeconds) * time.Second)
|
||||
}
|
||||
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
var (
|
||||
loc = t.Location()
|
||||
added = false
|
||||
yearLimit = t.Year() + 5
|
||||
)
|
||||
|
||||
WRAP:
|
||||
if t.Year() > yearLimit {
|
||||
return t // who will care the job that run in five years later
|
||||
}
|
||||
|
||||
for !s.match(s.monthMap, int(t.Month())) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
|
||||
}
|
||||
t = t.AddDate(0, 1, 0)
|
||||
// need recheck
|
||||
if t.Month() == time.January {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for !s.dayMatches(t) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
t = t.AddDate(0, 0, 1)
|
||||
|
||||
// Notice if the hour is no longer midnight due to DST.
|
||||
// Add an hour if it's 23, subtract an hour if it's 1.
|
||||
if t.Hour() != 0 {
|
||||
if t.Hour() > 12 {
|
||||
t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
|
||||
} else {
|
||||
t = t.Add(time.Duration(-t.Hour()) * time.Hour)
|
||||
}
|
||||
}
|
||||
if t.Day() == 1 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
for !s.match(s.hourMap, t.Hour()) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
|
||||
}
|
||||
t = t.Add(time.Hour)
|
||||
// need recheck
|
||||
if t.Hour() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
for !s.match(s.minuteMap, t.Minute()) {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Minute)
|
||||
}
|
||||
t = t.Add(1 * time.Minute)
|
||||
|
||||
if t.Minute() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
for !s.match(s.secondMap, t.Second()) {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Second)
|
||||
}
|
||||
t = t.Add(1 * time.Second)
|
||||
if t.Second() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
return t.In(loc)
|
||||
}
|
||||
|
||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
||||
// restrictions are satisfied by the given time.
|
||||
func (s *cronSchedule) dayMatches(t time.Time) bool {
|
||||
_, ok1 := s.dayMap[t.Day()]
|
||||
_, ok2 := s.weekMap[int(t.Weekday())]
|
||||
return ok1 && ok2
|
||||
}
|
||||
|
||||
func (s *cronSchedule) match(m map[int]struct{}, key int) bool {
|
||||
_, ok := m[key]
|
||||
return ok
|
||||
}
|
||||
|
152
os/gcron/gcron_schedule_check.go
Normal file
152
os/gcron/gcron_schedule_check.go
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gcron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// checkMeetAndUpdateLastSeconds checks if the given time `t` meets the runnable point for the job.
|
||||
// This function is called every second.
|
||||
func (s *cronSchedule) checkMeetAndUpdateLastSeconds(ctx context.Context, currentTime time.Time) (ok bool) {
|
||||
var (
|
||||
lastCheckTimestamp = s.getAndUpdateLastCheckTimestamp(ctx, currentTime)
|
||||
lastCheckTime = gtime.NewFromTimeStamp(lastCheckTimestamp)
|
||||
lastMeetTime = gtime.NewFromTimeStamp(s.lastMeetTimestamp.Val())
|
||||
)
|
||||
defer func() {
|
||||
if ok {
|
||||
s.lastMeetTimestamp.Set(currentTime.Unix())
|
||||
}
|
||||
}()
|
||||
if !s.checkMinIntervalAndItemMapMeet(lastMeetTime.Time, lastCheckTime.Time, currentTime) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkMinIntervalAndItemMapMeet(
|
||||
lastMeetTime, lastCheckTime, currentTime time.Time,
|
||||
) (ok bool) {
|
||||
if s.everySeconds != 0 {
|
||||
// It checks using interval.
|
||||
secondsAfterCreated := lastCheckTime.UnixNano()/1e9 - s.createTimestamp
|
||||
if secondsAfterCreated > 0 {
|
||||
return secondsAfterCreated%s.everySeconds == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
if !s.checkMeetSecond(lastMeetTime, currentTime) {
|
||||
return false
|
||||
}
|
||||
if !s.checkMeetMinute(currentTime) {
|
||||
return false
|
||||
}
|
||||
if !s.checkMeetHour(currentTime) {
|
||||
return false
|
||||
}
|
||||
if !s.checkMeetDay(currentTime) {
|
||||
return false
|
||||
}
|
||||
if !s.checkMeetMonth(currentTime) {
|
||||
return false
|
||||
}
|
||||
if !s.checkMeetWeek(currentTime) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkMeetSecond(lastMeetTime, currentTime time.Time) (ok bool) {
|
||||
if s.ignoreSeconds {
|
||||
if currentTime.Unix()-lastMeetTime.Unix() < 60 {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if !s.keyMatch(s.secondMap, currentTime.Second()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkMeetMinute(currentTime time.Time) (ok bool) {
|
||||
if !s.keyMatch(s.minuteMap, currentTime.Minute()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkMeetHour(currentTime time.Time) (ok bool) {
|
||||
if !s.keyMatch(s.hourMap, currentTime.Hour()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkMeetDay(currentTime time.Time) (ok bool) {
|
||||
if !s.keyMatch(s.dayMap, currentTime.Day()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkMeetMonth(currentTime time.Time) (ok bool) {
|
||||
if !s.keyMatch(s.monthMap, int(currentTime.Month())) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkMeetWeek(currentTime time.Time) (ok bool) {
|
||||
if !s.keyMatch(s.weekMap, int(currentTime.Weekday())) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *cronSchedule) keyMatch(m map[int]struct{}, key int) bool {
|
||||
_, ok := m[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *cronSchedule) checkItemMapMeet(lastMeetTime, currentTime time.Time) (ok bool) {
|
||||
// second.
|
||||
if s.ignoreSeconds {
|
||||
if currentTime.Unix()-lastMeetTime.Unix() < 60 {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if !s.keyMatch(s.secondMap, currentTime.Second()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// minute.
|
||||
if !s.keyMatch(s.minuteMap, currentTime.Minute()) {
|
||||
return false
|
||||
}
|
||||
// hour.
|
||||
if !s.keyMatch(s.hourMap, currentTime.Hour()) {
|
||||
return false
|
||||
}
|
||||
// day.
|
||||
if !s.keyMatch(s.dayMap, currentTime.Day()) {
|
||||
return false
|
||||
}
|
||||
// month.
|
||||
if !s.keyMatch(s.monthMap, int(currentTime.Month())) {
|
||||
return false
|
||||
}
|
||||
// week.
|
||||
if !s.keyMatch(s.weekMap, int(currentTime.Weekday())) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
@ -13,35 +13,48 @@ import (
|
||||
"github.com/gogf/gf/v2/internal/intlog"
|
||||
)
|
||||
|
||||
// getAndUpdateLastTimestamp checks fixes and returns the last timestamp that have delay fix in some seconds.
|
||||
func (s *cronSchedule) getAndUpdateLastTimestamp(ctx context.Context, t time.Time) int64 {
|
||||
// getAndUpdateLastCheckTimestamp checks fixes and returns the last timestamp that have delay fix in some seconds.
|
||||
func (s *cronSchedule) getAndUpdateLastCheckTimestamp(ctx context.Context, t time.Time) int64 {
|
||||
var (
|
||||
currentTimestamp = t.Unix()
|
||||
lastTimestamp = s.lastTimestamp.Val()
|
||||
lastCheckTimestamp = s.lastCheckTimestamp.Val()
|
||||
)
|
||||
switch {
|
||||
// Often happens, timer triggers in the same second.
|
||||
// Example:
|
||||
// lastCheckTimestamp: 10
|
||||
// currentTimestamp: 10
|
||||
case
|
||||
lastTimestamp == currentTimestamp:
|
||||
lastTimestamp += 1
|
||||
lastCheckTimestamp == currentTimestamp:
|
||||
lastCheckTimestamp += 1
|
||||
|
||||
// Often happens, no latency.
|
||||
// Example:
|
||||
// lastCheckTimestamp: 9
|
||||
// currentTimestamp: 10
|
||||
case
|
||||
lastTimestamp == currentTimestamp-1:
|
||||
lastTimestamp = currentTimestamp
|
||||
lastCheckTimestamp == currentTimestamp-1:
|
||||
lastCheckTimestamp = currentTimestamp
|
||||
|
||||
// Latency in 3 seconds, which can be tolerant.
|
||||
// Example:
|
||||
// lastCheckTimestamp: 7/8
|
||||
// currentTimestamp: 10
|
||||
case
|
||||
lastTimestamp == currentTimestamp-2,
|
||||
lastTimestamp == currentTimestamp-3:
|
||||
lastTimestamp += 1
|
||||
lastCheckTimestamp == currentTimestamp-2,
|
||||
lastCheckTimestamp == currentTimestamp-3:
|
||||
lastCheckTimestamp += 1
|
||||
|
||||
// Too much latency, it ignores the fix, the cron job might not be triggered.
|
||||
default:
|
||||
// Too much delay, let's update the last timestamp to current one.
|
||||
intlog.Printf(
|
||||
ctx,
|
||||
`too much delay, last timestamp "%d", current "%d"`,
|
||||
lastTimestamp, currentTimestamp,
|
||||
`too much latency, last timestamp "%d", current "%d", latency "%d"`,
|
||||
lastCheckTimestamp, currentTimestamp, currentTimestamp-lastCheckTimestamp,
|
||||
)
|
||||
lastTimestamp = currentTimestamp
|
||||
lastCheckTimestamp = currentTimestamp
|
||||
}
|
||||
s.lastTimestamp.Set(lastTimestamp)
|
||||
return lastTimestamp
|
||||
s.lastCheckTimestamp.Set(lastCheckTimestamp)
|
||||
return lastCheckTimestamp
|
||||
}
|
||||
|
79
os/gcron/gcron_schedule_next.go
Normal file
79
os/gcron/gcron_schedule_next.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gcron
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 *cronSchedule) Next(lastMeetTime time.Time) time.Time {
|
||||
if s.everySeconds != 0 {
|
||||
var (
|
||||
diff = lastMeetTime.Unix() - s.createTimestamp
|
||||
count = diff/s.everySeconds + 1
|
||||
)
|
||||
return lastMeetTime.Add(time.Duration(count*s.everySeconds) * time.Second)
|
||||
}
|
||||
|
||||
var currentTime = lastMeetTime
|
||||
if s.ignoreSeconds {
|
||||
// Start at the earliest possible time (the upcoming minute).
|
||||
currentTime = currentTime.Add(1*time.Minute - time.Duration(currentTime.Nanosecond())*time.Nanosecond)
|
||||
} else {
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
currentTime = currentTime.Add(1*time.Second - time.Duration(currentTime.Nanosecond())*time.Nanosecond)
|
||||
}
|
||||
|
||||
var (
|
||||
loc = currentTime.Location()
|
||||
yearLimit = currentTime.Year() + 5
|
||||
)
|
||||
|
||||
WRAP:
|
||||
if currentTime.Year() > yearLimit {
|
||||
return currentTime // who will care the job that run in five years later
|
||||
}
|
||||
|
||||
for !s.checkMeetMonth(currentTime) {
|
||||
currentTime = currentTime.AddDate(0, 1, 0)
|
||||
currentTime = time.Date(currentTime.Year(), currentTime.Month(), 1, 0, 0, 0, 0, loc)
|
||||
if currentTime.Month() == time.January {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
for !s.checkMeetWeek(currentTime) || !s.checkMeetDay(currentTime) {
|
||||
currentTime = currentTime.AddDate(0, 0, 1)
|
||||
currentTime = time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 0, 0, 0, 0, loc)
|
||||
if currentTime.Day() == 1 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
for !s.checkMeetHour(currentTime) {
|
||||
currentTime = currentTime.Add(time.Hour)
|
||||
currentTime = currentTime.Truncate(time.Hour)
|
||||
if currentTime.Hour() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
for !s.checkMeetMinute(currentTime) {
|
||||
currentTime = currentTime.Add(1 * time.Minute)
|
||||
currentTime = currentTime.Truncate(time.Minute)
|
||||
if currentTime.Minute() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for !s.checkMeetSecond(lastMeetTime, currentTime) {
|
||||
currentTime = currentTime.Add(1 * time.Second)
|
||||
if currentTime.Second() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
return currentTime.In(loc)
|
||||
}
|
@ -24,15 +24,14 @@ func TestSlash(t *testing.T) {
|
||||
}
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
for _, c := range runs {
|
||||
sched, err := newSchedule(c.spec)
|
||||
s, err := newSchedule(c.spec)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
}
|
||||
t.AssertEQ(sched.weekMap, c.expected)
|
||||
t.AssertEQ(s.weekMap, c.expected)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestNext(t *testing.T) {
|
||||
@ -82,19 +81,26 @@ func TestNext(t *testing.T) {
|
||||
{"Mon Jul 9 23:35 2012", "@daily", "Tue Jul 10 00:00:00 2012"},
|
||||
{"Mon Jul 9 23:35 2012", "@midnight", "Tue Jul 10 00:00:00 2012"},
|
||||
{"Mon Jul 9 23:35 2012", "@hourly", "Tue Jul 10 00:00:00 2012"},
|
||||
|
||||
// Ignore seconds.
|
||||
{"Mon Jul 9 23:35 2012", "# * * * * *", "Mon Jul 9 23:36 2012"},
|
||||
{"Mon Jul 9 23:35 2012", "# */2 * * * *", "Mon Jul 9 23:36 2012"},
|
||||
}
|
||||
|
||||
for _, c := range runs {
|
||||
sched, err := newSchedule(c.spec)
|
||||
s, err := newSchedule(c.spec)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
// fmt.Printf("%+v", sched)
|
||||
actual := sched.Next(getTime(c.time))
|
||||
actual := s.Next(getTime(c.time))
|
||||
expected := getTime(c.expected)
|
||||
if !(actual.Unix() == expected.Unix()) {
|
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
|
||||
t.Errorf(
|
||||
"%s, \"%s\": (expected) %v != %v (actual)",
|
||||
c.time, c.spec, expected, actual,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user