mirrored from https://chromium.googlesource.com/infra/luci/luci-go
/
schedule.go
166 lines (151 loc) · 5.28 KB
/
schedule.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package schedule
import (
"errors"
"fmt"
"math/rand"
"strings"
"sync"
"time"
"github.com/gorhill/cronexpr"
)
// DistantFuture is Jan 2116. It is used to indicate that next tick should not
// happen.
var DistantFuture = time.Unix(4604952467, 0).UTC()
// cronexpr library is using global variables without synchronizing the access.
var cronexprLock sync.Mutex
// Schedule knows when to run a periodic job (given current time and possibly
// a current state of the job).
//
// See 'Parse' for a list of supported kinds of schedules.
type Schedule struct {
asString string
randSeed uint64
cronExpr *cronexpr.Expression // set for absolute schedules
interval time.Duration // set for relative schedules
triggered bool // set for triggered schedule
}
// IsAbsolute is true for schedules that do not depend on a job state.
//
// Absolute schedules are basically static time tables specifying when to
// attempt to run a job.
//
// Non-absolute (aka relative) schedules may use job state transition times to
// make decisions.
//
// See comment for 'Parse' for some examples.
func (s *Schedule) IsAbsolute() bool {
return s.cronExpr != nil || s.triggered
}
// Next tells when to run the job the next time.
//
// 'now' is current time. 'prev' is when previous invocation has finished (or
// zero time for first invocation).
func (s *Schedule) Next(now, prev time.Time) time.Time {
if s.triggered {
return DistantFuture
}
// For an absolute schedule just look at the time table.
if s.cronExpr != nil {
return s.cronExpr.Next(now)
}
// Using relative schedule and this is a first invocation ever? Randomize
// start time, so that a bunch of newly registered cron jobs do not start all
// at once. Otherwise just wait for 'interval' seconds after previous
// invocation.
if prev.IsZero() {
// Pass seed through math/rand to make small seeds (used by unit tests),
// less special.
rnd := rand.New(rand.NewSource(int64(s.randSeed))).Float64()
return now.Add(time.Duration(float64(s.interval) * rnd))
}
next := prev.Add(s.interval)
if next.Sub(now) < 0 {
next = now
}
return next
}
// String serializes the schedule to a human readable string.
//
// It can be passed to Parse to get back the schedule.
func (s *Schedule) String() string {
return s.asString
}
// Parse converts human readable definition of a schedule to *Schedule object.
//
// Supported kinds of schedules (illustrated by examples):
// - "* 0 * * * *": standard cron-like expression. Cron engine will attempt
// to start a job at specified moments in time (based on UTC clock). If when
// triggering a job, previous invocation is still running, an overrun will
// be recorded (and next attempt to start a job happens based on the
// schedule, not when the previous invocation finishes). This is absolute
// schedule (i.e. doesn't depend on job state).
// - "with 10s interval": runs invocations in a loop, waiting 10s after
// finishing invocation before starting a new one. This is relative
// schedule. Overruns are not possible.
// - "continuously" is alias for "with 0s interval", meaning the job will run
// in a loop without any pauses.
// - "triggered" schedule indicates that job is always started via a trigger.
// 'Next' always returns DistantFuture constant.
func Parse(expr string, randSeed uint64) (sched *Schedule, err error) {
toParse := ""
switch expr {
case "triggered":
return &Schedule{
asString: "triggered",
randSeed: randSeed,
triggered: true,
}, nil
case "continuously":
toParse = "with 0s interval"
default:
toParse = expr
}
if strings.HasPrefix(toParse, "with ") {
sched, err = parseWithSchedule(toParse, randSeed)
} else {
sched, err = parseCronSchedule(toParse, randSeed)
}
if sched != nil {
sched.asString = expr
sched.randSeed = randSeed
}
return sched, err
}
// parseWithSchedule parses "with <interval> interval" schedule string.
func parseWithSchedule(expr string, randSeed uint64) (*Schedule, error) {
tokens := strings.SplitN(expr, " ", 3)
if len(tokens) != 3 || tokens[0] != "with" || tokens[2] != "interval" {
return nil, errors.New("expecting format \"with <duration> interval\"")
}
interval, err := time.ParseDuration(tokens[1])
if err != nil {
return nil, fmt.Errorf("bad duration %q - %s", tokens[1], err)
}
if interval < 0 {
return nil, fmt.Errorf("bad interval %q - it must be positive", tokens[1])
}
return &Schedule{interval: interval}, nil
}
// parseCronSchedule parses crontab-like schedule string.
func parseCronSchedule(expr string, randSeed uint64) (*Schedule, error) {
cronexprLock.Lock()
exp, err := cronexpr.Parse(expr)
cronexprLock.Unlock()
if err != nil {
return nil, err
}
return &Schedule{cronExpr: exp}, nil
}