mirror of
https://github.com/kemko/nomad.git
synced 2026-01-04 01:15:43 +03:00
Merge pull request #7894 from hashicorp/b-cronexpr-dst-fix
Fix Daylight saving transition handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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] == "" {
|
||||
|
||||
284
nomad/structs/structs_periodic_test.go
Normal file
284
nomad/structs/structs_periodic_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
2
vendor/vendor.json
vendored
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user