Move cron module to independent package
Make it easier to keep track of upstream changes and bug fixes
This commit is contained in:
parent
7140dbac95
commit
d8a994ef24
@ -17,6 +17,7 @@ github.com/go-sql-driver/mysql = commit:b4db83c
|
|||||||
github.com/go-xorm/core = commit:1e2868c
|
github.com/go-xorm/core = commit:1e2868c
|
||||||
github.com/go-xorm/xorm =
|
github.com/go-xorm/xorm =
|
||||||
github.com/gogits/chardet = commit:2404f77725
|
github.com/gogits/chardet = commit:2404f77725
|
||||||
|
github.com/gogits/cron =
|
||||||
github.com/gogits/git-module = commit:3b40eae
|
github.com/gogits/git-module = commit:3b40eae
|
||||||
github.com/gogits/go-gogs-client =
|
github.com/gogits/go-gogs-client =
|
||||||
github.com/issue9/identicon = commit:f8c0d2c
|
github.com/issue9/identicon = commit:f8c0d2c
|
||||||
|
@ -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()
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
// Copyright 2012 Rob Figueiredo. All rights reserved.
|
|
||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
@ -6,207 +5,59 @@
|
|||||||
package cron
|
package cron
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
"time"
|
"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
|
var c = cron.New()
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Job is an interface for submitted cron jobs.
|
func NewContext() {
|
||||||
type Job interface {
|
var (
|
||||||
Run()
|
entry *cron.Entry
|
||||||
}
|
err error
|
||||||
|
)
|
||||||
// The Schedule describes a job's duty cycle.
|
if setting.Cron.UpdateMirror.Enabled {
|
||||||
type Schedule interface {
|
entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, models.MirrorUpdate)
|
||||||
// Return the next activation time, later than the given time.
|
if err != nil {
|
||||||
// Next is invoked initially, and then each time the job is run.
|
log.Fatal(4, "Cron[Update mirrors]: %v", err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
if setting.Cron.UpdateMirror.RunAtStart {
|
||||||
select {
|
entry.Prev = time.Now()
|
||||||
case now = <-time.After(effective.Sub(now)):
|
entry.ExecTimes++
|
||||||
// Run every entry whose next time was this effective time.
|
go models.MirrorUpdate()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'now' should be updated after newEntry and snapshot cases.
|
|
||||||
now = time.Now().Local()
|
|
||||||
}
|
}
|
||||||
}
|
if setting.Cron.RepoHealthCheck.Enabled {
|
||||||
|
entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, models.GitFsck)
|
||||||
// Stop the cron scheduler.
|
if err != nil {
|
||||||
func (c *Cron) Stop() {
|
log.Fatal(4, "Cron[Repository health check]: %v", err)
|
||||||
c.stop <- struct{}{}
|
}
|
||||||
c.running = false
|
if setting.Cron.RepoHealthCheck.RunAtStart {
|
||||||
}
|
entry.Prev = time.Now()
|
||||||
|
entry.ExecTimes++
|
||||||
// entrySnapshot returns a copy of the current cron entry list.
|
go models.GitFsck()
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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 <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
|
|
||||||
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
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<<uint(t.Month())&s.Month == 0 {
|
|
||||||
// If we have to add a month, reset the other parts to 0.
|
|
||||||
if !added {
|
|
||||||
added = true
|
|
||||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
|
||||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
|
|
||||||
}
|
|
||||||
t = t.AddDate(0, 1, 0)
|
|
||||||
|
|
||||||
// Wrapped around.
|
|
||||||
if t.Month() == time.January {
|
|
||||||
goto WRAP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now get a day in that month.
|
|
||||||
for !dayMatches(s, t) {
|
|
||||||
if !added {
|
|
||||||
added = true
|
|
||||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
|
||||||
}
|
|
||||||
t = t.AddDate(0, 0, 1)
|
|
||||||
|
|
||||||
if t.Day() == 1 {
|
|
||||||
goto WRAP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for 1<<uint(t.Hour())&s.Hour == 0 {
|
|
||||||
if !added {
|
|
||||||
added = true
|
|
||||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())
|
|
||||||
}
|
|
||||||
t = t.Add(1 * time.Hour)
|
|
||||||
|
|
||||||
if t.Hour() == 0 {
|
|
||||||
goto WRAP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for 1<<uint(t.Minute())&s.Minute == 0 {
|
|
||||||
if !added {
|
|
||||||
added = true
|
|
||||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
|
|
||||||
}
|
|
||||||
t = t.Add(1 * time.Minute)
|
|
||||||
|
|
||||||
if t.Minute() == 0 {
|
|
||||||
goto WRAP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for 1<<uint(t.Second())&s.Second == 0 {
|
|
||||||
if !added {
|
|
||||||
added = true
|
|
||||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location())
|
|
||||||
}
|
|
||||||
t = t.Add(1 * time.Second)
|
|
||||||
|
|
||||||
if t.Second() == 0 {
|
|
||||||
goto WRAP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
|
||||||
// restrictions are satisfied by the given time.
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
|
|
||||||
return domMatch && dowMatch
|
|
||||||
}
|
|
||||||
return domMatch || dowMatch
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -14,8 +14,8 @@ import (
|
|||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
|
|
||||||
"github.com/gogits/gogs/models"
|
"github.com/gogits/gogs/models"
|
||||||
"github.com/gogits/gogs/models/cron"
|
|
||||||
"github.com/gogits/gogs/modules/base"
|
"github.com/gogits/gogs/modules/base"
|
||||||
|
"github.com/gogits/gogs/modules/cron"
|
||||||
"github.com/gogits/gogs/modules/middleware"
|
"github.com/gogits/gogs/modules/middleware"
|
||||||
"github.com/gogits/gogs/modules/process"
|
"github.com/gogits/gogs/modules/process"
|
||||||
"github.com/gogits/gogs/modules/setting"
|
"github.com/gogits/gogs/modules/setting"
|
||||||
|
@ -20,9 +20,9 @@ import (
|
|||||||
"github.com/gogits/git-module"
|
"github.com/gogits/git-module"
|
||||||
|
|
||||||
"github.com/gogits/gogs/models"
|
"github.com/gogits/gogs/models"
|
||||||
"github.com/gogits/gogs/models/cron"
|
|
||||||
"github.com/gogits/gogs/modules/auth"
|
"github.com/gogits/gogs/modules/auth"
|
||||||
"github.com/gogits/gogs/modules/base"
|
"github.com/gogits/gogs/modules/base"
|
||||||
|
"github.com/gogits/gogs/modules/cron"
|
||||||
"github.com/gogits/gogs/modules/log"
|
"github.com/gogits/gogs/modules/log"
|
||||||
"github.com/gogits/gogs/modules/mailer"
|
"github.com/gogits/gogs/modules/mailer"
|
||||||
"github.com/gogits/gogs/modules/middleware"
|
"github.com/gogits/gogs/modules/middleware"
|
||||||
|
Loading…
Reference in New Issue
Block a user