pax_global_header 0000666 0000000 0000000 00000000064 14365722306 0014522 g ustar 00root root 0000000 0000000 52 comment=8169ff9f7dad596a9bfdcddf2f25ed9b474c3290
golang-github-go-co-op-gocron-1.18.0/ 0000775 0000000 0000000 00000000000 14365722306 0017243 5 ustar 00root root 0000000 0000000 golang-github-go-co-op-gocron-1.18.0/.github/ 0000775 0000000 0000000 00000000000 14365722306 0020603 5 ustar 00root root 0000000 0000000 golang-github-go-co-op-gocron-1.18.0/.github/FUNDING.yml 0000664 0000000 0000000 00000001237 14365722306 0022423 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000001135 14365722306 0023433 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14365722306 0022640 5 ustar 00root root 0000000 0000000 golang-github-go-co-op-gocron-1.18.0/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000004460 14365722306 0026457 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000000533 14365722306 0026535 0 ustar 00root root 0000000 0000000 on:
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.yml 0000664 0000000 0000000 00000001120 14365722306 0025021 0 ustar 00root root 0000000 0000000 on:
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/.gitignore 0000664 0000000 0000000 00000000464 14365722306 0021237 0 ustar 00root root 0000000 0000000 # 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.yaml 0000664 0000000 0000000 00000002013 14365722306 0021764 0 ustar 00root root 0000000 0000000 run:
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.md 0000664 0000000 0000000 00000006121 14365722306 0022042 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000003342 14365722306 0021476 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000002053 14365722306 0020250 0 ustar 00root root 0000000 0000000 MIT 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/Makefile 0000664 0000000 0000000 00000000350 14365722306 0020701 0 ustar 00root root 0000000 0000000 .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.md 0000664 0000000 0000000 00000013757 14365722306 0020537 0 ustar 00root root 0000000 0000000 # gocron: A Golang Job Scheduling Package.
[](https://github.com/go-co-op/gocron/actions?query=workflow%3A"lint")  [](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

[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.md 0000664 0000000 0000000 00000001156 14365722306 0021037 0 ustar 00root root 0000000 0000000 # 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.go 0000664 0000000 0000000 00000050621 14365722306 0022270 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005322 14365722306 0021432 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002152 14365722306 0022467 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000000507 14365722306 0020353 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000003550 14365722306 0020401 0 ustar 00root root 0000000 0000000 github.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.go 0000664 0000000 0000000 00000010606 14365722306 0021064 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006677 14365722306 0022140 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000030243 14365722306 0020346 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013074 14365722306 0021410 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000106705 14365722306 0021561 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000230166 14365722306 0022617 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001450 14365722306 0021670 0 ustar 00root root 0000000 0000000 package 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)
}