Merge pull request #7894 from hashicorp/b-cronexpr-dst-fix

Fix Daylight saving transition handling
This commit is contained in:
Mahmood Ali
2020-05-12 16:36:11 -04:00
committed by GitHub
14 changed files with 470 additions and 273 deletions

View File

@@ -5,8 +5,8 @@ go 1.12
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/go-units v0.3.3
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75
github.com/gorilla/websocket v1.4.1
github.com/hashicorp/cronexpr v1.1.0
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-rootcerts v1.0.2
github.com/kr/pretty v0.1.0

View File

@@ -4,10 +4,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6 h1:hMI/9mZ+/xcLlMG1VjW/KwScPOJRDyY30b4aUzxfz0g=
github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
github.com/hashicorp/cronexpr v1.1.0 h1:dnNsWtH0V2ReN7JccYe8m//Bj14+PjJDntR1dz0Cixk=
github.com/hashicorp/cronexpr v1.1.0/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=

View File

@@ -7,7 +7,7 @@ import (
"strconv"
"time"
"github.com/gorhill/cronexpr"
"github.com/hashicorp/cronexpr"
)
const (
@@ -648,9 +648,11 @@ func (p *PeriodicConfig) Canonicalize() {
// passed time.
func (p *PeriodicConfig) Next(fromTime time.Time) (time.Time, error) {
if *p.SpecType == PeriodicSpecCron {
if e, err := cronexpr.Parse(*p.Spec); err == nil {
return cronParseNext(e, fromTime, *p.Spec)
e, err := cronexpr.Parse(*p.Spec)
if err != nil {
return time.Time{}, fmt.Errorf("failed parsing cron expression %q: %v", *p.Spec, err)
}
return cronParseNext(e, fromTime, *p.Spec)
}
return time.Time{}, nil
@@ -670,6 +672,7 @@ func cronParseNext(e *cronexpr.Expression, fromTime time.Time, spec string) (t t
return e.Next(fromTime), nil
}
func (p *PeriodicConfig) GetLocation() (*time.Location, error) {
if p.TimeZone == nil || *p.TimeZone == "" {
return time.UTC, nil

View File

@@ -16,6 +16,7 @@ import (
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type MockJobEvalDispatcher struct {
@@ -173,29 +174,22 @@ func TestPeriodicDispatch_Add_UpdateJob(t *testing.T) {
t.Parallel()
p, _ := testPeriodicDispatcher(t)
job := mock.PeriodicJob()
if err := p.Add(job); err != nil {
t.Fatalf("Add failed %v", err)
}
err := p.Add(job)
require.NoError(t, err)
tracked := p.Tracked()
if len(tracked) != 1 {
t.Fatalf("Add didn't track the job: %v", tracked)
}
require.Lenf(t, tracked, 1, "did not track the job")
// Update the job and add it again.
job.Periodic.Spec = "foo"
if err := p.Add(job); err != nil {
t.Fatalf("Add failed %v", err)
}
err = p.Add(job)
require.Error(t, err)
require.Contains(t, err.Error(), "failed parsing cron expression")
tracked = p.Tracked()
if len(tracked) != 1 {
t.Fatalf("Add didn't update: %v", tracked)
}
require.Lenf(t, tracked, 1, "did not update")
if !reflect.DeepEqual(job, tracked[0]) {
t.Fatalf("Add didn't properly update: got %v; want %v", tracked[0], job)
}
require.Equalf(t, job, tracked[0], "add did not properly update")
}
func TestPeriodicDispatch_Add_Remove_Namespaced(t *testing.T) {

View File

@@ -23,7 +23,7 @@ import (
"strings"
"time"
"github.com/gorhill/cronexpr"
"github.com/hashicorp/cronexpr"
"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
@@ -4424,9 +4424,11 @@ func CronParseNext(e *cronexpr.Expression, fromTime time.Time, spec string) (t t
func (p *PeriodicConfig) Next(fromTime time.Time) (time.Time, error) {
switch p.SpecType {
case PeriodicSpecCron:
if e, err := cronexpr.Parse(p.Spec); err == nil {
return CronParseNext(e, fromTime, p.Spec)
e, err := cronexpr.Parse(p.Spec)
if err != nil {
return time.Time{}, fmt.Errorf("failed parsing cron expression: %q: %v", p.Spec, err)
}
return CronParseNext(e, fromTime, p.Spec)
case PeriodicSpecTest:
split := strings.Split(p.Spec, ",")
if len(split) == 1 && split[0] == "" {

View File

@@ -0,0 +1,284 @@
package structs
import (
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPeriodicConfig_DSTChange_Transitions(t *testing.T) {
locName := "America/Los_Angeles"
loc, err := time.LoadLocation(locName)
require.NoError(t, err)
cases := []struct {
name string
pattern string
initTime time.Time
expected []time.Time
}{
{
"normal time",
"0 2 * * * 2019",
time.Date(2019, time.February, 7, 1, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.February, 7, 2, 0, 0, 0, loc),
time.Date(2019, time.February, 8, 2, 0, 0, 0, loc),
time.Date(2019, time.February, 9, 2, 0, 0, 0, loc),
},
},
{
"Spring forward but not in switch time",
"0 4 * * * 2019",
time.Date(2019, time.March, 9, 1, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.March, 9, 4, 0, 0, 0, loc),
time.Date(2019, time.March, 10, 4, 0, 0, 0, loc),
time.Date(2019, time.March, 11, 4, 0, 0, 0, loc),
},
},
{
"Spring forward at a skipped time odd",
"2 2 * * * 2019",
time.Date(2019, time.March, 9, 1, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.March, 9, 2, 2, 0, 0, loc),
// no time in March 10!
time.Date(2019, time.March, 11, 2, 2, 0, 0, loc),
time.Date(2019, time.March, 12, 2, 2, 0, 0, loc),
},
},
{
"Spring forward at a skipped time",
"1 2 * * * 2019",
time.Date(2019, time.March, 9, 1, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.March, 9, 2, 1, 0, 0, loc),
// no time in March 8!
time.Date(2019, time.March, 11, 2, 1, 0, 0, loc),
time.Date(2019, time.March, 12, 2, 1, 0, 0, loc),
},
},
{
"Spring forward at a skipped time boundary",
"0 2 * * * 2019",
time.Date(2019, time.March, 9, 1, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.March, 9, 2, 0, 0, 0, loc),
// no time in March 8!
time.Date(2019, time.March, 11, 2, 0, 0, 0, loc),
time.Date(2019, time.March, 12, 2, 0, 0, 0, loc),
},
},
{
"Spring forward at a boundary of repeating time",
"0 1 * * * 2019",
time.Date(2019, time.March, 9, 0, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.March, 9, 1, 0, 0, 0, loc),
time.Date(2019, time.March, 10, 0, 0, 0, 0, loc).Add(1 * time.Hour),
time.Date(2019, time.March, 11, 1, 0, 0, 0, loc),
time.Date(2019, time.March, 12, 1, 0, 0, 0, loc),
},
},
{
"Fall back: before transition",
"30 0 * * * 2019",
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.November, 3, 0, 30, 0, 0, loc),
time.Date(2019, time.November, 4, 0, 30, 0, 0, loc),
time.Date(2019, time.November, 5, 0, 30, 0, 0, loc),
time.Date(2019, time.November, 6, 0, 30, 0, 0, loc),
},
},
{
"Fall back: after transition",
"30 3 * * * 2019",
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.November, 3, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 4, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 5, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 6, 3, 30, 0, 0, loc),
},
},
{
"Fall back: after transition starting in repeated span before",
"30 3 * * * 2019",
time.Date(2019, time.November, 3, 0, 10, 0, 0, loc).Add(1 * time.Hour),
[]time.Time{
time.Date(2019, time.November, 3, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 4, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 5, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 6, 3, 30, 0, 0, loc),
},
},
{
"Fall back: after transition starting in repeated span after",
"30 3 * * * 2019",
time.Date(2019, time.November, 3, 0, 10, 0, 0, loc).Add(2 * time.Hour),
[]time.Time{
time.Date(2019, time.November, 3, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 4, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 5, 3, 30, 0, 0, loc),
time.Date(2019, time.November, 6, 3, 30, 0, 0, loc),
},
},
{
"Fall back: in repeated region",
"30 1 * * * 2019",
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(1 * time.Hour),
time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(2 * time.Hour),
time.Date(2019, time.November, 4, 1, 30, 0, 0, loc),
time.Date(2019, time.November, 5, 1, 30, 0, 0, loc),
time.Date(2019, time.November, 6, 1, 30, 0, 0, loc),
},
},
{
"Fall back: in repeated region boundary",
"0 1 * * * 2019",
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc).Add(1 * time.Hour),
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc).Add(2 * time.Hour),
time.Date(2019, time.November, 4, 1, 0, 0, 0, loc),
time.Date(2019, time.November, 5, 1, 0, 0, 0, loc),
time.Date(2019, time.November, 6, 1, 0, 0, 0, loc),
},
},
{
"Fall back: in repeated region boundary 2",
"0 2 * * * 2019",
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc).Add(3 * time.Hour),
time.Date(2019, time.November, 4, 2, 0, 0, 0, loc),
time.Date(2019, time.November, 5, 2, 0, 0, 0, loc),
time.Date(2019, time.November, 6, 2, 0, 0, 0, loc),
},
},
{
"Fall back: in repeated region, starting from within region",
"30 1 * * * 2019",
time.Date(2019, time.November, 3, 0, 40, 0, 0, loc).Add(1 * time.Hour),
[]time.Time{
time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(2 * time.Hour),
time.Date(2019, time.November, 4, 1, 30, 0, 0, loc),
time.Date(2019, time.November, 5, 1, 30, 0, 0, loc),
time.Date(2019, time.November, 6, 1, 30, 0, 0, loc),
},
},
{
"Fall back: in repeated region, starting from within region 2",
"30 1 * * * 2019",
time.Date(2019, time.November, 3, 0, 40, 0, 0, loc).Add(2 * time.Hour),
[]time.Time{
time.Date(2019, time.November, 4, 1, 30, 0, 0, loc),
time.Date(2019, time.November, 5, 1, 30, 0, 0, loc),
time.Date(2019, time.November, 6, 1, 30, 0, 0, loc),
},
},
{
"Fall back: wildcard",
"30 * * * * 2019",
time.Date(2019, time.November, 3, 0, 0, 0, 0, loc),
[]time.Time{
time.Date(2019, time.November, 3, 0, 30, 0, 0, loc),
time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(1 * time.Hour),
time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(2 * time.Hour),
time.Date(2019, time.November, 3, 2, 30, 0, 0, loc),
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
p := &PeriodicConfig{
Enabled: true,
SpecType: PeriodicSpecCron,
Spec: c.pattern,
TimeZone: locName,
}
p.Canonicalize()
starting := c.initTime
for _, next := range c.expected {
n, err := p.Next(starting)
assert.NoError(t, err)
assert.Equalf(t, next, n, "next time of %v", starting)
starting = next
}
})
}
}
func TestPeriodConfig_DSTSprintForward_Property(t *testing.T) {
locName := "America/Los_Angeles"
loc, err := time.LoadLocation(locName)
require.NoError(t, err)
cronExprs := []string{
"* * * * *",
"0 2 * * *",
"* 1 * * *",
}
times := []time.Time{
// spring forward
time.Date(2019, time.March, 11, 0, 0, 0, 0, loc),
time.Date(2019, time.March, 10, 0, 0, 0, 0, loc),
time.Date(2019, time.March, 11, 0, 0, 0, 0, loc),
// leap backwards
time.Date(2019, time.November, 4, 0, 0, 0, 0, loc),
time.Date(2019, time.November, 5, 0, 0, 0, 0, loc),
time.Date(2019, time.November, 6, 0, 0, 0, 0, loc),
}
testSpan := 4 * time.Hour
testCase := func(t *testing.T, cronExpr string, init time.Time) {
p := &PeriodicConfig{
Enabled: true,
SpecType: PeriodicSpecCron,
Spec: cronExpr,
TimeZone: "America/Los_Angeles",
}
p.Canonicalize()
lastNext := init
for start := init; start.Before(init.Add(testSpan)); start = start.Add(1 * time.Minute) {
next, err := p.Next(start)
require.NoError(t, err)
require.Truef(t, next.After(start),
"next(%v) = %v is not after init time", start, next)
if start.Before(lastNext) {
require.Equalf(t, lastNext, next, "next(%v) = %v is earlier than previously known next %v",
start, next, lastNext)
}
if strings.HasPrefix(cronExpr, "* * ") {
require.Equalf(t, next.Sub(start), 1*time.Minute,
"next(%v) = %v is the next minute", start, next)
}
lastNext = next
}
}
for _, cron := range cronExprs {
for _, startTime := range times {
t.Run(fmt.Sprintf("%v: %v", cron, startTime), func(t *testing.T) {
testCase(t, cron, startTime)
})
}
}
}

View File

@@ -2903,45 +2903,42 @@ func TestPeriodicConfig_ValidCron(t *testing.T) {
}
func TestPeriodicConfig_NextCron(t *testing.T) {
require := require.New(t)
type testExpectation struct {
Time time.Time
HasError bool
ErrorMsg string
}
from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC)
specs := []string{"0 0 29 2 * 1980",
"*/5 * * * *",
"1 15-0 * * 1-5"}
expected := []*testExpectation{
cases := []struct {
spec string
nextTime time.Time
errorMsg string
}{
{
Time: time.Time{},
HasError: false,
spec: "0 0 29 2 * 1980",
nextTime: time.Time{},
},
{
Time: time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC),
HasError: false,
spec: "*/5 * * * *",
nextTime: time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC),
},
{
Time: time.Time{},
HasError: true,
ErrorMsg: "failed parsing cron expression",
spec: "1 15-0 *",
nextTime: time.Time{},
errorMsg: "failed parsing cron expression",
},
}
for i, spec := range specs {
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
p.Canonicalize()
n, err := p.Next(from)
nextExpected := expected[i]
for i, c := range cases {
t.Run(fmt.Sprintf("case: %d: %s", i, c.spec), func(t *testing.T) {
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: c.spec}
p.Canonicalize()
n, err := p.Next(from)
require.Equal(nextExpected.Time, n)
require.Equal(err != nil, nextExpected.HasError)
if err != nil {
require.True(strings.Contains(err.Error(), nextExpected.ErrorMsg))
}
require.Equal(t, c.nextTime, n)
if c.errorMsg == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errorMsg)
}
})
}
}
@@ -2963,7 +2960,7 @@ func TestPeriodicConfig_DST(t *testing.T) {
p := &PeriodicConfig{
Enabled: true,
SpecType: PeriodicSpecCron,
Spec: "0 2 11-12 3 * 2017",
Spec: "0 2 11-13 3 * 2017",
TimeZone: "America/Los_Angeles",
}
p.Canonicalize()
@@ -2973,7 +2970,7 @@ func TestPeriodicConfig_DST(t *testing.T) {
// E1 is an 8 hour adjustment, E2 is a 7 hour adjustment
e1 := time.Date(2017, time.March, 11, 10, 0, 0, 0, time.UTC)
e2 := time.Date(2017, time.March, 12, 9, 0, 0, 0, time.UTC)
e2 := time.Date(2017, time.March, 13, 9, 0, 0, 0, time.UTC)
n1, err := p.Next(t1)
require.Nil(err)

View File

@@ -1,6 +1,8 @@
/*!
* Copyright 2013 Raymond Hill
*
* Modifications 2020 - HashiCorp
*
* Project: github.com/gorhill/cronexpr
* File: cronexpr.go
* Version: 1.0
@@ -160,78 +162,122 @@ func (expr *Expression) Next(fromTime time.Time) time.Time {
return fromTime
}
// Since expr.nextSecond()-expr.nextMonth() expects that the
// supplied time stamp is a perfect match to the underlying cron
// expression, and since this function is an entry point where `fromTime`
// does not necessarily matches the underlying cron expression,
// we first need to ensure supplied time stamp matches
// the cron expression. If not, this means the supplied time
// stamp falls in between matching time stamps, thus we move
// to closest future matching immediately upon encountering a mismatching
// time stamp.
loc := fromTime.Location()
t := fromTime.Add(time.Second - time.Duration(fromTime.Nanosecond())*time.Nanosecond)
// year
v := fromTime.Year()
i := sort.SearchInts(expr.yearList, v)
if i == len(expr.yearList) {
WRAP:
// let's find the next date that satisfies condition
v := t.Year()
if i := sort.SearchInts(expr.yearList, v); i == len(expr.yearList) {
return time.Time{}
}
if v != expr.yearList[i] {
return expr.nextYear(fromTime)
}
// month
v = int(fromTime.Month())
i = sort.SearchInts(expr.monthList, v)
if i == len(expr.monthList) {
return expr.nextYear(fromTime)
}
if v != expr.monthList[i] {
return expr.nextMonth(fromTime)
} else if v != expr.yearList[i] {
t = time.Date(expr.yearList[i], time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc)
}
expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(fromTime.Year(), int(fromTime.Month()))
v = int(t.Month())
if i := sort.SearchInts(expr.monthList, v); i == len(expr.monthList) {
// try again with a new year
t = time.Date(t.Year()+1, time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc)
goto WRAP
} else if v != expr.monthList[i] {
t = time.Date(t.Year(), time.Month(expr.monthList[i]), 1, 0, 0, 0, 0, loc)
}
expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), int(t.Month()))
if len(expr.actualDaysOfMonthList) == 0 {
return expr.nextMonth(fromTime)
t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc)
goto WRAP
}
// day of month
v = fromTime.Day()
i = sort.SearchInts(expr.actualDaysOfMonthList, v)
if i == len(expr.actualDaysOfMonthList) {
return expr.nextMonth(fromTime)
}
if v != expr.actualDaysOfMonthList[i] {
return expr.nextDayOfMonth(fromTime)
}
// hour
v = fromTime.Hour()
i = sort.SearchInts(expr.hourList, v)
if i == len(expr.hourList) {
return expr.nextDayOfMonth(fromTime)
}
if v != expr.hourList[i] {
return expr.nextHour(fromTime)
}
// minute
v = fromTime.Minute()
i = sort.SearchInts(expr.minuteList, v)
if i == len(expr.minuteList) {
return expr.nextHour(fromTime)
}
if v != expr.minuteList[i] {
return expr.nextMinute(fromTime)
}
// second
v = fromTime.Second()
i = sort.SearchInts(expr.secondList, v)
if i == len(expr.secondList) {
return expr.nextMinute(fromTime)
v = t.Day()
if i := sort.SearchInts(expr.actualDaysOfMonthList, v); i == len(expr.actualDaysOfMonthList) {
t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc)
goto WRAP
} else if v != expr.actualDaysOfMonthList[i] {
t = time.Date(t.Year(), t.Month(), expr.actualDaysOfMonthList[i], 0, 0, 0, 0, loc)
// in San Palo, before 2019, there may be no midnight (or multiple midnights)
// due to DST
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 we reach this point, there is nothing better to do
// than to move to the next second
if timeZoneInDay(t) {
goto SLOW_CLOCK
}
return expr.nextSecond(fromTime)
// Fast path where hours/minutes behave as expected trivially
v = t.Hour()
if i := sort.SearchInts(expr.hourList, v); i == len(expr.hourList) {
t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, loc)
goto WRAP
} else if v != expr.hourList[i] {
t = time.Date(t.Year(), t.Month(), t.Day(), expr.hourList[i], expr.minuteList[0], expr.secondList[0], 0, loc)
}
v = t.Minute()
if i := sort.SearchInts(expr.minuteList, v); i == len(expr.minuteList) {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, 0, 0, 0, loc)
goto WRAP
} else if v != expr.minuteList[i] {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), expr.minuteList[i], expr.secondList[0], 0, loc)
}
v = t.Second()
if i := sort.SearchInts(expr.secondList, v); i == len(expr.secondList) {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()+1, 0, 0, loc)
goto WRAP
} else if v != expr.secondList[i] {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), expr.secondList[i], 0, loc)
}
return t
SLOW_CLOCK:
// daylight saving effect is here, where odd things happen:
// An hour may have 60 minutes, 30 minutes or 90 minutes;
// partial hours may "repeat"!
for !sortContains(expr.hourList, t.Hour()) {
hourBefore := t.Hour()
t = t.Add(time.Hour)
if hourBefore == t.Hour() {
t = t.Add(time.Hour)
}
t = t.Truncate(time.Minute)
if t.Minute() != 0 {
t = t.Add(-1 * time.Minute * time.Duration(t.Minute()))
}
if t.Hour() == 0 {
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
goto WRAP
}
}
for !sortContains(expr.minuteList, t.Minute()) {
hoursBefore := t.Hour()
t = t.Truncate(time.Minute).Add(time.Minute)
if hoursBefore != t.Hour() {
goto WRAP
}
}
v = t.Second()
t = t.Truncate(time.Minute)
if i := sort.SearchInts(expr.secondList, v); i == len(expr.secondList) {
t = t.Add(time.Minute)
goto WRAP
} else {
t = t.Add(time.Duration(expr.secondList[i]) * time.Second)
}
return t
}
/******************************************************************************/
@@ -259,7 +305,7 @@ func (expr *Expression) NextN(fromTime time.Time, n uint) []time.Time {
if n == 0 {
break
}
fromTime = expr.nextSecond(fromTime)
fromTime = expr.Next(fromTime)
}
}
return nextTimes

View File

@@ -1,6 +1,8 @@
/*!
* Copyright 2013 Raymond Hill
*
* Modifications 2020 - HashiCorp
*
* Project: github.com/gorhill/cronexpr
* File: cronexpr_next.go
* Version: 1.0
@@ -33,160 +35,6 @@ var dowNormalizedOffsets = [][]int{
/******************************************************************************/
func (expr *Expression) nextYear(t time.Time) time.Time {
// Find index at which item in list is greater or equal to
// candidate year
i := sort.SearchInts(expr.yearList, t.Year()+1)
if i == len(expr.yearList) {
return time.Time{}
}
// Year changed, need to recalculate actual days of month
expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(expr.yearList[i], expr.monthList[0])
if len(expr.actualDaysOfMonthList) == 0 {
return expr.nextMonth(time.Date(
expr.yearList[i],
time.Month(expr.monthList[0]),
1,
expr.hourList[0],
expr.minuteList[0],
expr.secondList[0],
0,
t.Location()))
}
return time.Date(
expr.yearList[i],
time.Month(expr.monthList[0]),
expr.actualDaysOfMonthList[0],
expr.hourList[0],
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
}
/******************************************************************************/
func (expr *Expression) nextMonth(t time.Time) time.Time {
// Find index at which item in list is greater or equal to
// candidate month
i := sort.SearchInts(expr.monthList, int(t.Month())+1)
if i == len(expr.monthList) {
return expr.nextYear(t)
}
// Month changed, need to recalculate actual days of month
expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), expr.monthList[i])
if len(expr.actualDaysOfMonthList) == 0 {
return expr.nextMonth(time.Date(
t.Year(),
time.Month(expr.monthList[i]),
1,
expr.hourList[0],
expr.minuteList[0],
expr.secondList[0],
0,
t.Location()))
}
return time.Date(
t.Year(),
time.Month(expr.monthList[i]),
expr.actualDaysOfMonthList[0],
expr.hourList[0],
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
}
/******************************************************************************/
func (expr *Expression) nextDayOfMonth(t time.Time) time.Time {
// Find index at which item in list is greater or equal to
// candidate day of month
i := sort.SearchInts(expr.actualDaysOfMonthList, t.Day()+1)
if i == len(expr.actualDaysOfMonthList) {
return expr.nextMonth(t)
}
return time.Date(
t.Year(),
t.Month(),
expr.actualDaysOfMonthList[i],
expr.hourList[0],
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
}
/******************************************************************************/
func (expr *Expression) nextHour(t time.Time) time.Time {
// Find index at which item in list is greater or equal to
// candidate hour
i := sort.SearchInts(expr.hourList, t.Hour()+1)
if i == len(expr.hourList) {
return expr.nextDayOfMonth(t)
}
return time.Date(
t.Year(),
t.Month(),
t.Day(),
expr.hourList[i],
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
}
/******************************************************************************/
func (expr *Expression) nextMinute(t time.Time) time.Time {
// Find index at which item in list is greater or equal to
// candidate minute
i := sort.SearchInts(expr.minuteList, t.Minute()+1)
if i == len(expr.minuteList) {
return expr.nextHour(t)
}
return time.Date(
t.Year(),
t.Month(),
t.Day(),
t.Hour(),
expr.minuteList[i],
expr.secondList[0],
0,
t.Location())
}
/******************************************************************************/
func (expr *Expression) nextSecond(t time.Time) time.Time {
// nextSecond() assumes all other fields are exactly matched
// to the cron expression
// Find index at which item in list is greater or equal to
// candidate second
i := sort.SearchInts(expr.secondList, t.Second()+1)
if i == len(expr.secondList) {
return expr.nextMinute(t)
}
return time.Date(
t.Year(),
t.Month(),
t.Day(),
t.Hour(),
t.Minute(),
expr.secondList[i],
0,
t.Location())
}
/******************************************************************************/
func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int {
actualDaysOfMonthMap := make(map[int]bool)
firstDayOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
@@ -290,3 +138,18 @@ func workdayOfMonth(targetDom, lastDom time.Time) int {
}
return dom
}
func sortContains(a []int, x int) bool {
i := sort.SearchInts(a, x)
return i < len(a) && a[i] == x
}
func timeZoneInDay(t time.Time) bool {
if t.Location() == time.UTC {
return false
}
_, off := t.AddDate(0, 0, -1).Zone()
_, ndoff := t.AddDate(0, 0, 1).Zone()
return off != ndoff
}

View File

@@ -1,6 +1,8 @@
/*!
* Copyright 2013 Raymond Hill
*
* Modifications 2020 - HashiCorp
*
* Project: github.com/gorhill/cronexpr
* File: cronexpr_parse.go
* Version: 1.0
@@ -319,6 +321,10 @@ func (expr *Expression) dowFieldHandler(s string) error {
case one:
populateOne(expr.daysOfWeek, directive.first)
case span:
// To properly handle spans that end in 7 (Sunday)
if directive.last == 0 {
directive.last = 6
}
populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step)
case all:
populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step)

2
vendor/vendor.json vendored
View File

@@ -183,7 +183,6 @@
{"path":"github.com/google/go-cmp/cmp/internal/function","checksumSHA1":"kYtvRhMjM0X4bvEjR3pqEHLw1qo=","revision":"d5735f74713c51f7450a43d0a98d41ce2c1db3cb","revisionTime":"2017-09-01T21:42:48Z"},
{"path":"github.com/google/go-cmp/cmp/internal/value","checksumSHA1":"f+mgZLvc4VITtMmBv0bmew4rL2Y=","revision":"d5735f74713c51f7450a43d0a98d41ce2c1db3cb","revisionTime":"2017-09-01T21:42:48Z"},
{"path":"github.com/googleapis/gax-go/v2","checksumSHA1":"WZoHSeTnVjnPIX2+U1Otst5MUKw=","revision":"bd5b16380fd03dc758d11cef74ba2e3bc8b0e8c2","revisionTime":"2019-05-13T18:38:25Z"},
{"path":"github.com/gorhill/cronexpr","checksumSHA1":"m8B3L3qJ3tFfP6BI9pIFr9oal3w=","comment":"1.0.0","origin":"github.com/dadgar/cronexpr","revision":"675cac9b2d182dccb5ba8d5f8a0d5988df8a4394","revisionTime":"2017-09-15T18:30:32Z"},
{"path":"github.com/gorilla/context","checksumSHA1":"g/V4qrXjUGG9B+e3hB+4NAYJ5Gs=","revision":"08b5f424b9271eedf6f9f0ce86cb9396ed337a42","revisionTime":"2016-08-17T18:46:32Z"},
{"path":"github.com/gorilla/mux","checksumSHA1":"STQSdSj2FcpCf0NLfdsKhNutQT0=","revision":"e48e440e4c92e3251d812f8ce7858944dfa3331c","revisionTime":"2018-08-07T07:52:56Z"},
{"path":"github.com/gorilla/websocket","checksumSHA1":"gr0edNJuVv4+olNNZl5ZmwLgscA=","revision":"0ec3d1bd7fe50c503d6df98ee649d81f4857c564","revisionTime":"2019-03-06T00:42:57Z"},
@@ -210,6 +209,7 @@
{"path":"github.com/hashicorp/consul/sdk/testutil","checksumSHA1":"BdbalXv3cKiFTZpRCy4fgIzHBEU=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"},
{"path":"github.com/hashicorp/consul/sdk/testutil/retry","checksumSHA1":"d3PJhffDKar25kzK0iEqssVMkck=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"},
{"path":"github.com/hashicorp/consul/version","checksumSHA1":"fRbV3oycM2uY4oOkDoSXtP4o6Tc=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"},
{"path":"github.com/hashicorp/cronexpr","checksumSHA1":"lKpw2wPcDeImWS2fPAzfSJl+Gcc=","revision":"d968249ea977a46db0f3588b5c5e438243cca0cb","revisionTime":"2020-05-08T15:08:16Z","version":"v1.1.0","versionExact":"v1.1.0"},
{"path":"github.com/hashicorp/errwrap","checksumSHA1":"cdOCt0Yb+hdErz8NAQqayxPmRsY=","revision":"7554cd9344cec97297fa6649b055a8c98c2a1e55"},
{"path":"github.com/hashicorp/go-checkpoint","checksumSHA1":"D267IUMW2rcb+vNe3QU+xhfSrgY=","revision":"1545e56e46dec3bba264e41fde2c1e2aa65b5dd4","revisionTime":"2017-10-09T17:35:28Z"},
{"path":"github.com/hashicorp/go-cleanhttp","checksumSHA1":"6ihdHMkDfFx/rJ1A36com2F6bQk=","revision":"a45970658e51fea2c41445ff0f7e07106d007617","revisionTime":"2017-02-11T00:33:01Z"},