pax_global_header00006660000000000000000000000064143657223060014522gustar00rootroot0000000000000052 comment=8169ff9f7dad596a9bfdcddf2f25ed9b474c3290 golang-github-go-co-op-gocron-1.18.0/000077500000000000000000000000001436572230600172435ustar00rootroot00000000000000golang-github-go-co-op-gocron-1.18.0/.github/000077500000000000000000000000001436572230600206035ustar00rootroot00000000000000golang-github-go-co-op-gocron-1.18.0/.github/FUNDING.yml000066400000000000000000000012371436572230600224230ustar00rootroot00000000000000# 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'] golang-github-go-co-op-gocron-1.18.0/.github/dependabot.yml000066400000000000000000000011351436572230600234330ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" # Maintain Go dependencies - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" golang-github-go-co-op-gocron-1.18.0/.github/workflows/000077500000000000000000000000001436572230600226405ustar00rootroot00000000000000golang-github-go-co-op-gocron-1.18.0/.github/workflows/codeql-analysis.yml000066400000000000000000000044601436572230600264570ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '34 7 * * 1' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 golang-github-go-co-op-gocron-1.18.0/.github/workflows/file_formatting.yml000066400000000000000000000005331436572230600265350ustar00rootroot00000000000000on: push: branches: - main pull_request: branches: - main name: formatting jobs: check-sorted: name: check sorted runs-on: ubuntu-latest steps: - name: checkout code uses: actions/checkout@v3 - name: verify example_test.go run: | grep "^func " example_test.go | sort -C golang-github-go-co-op-gocron-1.18.0/.github/workflows/go_test.yml000066400000000000000000000011201436572230600250210ustar00rootroot00000000000000on: push: branches: - main pull_request: branches: - main name: golangci-lint jobs: golangci: strategy: matrix: go-version: - 1.19 name: lint and test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3.3.0 with: version: v1.48.0 - name: Install Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: test run: make test golang-github-go-co-op-gocron-1.18.0/.gitignore000066400000000000000000000004641436572230600212370ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test local_testing # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) vendor/ # IDE project files .idea golang-github-go-co-op-gocron-1.18.0/.golangci.yaml000066400000000000000000000020131436572230600217640ustar00rootroot00000000000000run: timeout: 2m issues-exit-code: 1 tests: true issues: max-same-issues: 100 exclude-rules: - path: _test\.go linters: - bodyclose - errcheck - gosec linters: enable: - bodyclose - deadcode - errcheck - gofmt - revive - gosec - gosimple - govet - ineffassign - misspell - staticcheck - structcheck - typecheck - unused - varcheck output: # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" format: colored-line-number # print lines of code with issue, default is true print-issued-lines: true # print linter name in the end of issue text, default is true print-linter-name: true # make issues output unique by line, default is true uniq-by-line: true # add a prefix to the output file references; default is no prefix path-prefix: "" # sorts results by: filepath, line and column sort-results: true linters-settings: golint: min-confidence: 0.8 golang-github-go-co-op-gocron-1.18.0/CODE_OF_CONDUCT.md000066400000000000000000000061211436572230600220420ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone. And we mean everyone! ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and kind language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team initially on Slack to coordinate private communication. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq golang-github-go-co-op-gocron-1.18.0/CONTRIBUTING.md000066400000000000000000000033421436572230600214760ustar00rootroot00000000000000# 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. golang-github-go-co-op-gocron-1.18.0/LICENSE000066400000000000000000000020531436572230600202500ustar00rootroot00000000000000MIT License Copyright (c) 2014, 辣椒面 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-go-co-op-gocron-1.18.0/Makefile000066400000000000000000000003501436572230600207010ustar00rootroot00000000000000.PHONY: fmt check-fmt lint vet test GO_PKGS := $(shell go list -f {{.Dir}} ./...) fmt: @go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {} lint: @golangci-lint run test: @go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS) golang-github-go-co-op-gocron-1.18.0/README.md000066400000000000000000000137571436572230600205370ustar00rootroot00000000000000# 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"lint") ![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://pkg.go.dev/github.com/go-co-op/gocron) gocron is a job scheduling package which lets you run Go functions at pre-determined intervals using a simple, human-friendly syntax. gocron is a Golang scheduler implementation similar to the Ruby module [clockwork](https://github.com/tomykaira/clockwork) and the Python job scheduling package [schedule](https://github.com/dbader/schedule). See also these two great articles that were used for design input: - [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) ## Concepts - **Scheduler**: The scheduler tracks all the jobs assigned to it and makes sure they are passed to the executor when ready to be run. The scheduler is able to manage overall aspects of job behavior like limiting how many jobs are running at one time. - **Job**: The job is simply aware of the task (go function) it's provided and is therefore only able to perform actions related to that task like preventing itself from overruning a previous task that is taking a long time. - **Executor**: The executor, as it's name suggests, is simply responsible for calling the task (go function) that the job hands to it when sent by the scheduler. ## Examples ```golang s := gocron.NewScheduler(time.UTC) s.Every(5).Seconds().Do(func(){ ... }) // strings parse to duration s.Every("5m").Do(func(){ ... }) s.Every(5).Days().Do(func(){ ... }) s.Every(1).Month(1, 2, 3).Do(func(){ ... }) // set time s.Every(1).Day().At("10:30").Do(func(){ ... }) // set multiple times s.Every(1).Day().At("10:30;08:00").Do(func(){ ... }) s.Every(1).Day().At("10:30").At("08:00").Do(func(){ ... }) // Schedule each last day of the month s.Every(1).MonthLastDay().Do(func(){ ... }) // Or each last day of every other month s.Every(2).MonthLastDay().Do(func(){ ... }) // cron expressions supported s.Cron("*/1 * * * *").Do(task) // every minute // you can start running the scheduler in two different ways: // starts the scheduler asynchronously s.StartAsync() // starts the scheduler and blocks current execution path s.StartBlocking() ``` For more examples, take a look in our [go docs](https://pkg.go.dev/github.com/go-co-op/gocron#pkg-examples) ## Options | Interval | Supported schedule options | | ------------ | ------------------------------------------------------------------- | | sub-second | `StartAt()` | | milliseconds | `StartAt()` | | seconds | `StartAt()` | | minutes | `StartAt()` | | hours | `StartAt()` | | days | `StartAt()`, `At()` | | weeks | `StartAt()`, `At()`, `Weekday()` (and all week day named functions) | | months | `StartAt()`, `At()` | There are several options available to restrict how jobs run: | Mode | Function | Behavior | | --------------- | ------------------------ | ------------------------------------------------------------------------------- | | Default | | jobs are rescheduled at every interval | | Job singleton | `SingletonMode()` | a long running job will not be rescheduled until the current run is completed | | Scheduler limit | `SetMaxConcurrentJobs()` | set a collective maximum number of concurrent jobs running across the scheduler | ## Tags Jobs may have arbitrary tags added which can be useful when tracking many jobs. The scheduler supports both enforcing tags to be unique and when not unique, running all jobs with a given tag. ```golang s := gocron.NewScheduler(time.UTC) s.TagsUnique() _, _ = s.Every(1).Week().Tag("foo").Do(task) _, err := s.Every(1).Week().Tag("foo").Do(task) // error!!! s := gocron.NewScheduler(time.UTC) s.Every(2).Day().Tag("tag").At("10:00").Do(task) s.Every(1).Minute().Tag("tag").Do(task) s.RunByTag("tag") // both jobs will run ``` ## 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) - Q: I've removed my job from the scheduler, but how can I stop a long-running job that has already been triggered? - A: We recommend using a means of canceling your job, e.g. a `context.WithCancel()`. --- 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 --- ## Design ![design-diagram](https://user-images.githubusercontent.com/19351306/110375142-2ba88680-8017-11eb-80c3-554cc746b165.png) [Jetbrains](https://www.jetbrains.com/?from=gocron) supports this project with GoLand licenses. We appreciate their support for free and open source software! golang-github-go-co-op-gocron-1.18.0/SECURITY.md000066400000000000000000000011561436572230600210370ustar00rootroot00000000000000# Security Policy ## Supported Versions The current plan is to maintain version 1 as long as possible incorporating any necessary security patches. | Version | Supported | | ------- | ------------------ | | 1.x.x | :white_check_mark: | ## Reporting a Vulnerability Vulnerabilities can be reported by [opening an issue](https://github.com/go-co-op/gocron/issues/new/choose) or reaching out on Slack: [](https://gophers.slack.com/archives/CQ7T0T1FW) We will do our best to addrerss any vulnerabilities in an expeditious manner. golang-github-go-co-op-gocron-1.18.0/example_test.go000066400000000000000000000506211436572230600222700ustar00rootroot00000000000000package gocron_test import ( "fmt" "log" "time" "github.com/go-co-op/gocron" ) var task = func() {} // --------------------------------------------------------------------- // ----------------------JOB-FUNCTIONS---------------------------------- // --------------------------------------------------------------------- func ExampleJob_Error() { s := gocron.NewScheduler(time.UTC) s.Every(1).Day().At("bad time") j := s.Jobs()[0] fmt.Println(j.Error()) // Output: // the given time format is not supported } func ExampleJob_IsRunning() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(10).Seconds().Do(func() { time.Sleep(2 * time.Second) }) fmt.Println(j.IsRunning()) s.StartAsync() time.Sleep(time.Second) fmt.Println(j.IsRunning()) time.Sleep(time.Second) s.Stop() time.Sleep(1 * time.Second) fmt.Println(j.IsRunning()) // Output: // false // true // false } func ExampleJob_LastRun() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) s.StartAsync() fmt.Println("Last run:", job.LastRun()) } func ExampleJob_LimitRunsTo() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) job.LimitRunsTo(2) 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_ScheduledAtTime() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Day().At("10:30").Do(task) s.StartAsync() fmt.Println(job.ScheduledAtTime()) // if multiple times are set, the earliest time will be returned job1, _ := s.Every(1).Day().At("10:30;08:00").Do(task) fmt.Println(job1.ScheduledAtTime()) // Output: // 10:30 // 8:0 } func ExampleJob_ScheduledAtTimes() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Day().At("10:30;08:00").Do(task) s.StartAsync() fmt.Println(job.ScheduledAtTimes()) // Output: // [8:0 10:30] } func ExampleJob_ScheduledTime() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Day().At("10:30").Do(task) s.StartAsync() fmt.Println(job.ScheduledTime()) } func ExampleJob_SetEventListeners() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Week().Do(task) job.SetEventListeners(func() {}, func() {}) } func ExampleJob_SingletonMode() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every(1).Second().Do(task) job.SingletonMode() } func ExampleJob_Tag() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every("1s").Do(task) job.Tag("tag1", "tag2", "tag3") s.StartAsync() fmt.Println(job.Tags()) // Output: // [tag1 tag2 tag3] } func ExampleJob_Tags() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every("1s").Do(task) job.Tag("tag1", "tag2", "tag3") s.StartAsync() fmt.Println(job.Tags()) // Output: // [tag1 tag2 tag3] } func ExampleJob_Untag() { s := gocron.NewScheduler(time.UTC) job, _ := s.Every("1s").Do(task) job.Tag("tag1", "tag2", "tag3") s.StartAsync() fmt.Println(job.Tags()) job.Untag("tag2") fmt.Println(job.Tags()) // Output: // [tag1 tag2 tag3] // [tag1 tag3] } func ExampleJob_Weekday() { s := gocron.NewScheduler(time.UTC) weeklyJob, _ := s.Every(1).Week().Monday().Do(task) weekday, _ := weeklyJob.Weekday() fmt.Println(weekday) dailyJob, _ := s.Every(1).Day().Do(task) _, err := dailyJob.Weekday() fmt.Println(err) // Output: // Monday // job not scheduled weekly on a weekday } func ExampleJob_Weekdays() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Week().Monday().Wednesday().Friday().Do(task) fmt.Println(j.Weekdays()) // Output: // [Monday Wednesday Friday] } // --------------------------------------------------------------------- // -------------------SCHEDULER-FUNCTIONS------------------------------- // --------------------------------------------------------------------- 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) // multiple _, _ = s.Every(1).Monday().At("10:30;18:00").Do(task) _, _ = s.Every(1).Monday().At("10:30").At("18:00").Do(task) } func ExampleScheduler_ChangeLocation() { s := gocron.NewScheduler(time.UTC) fmt.Println(s.Location()) location, err := time.LoadLocation("America/Los_Angeles") if err != nil { log.Fatalf("Error loading location: %s", err) } s.ChangeLocation(location) fmt.Println(s.Location()) // Output: // UTC // America/Los_Angeles } 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 ExampleScheduler_Cron() { s := gocron.NewScheduler(time.UTC) // parsing handled by https://pkg.go.dev/github.com/robfig/cron/v3 // which follows https://en.wikipedia.org/wiki/Cron _, _ = s.Cron("*/1 * * * *").Do(task) // every minute _, _ = s.Cron("0 1 * * *").Do(task) // every day at 1 am _, _ = s.Cron("0 0 * * 6,0").Do(task) // weekends only } func ExampleScheduler_CronWithSeconds() { s := gocron.NewScheduler(time.UTC) // parsing handled by https://pkg.go.dev/github.com/robfig/cron/v3 // which follows https://en.wikipedia.org/wiki/Cron _, _ = s.CronWithSeconds("*/1 * * * * *").Do(task) // every second _, _ = s.CronWithSeconds("0-30 * * * * *").Do(task) // every second 0-30 } func ExampleScheduler_CustomTime() { // Implement your own custom time struct // // type myCustomTime struct{} // // var _ gocron.TimeWrapper = (*myCustomTime)(nil) // // func (m myCustomTime) Now(loc *time.Location) time.Time { // panic("implement me") // } // // func (m myCustomTime) Sleep(duration time.Duration) { // panic("implement me") // } // // func (m myCustomTime) Unix(sec int64, nsec int64) time.Time { // panic("implement me") // } // // mct := myCustomTime{} // // s := gocron.NewScheduler(time.UTC) // s.CustomTime(mct) } func ExampleScheduler_CustomTimer() { s := gocron.NewScheduler(time.UTC) s.CustomTimer(func(d time.Duration, f func()) *time.Timer { // force jobs with 1 minute interval to run every second if d == time.Minute { d = time.Second } return time.AfterFunc(d, f) }) // this job will run every 1 second _, _ = s.Every("1m").Do(task) } func ExampleScheduler_Day() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("24h").Do(task) _, _ = s.Every(1).Day().Do(task) _, _ = s.Every(1).Days().Do(task) } func ExampleScheduler_Days() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("24h").Do(task) _, _ = s.Every(1).Day().Do(task) _, _ = s.Every(1).Days().Do(task) } func ExampleScheduler_Do() { s := gocron.NewScheduler(time.UTC) j1, err := s.Every(1).Second().Do(task) fmt.Printf("Job: %v, Error: %v", j1, err) taskWithParameters := func(param1, param2 string) {} j2, err := s.Every(1).Second().Do(taskWithParameters, "param1", "param2") fmt.Printf("Job: %v, Error: %v", j2, err) s.StartAsync() } func ExampleScheduler_DoWithJobDetails() { task := func(in string, job gocron.Job) { fmt.Printf("this job's last run: %s\nthis job's next run: %s", job.LastRun(), job.NextRun()) } s := gocron.NewScheduler(time.UTC) j, err := s.Every(1).Second().DoWithJobDetails(task, "foo") s.StartAsync() fmt.Printf("Job: %v, Error: %v", j, err) } func ExampleScheduler_Every() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Second().Do(task) _, _ = s.Every(1 * time.Second).Do(task) _, _ = s.Every("1s").Do(task) s.StartAsync() } func ExampleScheduler_EveryRandom() { s := gocron.NewScheduler(time.UTC) // every 1 - 5 seconds randomly _, _ = s.EveryRandom(1, 5).Seconds().Do(task) // every 5 - 10 hours randomly _, _ = s.EveryRandom(5, 10).Hours().Do(task) s.StartAsync() } func ExampleScheduler_Friday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Friday().Do(task) s.StartAsync() wd, _ := j.Weekday() fmt.Println(wd) // Output: // Friday } func ExampleScheduler_Hour() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1h").Do(task) _, _ = s.Every(1).Hour().Do(task) _, _ = s.Every(1).Hours().Do(task) } func ExampleScheduler_Hours() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1h").Do(task) _, _ = s.Every(1).Hour().Do(task) _, _ = s.Every(1).Hours().Do(task) } func ExampleScheduler_IsRunning() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1s").Do(task) fmt.Println(s.IsRunning()) s.StartAsync() fmt.Println(s.IsRunning()) // Output: // false // true } func ExampleScheduler_Job() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every("1s").Do(func() {}) s.StartAsync() time.Sleep(10 * time.Second) _, _ = s.Job(j).Every("10m").Update() time.Sleep(30 * time.Minute) _, _ = s.Job(j).Every(1).Day().At("02:00").Update() } func ExampleScheduler_Jobs() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1s").Do(task) _, _ = s.Every("1s").Do(task) _, _ = s.Every("1s").Do(task) fmt.Println(len(s.Jobs())) // Output: // 3 } func ExampleScheduler_Len() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1s").Do(task) _, _ = s.Every("1s").Do(task) _, _ = s.Every("1s").Do(task) fmt.Println(s.Len()) // Output: // 3 } func ExampleScheduler_Less() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1s").Do(task) _, _ = s.Every("2s").Do(task) s.StartAsync() fmt.Println(s.Less(0, 1)) // Output: // true } func ExampleScheduler_LimitRunsTo() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Second().LimitRunsTo(1).Do(task) s.StartAsync() fmt.Println(j.RunCount()) // Output: // 1 } func ExampleScheduler_Location() { s := gocron.NewScheduler(time.UTC) fmt.Println(s.Location()) // Output: // UTC } func ExampleScheduler_Midday() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().Midday().Do(task) s.StartAsync() } func ExampleScheduler_Millisecond() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Millisecond().Do(task) _, _ = s.Every(1).Milliseconds().Do(task) _, _ = s.Every("1ms").Seconds().Do(task) _, _ = s.Every(time.Millisecond).Seconds().Do(task) } func ExampleScheduler_Milliseconds() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Millisecond().Do(task) _, _ = s.Every(1).Milliseconds().Do(task) _, _ = s.Every("1ms").Seconds().Do(task) _, _ = s.Every(time.Millisecond).Seconds().Do(task) } func ExampleScheduler_Minute() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1m").Do(task) _, _ = s.Every(1).Minute().Do(task) _, _ = s.Every(1).Minutes().Do(task) } func ExampleScheduler_Minutes() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every("1m").Do(task) _, _ = s.Every(1).Minute().Do(task) _, _ = s.Every(1).Minutes().Do(task) } func ExampleScheduler_Monday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Monday().Do(task) s.StartAsync() wd, _ := j.Weekday() fmt.Println(wd) // Output: // Monday } func ExampleScheduler_Month() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Month().Do(task) _, _ = s.Every(1).Month(1).Do(task) _, _ = s.Every(1).Months(1).Do(task) _, _ = s.Every(1).Month(1, 2).Do(task) _, _ = s.Month(1, 2).Every(1).Do(task) } func ExampleScheduler_MonthFirstWeekday() { s := gocron.NewScheduler(time.UTC) _, _ = s.MonthFirstWeekday(time.Monday).Do(task) s.StartAsync() } func ExampleScheduler_MonthLastDay() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).MonthLastDay().Do(task) _, _ = s.Every(2).MonthLastDay().Do(task) } func ExampleScheduler_Months() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Month(1).Do(task) _, _ = s.Every(1).Months(1).Do(task) } func ExampleScheduler_NextRun() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().At("10:30").Do(task) s.StartAsync() _, t := s.NextRun() // print only the hour and minute (hh:mm) fmt.Println(t.Format("15:04")) // Output: // 10:30 } func ExampleScheduler_Remove() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Week().Do(task) s.StartAsync() s.Remove(task) fmt.Println(s.Len()) // Output: // 0 } func ExampleScheduler_RemoveByReference() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Week().Do(task) _, _ = s.Every(1).Week().Do(task) s.StartAsync() s.RemoveByReference(j) fmt.Println(s.Len()) // Output: // 1 } func ExampleScheduler_RemoveByTag() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Week().Tag("tag1").Do(task) _, _ = s.Every(1).Week().Tag("tag2").Do(task) s.StartAsync() _ = s.RemoveByTag("tag1") fmt.Println(s.Len()) // Output: // 1 } func ExampleScheduler_RemoveByTags() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Week().Tag("tag1", "tag2", "tag3").Do(task) _, _ = s.Every(1).Week().Tag("tag1", "tag2").Do(task) _, _ = s.Every(1).Week().Tag("tag1").Do(task) s.StartAsync() _ = s.RemoveByTags("tag1", "tag2") fmt.Println(s.Len()) // Output: // 1 } func ExampleScheduler_RemoveByTagsAny() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Week().Tag("tag1", "tag2", "tag3").Do(task) _, _ = s.Every(1).Week().Tag("tag1").Do(task) _, _ = s.Every(1).Week().Tag("tag2").Do(task) _, _ = s.Every(1).Week().Tag("tag3").Do(task) s.StartAsync() _ = s.RemoveByTagsAny("tag1", "tag3") fmt.Println(s.Len()) // Output: // 1 } func ExampleScheduler_RunAll() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().At("10:00").Do(task) _, _ = s.Every(2).Day().At("10:00").Do(task) _, _ = s.Every(3).Day().At("10:00").Do(task) s.StartAsync() s.RunAll() } func ExampleScheduler_RunAllWithDelay() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().At("10:00").Do(task) _, _ = s.Every(2).Day().At("10:00").Do(task) _, _ = s.Every(3).Day().At("10:00").Do(task) s.StartAsync() s.RunAllWithDelay(10 * time.Second) } func ExampleScheduler_RunByTag() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().At("10:00").Do(task) _, _ = s.Every(2).Day().Tag("tag").At("10:00").Do(task) s.StartAsync() _ = s.RunByTag("tag") } func ExampleScheduler_RunByTagWithDelay() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().Tag("tag").At("10:00").Do(task) _, _ = s.Every(2).Day().Tag("tag").At("10:00").Do(task) s.StartAsync() _ = s.RunByTagWithDelay("tag", 2*time.Second) } func ExampleScheduler_Saturday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Saturday().Do(task) s.StartAsync() wd, _ := j.Weekday() fmt.Println(wd) // Output: // Saturday } func ExampleScheduler_Second() { s := gocron.NewScheduler(time.UTC) // the default unit is seconds // these are all the same _, _ = s.Every(1).Do(task) _, _ = s.Every(1).Second().Do(task) _, _ = s.Every(1).Seconds().Do(task) _, _ = s.Every("1s").Seconds().Do(task) _, _ = s.Every(time.Second).Seconds().Do(task) } func ExampleScheduler_Seconds() { s := gocron.NewScheduler(time.UTC) // the default unit is seconds // these are all the same _, _ = s.Every(1).Do(task) _, _ = s.Every(1).Second().Do(task) _, _ = s.Every(1).Seconds().Do(task) _, _ = s.Every("1s").Seconds().Do(task) _, _ = s.Every(time.Second).Seconds().Do(task) } func ExampleScheduler_SetMaxConcurrentJobs() { s := gocron.NewScheduler(time.UTC) s.SetMaxConcurrentJobs(1, gocron.RescheduleMode) _, _ = s.Every(1).Seconds().Do(func() { fmt.Println("This will run once every 5 seconds even though it is scheduled every second because maximum concurrent job limit is set.") time.Sleep(5 * time.Second) }) } func ExampleScheduler_SingletonMode() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Second().SingletonMode().Do(task) } func ExampleScheduler_SingletonModeAll() { s := gocron.NewScheduler(time.UTC) s.SingletonModeAll() _, _ = s.Every(1).Second().Do(task) } 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_StartBlocking() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(3).Seconds().Do(task) s.StartBlocking() } func ExampleScheduler_StartImmediately() { s := gocron.NewScheduler(time.UTC) _, _ = s.Cron("0 0 * * 6,0").StartImmediately().Do(task) s.StartBlocking() } func ExampleScheduler_Stop() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Second().Do(task) s.StartAsync() s.Stop() fmt.Println(s.IsRunning()) s = gocron.NewScheduler(time.UTC) go func() { time.Sleep(1 * time.Second) s.Stop() }() s.StartBlocking() fmt.Println(".Stop() stops the blocking start") // Output: // false // .Stop() stops the blocking start } func ExampleScheduler_Sunday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Sunday().Do(task) s.StartAsync() wd, _ := j.Weekday() fmt.Println(wd) // Output: // Sunday } func ExampleScheduler_Swap() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Tag("tag1").Do(task) _, _ = s.Every(1).Tag("tag2").Day().Monday().Do(task) fmt.Println(s.Jobs()[0].Tags()[0], s.Jobs()[1].Tags()[0]) s.Swap(0, 1) fmt.Println(s.Jobs()[0].Tags()[0], s.Jobs()[1].Tags()[0]) // Output: // tag1 tag2 // tag2 tag1 } func ExampleScheduler_Tag() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Week().Tag("tag").Do(task) fmt.Println(j.Tags()) // Output: // [tag] } func ExampleScheduler_TagsUnique() { s := gocron.NewScheduler(time.UTC) s.TagsUnique() _, _ = s.Every(1).Week().Tag("foo").Do(task) _, err := s.Every(1).Week().Tag("foo").Do(task) fmt.Println(err) // Output: // a non-unique tag was set on the job: foo } func ExampleScheduler_TaskPresent() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Do(task) fmt.Println(s.TaskPresent(task)) // Output: // true } func ExampleScheduler_Thursday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Thursday().Do(task) s.StartAsync() wd, _ := j.Weekday() fmt.Println(wd) // Output: // Thursday } func ExampleScheduler_Tuesday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Tuesday().Do(task) s.StartAsync() wd, _ := j.Weekday() fmt.Println(wd) // Output: // Tuesday } func ExampleScheduler_Update() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every("1s").Do(task) s.StartAsync() time.Sleep(10 * time.Second) _, _ = s.Job(j).Every("10m").Update() time.Sleep(30 * time.Minute) _, _ = s.Job(j).Every(1).Day().At("02:00").Update() } func ExampleScheduler_WaitForSchedule() { s := gocron.NewScheduler(time.UTC) // job will run 5 minutes from the scheduler starting _, _ = s.Every("5m").WaitForSchedule().Do(task) // job will run immediately and 5 minutes from the scheduler starting _, _ = s.Every("5m").Do(task) s.StartAsync() } func ExampleScheduler_WaitForScheduleAll() { s := gocron.NewScheduler(time.UTC) s.WaitForScheduleAll() // all jobs will run 5 minutes from the scheduler starting _, _ = s.Every("5m").Do(task) _, _ = s.Every("5m").Do(task) s.StartAsync() } func ExampleScheduler_Wednesday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Wednesday().Do(task) s.StartAsync() wd, _ := j.Weekday() fmt.Println(wd) // Output: // Wednesday } func ExampleScheduler_Week() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Week().Do(task) _, _ = s.Every(1).Weeks().Do(task) _, _ = s.Every(1).Week().Monday().Wednesday().Friday().Do(task) } func ExampleScheduler_Weekday() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Week().Weekday(time.Monday).Do(task) _, _ = s.Every(1).Weeks().Weekday(time.Tuesday).Weekday(time.Friday).Do(task) } func ExampleScheduler_Weeks() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Week().Do(task) _, _ = s.Every(1).Weeks().Do(task) _, _ = s.Every(2).Weeks().Monday().Wednesday().Friday().Do(task) } // --------------------------------------------------------------------- // ---------------------OTHER-FUNCTIONS--------------------------------- // --------------------------------------------------------------------- func ExampleSetPanicHandler() { gocron.SetPanicHandler(func(jobName string, recoverData interface{}) { fmt.Printf("Panic in job: %s", jobName) fmt.Println("do something to handle the panic") }) } golang-github-go-co-op-gocron-1.18.0/executor.go000066400000000000000000000053221436572230600214320ustar00rootroot00000000000000package gocron import ( "context" "sync" "golang.org/x/sync/semaphore" ) const ( // RescheduleMode - the default is that if a limit on maximum // concurrent jobs is set and the limit is reached, a job will // skip it's run and try again on the next occurrence in the schedule RescheduleMode limitMode = iota // WaitMode - if a limit on maximum concurrent jobs is set // and the limit is reached, a job will wait to try and run // until a spot in the limit is freed up. // // Note: this mode can produce unpredictable results as // job execution order isn't guaranteed. For example, a job that // executes frequently may pile up in the wait queue and be executed // many times back to back when the queue opens. WaitMode ) type executor struct { jobFunctions chan jobFunction stopCh chan struct{} stoppedCh chan struct{} limitMode limitMode maxRunningJobs *semaphore.Weighted } func newExecutor() executor { return executor{ jobFunctions: make(chan jobFunction, 1), stopCh: make(chan struct{}), stoppedCh: make(chan struct{}), } } func (e *executor) start() { stopCtx, cancel := context.WithCancel(context.Background()) runningJobsWg := sync.WaitGroup{} for { select { case f := <-e.jobFunctions: runningJobsWg.Add(1) go func() { defer runningJobsWg.Done() panicHandlerMutex.RLock() defer panicHandlerMutex.RUnlock() if panicHandler != nil { defer func() { if r := recover(); r != interface{}(nil) { panicHandler(f.name, r) } }() } if e.maxRunningJobs != nil { if !e.maxRunningJobs.TryAcquire(1) { switch e.limitMode { case RescheduleMode: return case WaitMode: select { case <-stopCtx.Done(): return case <-f.ctx.Done(): return default: } if err := e.maxRunningJobs.Acquire(f.ctx, 1); err != nil { break } } } defer e.maxRunningJobs.Release(1) } runJob := func() { f.incrementRunState() callJobFunc(f.eventListeners.onBeforeJobExecution) callJobFuncWithParams(f.function, f.parameters) callJobFunc(f.eventListeners.onAfterJobExecution) f.decrementRunState() } switch f.runConfig.mode { case defaultMode: runJob() case singletonMode: _, _, _ = f.limiter.Do("main", func() (interface{}, error) { select { case <-stopCtx.Done(): return nil, nil case <-f.ctx.Done(): return nil, nil default: } runJob() return nil, nil }) } }() case <-e.stopCh: cancel() runningJobsWg.Wait() close(e.stoppedCh) return } } } func (e *executor) stop() { close(e.stopCh) <-e.stoppedCh } golang-github-go-co-op-gocron-1.18.0/executor_test.go000066400000000000000000000021521436572230600224670ustar00rootroot00000000000000package gocron import ( "fmt" "sync" "testing" "github.com/stretchr/testify/assert" ) func Test_ExecutorExecute(t *testing.T) { e := newExecutor() wg := &sync.WaitGroup{} wg.Add(1) go e.start() var runState = int64(0) e.jobFunctions <- jobFunction{ name: "test_fn", function: func(arg string) { assert.Equal(t, arg, "test") wg.Done() }, parameters: []interface{}{"test"}, runState: &runState, } wg.Wait() e.stop() } func Test_ExecutorPanicHandling(t *testing.T) { panicHandled := make(chan bool, 1) handler := func(jobName string, recoverData interface{}) { fmt.Println("PanicHandler called:") fmt.Println("panic in " + jobName) fmt.Println(recoverData) panicHandled <- true } SetPanicHandler(handler) e := newExecutor() wg := &sync.WaitGroup{} wg.Add(1) go e.start() var runState = int64(0) e.jobFunctions <- jobFunction{ name: "test_fn", function: func() { defer wg.Done() a := make([]string, 0) a[0] = "This will panic" }, parameters: nil, runState: &runState, } wg.Wait() e.stop() state := <-panicHandled assert.Equal(t, state, true) } golang-github-go-co-op-gocron-1.18.0/go.mod000066400000000000000000000005071436572230600203530ustar00rootroot00000000000000module github.com/go-co-op/gocron go 1.19 require ( github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.8.1 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-github-go-co-op-gocron-1.18.0/go.sum000066400000000000000000000035501436572230600204010ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-go-co-op-gocron-1.18.0/gocron.go000066400000000000000000000106061436572230600210640ustar00rootroot00000000000000// Package gocron : A Golang Job Scheduling Package. // // An in-process scheduler for periodic jobs that uses the builder pattern // for configuration. gocron lets you run Golang functions periodically // at pre-determined intervals using a simple, human-friendly syntax. package gocron import ( "errors" "fmt" "reflect" "regexp" "runtime" "sync" "time" ) // PanicHandlerFunc represents a type that can be set to handle panics occurring // during job execution. type PanicHandlerFunc func(jobName string, recoverData interface{}) // The global panic handler var ( panicHandler PanicHandlerFunc panicHandlerMutex = sync.RWMutex{} ) // SetPanicHandler sets the global panicHandler to the given function. // Leaving it nil or setting it to nil disables automatic panic handling. // If the panicHandler is not nil, any panic that occurs during executing a job will be recovered // and the panicHandlerFunc will be called with the job's name and the recover data. func SetPanicHandler(handler PanicHandlerFunc) { panicHandlerMutex.Lock() defer panicHandlerMutex.Unlock() panicHandler = handler } // Error declarations for gocron related errors var ( ErrNotAFunction = errors.New("only functions can be scheduled into the job queue") 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") ErrInvalidInterval = errors.New(".Every() interval must be greater than 0") ErrInvalidIntervalType = errors.New(".Every() interval must be int, time.Duration, or string") ErrInvalidIntervalUnitsSelection = errors.New(".Every(time.Duration) and .Cron() cannot be used with units (e.g. .Seconds())") ErrInvalidFunctionParameters = errors.New("length of function parameters must match job function parameters") ErrAtTimeNotSupported = errors.New("the At() method is not supported for this time unit") ErrWeekdayNotSupported = errors.New("weekday is not supported for time unit") ErrInvalidDayOfMonthEntry = errors.New("only days 1 through 28 are allowed for monthly schedules") ErrTagsUnique = func(tag string) error { return fmt.Errorf("a non-unique tag was set on the job: %s", tag) } ErrWrongParams = errors.New("wrong list of params") ErrDoWithJobDetails = errors.New("DoWithJobDetails expects a function whose last parameter is a gocron.Job") ErrUpdateCalledWithoutJob = errors.New("a call to Scheduler.Update() requires a call to Scheduler.Job() first") ErrCronParseFailure = errors.New("cron expression failed to be parsed") ErrInvalidDaysOfMonthDuplicateValue = errors.New("duplicate days of month is not allowed in Month() and Months() methods") ) func wrapOrError(toWrap error, err error) error { var returnErr error if toWrap != nil && !errors.Is(err, toWrap) { returnErr = fmt.Errorf("%s: %w", err, toWrap) } else { returnErr = err } return returnErr } // 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 schedulingUnit int const ( // default unit is seconds milliseconds schedulingUnit = iota seconds minutes hours days weeks months duration crontab ) func callJobFunc(jobFunc interface{}) { if jobFunc != nil { reflect.ValueOf(jobFunc).Call([]reflect.Value{}) } } func callJobFuncWithParams(jobFunc interface{}, params []interface{}) { f := reflect.ValueOf(jobFunc) if len(params) != f.Type().NumIn() { return } in := make([]reflect.Value, len(params)) for k, param := range params { in[k] = reflect.ValueOf(param) } f.Call(in) } func getFunctionName(fn interface{}) string { return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() } 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 } golang-github-go-co-op-gocron-1.18.0/gocron_test.go000066400000000000000000000066771436572230600221400ustar00rootroot00000000000000package gocron import ( "errors" "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) } }) } } func Test_callJobFuncWithParams(t *testing.T) { type args struct { jobFunc interface{} params []interface{} } tests := []struct { name string args args err bool }{ { name: "test call func with no args", args: args{ jobFunc: func() {}, params: nil, }, }, { name: "test call func with single arg", args: args{ jobFunc: func(arg string) {}, params: []interface{}{"test"}, }, }, { name: "test call func with wrong arg type", args: args{ jobFunc: func(arg int) {}, params: []interface{}{"test"}, }, err: true, }, { name: "test call func with wrong arg count", args: args{ jobFunc: func(arg int) {}, params: []interface{}{}, }, err: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := panicFnToErr(func() { callJobFuncWithParams(tt.args.jobFunc, tt.args.params) }) if err != nil && !tt.err { t.Fatalf("unexpected panic: %s", err.Error()) } }) } } func panicFnToErr(fn func()) (err error) { defer func() { if r := recover(); r != nil { err = errors.New("func panic") } }() fn() return err } func Test_getFunctionName(t *testing.T) { type args struct { fn interface{} } tests := []struct { name string args args want string }{ { name: "test get function name", args: args{ fn: Test_getFunctionName, }, want: "github.com/go-co-op/gocron.Test_getFunctionName", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, tt.want, getFunctionName(tt.args.fn), "getFunctionName(%v)", tt.args.fn) }) } } golang-github-go-co-op-gocron-1.18.0/job.go000066400000000000000000000302431436572230600203460ustar00rootroot00000000000000package gocron import ( "context" "fmt" "math/rand" "sort" "sync" "sync/atomic" "time" "github.com/robfig/cron/v3" "golang.org/x/sync/singleflight" ) // Job struct stores the information necessary to run a Job type Job struct { mu *jobMutex jobFunction interval int // interval * unit between runs random // details for randomness duration time.Duration // time duration between runs unit schedulingUnit // time units, e.g. 'minutes', 'hours'... startsImmediately bool // if the Job should run upon scheduler start atTimes []time.Duration // optional time(s) at which this Job runs when interval is day startAtTime time.Time // optional time at which the Job starts error error // error related to Job lastRun time.Time // datetime of last run nextRun time.Time // datetime of next run scheduledWeekdays []time.Weekday // Specific days of the week to start on daysOfTheMonth []int // Specific days of the month to run the job tags []string // allow the user to tag Jobs with certain labels runCount int // number of times the job ran timer *time.Timer // handles running tasks at specific time cronSchedule cron.Schedule // stores the schedule when a task uses cron runWithDetails bool // when true the job is passed as the last arg of the jobFunc } type random struct { rand *rand.Rand randomizeInterval bool // whether the interval is random randomIntervalRange [2]int // random interval range } type jobFunction struct { eventListeners // additional functions to allow run 'em during job performing function interface{} // task's function parameters []interface{} // task's function parameters parametersLen int // length of the passed parameters name string //nolint the function name to run runConfig runConfig // configuration for how many times to run the job limiter *singleflight.Group // limits inflight runs of job to one ctx context.Context // for cancellation cancel context.CancelFunc // for cancellation runState *int64 // will be non-zero when jobs are running } type eventListeners struct { onBeforeJobExecution interface{} // performs before job executing onAfterJobExecution interface{} // performs after job executing } type jobMutex struct { sync.RWMutex } func (jf *jobFunction) incrementRunState() { if jf.runState != nil { atomic.AddInt64(jf.runState, 1) } } func (jf *jobFunction) decrementRunState() { if jf.runState != nil { atomic.AddInt64(jf.runState, -1) } } func (jf *jobFunction) copy() jobFunction { cp := jobFunction{ eventListeners: jf.eventListeners, function: jf.function, parameters: nil, parametersLen: jf.parametersLen, name: jf.name, runConfig: jf.runConfig, limiter: jf.limiter, ctx: jf.ctx, cancel: jf.cancel, runState: jf.runState, } cp.parameters = append(cp.parameters, jf.parameters...) return cp } type runConfig struct { finiteRuns bool maxRuns int mode mode } // mode is the Job's running mode type mode int8 const ( // defaultMode disable any mode defaultMode mode = iota // singletonMode switch to single job mode singletonMode ) // newJob creates a new Job with the provided interval func newJob(interval int, startImmediately bool, singletonMode bool) *Job { ctx, cancel := context.WithCancel(context.Background()) var zero int64 job := &Job{ mu: &jobMutex{}, interval: interval, unit: seconds, lastRun: time.Time{}, nextRun: time.Time{}, jobFunction: jobFunction{ ctx: ctx, cancel: cancel, runState: &zero, }, tags: []string{}, startsImmediately: startImmediately, } if singletonMode { job.SingletonMode() } return job } func (j *Job) setRandomInterval(a, b int) { j.random.rand = rand.New(rand.NewSource(time.Now().UnixNano())) // nolint j.random.randomizeInterval = true if a < b { j.random.randomIntervalRange[0] = a j.random.randomIntervalRange[1] = b + 1 } else { j.random.randomIntervalRange[0] = b j.random.randomIntervalRange[1] = a + 1 } } func (j *Job) getRandomInterval() int { randNum := j.rand.Intn(j.randomIntervalRange[1] - j.randomIntervalRange[0]) return j.randomIntervalRange[0] + randNum } func (j *Job) getInterval() int { if j.randomizeInterval { return j.getRandomInterval() } return j.interval } func (j *Job) neverRan() bool { jobLastRun := j.LastRun() return jobLastRun.IsZero() } func (j *Job) getStartsImmediately() bool { return j.startsImmediately } func (j *Job) setStartsImmediately(b bool) { j.startsImmediately = b } func (j *Job) setTimer(t *time.Timer) { j.mu.Lock() defer j.mu.Unlock() j.timer = t } func (j *Job) getFirstAtTime() time.Duration { var t time.Duration if len(j.atTimes) > 0 { t = j.atTimes[0] } return t } func (j *Job) getAtTime(lastRun time.Time) time.Duration { var r time.Duration if len(j.atTimes) == 0 { return r } if len(j.atTimes) == 1 { return j.atTimes[0] } if lastRun.IsZero() { r = j.atTimes[0] } else { for _, d := range j.atTimes { nt := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, lastRun.Location()).Add(d) if nt.After(lastRun) { r = d break } } } return r } func (j *Job) addAtTime(t time.Duration) { if len(j.atTimes) == 0 { j.atTimes = append(j.atTimes, t) return } exist := false index := sort.Search(len(j.atTimes), func(i int) bool { atTime := j.atTimes[i] b := atTime >= t if b { exist = atTime == t } return b }) // ignore if present if exist { return } j.atTimes = append(j.atTimes, time.Duration(0)) copy(j.atTimes[index+1:], j.atTimes[index:]) j.atTimes[index] = t } func (j *Job) getStartAtTime() time.Time { return j.startAtTime } func (j *Job) setStartAtTime(t time.Time) { j.startAtTime = t } func (j *Job) getUnit() schedulingUnit { j.mu.RLock() defer j.mu.RUnlock() return j.unit } func (j *Job) setUnit(t schedulingUnit) { j.mu.Lock() defer j.mu.Unlock() j.unit = t } func (j *Job) getDuration() time.Duration { j.mu.RLock() defer j.mu.RUnlock() return j.duration } func (j *Job) setDuration(t time.Duration) { j.mu.Lock() defer j.mu.Unlock() j.duration = t } // hasTags returns true if all tags are matched on this Job func (j *Job) hasTags(tags ...string) bool { // Build map of all Job tags for easy comparison jobTags := map[string]int{} for _, tag := range j.tags { jobTags[tag] = 0 } // Loop through required tags and if one doesn't exist, return false for _, tag := range tags { _, ok := jobTags[tag] if !ok { return false } } return true } // Error returns an error if one occurred while creating the Job. // If multiple errors occurred, they will be wrapped and can be // checked using the standard unwrap options. func (j *Job) Error() error { return j.error } // Tag allows you to add arbitrary labels to a Job that do not // impact the functionality of the Job func (j *Job) Tag(tags ...string) { j.tags = append(j.tags, tags...) } // Untag removes a tag from a Job func (j *Job) Untag(t string) { 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 { return j.tags } // SetEventListeners accepts two functions that will be called, one before and one after the job is run func (j *Job) SetEventListeners(onBeforeJobExecution interface{}, onAfterJobExecution interface{}) { j.eventListeners = eventListeners{ onBeforeJobExecution: onBeforeJobExecution, onAfterJobExecution: onAfterJobExecution, } } // ScheduledTime returns the time of the Job's next scheduled run func (j *Job) ScheduledTime() time.Time { j.mu.RLock() defer j.mu.RUnlock() return j.nextRun } // ScheduledAtTime returns the specific time of day the Job will run at. // If multiple times are set, the earliest time will be returned. func (j *Job) ScheduledAtTime() string { if len(j.atTimes) == 0 { return "0:0" } return fmt.Sprintf("%d:%d", j.getFirstAtTime()/time.Hour, (j.getFirstAtTime()%time.Hour)/time.Minute) } // ScheduledAtTimes returns the specific times of day the Job will run at func (j *Job) ScheduledAtTimes() []string { r := make([]string, len(j.atTimes)) for i, t := range j.atTimes { r[i] = fmt.Sprintf("%d:%d", t/time.Hour, (t%time.Hour)/time.Minute) } return r } // 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) { if len(j.scheduledWeekdays) == 0 { return time.Sunday, ErrNotScheduledWeekday } return j.scheduledWeekdays[0], nil } // Weekdays returns a slice of time.Weekday that the Job will run in a week and // will return an error if the Job is not scheduled weekly func (j *Job) Weekdays() []time.Weekday { // appending on j.scheduledWeekdays may cause a side effect if len(j.scheduledWeekdays) == 0 { return []time.Weekday{time.Sunday} } return j.scheduledWeekdays } // LimitRunsTo limits the number of executions of this job to n. // Upon reaching the limit, the job is removed from the scheduler. // // Note: If a job is added to a running scheduler and this method is then used // you may see the job run more than the set limit as job is scheduled immediately // by default upon being added to the scheduler. It is recommended to use the // LimitRunsTo() func on the scheduler chain when scheduling the job. // For example: scheduler.LimitRunsTo(1).Do() func (j *Job) LimitRunsTo(n int) { j.mu.Lock() defer j.mu.Unlock() j.runConfig.finiteRuns = true j.runConfig.maxRuns = n } // SingletonMode prevents a new job from starting if the prior job has not yet // completed it's run // Note: If a job is added to a running scheduler and this method is then used // you may see the job run overrun itself as job is scheduled immediately // by default upon being added to the scheduler. It is recommended to use the // SingletonMode() func on the scheduler chain when scheduling the job. func (j *Job) SingletonMode() { j.mu.Lock() defer j.mu.Unlock() j.runConfig.mode = singletonMode j.jobFunction.limiter = &singleflight.Group{} } // shouldRun evaluates if this job should run again // based on the runConfig func (j *Job) shouldRun() bool { j.mu.RLock() defer j.mu.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.mu.RLock() defer j.mu.RUnlock() return j.lastRun } func (j *Job) setLastRun(t time.Time) { j.lastRun = t } // NextRun returns the time the job will run next func (j *Job) NextRun() time.Time { j.mu.RLock() defer j.mu.RUnlock() return j.nextRun } func (j *Job) setNextRun(t time.Time) { j.mu.Lock() defer j.mu.Unlock() j.nextRun = t } // RunCount returns the number of time the job ran so far func (j *Job) RunCount() int { j.mu.Lock() defer j.mu.Unlock() return j.runCount } func (j *Job) stop() { j.mu.Lock() defer j.mu.Unlock() if j.timer != nil { j.timer.Stop() } if j.cancel != nil { j.cancel() } } // IsRunning reports whether any instances of the job function are currently running func (j *Job) IsRunning() bool { return atomic.LoadInt64(j.runState) != 0 } // you must lock the job before calling copy func (j *Job) copy() Job { return Job{ mu: &jobMutex{}, jobFunction: j.jobFunction, interval: j.interval, duration: j.duration, unit: j.unit, startsImmediately: j.startsImmediately, atTimes: j.atTimes, startAtTime: j.startAtTime, error: j.error, lastRun: j.lastRun, nextRun: j.nextRun, scheduledWeekdays: j.scheduledWeekdays, daysOfTheMonth: j.daysOfTheMonth, tags: j.tags, runCount: j.runCount, timer: j.timer, cronSchedule: j.cronSchedule, runWithDetails: j.runWithDetails, } } golang-github-go-co-op-gocron-1.18.0/job_test.go000066400000000000000000000130741436572230600214100ustar00rootroot00000000000000package gocron import ( "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) 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 TestHasTags(t *testing.T) { tests := []struct { name string jobTags []string matchTags []string expected bool }{ { "OneTagMatch", []string{"tag1"}, []string{"tag1"}, true, }, { "OneTagNoMatch", []string{"tag1"}, []string{"tag2"}, false, }, { "DuplicateJobTagsMatch", []string{"tag1", "tag1"}, []string{"tag1"}, true, }, { "DuplicateInputTagsMatch", []string{"tag1"}, []string{"tag1", "tag1"}, true, }, { "MultipleTagsMatch", []string{"tag1", "tag2"}, []string{"tag2", "tag1"}, true, }, { "MultipleTagsNoMatch", []string{"tag1", "tag2"}, []string{"tag2", "tag1", "tag3"}, false, }, { "MultipleDuplicateTagsMatch", []string{"tag1", "tag1", "tag1", "tag2"}, []string{"tag1", "tag2"}, true, }, { "MultipleDuplicateTagsNoMatch", []string{"tag1", "tag1", "tag1"}, []string{"tag1", "tag1", "tag3"}, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { j, _ := NewScheduler(time.UTC).Every(1).Minute().Do(task) j.Tag(tt.jobTags...) assert.Equal(t, tt.expected, j.hasTags(tt.matchTags...)) }) } } func TestJob_IsRunning(t *testing.T) { s := NewScheduler(time.UTC) j, err := s.Every(10).Seconds().Do(func() { time.Sleep(2 * time.Second) }) require.NoError(t, err) assert.False(t, j.IsRunning()) s.StartAsync() time.Sleep(time.Second) assert.True(t, j.IsRunning()) time.Sleep(time.Second) s.Stop() assert.False(t, j.IsRunning()) } func TestGetScheduledTime(t *testing.T) { t.Run("valid", func(t *testing.T) { j, err := NewScheduler(time.UTC).Every(1).Day().At("10:30").Do(task) require.NoError(t, err) assert.Equal(t, "10:30", j.ScheduledAtTime()) }) t.Run("invalid", func(t *testing.T) { j, err := NewScheduler(time.UTC).Every(1).Minute().At("10:30").Do(task) assert.EqualError(t, err, ErrAtTimeNotSupported.Error()) assert.Nil(t, j) }) } 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{ mu: &jobMutex{}, jobFunction: jobFunction{ 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.runCount++ assert.Equal(t, j.shouldRun(), true, "Expecting it to run again") j.runCount++ 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() s.Stop() assert.False(t, j.NextRun().IsZero()) j.runCount = 5 assert.Equal(t, 5, j.RunCount()) lastRun := time.Now() j.mu.Lock() j.lastRun = lastRun j.mu.Unlock() assert.Equal(t, lastRun, j.LastRun()) } func TestJob_SetEventListeners(t *testing.T) { t.Run("run event listeners callbacks for a job", func(t *testing.T) { var ( jobRanPassed = false beforeCallbackPassed = false afterCallbackPassed = false wg = &sync.WaitGroup{} ) wg.Add(1) s := NewScheduler(time.UTC) job, err := s.Tag("tag1").Every("1ms").Do(func() { jobRanPassed = true }) job.SetEventListeners(func() { beforeCallbackPassed = true }, func() { defer wg.Done() afterCallbackPassed = true }) s.StartAsync() s.stop() wg.Wait() require.NoError(t, err) assert.True(t, jobRanPassed) assert.True(t, beforeCallbackPassed) assert.True(t, afterCallbackPassed) }) } golang-github-go-co-op-gocron-1.18.0/scheduler.go000066400000000000000000001067051436572230600215610ustar00rootroot00000000000000package gocron import ( "context" "fmt" "reflect" "sort" "strings" "sync" "time" "github.com/robfig/cron/v3" "golang.org/x/sync/semaphore" ) type limitMode int8 // Scheduler struct stores a list of Jobs and the location of time used by the Scheduler, // and 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 time TimeWrapper // wrapper around time.Time timer func(d time.Duration, f func()) *time.Timer executor *executor // executes jobs passed via chan tags sync.Map // for storing tags when unique tags is set tagsUnique bool // defines whether tags should be unique updateJob bool // so the scheduler knows to create a new job or update the current waitForInterval bool // defaults jobs to waiting for first interval to start singletonMode bool // defaults all jobs to use SingletonMode() jobCreated bool // so the scheduler knows a job was created prior to calling Every or Cron startBlockingStopChanMutex sync.Mutex startBlockingStopChan chan struct{} // stops the scheduler } // days in a week const allWeekDays = 7 // NewScheduler creates a new Scheduler func NewScheduler(loc *time.Location) *Scheduler { executor := newExecutor() return &Scheduler{ jobs: make([]*Job, 0), location: loc, running: false, time: &trueTime{}, executor: &executor, tagsUnique: false, timer: afterFunc, } } // SetMaxConcurrentJobs limits how many jobs can be running at the same time. // This is useful when running resource intensive jobs and a precise start time is not critical. func (s *Scheduler) SetMaxConcurrentJobs(n int, mode limitMode) { s.executor.maxRunningJobs = semaphore.NewWeighted(int64(n)) s.executor.limitMode = mode } // StartBlocking starts all jobs and blocks the current thread. // This blocking method can be stopped with Stop() from a separate goroutine. func (s *Scheduler) StartBlocking() { s.StartAsync() s.startBlockingStopChanMutex.Lock() s.startBlockingStopChan = make(chan struct{}, 1) s.startBlockingStopChanMutex.Unlock() <-s.startBlockingStopChan } // StartAsync starts all jobs without blocking the current thread func (s *Scheduler) StartAsync() { if !s.IsRunning() { s.start() } } // start starts the scheduler, scheduling and running jobs func (s *Scheduler) start() { go s.executor.start() s.setRunning(true) s.runJobs(s.Jobs()) } func (s *Scheduler) runJobs(jobs []*Job) { for _, job := range jobs { s.runContinuous(job) } } 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 places each job into the other job's position given // the provided job indexes. 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] } // Less compares the next run of jobs based on their index. // Returns true if the second job is after the first. func (s *Scheduler) Less(first, second int) bool { return s.Jobs()[second].NextRun().Unix() >= s.Jobs()[first].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 } type nextRun struct { duration time.Duration dateTime time.Time } // scheduleNextRun Compute the instant when this Job should run next func (s *Scheduler) scheduleNextRun(job *Job) (bool, nextRun) { now := s.now() if !s.jobPresent(job) { return false, nextRun{} } lastRun := now if job.neverRan() { // Increment startAtTime to the future if !job.startAtTime.IsZero() && job.startAtTime.Before(now) { duration := s.durationToNextRun(job.startAtTime, job).duration job.startAtTime = job.startAtTime.Add(duration) if job.startAtTime.Before(now) { diff := now.Sub(job.startAtTime) duration := s.durationToNextRun(job.startAtTime, job).duration count := diff / duration if diff%duration != 0 { count++ } job.startAtTime = job.startAtTime.Add(duration * count) } } } else { lastRun = job.LastRun() } if !job.shouldRun() { s.RemoveByReference(job) return false, nextRun{} } next := s.durationToNextRun(lastRun, job) job.setLastRun(job.NextRun()) if next.dateTime.IsZero() { next.dateTime = lastRun.Add(next.duration) job.setNextRun(next.dateTime) } else { job.setNextRun(next.dateTime) } return true, next } // durationToNextRun calculate how much time to the next run, depending on unit func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) nextRun { // job can be scheduled with .StartAt() if job.getStartAtTime().After(lastRun) { return nextRun{duration: job.getStartAtTime().Sub(s.now()), dateTime: job.getStartAtTime()} } var next nextRun switch job.getUnit() { case milliseconds, seconds, minutes, hours: next.duration = s.calculateDuration(job) case days: next = s.calculateDays(job, lastRun) case weeks: if len(job.scheduledWeekdays) != 0 { // weekday selected, Every().Monday(), for example next = s.calculateWeekday(job, lastRun) } else { next = s.calculateWeeks(job, lastRun) } case months: next = s.calculateMonths(job, lastRun) case duration: next.duration = job.getDuration() case crontab: next.dateTime = job.cronSchedule.Next(lastRun) next.duration = next.dateTime.Sub(lastRun) } return next } func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun { lastRunRoundedMidnight := s.roundToMidnight(lastRun) // Special case: the last day of the month if len(job.daysOfTheMonth) == 1 && job.daysOfTheMonth[0] == -1 { return calculateNextRunForLastDayOfMonth(s, job, lastRun) } if len(job.daysOfTheMonth) != 0 { // calculate days to job.daysOfTheMonth nextRunDateMap := make(map[int]nextRun) for _, day := range job.daysOfTheMonth { nextRunDateMap[day] = calculateNextRunForMonth(s, job, lastRun, day) } nextRunResult := nextRun{} for _, val := range nextRunDateMap { if nextRunResult.dateTime.IsZero() { nextRunResult = val } else if nextRunResult.dateTime.Sub(val.dateTime).Milliseconds() > 0 { nextRunResult = val } } return nextRunResult } next := lastRunRoundedMidnight.Add(job.getFirstAtTime()).AddDate(0, job.getInterval(), 0) return nextRun{duration: until(lastRun, next), dateTime: next} } func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time) nextRun { // Calculate the last day of the next month, by adding job.interval+1 months (i.e. the // first day of the month after the next month), and subtracting one day, unless the // last run occurred before the end of the month. addMonth := job.getInterval() atTime := job.getAtTime(lastRun) if testDate := lastRun.AddDate(0, 0, 1); testDate.Month() != lastRun.Month() && !s.roundToMidnight(lastRun).Add(atTime).After(lastRun) { // Our last run was on the last day of this month. addMonth++ atTime = job.getFirstAtTime() } next := time.Date(lastRun.Year(), lastRun.Month(), 1, 0, 0, 0, 0, s.Location()). Add(atTime). AddDate(0, addMonth, 0). AddDate(0, 0, -1) return nextRun{duration: until(lastRun, next), dateTime: next} } func calculateNextRunForMonth(s *Scheduler, job *Job, lastRun time.Time, dayOfMonth int) nextRun { atTime := job.getAtTime(lastRun) natTime := atTime jobDay := time.Date(lastRun.Year(), lastRun.Month(), dayOfMonth, 0, 0, 0, 0, s.Location()).Add(atTime) difference := absDuration(lastRun.Sub(jobDay)) next := lastRun if jobDay.Before(lastRun) { // shouldn't run this month; schedule for next interval minus day difference next = next.AddDate(0, job.getInterval(), -0) next = next.Add(-difference) natTime = job.getFirstAtTime() } else { if job.getInterval() == 1 && !jobDay.Equal(lastRun) { // every month counts current month next = next.AddDate(0, job.getInterval()-1, 0) } else { // should run next month interval next = next.AddDate(0, job.getInterval(), 0) natTime = job.getFirstAtTime() } next = next.Add(difference) } if atTime != natTime { next = next.Add(-atTime).Add(natTime) } return nextRun{duration: until(lastRun, next), dateTime: next} } func (s *Scheduler) calculateWeekday(job *Job, lastRun time.Time) nextRun { daysToWeekday := s.remainingDaysToWeekday(lastRun, job) totalDaysDifference := s.calculateTotalDaysDifference(lastRun, daysToWeekday, job) acTime := job.getAtTime(lastRun) if totalDaysDifference > 0 { acTime = job.getFirstAtTime() } next := s.roundToMidnight(lastRun).Add(acTime).AddDate(0, 0, totalDaysDifference) return nextRun{duration: until(lastRun, next), dateTime: next} } func (s *Scheduler) calculateWeeks(job *Job, lastRun time.Time) nextRun { totalDaysDifference := int(job.getInterval()) * 7 next := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, totalDaysDifference) return nextRun{duration: until(lastRun, next), dateTime: next} } func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekday int, job *Job) int { if job.getInterval() > 1 { // just count weeks after the first jobs were done if job.RunCount() < len(job.Weekdays()) { return daysToWeekday } if daysToWeekday > 0 { return int(job.getInterval())*7 - (allWeekDays - daysToWeekday) } return int(job.getInterval()) * 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(lastRun)) if lastRun.Before(lastRunAtTime) { return 0 } return 7 } return daysToWeekday } func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) nextRun { if job.getInterval() == 1 { lastRunDayPlusJobAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime(lastRun)) if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) { return nextRun{duration: until(lastRun, lastRunDayPlusJobAtTime), dateTime: lastRunDayPlusJobAtTime} } } nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, job.getInterval()).In(s.Location()) return nextRun{duration: until(lastRun, nextRunAtTime), dateTime: nextRunAtTime} } func 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 in(scheduleWeekdays []time.Weekday, weekday time.Weekday) bool { in := false for _, weekdayInSchedule := range scheduleWeekdays { if int(weekdayInSchedule) == int(weekday) { in = true break } } return in } func (s *Scheduler) calculateDuration(job *Job) time.Duration { 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 now := s.time.Now(s.location) next := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, s.Location()).Add(job.getFirstAtTime()) if now.Before(next) || now.Equal(next) { return next.Sub(now) } } interval := job.getInterval() switch job.getUnit() { case milliseconds: return time.Duration(interval) * time.Millisecond 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 { jobLastRun := job.LastRun() return job.getAtTime(jobLastRun) != 0 } func (s *Scheduler) remainingDaysToWeekday(lastRun time.Time, job *Job) int { weekDays := job.Weekdays() sort.Slice(weekDays, func(i, j int) bool { return weekDays[i] < weekDays[j] }) equals := false lastRunWeekday := lastRun.Weekday() index := sort.Search(len(weekDays), func(i int) bool { b := weekDays[i] >= lastRunWeekday if b { equals = weekDays[i] == lastRunWeekday } return b }) // check atTime if equals { if s.roundToMidnight(lastRun).Add(job.getAtTime(lastRun)).After(lastRun) { return 0 } index++ } if index < len(weekDays) { return int(weekDays[index] - lastRunWeekday) } return int(weekDays[0]) + allWeekDays - int(lastRunWeekday) } // absDuration returns the abs time difference func absDuration(a time.Duration) time.Duration { if a >= 0 { return a } return -a } // 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.now() } sort.Sort(s) return s.Jobs()[0], s.Jobs()[0].NextRun() } // EveryRandom schedules a new period Job that runs at random intervals // between the provided lower (inclusive) and upper (inclusive) bounds. // The default unit is Seconds(). Call a different unit in the chain // if you would like to change that. For example, Minutes(), Hours(), etc. func (s *Scheduler) EveryRandom(lower, upper int) *Scheduler { job := s.newJob(0) if s.updateJob || s.jobCreated { job = s.getCurrentJob() } job.setRandomInterval(lower, upper) if s.updateJob || s.jobCreated { s.setJobs(append(s.Jobs()[:len(s.Jobs())-1], job)) if s.jobCreated { s.jobCreated = false } } else { s.setJobs(append(s.Jobs(), job)) } return s } // Every schedules a new periodic Job with an interval. // Interval can be an int, time.Duration or a string that // parses with time.ParseDuration(). // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". func (s *Scheduler) Every(interval interface{}) *Scheduler { job := s.newJob(0) if s.updateJob || s.jobCreated { job = s.getCurrentJob() } switch interval := interval.(type) { case int: job.interval = interval if interval <= 0 { job.error = wrapOrError(job.error, ErrInvalidInterval) } case time.Duration: job.interval = 0 job.setDuration(interval) job.setUnit(duration) case string: d, err := time.ParseDuration(interval) if err != nil { job.error = wrapOrError(job.error, err) } job.setDuration(d) job.setUnit(duration) default: job.error = wrapOrError(job.error, ErrInvalidIntervalType) } if s.updateJob || s.jobCreated { s.setJobs(append(s.Jobs()[:len(s.Jobs())-1], job)) if s.jobCreated { s.jobCreated = false } } else { s.setJobs(append(s.Jobs(), job)) } return s } func (s *Scheduler) run(job *Job) { if !s.IsRunning() { return } job.mu.Lock() if job.function == nil { job.mu.Unlock() s.Remove(job) return } defer job.mu.Unlock() if job.runWithDetails { switch len(job.parameters) { case job.parametersLen: job.parameters = append(job.parameters, job.copy()) case job.parametersLen + 1: job.parameters[job.parametersLen] = job.copy() default: // something is really wrong and we should never get here job.error = wrapOrError(job.error, ErrInvalidFunctionParameters) return } } s.executor.jobFunctions <- job.jobFunction.copy() job.runCount++ } func (s *Scheduler) runContinuous(job *Job) { shouldRun, next := s.scheduleNextRun(job) if !shouldRun { return } if !job.getStartsImmediately() { job.setStartsImmediately(true) } else { s.run(job) } nextRun := next.dateTime.Sub(s.now()) if nextRun < 0 { time.Sleep(absDuration(nextRun)) shouldRun, next := s.scheduleNextRun(job) if !shouldRun { return } nextRun = next.dateTime.Sub(s.now()) } job.setTimer(s.timer(nextRun, func() { if !next.dateTime.IsZero() { for { n := s.now().UnixNano() - next.dateTime.UnixNano() if n >= 0 { break } s.time.Sleep(time.Duration(n)) } } s.runContinuous(job) })) } // 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 the provided delay in between each job func (s *Scheduler) RunAllWithDelay(d time.Duration) { for _, job := range s.Jobs() { s.run(job) s.time.Sleep(d) } } // RunByTag runs all the jobs containing a specific tag // regardless of whether they are scheduled to run or not func (s *Scheduler) RunByTag(tag string) error { return s.RunByTagWithDelay(tag, 0) } // RunByTagWithDelay is same as RunByTag but introduces a delay between // each job execution func (s *Scheduler) RunByTagWithDelay(tag string, d time.Duration) error { jobs, err := s.FindJobsByTag(tag) if err != nil { return err } for _, job := range jobs { s.run(job) s.time.Sleep(d) } return nil } // Remove specific Job by function // // Removing a job stops that job's timer. However, if a job has already // been started by by the job's timer before being removed, there is no way to stop // it through gocron as https://pkg.go.dev/time#Timer.Stop explains. // The job function would need to have implemented a means of // stopping, e.g. using a context.WithCancel(). func (s *Scheduler) Remove(job interface{}) { fName := getFunctionName(job) j := s.findJobByTaskName(fName) s.removeJobsUniqueTags(j) s.removeByCondition(func(someJob *Job) bool { return someJob.name == fName }) } // RemoveByReference removes specific Job by reference func (s *Scheduler) RemoveByReference(job *Job) { s.removeJobsUniqueTags(job) s.removeByCondition(func(someJob *Job) bool { job.mu.RLock() defer job.mu.RUnlock() return someJob == job }) } func (s *Scheduler) findJobByTaskName(name string) *Job { for _, job := range s.Jobs() { if job.name == name { return job } } return nil } func (s *Scheduler) removeJobsUniqueTags(job *Job) { if job == nil { return } if s.tagsUnique && len(job.tags) > 0 { for _, tag := range job.tags { s.tags.Delete(tag) } } } func (s *Scheduler) removeByCondition(shouldRemove func(*Job) bool) { retainedJobs := make([]*Job, 0) for _, job := range s.Jobs() { if !shouldRemove(job) { retainedJobs = append(retainedJobs, job) } else { job.stop() } } s.setJobs(retainedJobs) } // RemoveByTag will remove Jobs that match the given tag. func (s *Scheduler) RemoveByTag(tag string) error { return s.RemoveByTags(tag) } // RemoveByTags will remove Jobs that match all given tags. func (s *Scheduler) RemoveByTags(tags ...string) error { jobs, err := s.FindJobsByTag(tags...) if err != nil { return err } for _, job := range jobs { s.RemoveByReference(job) } return nil } // RemoveByTagsAny will remove Jobs that match any one of the given tags. func (s *Scheduler) RemoveByTagsAny(tags ...string) error { var errs error mJob := make(map[*Job]struct{}) for _, tag := range tags { jobs, err := s.FindJobsByTag(tag) if err != nil { errs = wrapOrError(errs, fmt.Errorf("%s: %s", err.Error(), tag)) } for _, job := range jobs { mJob[job] = struct{}{} } } for job := range mJob { s.RemoveByReference(job) } return errs } // FindJobsByTag will return a slice of Jobs that match all given tags func (s *Scheduler) FindJobsByTag(tags ...string) ([]*Job, error) { var jobs []*Job Jobs: for _, job := range s.Jobs() { if job.hasTags(tags...) { jobs = append(jobs, job) continue Jobs } } if len(jobs) > 0 { return jobs, nil } return nil, ErrJobNotFoundWithTag } // MonthFirstWeekday sets the job to run the first specified weekday of the month func (s *Scheduler) MonthFirstWeekday(weekday time.Weekday) *Scheduler { _, month, day := s.time.Now(time.UTC).Date() if day < 7 { return s.Cron(fmt.Sprintf("0 0 %d %d %d", day, month, weekday)) } return s.Cron(fmt.Sprintf("0 0 %d %d %d", day, month+1, weekday)) } // LimitRunsTo limits the number of executions of this job to n. // Upon reaching the limit, the job is removed from the scheduler. func (s *Scheduler) LimitRunsTo(i int) *Scheduler { job := s.getCurrentJob() job.LimitRunsTo(i) return s } // SingletonMode prevents a new job from starting if the prior job has not yet // completed its run func (s *Scheduler) SingletonMode() *Scheduler { job := s.getCurrentJob() job.SingletonMode() return s } // SingletonModeAll prevents new jobs from starting if the prior instance of the // particular job has not yet completed its run func (s *Scheduler) SingletonModeAll() { s.singletonMode = true } // TaskPresent checks if specific job's function was added to the scheduler. func (s *Scheduler) TaskPresent(j interface{}) bool { for _, job := range s.Jobs() { if job.name == getFunctionName(j) { return true } } return false } // To avoid the recursive read lock on s.Jobs() and this function, // creating this new function and distributing the lock between jobPresent, _jobPresent func (s *Scheduler) _jobPresent(j *Job, jobs []*Job) bool { s.jobsMutex.RLock() defer s.jobsMutex.RUnlock() for _, job := range jobs { if job == j { return true } } return false } func (s *Scheduler) jobPresent(j *Job) bool { return s._jobPresent(j, s.Jobs()) } // Clear clears all Jobs from this scheduler func (s *Scheduler) Clear() { for _, job := range s.Jobs() { job.stop() } s.setJobs(make([]*Job, 0)) // If unique tags was enabled, delete all the tags loaded in the tags sync.Map if s.tagsUnique { s.tags.Range(func(key interface{}, value interface{}) bool { s.tags.Delete(key) return true }) } } // Stop stops the scheduler. This is a no-op if the scheduler is already stopped. // It waits for all running jobs to finish before returning, so it is safe to assume that running jobs will finish when calling this. func (s *Scheduler) Stop() { if s.IsRunning() { s.stop() } } func (s *Scheduler) stop() { s.setRunning(false) s.stopJobs(s.jobs) s.executor.stop() s.StopBlockingChan() } func (s *Scheduler) stopJobs(jobs []*Job) { for _, job := range jobs { job.stop() } } func (s *Scheduler) doCommon(jobFun interface{}, params ...interface{}) (*Job, error) { job := s.getCurrentJob() jobUnit := job.getUnit() jobLastRun := job.LastRun() if job.getAtTime(jobLastRun) != 0 && (jobUnit <= hours || jobUnit >= duration) { job.error = wrapOrError(job.error, ErrAtTimeNotSupported) } if len(job.scheduledWeekdays) != 0 && jobUnit != weeks { job.error = wrapOrError(job.error, ErrWeekdayNotSupported) } if job.unit != crontab && job.getInterval() == 0 { if job.unit != duration { job.error = wrapOrError(job.error, ErrInvalidInterval) } } if job.error != nil { // delete the job from the scheduler as this job // cannot be executed s.RemoveByReference(job) return nil, job.error } typ := reflect.TypeOf(jobFun) if typ.Kind() != reflect.Func { // delete the job for the same reason as above s.RemoveByReference(job) return nil, ErrNotAFunction } fname := getFunctionName(jobFun) if job.name != fname { job.function = jobFun job.parameters = params job.name = fname } f := reflect.ValueOf(jobFun) expectedParamLength := f.Type().NumIn() if job.runWithDetails { expectedParamLength-- } if len(params) != expectedParamLength { s.RemoveByReference(job) job.error = wrapOrError(job.error, ErrWrongParams) return nil, job.error } if job.runWithDetails && f.Type().In(len(params)).Kind() != reflect.ValueOf(*job).Kind() { s.RemoveByReference(job) job.error = wrapOrError(job.error, ErrDoWithJobDetails) return nil, job.error } // we should not schedule if not running since we can't foresee how long it will take for the scheduler to start if s.IsRunning() { s.runContinuous(job) } return job, nil } // Do specifies the jobFunc that should be called every time the Job runs func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error) { return s.doCommon(jobFun, params...) } // DoWithJobDetails specifies the jobFunc that should be called every time the Job runs // and additionally passes the details of the current job to the jobFunc. // The last argument of the function must be a gocron.Job that will be passed by // the scheduler when the function is called. func (s *Scheduler) DoWithJobDetails(jobFun interface{}, params ...interface{}) (*Job, error) { job := s.getCurrentJob() job.runWithDetails = true job.parametersLen = len(params) return s.doCommon(jobFun, params...) } // At schedules the Job at a specific time of day in the form "HH:MM:SS" or "HH:MM" // or time.Time (note that only the hours, minutes, seconds and nanos are used). func (s *Scheduler) At(i interface{}) *Scheduler { job := s.getCurrentJob() switch t := i.(type) { case string: for _, tt := range strings.Split(t, ";") { hour, min, sec, err := parseTime(tt) if err != nil { job.error = wrapOrError(job.error, err) return s } // save atTime start as duration from midnight job.addAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second) } case time.Time: job.addAtTime(time.Duration(t.Hour())*time.Hour + time.Duration(t.Minute())*time.Minute + time.Duration(t.Second())*time.Second + time.Duration(t.Nanosecond())*time.Nanosecond) default: job.error = wrapOrError(job.error, ErrUnsupportedTimeFormat) } job.startsImmediately = false return s } // Tag will add a tag when creating a job. func (s *Scheduler) Tag(t ...string) *Scheduler { job := s.getCurrentJob() if s.tagsUnique { for _, tag := range t { if _, ok := s.tags.Load(tag); ok { job.error = wrapOrError(job.error, ErrTagsUnique(tag)) return s } s.tags.Store(tag, struct{}{}) } } job.tags = append(job.tags, t...) return s } // StartAt schedules the next run of the Job. If this time is in the past, the configured interval will be used // to calculate the next future time func (s *Scheduler) StartAt(t time.Time) *Scheduler { job := s.getCurrentJob() job.setStartAtTime(t) job.startsImmediately = false return s } // setUnit sets the unit type func (s *Scheduler) setUnit(unit schedulingUnit) { job := s.getCurrentJob() currentUnit := job.getUnit() if currentUnit == duration || currentUnit == crontab { job.error = wrapOrError(job.error, ErrInvalidIntervalUnitsSelection) return } job.setUnit(unit) } // Millisecond sets the unit with seconds func (s *Scheduler) Millisecond() *Scheduler { return s.Milliseconds() } // Milliseconds sets the unit with seconds func (s *Scheduler) Milliseconds() *Scheduler { s.setUnit(milliseconds) return s } // 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(daysOfMonth ...int) *Scheduler { return s.Months(daysOfMonth...) } // MonthLastDay sets the unit with months at every last day of the month func (s *Scheduler) MonthLastDay() *Scheduler { return s.Months(-1) } // Months sets the unit with months // Note: Only days 1 through 28 are allowed for monthly schedules // Note: Multiple add same days of month cannot be allowed // Note: -1 is a special value and can only occur as single argument func (s *Scheduler) Months(daysOfTheMonth ...int) *Scheduler { job := s.getCurrentJob() if len(daysOfTheMonth) == 0 { job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry) } else if len(daysOfTheMonth) == 1 { dayOfMonth := daysOfTheMonth[0] if dayOfMonth != -1 && (dayOfMonth < 1 || dayOfMonth > 28) { job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry) } } else { repeatMap := make(map[int]int) for _, dayOfMonth := range daysOfTheMonth { if dayOfMonth < 1 || dayOfMonth > 28 { job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry) break } for _, dayOfMonthInJob := range job.daysOfTheMonth { if dayOfMonthInJob == dayOfMonth { job.error = wrapOrError(job.error, ErrInvalidDaysOfMonthDuplicateValue) break } } if _, ok := repeatMap[dayOfMonth]; ok { job.error = wrapOrError(job.error, ErrInvalidDaysOfMonthDuplicateValue) break } else { repeatMap[dayOfMonth]++ } } } if job.daysOfTheMonth == nil { job.daysOfTheMonth = make([]int, 0) } job.daysOfTheMonth = append(job.daysOfTheMonth, daysOfTheMonth...) 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 scheduledWeekdays with a specifics weekdays func (s *Scheduler) Weekday(weekDay time.Weekday) *Scheduler { job := s.getCurrentJob() if in := in(job.scheduledWeekdays, weekDay); !in { job.scheduledWeekdays = append(job.scheduledWeekdays, weekDay) } job.startsImmediately = false s.setUnit(weeks) return s } func (s *Scheduler) Midday() *Scheduler { return s.At("12:00") } // 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 { if len(s.Jobs()) == 0 { s.setJobs([]*Job{s.newJob(0)}) s.jobCreated = true } return s.Jobs()[len(s.Jobs())-1] } func (s *Scheduler) now() time.Time { return s.time.Now(s.Location()) } // TagsUnique forces job tags to be unique across the scheduler // when adding tags with (s *Scheduler) Tag(). // This does not enforce uniqueness on tags added via // (j *Job) Tag() func (s *Scheduler) TagsUnique() { s.tagsUnique = true } // Job puts the provided job in focus for the purpose // of making changes to the job with the scheduler chain // and finalized by calling Update() func (s *Scheduler) Job(j *Job) *Scheduler { jobs := s.Jobs() for index, job := range jobs { if job == j { // the current job is always last, so put this job there s.Swap(len(jobs)-1, index) } } s.updateJob = true return s } // Update stops the job (if running) and starts it with any updates // that were made to the job in the scheduler chain. Job() must be // called first to put the given job in focus. func (s *Scheduler) Update() (*Job, error) { job := s.getCurrentJob() if !s.updateJob { return job, wrapOrError(job.error, ErrUpdateCalledWithoutJob) } s.updateJob = false job.stop() job.ctx, job.cancel = context.WithCancel(context.Background()) job.setStartsImmediately(false) if job.runWithDetails { return s.DoWithJobDetails(job.function, job.parameters...) } return s.Do(job.function, job.parameters...) } func (s *Scheduler) Cron(cronExpression string) *Scheduler { return s.cron(cronExpression, false) } func (s *Scheduler) CronWithSeconds(cronExpression string) *Scheduler { return s.cron(cronExpression, true) } func (s *Scheduler) cron(cronExpression string, withSeconds bool) *Scheduler { job := s.newJob(0) if s.updateJob || s.jobCreated { job = s.getCurrentJob() } var withLocation string if strings.HasPrefix(cronExpression, "TZ=") || strings.HasPrefix(cronExpression, "CRON_TZ=") { withLocation = cronExpression } else { withLocation = fmt.Sprintf("CRON_TZ=%s %s", s.location.String(), cronExpression) } var ( cronSchedule cron.Schedule err error ) if withSeconds { p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) cronSchedule, err = p.Parse(withLocation) } else { cronSchedule, err = cron.ParseStandard(withLocation) } if err != nil { job.error = wrapOrError(err, ErrCronParseFailure) } job.cronSchedule = cronSchedule job.setUnit(crontab) job.startsImmediately = false if s.updateJob || s.jobCreated { s.setJobs(append(s.Jobs()[:len(s.Jobs())-1], job)) s.jobCreated = false } else { s.setJobs(append(s.Jobs(), job)) } return s } func (s *Scheduler) newJob(interval int) *Job { return newJob(interval, !s.waitForInterval, s.singletonMode) } // WaitForScheduleAll defaults the scheduler to create all // new jobs with the WaitForSchedule option as true. // The jobs will not start immediately but rather will // wait until their first scheduled interval. func (s *Scheduler) WaitForScheduleAll() { s.waitForInterval = true } // WaitForSchedule sets the job to not start immediately // but rather wait until the first scheduled interval. func (s *Scheduler) WaitForSchedule() *Scheduler { job := s.getCurrentJob() job.startsImmediately = false return s } // StartImmediately sets the job to run immediately upon // starting the scheduler or adding the job to a running // scheduler. This overrides the jobs start status of any // previously called methods in the chain. // // Note: This is the default behavior of the scheduler // for most jobs, but is useful for overriding the default // behavior of Cron scheduled jobs which default to // WaitForSchedule. func (s *Scheduler) StartImmediately() *Scheduler { job := s.getCurrentJob() job.startsImmediately = true return s } // CustomTime takes an in a struct that implements the TimeWrapper interface // allowing the caller to mock the time used by the scheduler. This is useful // for tests relying on gocron. func (s *Scheduler) CustomTime(customTimeWrapper TimeWrapper) { s.time = customTimeWrapper } // CustomTimer takes in a function that mirrors the time.AfterFunc // This is used to mock the time.AfterFunc function used by the scheduler // for testing long intervals in a short amount of time. func (s *Scheduler) CustomTimer(customTimer func(d time.Duration, f func()) *time.Timer) { s.timer = customTimer } func (s *Scheduler) StopBlockingChan() { s.startBlockingStopChanMutex.Lock() if s.startBlockingStopChan != nil { s.startBlockingStopChan <- struct{}{} } s.startBlockingStopChanMutex.Unlock() } golang-github-go-co-op-gocron-1.18.0/scheduler_test.go000066400000000000000000002301661436572230600226170ustar00rootroot00000000000000package gocron import ( "fmt" "log" "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var _ TimeWrapper = (*fakeTime)(nil) 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 task() { fmt.Println("I am a running job.") } func taskWithParams(a int, b string) { fmt.Println(a, b) } func TestImmediateExecution(t *testing.T) { s := NewScheduler(time.UTC) semaphore := make(chan bool) _, err := s.Every(1).Second().Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() select { case <-time.After(1 * time.Second): t.Fatal("job did not run immediately") case <-semaphore: // test passed } } func TestScheduler_Every_InvalidInterval(t *testing.T) { testCases := []struct { description string interval interface{} expectedError string }{ {"zero", 0, ErrInvalidInterval.Error()}, {"negative", -1, ErrInvalidInterval.Error()}, {"invalid string duration", "bad", "time: invalid duration \"bad\""}, } s := NewScheduler(time.UTC) for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { _, err := s.Every(tc.interval).Do(func() {}) require.Error(t, err) assert.EqualError(t, err, tc.expectedError) }) } } func TestScheduler_EveryRandom(t *testing.T) { s := NewScheduler(time.UTC) semaphore := make(chan bool) j, err := s.EveryRandom(1, 2).Seconds().Do(func() { semaphore <- true }) require.NoError(t, err) assert.True(t, j.randomizeInterval) s.StartAsync() var counter int now := time.Now() for time.Now().Before(now.Add(2 * time.Second)) { if <-semaphore { counter++ } } s.Stop() assert.LessOrEqual(t, counter, 3) assert.GreaterOrEqual(t, counter, 1) } func TestScheduler_Every(t *testing.T) { t.Run("time.Duration", func(t *testing.T) { s := NewScheduler(time.UTC) semaphore := make(chan bool) _, err := s.Every(100 * time.Millisecond).Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() var counter int now := time.Now() for time.Now().Before(now.Add(500 * time.Millisecond)) { if <-semaphore { counter++ } } s.Stop() assert.Equal(t, 6, counter) }) t.Run("int", func(t *testing.T) { s := NewScheduler(time.UTC) semaphore := make(chan bool) _, err := s.Every(100).Milliseconds().Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() var counter int now := time.Now() for time.Now().Before(now.Add(100 * time.Millisecond)) { if <-semaphore { counter++ } } s.Stop() assert.Equal(t, 2, counter) }) t.Run("string duration", func(t *testing.T) { s := NewScheduler(time.UTC) semaphore := make(chan bool) _, err := s.Every("100ms").Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() var counter int now := time.Now() for time.Now().Before(now.Add(100 * time.Millisecond)) { if <-semaphore { counter++ } } s.Stop() assert.Equal(t, 2, counter) }) } func TestExecutionSeconds(t *testing.T) { s := NewScheduler(time.UTC) jobDone := make(chan bool) var ( executions []int64 interval = 1 expectedExecutions = 2 mu sync.RWMutex ) runTime := 1 * time.Second startTime := time.Now() // default unit is seconds _, err := s.Every(interval).Do(func() { mu.Lock() defer mu.Unlock() executions = append(executions, time.Now().UTC().Unix()) if time.Now().After(startTime.Add(runTime)) { jobDone <- true } }) require.NoError(t, err) s.StartAsync() <-jobDone // Wait job done s.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) { t.Run("simple", func(t *testing.T) { s := NewScheduler(time.UTC) _, err := s.Every(1).Second().Do(task) require.NoError(t, err) assert.True(t, s.TaskPresent(task)) }) t.Run("with tag", func(t *testing.T) { s := NewScheduler(time.UTC) _, err := s.Every(1).Hour().Tag("my_custom_tag").Do(task) require.NoError(t, err) assert.True(t, s.TaskPresent(task)) }) } func TestAt(t *testing.T) { t.Run("job scheduled for future hasn't run yet", func(t *testing.T) { ft := fakeTime{onNow: func(l *time.Location) time.Time { return time.Date(1970, 1, 1, 12, 0, 0, 0, l) }} s := NewScheduler(time.UTC) s.time = ft now := ft.onNow(time.UTC) semaphore := make(chan bool) nextMinuteTime := now.Add(1 * time.Minute) startAt := fmt.Sprintf("%02d:%02d:%02d", nextMinuteTime.Hour(), nextMinuteTime.Minute(), nextMinuteTime.Second()) dayJob, err := s.Every(1).Day().At(startAt).Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() time.Sleep(1 * time.Second) select { case <-time.After(1 * time.Second): log.Println(now.Add(time.Minute)) log.Println(dayJob.nextRun) assert.Equal(t, now.Add(1*time.Minute), dayJob.nextRun) case <-semaphore: t.Fatal("job ran even though scheduled in 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.EqualError(t, err, ErrUnsupportedTimeFormat.Error()) assert.Zero(t, s.Len()) }) } func TestMultipleAtTimesDecoding(t *testing.T) { exp := []time.Duration{_getHours(1), _getHours(3), _getHours(4), _getHours(7), _getHours(15)} testCases := []struct { name string params []interface{} result []time.Duration }{ { name: "multiple simple strings", params: []interface{}{"03:00", "15:00", "01:00", "07:00", "04:00"}, result: exp, }, { name: "single string separated by semicolons", params: []interface{}{"03:00;15:00;01:00;07:00;04:00"}, result: exp, }, { name: "interpolation of semicolons string, time.Time and simple string", params: []interface{}{"03:00;15:00;01:00", time.Date(0, 0, 0, 7, 0, 0, 0, time.UTC), "04:00"}, result: exp, }, { name: "repeated values on input don't get duplicated after decoding", params: []interface{}{"03:00;15:00;01:00;07:00;04:00;01:00"}, result: exp, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewScheduler(time.UTC) for _, p := range tc.params { s.At(p) } got := s.getCurrentJob().atTimes assert.Equalf(t, tc.result, got, fmt.Sprintf("expected %v / got %v", tc.result, got)) }) } } func schedulerForNextOrPreviousWeekdayEveryNTimes(weekday time.Weekday, next bool, n int, 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, err := s.Do(task) require.NoError(t, err) 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 TestScheduler_Remove(t *testing.T) { t.Run("remove from non-running", func(t *testing.T) { s := NewScheduler(time.UTC) s.TagsUnique() _, err := s.Every(1).Minute().Tag("tag1").Do(task) require.NoError(t, err) _, err = s.Every(1).Minute().Do(taskWithParams, 1, "hello") require.NoError(t, err) _, err = s.Every(1).Minute().Do(task) require.NoError(t, err) assert.Equal(t, 3, s.Len(), "Incorrect number of jobs") s.Remove(task) assert.Equal(t, 1, s.Len(), "Incorrect number of jobs after removing 2 job") s.Remove(task) assert.Equal(t, 1, s.Len(), "Incorrect number of jobs after removing non-existent job") }) t.Run("remove from running scheduler", func(t *testing.T) { s := NewScheduler(time.UTC) semaphore := make(chan bool) j, err := s.Every("100ms").StartAt(s.time.Now(s.location).Add(time.Second)).Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() s.Remove(j.function) select { case <-time.After(200 * time.Millisecond): // test passed case <-semaphore: t.Fatal("job ran after being removed") } }) } func TestScheduler_RemoveByReference(t *testing.T) { t.Run("remove from non-running scheduler", func(t *testing.T) { s := NewScheduler(time.UTC) job1, _ := s.Every(1).Minute().Do(task) job2, _ := s.Every(1).Minute().Do(taskWithParams, 1, "hello") assert.Equal(t, 2, s.Len(), "Incorrect number of jobs") s.RemoveByReference(job1) assert.ElementsMatch(t, []*Job{job2}, s.Jobs()) }) t.Run("remove from running scheduler", func(t *testing.T) { s := NewScheduler(time.UTC) s.TagsUnique() semaphore := make(chan bool) j, err := s.Every("100ms").StartAt(s.time.Now(s.location).Add(100 * time.Millisecond)).Tag("tag1").Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() s.RemoveByReference(j) select { case <-time.After(200 * time.Millisecond): // test passed case <-semaphore: t.Fatal("job ran after being removed") } _, ok := s.tags.Load("tag1") assert.False(t, ok) }) } func TestScheduler_RemoveByTags(t *testing.T) { t.Run("non unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) // Creating 2 Jobs with different tags tag1 := "a" tag2 := "ab" _, err := s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) // check Jobs()[0] tags is equal with tag "a" (tag1) assert.Equal(t, s.Jobs()[0].Tags()[0], tag1, "Job With Tag 'a' is removed from index 0") err = s.RemoveByTags(tag1) require.NoError(t, err) assert.Equal(t, 1, s.Len(), "Incorrect number of jobs after removing 1 job") // check Jobs()[0] tags is equal with tag "tag two" (tag2) after removing "a" assert.Equal(t, s.Jobs()[0].Tags()[0], tag2, "Job With Tag 'tag two' is removed from index 0") // Removing Non Existent Job with "a" because already removed above (will not removing any jobs because tag not match) err = s.RemoveByTags(tag1) assert.EqualError(t, err, ErrJobNotFoundWithTag.Error()) }) t.Run("unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) s.TagsUnique() // Creating 2 Jobs with unique tags tag1 := "tag one" tag2 := "tag two" _, err := s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) err = s.RemoveByTags("tag one") require.NoError(t, err) // Adding job with tag after removing by tag, assuming the unique tag has been removed as well _, err = s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") assert.Nil(t, err, "Unique tag is not deleted when removing by tag") }) t.Run("multiple non unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) // Creating 2 Jobs with different tags tag1 := "a" tag2 := "ab" tag3 := "abc" _, err := s.Every(1).Second().Tag(tag1, tag3).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag1, tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) // check Jobs()[0] tags contains tag "a" (tag1) and "abc" (tag3) assert.Contains(t, s.Jobs()[0].Tags(), tag1, "Job With Tag 'a' is removed from index 0") assert.Contains(t, s.Jobs()[0].Tags(), tag3, "Job With Tag 'abc' is removed from index 0") err = s.RemoveByTags(tag1, tag3) require.NoError(t, err) assert.Equal(t, 1, s.Len(), "Incorrect number of jobs after removing 1 job") // check Jobs()[0] tags is equal with tag "a" (tag1) and "ab" (tag2) after removing "a"+"abc" assert.Contains(t, s.Jobs()[0].Tags(), tag1, "Job With Tag 'a' is removed from index 0") assert.Contains(t, s.Jobs()[0].Tags(), tag2, "Job With Tag 'ab' is removed from index 0") // Removing Non Existent Job with "a"+"abc" because already removed above (will not removing any jobs because tag not match) err = s.RemoveByTags(tag1, tag3) assert.EqualError(t, err, ErrJobNotFoundWithTag.Error()) }) t.Run("multiple unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) s.TagsUnique() // Creating 2 Jobs with unique tags tag1 := "tag one" tag2 := "tag two" tag3 := "tag three" _, err := s.Every(1).Second().Tag(tag1, tag3).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) err = s.RemoveByTags(tag1, tag3) require.NoError(t, err) // Adding job with tag after removing by tag, assuming the unique tag has been removed as well _, err = s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") assert.Nil(t, err, "Unique tag is not deleted when removing by tag") }) } func TestScheduler_RemoveByTagsAny(t *testing.T) { t.Run("non unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) // Creating 2 Jobs with different tags tag1 := "a" tag2 := "ab" _, err := s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) // check Jobs()[0] tags is equal with tag "a" (tag1) assert.Equal(t, s.Jobs()[0].Tags()[0], tag1, "Job With Tag 'a' is removed from index 0") err = s.RemoveByTagsAny(tag1, tag2) require.NoError(t, err) assert.Equal(t, 0, s.Len(), "Incorrect number of jobs after removing 1 job") // Removing Non Existent Job with "a" because already removed above (will not removing any jobs because tag not match) err = s.RemoveByTagsAny(tag1) assert.EqualError(t, err, ErrJobNotFoundWithTag.Error()+": "+tag1) }) t.Run("unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) s.TagsUnique() // Creating 2 Jobs with unique tags tag1 := "tag one" tag2 := "tag two" _, err := s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) err = s.RemoveByTagsAny(tag1, tag2) require.NoError(t, err) // Adding job with tag after removing by tag, assuming the unique tag has been removed as well _, err = s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") assert.Nil(t, err, "Unique tag is not deleted when removing by tag") }) t.Run("multiple non unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) // Creating 2 Jobs with different tags tag1 := "a" tag2 := "ab" tag3 := "abc" _, err := s.Every(1).Second().Tag(tag1, tag3).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag1, tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) // check Jobs()[0] tags contains tag "a" (tag1) and "abc" (tag3) assert.Contains(t, s.Jobs()[0].Tags(), tag1, "Job With Tag 'a' is removed from index 0") assert.Contains(t, s.Jobs()[0].Tags(), tag3, "Job With Tag 'abc' is removed from index 0") err = s.RemoveByTagsAny(tag1, tag2, tag3) require.NoError(t, err) assert.Equal(t, 0, s.Len(), "Incorrect number of jobs after removing 1 job") // Removing Non Existent Job with "a"+"abc" because already removed above (will not removing any jobs because tag not match) err = s.RemoveByTagsAny(tag1, tag3) assert.EqualError(t, err, ErrJobNotFoundWithTag.Error()+": "+tag3+": "+ErrJobNotFoundWithTag.Error()+": "+tag1) }) t.Run("multiple unique tags", func(t *testing.T) { s := NewScheduler(time.UTC) s.TagsUnique() // Creating 2 Jobs with unique tags tag1 := "tag one" tag2 := "tag two" tag3 := "tag three" _, err := s.Every(1).Second().Tag(tag1, tag3).Do(taskWithParams, 1, "hello") // index 0 require.NoError(t, err) _, err = s.Every(1).Second().Tag(tag2).Do(taskWithParams, 2, "world") // index 1 require.NoError(t, err) err = s.RemoveByTagsAny(tag1) require.NoError(t, err) // Adding job with tag after removing by tag, assuming the unique tag has been removed as well _, err = s.Every(1).Second().Tag(tag1).Do(taskWithParams, 1, "hello") assert.Nil(t, err, "Unique tag is not deleted when removing by tag") }) } func TestScheduler_Jobs(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 TestScheduler_Len(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) _, err := s.Every(1).Minute().Do(task) require.NoError(t, err) _, err = s.Every(2).Minute().Do(task) require.NoError(t, err) jb := s.Jobs() var jobsBefore []*Job jobsBefore = append(jobsBefore, jb...) 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) semaphore := make(chan bool) _, err := s.Every("100ms").Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() s.Clear() assert.Equal(t, 0, s.Len()) var counter int now := time.Now() for time.Now().Before(now.Add(200 * time.Millisecond)) { select { case <-semaphore: counter++ default: } } // job should run only once - immediately and then // be stopped on s.Clear() assert.Equal(t, 1, counter) } func TestClearUnique(t *testing.T) { s := NewScheduler(time.UTC) s.TagsUnique() semaphore := make(chan bool) _, err := s.Every("100ms").Tag("tag1").Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() s.Clear() assert.Equal(t, 0, s.Len()) var counter int now := time.Now() for time.Now().Before(now.Add(200 * time.Millisecond)) { select { case <-semaphore: counter++ default: } } // job should run only once - immediately and then // be stopped on s.Clear() assert.Equal(t, 1, counter) s.tags.Range(func(key, value interface{}) bool { assert.FailNow(t, "map should be empty") return true }) } func TestSetUnit(t *testing.T) { testCases := []struct { desc string timeUnit schedulingUnit }{ {"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 calling .Stop()", func(t *testing.T) { s := NewScheduler(time.UTC) s.StartAsync() assert.True(t, s.IsRunning()) s.Stop() 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()) }) t.Run("stops all jobs", func(t *testing.T) { t.Parallel() s := NewScheduler(time.UTC) job, _ := s.Every(3).Second().Do(func() { //noop }) s.StartAsync() time.Sleep(1 * time.Second) // enough time for job to run preStopJobTimer := job.timer s.Stop() time.Sleep(3 * time.Second) // enough time for job timer to reset afterStopJobTimer := job.timer assert.Same(t, preStopJobTimer, afterStopJobTimer) }) t.Run("waits for jobs to finish processing before returning .Stop()", func(t *testing.T) { t.Parallel() i := int32(0) s := NewScheduler(time.UTC) s.Every(10).Second().Do(func() { time.Sleep(2 * time.Second) atomic.AddInt32(&i, 1) }) s.StartAsync() time.Sleep(1 * time.Second) // enough time for job to run s.Stop() assert.EqualValues(t, 1, atomic.LoadInt32(&i)) }) t.Run("stops a running scheduler calling .Stop()", func(t *testing.T) { s := NewScheduler(time.UTC) go func() { time.Sleep(1 * time.Second) assert.True(t, s.IsRunning()) s.Stop() time.Sleep(100 * time.Millisecond) // wait for stop goroutine to catch up }() s.StartBlocking() log.Println(".Stop() stops the blocking start") assert.False(t, s.IsRunning()) }) } func TestScheduler_StartAt(t *testing.T) { t.Run("scheduling", func(t *testing.T) { s := NewScheduler(time.Local) now := time.Now() // With StartAt job, _ := s.Every(3).Seconds().StartAt(now.Add(time.Second * 5)).Do(func() {}) assert.False(t, job.getStartsImmediately()) s.start() assert.Equal(t, now.Add(time.Second*5).Truncate(time.Second), job.NextRun().Truncate(time.Second)) s.stop() // Without StartAt job, _ = s.Every(3).Seconds().Do(func() {}) assert.True(t, job.getStartsImmediately()) }) t.Run("run", func(t *testing.T) { s := NewScheduler(time.UTC) semaphore := make(chan bool) s.Every(1).Day().StartAt(s.time.Now(s.location).Add(100 * time.Millisecond)).Do(func() { semaphore <- true }) s.StartAsync() select { case <-time.After(200 * time.Millisecond): t.Fatal("job did not run at 1 second") case <-semaphore: // test passed } }) t.Run("start in past", func(t *testing.T) { s := NewScheduler(time.Local) now := time.Now() // Start 5 seconds ago and make sure next run is in the future job, _ := s.Every(24).Hours().StartAt(now.Add(-24 * time.Hour).Add(10 * time.Minute)).Do(func() {}) assert.False(t, job.getStartsImmediately()) s.start() assert.Equal(t, now.Add(10*time.Minute).Truncate(time.Second), job.NextRun().Truncate(time.Second)) s.stop() }) } func TestScheduler_CalculateNextRun(t *testing.T) { ft := fakeTime{onNow: func(l *time.Location) time.Time { return time.Date(1970, 1, 1, 12, 0, 0, 0, l) }} 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) } januaryFirst2019At := func(hour, minute, second int) time.Time { return time.Date(2019, 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) } testCases := []struct { name string job *Job wantTimeUntilNextRun time.Duration }{ // SECONDS {name: "every second test", job: &Job{mu: &jobMutex{}, interval: 1, unit: seconds, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getSeconds(1)}, {name: "every 62 seconds test", job: &Job{mu: &jobMutex{}, interval: 62, unit: seconds, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getSeconds(62)}, // MINUTES {name: "every minute test", job: &Job{mu: &jobMutex{}, interval: 1, unit: minutes, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getMinutes(1)}, {name: "every 62 minutes test", job: &Job{mu: &jobMutex{}, interval: 62, unit: minutes, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getMinutes(62)}, // HOURS {name: "every hour test", job: &Job{mu: &jobMutex{}, interval: 1, unit: hours, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getHours(1)}, {name: "every 25 hours test", job: &Job{mu: &jobMutex{}, interval: 25, unit: hours, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getHours(25)}, // DAYS {name: "every day at midnight", job: &Job{mu: &jobMutex{}, 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{mu: &jobMutex{}, interval: 1, unit: days, atTimes: []time.Duration{_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{mu: &jobMutex{}, interval: 1, unit: days, atTimes: []time.Duration{_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{mu: &jobMutex{}, 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{mu: &jobMutex{}, interval: 1, unit: days, atTimes: []time.Duration{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{mu: &jobMutex{}, interval: 1, unit: days, atTimes: []time.Duration{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{mu: &jobMutex{}, interval: 2, unit: days, atTimes: []time.Duration{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{mu: &jobMutex{}, interval: 2, unit: days, atTimes: []time.Duration{8*time.Hour + 30*time.Minute}, lastRun: januaryFirst2020At(8, 30, 0)}, wantTimeUntilNextRun: 2 * day}, {name: "daily, last run was 1 second ago", job: &Job{mu: &jobMutex{}, interval: 1, unit: days, atTimes: []time.Duration{12 * time.Hour}, lastRun: ft.Now(time.UTC).Add(-time.Second)}, wantTimeUntilNextRun: 1 * time.Second}, //// WEEKS {name: "every week should run in 7 days", job: &Job{mu: &jobMutex{}, 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{mu: &jobMutex{}, interval: 1, atTimes: []time.Duration{_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{mu: &jobMutex{}, 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{mu: &jobMutex{}, 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{mu: &jobMutex{}, 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{mu: &jobMutex{}, 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{mu: &jobMutex{}, 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{mu: &jobMutex{}, 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", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 31 * day}, {name: "every month at day should consider at days", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{2}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 1 * day}, {name: "every month at day should consider at hours", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, atTimes: []time.Duration{_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{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 7)}, wantTimeUntilNextRun: 24 * day}, {name: "every month same as lastRun, should run February 1st", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 31 * day}, {name: "every 2 months at day 1, starting at day 1, should run in 2 months", job: &Job{mu: &jobMutex{}, interval: 2, unit: months, daysOfTheMonth: []int{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{mu: &jobMutex{}, interval: 2, unit: months, daysOfTheMonth: []int{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{mu: &jobMutex{}, interval: 2, unit: months, daysOfTheMonth: []int{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{mu: &jobMutex{}, interval: 13, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: januaryFirst2020At(0, 0, 0).AddDate(0, 13, -1).Sub(januaryFirst2020At(0, 0, 0))}, {name: "every last day of the month started on leap year february should run on march 31", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: time.Date(2020, time.February, 29, 0, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: 31 * day}, {name: "every last day of the month started on non-leap year february should run on march 31", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: time.Date(2019, time.February, 28, 0, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: 31 * day}, {name: "every last day of 2 months started on leap year february should run on april 30", job: &Job{mu: &jobMutex{}, interval: 2, unit: months, daysOfTheMonth: []int{-1}, lastRun: time.Date(2020, time.February, 29, 0, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: 31*day + 30*day}, {name: "every last day of 2 months started on non-leap year february should run on april 30", job: &Job{mu: &jobMutex{}, interval: 2, unit: months, daysOfTheMonth: []int{-1}, lastRun: time.Date(2019, time.February, 28, 0, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: 31*day + 30*day}, {name: "every last day of the month started on january 1 in leap year should run on january 31", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 30 * day}, {name: "every last day of the month started on january 1 in non-leap year should run on january 31", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2019At(0, 0, 0)}, wantTimeUntilNextRun: 30 * day}, {name: "every last day of the month started on january 30 in leap year should run on january 31", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 29)}, wantTimeUntilNextRun: 1 * day}, {name: "every last day of the month started on january 30 in non-leap year should run on january 31", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2019At(0, 0, 0).AddDate(0, 0, 29)}, wantTimeUntilNextRun: 1 * day}, {name: "every last day of the month started on january 31 in leap year should run on february 29", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 30)}, wantTimeUntilNextRun: 29 * day}, {name: "every last day of the month started on january 31 in non-leap year should run on february 28", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2019At(0, 0, 0).AddDate(0, 0, 30)}, wantTimeUntilNextRun: 28 * day}, {name: "every last day of the month started on december 31 should run on january 31 of the next year", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2019At(0, 0, 0).AddDate(0, 0, -1)}, wantTimeUntilNextRun: 31 * day}, {name: "every last day of 2 months started on december 31, 2018 should run on february 28, 2019", job: &Job{mu: &jobMutex{}, interval: 2, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2019At(0, 0, 0).AddDate(0, 0, -1)}, wantTimeUntilNextRun: 31*day + 28*day}, {name: "every last day of 2 months started on december 31, 2019 should run on february 29, 2020", job: &Job{mu: &jobMutex{}, interval: 2, unit: months, daysOfTheMonth: []int{-1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, -1)}, wantTimeUntilNextRun: 31*day + 29*day}, //// WEEKDAYS {name: "every weekday starting on one day before it should run this weekday", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0)}, wantTimeUntilNextRun: 1 * day}, {name: "every weekday starting on same weekday should run in 7 days", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 7 * day}, {name: "every 2 weekdays counting this week's weekday should run next weekday", job: &Job{mu: &jobMutex{}, interval: 2, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0)}, wantTimeUntilNextRun: day}, {name: "every weekday starting on one day after should count days remaining", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_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{mu: &jobMutex{}, interval: 1, unit: weeks, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, scheduledWeekdays: []time.Weekday{*_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{mu: &jobMutex{}, interval: 1, unit: weeks, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(10, 30, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 6*day + _getHours(23) + _getMinutes(0)}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewScheduler(time.UTC) s.time = ft got := s.durationToNextRun(tc.job.LastRun(), tc.job).duration assert.Equalf(t, tc.wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tc.wantTimeUntilNextRun.String(), got.String())) }) } } // helper test method func _tuesdayWeekday() *time.Weekday { tuesday := time.Tuesday return &tuesday } // helper test method func _getDays(i int) time.Duration { return time.Duration(i) * time.Hour * 24 } // 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) { testCases := []struct { description string evalFunc func(*Scheduler) }{ { "adding a new job before scheduler starts does not schedule job", func(s *Scheduler) { s.setRunning(false) job, err := s.Every(1).Second().Do(func() {}) assert.Equal(t, nil, err) assert.True(t, job.NextRun().IsZero()) }, }, { "adding a new job when scheduler is running schedules job", func(s *Scheduler) { s.setRunning(true) job, err := s.Every(1).Second().Do(func() {}) assert.Equal(t, nil, err) assert.False(t, job.NextRun().IsZero()) }, }, { description: "error due to the arg passed to Do() not being a function", evalFunc: func(s *Scheduler) { _, err := s.Every(1).Second().Do(1) assert.EqualError(t, err, ErrNotAFunction.Error()) assert.Zero(t, s.Len(), "The job should be deleted if the arg passed to Do() is not a function") }, }, { description: "error due to every/cron not called", evalFunc: func(s *Scheduler) { _, err := s.Do(1) assert.EqualError(t, err, ErrInvalidInterval.Error()) assert.Zero(t, s.Len(), "The job should be deleted if Every or Cron is not called") }, }, { description: "positive case", evalFunc: func(s *Scheduler) { _, err := s.Every(1).Day().Do(func() {}) require.NoError(t, err) assert.Equal(t, 1, s.Len()) }, }, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.Local) tc.evalFunc(s) }) } } func TestRunJobsWithLimit(t *testing.T) { t.Run("simple", func(t *testing.T) { semaphore := make(chan bool) s := NewScheduler(time.UTC) j, err := s.Every("100ms").Do(func() { semaphore <- true }) require.NoError(t, err) j.LimitRunsTo(1) s.StartAsync() time.Sleep(200 * time.Millisecond) var counter int now := time.Now() for time.Now().Before(now.Add(200 * time.Millisecond)) { select { case <-semaphore: counter++ default: } } assert.Equal(t, 1, counter) }) t.Run("remove after last run", func(t *testing.T) { semaphore := make(chan bool) s := NewScheduler(time.UTC) j, err := s.Every("100ms").Do(func() { semaphore <- true }) require.NoError(t, err) j.LimitRunsTo(1) s.StartAsync() time.Sleep(200 * time.Millisecond) var counter int select { case <-time.After(200 * time.Millisecond): assert.Equal(t, 0, s.Len()) case <-semaphore: counter++ require.LessOrEqual(t, counter, 1) } }) t.Run("remove unique tags also", func(t *testing.T) { semaphore := make(chan bool) s := NewScheduler(time.UTC) s.TagsUnique() j, err := s.Every("100ms").Tag("tag1", "tag2", "tag3").Do(func() { semaphore <- true }) require.NoError(t, err) j.LimitRunsTo(1) s.StartAsync() time.Sleep(200 * time.Millisecond) var counter int select { case <-time.After(200 * time.Millisecond): assert.Equal(t, 0, s.Len()) case <-semaphore: counter++ require.LessOrEqual(t, counter, 1) } s.tags.Range(func(key, value interface{}) bool { assert.FailNow(t, "map should be empty") return true }) }) } func TestCalculateMonthsError(t *testing.T) { testCases := []struct { desc string dayOfMonth []int }{ // -1 is now interpreted as "last day of the month" {"invalid 29", []int{29}}, {"invalid -1 in list", []int{27, -1}}, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { s := NewScheduler(time.UTC) job, err := s.Every(1).Month(tc.dayOfMonth...).Do(func() { fmt.Println("hello task") }) require.Error(t, err) require.Nil(t, job) }) } } func TestCalculateMonths(t *testing.T) { maySecond2021At0200 := time.Date(2021, 5, 2, 2, 0, 0, 0, time.UTC) maySecond2021At0800 := time.Date(2021, 5, 2, 8, 0, 0, 0, time.UTC) maySixth2021At0200 := time.Date(2021, 5, 6, 2, 0, 0, 0, time.UTC) maySixth2021At0500 := fakeTime{onNow: func(l *time.Location) time.Time { return time.Date(2021, 5, 6, 5, 0, 0, 0, l) }} maySixth2021At0800 := time.Date(2021, 5, 6, 8, 0, 0, 0, time.UTC) mayTenth2021At0200 := time.Date(2021, 5, 10, 2, 0, 0, 0, time.UTC) mayTenth2021At0800 := time.Date(2021, 5, 10, 8, 0, 0, 0, time.UTC) day := time.Hour * 24 testCases := []struct { description string job *Job wantTimeUntilNextRun time.Duration }{ {description: "day before current and before current time, should run next month", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{2}, atTimes: []time.Duration{_getHours(2)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySecond2021At0200)}, {description: "day before current and after current time, should run next month", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{2}, atTimes: []time.Duration{_getHours(8)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySecond2021At0800)}, {description: "current day and before current time, should run next month", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{6}, atTimes: []time.Duration{_getHours(2)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySixth2021At0200)}, {description: "current day and after current time, should run on current day", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{6}, atTimes: []time.Duration{_getHours(8)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: maySixth2021At0800.Sub(maySixth2021At0500.Now(time.UTC))}, {description: "day after current and before current time, should run on current month", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{10}, atTimes: []time.Duration{_getHours(2)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: mayTenth2021At0200.Sub(maySixth2021At0500.Now(time.UTC))}, {description: "day after current and after current time, should run on current month", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{10}, atTimes: []time.Duration{_getHours(8)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: mayTenth2021At0800.Sub(maySixth2021At0500.Now(time.UTC))}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) s.time = maySixth2021At0500 got := s.durationToNextRun(tc.job.LastRun(), tc.job).duration assert.Equalf(t, tc.wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tc.wantTimeUntilNextRun.String(), got.String())) }) } } func TestScheduler_SingletonMode(t *testing.T) { testCases := []struct { description string removeJob bool }{ {"with scheduler stop", false}, {"with job removal", true}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) var trigger int32 j, err := s.Every("100ms").SingletonMode().Do(func() { if atomic.LoadInt32(&trigger) == 1 { t.Fatal("Restart should not occur") } atomic.AddInt32(&trigger, 1) time.Sleep(300 * time.Millisecond) }) require.NoError(t, err) s.StartAsync() time.Sleep(200 * time.Millisecond) if tc.removeJob { s.RemoveByReference(j) time.Sleep(300 * time.Millisecond) } s.Stop() }) } } func TestScheduler_SingletonModeAll(t *testing.T) { testCases := []struct { description string removeJob bool }{ {"with scheduler stop", false}, {"with job removal", true}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) s.SingletonModeAll() var trigger int32 j, err := s.Every("100ms").Do(func() { if atomic.LoadInt32(&trigger) == 1 { t.Fatal("Restart should not occur") } atomic.AddInt32(&trigger, 1) time.Sleep(300 * time.Millisecond) }) require.NoError(t, err) s.StartAsync() time.Sleep(200 * time.Millisecond) if tc.removeJob { s.RemoveByReference(j) time.Sleep(300 * time.Millisecond) } s.Stop() }) } } func TestScheduler_LimitRunsTo(t *testing.T) { t.Run("job added before starting scheduler", func(t *testing.T) { semaphore := make(chan bool) s := NewScheduler(time.UTC) _, err := s.Every("100ms").LimitRunsTo(1).Do(func() { semaphore <- true }) require.NoError(t, err) s.StartAsync() time.Sleep(200 * time.Millisecond) var counter int select { case <-time.After(200 * time.Millisecond): assert.Equal(t, 1, counter) case <-semaphore: counter++ require.LessOrEqual(t, counter, 1) } }) t.Run("job added after starting scheduler", func(t *testing.T) { semaphore := make(chan bool) s := NewScheduler(time.UTC) s.StartAsync() _, err := s.Every("100ms").LimitRunsTo(1).Do(func() { semaphore <- true }) require.NoError(t, err) time.Sleep(200 * time.Millisecond) var counter int select { case <-time.After(200 * time.Millisecond): assert.Equal(t, 1, counter) case <-semaphore: counter++ require.LessOrEqual(t, counter, 1) } }) t.Run("job added after starting scheduler - using job's LimitRunsTo - results in two runs", func(t *testing.T) { semaphore := make(chan bool) s := NewScheduler(time.UTC) s.StartAsync() j, err := s.Every("100ms").Do(func() { semaphore <- true }) require.NoError(t, err) j.LimitRunsTo(1) time.Sleep(200 * time.Millisecond) var counter int select { case <-time.After(200 * time.Millisecond): assert.Equal(t, 2, counter) case <-semaphore: counter++ require.LessOrEqual(t, counter, 2) } }) } func TestScheduler_SetMaxConcurrentJobs(t *testing.T) { semaphore := make(chan bool) testCases := []struct { description string maxConcurrentJobs int mode limitMode expectedRuns int removeJobs bool f func() }{ // Expecting a total of 4 job runs: // 0s - jobs 1 & 3 run, job 2 hits the limit and is skipped // 1s - job 1 hits the limit and is skipped // 2s - job 1 & 2 run // 3s - job 1 hits the limit and is skipped { "reschedule mode", 2, RescheduleMode, 4, false, func() { semaphore <- true time.Sleep(200 * time.Millisecond) }, }, // Expecting a total of 8 job runs. The exact order of jobs may vary, for example: // 0s - jobs 2 & 3 run, job 1 hits the limit and waits // 1s - job 1 runs twice, the blocked run and the regularly scheduled run // 2s - jobs 1 & 3 run // 3s - jobs 2 & 3 run, job 1 hits the limit and waits { "wait mode", 2, WaitMode, 8, false, func() { semaphore <- true time.Sleep(100 * time.Millisecond) }, }, // Same as above - this confirms the same behavior when jobs are removed rather than the scheduler being stopped { "wait mode - with job removal", 2, WaitMode, 8, true, func() { semaphore <- true time.Sleep(100 * time.Millisecond) }, }, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) s.SetMaxConcurrentJobs(tc.maxConcurrentJobs, tc.mode) j1, err := s.Every("100ms").Do(tc.f) require.NoError(t, err) j2, err := s.Every("200ms").Do(tc.f) require.NoError(t, err) j3, err := s.Every("300ms").Do(tc.f) require.NoError(t, err) s.StartAsync() var counter int now := time.Now() for time.Now().Before(now.Add(400 * time.Millisecond)) { select { case <-semaphore: counter++ default: } } if tc.removeJobs { s.RemoveByReference(j1) s.RemoveByReference(j2) s.RemoveByReference(j3) defer s.Stop() } else { s.Stop() } // make sure no more jobs are run as the executor // or job should be properly stopped now = time.Now() for time.Now().Before(now.Add(200 * time.Millisecond)) { select { case <-semaphore: counter++ default: } } assert.Equal(t, tc.expectedRuns, counter) }) } } func TestScheduler_TagsUnique(t *testing.T) { const ( foo = "foo" bar = "bar" baz = "baz" ) s := NewScheduler(time.UTC) s.TagsUnique() j, err := s.Every("1s").Tag(foo, bar).Do(func() {}) require.NoError(t, err) // uniqueness not enforced on jobs tagged with job.Tag() // thus tagging the job here is allowed j.Tag(baz) _, err = s.Every("1s").Tag(baz).Do(func() {}) require.NoError(t, err) _, err = s.Every("1s").Tag(foo).Do(func() {}) assert.EqualError(t, err, ErrTagsUnique(foo).Error()) _, err = s.Every("1s").Tag(bar).Do(func() {}) assert.EqualError(t, err, ErrTagsUnique(bar).Error()) } func TestScheduler_MultipleTagsChained(t *testing.T) { const ( tag1 = "tag1" tag2 = "tag2" ) s := NewScheduler(time.UTC) j, err := s.Every("1s").Tag(tag1).Tag(tag2).Do(func() {}) require.NoError(t, err) assert.EqualValues(t, []string{tag1, tag2}, j.Tags()) } func TestScheduler_DoParameterValidation(t *testing.T) { testCases := []struct { description string parameters []interface{} }{ {"less than expected", []interface{}{"p1"}}, {"more than expected", []interface{}{"p1", "p2", "p3"}}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) f := func(s1, s2 string) { fmt.Println("ok") } _, err := s.Every(1).Days().StartAt(time.Now().UTC().Add(time.Second*10)).Do(f, tc.parameters...) assert.EqualError(t, err, ErrWrongParams.Error()) }) } } func TestScheduler_Job(t *testing.T) { s := NewScheduler(time.UTC) j1, err := s.Every("1s").Do(func() { log.Println("one") }) require.NoError(t, err) assert.Equal(t, j1, s.getCurrentJob()) j2, err := s.Every("1s").Do(func() { log.Println("two") }) require.NoError(t, err) assert.Equal(t, j2, s.getCurrentJob()) s.Job(j1) assert.Equal(t, j1, s.getCurrentJob()) s.Job(j2) assert.Equal(t, j2, s.getCurrentJob()) } func TestScheduler_Update(t *testing.T) { t.Run("happy path", func(t *testing.T) { s := NewScheduler(time.UTC) var counterMutex sync.RWMutex counter := 0 j, err := s.Every(1).Day().Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++ }) require.NoError(t, err) s.StartAsync() time.Sleep(300 * time.Millisecond) _, err = s.Job(j).Every("500ms").Update() require.NoError(t, err) time.Sleep(550 * time.Millisecond) _, err = s.Job(j).Every("750ms").Update() require.NoError(t, err) time.Sleep(800 * time.Millisecond) _, err = s.Job(j).CronWithSeconds("*/1 * * * * *").Update() require.NoError(t, err) time.Sleep(time.Second) s.Stop() counterMutex.RLock() defer counterMutex.RUnlock() assert.Equal(t, 4, counter) }) t.Run("happy singleton mode", func(t *testing.T) { s := NewScheduler(time.UTC) var counterMutex sync.RWMutex counter := 0 j, err := s.Every(1).Day().SingletonMode().Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++ }) require.NoError(t, err) s.StartAsync() time.Sleep(300 * time.Millisecond) _, err = s.Job(j).Every("500ms").Update() require.NoError(t, err) time.Sleep(550 * time.Millisecond) _, err = s.Job(j).Every("750ms").Update() require.NoError(t, err) time.Sleep(800 * time.Millisecond) s.Stop() counterMutex.RLock() defer counterMutex.RUnlock() assert.Equal(t, 3, counter) }) t.Run("update called without job call", func(t *testing.T) { s := NewScheduler(time.UTC) _, err := s.Every("1s").Do(func() {}) require.NoError(t, err) _, err = s.Update() assert.EqualError(t, err, ErrUpdateCalledWithoutJob.Error()) }) t.Run("update, delete, create", func(t *testing.T) { s := NewScheduler(time.UTC) var counterMutex sync.RWMutex counter := 0 j, err := s.Every(1).Day().Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++ }) require.NoError(t, err) s.StartAsync() time.Sleep(300 * time.Millisecond) _, err = s.Job(j).Every("500ms").Update() require.NoError(t, err) time.Sleep(550 * time.Millisecond) s.RemoveByReference(j) j, err = s.Every("750ms").WaitForSchedule().Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++ }) require.NoError(t, err) time.Sleep(800 * time.Millisecond) _, err = s.Job(j).CronWithSeconds("*/1 * * * * *").Update() require.NoError(t, err) time.Sleep(time.Second) s.Stop() counterMutex.RLock() defer counterMutex.RUnlock() assert.Equal(t, 4, counter) }) } func TestScheduler_RunByTag(t *testing.T) { var ( s = NewScheduler(time.Local) wg sync.WaitGroup counterMutex sync.RWMutex count = 0 ) s.Every(1).Day().StartAt(time.Now().Add(time.Hour)).Tag("tag").Do(func() { counterMutex.Lock() defer counterMutex.Unlock() count++ wg.Done() }) wg.Add(3) s.StartAsync() assert.NoError(t, s.RunByTag("tag")) assert.NoError(t, s.RunByTag("tag")) assert.NoError(t, s.RunByTag("tag")) wg.Wait() counterMutex.RLock() defer counterMutex.RUnlock() assert.Equal(t, 3, count) assert.Error(t, s.RunByTag("wrong-tag")) } func TestScheduler_Cron(t *testing.T) { ft := fakeTime{onNow: func(l *time.Location) time.Time { // January 1st, 12 noon, Thursday, 1970 return time.Date(1970, 1, 1, 12, 0, 0, 0, l) }} s := NewScheduler(time.UTC) s.time = ft testCases := []struct { description string cronTab string expectedNextRun time.Time expectedError error }{ // https://crontab.guru/ {"every minute", "*/1 * * * *", ft.onNow(time.UTC).Add(1 * time.Minute), nil}, {"every day 1am", "0 1 * * *", ft.onNow(time.UTC).Add(13 * time.Hour), nil}, {"weekends only", "0 0 * * 6,0", ft.onNow(time.UTC).Add(36 * time.Hour), nil}, {"at time monday thru friday", "0 22 * * 1-5", ft.onNow(time.UTC).Add(10 * time.Hour), nil}, {"every minute in range, monday thru friday", "15-30 * * * 1-5", ft.onNow(time.UTC).Add(15 * time.Minute), nil}, {"at every minute past every hour from 1 through 5 on every day-of-week from Monday through Friday.", "* 1-5 * * 1-5", ft.onNow(time.UTC).Add(13 * time.Hour), nil}, {"hourly", "@hourly", ft.onNow(time.UTC).Add(1 * time.Hour), nil}, {"every day 1am in shanghai", "CRON_TZ=Asia/Shanghai 0 1 * * *", ft.onNow(time.UTC).Add(5 * time.Hour), nil}, {"bad expression", "bad", time.Time{}, wrapOrError(fmt.Errorf("expected exactly 5 fields, found 1: [bad]"), ErrCronParseFailure)}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { j, err := s.Cron(tc.cronTab).Do(func() {}) if tc.expectedError == nil { require.NoError(t, err) s.scheduleNextRun(j) assert.Exactly(t, tc.expectedNextRun, j.NextRun()) } else { assert.EqualError(t, err, tc.expectedError.Error()) } }) } t.Run("error At() called with Cron()", func(t *testing.T) { _, err := s.Cron("@hourly").At("1:00").Do(func() {}) assert.EqualError(t, err, ErrAtTimeNotSupported.Error()) }) t.Run("error Weekday() called with Cron()", func(t *testing.T) { _, err := s.Cron("@hourly").Sunday().Do(func() {}) assert.EqualError(t, err, wrapOrError(ErrInvalidIntervalUnitsSelection, ErrWeekdayNotSupported).Error()) }) } func TestScheduler_CronWithSeconds(t *testing.T) { ft := fakeTime{onNow: func(l *time.Location) time.Time { // January 1st, 12 noon, Thursday, 1970 return time.Date(1970, 1, 1, 12, 0, 0, 0, l) }} s := NewScheduler(time.UTC) s.time = ft testCases := []struct { description string cronTab string expectedNextRun time.Time expectedError error }{ // https://crontab.guru/ {"every second", "*/1 * * * * *", ft.onNow(time.UTC).Add(1 * time.Second), nil}, {"every second from 0-30", "0-30 * * * * *", ft.onNow(time.UTC).Add(1 * time.Second), nil}, {"every minute", "0 */1 * * * *", ft.onNow(time.UTC).Add(1 * time.Minute), nil}, {"every day 1am", "* 0 1 * * *", ft.onNow(time.UTC).Add(13 * time.Hour), nil}, {"weekends only", "* 0 0 * * 6,0", ft.onNow(time.UTC).Add(36 * time.Hour), nil}, {"at time monday thru friday", "* 0 22 * * 1-5", ft.onNow(time.UTC).Add(10 * time.Hour), nil}, {"every minute in range, monday thru friday", "* 15-30 * * * 1-5", ft.onNow(time.UTC).Add(15 * time.Minute), nil}, {"at every minute past every hour from 1 through 5 on every day-of-week from Monday through Friday.", "* * 1-5 * * 1-5", ft.onNow(time.UTC).Add(13 * time.Hour), nil}, {"hourly", "@hourly", ft.onNow(time.UTC).Add(1 * time.Hour), nil}, {"bad expression", "bad", time.Time{}, wrapOrError(fmt.Errorf("expected exactly 6 fields, found 1: [bad]"), ErrCronParseFailure)}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { j, err := s.CronWithSeconds(tc.cronTab).Do(func() {}) if tc.expectedError == nil { require.NoError(t, err) s.scheduleNextRun(j) assert.Exactly(t, tc.expectedNextRun, j.NextRun()) } else { assert.EqualError(t, err, tc.expectedError.Error()) } }) } t.Run("error At() called with Cron()", func(t *testing.T) { _, err := s.Cron("@hourly").At("1:00").Do(func() {}) assert.EqualError(t, err, ErrAtTimeNotSupported.Error()) }) t.Run("error Weekday() called with Cron()", func(t *testing.T) { _, err := s.Cron("@hourly").Sunday().Do(func() {}) assert.EqualError(t, err, wrapOrError(ErrInvalidIntervalUnitsSelection, ErrWeekdayNotSupported).Error()) }) } func TestScheduler_WaitForSchedule(t *testing.T) { s := NewScheduler(time.UTC) var counterMutex sync.RWMutex counter := 0 _, err := s.Every("100ms").WaitForSchedule().Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++ }) require.NoError(t, err) s.StartAsync() time.Sleep(350 * time.Millisecond) s.Stop() counterMutex.RLock() defer counterMutex.RUnlock() assert.Equal(t, 3, counter) } func TestScheduler_WaitForSchedules(t *testing.T) { s := NewScheduler(time.UTC) s.WaitForScheduleAll() var counterMutex sync.RWMutex counter := 0 _, err := s.Every("1s").Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++; log.Println("job 1") }) require.NoError(t, err) _, err = s.CronWithSeconds("*/1 * * * * *").Do(func() { counterMutex.Lock(); defer counterMutex.Unlock(); counter++; log.Println("job 2") }) require.NoError(t, err) s.StartAsync() time.Sleep(1050 * time.Millisecond) s.Stop() counterMutex.RLock() defer counterMutex.RUnlock() assert.Equal(t, 2, counter) } func TestScheduler_LenWeekDays(t *testing.T) { testCases := []struct { description string weekDays []time.Weekday finalLen int }{ {"no week day", []time.Weekday{}, 0}, {"equal week day", []time.Weekday{time.Friday, time.Friday, time.Friday}, 1}, {"more than one week day", []time.Weekday{time.Friday, time.Saturday, time.Sunday}, 3}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) s = s.Every(1) for _, weekDay := range tc.weekDays { s = s.Weekday(weekDay) } j, err := s.Do(func() {}) require.NoError(t, err) assert.Equal(t, len(j.scheduledWeekdays), tc.finalLen) }) } } func TestScheduler_CallNextWeekDay(t *testing.T) { januaryFirst2020At := func(hour, minute, second int) time.Time { return time.Date(2020, time.January, 1, hour, minute, second, 0, time.UTC) } const wantTimeUntilNextRun = time.Hour * 24 * 2 lastRun := januaryFirst2020At(0, 0, 0) testCases := []struct { description string weekDays []time.Weekday }{ {"week days not in order", []time.Weekday{time.Monday, time.Friday}}, {"week days in order", []time.Weekday{time.Friday, time.Monday}}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) s.Every(1) for _, weekDay := range tc.weekDays { s.Weekday(weekDay) } job, err := s.Do(func() {}) require.NoError(t, err) job.lastRun = lastRun got := s.durationToNextRun(lastRun, job).duration assert.Equal(t, wantTimeUntilNextRun, got) }) } } func TestScheduler_Midday(t *testing.T) { currentMidday := time.Date(2022, time.January, 0, 12, 0, 0, 0, time.UTC) expectedTime := 24 * time.Hour t.Run("check time till next midday", func(t *testing.T) { s := NewScheduler(time.UTC) job, _ := s.Every(1).Day().Midday().Do(func() {}) job.lastRun = currentMidday durationToNextTime := s.durationToNextRun(currentMidday, job).duration assert.Equal(t, expectedTime, durationToNextTime) }) } func TestScheduler_EveryMonthFirstWeekday(t *testing.T) { testCases := []struct { current time.Time expected time.Time weekday time.Weekday }{ { current: time.Date(2022, time.March, 7, 0, 0, 0, 0, time.UTC), expected: time.Date(2022, time.April, 6, 0, 0, 0, 0, time.UTC), weekday: time.Wednesday, }, { current: time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC), expected: time.Date(2022, time.March, 3, 0, 0, 0, 0, time.UTC), weekday: time.Thursday, }, { current: time.Date(2022, time.March, 23, 0, 0, 0, 0, time.UTC), expected: time.Date(2022, time.April, 4, 0, 0, 0, 0, time.UTC), weekday: time.Monday, }, { current: time.Date(2022, time.February, 28, 0, 0, 0, 0, time.UTC), expected: time.Date(2022, time.March, 7, 0, 0, 0, 0, time.UTC), weekday: time.Monday, }, } for _, tc := range testCases { t.Run("check first weekday of the month", func(t *testing.T) { s := NewScheduler(time.UTC) s.time = fakeTime{ onNow: func(l *time.Location) time.Time { return tc.current }, } job, _ := s.MonthFirstWeekday(tc.weekday).Do(func() {}) job.lastRun = tc.current durationToNextTime := s.durationToNextRun(tc.current, job) assert.Equal(t, tc.expected, durationToNextTime.dateTime) }) } } func TestScheduler_CheckNextWeekDay(t *testing.T) { januaryFirst2020At := func(hour, minute, second int) time.Time { return time.Date(2020, time.January, 1, hour, minute, second, 0, time.UTC) } januarySecond2020At := func(hour, minute, second int) time.Time { return time.Date(2020, time.January, 2, hour, minute, second, 0, time.UTC) } const ( wantTimeUntilNextFirstRun = 1 * time.Second // all day long wantTimeUntilNextSecondRun = 24 * time.Hour ) t.Run("check slice next run", func(t *testing.T) { s := NewScheduler(time.UTC) lastRun := januaryFirst2020At(23, 59, 59) secondLastRun := januarySecond2020At(0, 0, 0) job, err := s.Every(1).Week().Friday().Thursday().Do(func() {}) require.NoError(t, err) job.lastRun = lastRun gotFirst := s.durationToNextRun(lastRun, job).duration assert.Equal(t, wantTimeUntilNextFirstRun, gotFirst) job.lastRun = secondLastRun gotSecond := s.durationToNextRun(secondLastRun, job).duration assert.Equal(t, wantTimeUntilNextSecondRun, gotSecond) }) } func TestScheduler_CheckEveryWeekHigherThanOne(t *testing.T) { januaryDay2020At := func(day int) time.Time { return time.Date(2020, time.January, day, 0, 0, 0, 0, time.UTC) } testCases := []struct { description string interval int weekDays []time.Weekday daysToTest []int caseTest int }{ {description: "every two weeks after run the first scheduled task", interval: 2, weekDays: []time.Weekday{time.Thursday}, daysToTest: []int{1, 2}, caseTest: 1}, {description: "every three weeks after run the first scheduled task", interval: 3, weekDays: []time.Weekday{time.Thursday}, daysToTest: []int{1, 2}, caseTest: 2}, {description: "every two weeks after run the first 2 scheduled tasks", interval: 2, weekDays: []time.Weekday{time.Thursday, time.Friday}, daysToTest: []int{1, 2, 3}, caseTest: 3}, } const ( wantTimeUntilNextRunOneDay = 24 * time.Hour // two weeks difference wantTimeUntilNextRunTwoWeeks = 24 * time.Hour * 14 // three weeks difference wantTimeUntilNextRunThreeWeeks = 24 * time.Hour * 21 // two weeks difference less one day wantTimeUntilNextRunTwoWeeksLessOneDay = 24 * time.Hour * (14 - 1) ) for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) s.Every(tc.interval) for _, weekDay := range tc.weekDays { s.Weekday(weekDay) } job, err := s.Do(func() {}) require.NoError(t, err) for numJob, day := range tc.daysToTest { lastRun := januaryDay2020At(day) job.lastRun = lastRun got := s.durationToNextRun(lastRun, job).duration if numJob < len(tc.weekDays) { assert.Equal(t, wantTimeUntilNextRunOneDay, got) } else { if tc.caseTest == 1 { assert.Equal(t, wantTimeUntilNextRunTwoWeeks, got) } else if tc.caseTest == 2 { assert.Equal(t, wantTimeUntilNextRunThreeWeeks, got) } else if tc.caseTest == 3 { assert.Equal(t, wantTimeUntilNextRunTwoWeeksLessOneDay, got) } } job.runCount++ } }) } } func TestScheduler_StartImmediately(t *testing.T) { testCases := []struct { description string scheduler *Scheduler expectedToStartImmediately bool }{ {"true cron", NewScheduler(time.UTC).Cron("0 0 * * 6,0").StartImmediately(), true}, {"true default", NewScheduler(time.UTC).Every("1m"), true}, {"true every", NewScheduler(time.UTC).Every("1m").StartImmediately(), true}, {"false cron default", NewScheduler(time.UTC).Cron("0 0 * * 6,0"), false}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { j, err := tc.scheduler.Do(func() {}) require.NoError(t, err) assert.Exactly(t, tc.expectedToStartImmediately, j.startsImmediately) }) } } func TestScheduler_CheckCalculateDaysOfMonth(t *testing.T) { lastRunFirstCaseDate := time.Date(2021, 9, 10, 0, 0, 0, 0, time.UTC) lastRunSecondCaseDate := time.Date(2021, 9, 10, 5, 0, 0, 0, time.UTC) lastRunThirdCaseDate := time.Date(2021, 10, 6, 0, 0, 0, 0, time.UTC) lastRunFourthCaseDate := time.Date(2021, 9, 11, 0, 0, 0, 0, time.UTC) curTime := fakeTime{onNow: func(l *time.Location) time.Time { return time.Date(2021, 9, 8, 0, 0, 0, 0, l) }} testCases := []struct { description string job *Job wantTimeUntilNextRun time.Duration }{ {description: "should run current month 10", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{10, 6}, atTimes: []time.Duration{_getHours(0)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunFirstCaseDate.Sub(curTime.Now(time.UTC))}, {description: "should run current month 10", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{10, 6}, atTimes: []time.Duration{_getHours(5)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunSecondCaseDate.Sub(curTime.Now(time.UTC))}, {description: "should run next month 6", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{6, 7}, atTimes: []time.Duration{_getHours(0)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunThirdCaseDate.Sub(curTime.Now(time.UTC))}, {description: "should run next month 11", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, daysOfTheMonth: []int{12, 11}, atTimes: []time.Duration{_getHours(0)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunFourthCaseDate.Sub(curTime.Now(time.UTC))}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) s.time = curTime got := s.durationToNextRun(tc.job.LastRun(), tc.job).duration assert.Equalf(t, tc.wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tc.wantTimeUntilNextRun.String(), got.String())) }) } } func TestScheduler_CheckSetBehaviourBeforeJobCreated(t *testing.T) { s := NewScheduler(time.UTC) s.Month(1, 2).Every(1).Do(func() {}) } func TestScheduler_MonthLastDayAtTime(t *testing.T) { testCases := []struct { name string job *Job wantTimeUntilNextRun time.Duration }{ {name: "month last day before run at time", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, atTimes: []time.Duration{_getHours(20) + _getMinutes(0)}, daysOfTheMonth: []int{-1}, lastRun: time.Date(2022, 2, 28, 10, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: _getHours(10)}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewScheduler(time.UTC) got := s.durationToNextRun(tc.job.LastRun(), tc.job).duration assert.Equalf(t, tc.wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tc.wantTimeUntilNextRun.String(), got.String())) }) } } func TestScheduler_WeekdayIsCurrentDay(t *testing.T) { s := NewScheduler(time.UTC) s.time = fakeTime{onNow: func(l *time.Location) time.Time { return time.Date(2022, 2, 17, 20, 0, 0, 0, l) }} s.StartAsync() job, _ := s.Every(1).Week().Thursday().Friday().Saturday().At("23:00").Do(func() {}) assert.Equal(t, time.Date(2022, 2, 17, 23, 0, 0, 0, s.Location()), job.NextRun()) } func TestScheduler_MultipleAtTime(t *testing.T) { getTime := func(hour, min, sec int) time.Time { return time.Date(2022, 2, 16, hour, min, sec, 0, time.UTC) } getMonthLastDayTime := func(hour, min, sec int) time.Time { return time.Date(2022, 2, 28, hour, min, sec, 0, time.UTC) } atTimes := []time.Duration{ _getHours(3) + _getMinutes(20), _getHours(5) + _getMinutes(30), _getHours(7) + _getMinutes(0), _getHours(14) + _getMinutes(10), } testCases := []struct { description string job *Job wantTimeUntilNextRun time.Duration }{ {description: "day test1", job: &Job{mu: &jobMutex{}, interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(1, 0, 0)}, wantTimeUntilNextRun: _getHours(2) + _getMinutes(20)}, {description: "day test2", job: &Job{mu: &jobMutex{}, interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(3, 30, 0)}, wantTimeUntilNextRun: _getHours(2)}, {description: "day test3", job: &Job{mu: &jobMutex{}, interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(5, 27, 10)}, wantTimeUntilNextRun: _getMinutes(2) + _getSeconds(50)}, {description: "day test4", job: &Job{mu: &jobMutex{}, interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getHours(1) + _getMinutes(30)}, {description: "day test5", job: &Job{mu: &jobMutex{}, interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getHours(12) + _getMinutes(20)}, {description: "week test1", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getDays(7) - _getHours(2) - _getMinutes(10)}, {description: "week test2", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(7) - _getHours(15) + _getHours(3) + _getMinutes(20)}, {description: "weekday before test1", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Tuesday}, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getDays(6) - _getHours(2) - _getMinutes(10)}, {description: "weekday before test2", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Tuesday}, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(6) - _getHours(15) + _getHours(3) + _getMinutes(20)}, {description: "weekday equals test1", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Wednesday}, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getHours(1) + _getMinutes(30)}, {description: "weekday equals test2", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Wednesday}, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(6) + _getHours(9) + _getHours(3) + _getMinutes(20)}, {description: "weekday after test1", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Thursday}, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getDays(1) - _getHours(2) - _getMinutes(10)}, {description: "weekday after test2", job: &Job{mu: &jobMutex{}, interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Thursday}, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(1) - _getHours(15) + _getHours(3) + _getMinutes(20)}, {description: "month test1", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, atTimes: atTimes, lastRun: getTime(5, 30, 0), daysOfTheMonth: []int{1}}, wantTimeUntilNextRun: _getDays(13) - _getHours(2) - _getMinutes(10)}, {description: "month test2", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, atTimes: atTimes, lastRun: getTime(15, 0, 0), daysOfTheMonth: []int{1}}, wantTimeUntilNextRun: _getDays(12) + _getHours(9) + _getHours(3) + _getMinutes(20)}, {description: "month last day test1", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, atTimes: atTimes, lastRun: getMonthLastDayTime(5, 30, 0), daysOfTheMonth: []int{-1}}, wantTimeUntilNextRun: _getHours(1) + _getMinutes(30)}, {description: "month last day test2", job: &Job{mu: &jobMutex{}, interval: 1, unit: months, atTimes: atTimes, lastRun: getMonthLastDayTime(15, 0, 0), daysOfTheMonth: []int{-1}}, wantTimeUntilNextRun: _getDays(30) + _getHours(9) + _getHours(3) + _getMinutes(20)}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) got := s.durationToNextRun(tc.job.LastRun(), tc.job).duration assert.Equalf(t, tc.wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tc.wantTimeUntilNextRun.String(), got.String())) }) } } func TestScheduler_DoWithJobDetails(t *testing.T) { testCases := []struct { description string jobFunc interface{} params []interface{} expectedError string }{ {"no error", func(foo, bar string, job Job) {}, []interface{}{"foo", "bar"}, ""}, {"too few params", func(foo, bar string, job Job) {}, []interface{}{"foo"}, ErrWrongParams.Error()}, {"too many params", func(foo, bar string, job Job) {}, []interface{}{"foo", "bar", "baz"}, ErrWrongParams.Error()}, {"jobFunc doesn't have Job param", func(foo, bar string) {}, []interface{}{"foo"}, ErrDoWithJobDetails.Error()}, {"jobFunc has Job param but not last param", func(job Job, foo, bar string) {}, []interface{}{"foo", "bar"}, ErrDoWithJobDetails.Error()}, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { s := NewScheduler(time.UTC) _, err := s.Every("1s").DoWithJobDetails(tc.jobFunc, tc.params...) if tc.expectedError == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.expectedError) } }) } t.Run("run job with details", func(t *testing.T) { s := NewScheduler(time.UTC) _, err := s.Tag("tag1").Every("100ms").DoWithJobDetails(func(job Job) { log.Printf("job last run: %s, job next run: %s", job.LastRun(), job.NextRun()) }) require.NoError(t, err) s.StartAsync() time.Sleep(500 * time.Millisecond) }) } golang-github-go-co-op-gocron-1.18.0/timeHelper.go000066400000000000000000000014501436572230600216700ustar00rootroot00000000000000package gocron import "time" var _ TimeWrapper = (*trueTime)(nil) // TimeWrapper is an interface that wraps the Now, Sleep, and Unix methods of the time package. // This allows the library and users to mock the time package for testing. type TimeWrapper interface { Now(*time.Location) time.Time Unix(int64, int64) time.Time Sleep(time.Duration) } 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) } // afterFunc proxies the time.AfterFunc function. // This allows it to be mocked for testing. func afterFunc(d time.Duration, f func()) *time.Timer { return time.AfterFunc(d, f) }