浏览代码

Make the Cron Schedule an interface and add a generic Schedule routine. Add a constant delay schedule with an interface of either Every(time.Minute) or a spec of "@every 5m"

Rob Figueiredo 13 年之前
父节点
当前提交
c4429b3357
共有 9 个文件被更改,包括 186 次插入28 次删除
  1. 22 1
      README.md
  2. 28 0
      constantdelay.go
  3. 51 0
      constantdelay_test.go
  4. 18 6
      cron.go
  5. 35 5
      cron_test.go
  6. 22 8
      parser.go
  7. 5 3
      parser_test.go
  8. 5 5
      spec.go
  9. 0 0
      spec_test.go

+ 22 - 1
README.md

@@ -12,7 +12,8 @@ them in their own goroutines.
 ```go
 c := cron.New()
 c.AddFunc("0 5 * * * *", func() { fmt.Println("Every 5 minutes") })
-c.AddFunc("@hourly", func() { fmt.Println("Every hour") })
+c.AddFunc("@hourly",     func() { fmt.Println("Every hour") })
+c.AddFunc("@every 1h30m, func() { fmt.Println("Every hour thirty") })
 c.Start()
 ..
 // Funcs are invoked in their own goroutine, asynchronously.
@@ -91,6 +92,26 @@ Entry | Description | Equivalent To
 @daily (or @midnight) | Run once a day, midnight | <code>0 0 0 * * *</code>
 @hourly | Run once an hour, beginning of hour | <code>0 0 * * * *</code>
 
+## Intervals
+
+You may also schedule a job to execute at fixed intervals.  This is supported by
+formatting the cron spec like this:
+
+    @every <duration>
+
+where `<duration>` is a string accepted by
+[`time.ParseDuration`](http://golang.org/pkg/time/#ParseDuration).
+
+For example
+
+    @every 1h30m10s
+
+would indicate a schedule that activates every 1 hour, 30 minutes, 10 seconds.
+
+> Note: The interval does not take the job runtime into account.  For example,
+> if a job takes *3 minutes* to run, and it is scheduled to run every *5 minutes*,
+> it will have only *2 minutes* of idle time between each run.
+
 ## Time zones
 
 All interpretation and scheduling is done in the machine's local time zone (as

+ 28 - 0
constantdelay.go

@@ -0,0 +1,28 @@
+package cron
+
+import "time"
+
+// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
+// It does not support jobs more frequent than once a second.
+type ConstantDelaySchedule struct {
+	Delay time.Duration
+}
+
+// Every returns a crontab Schedule that activates once every duration.
+// Delays of less than a second are not supported (will panic).
+// Any fields less than a Second are truncated.
+func Every(duration time.Duration) ConstantDelaySchedule {
+	if duration < time.Second {
+		panic("cron/constantdelay: delays of less than a second are not supported: " +
+			duration.String())
+	}
+	return ConstantDelaySchedule{
+		Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
+	}
+}
+
+// Next returns the next time this should be run.
+// This rounds so that the next activation time will be on the second.
+func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
+	return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
+}

+ 51 - 0
constantdelay_test.go

@@ -0,0 +1,51 @@
+package cron
+
+import (
+	"testing"
+	"time"
+)
+
+func TestConstantDelayNext(t *testing.T) {
+	tests := []struct {
+		time     string
+		delay    time.Duration
+		expected string
+	}{
+		// Simple cases
+		{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
+		{"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"},
+		{"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"},
+
+		// Wrap around hours
+		{"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"},
+
+		// Wrap around days
+		{"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"},
+		{"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"},
+		{"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"},
+		{"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"},
+
+		// Wrap around months
+		{"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"},
+
+		// Wrap around minute, hour, day, month, and year
+		{"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"},
+
+		// Round to nearest second on the delay
+		{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
+
+		// Round to nearest second when calculating the next time.
+		{"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"},
+
+		// Round to nearest second for both.
+		{"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
+	}
+
+	for _, c := range tests {
+		actual := Every(c.delay).Next(getTime(c.time))
+		expected := getTime(c.expected)
+		if actual != expected {
+			t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual)
+		}
+	}
+}

+ 18 - 6
cron.go

@@ -23,10 +23,17 @@ type Job interface {
 	Run()
 }
 
+// The Schedule describes a job's duty cycle.
+type Schedule interface {
+	// Return the next activation time, later than the given time.
+	// Next is invoked initially, and then each time the job is run.
+	Next(time.Time) time.Time
+}
+
 // Entry consists of a schedule and the func to execute on that schedule.
 type Entry struct {
 	// The schedule on which this job should be run.
-	*Schedule
+	Schedule Schedule
 
 	// The next time the job will run. This is the zero time if Cron has not been
 	// started or this entry's schedule is unsatisfiable
@@ -70,20 +77,25 @@ func New() *Cron {
 	}
 }
 
-// jobAdapter provides a default implementation for wrapping a simple func.
-type jobAdapter func()
+// A wrapper that turns a func() into a cron.Job
+type FuncJob func()
 
-func (r jobAdapter) Run() { r() }
+func (f FuncJob) Run() { f() }
 
 // AddFunc adds a func to the Cron to be run on the given schedule.
 func (c *Cron) AddFunc(spec string, cmd func()) {
-	c.AddJob(spec, jobAdapter(cmd))
+	c.AddJob(spec, FuncJob(cmd))
 }
 
 // AddFunc adds a Job to the Cron to be run on the given schedule.
 func (c *Cron) AddJob(spec string, cmd Job) {
+	c.Schedule(Parse(spec), cmd)
+}
+
+// Schedule adds a Job to the Cron to be run on the given schedule.
+func (c *Cron) Schedule(schedule Schedule, cmd Job) {
 	entry := &Entry{
-		Schedule: Parse(spec),
+		Schedule: schedule,
 		Job:      cmd,
 	}
 	if !c.running {

+ 35 - 5
cron_test.go

@@ -108,8 +108,30 @@ func TestRunningJobTwice(t *testing.T) {
 
 	cron := New()
 	cron.AddFunc("0 0 0 1 1 ?", func() {})
+	cron.AddFunc("0 0 0 31 12 ?", func() {})
 	cron.AddFunc("* * * * * ?", func() { wg.Done() })
+
+	cron.Start()
+	defer cron.Stop()
+
+	select {
+	case <-time.After(2 * ONE_SECOND):
+		t.FailNow()
+	case <-wait(wg):
+	}
+}
+
+func TestRunningMultipleSchedules(t *testing.T) {
+	wg := &sync.WaitGroup{}
+	wg.Add(2)
+
+	cron := New()
+	cron.AddFunc("0 0 0 1 1 ?", func() {})
 	cron.AddFunc("0 0 0 31 12 ?", func() {})
+	cron.AddFunc("* * * * * ?", func() { wg.Done() })
+	cron.Schedule(Every(time.Minute), FuncJob(func() {}))
+	cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))
+	cron.Schedule(Every(time.Hour), FuncJob(func() {}))
 
 	cron.Start()
 	defer cron.Stop()
@@ -161,6 +183,8 @@ func TestJob(t *testing.T) {
 	cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"})
 	cron.AddJob("* * * * * ?", testJob{wg, "job2"})
 	cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"})
+	cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
+	cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"})
 
 	cron.Start()
 	defer cron.Stop()
@@ -172,11 +196,17 @@ func TestJob(t *testing.T) {
 	}
 
 	// Ensure the entries are in the right order.
-	answers := []string{"job2", "job1", "job3", "job0"}
-	for i, answer := range answers {
-		actual := cron.Entries()[i].Job.(testJob).name
-		if actual != answer {
-			t.Errorf("Jobs not in the right order.  (expected) %s != %s (actual)", answer, actual)
+	expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}
+
+	var actuals []string
+	for _, entry := range cron.Entries() {
+		actuals = append(actuals, entry.Job.(testJob).name)
+	}
+
+	for i, expected := range expecteds {
+		if actuals[i] != expected {
+			t.Errorf("Jobs not in the right order.  (expected) %s != %s (actual)", expecteds, actuals)
+			t.FailNow()
 		}
 	}
 }

+ 22 - 8
parser.go

@@ -5,11 +5,16 @@ import (
 	"math"
 	"strconv"
 	"strings"
+	"time"
 )
 
 // Parse returns a new crontab schedule representing the given spec.
 // It panics with a descriptive error if the spec is not valid.
-func Parse(spec string) *Schedule {
+//
+// It accepts
+//   - Full crontab specs, e.g. "* * * * * ?"
+//   - Descriptors, e.g. "@midnight", "@every 1h30m"
+func Parse(spec string) Schedule {
 	if spec[0] == '@' {
 		return parseDescriptor(spec)
 	}
@@ -26,7 +31,7 @@ func Parse(spec string) *Schedule {
 		fields = append(fields, "*")
 	}
 
-	schedule := &Schedule{
+	schedule := &SpecSchedule{
 		Second: getField(fields[0], seconds),
 		Minute: getField(fields[1], minutes),
 		Hour:   getField(fields[2], hours),
@@ -156,10 +161,10 @@ func first(r bounds) uint64 {
 
 // parseDescriptor returns a pre-defined schedule for the expression, or panics
 // if none matches.
-func parseDescriptor(spec string) *Schedule {
+func parseDescriptor(spec string) Schedule {
 	switch spec {
 	case "@yearly", "@annually":
-		return &Schedule{
+		return &SpecSchedule{
 			Second: 1 << seconds.min,
 			Minute: 1 << minutes.min,
 			Hour:   1 << hours.min,
@@ -169,7 +174,7 @@ func parseDescriptor(spec string) *Schedule {
 		}
 
 	case "@monthly":
-		return &Schedule{
+		return &SpecSchedule{
 			Second: 1 << seconds.min,
 			Minute: 1 << minutes.min,
 			Hour:   1 << hours.min,
@@ -179,7 +184,7 @@ func parseDescriptor(spec string) *Schedule {
 		}
 
 	case "@weekly":
-		return &Schedule{
+		return &SpecSchedule{
 			Second: 1 << seconds.min,
 			Minute: 1 << minutes.min,
 			Hour:   1 << hours.min,
@@ -189,7 +194,7 @@ func parseDescriptor(spec string) *Schedule {
 		}
 
 	case "@daily", "@midnight":
-		return &Schedule{
+		return &SpecSchedule{
 			Second: 1 << seconds.min,
 			Minute: 1 << minutes.min,
 			Hour:   1 << hours.min,
@@ -199,7 +204,7 @@ func parseDescriptor(spec string) *Schedule {
 		}
 
 	case "@hourly":
-		return &Schedule{
+		return &SpecSchedule{
 			Second: 1 << seconds.min,
 			Minute: 1 << minutes.min,
 			Hour:   all(hours),
@@ -209,6 +214,15 @@ func parseDescriptor(spec string) *Schedule {
 		}
 	}
 
+	const every = "@every "
+	if strings.HasPrefix(spec, every) {
+		duration, err := time.ParseDuration(spec[len(every):])
+		if err != nil {
+			log.Panicf("Failed to parse duration %s: %s", spec, err)
+		}
+		return Every(duration)
+	}
+
 	log.Panicf("Unrecognized descriptor: %s", spec)
 	return nil
 }

+ 5 - 3
parser_test.go

@@ -3,6 +3,7 @@ package cron
 import (
 	"reflect"
 	"testing"
+	"time"
 )
 
 func TestRange(t *testing.T) {
@@ -95,16 +96,17 @@ func TestBits(t *testing.T) {
 	}
 }
 
-func TestSchedule(t *testing.T) {
+func TestSpecSchedule(t *testing.T) {
 	entries := []struct {
 		expr     string
 		expected Schedule
 	}{
-		{"* 5 * * * *", Schedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
+		{"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
+		{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}},
 	}
 
 	for _, c := range entries {
-		actual := *Parse(c.expr)
+		actual := Parse(c.expr)
 		if !reflect.DeepEqual(actual, c.expected) {
 			t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
 		}

+ 5 - 5
schedule.go → spec.go

@@ -4,9 +4,9 @@ import (
 	"time"
 )
 
-// Schedule specifies a duty cycle (to the second granularity).
-// Schedules are computed initially and stored as bit sets.
-type Schedule struct {
+// SpecSchedule specifies a duty cycle (to the second granularity), based on a
+// traditional crontab specification. It is computed initially and stored as bit sets.
+type SpecSchedule struct {
 	Second, Minute, Hour, Dom, Month, Dow uint64
 }
 
@@ -54,7 +54,7 @@ const (
 
 // 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 *Schedule) Next(t time.Time) time.Time {
+func (s *SpecSchedule) Next(t time.Time) time.Time {
 	// General approach:
 	// For Month, Day, Hour, Minute, Second:
 	// Check if the time value matches.  If yes, continue to the next field.
@@ -148,7 +148,7 @@ WRAP:
 
 // dayMatches returns true if the schedule's day-of-week and day-of-month
 // restrictions are satisfied by the given time.
-func dayMatches(s *Schedule, t time.Time) bool {
+func dayMatches(s *SpecSchedule, t time.Time) bool {
 	var (
 		domMatch bool = 1<<uint(t.Day())&s.Dom > 0
 		dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0

+ 0 - 0
schedule_test.go → spec_test.go