pax_global_header00006660000000000000000000000064137730422530014520gustar00rootroot0000000000000052 comment=7eacf6d6bc79b4268759884d88e1e2c218e6995e gocron-0.5.0/000077500000000000000000000000001377304225300130115ustar00rootroot00000000000000gocron-0.5.0/.github/000077500000000000000000000000001377304225300143515ustar00rootroot00000000000000gocron-0.5.0/.github/FUNDING.yml000066400000000000000000000012371377304225300161710ustar00rootroot00000000000000# These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: go-co-op ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] gocron-0.5.0/.github/workflows/000077500000000000000000000000001377304225300164065ustar00rootroot00000000000000gocron-0.5.0/.github/workflows/branchcleanup.yml000066400000000000000000000006011377304225300217330ustar00rootroot00000000000000 --- name: Branch Cleanup # This workflow is triggered on all closed pull requests. # However the script does not do anything if a merge was not performed. on: pull_request: types: [closed] env: NO_BRANCH_DELETED_EXIT_CODE: 0 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: build: runs-on: ubuntu-latest steps: - uses: jessfraz/branch-cleanup-action@master gocron-0.5.0/.github/workflows/go_test.yml000066400000000000000000000013011377304225300205700ustar00rootroot00000000000000on: push: branches: - master pull_request: branches: - master name: Go Test jobs: test: strategy: matrix: go-version: - 1.14 - 1.15 runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v1 - name: fmt run: make check-fmt - name: lint run: | go get golang.org/x/lint/golint $(go list -f {{.Target}} golang.org/x/lint/golint) -set_exit_status ./... - name: vet run: make vet - name: test run: make test gocron-0.5.0/.gitignore000066400000000000000000000004361377304225300150040ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test # IDE project files .idea gocron-0.5.0/CONTRIBUTING.md000066400000000000000000000033431377304225300152450ustar00rootroot00000000000000# Contributing to gocron Thank you for coming to contribute to gocron! We welcome new ideas, PRs and general feedback. ## Reporting Bugs If you find a bug then please let the project know by opening an issue after doing the following: - Do a quick search of the existing issues to make sure the bug isn't already reported - Try and make a minimal list of steps that can reliably reproduce the bug you are experiencing - Collect as much information as you can to help identify what the issue is (project version, configuration files, etc) ## Suggesting Enhancements If you have a use case that you don't see a way to support yet, we would welcome the feedback in an issue. Before opening the issue, please consider: - Is this a common use case? - Is it simple to understand? You can help us out by doing the following before raising a new issue: - Check that the feature hasn't been requested already by searching existing issues - Try and reduce your enhancement into a single, concise and deliverable request, rather than a general idea - Explain your own use cases as the basis of the request ## Adding Features Pull requests are always welcome. However, before going through the trouble of implementing a change it's worth creating a bug or feature request issue. This allows us to discuss the changes and make sure they are a good fit for the project. Please always make sure a pull request has been: - Unit tested with `make test` - Linted with `make lint` - Vetted with `make vet` - Formatted with `make fmt` or validated with `make check-fmt` ## Writing Tests Tests should follow the [table driven test pattern](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go). See other tests in the code base for additional examples. gocron-0.5.0/LICENSE000066400000000000000000000024151377304225300140200ustar00rootroot00000000000000Copyright (c) 2014, 辣椒面 All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. gocron-0.5.0/Makefile000066400000000000000000000013341377304225300144520ustar00rootroot00000000000000.PHONY: fmt check-fmt lint vet test GO_PKGS := $(shell go list -f {{.Dir}} ./...) fmt: @go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {} check-fmt: @echo "Checking formatting..." @FMT="0"; \ for pkg in $(GO_PKGS); do \ OUTPUT=`gofmt -l $$pkg/*.go`; \ if [ -n "$$OUTPUT" ]; then \ echo "$$OUTPUT"; \ FMT="1"; \ fi; \ done ; \ if [ "$$FMT" -eq "1" ]; then \ echo "Problem with formatting in files above."; \ exit 1; \ else \ echo "Success - way to run gofmt!"; \ fi lint: # Add -set_exit_status=true when/if we want to enforce the linter rules @golint -min_confidence 0.8 -set_exit_status $(GO_PKGS) vet: @go vet $(GO_FLAGS) $(GO_PKGS) test: @go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS) gocron-0.5.0/README.md000066400000000000000000000110321377304225300142650ustar00rootroot00000000000000# goCron: A Golang Job Scheduling Package. [![CI State](https://github.com/go-co-op/gocron/workflows/Go%20Test/badge.svg)](https://github.com/go-co-op/gocron/actions?query=workflow%3A"Go+Test") ![Go Report Card](https://goreportcard.com/badge/github.com/go-co-op/gocron) [![Go Doc](https://godoc.org/github.com/go-co-op/gocron?status.svg)](https://godoc.org/github.com/go-co-op/gocron) goCron is a Golang job scheduling package which lets you run Go functions periodically at pre-determined interval using a simple, human-friendly syntax. goCron is a Golang implementation of Ruby module [clockwork](https://github.com/tomykaira/clockwork) and Python job scheduling package [schedule](https://github.com/dbader/schedule). See also these two great articles: - [Rethinking Cron](http://adam.herokuapp.com/past/2010/4/13/rethinking_cron/) - [Replace Cron with Clockwork](http://adam.herokuapp.com/past/2010/6/30/replace_cron_with_clockwork/) If you want to chat, you can find us at Slack! [](https://gophers.slack.com/archives/CQ7T0T1FW) ## Examples: ```go package main import ( "fmt" "time" "github.com/go-co-op/gocron" ) func task() { fmt.Println("I am running task.") } func taskWithParams(a int, b string) { fmt.Println(a, b) } func main() { // defines a new scheduler that schedules and runs jobs s1 := gocron.NewScheduler(time.UTC) s1.Every(3).Seconds().Do(task) // scheduler starts running jobs and current thread continues to execute s1.StartAsync() // Do jobs without params s2 := gocron.NewScheduler(time.UTC) s2.Every(1).Second().Do(task) s2.Every(2).Seconds().Do(task) s2.Every(1).Minute().Do(task) s2.Every(2).Minutes().Do(task) s2.Every(1).Hour().Do(task) s2.Every(2).Hours().Do(task) s2.Every(1).Day().Do(task) s2.Every(2).Days().Do(task) s2.Every(1).Week().Do(task) s2.Every(2).Weeks().Do(task) s2.Every(1).Month(time.Now().Day()).Do(task) s2.Every(2).Months(15).Do(task) // check for errors _, err := s2.Every(1).Day().At("bad-time").Do(task) if err != nil { log.Fatalf("error creating job: %v", err) } // Do jobs with params s2.Every(1).Second().Do(taskWithParams, 1, "hello") // Do Jobs with tags // initialize tag tag1 := []string{"tag1"} tag2 := []string{"tag2"} s2.Every(1).Week().SetTag(tag1).Do(task) s2.Every(1).Week().SetTag(tag2).Do(task) // Removing Job Based on Tag s2.RemoveJobByTag("tag1") // Remove a Job after its last execution j, _ := s2.Every(1).StartAt(time.Now().Add(30*time.Second)).Do(task) j.LimitRunsTo(1) j.RemoveAfterLastRun() // Do jobs on specific weekday s2.Every(1).Monday().Do(task) s2.Every(1).Thursday().Do(task) // Do a job at a specific time - 'hour:min:sec' - seconds optional s2.Every(1).Day().At("10:30").Do(task) s2.Every(1).Monday().At("18:30").Do(task) s2.Every(1).Tuesday().At("18:30:59").Do(task) s2.Every(1).Wednesday().At("1:01").Do(task) // Begin job at a specific date/time. t := time.Date(2019, time.November, 10, 15, 0, 0, 0, time.UTC) s2.Every(1).Hour().StartAt(t).Do(task) // Delay start of job s2.Every(1).Hour().StartAt(time.Now().Add(time.Duration(1 * time.Hour)).Do(task) // NextRun gets the next running time _, time := s2.NextRun() fmt.Println(time) // Remove a specific job s2.Remove(task) // Clear all scheduled jobs s2.Clear() // stop our first scheduler (it still exists but doesn't run anymore) s1.Stop() // executes the scheduler and blocks current thread s2.StartBlocking() // this line is never reached } ``` ### FAQ * Q: I'm running multiple pods on a distributed environment. How can I make a job not run once per pod causing duplication? * A: We recommend using your own lock solution within the jobs themselves (you could use [Redis](https://redis.io/topics/distlock), for example) --- Looking to contribute? Try to follow these guidelines: * Use issues for everything * For a small change, just send a PR! * For bigger changes, please open an issue for discussion before sending a PR. * PRs should have: tests, documentation and examples (if it makes sense) * You can also contribute by: * Reporting issues * Suggesting new features or enhancements * Improving/fixing documentation --- [Jetbrains](https://www.jetbrains.com/?from=gocron) supports this project with GoLand licenses. We appreciate their support for free and open source software! gocron-0.5.0/example_test.go000066400000000000000000000065771377304225300160510ustar00rootroot00000000000000package gocron_test import ( "fmt" "time" "github.com/go-co-op/gocron" ) var task = func() { fmt.Println("I am a task") } func ExampleScheduler_Location() { s := gocron.NewScheduler(time.UTC) fmt.Println(s.Location()) // Output: UTC } func ExampleScheduler_ChangeLocation() { s := gocron.NewScheduler(time.UTC) fmt.Println(s.Location()) location, err := time.LoadLocation("America/Los_Angeles") if err != nil { panic(err) } s.ChangeLocation(location) fmt.Println(s.Location()) // Output: // UTC // America/Los_Angeles } func ExampleScheduler_StartBlocking() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(3).Seconds().Do(task) s.StartBlocking() } func ExampleScheduler_StartAsync() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(3).Seconds().Do(task) s.StartAsync() } func ExampleScheduler_StartAt() { s := gocron.NewScheduler(time.UTC) specificTime := time.Date(2019, time.November, 10, 15, 0, 0, 0, time.UTC) _, _ = s.Every(1).Hour().StartAt(specificTime).Do(task) s.StartBlocking() } func ExampleScheduler_Stop() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Second().Do(task) s.StartAsync() s.Stop() } func ExampleScheduler_At() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().At("10:30").Do(task) _, _ = s.Every(1).Monday().At("10:30:01").Do(task) } func ExampleScheduler_RemoveJobByTag() { s := gocron.NewScheduler(time.UTC) tag1 := []string{"tag1"} tag2 := []string{"tag2"} _, _ = s.Every(1).Week().SetTag(tag1).Do(task) _, _ = s.Every(1).Week().SetTag(tag2).Do(task) s.StartAsync() _ = s.RemoveJobByTag("tag1") } func ExampleScheduler_NextRun() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().At("10:30").Do(task) s.StartAsync() _, time := s.NextRun() fmt.Println(time.Format("15:04")) // print only the hour and minute (hh:mm) // Output: 10:30 } func ExampleScheduler_Clear() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Second().Do(task) _, _ = s.Every(1).Minute().Do(task) _, _ = s.Every(1).Month(1).Do(task) fmt.Println(len(s.Jobs())) // Print the number of jobs before clearing s.Clear() // Clear all the jobs fmt.Println(len(s.Jobs())) // Print the number of jobs after clearing s.StartAsync() // Output: // 3 // 0 } func ExampleJob_ScheduledTime() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Day().At("10:30").Do(task) fmt.Println(job.ScheduledAtTime()) // Output: 10:30 } func ExampleJob_LimitRunsTo() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) job.LimitRunsTo(2) s.StartAsync() } func ExampleJob_LastRun() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) go func() { for { fmt.Println("Last run", job.LastRun()) time.Sleep(time.Second) } }() <-s.StartAsync() } func ExampleJob_NextRun() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) go func() { for { fmt.Println("Next run", job.NextRun()) time.Sleep(time.Second) } }() <-s.StartAsync() } func ExampleJob_RunCount() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) go func() { for { fmt.Println("Run count", job.RunCount()) time.Sleep(time.Second) } }() <-s.StartAsync() } func ExampleJob_RemoveAfterLastRun() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) job.LimitRunsTo(1) job.RemoveAfterLastRun() s.StartAsync() } gocron-0.5.0/go.mod000066400000000000000000000001271377304225300141170ustar00rootroot00000000000000module github.com/go-co-op/gocron go 1.13 require github.com/stretchr/testify v1.4.0 gocron-0.5.0/go.sum000066400000000000000000000017101377304225300141430ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gocron-0.5.0/gocron.go000066400000000000000000000046561377304225300146420ustar00rootroot00000000000000// Package gocron : A Golang Job Scheduling Package. // // An in-process scheduler for periodic jobs that uses the builder pattern // for configuration. Schedule lets you run Golang functions periodically // at pre-determined intervals using a simple, human-friendly syntax. // // Copyright 2014 Jason Lyu. jasonlvhit@gmail.com . // All rights reserved. // Use of this source code is governed by a BSD-style . // license that can be found in the LICENSE file. package gocron import ( "crypto/sha256" "errors" "fmt" "reflect" "regexp" "runtime" "time" ) // Error declarations for gocron related errors var ( ErrTimeFormat = errors.New("time format error") ErrParamsNotAdapted = errors.New("the number of params is not adapted") ErrNotAFunction = errors.New("only functions can be schedule into the job queue") ErrPeriodNotSpecified = errors.New("unspecified job period") ErrNotScheduledWeekday = errors.New("job not scheduled weekly on a weekday") ErrJobNotFoundWithTag = errors.New("no jobs found with given tag") ErrUnsupportedTimeFormat = errors.New("the given time format is not supported") ) // regex patterns for supported time formats var ( timeWithSeconds = regexp.MustCompile(`(?m)^\d{1,2}:\d\d:\d\d$`) timeWithoutSeconds = regexp.MustCompile(`(?m)^\d{1,2}:\d\d$`) ) type timeUnit int const ( seconds timeUnit = iota + 1 minutes hours days weeks months ) func callJobFuncWithParams(jobFunc interface{}, params []interface{}) ([]reflect.Value, error) { f := reflect.ValueOf(jobFunc) if len(params) != f.Type().NumIn() { return nil, ErrParamsNotAdapted } in := make([]reflect.Value, len(params)) for k, param := range params { in[k] = reflect.ValueOf(param) } return f.Call(in), nil } func getFunctionName(fn interface{}) string { return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() } func getFunctionKey(funcName string) string { h := sha256.New() h.Write([]byte(funcName)) return fmt.Sprintf("%x", h.Sum(nil)) } func parseTime(t string) (hour, min, sec int, err error) { var timeLayout string switch { case timeWithSeconds.Match([]byte(t)): timeLayout = "15:04:05" case timeWithoutSeconds.Match([]byte(t)): timeLayout = "15:04" default: return 0, 0, 0, ErrUnsupportedTimeFormat } parsedTime, err := time.Parse(timeLayout, t) if err != nil { return 0, 0, 0, ErrUnsupportedTimeFormat } return parsedTime.Hour(), parsedTime.Minute(), parsedTime.Second(), nil } gocron-0.5.0/gocron_test.go000066400000000000000000000035151377304225300156720ustar00rootroot00000000000000package gocron import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseTime(t *testing.T) { tests := []struct { name string args string wantHour int wantMin int wantSec int wantErr bool }{ { name: "normal", args: "16:18", wantHour: 16, wantMin: 18, wantErr: false, }, { name: "normal - no leading hour zeros", args: "6:08", wantHour: 6, wantMin: 8, wantErr: false, }, { name: "normal_with_second", args: "06:18:01", wantHour: 6, wantMin: 18, wantSec: 1, wantErr: false, }, { name: "normal_with_second - no leading hour zeros", args: "6:08:01", wantHour: 6, wantMin: 8, wantSec: 1, wantErr: false, }, { name: "not_a_number", args: "e:18", wantHour: 0, wantMin: 0, wantErr: true, }, { name: "out_of_range_hour", args: "25:18", wantHour: 0, wantMin: 0, wantErr: true, }, { name: "out_of_range_minute", args: "23:60", wantHour: 0, wantMin: 0, wantErr: true, }, { name: "wrong_format", args: "19:18:17:17", wantHour: 0, wantMin: 0, wantErr: true, }, { name: "wrong_minute", args: "19:1e", wantHour: 19, wantMin: 0, wantErr: true, }, { name: "wrong_hour", args: "1e:10", wantHour: 11, wantMin: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotHour, gotMin, gotSec, err := parseTime(tt.args) if tt.wantErr { assert.NotEqual(t, nil, err, tt.args) return } assert.Equal(t, tt.wantHour, gotHour, tt.args) assert.Equal(t, tt.wantMin, gotMin, tt.args) if tt.wantSec != 0 { assert.Equal(t, tt.wantSec, gotSec) } else { assert.Zero(t, gotSec) } }) } } gocron-0.5.0/job.go000066400000000000000000000133611377304225300141160ustar00rootroot00000000000000package gocron import ( "fmt" "sync" "time" ) type jobInterval uint64 // Job struct stores the information necessary to run a Job type Job struct { sync.RWMutex interval jobInterval // pause interval * unit between runs unit timeUnit // time units, ,e.g. 'minutes', 'hours'... startsImmediately bool // if the Job should run upon scheduler start jobFunc string // the Job jobFunc to run, func[jobFunc] atTime time.Duration // optional time at which this Job runs err error // error related to Job lastRun time.Time // datetime of last run nextRun time.Time // datetime of next run scheduledWeekday *time.Weekday // Specific day of the week to start on dayOfTheMonth int // Specific day of the month to run the job funcs map[string]interface{} // Map for the function task store fparams map[string][]interface{} // Map for function and params of function tags []string // allow the user to tag Jobs with certain labels runConfig runConfig // configuration for how many times to run the job runCount int // number of times the job ran timer *time.Timer } type runConfig struct { finiteRuns bool maxRuns int removeAfterLastRun bool } // NewJob creates a new Job with the provided interval func NewJob(interval uint64) *Job { return &Job{ interval: jobInterval(interval), lastRun: time.Time{}, nextRun: time.Time{}, funcs: make(map[string]interface{}), fparams: make(map[string][]interface{}), tags: []string{}, startsImmediately: true, } } // Run the Job and immediately reschedule it func (j *Job) run() { j.Lock() defer j.Unlock() j.runCount++ go callJobFuncWithParams(j.funcs[j.jobFunc], j.fparams[j.jobFunc]) } func (j *Job) neverRan() bool { j.RLock() defer j.RUnlock() return j.lastRun.IsZero() } func (j *Job) getStartsImmediately() bool { j.RLock() defer j.RUnlock() return j.startsImmediately } func (j *Job) setStartsImmediately(b bool) { j.Lock() defer j.Unlock() j.startsImmediately = b } func (j *Job) getTimer() *time.Timer { j.RLock() defer j.RUnlock() return j.timer } func (j *Job) setTimer(t *time.Timer) { j.Lock() defer j.Unlock() j.timer = t } func (j *Job) getAtTime() time.Duration { j.RLock() defer j.RUnlock() return j.atTime } func (j *Job) setAtTime(t time.Duration) { j.Lock() defer j.Unlock() j.atTime = t } // Err returns an error if one occurred while creating the Job func (j *Job) Err() error { j.RLock() defer j.RUnlock() return j.err } // Tag allows you to add arbitrary labels to a Job that do not // impact the functionality of the Job func (j *Job) Tag(t string, others ...string) { j.Lock() defer j.Unlock() j.tags = append(j.tags, t) for _, tag := range others { j.tags = append(j.tags, tag) } } // Untag removes a tag from a Job func (j *Job) Untag(t string) { j.Lock() defer j.Unlock() var newTags []string for _, tag := range j.tags { if t != tag { newTags = append(newTags, tag) } } j.tags = newTags } // Tags returns the tags attached to the Job func (j *Job) Tags() []string { j.RLock() defer j.RUnlock() return j.tags } // ScheduledTime returns the time of the Job's next scheduled run func (j *Job) ScheduledTime() time.Time { j.RLock() defer j.RUnlock() return j.nextRun } // ScheduledAtTime returns the specific time of day the Job will run at func (j *Job) ScheduledAtTime() string { j.RLock() defer j.RUnlock() return fmt.Sprintf("%d:%d", j.atTime/time.Hour, (j.atTime%time.Hour)/time.Minute) } // Weekday returns which day of the week the Job will run on and // will return an error if the Job is not scheduled weekly func (j *Job) Weekday() (time.Weekday, error) { j.RLock() defer j.RUnlock() if j.scheduledWeekday == nil { return time.Sunday, ErrNotScheduledWeekday } return *j.scheduledWeekday, nil } // LimitRunsTo limits the number of executions of this // job to n. However, the job will still remain in the // scheduler func (j *Job) LimitRunsTo(n int) { j.Lock() defer j.Unlock() j.runConfig = runConfig{ finiteRuns: true, maxRuns: n, } } // shouldRun evaluates if this job should run again // based on the runConfig func (j *Job) shouldRun() bool { j.RLock() defer j.RUnlock() return !j.runConfig.finiteRuns || j.runCount < j.runConfig.maxRuns } // LastRun returns the time the job was run last func (j *Job) LastRun() time.Time { j.RLock() defer j.RUnlock() return j.lastRun } func (j *Job) setLastRun(t time.Time) { j.Lock() defer j.Unlock() j.lastRun = t } // NextRun returns the time the job will run next func (j *Job) NextRun() time.Time { j.RLock() defer j.RUnlock() return j.nextRun } func (j *Job) setNextRun(t time.Time) { j.Lock() defer j.Unlock() j.nextRun = t } // RunCount returns the number of time the job ran so far func (j *Job) RunCount() int { j.RLock() defer j.RUnlock() return j.runCount } func (j *Job) setRunCount(i int) { j.Lock() defer j.Unlock() j.runCount = i } // RemoveAfterLastRun update the job in order to remove the job after its last exec func (j *Job) RemoveAfterLastRun() *Job { j.Lock() defer j.Unlock() j.runConfig.removeAfterLastRun = true return j } func (j *Job) getFiniteRuns() bool { j.RLock() defer j.RUnlock() return j.runConfig.finiteRuns } func (j *Job) getMaxRuns() int { j.RLock() defer j.RUnlock() return j.runConfig.maxRuns } func (j *Job) getRemoveAfterLastRun() bool { j.RLock() defer j.RUnlock() return j.runConfig.removeAfterLastRun } gocron-0.5.0/job_test.go000066400000000000000000000057411377304225300151600ustar00rootroot00000000000000package gocron import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestTags(t *testing.T) { j, _ := NewScheduler(time.UTC).Every(1).Minute().Do(task) j.Tag("some") j.Tag("tag") j.Tag("more") j.Tag("tags") assert.ElementsMatch(t, j.Tags(), []string{"tags", "tag", "more", "some"}) j.Untag("more") assert.ElementsMatch(t, j.Tags(), []string{"tags", "tag", "some"}) } func TestGetScheduledTime(t *testing.T) { j, _ := NewScheduler(time.UTC).Every(1).Minute().At("10:30").Do(task) assert.Equal(t, "10:30", j.ScheduledAtTime()) } func TestGetWeekday(t *testing.T) { s := NewScheduler(time.UTC) wednesday := time.Wednesday weedayJob, _ := s.Every(1).Weekday(wednesday).Do(task) nonWeekdayJob, _ := s.Every(1).Minute().Do(task) testCases := []struct { desc string job *Job expectedWeekday time.Weekday expectedError error }{ {"success", weedayJob, wednesday, nil}, {"fail - not set for weekday", nonWeekdayJob, time.Sunday, ErrNotScheduledWeekday}, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { weekday, err := tc.job.Weekday() if tc.expectedError != nil { assert.Error(t, tc.expectedError, err) } else { assert.Equal(t, tc.expectedWeekday, weekday) assert.Nil(t, err) } }) } } func TestJob_shouldRunAgain(t *testing.T) { tests := []struct { name string runConfig runConfig runCount int want bool }{ { name: "should run again (infinite)", runConfig: runConfig{finiteRuns: false}, want: true, }, { name: "should run again (finite)", runConfig: runConfig{finiteRuns: true, maxRuns: 2}, runCount: 1, want: true, }, { name: "shouldn't run again #1", runConfig: runConfig{finiteRuns: true, maxRuns: 2}, runCount: 2, want: false, }, { name: "shouldn't run again #2", runConfig: runConfig{finiteRuns: true, maxRuns: 2}, runCount: 4, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { j := &Job{ runConfig: tt.runConfig, runCount: tt.runCount, } if got := j.shouldRun(); got != tt.want { t.Errorf("Job.shouldRunAgain() = %v, want %v", got, tt.want) } }) } } func TestJob_LimitRunsTo(t *testing.T) { j, _ := NewScheduler(time.Local).Every(1).Second().Do(func() {}) j.LimitRunsTo(2) assert.Equal(t, j.shouldRun(), true, "Expecting it to run again") j.run() assert.Equal(t, j.shouldRun(), true, "Expecting it to run again") j.run() assert.Equal(t, j.shouldRun(), false, "Not expecting it to run again") } func TestJob_CommonExports(t *testing.T) { s := NewScheduler(time.Local) j, _ := s.Every(1).Second().Do(func() {}) assert.Equal(t, 0, j.RunCount()) assert.True(t, j.LastRun().IsZero()) assert.True(t, j.NextRun().IsZero()) s.StartAsync() assert.False(t, j.NextRun().IsZero()) j.runCount = 5 assert.Equal(t, 5, j.RunCount()) lastRun := time.Now() j.lastRun = lastRun assert.Equal(t, lastRun, j.LastRun()) } gocron-0.5.0/scheduler.go000066400000000000000000000374231377304225300153270ustar00rootroot00000000000000package gocron import ( "math" "reflect" "sort" "strings" "sync" "time" ) // Scheduler struct stores a list of Jobs and the location of time Scheduler // Scheduler implements the sort.Interface{} for sorting Jobs, by the time of nextRun type Scheduler struct { jobsMutex sync.RWMutex jobs []*Job locationMutex sync.RWMutex location *time.Location runningMutex sync.RWMutex running bool // represents if the scheduler is running at the moment or not stopChan chan struct{} // signal to stop scheduling time timeWrapper // wrapper around time.Time } // NewScheduler creates a new Scheduler func NewScheduler(loc *time.Location) *Scheduler { return &Scheduler{ jobs: make([]*Job, 0), location: loc, running: false, stopChan: make(chan struct{}, 1), time: &trueTime{}, } } // StartBlocking starts all jobs and blocks the current thread func (s *Scheduler) StartBlocking() { <-s.StartAsync() } // StartAsync starts all jobs without blocking the current thread func (s *Scheduler) StartAsync() chan struct{} { if s.IsRunning() { return s.stopChan } s.start() go func() { <-s.stopChan s.setRunning(false) return }() return s.stopChan } //start starts the scheduler, scheduling and running jobs func (s *Scheduler) start() { s.setRunning(true) s.runJobs(s.Jobs()) } func (s *Scheduler) runJobs(jobs []*Job) { for _, j := range jobs { if j.getStartsImmediately() { s.run(j) j.setStartsImmediately(false) } if !j.shouldRun() { if j.getRemoveAfterLastRun() { // TODO: this method seems unnecessary as we could always remove after the run cout has expired. Maybe remove this in the future? s.RemoveByReference(j) } continue } s.scheduleNextRun(j) } } func (s *Scheduler) setRunning(b bool) { s.runningMutex.Lock() defer s.runningMutex.Unlock() s.running = b } // IsRunning returns true if the scheduler is running func (s *Scheduler) IsRunning() bool { s.runningMutex.RLock() defer s.runningMutex.RUnlock() return s.running } // Jobs returns the list of Jobs from the Scheduler func (s *Scheduler) Jobs() []*Job { s.jobsMutex.RLock() defer s.jobsMutex.RUnlock() return s.jobs } func (s *Scheduler) setJobs(jobs []*Job) { s.jobsMutex.Lock() defer s.jobsMutex.Unlock() s.jobs = jobs } // Len returns the number of Jobs in the Scheduler - implemented for sort func (s *Scheduler) Len() int { s.jobsMutex.RLock() defer s.jobsMutex.RUnlock() return len(s.jobs) } // Swap func (s *Scheduler) Swap(i, j int) { s.jobsMutex.Lock() defer s.jobsMutex.Unlock() s.jobs[i], s.jobs[j] = s.jobs[j], s.jobs[i] } func (s *Scheduler) Less(i, j int) bool { return s.Jobs()[j].NextRun().Unix() >= s.Jobs()[i].NextRun().Unix() } // ChangeLocation changes the default time location func (s *Scheduler) ChangeLocation(newLocation *time.Location) { s.locationMutex.Lock() defer s.locationMutex.Unlock() s.location = newLocation } // Location provides the current location set on the scheduler func (s *Scheduler) Location() *time.Location { s.locationMutex.RLock() defer s.locationMutex.RUnlock() return s.location } // scheduleNextRun Compute the instant when this Job should run next func (s *Scheduler) scheduleNextRun(job *Job) { now := s.now() lastRun := job.LastRun() // job can be scheduled with .StartAt() if job.neverRan() { if !job.NextRun().IsZero() { return // scheduled for future run and should skip scheduling } lastRun = now } durationToNextRun := s.durationToNextRun(lastRun, job) job.setNextRun(lastRun.Add(durationToNextRun)) job.setTimer(time.AfterFunc(durationToNextRun, func() { s.run(job) s.scheduleNextRun(job) })) } func (s *Scheduler) durationToNextRun(t time.Time, job *Job) time.Duration { var duration time.Duration switch job.unit { case seconds, minutes, hours: duration = s.calculateDuration(job) case days: duration = s.calculateDays(job, t) case weeks: if job.scheduledWeekday != nil { // weekday selected, Every().Monday(), for example duration = s.calculateWeekday(job, t) } else { duration = s.calculateWeeks(job, t) } case months: duration = s.calculateMonths(job, t) } return duration } func (s *Scheduler) getJobLastRun(job *Job) time.Time { if job.neverRan() { return s.time.Now(s.Location()) } return job.LastRun() } func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) time.Duration { lastRunRoundedMidnight := s.roundToMidnight(lastRun) if job.dayOfTheMonth > 0 { // calculate days to j.dayOfTheMonth jobDay := time.Date(lastRun.Year(), lastRun.Month(), job.dayOfTheMonth, 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) daysDifference := int(math.Abs(lastRun.Sub(jobDay).Hours()) / 24) nextRun := s.roundToMidnight(lastRun).Add(job.getAtTime()) if jobDay.Before(lastRun) { // shouldn't run this month; schedule for next interval minus day difference nextRun = nextRun.AddDate(0, int(job.interval), -daysDifference) } else { if job.interval == 1 { // every month counts current month nextRun = nextRun.AddDate(0, int(job.interval)-1, daysDifference) } else { // should run next month interval nextRun = nextRun.AddDate(0, int(job.interval), daysDifference) } } return s.until(lastRunRoundedMidnight, nextRun) } nextRun := lastRunRoundedMidnight.Add(job.getAtTime()).AddDate(0, int(job.interval), 0) return s.until(lastRunRoundedMidnight, nextRun) } func (s *Scheduler) calculateWeekday(job *Job, lastRun time.Time) time.Duration { daysToWeekday := remainingDaysToWeekday(lastRun.Weekday(), *job.scheduledWeekday) totalDaysDifference := s.calculateTotalDaysDifference(lastRun, daysToWeekday, job) nextRun := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference) return s.until(lastRun, nextRun) } func (s *Scheduler) calculateWeeks(job *Job, lastRun time.Time) time.Duration { totalDaysDifference := int(job.interval) * 7 nextRun := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference) return s.until(lastRun, nextRun) } func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekday int, job *Job) int { if job.interval > 1 { // every N weeks counts rest of this week and full N-1 weeks return daysToWeekday + int(job.interval-1)*7 } if daysToWeekday == 0 { // today, at future time or already passed lastRunAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) if lastRun.Before(lastRunAtTime) || lastRun.Equal(lastRunAtTime) { return 0 } return 7 } return daysToWeekday } func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) time.Duration { if job.interval == 1 { lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) { return s.until(lastRun, s.roundToMidnight(lastRun).Add(job.getAtTime())) } } nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, int(job.interval)).In(s.Location()) return s.until(lastRun, nextRunAtTime) } func (s *Scheduler) until(from time.Time, until time.Time) time.Duration { return until.Sub(from) } func shouldRunToday(lastRun time.Time, atTime time.Time) bool { return lastRun.Before(atTime) } func (s *Scheduler) calculateDuration(job *Job) time.Duration { lastRun := job.LastRun() if job.neverRan() && shouldRunAtSpecificTime(job) { // ugly. in order to avoid this we could prohibit setting .At() and allowing only .StartAt() when dealing with Duration types atTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) if lastRun.Before(atTime) || lastRun.Equal(atTime) { return time.Until(s.roundToMidnight(lastRun).Add(job.getAtTime())) } } interval := job.interval switch job.unit { case seconds: return time.Duration(interval) * time.Second case minutes: return time.Duration(interval) * time.Minute default: return time.Duration(interval) * time.Hour } } func shouldRunAtSpecificTime(job *Job) bool { return job.getAtTime() != 0 } func remainingDaysToWeekday(from time.Weekday, to time.Weekday) int { daysUntilScheduledDay := int(to) - int(from) if daysUntilScheduledDay < 0 { daysUntilScheduledDay += 7 } return daysUntilScheduledDay } // roundToMidnight truncates time to midnight func (s *Scheduler) roundToMidnight(t time.Time) time.Time { return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.Location()) } // NextRun datetime when the next Job should run. func (s *Scheduler) NextRun() (*Job, time.Time) { if len(s.Jobs()) <= 0 { return nil, s.time.Now(s.Location()) } sort.Sort(s) return s.Jobs()[0], s.Jobs()[0].NextRun() } // Every schedules a new periodic Job with interval func (s *Scheduler) Every(interval uint64) *Scheduler { job := NewJob(interval) s.setJobs(append(s.Jobs(), job)) return s } func (s *Scheduler) run(job *Job) { if !s.IsRunning() { return } job.setLastRun(s.now()) job.run() } // RunAll run all Jobs regardless if they are scheduled to run or not func (s *Scheduler) RunAll() { s.RunAllWithDelay(0) } // RunAllWithDelay runs all Jobs with delay seconds func (s *Scheduler) RunAllWithDelay(d int) { for _, job := range s.Jobs() { s.run(job) s.time.Sleep(time.Duration(d) * time.Second) } } // Remove specific Job j by function func (s *Scheduler) Remove(j interface{}) { s.removeByCondition(func(someJob *Job) bool { return someJob.jobFunc == getFunctionName(j) }) } // RemoveByReference removes specific Job j by reference func (s *Scheduler) RemoveByReference(j *Job) { s.removeByCondition(func(someJob *Job) bool { return someJob == j }) } func (s *Scheduler) removeByCondition(shouldRemove func(*Job) bool) { retainedJobs := make([]*Job, 0) for _, job := range s.Jobs() { if !shouldRemove(job) { retainedJobs = append(retainedJobs, job) } } s.setJobs(retainedJobs) } // RemoveJobByTag will Remove Jobs by Tag func (s *Scheduler) RemoveJobByTag(tag string) error { jobindex, err := s.findJobsIndexByTag(tag) if err != nil { return err } // Remove job if jobindex is valid s.setJobs(removeAtIndex(s.jobs, jobindex)) return nil } // Find first job index by given string func (s *Scheduler) findJobsIndexByTag(tag string) (int, error) { for i, job := range s.Jobs() { if strings.Contains(strings.Join(job.Tags(), " "), tag) { return i, nil } } return -1, ErrJobNotFoundWithTag } func removeAtIndex(jobs []*Job, i int) []*Job { if i == len(jobs)-1 { return jobs[:i] } jobs = append(jobs[:i], jobs[i+1:]...) return jobs } // Scheduled checks if specific Job j was already added func (s *Scheduler) Scheduled(j interface{}) bool { for _, job := range s.Jobs() { if job.jobFunc == getFunctionName(j) { return true } } return false } // Clear clear all Jobs from this scheduler func (s *Scheduler) Clear() { s.setJobs(make([]*Job, 0)) } // Stop stops the scheduler. This is a no-op if the scheduler is already stopped . func (s *Scheduler) Stop() { if s.IsRunning() { s.stop() } } func (s *Scheduler) stop() { s.setRunning(false) s.stopChan <- struct{}{} } // Do specifies the jobFunc that should be called every time the Job runs func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error) { j := s.getCurrentJob() if j.err != nil { // delete the job from the scheduler as this job // cannot be executed s.RemoveByReference(j) return nil, j.err } typ := reflect.TypeOf(jobFun) if typ.Kind() != reflect.Func { // delete the job for the same reason as above s.RemoveByReference(j) return nil, ErrNotAFunction } fname := getFunctionName(jobFun) j.funcs[fname] = jobFun j.fparams[fname] = params j.jobFunc = fname // we should not schedule if not running since we cant foresee how long it will take for the scheduler to start if s.IsRunning() { s.scheduleNextRun(j) } return j, nil } // At schedules the Job at a specific time of day in the form "HH:MM:SS" or "HH:MM" func (s *Scheduler) At(t string) *Scheduler { j := s.getCurrentJob() hour, min, sec, err := parseTime(t) if err != nil { j.err = ErrTimeFormat return s } // save atTime start as duration from midnight j.setAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second) j.startsImmediately = false return s } // SetTag will add tag when creating a job func (s *Scheduler) SetTag(t []string) *Scheduler { job := s.getCurrentJob() job.tags = t return s } // StartAt schedules the next run of the Job func (s *Scheduler) StartAt(t time.Time) *Scheduler { job := s.getCurrentJob() job.setNextRun(t) job.startsImmediately = false return s } // shouldRun returns true if the Job should be run now func (s *Scheduler) shouldRun(j *Job) bool { // option remove the job's in the scheduler after its last execution if j.getRemoveAfterLastRun() && (j.getMaxRuns()-j.RunCount()) == 1 { s.RemoveByReference(j) } return j.shouldRun() && s.time.Now(s.Location()).Unix() >= j.NextRun().Unix() } // setUnit sets the unit type func (s *Scheduler) setUnit(unit timeUnit) { currentJob := s.getCurrentJob() currentJob.unit = unit } // Second sets the unit with seconds func (s *Scheduler) Second() *Scheduler { return s.Seconds() } // Seconds sets the unit with seconds func (s *Scheduler) Seconds() *Scheduler { s.setUnit(seconds) return s } // Minute sets the unit with minutes func (s *Scheduler) Minute() *Scheduler { return s.Minutes() } // Minutes sets the unit with minutes func (s *Scheduler) Minutes() *Scheduler { s.setUnit(minutes) return s } // Hour sets the unit with hours func (s *Scheduler) Hour() *Scheduler { return s.Hours() } // Hours sets the unit with hours func (s *Scheduler) Hours() *Scheduler { s.setUnit(hours) return s } // Day sets the unit with days func (s *Scheduler) Day() *Scheduler { s.setUnit(days) return s } // Days set the unit with days func (s *Scheduler) Days() *Scheduler { s.setUnit(days) return s } // Week sets the unit with weeks func (s *Scheduler) Week() *Scheduler { s.setUnit(weeks) return s } // Weeks sets the unit with weeks func (s *Scheduler) Weeks() *Scheduler { s.setUnit(weeks) return s } // Month sets the unit with months func (s *Scheduler) Month(dayOfTheMonth int) *Scheduler { return s.Months(dayOfTheMonth) } // Months sets the unit with months func (s *Scheduler) Months(dayOfTheMonth int) *Scheduler { job := s.getCurrentJob() job.dayOfTheMonth = dayOfTheMonth job.startsImmediately = false s.setUnit(months) return s } // NOTE: If the dayOfTheMonth for the above two functions is // more than the number of days in that month, the extra day(s) // spill over to the next month. Similarly, if it's less than 0, // it will go back to the month before // Weekday sets the start with a specific weekday weekday func (s *Scheduler) Weekday(startDay time.Weekday) *Scheduler { job := s.getCurrentJob() job.scheduledWeekday = &startDay job.startsImmediately = false s.setUnit(weeks) return s } // Monday sets the start day as Monday func (s *Scheduler) Monday() *Scheduler { return s.Weekday(time.Monday) } // Tuesday sets the start day as Tuesday func (s *Scheduler) Tuesday() *Scheduler { return s.Weekday(time.Tuesday) } // Wednesday sets the start day as Wednesday func (s *Scheduler) Wednesday() *Scheduler { return s.Weekday(time.Wednesday) } // Thursday sets the start day as Thursday func (s *Scheduler) Thursday() *Scheduler { return s.Weekday(time.Thursday) } // Friday sets the start day as Friday func (s *Scheduler) Friday() *Scheduler { return s.Weekday(time.Friday) } // Saturday sets the start day as Saturday func (s *Scheduler) Saturday() *Scheduler { return s.Weekday(time.Saturday) } // Sunday sets the start day as Sunday func (s *Scheduler) Sunday() *Scheduler { return s.Weekday(time.Sunday) } func (s *Scheduler) getCurrentJob() *Job { return s.Jobs()[len(s.jobs)-1] } func (s *Scheduler) now() time.Time { return s.time.Now(s.Location()) } gocron-0.5.0/scheduler_test.go000066400000000000000000000560201377304225300163600ustar00rootroot00000000000000package gocron import ( "fmt" "sync" "testing" "time" "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" ) type fakeTime struct { onNow func(location *time.Location) time.Time } func (f fakeTime) Now(loc *time.Location) time.Time { return f.onNow(loc) } func (f fakeTime) Unix(i int64, i2 int64) time.Time { panic("implement me") } func (f fakeTime) Sleep(duration time.Duration) { panic("implement me") } func (f fakeTime) NewTicker(duration time.Duration) *time.Ticker { panic("implement me") } func task() { fmt.Println("I am a running job.") } func taskWithParams(a int, b string) { fmt.Println(a, b) } func TestImmediateExecution(t *testing.T) { sched := NewScheduler(time.UTC) semaphore := make(chan bool) sched.Every(1).Second().Do(func() { semaphore <- true }) sched.StartAsync() select { case <-time.After(1 * time.Second): t.Fatal("job did not run immediately") case <-semaphore: // test passed } } func TestExecutionSeconds(t *testing.T) { sched := NewScheduler(time.UTC) jobDone := make(chan bool) var ( executions []int64 interval uint64 = 2 expectedExecutions = 4 mu sync.RWMutex ) runTime := time.Duration(6 * time.Second) startTime := time.Now() sched.Every(interval).Seconds().Do(func() { mu.Lock() defer mu.Unlock() executions = append(executions, time.Now().UTC().Unix()) if time.Now().After(startTime.Add(runTime)) { jobDone <- true } }) stop := sched.StartAsync() <-jobDone // Wait job done close(stop) mu.RLock() defer mu.RUnlock() assert.Equal(t, expectedExecutions, len(executions), "did not run expected number of times") for i := 1; i < expectedExecutions; i++ { durationBetweenExecutions := executions[i] - executions[i-1] assert.Equal(t, int64(interval), durationBetweenExecutions, "duration between tasks does not correspond to expectations") } } func TestScheduled(t *testing.T) { n := NewScheduler(time.UTC) n.Every(1).Second().Do(task) if !n.Scheduled(task) { t.Fatal("Task was scheduled but function couldn't find it") } } func TestScheduledWithTag(t *testing.T) { sched := NewScheduler(time.UTC) customtag := []string{"mycustomtag"} sched.Every(1).Hour().SetTag(customtag).Do(task) if !sched.Scheduled(task) { t.Fatal("Task was scheduled but function couldn't find it") } } func TestAtFuture(t *testing.T) { t.Run("calls to .At() should parse time correctly", func(t *testing.T) { s := NewScheduler(time.UTC) now := time.Now().UTC() // Schedule to run in next minute nextMinuteTime := now.Add(1 * time.Minute) // fixme: test fails any hour at :59 startAt := fmt.Sprintf("%02d:%02d:%02d", nextMinuteTime.Hour(), nextMinuteTime.Minute(), nextMinuteTime.Second()) var hasRan bool dayJob, _ := s.Every(1).Day().At(startAt).Do(func() { hasRan = true }) s.start() // Check first run expectedStartTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Add(time.Minute).Minute(), now.Second(), 0, time.UTC) nextRun := dayJob.ScheduledTime() assert.Equal(t, expectedStartTime, nextRun) // Check next run's scheduled time nextRun = dayJob.ScheduledTime() assert.Equal(t, expectedStartTime, nextRun) assert.False(t, hasRan, "Day job was not expected to run as it was in the future") }) t.Run("error due to bad time format", func(t *testing.T) { s := NewScheduler(time.UTC) badTime := "0:0" _, err := s.Every(1).Day().At(badTime).Do(func() {}) assert.Error(t, err, "bad time format should not include jobs to the scheduler") assert.Zero(t, len(s.jobs)) }) } func schedulerForNextOrPreviousWeekdayEveryNTimes(weekday time.Weekday, next bool, n uint64, s *Scheduler) *Scheduler { switch weekday { case time.Monday: if next { s = s.Every(n).Tuesday() } else { s = s.Every(n).Sunday() } case time.Tuesday: if next { s = s.Every(n).Wednesday() } else { s = s.Every(n).Monday() } case time.Wednesday: if next { s = s.Every(n).Thursday() } else { s = s.Every(n).Tuesday() } case time.Thursday: if next { s = s.Every(n).Friday() } else { s = s.Every(n).Wednesday() } case time.Friday: if next { s = s.Every(n).Saturday() } else { s = s.Every(n).Thursday() } case time.Saturday: if next { s = s.Every(n).Sunday() } else { s = s.Every(n).Friday() } case time.Sunday: if next { s = s.Every(n).Monday() } else { s = s.Every(n).Saturday() } } return s } func TestWeekdayBeforeToday(t *testing.T) { now := time.Now().In(time.UTC) s := NewScheduler(time.UTC) s = schedulerForNextOrPreviousWeekdayEveryNTimes(now.Weekday(), false, 1, s) weekJob, _ := s.Do(task) s.scheduleNextRun(weekJob) sixDaysFromNow := now.AddDate(0, 0, 6) exp := time.Date(sixDaysFromNow.Year(), sixDaysFromNow.Month(), sixDaysFromNow.Day(), 0, 0, 0, 0, time.UTC) assert.Equal(t, exp, weekJob.nextRun) } func TestWeekdayAt(t *testing.T) { t.Run("asserts weekday scheduling starts at the current week", func(t *testing.T) { s := NewScheduler(time.UTC) now := time.Now().UTC() s = schedulerForNextOrPreviousWeekdayEveryNTimes(now.Weekday(), true, 1, s) weekdayJob, _ := s.Do(task) s.scheduleNextRun(weekdayJob) tomorrow := now.AddDate(0, 0, 1) exp := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, time.UTC) nextRun := weekdayJob.nextRun nextRunDate := time.Date(nextRun.Year(), nextRun.Month(), nextRun.Day(), 0, 0, 0, 0, time.UTC) assert.Equal(t, exp, nextRunDate) }) } func TestRemove(t *testing.T) { scheduler := NewScheduler(time.UTC) scheduler.Every(1).Minute().Do(task) scheduler.Every(1).Minute().Do(taskWithParams, 1, "hello") scheduler.Every(1).Minute().Do(task) assert.Equal(t, 3, scheduler.Len(), "Incorrect number of jobs") scheduler.Remove(task) assert.Equal(t, 1, scheduler.Len(), "Incorrect number of jobs after removing 2 job") scheduler.Remove(task) assert.Equal(t, 1, scheduler.Len(), "Incorrect number of jobs after removing non-existent job") } func TestRemoveByRef(t *testing.T) { scheduler := NewScheduler(time.UTC) job1, _ := scheduler.Every(1).Minute().Do(task) job2, _ := scheduler.Every(1).Minute().Do(taskWithParams, 1, "hello") assert.Equal(t, 2, scheduler.Len(), "Incorrect number of jobs") scheduler.RemoveByReference(job1) assert.ElementsMatch(t, []*Job{job2}, scheduler.Jobs()) } func TestRemoveByTag(t *testing.T) { scheduler := NewScheduler(time.UTC) // Creating 2 Jobs with Unique tags customtag1 := []string{"tag one"} customtag2 := []string{"tag two"} scheduler.Every(1).Minute().SetTag(customtag1).Do(taskWithParams, 1, "hello") // index 0 scheduler.Every(1).Minute().SetTag(customtag2).Do(taskWithParams, 2, "world") // index 1 assert.Equal(t, 2, scheduler.Len(), "Incorrect number of jobs") // check Jobs()[0] tags is equal with tag "tag one" (customtag1) assert.Equal(t, scheduler.Jobs()[0].Tags(), customtag1, "Job With Tag 'tag one' is removed from index 0") scheduler.RemoveJobByTag("tag one") assert.Equal(t, 1, scheduler.Len(), "Incorrect number of jobs after removing 1 job") // check Jobs()[0] tags is equal with tag "tag two" (customtag2) after removing "tag one" assert.Equal(t, scheduler.Jobs()[0].Tags(), customtag2, "Job With Tag 'tag two' is removed from index 0") // Removing Non Existent Job with "tag one" because already removed above (will not removing any jobs because tag not match) scheduler.RemoveJobByTag("tag one") assert.Equal(t, 1, scheduler.Len(), "Incorrect number of jobs after removing non-existent job") } func TestJobs(t *testing.T) { s := NewScheduler(time.UTC) s.Every(1).Minute().Do(task) s.Every(2).Minutes().Do(task) s.Every(3).Minutes().Do(task) s.Every(4).Minutes().Do(task) js := s.Jobs() assert.Len(t, js, 4) } func TestLen(t *testing.T) { s := NewScheduler(time.UTC) s.Every(1).Minute().Do(task) s.Every(2).Minutes().Do(task) s.Every(3).Minutes().Do(task) s.Every(4).Minutes().Do(task) l := s.Len() assert.Equal(t, l, 4) } func TestSwap(t *testing.T) { s := NewScheduler(time.UTC) s.Every(1).Minute().Do(task) s.Every(2).Minute().Do(task) jb := s.Jobs() var jobsBefore []*Job for _, p := range jb { jobsBefore = append(jobsBefore, p) } s.Swap(1, 0) jobsAfter := s.Jobs() assert.Equal(t, jobsBefore[0], jobsAfter[1]) assert.Equal(t, jobsBefore[1], jobsAfter[0]) } func TestLess(t *testing.T) { s := NewScheduler(time.UTC) s.Every(1).Minute().Do(task) s.Every(2).Minute().Do(task) assert.True(t, s.Less(0, 1)) } func TestSetLocation(t *testing.T) { s := NewScheduler(time.FixedZone("UTC-8", -8*60*60)) assert.Equal(t, time.FixedZone("UTC-8", -8*60*60), s.Location()) s.ChangeLocation(time.UTC) assert.Equal(t, time.UTC, s.Location()) } func TestClear(t *testing.T) { s := NewScheduler(time.UTC) s.Every(1).Minute().Do(task) s.Every(2).Minute().Do(task) assert.Equal(t, 2, s.Len()) s.Clear() assert.Equal(t, 0, s.Len()) } func TestSetUnit(t *testing.T) { testCases := []struct { desc string timeUnit timeUnit }{ {"seconds", seconds}, {"minutes", minutes}, {"hours", hours}, {"days", days}, {"weeks", weeks}, } for _, tc := range testCases { s := NewScheduler(time.UTC) t.Run(tc.desc, func(t *testing.T) { switch tc.timeUnit { case seconds: s.Every(2).Seconds().Do(task) case minutes: s.Every(2).Minutes().Do(task) case hours: s.Every(2).Hours().Do(task) case days: s.Every(2).Days().Do(task) case weeks: s.Every(2).Weeks().Do(task) } j := s.jobs[0] assert.Equal(t, tc.timeUnit, j.unit) }) } } func TestScheduler_Stop(t *testing.T) { t.Run("stops a running scheduler", func(t *testing.T) { s := NewScheduler(time.UTC) s.StartAsync() assert.True(t, s.IsRunning()) s.Stop() assert.False(t, s.IsRunning()) }) t.Run("stops a running scheduler through StartAsync chan", func(t *testing.T) { s := NewScheduler(time.UTC) c := s.StartAsync() assert.True(t, s.IsRunning()) close(c) time.Sleep(1 * time.Millisecond) // wait for stop goroutine to catch up assert.False(t, s.IsRunning()) }) t.Run("noop on stopped scheduler", func(t *testing.T) { s := NewScheduler(time.UTC) s.Stop() assert.False(t, s.IsRunning()) }) } func TestScheduler_StartAt(t *testing.T) { scheduler := NewScheduler(time.Local) now := time.Now() // With StartAt job, _ := scheduler.Every(3).Seconds().StartAt(now.Add(time.Second * 5)).Do(func() {}) assert.False(t, job.getStartsImmediately()) scheduler.start() assert.Equal(t, now.Add(time.Second*5), job.NextRun()) scheduler.stop() // Without StartAt job, _ = scheduler.Every(3).Seconds().Do(func() {}) assert.True(t, job.getStartsImmediately()) } func TestScheduler_CalculateNextRun(t *testing.T) { day := time.Hour * 24 januaryFirst2020At := func(hour, minute, second int) time.Time { return time.Date(2020, time.January, 1, hour, minute, second, 0, time.UTC) } mondayAt := func(hour, minute, second int) time.Time { return time.Date(2020, time.January, 6, hour, minute, second, 0, time.UTC) } var tests = []struct { name string job Job wantTimeUntilNextRun time.Duration }{ // SECONDS { name: "every second test", job: Job{ interval: 1, unit: seconds, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: _getSeconds(1), }, { name: "every 62 seconds test", job: Job{ interval: 62, unit: seconds, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: _getSeconds(62), }, // MINUTES { name: "every minute test", job: Job{ interval: 1, unit: minutes, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: _getMinutes(1), }, { name: "every 62 minutes test", job: Job{ interval: 62, unit: minutes, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: _getMinutes(62), }, // HOURS { name: "every hour test", job: Job{ interval: 1, unit: hours, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: _getHours(1), }, { name: "every 25 hours test", job: Job{ interval: 25, unit: hours, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: _getHours(25), }, // DAYS { name: "every day at midnight", job: Job{ interval: 1, unit: days, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 1 * day, }, { name: "every day at 09:30AM with scheduler starting before 09:30AM should run at same day at time", job: Job{ interval: 1, unit: days, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30), }, { name: "every day at 09:30AM which just ran should run tomorrow at 09:30AM", job: Job{ interval: 1, unit: days, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(9, 30, 0), }, wantTimeUntilNextRun: 1 * day, }, { name: "every 31 days at midnight should run 31 days later", job: Job{ interval: 31, unit: days, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 31 * day, }, { name: "daily job just ran at 8:30AM and should be scheduled for next day's 8:30AM", job: Job{ interval: 1, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(8, 30, 0), }, wantTimeUntilNextRun: 24 * time.Hour, }, { name: "daily job just ran at 5:30AM and should be scheduled for today at 8:30AM", job: Job{ interval: 1, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(5, 30, 0), }, wantTimeUntilNextRun: 3 * time.Hour, }, { name: "job runs every 2 days, just ran at 5:30AM and should be scheduled for 2 days at 8:30AM", job: Job{ interval: 2, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(5, 30, 0), }, wantTimeUntilNextRun: (2 * day) + 3*time.Hour, }, { name: "job runs every 2 days, just ran at 8:30AM and should be scheduled for 2 days at 8:30AM", job: Job{ interval: 2, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(8, 30, 0), }, wantTimeUntilNextRun: 2 * day, }, //// WEEKS { name: "every week should run in 7 days", job: Job{ interval: 1, unit: weeks, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 7 * day, }, { name: "every week with .At time rule should run respect .At time rule", job: Job{ interval: 1, atTime: _getHours(9) + _getMinutes(30), unit: weeks, lastRun: januaryFirst2020At(9, 30, 0), }, wantTimeUntilNextRun: 7 * day, }, { name: "every two weeks at 09:30AM should run in 14 days at 09:30AM", job: Job{ interval: 2, unit: weeks, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 14 * day, }, { name: "every 31 weeks ran at jan 1st at midnight should run at August 5, 2020", job: Job{ interval: 31, unit: weeks, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 31 * 7 * day, }, // MONTHS { name: "every month in a 31 days month should be scheduled for 31 days ahead", job: Job{ interval: 1, unit: months, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 31 * day, }, { name: "every month in a 30 days month should be scheduled for 30 days ahead", job: Job{ interval: 1, unit: months, lastRun: time.Date(2020, time.April, 1, 0, 0, 0, 0, time.UTC), }, wantTimeUntilNextRun: 30 * day, }, { name: "every month at february on leap year should count 29 days", job: Job{ interval: 1, unit: months, lastRun: time.Date(2020, time.February, 1, 0, 0, 0, 0, time.UTC), }, wantTimeUntilNextRun: 29 * day, }, { name: "every month at february on non leap year should count 28 days", job: Job{ interval: 1, unit: months, lastRun: time.Date(2019, time.February, 1, 0, 0, 0, 0, time.UTC), }, wantTimeUntilNextRun: 28 * day, }, { name: "every month at first day at time should run next month + at time", job: Job{ interval: 1, unit: months, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(9, 30, 0), }, wantTimeUntilNextRun: 31*day + _getHours(9) + _getMinutes(30), }, { name: "every month at day should consider at days", job: Job{ interval: 1, unit: months, dayOfTheMonth: 2, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 1 * day, }, { name: "every month at day should consider at hours", job: Job{ interval: 1, unit: months, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 31*day + _getHours(9) + _getMinutes(30), }, { name: "every month on the first day, but started on january 8th, should run February 1st", job: Job{ interval: 1, unit: months, dayOfTheMonth: 1, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 7), }, wantTimeUntilNextRun: 24 * day, }, { name: "every 2 months at day 1, starting at day 1, should run in 2 months", job: Job{ interval: 2, unit: months, dayOfTheMonth: 1, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 31*day + 29*day, // 2020 january and february }, { name: "every 2 months at day 2, starting at day 1, should run in 2 months + 1 day", job: Job{ interval: 2, unit: months, dayOfTheMonth: 2, lastRun: januaryFirst2020At(0, 0, 0), }, wantTimeUntilNextRun: 31*day + 29*day + 1*day, // 2020 january and february }, { name: "every 2 months at day 1, starting at day 2, should run in 2 months - 1 day", job: Job{ interval: 2, unit: months, dayOfTheMonth: 1, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 1), }, wantTimeUntilNextRun: 30*day + 29*day, // 2020 january and february }, { name: "every 13 months at day 1, starting at day 2 run in 13 months - 1 day", job: Job{ interval: 13, unit: months, dayOfTheMonth: 1, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 1), }, wantTimeUntilNextRun: januaryFirst2020At(0, 0, 0).AddDate(0, 13, -1).Sub(januaryFirst2020At(0, 0, 0)), }, //// WEEKDAYS { name: "every weekday starting on one day before it should run this weekday", job: Job{ interval: 1, unit: weeks, scheduledWeekday: _tuesdayWeekday(), lastRun: mondayAt(0, 0, 0), }, wantTimeUntilNextRun: 1 * day, }, { name: "every weekday starting on same weekday should run on same immediately", job: Job{ interval: 1, unit: weeks, scheduledWeekday: _tuesdayWeekday(), lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1), }, wantTimeUntilNextRun: 0, }, { name: "every 2 weekdays counting this week's weekday should run next weekday", job: Job{ interval: 2, unit: weeks, scheduledWeekday: _tuesdayWeekday(), lastRun: mondayAt(0, 0, 0), }, wantTimeUntilNextRun: 8 * day, }, { name: "every weekday starting on one day after should count days remaning", job: Job{ interval: 1, unit: weeks, scheduledWeekday: _tuesdayWeekday(), lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 2), }, wantTimeUntilNextRun: 6 * day, }, { name: "every weekday starting before jobs .At() time should run at same day at time", job: Job{ interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekday: _tuesdayWeekday(), lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1), }, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30), }, { name: "every weekday starting at same day at time that already passed should run at next week at time", job: Job{ interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekday: _tuesdayWeekday(), lastRun: mondayAt(10, 30, 0).AddDate(0, 0, 1), }, wantTimeUntilNextRun: 6*day + _getHours(23) + _getMinutes(0), }, } for i := range tests { t.Run(tests[i].name, func(t *testing.T) { sched := NewScheduler(time.UTC) got := sched.durationToNextRun(tests[i].job.LastRun(), &tests[i].job) assert.Equalf(t, tests[i].wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tests[i].wantTimeUntilNextRun.String(), got.String())) }) } } // helper test method func _tuesdayWeekday() *time.Weekday { tuesday := time.Tuesday return &tuesday } // helper test method func _getSeconds(i int) time.Duration { return time.Duration(i) * time.Second } // helper test method func _getHours(i int) time.Duration { return time.Duration(i) * time.Hour } // helper test method func _getMinutes(i int) time.Duration { return time.Duration(i) * time.Minute } func TestScheduler_Do(t *testing.T) { t.Run("adding a new job before scheduler starts does not schedule job", func(t *testing.T) { s := NewScheduler(time.UTC) s.setRunning(false) job, err := s.Every(1).Second().Do(func() {}) assert.Equal(t, nil, err) assert.True(t, job.NextRun().IsZero()) }) t.Run("adding a new job when scheduler is running schedules job", func(t *testing.T) { s := NewScheduler(time.UTC) s.setRunning(true) job, err := s.Every(1).Second().Do(func() {}) assert.Equal(t, nil, err) assert.False(t, job.NextRun().IsZero()) }) } func TestRunJobsWithLimit(t *testing.T) { f := func(in *int, mu *sync.RWMutex) { mu.Lock() defer mu.Unlock() *in = *in + 1 } s := NewScheduler(time.UTC) var j1Counter, j2Counter int var j1Mutex, j2Mutex sync.RWMutex j1, err := s.Every(1).Second().Do(f, &j1Counter, &j1Mutex) require.NoError(t, err) j1.LimitRunsTo(1) j2, err := s.Every(1).Second().Do(f, &j2Counter, &j2Mutex) require.NoError(t, err) j2.LimitRunsTo(1) s.StartAsync() time.Sleep(3 * time.Second) j1Mutex.RLock() j1Mutex.RUnlock() assert.Exactly(t, 1, j1Counter) j2Mutex.RLock() j2Mutex.RUnlock() assert.Exactly(t, 1, j2Counter) } func TestDo(t *testing.T) { var tests = []struct { name string evalFunc func(*Scheduler) }{ { name: "error due to the arg passed to Do() not being a function", evalFunc: func(s *Scheduler) { s.Every(1).Second().Do(1) assert.Zero(t, len(s.jobs), "The job should be deleted if the arg passed to Do() is not a function") }, }, { name: "positive case", evalFunc: func(s *Scheduler) { s.Every(1).Day().Do(func() {}) assert.Equal(t, 1, len(s.jobs)) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewScheduler(time.Local) tt.evalFunc(s) }) } } func TestRemoveAfterExec(t *testing.T) { s := NewScheduler(time.UTC) job, err := s.Every(1).Second().Do(task, s) require.NoError(t, err) job.LimitRunsTo(1) job.RemoveAfterLastRun() s.StartAsync() time.Sleep(2 * time.Second) assert.Zero(t, len(s.Jobs())) } gocron-0.5.0/timeHelper.go000066400000000000000000000010421377304225300154330ustar00rootroot00000000000000package gocron import "time" type timeWrapper interface { Now(*time.Location) time.Time Unix(int64, int64) time.Time Sleep(time.Duration) NewTicker(time.Duration) *time.Ticker } type trueTime struct{} func (t *trueTime) Now(location *time.Location) time.Time { return time.Now().In(location) } func (t *trueTime) Unix(sec int64, nsec int64) time.Time { return time.Unix(sec, nsec) } func (t *trueTime) Sleep(d time.Duration) { time.Sleep(d) } func (t *trueTime) NewTicker(d time.Duration) *time.Ticker { return time.NewTicker(d) }