From d8a994ef243349f321568f9e36d5c3f444b99cae Mon Sep 17 00:00:00 2001 From: Unknwon Date: Sat, 20 Feb 2016 15:58:09 -0500 Subject: [PATCH] Move cron module to independent package Make it easier to keep track of upstream changes and bug fixes --- .gopmfile | 1 + models/cron/cron.go | 59 ------- modules/cron/constantdelay.go | 27 --- modules/cron/constantdelay_test.go | 54 ------ modules/cron/cron.go | 243 ++++++--------------------- modules/cron/cron_test.go | 255 ----------------------------- modules/cron/doc.go | 129 --------------- modules/cron/parser.go | 231 -------------------------- modules/cron/parser_test.go | 117 ------------- modules/cron/spec.go | 161 ------------------ modules/cron/spec_test.go | 173 ------------------- routers/admin/admin.go | 2 +- routers/install.go | 2 +- 13 files changed, 50 insertions(+), 1404 deletions(-) delete mode 100644 models/cron/cron.go delete mode 100644 modules/cron/constantdelay.go delete mode 100644 modules/cron/constantdelay_test.go delete mode 100644 modules/cron/cron_test.go delete mode 100644 modules/cron/doc.go delete mode 100644 modules/cron/parser.go delete mode 100644 modules/cron/parser_test.go delete mode 100644 modules/cron/spec.go delete mode 100644 modules/cron/spec_test.go diff --git a/.gopmfile b/.gopmfile index 765d10030..cfa86abf1 100644 --- a/.gopmfile +++ b/.gopmfile @@ -17,6 +17,7 @@ github.com/go-sql-driver/mysql = commit:b4db83c github.com/go-xorm/core = commit:1e2868c github.com/go-xorm/xorm = github.com/gogits/chardet = commit:2404f77725 +github.com/gogits/cron = github.com/gogits/git-module = commit:3b40eae github.com/gogits/go-gogs-client = github.com/issue9/identicon = commit:f8c0d2c diff --git a/models/cron/cron.go b/models/cron/cron.go deleted file mode 100644 index 8e494e553..000000000 --- a/models/cron/cron.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package cron - -import ( - "time" - - "github.com/gogits/gogs/models" - "github.com/gogits/gogs/modules/cron" - "github.com/gogits/gogs/modules/log" - "github.com/gogits/gogs/modules/setting" -) - -var c = cron.New() - -func NewContext() { - var ( - entry *cron.Entry - err error - ) - if setting.Cron.UpdateMirror.Enabled { - entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, models.MirrorUpdate) - if err != nil { - log.Fatal(4, "Cron[Update mirrors]: %v", err) - } - if setting.Cron.UpdateMirror.RunAtStart { - entry.Prev = time.Now() - go models.MirrorUpdate() - } - } - if setting.Cron.RepoHealthCheck.Enabled { - entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, models.GitFsck) - if err != nil { - log.Fatal(4, "Cron[Repository health check]: %v", err) - } - if setting.Cron.RepoHealthCheck.RunAtStart { - entry.Prev = time.Now() - go models.GitFsck() - } - } - if setting.Cron.CheckRepoStats.Enabled { - entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, models.CheckRepoStats) - if err != nil { - log.Fatal(4, "Cron[Check repository statistics]: %v", err) - } - if setting.Cron.CheckRepoStats.RunAtStart { - entry.Prev = time.Now() - go models.CheckRepoStats() - } - } - c.Start() -} - -// ListTasks returns all running cron tasks. -func ListTasks() []*cron.Entry { - return c.Entries() -} diff --git a/modules/cron/constantdelay.go b/modules/cron/constantdelay.go deleted file mode 100644 index cd6e7b1be..000000000 --- a/modules/cron/constantdelay.go +++ /dev/null @@ -1,27 +0,0 @@ -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 round up to 1 second). -// Any fields less than a Second are truncated. -func Every(duration time.Duration) ConstantDelaySchedule { - if duration < time.Second { - duration = time.Second - } - 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) -} diff --git a/modules/cron/constantdelay_test.go b/modules/cron/constantdelay_test.go deleted file mode 100644 index f43a58ad2..000000000 --- a/modules/cron/constantdelay_test.go +++ /dev/null @@ -1,54 +0,0 @@ -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 up to 1 second if the duration is less. - {"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 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) - } - } -} diff --git a/modules/cron/cron.go b/modules/cron/cron.go index 3491afac9..7695b974b 100644 --- a/modules/cron/cron.go +++ b/modules/cron/cron.go @@ -1,4 +1,3 @@ -// Copyright 2012 Rob Figueiredo. All rights reserved. // Copyright 2014 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -6,207 +5,59 @@ package cron import ( - "sort" "time" + + "github.com/gogits/cron" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/setting" ) -// Cron keeps track of any number of entries, invoking the associated func as -// specified by the schedule. It may be started, stopped, and the entries may -// be inspected while running. -type Cron struct { - entries []*Entry - stop chan struct{} - add chan *Entry - snapshot chan []*Entry - running bool -} +var c = cron.New() -// Job is an interface for submitted cron jobs. -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 { - Description string - Spec string - - // The schedule on which this job should be run. - 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 - Next time.Time - - // The last time this job was run. This is the zero time if the job has never - // been run. - Prev time.Time - - // The Job to run. - Job Job - - ExecTimes int // Execute times count. -} - -// byTime is a wrapper for sorting the entry array by time -// (with zero time at the end). -type byTime []*Entry - -func (s byTime) Len() int { return len(s) } -func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s byTime) Less(i, j int) bool { - // Two zero times should return false. - // Otherwise, zero is "greater" than any other time. - // (To sort it at the end of the list.) - if s[i].Next.IsZero() { - return false - } - if s[j].Next.IsZero() { - return true - } - return s[i].Next.Before(s[j].Next) -} - -// New returns a new Cron job runner. -func New() *Cron { - return &Cron{ - entries: nil, - add: make(chan *Entry), - stop: make(chan struct{}), - snapshot: make(chan []*Entry), - running: false, - } -} - -// A wrapper that turns a func() into a cron.Job -type FuncJob func() - -func (f FuncJob) Run() { f() } - -// AddFunc adds a func to the Cron to be run on the given schedule. -func (c *Cron) AddFunc(desc, spec string, cmd func()) (*Entry, error) { - return c.AddJob(desc, spec, FuncJob(cmd)) -} - -// AddFunc adds a Job to the Cron to be run on the given schedule. -func (c *Cron) AddJob(desc, spec string, cmd Job) (*Entry, error) { - schedule, err := Parse(spec) - if err != nil { - return nil, err - } - return c.Schedule(desc, spec, schedule, cmd), nil -} - -// Schedule adds a Job to the Cron to be run on the given schedule. -func (c *Cron) Schedule(desc, spec string, schedule Schedule, cmd Job) *Entry { - entry := &Entry{ - Description: desc, - Spec: spec, - Schedule: schedule, - Job: cmd, - } - if c.running { - c.add <- entry - } else { - c.entries = append(c.entries, entry) - } - return entry -} - -// Entries returns a snapshot of the cron entries. -func (c *Cron) Entries() []*Entry { - if c.running { - c.snapshot <- nil - x := <-c.snapshot - return x - } - return c.entrySnapshot() -} - -// Start the cron scheduler in its own go-routine. -func (c *Cron) Start() { - c.running = true - go c.run() -} - -// Run the scheduler.. this is private just due to the need to synchronize -// access to the 'running' state variable. -func (c *Cron) run() { - // Figure out the next activation times for each entry. - now := time.Now().Local() - for _, entry := range c.entries { - entry.Next = entry.Schedule.Next(now) - } - - for { - // Determine the next entry to run. - sort.Sort(byTime(c.entries)) - - var effective time.Time - if len(c.entries) == 0 || c.entries[0].Next.IsZero() { - // If there are no entries yet, just sleep - it still handles new entries - // and stop requests. - effective = now.AddDate(10, 0, 0) - } else { - effective = c.entries[0].Next +func NewContext() { + var ( + entry *cron.Entry + err error + ) + if setting.Cron.UpdateMirror.Enabled { + entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, models.MirrorUpdate) + if err != nil { + log.Fatal(4, "Cron[Update mirrors]: %v", err) } - - select { - case now = <-time.After(effective.Sub(now)): - // Run every entry whose next time was this effective time. - for _, e := range c.entries { - if e.Next != effective { - break - } - go e.Job.Run() - e.ExecTimes++ - e.Prev = e.Next - e.Next = e.Schedule.Next(effective) - } - continue - - case newEntry := <-c.add: - c.entries = append(c.entries, newEntry) - newEntry.Next = newEntry.Schedule.Next(now) - - case <-c.snapshot: - c.snapshot <- c.entrySnapshot() - - case <-c.stop: - return + if setting.Cron.UpdateMirror.RunAtStart { + entry.Prev = time.Now() + entry.ExecTimes++ + go models.MirrorUpdate() } - - // 'now' should be updated after newEntry and snapshot cases. - now = time.Now().Local() } -} - -// Stop the cron scheduler. -func (c *Cron) Stop() { - c.stop <- struct{}{} - c.running = false -} - -// entrySnapshot returns a copy of the current cron entry list. -func (c *Cron) entrySnapshot() []*Entry { - entries := make([]*Entry, 0, len(c.entries)) - for _, e := range c.entries { - entries = append(entries, &Entry{ - Description: e.Description, - Spec: e.Spec, - Schedule: e.Schedule, - Next: e.Next, - Prev: e.Prev, - Job: e.Job, - ExecTimes: e.ExecTimes, - }) + if setting.Cron.RepoHealthCheck.Enabled { + entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, models.GitFsck) + if err != nil { + log.Fatal(4, "Cron[Repository health check]: %v", err) + } + if setting.Cron.RepoHealthCheck.RunAtStart { + entry.Prev = time.Now() + entry.ExecTimes++ + go models.GitFsck() + } } - return entries + if setting.Cron.CheckRepoStats.Enabled { + entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, models.CheckRepoStats) + if err != nil { + log.Fatal(4, "Cron[Check repository statistics]: %v", err) + } + if setting.Cron.CheckRepoStats.RunAtStart { + entry.Prev = time.Now() + entry.ExecTimes++ + go models.CheckRepoStats() + } + } + c.Start() +} + +// ListTasks returns all running cron tasks. +func ListTasks() []*cron.Entry { + return c.Entries() } diff --git a/modules/cron/cron_test.go b/modules/cron/cron_test.go deleted file mode 100644 index 417247a05..000000000 --- a/modules/cron/cron_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package cron - -import ( - "fmt" - "sync" - "testing" - "time" -) - -// Many tests schedule a job for every second, and then wait at most a second -// for it to run. This amount is just slightly larger than 1 second to -// compensate for a few milliseconds of runtime. -const ONE_SECOND = 1*time.Second + 10*time.Millisecond - -// Start and stop cron with no entries. -func TestNoEntries(t *testing.T) { - cron := New() - cron.Start() - - select { - case <-time.After(ONE_SECOND): - t.FailNow() - case <-stop(cron): - } -} - -// Start, stop, then add an entry. Verify entry doesn't run. -func TestStopCausesJobsToNotRun(t *testing.T) { - wg := &sync.WaitGroup{} - wg.Add(1) - - cron := New() - cron.Start() - cron.Stop() - cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) - - select { - case <-time.After(ONE_SECOND): - // No job ran! - case <-wait(wg): - t.FailNow() - } -} - -// Add a job, start cron, expect it runs. -func TestAddBeforeRunning(t *testing.T) { - wg := &sync.WaitGroup{} - wg.Add(1) - - cron := New() - cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) - cron.Start() - defer cron.Stop() - - // Give cron 2 seconds to run our job (which is always activated). - select { - case <-time.After(ONE_SECOND): - t.FailNow() - case <-wait(wg): - } -} - -// Start cron, add a job, expect it runs. -func TestAddWhileRunning(t *testing.T) { - wg := &sync.WaitGroup{} - wg.Add(1) - - cron := New() - cron.Start() - defer cron.Stop() - cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) - - select { - case <-time.After(ONE_SECOND): - t.FailNow() - case <-wait(wg): - } -} - -// Test timing with Entries. -func TestSnapshotEntries(t *testing.T) { - wg := &sync.WaitGroup{} - wg.Add(1) - - cron := New() - cron.AddFunc("", "@every 2s", func() { wg.Done() }) - cron.Start() - defer cron.Stop() - - // Cron should fire in 2 seconds. After 1 second, call Entries. - select { - case <-time.After(ONE_SECOND): - cron.Entries() - } - - // Even though Entries was called, the cron should fire at the 2 second mark. - select { - case <-time.After(ONE_SECOND): - t.FailNow() - case <-wait(wg): - } - -} - -// Test that the entries are correctly sorted. -// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure -// that the immediate entry runs immediately. -// Also: Test that multiple jobs run in the same instant. -func TestMultipleEntries(t *testing.T) { - wg := &sync.WaitGroup{} - wg.Add(2) - - cron := New() - cron.AddFunc("", "0 0 0 1 1 ?", func() {}) - cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) - cron.AddFunc("", "0 0 0 31 12 ?", func() {}) - cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) - - cron.Start() - defer cron.Stop() - - select { - case <-time.After(ONE_SECOND): - t.FailNow() - case <-wait(wg): - } -} - -// Test running the same job twice. -func TestRunningJobTwice(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.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() - - select { - case <-time.After(2 * ONE_SECOND): - t.FailNow() - case <-wait(wg): - } -} - -// Test that the cron is run in the local time zone (as opposed to UTC). -func TestLocalTimezone(t *testing.T) { - wg := &sync.WaitGroup{} - wg.Add(1) - - now := time.Now().Local() - spec := fmt.Sprintf("%d %d %d %d %d ?", - now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month()) - - cron := New() - cron.AddFunc("", spec, func() { wg.Done() }) - cron.Start() - defer cron.Stop() - - select { - case <-time.After(ONE_SECOND): - t.FailNow() - case <-wait(wg): - } -} - -type testJob struct { - wg *sync.WaitGroup - name string -} - -func (t testJob) Run() { - t.wg.Done() -} - -// Simple test using Runnables. -func TestJob(t *testing.T) { - wg := &sync.WaitGroup{} - wg.Add(1) - - cron := New() - cron.AddJob("", "0 0 0 30 Feb ?", testJob{wg, "job0"}) - 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() - - select { - case <-time.After(ONE_SECOND): - t.FailNow() - case <-wait(wg): - } - - // Ensure the entries are in the right order. - 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() - } - } -} - -func wait(wg *sync.WaitGroup) chan bool { - ch := make(chan bool) - go func() { - wg.Wait() - ch <- true - }() - return ch -} - -func stop(cron *Cron) chan bool { - ch := make(chan bool) - go func() { - cron.Stop() - ch <- true - }() - return ch -} diff --git a/modules/cron/doc.go b/modules/cron/doc.go deleted file mode 100644 index 258fe1eff..000000000 --- a/modules/cron/doc.go +++ /dev/null @@ -1,129 +0,0 @@ -/* -Package cron implements a cron spec parser and job runner. - -Usage - -Callers may register Funcs to be invoked on a given schedule. Cron will run -them in their own goroutines. - - c := cron.New() - c.AddFunc("Every hour on the half hour","0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) - c.AddFunc("Every hour","@hourly", func() { fmt.Println("Every hour") }) - c.AddFunc("Every hour and a half","@every 1h30m", func() { fmt.Println("Every hour thirty") }) - c.Start() - .. - // Funcs are invoked in their own goroutine, asynchronously. - ... - // Funcs may also be added to a running Cron - c.AddFunc("@daily", func() { fmt.Println("Every day") }) - .. - // Inspect the cron job entries' next and previous run times. - inspect(c.Entries()) - .. - c.Stop() // Stop the scheduler (does not stop any jobs already running). - -CRON Expression Format - -A cron expression represents a set of times, using 6 space-separated fields. - - Field name | Mandatory? | Allowed values | Allowed special characters - ---------- | ---------- | -------------- | -------------------------- - Seconds | Yes | 0-59 | * / , - - Minutes | Yes | 0-59 | * / , - - Hours | Yes | 0-23 | * / , - - Day of month | Yes | 1-31 | * / , - ? - Month | Yes | 1-12 or JAN-DEC | * / , - - Day of week | Yes | 0-6 or SUN-SAT | * / , - ? - -Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", -and "sun" are equally accepted. - -Special Characters - -Asterisk ( * ) - -The asterisk indicates that the cron expression will match for all values of the -field; e.g., using an asterisk in the 5th field (month) would indicate every -month. - -Slash ( / ) - -Slashes are used to describe increments of ranges. For example 3-59/15 in the -1st field (minutes) would indicate the 3rd minute of the hour and every 15 -minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", -that is, an increment over the largest possible range of the field. The form -"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the -increment until the end of that specific range. It does not wrap around. - -Comma ( , ) - -Commas are used to separate items of a list. For example, using "MON,WED,FRI" in -the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. - -Hyphen ( - ) - -Hyphens are used to define ranges. For example, 9-17 would indicate every -hour between 9am and 5pm inclusive. - -Question mark ( ? ) - -Question mark may be used instead of '*' for leaving either day-of-month or -day-of-week blank. - -Predefined schedules - -You may use one of several pre-defined schedules in place of a cron expression. - - Entry | Description | Equivalent To - ----- | ----------- | ------------- - @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * - @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * - @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 - @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * - @hourly | Run once an hour, beginning of hour | 0 0 * * * * - -Intervals - -You may also schedule a job to execute at fixed intervals. This is supported by -formatting the cron spec like this: - - @every - -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 -provided by the Go time package (http://www.golang.org/pkg/time). - -Be aware that jobs scheduled during daylight-savings leap-ahead transitions will -not be run! - -Thread safety - -Since the Cron service runs concurrently with the calling code, some amount of -care must be taken to ensure proper synchronization. - -All cron methods are designed to be correctly synchronized as long as the caller -ensures that invocations have a clear happens-before ordering between them. - -Implementation - -Cron entries are stored in an array, sorted by their next activation time. Cron -sleeps until the next job is due to be run. - -Upon waking: - - it runs each entry that is active on that second - - it calculates the next run times for the jobs that were run - - it re-sorts the array of entries by next activation time. - - it goes to sleep until the soonest job. -*/ -package cron diff --git a/modules/cron/parser.go b/modules/cron/parser.go deleted file mode 100644 index 4224fa930..000000000 --- a/modules/cron/parser.go +++ /dev/null @@ -1,231 +0,0 @@ -package cron - -import ( - "fmt" - "log" - "math" - "strconv" - "strings" - "time" -) - -// Parse returns a new crontab schedule representing the given spec. -// It returns a descriptive error if the spec is not valid. -// -// It accepts -// - Full crontab specs, e.g. "* * * * * ?" -// - Descriptors, e.g. "@midnight", "@every 1h30m" -func Parse(spec string) (_ Schedule, err error) { - // Convert panics into errors - defer func() { - if recovered := recover(); recovered != nil { - err = fmt.Errorf("%v", recovered) - } - }() - - if spec[0] == '@' { - return parseDescriptor(spec), nil - } - - // Split on whitespace. We require 5 or 6 fields. - // (second) (minute) (hour) (day of month) (month) (day of week, optional) - fields := strings.Fields(spec) - if len(fields) != 5 && len(fields) != 6 { - log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec) - } - - // If a sixth field is not provided (DayOfWeek), then it is equivalent to star. - if len(fields) == 5 { - fields = append(fields, "*") - } - - schedule := &SpecSchedule{ - Second: getField(fields[0], seconds), - Minute: getField(fields[1], minutes), - Hour: getField(fields[2], hours), - Dom: getField(fields[3], dom), - Month: getField(fields[4], months), - Dow: getField(fields[5], dow), - } - - return schedule, nil -} - -// getField returns an Int with the bits set representing all of the times that -// the field represents. A "field" is a comma-separated list of "ranges". -func getField(field string, r bounds) uint64 { - // list = range {"," range} - var bits uint64 - ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) - for _, expr := range ranges { - bits |= getRange(expr, r) - } - return bits -} - -// getRange returns the bits indicated by the given expression: -// number | number "-" number [ "/" number ] -func getRange(expr string, r bounds) uint64 { - - var ( - start, end, step uint - rangeAndStep = strings.Split(expr, "/") - lowAndHigh = strings.Split(rangeAndStep[0], "-") - singleDigit = len(lowAndHigh) == 1 - ) - - var extra_star uint64 - if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { - start = r.min - end = r.max - extra_star = starBit - } else { - start = parseIntOrName(lowAndHigh[0], r.names) - switch len(lowAndHigh) { - case 1: - end = start - case 2: - end = parseIntOrName(lowAndHigh[1], r.names) - default: - log.Panicf("Too many hyphens: %s", expr) - } - } - - switch len(rangeAndStep) { - case 1: - step = 1 - case 2: - step = mustParseInt(rangeAndStep[1]) - - // Special handling: "N/step" means "N-max/step". - if singleDigit { - end = r.max - } - default: - log.Panicf("Too many slashes: %s", expr) - } - - if start < r.min { - log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) - } - if end > r.max { - log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr) - } - if start > end { - log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) - } - - return getBits(start, end, step) | extra_star -} - -// parseIntOrName returns the (possibly-named) integer contained in expr. -func parseIntOrName(expr string, names map[string]uint) uint { - if names != nil { - if namedInt, ok := names[strings.ToLower(expr)]; ok { - return namedInt - } - } - return mustParseInt(expr) -} - -// mustParseInt parses the given expression as an int or panics. -func mustParseInt(expr string) uint { - num, err := strconv.Atoi(expr) - if err != nil { - log.Panicf("Failed to parse int from %s: %s", expr, err) - } - if num < 0 { - log.Panicf("Negative number (%d) not allowed: %s", num, expr) - } - - return uint(num) -} - -// getBits sets all bits in the range [min, max], modulo the given step size. -func getBits(min, max, step uint) uint64 { - var bits uint64 - - // If step is 1, use shifts. - if step == 1 { - return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) - } - - // Else, use a simple loop. - for i := min; i <= max; i += step { - bits |= 1 << i - } - return bits -} - -// all returns all bits within the given bounds. (plus the star bit) -func all(r bounds) uint64 { - return getBits(r.min, r.max, 1) | starBit -} - -// parseDescriptor returns a pre-defined schedule for the expression, or panics -// if none matches. -func parseDescriptor(spec string) Schedule { - switch spec { - case "@yearly", "@annually": - return &SpecSchedule{ - Second: 1 << seconds.min, - Minute: 1 << minutes.min, - Hour: 1 << hours.min, - Dom: 1 << dom.min, - Month: 1 << months.min, - Dow: all(dow), - } - - case "@monthly": - return &SpecSchedule{ - Second: 1 << seconds.min, - Minute: 1 << minutes.min, - Hour: 1 << hours.min, - Dom: 1 << dom.min, - Month: all(months), - Dow: all(dow), - } - - case "@weekly": - return &SpecSchedule{ - Second: 1 << seconds.min, - Minute: 1 << minutes.min, - Hour: 1 << hours.min, - Dom: all(dom), - Month: all(months), - Dow: 1 << dow.min, - } - - case "@daily", "@midnight": - return &SpecSchedule{ - Second: 1 << seconds.min, - Minute: 1 << minutes.min, - Hour: 1 << hours.min, - Dom: all(dom), - Month: all(months), - Dow: all(dow), - } - - case "@hourly": - return &SpecSchedule{ - Second: 1 << seconds.min, - Minute: 1 << minutes.min, - Hour: all(hours), - Dom: all(dom), - Month: all(months), - Dow: all(dow), - } - } - - 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 -} diff --git a/modules/cron/parser_test.go b/modules/cron/parser_test.go deleted file mode 100644 index f03299e5e..000000000 --- a/modules/cron/parser_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package cron - -import ( - "reflect" - "testing" - "time" -) - -func TestRange(t *testing.T) { - ranges := []struct { - expr string - min, max uint - expected uint64 - }{ - {"5", 0, 7, 1 << 5}, - {"0", 0, 7, 1 << 0}, - {"7", 0, 7, 1 << 7}, - - {"5-5", 0, 7, 1 << 5}, - {"5-6", 0, 7, 1<<5 | 1<<6}, - {"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7}, - - {"5-6/2", 0, 7, 1 << 5}, - {"5-7/2", 0, 7, 1<<5 | 1<<7}, - {"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7}, - - {"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit}, - {"*/2", 1, 3, 1<<1 | 1<<3 | starBit}, - } - - for _, c := range ranges { - actual := getRange(c.expr, bounds{c.min, c.max, nil}) - if actual != c.expected { - t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) - } - } -} - -func TestField(t *testing.T) { - fields := []struct { - expr string - min, max uint - expected uint64 - }{ - {"5", 1, 7, 1 << 5}, - {"5,6", 1, 7, 1<<5 | 1<<6}, - {"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, - {"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, - } - - for _, c := range fields { - actual := getField(c.expr, bounds{c.min, c.max, nil}) - if actual != c.expected { - t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) - } - } -} - -func TestBits(t *testing.T) { - allBits := []struct { - r bounds - expected uint64 - }{ - {minutes, 0xfffffffffffffff}, // 0-59: 60 ones - {hours, 0xffffff}, // 0-23: 24 ones - {dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero - {months, 0x1ffe}, // 1-12: 12 ones, 1 zero - {dow, 0x7f}, // 0-6: 7 ones - } - - for _, c := range allBits { - actual := all(c.r) // all() adds the starBit, so compensate for that.. - if c.expected|starBit != actual { - t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", - c.r.min, c.r.max, 1, c.expected|starBit, actual) - } - } - - bits := []struct { - min, max, step uint - expected uint64 - }{ - - {0, 0, 1, 0x1}, - {1, 1, 1, 0x2}, - {1, 5, 2, 0x2a}, // 101010 - {1, 4, 2, 0xa}, // 1010 - } - - for _, c := range bits { - actual := getBits(c.min, c.max, c.step) - if c.expected != actual { - t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", - c.min, c.max, c.step, c.expected, actual) - } - } -} - -func TestSpecSchedule(t *testing.T) { - entries := []struct { - expr string - expected Schedule - }{ - {"* 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, err := Parse(c.expr) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(actual, c.expected) { - t.Errorf("%s => (expected) %v != %v (actual)", c.expr, c.expected, actual) - } - } -} diff --git a/modules/cron/spec.go b/modules/cron/spec.go deleted file mode 100644 index cb3743325..000000000 --- a/modules/cron/spec.go +++ /dev/null @@ -1,161 +0,0 @@ -package cron - -import ( - "time" -) - -// 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 -} - -// bounds provides a range of acceptable values (plus a map of name to value). -type bounds struct { - min, max uint - names map[string]uint -} - -// The bounds for each field. -var ( - seconds = bounds{0, 59, nil} - minutes = bounds{0, 59, nil} - hours = bounds{0, 23, nil} - dom = bounds{1, 31, nil} - months = bounds{1, 12, map[string]uint{ - "jan": 1, - "feb": 2, - "mar": 3, - "apr": 4, - "may": 5, - "jun": 6, - "jul": 7, - "aug": 8, - "sep": 9, - "oct": 10, - "nov": 11, - "dec": 12, - }} - dow = bounds{0, 6, map[string]uint{ - "sun": 0, - "mon": 1, - "tue": 2, - "wed": 3, - "thu": 4, - "fri": 5, - "sat": 6, - }} -) - -const ( - // Set the top bit if a star was included in the expression. - starBit = 1 << 63 -) - -// 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 *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. - // If the field doesn't match the schedule, then increment the field until it matches. - // While incrementing the field, a wrap-around brings it back to the beginning - // of the field list (since it is necessary to re-verify previous field - // values) - - // Start at the earliest possible time (the upcoming second). - t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) - - // This flag indicates whether a field has been incremented. - added := false - - // If no time is found within five years, return zero. - yearLimit := t.Year() + 5 - -WRAP: - if t.Year() > yearLimit { - return time.Time{} - } - - // Find the first applicable month. - // If it's this month, then do nothing. - for 1< 0 - dowMatch bool = 1< 0 - ) - - if s.Dom&starBit > 0 || s.Dow&starBit > 0 { - return domMatch && dowMatch - } - return domMatch || dowMatch -} diff --git a/modules/cron/spec_test.go b/modules/cron/spec_test.go deleted file mode 100644 index 855d79831..000000000 --- a/modules/cron/spec_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package cron - -import ( - "testing" - "time" -) - -func TestActivation(t *testing.T) { - tests := []struct { - time, spec string - expected bool - }{ - // Every fifteen minutes. - {"Mon Jul 9 15:00 2012", "0 0/15 * * *", true}, - {"Mon Jul 9 15:45 2012", "0 0/15 * * *", true}, - {"Mon Jul 9 15:40 2012", "0 0/15 * * *", false}, - - // Every fifteen minutes, starting at 5 minutes. - {"Mon Jul 9 15:05 2012", "0 5/15 * * *", true}, - {"Mon Jul 9 15:20 2012", "0 5/15 * * *", true}, - {"Mon Jul 9 15:50 2012", "0 5/15 * * *", true}, - - // Named months - {"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true}, - {"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false}, - - // Everything set. - {"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true}, - {"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true}, - {"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false}, - {"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false}, - - // Predefined schedules - {"Mon Jul 9 15:00 2012", "@hourly", true}, - {"Mon Jul 9 15:04 2012", "@hourly", false}, - {"Mon Jul 9 15:00 2012", "@daily", false}, - {"Mon Jul 9 00:00 2012", "@daily", true}, - {"Mon Jul 9 00:00 2012", "@weekly", false}, - {"Sun Jul 8 00:00 2012", "@weekly", true}, - {"Sun Jul 8 01:00 2012", "@weekly", false}, - {"Sun Jul 8 00:00 2012", "@monthly", false}, - {"Sun Jul 1 00:00 2012", "@monthly", true}, - - // Test interaction of DOW and DOM. - // If both are specified, then only one needs to match. - {"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true}, - {"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true}, - {"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true}, - - // However, if one has a star, then both need to match. - {"Sun Jul 15 00:00 2012", "0 * * * * Mon", false}, - {"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false}, - {"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false}, - {"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true}, - {"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true}, - } - - for _, test := range tests { - sched, err := Parse(test.spec) - if err != nil { - t.Error(err) - continue - } - actual := sched.Next(getTime(test.time).Add(-1 * time.Second)) - expected := getTime(test.time) - if test.expected && expected != actual || !test.expected && expected == actual { - t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)", - test.spec, test.time, expected, actual) - } - } -} - -func TestNext(t *testing.T) { - runs := []struct { - time, spec string - expected string - }{ - // Simple cases - {"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, - {"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, - {"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, - - // Wrap around hours - {"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"}, - - // Wrap around days - {"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"}, - {"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"}, - {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"}, - {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"}, - {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"}, - - {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"}, - {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"}, - {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"}, - - // Wrap around months - {"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"}, - {"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"}, - {"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"}, - - // Wrap around years - {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"}, - {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"}, - - // Wrap around minute, hour, day, month, and year - {"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"}, - - // Leap year - {"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"}, - - // Daylight savings time EST -> EDT - {"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"}, - - // Daylight savings time EDT -> EST - {"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"}, - {"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"}, - - // Unsatisfiable - {"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""}, - {"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""}, - } - - for _, c := range runs { - sched, err := Parse(c.spec) - if err != nil { - t.Error(err) - continue - } - actual := sched.Next(getTime(c.time)) - expected := getTime(c.expected) - if !actual.Equal(expected) { - t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) - } - } -} - -func TestErrors(t *testing.T) { - invalidSpecs := []string{ - "xyz", - "60 0 * * *", - "0 60 * * *", - "0 0 * * XYZ", - } - for _, spec := range invalidSpecs { - _, err := Parse(spec) - if err == nil { - t.Error("expected an error parsing: ", spec) - } - } -} - -func getTime(value string) time.Time { - if value == "" { - return time.Time{} - } - t, err := time.Parse("Mon Jan 2 15:04 2006", value) - if err != nil { - t, err = time.Parse("Mon Jan 2 15:04:05 2006", value) - if err != nil { - t, err = time.Parse("2006-01-02T15:04:05-0700", value) - if err != nil { - panic(err) - } - // Daylight savings time tests require location - if ny, err := time.LoadLocation("America/New_York"); err == nil { - t = t.In(ny) - } - } - } - - return t -} diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 5d665adba..27f2fa5b2 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -14,8 +14,8 @@ import ( "gopkg.in/macaron.v1" "github.com/gogits/gogs/models" - "github.com/gogits/gogs/models/cron" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/cron" "github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/setting" diff --git a/routers/install.go b/routers/install.go index b8b7dec1e..0a36b315f 100644 --- a/routers/install.go +++ b/routers/install.go @@ -20,9 +20,9 @@ import ( "github.com/gogits/git-module" "github.com/gogits/gogs/models" - "github.com/gogits/gogs/models/cron" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/cron" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/mailer" "github.com/gogits/gogs/modules/middleware"