Skip to content

Commit 9f6c956

Browse files
committed
Merge branch 'master' of https://github.com/robfig/cron
; Conflicts: ; cron.go
2 parents c803a08 + 2315d57 commit 9f6c956

File tree

4 files changed

+152
-67
lines changed

4 files changed

+152
-67
lines changed

cron.go

+37-31
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ func (c *Cron) runWithRecovery(j Job) {
171171
j.Run()
172172
}
173173

174-
// Run the scheduler.. this is private just due to the need to synchronize
174+
// Run the scheduler. this is private just due to the need to synchronize
175175
// access to the 'running' state variable.
176176
func (c *Cron) run() {
177177
// Figure out the next activation times for each entry.
178-
now := time.Now().In(c.location)
178+
now := c.now()
179179
for _, entry := range c.entries {
180180
entry.Next = entry.Schedule.Next(now)
181181
}
@@ -184,46 +184,47 @@ func (c *Cron) run() {
184184
// Determine the next entry to run.
185185
sort.Sort(byTime(c.entries))
186186

187-
var effective time.Time
187+
var timer *time.Timer
188188
if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
189189
// If there are no entries yet, just sleep - it still handles new entries
190190
// and stop requests.
191-
effective = now.AddDate(10, 0, 0)
191+
timer = time.NewTimer(100000 * time.Hour)
192192
} else {
193-
effective = c.entries[0].Next
193+
timer = time.NewTimer(c.entries[0].Next.Sub(now))
194194
}
195195

196-
timer := time.NewTimer(effective.Sub(now))
197-
select {
198-
case now = <-timer.C:
199-
now = now.In(c.location)
200-
// Run every entry whose next time was this effective time.
201-
for _, e := range c.entries {
202-
if e.Next != effective {
203-
break
196+
for {
197+
select {
198+
case now = <-timer.C:
199+
now = now.In(c.location)
200+
// Run every entry whose next time was less than now
201+
for _, e := range c.entries {
202+
if e.Next.After(now) || e.Next.IsZero() {
203+
break
204+
}
205+
go c.runWithRecovery(e.Job)
206+
e.ExecTimes++
207+
e.Prev = e.Next
208+
e.Next = e.Schedule.Next(now)
204209
}
205-
go c.runWithRecovery(e.Job)
206-
e.ExecTimes++
207-
e.Prev = e.Next
208-
e.Next = e.Schedule.Next(now)
209-
}
210-
continue
211210

212-
case newEntry := <-c.add:
213-
c.entries = append(c.entries, newEntry)
214-
newEntry.Next = newEntry.Schedule.Next(time.Now().In(c.location))
211+
case newEntry := <-c.add:
212+
timer.Stop()
213+
now = c.now()
214+
newEntry.Next = newEntry.Schedule.Next(now)
215+
c.entries = append(c.entries, newEntry)
215216

216-
case <-c.snapshot:
217-
c.snapshot <- c.entrySnapshot()
217+
case <-c.snapshot:
218+
c.snapshot <- c.entrySnapshot()
219+
continue
218220

219-
case <-c.stop:
220-
timer.Stop()
221-
return
222-
}
221+
case <-c.stop:
222+
timer.Stop()
223+
return
224+
}
223225

224-
// 'now' should be updated after newEntry and snapshot cases.
225-
now = time.Now().In(c.location)
226-
timer.Stop()
226+
break
227+
}
227228
}
228229
}
229230

@@ -261,3 +262,8 @@ func (c *Cron) entrySnapshot() []*Entry {
261262
}
262263
return entries
263264
}
265+
266+
// now returns current time in c location
267+
func (c *Cron) now() time.Time {
268+
return time.Now().In(c.location)
269+
}

cron_test.go

+110-31
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
// Many tests schedule a job for every second, and then wait at most a second
1111
// for it to run. This amount is just slightly larger than 1 second to
1212
// compensate for a few milliseconds of runtime.
13-
const ONE_SECOND = 1*time.Second + 10*time.Millisecond
13+
const OneSecond = 1*time.Second + 10*time.Millisecond
1414

1515
func TestFuncPanicRecovery(t *testing.T) {
1616
cron := New()
@@ -19,7 +19,7 @@ func TestFuncPanicRecovery(t *testing.T) {
1919
cron.AddFunc("", "* * * * * ?", func() { panic("YOLO") })
2020

2121
select {
22-
case <-time.After(ONE_SECOND):
22+
case <-time.After(OneSecond):
2323
return
2424
}
2525
}
@@ -39,7 +39,7 @@ func TestJobPanicRecovery(t *testing.T) {
3939
cron.AddJob("", "* * * * * ?", job)
4040

4141
select {
42-
case <-time.After(ONE_SECOND):
42+
case <-time.After(OneSecond):
4343
return
4444
}
4545
}
@@ -50,8 +50,8 @@ func TestNoEntries(t *testing.T) {
5050
cron.Start()
5151

5252
select {
53-
case <-time.After(ONE_SECOND):
54-
t.FailNow()
53+
case <-time.After(OneSecond):
54+
t.Fatal("expected cron will be stopped immediately")
5555
case <-stop(cron):
5656
}
5757
}
@@ -67,10 +67,10 @@ func TestStopCausesJobsToNotRun(t *testing.T) {
6767
cron.AddFunc("", "* * * * * ?", func() { wg.Done() })
6868

6969
select {
70-
case <-time.After(ONE_SECOND):
70+
case <-time.After(OneSecond):
7171
// No job ran!
7272
case <-wait(wg):
73-
t.FailNow()
73+
t.Fatal("expected stopped cron does not run any job")
7474
}
7575
}
7676

@@ -86,8 +86,8 @@ func TestAddBeforeRunning(t *testing.T) {
8686

8787
// Give cron 2 seconds to run our job (which is always activated).
8888
select {
89-
case <-time.After(ONE_SECOND):
90-
t.FailNow()
89+
case <-time.After(OneSecond):
90+
t.Fatal("expected job runs")
9191
case <-wait(wg):
9292
}
9393
}
@@ -103,8 +103,8 @@ func TestAddWhileRunning(t *testing.T) {
103103
cron.AddFunc("", "* * * * * ?", func() { wg.Done() })
104104

105105
select {
106-
case <-time.After(ONE_SECOND):
107-
t.FailNow()
106+
case <-time.After(OneSecond):
107+
t.Fatal("expected job runs")
108108
case <-wait(wg):
109109
}
110110
}
@@ -118,10 +118,9 @@ func TestAddWhileRunningWithDelay(t *testing.T) {
118118
var calls = 0
119119
cron.AddFunc("", "* * * * * *", func() { calls += 1 })
120120

121-
<-time.After(ONE_SECOND)
121+
<-time.After(OneSecond)
122122
if calls != 1 {
123-
fmt.Printf("called %d times, expected 1\n", calls)
124-
t.Fail()
123+
t.Errorf("called %d times, expected 1\n", calls)
125124
}
126125
}
127126

@@ -137,14 +136,14 @@ func TestSnapshotEntries(t *testing.T) {
137136

138137
// Cron should fire in 2 seconds. After 1 second, call Entries.
139138
select {
140-
case <-time.After(ONE_SECOND):
139+
case <-time.After(OneSecond):
141140
cron.Entries()
142141
}
143142

144143
// Even though Entries was called, the cron should fire at the 2 second mark.
145144
select {
146-
case <-time.After(ONE_SECOND):
147-
t.FailNow()
145+
case <-time.After(OneSecond):
146+
t.Error("expected job runs at 2 second mark")
148147
case <-wait(wg):
149148
}
150149

@@ -168,8 +167,8 @@ func TestMultipleEntries(t *testing.T) {
168167
defer cron.Stop()
169168

170169
select {
171-
case <-time.After(ONE_SECOND):
172-
t.FailNow()
170+
case <-time.After(OneSecond):
171+
t.Error("expected job run in proper order")
173172
case <-wait(wg):
174173
}
175174
}
@@ -188,8 +187,8 @@ func TestRunningJobTwice(t *testing.T) {
188187
defer cron.Stop()
189188

190189
select {
191-
case <-time.After(2 * ONE_SECOND):
192-
t.FailNow()
190+
case <-time.After(2 * OneSecond):
191+
t.Error("expected job fires 2 times")
193192
case <-wait(wg):
194193
}
195194
}
@@ -210,8 +209,8 @@ func TestRunningMultipleSchedules(t *testing.T) {
210209
defer cron.Stop()
211210

212211
select {
213-
case <-time.After(2 * ONE_SECOND):
214-
t.FailNow()
212+
case <-time.After(2 * OneSecond):
213+
t.Error("expected job fires 2 times")
215214
case <-wait(wg):
216215
}
217216
}
@@ -221,7 +220,7 @@ func TestLocalTimezone(t *testing.T) {
221220
wg := &sync.WaitGroup{}
222221
wg.Add(2)
223222

224-
now := time.Now().Local()
223+
now := time.Now()
225224
spec := fmt.Sprintf("%d,%d %d %d %d %d ?",
226225
now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())
227226

@@ -231,8 +230,8 @@ func TestLocalTimezone(t *testing.T) {
231230
defer cron.Stop()
232231

233232
select {
234-
case <-time.After(ONE_SECOND * 2):
235-
t.FailNow()
233+
case <-time.After(OneSecond * 2):
234+
t.Error("expected job fires 2 times")
236235
case <-wait(wg):
237236
}
238237
}
@@ -258,8 +257,8 @@ func TestNonLocalTimezone(t *testing.T) {
258257
defer cron.Stop()
259258

260259
select {
261-
case <-time.After(ONE_SECOND * 2):
262-
t.FailNow()
260+
case <-time.After(OneSecond * 2):
261+
t.Error("expected job fires 2 times")
263262
case <-wait(wg):
264263
}
265264
}
@@ -280,6 +279,67 @@ func (t testJob) Run() {
280279
t.wg.Done()
281280
}
282281

282+
// Test that adding an invalid job spec returns an error
283+
func TestInvalidJobSpec(t *testing.T) {
284+
cron := New()
285+
_, err := cron.AddJob("", "this will not parse", nil)
286+
if err == nil {
287+
t.Errorf("expected an error with invalid spec, got nil")
288+
}
289+
}
290+
291+
// Test blocking run method behaves as Start()
292+
func TestBlockingRun(t *testing.T) {
293+
wg := &sync.WaitGroup{}
294+
wg.Add(1)
295+
296+
cron := New()
297+
cron.AddFunc("", "* * * * * ?", func() { wg.Done() })
298+
299+
var unblockChan = make(chan struct{})
300+
301+
go func() {
302+
cron.Run()
303+
close(unblockChan)
304+
}()
305+
defer cron.Stop()
306+
307+
select {
308+
case <-time.After(OneSecond):
309+
t.Error("expected job fires")
310+
case <-unblockChan:
311+
t.Error("expected that Run() blocks")
312+
case <-wait(wg):
313+
}
314+
}
315+
316+
// Test that double-running is a no-op
317+
func TestStartNoop(t *testing.T) {
318+
var tickChan = make(chan struct{}, 2)
319+
320+
cron := New()
321+
cron.AddFunc("", "* * * * * ?", func() {
322+
tickChan <- struct{}{}
323+
})
324+
325+
cron.Start()
326+
defer cron.Stop()
327+
328+
// Wait for the first firing to ensure the runner is going
329+
<-tickChan
330+
331+
cron.Start()
332+
333+
<-tickChan
334+
335+
// Fail if this job fires again in a short period, indicating a double-run
336+
select {
337+
case <-time.After(time.Millisecond):
338+
case <-tickChan:
339+
t.Error("expected job fires exactly twice")
340+
}
341+
}
342+
283343
// Simple test using Runnables.
284344
func TestJob(t *testing.T) {
285345
wg := &sync.WaitGroup{}
@@ -297,7 +357,7 @@ func TestJob(t *testing.T) {
297357
defer cron.Stop()
298358

299359
select {
300-
case <-time.After(ONE_SECOND):
360+
case <-time.After(OneSecond):
301361
t.FailNow()
302362
case <-wait(wg):
303363
}
@@ -312,12 +372,31 @@ func TestJob(t *testing.T) {
312372

313373
for i, expected := range expecteds {
314374
if actuals[i] != expected {
315-
t.Errorf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
316-
t.FailNow()
375+
t.Fatalf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals)
317376
}
318377
}
319378
}
320379

380+
type ZeroSchedule struct{}
381+
382+
func (*ZeroSchedule) Next(time.Time) time.Time {
383+
return time.Time{}
384+
}
385+
386+
// Tests that job without time does not run
387+
func TestJobWithZeroTimeDoesNotRun(t *testing.T) {
388+
cron := New()
389+
calls := 0
390+
cron.AddFunc("", "* * * * * *", func() { calls += 1 })
391+
cron.Schedule("", "", new(ZeroSchedule), FuncJob(func() { t.Error("expected zero task will not run") }))
392+
cron.Start()
393+
defer cron.Stop()
394+
<-time.After(OneSecond)
395+
if calls != 1 {
396+
t.Errorf("called %d times, expected 1\n", calls)
397+
}
398+
}
399+
321400
func wait(wg *sync.WaitGroup) chan bool {
322401
ch := make(chan bool)
323402
go func() {

doc.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,16 @@ You may use one of several pre-defined schedules in place of a cron expression.
8484
8585
Intervals
8686
87-
You may also schedule a job to execute at fixed intervals. This is supported by
88-
formatting the cron spec like this:
87+
You may also schedule a job to execute at fixed intervals, starting at the time it's added
88+
or cron is run. This is supported by formatting the cron spec like this:
8989
9090
@every <duration>
9191
9292
where "duration" is a string accepted by time.ParseDuration
9393
(http://golang.org/pkg/time/#ParseDuration).
9494
95-
For example, "@every 1h30m10s" would indicate a schedule that activates every
96-
1 hour, 30 minutes, 10 seconds.
95+
For example, "@every 1h30m10s" would indicate a schedule that activates immediately,
96+
and then every 1 hour, 30 minutes, 10 seconds.
9797
9898
Note: The interval does not take the job runtime into account. For example,
9999
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,

0 commit comments

Comments
 (0)