mirror of https://github.com/go-co-op/gocron.git
initial clean v2 commit history
This commit is contained in:
commit
ad26a71e0e
|
|
@ -0,0 +1,12 @@
|
||||||
|
# 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: gocron
|
||||||
|
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']
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 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"
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
# 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: [ v2 ]
|
||||||
|
branches-ignore:
|
||||||
|
- "dependabot/**"
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ v2 ]
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- v2
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- v2
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- v2
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- v2
|
||||||
|
|
||||||
|
name: golangci-lint
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version:
|
||||||
|
- "1.21"
|
||||||
|
name: lint and test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3.7.0
|
||||||
|
with:
|
||||||
|
version: v1.55.2
|
||||||
|
- name: test
|
||||||
|
run: make test
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
local_testing
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
issues-exit-code: 1
|
||||||
|
tests: true
|
||||||
|
skip-dirs:
|
||||||
|
- local
|
||||||
|
|
||||||
|
issues:
|
||||||
|
max-same-issues: 100
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- bodyclose
|
||||||
|
- errcheck
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- exportloopref
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
- whitespace
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
fix: true
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.5.0
|
||||||
|
hooks:
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-yaml
|
||||||
|
- id: detect-private-key
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v1.55.2
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint
|
||||||
|
- repo: https://github.com/TekWizely/pre-commit-golang
|
||||||
|
rev: v1.0.0-rc.1
|
||||||
|
hooks:
|
||||||
|
- id: go-fumpt
|
||||||
|
args:
|
||||||
|
- -w
|
||||||
|
- id: go-mod-tidy
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# 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
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
.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:
|
||||||
|
@grep "^func " example_test.go | sort -c
|
||||||
|
@golangci-lint run
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
# gocron: A Golang Job Scheduling Package.
|
||||||
|
|
||||||
|
[](https://github.com/go-co-op/gocron/actions)
|
||||||
|
 [](https://pkg.go.dev/github.com/go-co-op/gocron/v2)
|
||||||
|
|
||||||
|
gocron is a job scheduling package which lets you run Go functions at pre-determined intervals.
|
||||||
|
|
||||||
|
If you want to chat, you can find us on Slack at
|
||||||
|
[<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW)
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
- **Job**: The encapsulates a "task", which is made up of a go func and any function parameters, and then
|
||||||
|
provides the scheduler with the time the job should be scheduled to run.
|
||||||
|
- **Executor**: The executor, calls the "task" function and manages the complexities of different job
|
||||||
|
execution timing (e.g. singletons that shouldn't overrun each other, limiting the max number of jobs running)
|
||||||
|
- **Scheduler**: The scheduler keeps track of all the jobs and sends each job to the executor when
|
||||||
|
it is ready to be run.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/go-co-op/gocron/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
```golang
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// create a scheduler
|
||||||
|
s, err := gocron.NewScheduler()
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a job to the scheduler
|
||||||
|
j, err := s.NewJob(
|
||||||
|
gocron.DurationJob(
|
||||||
|
10*time.Second,
|
||||||
|
),
|
||||||
|
gocron.NewTask(
|
||||||
|
func(a string, b int) {
|
||||||
|
// do things
|
||||||
|
},
|
||||||
|
"hello",
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
// each job has a unique id
|
||||||
|
fmt.Println(j.ID())
|
||||||
|
|
||||||
|
// start the scheduler
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
// when you're done, shut it down
|
||||||
|
err = s.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Job types**: Jobs can be run at various intervals.
|
||||||
|
- [**Duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationJob):
|
||||||
|
Jobs can be run at a fixed `time.Duration`.
|
||||||
|
- [**Random duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationRandomJob):
|
||||||
|
Jobs can be run at a random `time.Duration` between a min and max.
|
||||||
|
- [**Cron**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#CronJob):
|
||||||
|
Jobs can be run using a crontab.
|
||||||
|
- [**Daily**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DailyJob):
|
||||||
|
Jobs can be run every x days at specific times.
|
||||||
|
- [**Weekly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WeeklyJob):
|
||||||
|
Jobs can be run every x weeks on specific days of the week and at specific times.
|
||||||
|
- [**Monthly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonthlyJob):
|
||||||
|
Jobs can be run every x months on specific days of the month and at specific times.
|
||||||
|
- **Limited Concurrency**: Jobs can be limited individually or across the entire scheduler.
|
||||||
|
- [**Per job limiting with singleton mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithSingletonMode):
|
||||||
|
Jobs can be limited to a single concurrent execution that either reschedules (skips overlapping executions)
|
||||||
|
or queues (waits for the previous execution to finish).
|
||||||
|
- [**Per scheduler limiting with limit mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithLimitConcurrentJobs):
|
||||||
|
Jobs can be limited to a certain number of concurrent executions across the entire scheduler
|
||||||
|
using either reschedule (skip when the limit is met) or queue (jobs are added to a queue to
|
||||||
|
wait for the limit to be available).
|
||||||
|
- **Distributed instances of gocron**: Multiple instances of gocron can be run.
|
||||||
|
- [**Elector**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedElector):
|
||||||
|
An elector can be used to elect a single instance of gocron to run as the primary with the
|
||||||
|
other instances checking to see if a new leader needs to be elected.
|
||||||
|
- **Events**: Job events can trigger actions.
|
||||||
|
- [**Listeners**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithEventListeners):
|
||||||
|
[Event listeners](https://pkg.go.dev/github.com/go-co-op/gocron/v2#EventListener)
|
||||||
|
can be added to a job or all jobs in the
|
||||||
|
[scheduler](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions)
|
||||||
|
to listen for job events and trigger actions.
|
||||||
|
- **Options**: Many job and scheduler options are available
|
||||||
|
- [**Job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#JobOption):
|
||||||
|
Job options can be set when creating a job using `NewJob`.
|
||||||
|
- [**Global job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions):
|
||||||
|
Global job options can be set when creating a scheduler using `NewScheduler`.
|
||||||
|
- [**Scheduler options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#SchedulerOption):
|
||||||
|
Scheduler options can be set when creating a scheduler using `NewScheduler`.
|
||||||
|
|
||||||
|
## Supporters
|
||||||
|
|
||||||
|
[Jetbrains](https://www.jetbrains.com/?from=gocron) supports this project with Intellij licenses.
|
||||||
|
We appreciate their support for free and open source software!
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#go-co-op/gocron&Date)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# 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: [<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW)
|
||||||
|
|
||||||
|
We will do our best to addrerss any vulnerabilities in an expeditious manner.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Elector determines the leader from instances asking to be the leader. Only
|
||||||
|
// the leader runs jobs. If the leader goes down, a new leader will be elected.
|
||||||
|
type Elector interface {
|
||||||
|
// IsLeader should return nil if the job should be scheduled by the instance
|
||||||
|
// making the request and an error if the job should not be scheduled.
|
||||||
|
IsLeader(context.Context) error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Public error definitions
|
||||||
|
var (
|
||||||
|
ErrCronJobParse = fmt.Errorf("gocron: CronJob: crontab parse failure")
|
||||||
|
ErrDailyJobAtTimeNil = fmt.Errorf("gocron: DailyJob: atTime within atTimes must not be nil")
|
||||||
|
ErrDailyJobAtTimesNil = fmt.Errorf("gocron: DailyJob: atTimes must not be nil")
|
||||||
|
ErrDailyJobHours = fmt.Errorf("gocron: DailyJob: atTimes hours must be between 0 and 23 inclusive")
|
||||||
|
ErrDailyJobMinutesSeconds = fmt.Errorf("gocron: DailyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
|
||||||
|
ErrDurationRandomJobMinMax = fmt.Errorf("gocron: DurationRandomJob: minimum duration must be less than maximum duration")
|
||||||
|
ErrEventListenerFuncNil = fmt.Errorf("gocron: eventListenerFunc must not be nil")
|
||||||
|
ErrJobNotFound = fmt.Errorf("gocron: job not found")
|
||||||
|
ErrMonthlyJobDays = fmt.Errorf("gocron: MonthlyJob: daysOfTheMonth must be between 31 and -31 inclusive, and not 0")
|
||||||
|
ErrMonthlyJobAtTimeNil = fmt.Errorf("gocron: MonthlyJob: atTime within atTimes must not be nil")
|
||||||
|
ErrMonthlyJobAtTimesNil = fmt.Errorf("gocron: MonthlyJob: atTimes must not be nil")
|
||||||
|
ErrMonthlyJobDaysNil = fmt.Errorf("gocron: MonthlyJob: daysOfTheMonth must not be nil")
|
||||||
|
ErrMonthlyJobHours = fmt.Errorf("gocron: MonthlyJob: atTimes hours must be between 0 and 23 inclusive")
|
||||||
|
ErrMonthlyJobMinutesSeconds = fmt.Errorf("gocron: MonthlyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
|
||||||
|
ErrNewJobTask = fmt.Errorf("gocron: NewJob: Task.Function must be of kind reflect.Func")
|
||||||
|
ErrNewJobTaskNil = fmt.Errorf("gocron: NewJob: Task must not be nil")
|
||||||
|
ErrStopExecutorTimedOut = fmt.Errorf("gocron: timed out waiting for executor to stop")
|
||||||
|
ErrStopJobsTimedOut = fmt.Errorf("gocron: timed out waiting for jobs to finish")
|
||||||
|
ErrStopSchedulerTimedOut = fmt.Errorf("gocron: timed out waiting for scheduler to stop")
|
||||||
|
ErrWeeklyJobAtTimeNil = fmt.Errorf("gocron: WeeklyJob: atTime within atTimes must not be nil")
|
||||||
|
ErrWeeklyJobAtTimesNil = fmt.Errorf("gocron: WeeklyJob: atTimes must not be nil")
|
||||||
|
ErrWeeklyJobDaysOfTheWeekNil = fmt.Errorf("gocron: WeeklyJob: daysOfTheWeek must not be nil")
|
||||||
|
ErrWeeklyJobHours = fmt.Errorf("gocron: WeeklyJob: atTimes hours must be between 0 and 23 inclusive")
|
||||||
|
ErrWeeklyJobMinutesSeconds = fmt.Errorf("gocron: WeeklyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
|
||||||
|
ErrWithDistributedElector = fmt.Errorf("gocron: WithDistributedElector: elector must not be nil")
|
||||||
|
ErrWithClockNil = fmt.Errorf("gocron: WithClock: clock must not be nil")
|
||||||
|
ErrWithLocationNil = fmt.Errorf("gocron: WithLocation: location must not be nil")
|
||||||
|
ErrWithLoggerNil = fmt.Errorf("gocron: WithLogger: logger must not be nil")
|
||||||
|
ErrWithNameEmpty = fmt.Errorf("gocron: WithName: name must not be empty")
|
||||||
|
ErrWithStartDateTimePast = fmt.Errorf("gocron: WithStartDateTime: start must not be in the past")
|
||||||
|
)
|
||||||
|
|
||||||
|
// internal errors
|
||||||
|
var (
|
||||||
|
errAtTimeNil = fmt.Errorf("errAtTimeNil")
|
||||||
|
errAtTimesNil = fmt.Errorf("errAtTimesNil")
|
||||||
|
errAtTimeHours = fmt.Errorf("errAtTimeHours")
|
||||||
|
errAtTimeMinSec = fmt.Errorf("errAtTimeMinSec")
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,654 @@
|
||||||
|
package gocron_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "github.com/go-co-op/gocron/v2" // nolint:revive
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleAfterJobRuns() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithEventListeners(
|
||||||
|
AfterJobRuns(
|
||||||
|
func(jobID uuid.UUID) {
|
||||||
|
// do something after the job completes
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleAfterJobRunsWithError() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithEventListeners(
|
||||||
|
AfterJobRunsWithError(
|
||||||
|
func(jobID uuid.UUID, err error) {
|
||||||
|
// do something when the job returns an error
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleBeforeJobRuns() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithEventListeners(
|
||||||
|
BeforeJobRuns(
|
||||||
|
func(jobID uuid.UUID) {
|
||||||
|
// do something immediately before the job is run
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleCronJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
CronJob(
|
||||||
|
// standard cron tab parsing
|
||||||
|
"1 * * * *",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
CronJob(
|
||||||
|
// optionally include seconds as the first field
|
||||||
|
"* 1 * * * *",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDailyJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DailyJob(
|
||||||
|
1,
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(10, 30, 0),
|
||||||
|
NewAtTime(14, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(a, b string) {},
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDurationJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second*5,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDurationRandomJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationRandomJob(
|
||||||
|
time.Second,
|
||||||
|
5*time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleJob_ID() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println(j.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleJob_LastRun() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println(j.LastRun())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleJob_NextRun() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println(j.NextRun())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMonthlyJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(3, -5, -1),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(10, 30, 0),
|
||||||
|
NewAtTime(11, 15, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewScheduler() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
fmt.Println(s.Jobs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScheduler_NewJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, err := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
10*time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(j.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScheduler_RemoveByTags() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithTags("tag1"),
|
||||||
|
)
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithTags("tag2"),
|
||||||
|
)
|
||||||
|
fmt.Println(len(s.Jobs()))
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
s.RemoveByTags("tag1", "tag2")
|
||||||
|
|
||||||
|
fmt.Println(len(s.Jobs()))
|
||||||
|
// Output:
|
||||||
|
// 2
|
||||||
|
// 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScheduler_RemoveJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println(len(s.Jobs()))
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
_ = s.RemoveJob(j.ID())
|
||||||
|
|
||||||
|
fmt.Println(len(s.Jobs()))
|
||||||
|
// Output:
|
||||||
|
// 1
|
||||||
|
// 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScheduler_Start() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
CronJob(
|
||||||
|
"* * * * *",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScheduler_StopJobs() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
CronJob(
|
||||||
|
"* * * * *",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
_ = s.StopJobs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleScheduler_Update() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
CronJob(
|
||||||
|
"* * * * *",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
// after some time, need to change the job
|
||||||
|
|
||||||
|
j, _ = s.Update(
|
||||||
|
j.ID(),
|
||||||
|
DurationJob(
|
||||||
|
5*time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWeeklyJob() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
WeeklyJob(
|
||||||
|
2,
|
||||||
|
NewWeekdays(time.Tuesday, time.Wednesday, time.Saturday),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 30, 0),
|
||||||
|
NewAtTime(12, 0, 30),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithClock() {
|
||||||
|
fakeClock := clockwork.NewFakeClock()
|
||||||
|
s, _ := NewScheduler(
|
||||||
|
WithClock(fakeClock),
|
||||||
|
)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second*5,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(one string, two int) {
|
||||||
|
fmt.Printf("%s, %d\n", one, two)
|
||||||
|
wg.Done()
|
||||||
|
},
|
||||||
|
"one", 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
s.Start()
|
||||||
|
fakeClock.BlockUntil(1)
|
||||||
|
fakeClock.Advance(time.Second * 5)
|
||||||
|
wg.Wait()
|
||||||
|
_ = s.StopJobs()
|
||||||
|
// Output:
|
||||||
|
// one, 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithDistributedElector() {
|
||||||
|
//var _ Elector = (*myElector)(nil)
|
||||||
|
//
|
||||||
|
//type myElector struct{}
|
||||||
|
//
|
||||||
|
//func (m myElector) IsLeader(_ context.Context) error {
|
||||||
|
// return nil
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//elector := myElector{}
|
||||||
|
//
|
||||||
|
//_, _ = NewScheduler(
|
||||||
|
// WithDistributedElector(elector),
|
||||||
|
//)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithEventListeners() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithEventListeners(
|
||||||
|
AfterJobRuns(
|
||||||
|
func(jobID uuid.UUID) {
|
||||||
|
// do something after the job completes
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AfterJobRunsWithError(
|
||||||
|
func(jobID uuid.UUID, err error) {
|
||||||
|
// do something when the job returns an error
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BeforeJobRuns(
|
||||||
|
func(jobID uuid.UUID) {
|
||||||
|
// do something immediately before the job is run
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithGlobalJobOptions() {
|
||||||
|
s, _ := NewScheduler(
|
||||||
|
WithGlobalJobOptions(
|
||||||
|
WithTags("tag1", "tag2", "tag3"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(one string, two int) {
|
||||||
|
fmt.Printf("%s, %d", one, two)
|
||||||
|
},
|
||||||
|
"one", 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// The job will have the globally applied tags
|
||||||
|
fmt.Println(j.Tags())
|
||||||
|
|
||||||
|
s2, _ := NewScheduler(
|
||||||
|
WithGlobalJobOptions(
|
||||||
|
WithTags("tag1", "tag2", "tag3"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
j2, _ := s2.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(one string, two int) {
|
||||||
|
fmt.Printf("%s, %d", one, two)
|
||||||
|
},
|
||||||
|
"one", 2,
|
||||||
|
),
|
||||||
|
WithTags("tag4", "tag5", "tag6"),
|
||||||
|
)
|
||||||
|
// The job will have the tags set specifically on the job
|
||||||
|
// overriding those set globally by the scheduler
|
||||||
|
fmt.Println(j2.Tags())
|
||||||
|
// Output:
|
||||||
|
// [tag1 tag2 tag3]
|
||||||
|
// [tag4 tag5 tag6]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithLimitConcurrentJobs() {
|
||||||
|
_, _ = NewScheduler(
|
||||||
|
WithLimitConcurrentJobs(
|
||||||
|
1,
|
||||||
|
LimitModeReschedule,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithLimitedRuns() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Millisecond,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(one string, two int) {
|
||||||
|
fmt.Printf("%s, %d\n", one, two)
|
||||||
|
},
|
||||||
|
"one", 2,
|
||||||
|
),
|
||||||
|
WithLimitedRuns(1),
|
||||||
|
)
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
fmt.Printf("no jobs in scheduler: %v\n", s.Jobs())
|
||||||
|
_ = s.StopJobs()
|
||||||
|
// Output:
|
||||||
|
// one, 2
|
||||||
|
// no jobs in scheduler: []
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithLocation() {
|
||||||
|
location, _ := time.LoadLocation("Asia/Kolkata")
|
||||||
|
|
||||||
|
_, _ = NewScheduler(
|
||||||
|
WithLocation(location),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithLogger() {
|
||||||
|
_, _ = NewScheduler(
|
||||||
|
WithLogger(
|
||||||
|
NewJSONSlogLogger(slog.LevelInfo),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithName() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(one string, two int) {
|
||||||
|
fmt.Printf("%s, %d", one, two)
|
||||||
|
},
|
||||||
|
"one", 2,
|
||||||
|
),
|
||||||
|
WithName("job 1"),
|
||||||
|
)
|
||||||
|
fmt.Println(j.Name())
|
||||||
|
// Output:
|
||||||
|
// job 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithSingletonMode() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
_, _ = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {
|
||||||
|
// this job will skip half it's executions
|
||||||
|
// and effectively run every 2 seconds
|
||||||
|
time.Sleep(1500 * time.Second)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
WithSingletonMode(LimitModeReschedule),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithStartAt() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
start := time.Date(9999, 9, 9, 9, 9, 9, 9, time.UTC)
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(one string, two int) {
|
||||||
|
fmt.Printf("%s, %d", one, two)
|
||||||
|
},
|
||||||
|
"one", 2,
|
||||||
|
),
|
||||||
|
WithStartAt(
|
||||||
|
WithStartDateTime(start),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
s.Start()
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
next, _ := j.NextRun()
|
||||||
|
fmt.Println(next)
|
||||||
|
|
||||||
|
_ = s.StopJobs()
|
||||||
|
// Output:
|
||||||
|
// 9999-09-09 09:09:09.000000009 +0000 UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithStopTimeout() {
|
||||||
|
_, _ = NewScheduler(
|
||||||
|
WithStopTimeout(time.Second * 5),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWithTags() {
|
||||||
|
s, _ := NewScheduler()
|
||||||
|
defer func() { _ = s.Shutdown() }()
|
||||||
|
|
||||||
|
j, _ := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func(one string, two int) {
|
||||||
|
fmt.Printf("%s, %d", one, two)
|
||||||
|
},
|
||||||
|
"one", 2,
|
||||||
|
),
|
||||||
|
WithTags("tag1", "tag2", "tag3"),
|
||||||
|
)
|
||||||
|
fmt.Println(j.Tags())
|
||||||
|
// Output:
|
||||||
|
// [tag1 tag2 tag3]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,355 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type executor struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
logger Logger
|
||||||
|
stopCh chan struct{}
|
||||||
|
jobsIDsIn chan uuid.UUID
|
||||||
|
jobIDsOut chan uuid.UUID
|
||||||
|
jobOutRequest chan jobOutRequest
|
||||||
|
stopTimeout time.Duration
|
||||||
|
done chan error
|
||||||
|
singletonRunners map[uuid.UUID]singletonRunner
|
||||||
|
limitMode *limitModeConfig
|
||||||
|
elector Elector
|
||||||
|
}
|
||||||
|
|
||||||
|
type singletonRunner struct {
|
||||||
|
in chan uuid.UUID
|
||||||
|
rescheduleLimiter chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type limitModeConfig struct {
|
||||||
|
started bool
|
||||||
|
mode LimitMode
|
||||||
|
limit uint
|
||||||
|
rescheduleLimiter chan struct{}
|
||||||
|
in chan uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) start() {
|
||||||
|
e.logger.Debug("executor started")
|
||||||
|
|
||||||
|
// creating the executor's context here as the executor
|
||||||
|
// is the only goroutine that should access this context
|
||||||
|
// any other uses within the executor should create a context
|
||||||
|
// using the executor context as parent.
|
||||||
|
e.ctx, e.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// the standardJobsWg tracks
|
||||||
|
standardJobsWg := waitGroupWithMutex{
|
||||||
|
wg: sync.WaitGroup{},
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
singletonJobsWg := waitGroupWithMutex{
|
||||||
|
wg: sync.WaitGroup{},
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
limitModeJobsWg := waitGroupWithMutex{
|
||||||
|
wg: sync.WaitGroup{},
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the for leap that is the executor
|
||||||
|
// selecting on channels for work to do
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// job ids in are sent from 1 of 2 places:
|
||||||
|
// 1. the scheduler sends directly when jobs
|
||||||
|
// are run immediately.
|
||||||
|
// 2. sent from time.AfterFuncs in which job schedules
|
||||||
|
// are spun up by the scheduler
|
||||||
|
case id := <-e.jobsIDsIn:
|
||||||
|
select {
|
||||||
|
case <-e.stopCh:
|
||||||
|
e.stop(&standardJobsWg, &singletonJobsWg, &limitModeJobsWg)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// this context is used to handle cancellation of the executor
|
||||||
|
// on requests for a job to the scheduler via requestJobCtx
|
||||||
|
ctx, cancel := context.WithCancel(e.ctx)
|
||||||
|
|
||||||
|
if e.limitMode != nil && !e.limitMode.started {
|
||||||
|
// check if we are already running the limit mode runners
|
||||||
|
// if not, spin up the required number i.e. limit!
|
||||||
|
e.limitMode.started = true
|
||||||
|
for i := e.limitMode.limit; i > 0; i-- {
|
||||||
|
limitModeJobsWg.Add(1)
|
||||||
|
go e.limitModeRunner("limitMode-"+strconv.Itoa(int(i)), e.limitMode.in, &limitModeJobsWg, e.limitMode.mode, e.limitMode.rescheduleLimiter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spin off into a goroutine to unblock the executor and
|
||||||
|
// allow for processing for more work
|
||||||
|
go func() {
|
||||||
|
// make sure to cancel the above context per the docs
|
||||||
|
// // Canceling this context releases resources associated with it, so code should
|
||||||
|
// // call cancel as soon as the operations running in this Context complete.
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// check for limit mode - this spins up a separate runner which handles
|
||||||
|
// limiting the total number of concurrently running jobs
|
||||||
|
if e.limitMode != nil {
|
||||||
|
if e.limitMode.mode == LimitModeReschedule {
|
||||||
|
select {
|
||||||
|
// rescheduleLimiter is a channel the size of the limit
|
||||||
|
// this blocks publishing to the channel and keeps
|
||||||
|
// the executor from building up a waiting queue
|
||||||
|
// and forces rescheduling
|
||||||
|
case e.limitMode.rescheduleLimiter <- struct{}{}:
|
||||||
|
e.limitMode.in <- id
|
||||||
|
default:
|
||||||
|
// all runners are busy, reschedule the work for later
|
||||||
|
// which means we just skip it here and do nothing
|
||||||
|
// TODO when metrics are added, this should increment a rescheduled metric
|
||||||
|
select {
|
||||||
|
case e.jobIDsOut <- id:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// since we're not using LimitModeReschedule, but instead using LimitModeWait
|
||||||
|
// we do want to queue up the work to the limit mode runners and allow them
|
||||||
|
// to work through the channel backlog. A hard limit of 1000 is in place
|
||||||
|
// at which point this call would block.
|
||||||
|
// TODO when metrics are added, this should increment a wait metric
|
||||||
|
e.limitMode.in <- id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no limit mode, so we're either running a regular job or
|
||||||
|
// a job with a singleton mode
|
||||||
|
//
|
||||||
|
// get the job, so we can figure out what kind it is and how
|
||||||
|
// to execute it
|
||||||
|
j := requestJobCtx(ctx, id, e.jobOutRequest)
|
||||||
|
if j == nil {
|
||||||
|
// safety check as it'd be strange bug if this occurred
|
||||||
|
// TODO add a log line here
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if j.singletonMode {
|
||||||
|
// for singleton mode, get the existing runner for the job
|
||||||
|
// or spin up a new one
|
||||||
|
runner, ok := e.singletonRunners[id]
|
||||||
|
if !ok {
|
||||||
|
runner.in = make(chan uuid.UUID, 1000)
|
||||||
|
if j.singletonLimitMode == LimitModeReschedule {
|
||||||
|
runner.rescheduleLimiter = make(chan struct{}, 1)
|
||||||
|
}
|
||||||
|
e.singletonRunners[id] = runner
|
||||||
|
singletonJobsWg.Add(1)
|
||||||
|
go e.limitModeRunner("singleton-"+id.String(), runner.in, &singletonJobsWg, j.singletonLimitMode, runner.rescheduleLimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.singletonLimitMode == LimitModeReschedule {
|
||||||
|
// reschedule mode uses the limiter channel to check
|
||||||
|
// for a running job and reschedules if the channel is full.
|
||||||
|
select {
|
||||||
|
case runner.rescheduleLimiter <- struct{}{}:
|
||||||
|
runner.in <- id
|
||||||
|
default:
|
||||||
|
// runner is busy, reschedule the work for later
|
||||||
|
// which means we just skip it here and do nothing
|
||||||
|
// TODO when metrics are added, this should increment a rescheduled metric
|
||||||
|
select {
|
||||||
|
case e.jobIDsOut <- id:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// wait mode, fill up that queue (buffered channel, so it's ok)
|
||||||
|
runner.in <- id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
select {
|
||||||
|
case <-e.stopCh:
|
||||||
|
e.stop(&standardJobsWg, &singletonJobsWg, &limitModeJobsWg)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// we've gotten to the basic / standard jobs --
|
||||||
|
// the ones without anything special that just want
|
||||||
|
// to be run. Add to the WaitGroup so that
|
||||||
|
// stopping or shutting down can wait for the jobs to
|
||||||
|
// complete.
|
||||||
|
standardJobsWg.Add(1)
|
||||||
|
go func(j internalJob) {
|
||||||
|
e.runJob(j)
|
||||||
|
standardJobsWg.Done()
|
||||||
|
}(*j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case <-e.stopCh:
|
||||||
|
e.stop(&standardJobsWg, &singletonJobsWg, &limitModeJobsWg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) stop(standardJobsWg, singletonJobsWg, limitModeJobsWg *waitGroupWithMutex) {
|
||||||
|
e.logger.Debug("stopping executor")
|
||||||
|
// we've been asked to stop. This is either because the scheduler has been told
|
||||||
|
// to stop all jobs or the scheduler has been asked to completely shutdown.
|
||||||
|
//
|
||||||
|
// cancel tells all the functions to stop their work and send in a done response
|
||||||
|
e.cancel()
|
||||||
|
|
||||||
|
// the wait for job channels are used to report back whether we successfully waited
|
||||||
|
// for all jobs to complete or if we hit the configured timeout.
|
||||||
|
waitForJobs := make(chan struct{}, 1)
|
||||||
|
waitForSingletons := make(chan struct{}, 1)
|
||||||
|
waitForLimitMode := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
// the waiter context is used to cancel the functions waiting on jobs.
|
||||||
|
// this is done to avoid goroutine leaks.
|
||||||
|
waiterCtx, waiterCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// wait for standard jobs to complete
|
||||||
|
go func() {
|
||||||
|
e.logger.Debug("waiting for standard jobs to complete")
|
||||||
|
go func() {
|
||||||
|
// this is done in a separate goroutine, so we aren't
|
||||||
|
// blocked by the WaitGroup's Wait call in the event
|
||||||
|
// that the waiter context is cancelled.
|
||||||
|
// This particular goroutine could leak in the event that
|
||||||
|
// some long-running standard job doesn't complete.
|
||||||
|
standardJobsWg.Wait()
|
||||||
|
e.logger.Debug("standard jobs completed")
|
||||||
|
waitForJobs <- struct{}{}
|
||||||
|
}()
|
||||||
|
<-waiterCtx.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for per job singleton limit mode runner jobs to complete
|
||||||
|
go func() {
|
||||||
|
e.logger.Debug("waiting for singleton jobs to complete")
|
||||||
|
go func() {
|
||||||
|
singletonJobsWg.Wait()
|
||||||
|
e.logger.Debug("singleton jobs completed")
|
||||||
|
waitForSingletons <- struct{}{}
|
||||||
|
}()
|
||||||
|
<-waiterCtx.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for limit mode runners to complete
|
||||||
|
go func() {
|
||||||
|
e.logger.Debug("waiting for limit mode jobs to complete")
|
||||||
|
go func() {
|
||||||
|
limitModeJobsWg.Wait()
|
||||||
|
e.logger.Debug("limitMode jobs completed")
|
||||||
|
waitForLimitMode <- struct{}{}
|
||||||
|
}()
|
||||||
|
<-waiterCtx.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// now either wait for all the jobs to complete,
|
||||||
|
// or hit the timeout.
|
||||||
|
var count int
|
||||||
|
timeout := time.Now().Add(e.stopTimeout)
|
||||||
|
for time.Now().Before(timeout) && count < 3 {
|
||||||
|
select {
|
||||||
|
case <-waitForJobs:
|
||||||
|
count++
|
||||||
|
case <-waitForSingletons:
|
||||||
|
count++
|
||||||
|
// emptying the singleton runner map
|
||||||
|
// as we'll need to spin up new runners
|
||||||
|
// in the event that the scheduler is started again
|
||||||
|
e.singletonRunners = make(map[uuid.UUID]singletonRunner)
|
||||||
|
case <-waitForLimitMode:
|
||||||
|
count++
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count < 3 {
|
||||||
|
e.done <- ErrStopJobsTimedOut
|
||||||
|
e.logger.Debug("executor stopped - timed out")
|
||||||
|
} else {
|
||||||
|
e.done <- nil
|
||||||
|
e.logger.Debug("executor stopped")
|
||||||
|
}
|
||||||
|
waiterCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) limitModeRunner(name string, in chan uuid.UUID, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
|
||||||
|
e.logger.Debug("limitModeRunner starting", "name", name)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case id := <-in:
|
||||||
|
log.Println("limitModeRunner got job", id)
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
e.logger.Debug("limitModeRunner shutting down", "name", name)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(e.ctx)
|
||||||
|
j := requestJobCtx(ctx, id, e.jobOutRequest)
|
||||||
|
if j != nil {
|
||||||
|
log.Println("limitModeRunner running job", id)
|
||||||
|
e.runJob(*j)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
log.Println("limitModeRunner finished job", id)
|
||||||
|
|
||||||
|
// remove the limiter block to allow another job to be scheduled
|
||||||
|
if limitMode == LimitModeReschedule {
|
||||||
|
select {
|
||||||
|
case <-rescheduleLimiter:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println("limitModeRunner job done", id)
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
e.logger.Debug("limitModeRunner shutting down", "name", name)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *executor) runJob(j internalJob) {
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
case <-j.ctx.Done():
|
||||||
|
default:
|
||||||
|
if e.elector != nil {
|
||||||
|
if err := e.elector.IsLeader(j.ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = callJobFuncWithParams(j.beforeJobRuns, j.id)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-e.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-j.ctx.Done():
|
||||||
|
return
|
||||||
|
case e.jobIDsOut <- j.id:
|
||||||
|
}
|
||||||
|
|
||||||
|
err := callJobFuncWithParams(j.function, j.parameters...)
|
||||||
|
if err != nil {
|
||||||
|
_ = callJobFuncWithParams(j.afterJobRunsWithError, j.id, err)
|
||||||
|
} else {
|
||||||
|
_ = callJobFuncWithParams(j.afterJobRuns, j.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
module github.com/go-co-op/gocron/v2
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.4.0
|
||||||
|
github.com/jonboulle/clockwork v0.4.0
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
go.uber.org/goleak v1.3.0
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||||
|
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
@ -0,0 +1,779 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// internalJob stores the information needed by the scheduler
|
||||||
|
// to manage scheduling, starting and stopping the job
|
||||||
|
type internalJob struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
id uuid.UUID
|
||||||
|
name string
|
||||||
|
tags []string
|
||||||
|
jobSchedule
|
||||||
|
lastRun, nextRun time.Time
|
||||||
|
function any
|
||||||
|
parameters []any
|
||||||
|
timer clockwork.Timer
|
||||||
|
singletonMode bool
|
||||||
|
singletonLimitMode LimitMode
|
||||||
|
limitRunsTo *limitRunsTo
|
||||||
|
startTime time.Time
|
||||||
|
startImmediately bool
|
||||||
|
// event listeners
|
||||||
|
afterJobRuns func(jobID uuid.UUID)
|
||||||
|
beforeJobRuns func(jobID uuid.UUID)
|
||||||
|
afterJobRunsWithError func(jobID uuid.UUID, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop is used to stop the job's timer and cancel the context
|
||||||
|
// stopping the timer is critical for cleaning up jobs that are
|
||||||
|
// sleeping in a time.AfterFunc timer when the job is being stopped.
|
||||||
|
// cancelling the context keeps the executor from continuing to try
|
||||||
|
// and run the job.
|
||||||
|
func (j *internalJob) stop() {
|
||||||
|
if j.timer != nil {
|
||||||
|
j.timer.Stop()
|
||||||
|
}
|
||||||
|
j.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// task stores the function and parameters
|
||||||
|
// that are actually run when the job is executed.
|
||||||
|
type task struct {
|
||||||
|
function any
|
||||||
|
parameters []any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task defines a function that returns the task
|
||||||
|
// function and parameters.
|
||||||
|
type Task func() task
|
||||||
|
|
||||||
|
// NewTask provides the job's task function and parameters.
|
||||||
|
func NewTask(function any, parameters ...any) Task {
|
||||||
|
return func() task {
|
||||||
|
return task{
|
||||||
|
function: function,
|
||||||
|
parameters: parameters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// limitRunsTo is used for managing the number of runs
|
||||||
|
// when the user only wants the job to run a certain
|
||||||
|
// number of times and then be removed from the scheduler.
|
||||||
|
type limitRunsTo struct {
|
||||||
|
limit uint
|
||||||
|
runCount uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// --------------- Job Variants ------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// JobDefinition defines the interface that must be
|
||||||
|
// implemented to create a job from the definition.
|
||||||
|
type JobDefinition interface {
|
||||||
|
setup(*internalJob, *time.Location) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*cronJobDefinition)(nil)
|
||||||
|
|
||||||
|
type cronJobDefinition struct {
|
||||||
|
crontab string
|
||||||
|
withSeconds bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cronJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
var withLocation string
|
||||||
|
if strings.HasPrefix(c.crontab, "TZ=") || strings.HasPrefix(c.crontab, "CRON_TZ=") {
|
||||||
|
withLocation = c.crontab
|
||||||
|
} else {
|
||||||
|
// since the user didn't provide a timezone default to the location
|
||||||
|
// passed in by the scheduler. Default: time.Local
|
||||||
|
withLocation = fmt.Sprintf("CRON_TZ=%s %s", location.String(), c.crontab)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cronSchedule cron.Schedule
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.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 {
|
||||||
|
return errors.Join(ErrCronJobParse, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.jobSchedule = &cronJob{cronSchedule: cronSchedule}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CronJob defines a new job using the crontab syntax: `* * * * *`.
|
||||||
|
// An optional 6th field can be used at the beginning if withSeconds
|
||||||
|
// is set to true: `* * * * * *`.
|
||||||
|
// The timezone can be set on the Scheduler using WithLocation, or in the
|
||||||
|
// crontab in the form `TZ=America/Chicago * * * * *` or
|
||||||
|
// `CRON_TZ=America/Chicago * * * * *`
|
||||||
|
func CronJob(crontab string, withSeconds bool) JobDefinition {
|
||||||
|
return cronJobDefinition{
|
||||||
|
crontab: crontab,
|
||||||
|
withSeconds: withSeconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*durationJobDefinition)(nil)
|
||||||
|
|
||||||
|
type durationJobDefinition struct {
|
||||||
|
duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d durationJobDefinition) setup(j *internalJob, _ *time.Location) error {
|
||||||
|
j.jobSchedule = &durationJob{duration: d.duration}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurationJob defines a new job using time.Duration
|
||||||
|
// for the interval.
|
||||||
|
func DurationJob(duration time.Duration) JobDefinition {
|
||||||
|
return durationJobDefinition{
|
||||||
|
duration: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*durationRandomJobDefinition)(nil)
|
||||||
|
|
||||||
|
type durationRandomJobDefinition struct {
|
||||||
|
min, max time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d durationRandomJobDefinition) setup(j *internalJob, _ *time.Location) error {
|
||||||
|
if d.min >= d.max {
|
||||||
|
return ErrDurationRandomJobMinMax
|
||||||
|
}
|
||||||
|
|
||||||
|
j.jobSchedule = &durationRandomJob{
|
||||||
|
min: d.min,
|
||||||
|
max: d.max,
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurationRandomJob defines a new job that runs on a random interval
|
||||||
|
// between the min and max duration values provided.
|
||||||
|
//
|
||||||
|
// To achieve a similar behavior as tools that use a splay/jitter technique
|
||||||
|
// consider the median value as the baseline and the difference between the
|
||||||
|
// max-median or median-min as the splay/jitter.
|
||||||
|
//
|
||||||
|
// For example, if you want a job to run every 5 minutes, but want to add
|
||||||
|
// up to 1 min of jitter to the interval, you could use
|
||||||
|
// DurationRandomJob(4*time.Minute, 6*time.Minute)
|
||||||
|
func DurationRandomJob(minDuration, maxDuration time.Duration) JobDefinition {
|
||||||
|
return durationRandomJobDefinition{
|
||||||
|
min: minDuration,
|
||||||
|
max: maxDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DailyJob(interval uint, atTimes AtTimes) JobDefinition {
|
||||||
|
return dailyJobDefinition{
|
||||||
|
interval: interval,
|
||||||
|
atTimes: atTimes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*dailyJobDefinition)(nil)
|
||||||
|
|
||||||
|
type dailyJobDefinition struct {
|
||||||
|
interval uint
|
||||||
|
atTimes AtTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dailyJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
atTimesDate, err := convertAtTimesToDateTime(d.atTimes, location)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errAtTimesNil):
|
||||||
|
return ErrDailyJobAtTimesNil
|
||||||
|
case errors.Is(err, errAtTimeNil):
|
||||||
|
return ErrDailyJobAtTimeNil
|
||||||
|
case errors.Is(err, errAtTimeHours):
|
||||||
|
return ErrDailyJobHours
|
||||||
|
case errors.Is(err, errAtTimeMinSec):
|
||||||
|
return ErrDailyJobMinutesSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
ds := dailyJob{
|
||||||
|
interval: d.interval,
|
||||||
|
atTimes: atTimesDate,
|
||||||
|
}
|
||||||
|
j.jobSchedule = ds
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*weeklyJobDefinition)(nil)
|
||||||
|
|
||||||
|
type weeklyJobDefinition struct {
|
||||||
|
interval uint
|
||||||
|
daysOfTheWeek Weekdays
|
||||||
|
atTimes AtTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w weeklyJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
var ws weeklyJob
|
||||||
|
ws.interval = w.interval
|
||||||
|
|
||||||
|
if w.daysOfTheWeek == nil {
|
||||||
|
return ErrWeeklyJobDaysOfTheWeekNil
|
||||||
|
}
|
||||||
|
|
||||||
|
daysOfTheWeek := w.daysOfTheWeek()
|
||||||
|
|
||||||
|
slices.Sort(daysOfTheWeek)
|
||||||
|
|
||||||
|
atTimesDate, err := convertAtTimesToDateTime(w.atTimes, location)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errAtTimesNil):
|
||||||
|
return ErrWeeklyJobAtTimesNil
|
||||||
|
case errors.Is(err, errAtTimeNil):
|
||||||
|
return ErrWeeklyJobAtTimeNil
|
||||||
|
case errors.Is(err, errAtTimeHours):
|
||||||
|
return ErrWeeklyJobHours
|
||||||
|
case errors.Is(err, errAtTimeMinSec):
|
||||||
|
return ErrWeeklyJobMinutesSeconds
|
||||||
|
}
|
||||||
|
ws.atTimes = atTimesDate
|
||||||
|
|
||||||
|
j.jobSchedule = ws
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Weekdays func() []time.Weekday
|
||||||
|
|
||||||
|
func NewWeekdays(weekday time.Weekday, weekdays ...time.Weekday) Weekdays {
|
||||||
|
return func() []time.Weekday {
|
||||||
|
return append(weekdays, weekday)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WeeklyJob(interval uint, daysOfTheWeek Weekdays, atTimes AtTimes) JobDefinition {
|
||||||
|
return weeklyJobDefinition{
|
||||||
|
interval: interval,
|
||||||
|
daysOfTheWeek: daysOfTheWeek,
|
||||||
|
atTimes: atTimes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ JobDefinition = (*monthlyJobDefinition)(nil)
|
||||||
|
|
||||||
|
type monthlyJobDefinition struct {
|
||||||
|
interval uint
|
||||||
|
daysOfTheMonth DaysOfTheMonth
|
||||||
|
atTimes AtTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m monthlyJobDefinition) setup(j *internalJob, location *time.Location) error {
|
||||||
|
var ms monthlyJob
|
||||||
|
ms.interval = m.interval
|
||||||
|
|
||||||
|
if m.daysOfTheMonth == nil {
|
||||||
|
return ErrMonthlyJobDaysNil
|
||||||
|
}
|
||||||
|
|
||||||
|
var daysStart, daysEnd []int
|
||||||
|
for _, day := range m.daysOfTheMonth() {
|
||||||
|
if day > 31 || day == 0 || day < -31 {
|
||||||
|
return ErrMonthlyJobDays
|
||||||
|
}
|
||||||
|
if day > 0 {
|
||||||
|
daysStart = append(daysStart, day)
|
||||||
|
} else {
|
||||||
|
daysEnd = append(daysEnd, day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
daysStart = removeSliceDuplicatesInt(daysStart)
|
||||||
|
slices.Sort(daysStart)
|
||||||
|
ms.days = daysStart
|
||||||
|
|
||||||
|
daysEnd = removeSliceDuplicatesInt(daysEnd)
|
||||||
|
slices.Sort(daysEnd)
|
||||||
|
ms.daysFromEnd = daysEnd
|
||||||
|
|
||||||
|
atTimesDate, err := convertAtTimesToDateTime(m.atTimes, location)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, errAtTimesNil):
|
||||||
|
return ErrMonthlyJobAtTimesNil
|
||||||
|
case errors.Is(err, errAtTimeNil):
|
||||||
|
return ErrMonthlyJobAtTimeNil
|
||||||
|
case errors.Is(err, errAtTimeHours):
|
||||||
|
return ErrMonthlyJobHours
|
||||||
|
case errors.Is(err, errAtTimeMinSec):
|
||||||
|
return ErrMonthlyJobMinutesSeconds
|
||||||
|
}
|
||||||
|
ms.atTimes = atTimesDate
|
||||||
|
|
||||||
|
j.jobSchedule = ms
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type days []int
|
||||||
|
|
||||||
|
// DaysOfTheMonth defines a function that returns a list of days.
|
||||||
|
type DaysOfTheMonth func() days
|
||||||
|
|
||||||
|
// NewDaysOfTheMonth provide the days of the month the job should
|
||||||
|
// run. The days can be positive 1 to 31 and/or negative -31 to -1.
|
||||||
|
// Negative values count backwards from the end of the month.
|
||||||
|
// For example: -1 == the last day of the month.
|
||||||
|
//
|
||||||
|
// -5 == 5 days before the end of the month.
|
||||||
|
func NewDaysOfTheMonth(day int, moreDays ...int) DaysOfTheMonth {
|
||||||
|
return func() days {
|
||||||
|
moreDays = append(moreDays, day)
|
||||||
|
return moreDays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type atTime struct {
|
||||||
|
hours, minutes, seconds uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a atTime) time(location *time.Location) time.Time {
|
||||||
|
return time.Date(0, 0, 0, int(a.hours), int(a.minutes), int(a.seconds), 0, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AtTime defines a function that returns the internal atTime
|
||||||
|
type AtTime func() atTime
|
||||||
|
|
||||||
|
// NewAtTime provide the hours, minutes and seconds at which
|
||||||
|
// the job should be run
|
||||||
|
func NewAtTime(hours, minutes, seconds uint) AtTime {
|
||||||
|
return func() atTime {
|
||||||
|
return atTime{hours: hours, minutes: minutes, seconds: seconds}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AtTimes define a list of AtTime
|
||||||
|
type AtTimes func() []AtTime
|
||||||
|
|
||||||
|
// NewAtTimes provide the hours, minutes and seconds at which
|
||||||
|
// the job should be run
|
||||||
|
func NewAtTimes(atTime AtTime, atTimes ...AtTime) AtTimes {
|
||||||
|
return func() []AtTime {
|
||||||
|
atTimes = append(atTimes, atTime)
|
||||||
|
return atTimes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonthlyJob runs the job on the interval of months, on the specific days of the month
|
||||||
|
// specified, and at the set times. Days of the month can be 1 to 31 or negative (-1 to -31), which
|
||||||
|
// count backwards from the end of the month. E.g. -1 is the last day of the month.
|
||||||
|
//
|
||||||
|
// If a day of the month is selected that does not exist in all months (e.g. 31st)
|
||||||
|
// any month that does not have that day will be skipped.
|
||||||
|
//
|
||||||
|
// By default, the job will start the next available day, considering the last run to be now,
|
||||||
|
// and the time and month based on the interval, days and times you input.
|
||||||
|
// This means, if you select an interval greater than 1, your job by default will run
|
||||||
|
// X (interval) months from now.
|
||||||
|
// You can use WithStartAt to tell the scheduler to start the job sooner.
|
||||||
|
//
|
||||||
|
// Carefully consider your configuration!
|
||||||
|
// - For example: an interval of 2 months on the 31st of each month, starting 12/31
|
||||||
|
// would skip Feb, April, June, and next run would be in August.
|
||||||
|
func MonthlyJob(interval uint, daysOfTheMonth DaysOfTheMonth, atTimes AtTimes) JobDefinition {
|
||||||
|
return monthlyJobDefinition{
|
||||||
|
interval: interval,
|
||||||
|
daysOfTheMonth: daysOfTheMonth,
|
||||||
|
atTimes: atTimes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ----------------- Job Options -----------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
type JobOption func(*internalJob) error
|
||||||
|
|
||||||
|
func WithEventListeners(eventListeners ...EventListener) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
for _, eventListener := range eventListeners {
|
||||||
|
if err := eventListener(j); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLimitedRuns limits the number of executions of this job to n.
|
||||||
|
// Upon reaching the limit, the job is removed from the scheduler.
|
||||||
|
func WithLimitedRuns(limit uint) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.limitRunsTo = &limitRunsTo{
|
||||||
|
limit: limit,
|
||||||
|
runCount: 0,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithName sets the name of the job. Name provides
|
||||||
|
// a human-readable identifier for the job.
|
||||||
|
func WithName(name string) JobOption {
|
||||||
|
// TODO use the name for metrics and future logging option
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if name == "" {
|
||||||
|
return ErrWithNameEmpty
|
||||||
|
}
|
||||||
|
j.name = name
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSingletonMode(mode LimitMode) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.singletonMode = true
|
||||||
|
j.singletonLimitMode = mode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStartAt sets the option for starting the job
|
||||||
|
func WithStartAt(option StartAtOption) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
return option(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAtOption defines options for starting the job
|
||||||
|
type StartAtOption func(*internalJob) error
|
||||||
|
|
||||||
|
// WithStartImmediately tells the scheduler to run the job immediately
|
||||||
|
// regardless of the type or schedule of job. After this immediate run
|
||||||
|
// the job is scheduled from this time based on the job definition.
|
||||||
|
func WithStartImmediately() StartAtOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.startImmediately = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStartDateTime sets the first date & time at which the job should run.
|
||||||
|
func WithStartDateTime(start time.Time) StartAtOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if start.IsZero() || start.Before(time.Now()) {
|
||||||
|
return ErrWithStartDateTimePast
|
||||||
|
}
|
||||||
|
j.startTime = start
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTags(tags ...string) JobOption {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
j.tags = tags
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ------------- Job Event Listeners -------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
type EventListener func(*internalJob) error
|
||||||
|
|
||||||
|
func AfterJobRuns(eventListenerFunc func(jobID uuid.UUID)) EventListener {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if eventListenerFunc == nil {
|
||||||
|
return ErrEventListenerFuncNil
|
||||||
|
}
|
||||||
|
j.afterJobRuns = eventListenerFunc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, err error)) EventListener {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if eventListenerFunc == nil {
|
||||||
|
return ErrEventListenerFuncNil
|
||||||
|
}
|
||||||
|
j.afterJobRunsWithError = eventListenerFunc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID)) EventListener {
|
||||||
|
return func(j *internalJob) error {
|
||||||
|
if eventListenerFunc == nil {
|
||||||
|
return ErrEventListenerFuncNil
|
||||||
|
}
|
||||||
|
j.beforeJobRuns = eventListenerFunc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ---------------- Job Schedules ----------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
type jobSchedule interface {
|
||||||
|
next(lastRun time.Time) time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*cronJob)(nil)
|
||||||
|
|
||||||
|
type cronJob struct {
|
||||||
|
cronSchedule cron.Schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *cronJob) next(lastRun time.Time) time.Time {
|
||||||
|
return j.cronSchedule.Next(lastRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*durationJob)(nil)
|
||||||
|
|
||||||
|
type durationJob struct {
|
||||||
|
duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *durationJob) next(lastRun time.Time) time.Time {
|
||||||
|
return lastRun.Add(j.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*durationRandomJob)(nil)
|
||||||
|
|
||||||
|
type durationRandomJob struct {
|
||||||
|
min, max time.Duration
|
||||||
|
rand *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *durationRandomJob) next(lastRun time.Time) time.Time {
|
||||||
|
r := j.rand.Int63n(int64(j.max - j.min))
|
||||||
|
return lastRun.Add(j.min + time.Duration(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*dailyJob)(nil)
|
||||||
|
|
||||||
|
type dailyJob struct {
|
||||||
|
interval uint
|
||||||
|
atTimes []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dailyJob) next(lastRun time.Time) time.Time {
|
||||||
|
next := d.nextDay(lastRun)
|
||||||
|
if !next.IsZero() {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
startNextDay := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(d.interval), 0, 0, 0, lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
return d.nextDay(startNextDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dailyJob) nextDay(lastRun time.Time) time.Time {
|
||||||
|
for _, at := range d.atTimes {
|
||||||
|
// sub the at time hour/min/sec onto the lastRun's values
|
||||||
|
// to use in checks to see if we've got our next run time
|
||||||
|
atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
|
||||||
|
if atDate.After(lastRun) {
|
||||||
|
// checking to see if it is after i.e. greater than,
|
||||||
|
// and not greater or equal as our lastRun day/time
|
||||||
|
// will be in the loop, and we don't want to select it again
|
||||||
|
return atDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*weeklyJob)(nil)
|
||||||
|
|
||||||
|
type weeklyJob struct {
|
||||||
|
interval uint
|
||||||
|
daysOfWeek []time.Weekday
|
||||||
|
atTimes []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w weeklyJob) next(lastRun time.Time) time.Time {
|
||||||
|
next := w.nextWeekDayAtTime(lastRun)
|
||||||
|
if !next.IsZero() {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
startOfTheNextIntervalWeek := (lastRun.Day() - int(lastRun.Weekday())) + int(w.interval*7)
|
||||||
|
from := time.Date(lastRun.Year(), lastRun.Month(), startOfTheNextIntervalWeek, 0, 0, 0, 0, lastRun.Location())
|
||||||
|
return w.nextWeekDayAtTime(from)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w weeklyJob) nextWeekDayAtTime(lastRun time.Time) time.Time {
|
||||||
|
for _, wd := range w.daysOfWeek {
|
||||||
|
// checking if we're on the same day or later in the same week
|
||||||
|
if wd >= lastRun.Weekday() {
|
||||||
|
// weekDayDiff is used to add the correct amount to the atDate day below
|
||||||
|
weekDayDiff := wd - lastRun.Weekday()
|
||||||
|
for _, at := range w.atTimes {
|
||||||
|
// sub the at time hour/min/sec onto the lastRun's values
|
||||||
|
// to use in checks to see if we've got our next run time
|
||||||
|
atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(weekDayDiff), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
|
||||||
|
if atDate.After(lastRun) {
|
||||||
|
// checking to see if it is after i.e. greater than,
|
||||||
|
// and not greater or equal as our lastRun day/time
|
||||||
|
// will be in the loop, and we don't want to select it again
|
||||||
|
return atDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ jobSchedule = (*monthlyJob)(nil)
|
||||||
|
|
||||||
|
type monthlyJob struct {
|
||||||
|
interval uint
|
||||||
|
days []int
|
||||||
|
daysFromEnd []int
|
||||||
|
atTimes []time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m monthlyJob) next(lastRun time.Time) time.Time {
|
||||||
|
daysList := make([]int, len(m.days))
|
||||||
|
copy(daysList, m.days)
|
||||||
|
firstDayNextMonth := time.Date(lastRun.Year(), lastRun.Month()+1, 1, 0, 0, 0, 0, lastRun.Location())
|
||||||
|
for _, daySub := range m.daysFromEnd {
|
||||||
|
// getting a combined list of all the daysList and the negative daysList
|
||||||
|
// which count backwards from the first day of the next month
|
||||||
|
// -1 == the last day of the month
|
||||||
|
day := firstDayNextMonth.AddDate(0, 0, daySub).Day()
|
||||||
|
daysList = append(daysList, day)
|
||||||
|
}
|
||||||
|
slices.Sort(daysList)
|
||||||
|
|
||||||
|
next := m.nextMonthDayAtTime(lastRun, daysList)
|
||||||
|
if !next.IsZero() {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
from := time.Date(lastRun.Year(), lastRun.Month()+time.Month(m.interval), 1, 0, 0, 0, 0, lastRun.Location())
|
||||||
|
for next.IsZero() {
|
||||||
|
next = m.nextMonthDayAtTime(from, daysList)
|
||||||
|
from = from.AddDate(0, int(m.interval), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int) time.Time {
|
||||||
|
// find the next day in the month that should run and then check for an at time
|
||||||
|
for _, day := range days {
|
||||||
|
if day >= lastRun.Day() {
|
||||||
|
for _, at := range m.atTimes {
|
||||||
|
// sub the day, and the at time hour/min/sec onto the lastRun's values
|
||||||
|
// to use in checks to see if we've got our next run time
|
||||||
|
atDate := time.Date(lastRun.Year(), lastRun.Month(), day, at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
|
||||||
|
|
||||||
|
if atDate.Month() != lastRun.Month() {
|
||||||
|
// this check handles if we're setting a day not in the current month
|
||||||
|
// e.g. setting day 31 in Feb results in March 2nd
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if atDate.After(lastRun) {
|
||||||
|
// checking to see if it is after i.e. greater than,
|
||||||
|
// and not greater or equal as our lastRun day/time
|
||||||
|
// will be in the loop, and we don't want to select it again
|
||||||
|
return atDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ---------------- Job Interface ----------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// Job provides the available methods on the job
|
||||||
|
// available to the caller.
|
||||||
|
type Job interface {
|
||||||
|
ID() uuid.UUID
|
||||||
|
LastRun() (time.Time, error)
|
||||||
|
Name() string
|
||||||
|
NextRun() (time.Time, error)
|
||||||
|
Tags() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Job = (*job)(nil)
|
||||||
|
|
||||||
|
// job is the internal struct that implements
|
||||||
|
// the public interface. This is used to avoid
|
||||||
|
// leaking information the caller never needs
|
||||||
|
// to have or tinker with.
|
||||||
|
type job struct {
|
||||||
|
id uuid.UUID
|
||||||
|
name string
|
||||||
|
tags []string
|
||||||
|
jobOutRequest chan jobOutRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the job's unique identifier.
|
||||||
|
func (j job) ID() uuid.UUID {
|
||||||
|
return j.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastRun returns the time of the job's last run
|
||||||
|
func (j job) LastRun() (time.Time, error) {
|
||||||
|
ij := requestJob(j.id, j.jobOutRequest)
|
||||||
|
if ij == nil || ij.id == uuid.Nil {
|
||||||
|
return time.Time{}, ErrJobNotFound
|
||||||
|
}
|
||||||
|
return ij.lastRun, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name defined on the job.
|
||||||
|
func (j job) Name() string {
|
||||||
|
return j.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextRun returns the time of the job's next scheduled run.
|
||||||
|
func (j job) NextRun() (time.Time, error) {
|
||||||
|
ij := requestJob(j.id, j.jobOutRequest)
|
||||||
|
if ij == nil || ij.id == uuid.Nil {
|
||||||
|
return time.Time{}, ErrJobNotFound
|
||||||
|
}
|
||||||
|
return ij.nextRun, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags returns the job's string tags.
|
||||||
|
func (j job) Tags() []string {
|
||||||
|
return j.tags
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDurationJob_next(t *testing.T) {
|
||||||
|
tests := []time.Duration{
|
||||||
|
time.Millisecond,
|
||||||
|
time.Second,
|
||||||
|
100 * time.Second,
|
||||||
|
1000 * time.Second,
|
||||||
|
5 * time.Second,
|
||||||
|
50 * time.Second,
|
||||||
|
time.Minute,
|
||||||
|
5 * time.Minute,
|
||||||
|
100 * time.Minute,
|
||||||
|
time.Hour,
|
||||||
|
2 * time.Hour,
|
||||||
|
100 * time.Hour,
|
||||||
|
1000 * time.Hour,
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRun := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
for _, duration := range tests {
|
||||||
|
t.Run(duration.String(), func(t *testing.T) {
|
||||||
|
d := durationJob{duration: duration}
|
||||||
|
next := d.next(lastRun)
|
||||||
|
expected := lastRun.Add(duration)
|
||||||
|
|
||||||
|
assert.Equal(t, expected, next)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDailyJob_next(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
interval uint
|
||||||
|
atTimes []time.Time
|
||||||
|
lastRun time.Time
|
||||||
|
expectedNextRun time.Time
|
||||||
|
expectedDurationToNextRun time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"daily multiple at times",
|
||||||
|
1,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),
|
||||||
|
7 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"every 2 days multiple at times",
|
||||||
|
2,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 3, 5, 30, 0, 0, time.UTC),
|
||||||
|
41 * time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
d := dailyJob{
|
||||||
|
interval: tt.interval,
|
||||||
|
atTimes: tt.atTimes,
|
||||||
|
}
|
||||||
|
|
||||||
|
next := d.next(tt.lastRun)
|
||||||
|
assert.Equal(t, tt.expectedNextRun, next)
|
||||||
|
assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeeklyJob_next(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
interval uint
|
||||||
|
daysOfWeek []time.Weekday
|
||||||
|
atTimes []time.Time
|
||||||
|
lastRun time.Time
|
||||||
|
expectedNextRun time.Time
|
||||||
|
expectedDurationToNextRun time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"last run Monday, next run is Thursday",
|
||||||
|
1,
|
||||||
|
[]time.Weekday{time.Monday, time.Thursday},
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 3, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 6, 5, 30, 0, 0, time.UTC),
|
||||||
|
3 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"last run Thursday, next run is Monday",
|
||||||
|
1,
|
||||||
|
[]time.Weekday{time.Monday, time.Thursday},
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 6, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 10, 5, 30, 0, 0, time.UTC),
|
||||||
|
4 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
w := weeklyJob{
|
||||||
|
interval: tt.interval,
|
||||||
|
daysOfWeek: tt.daysOfWeek,
|
||||||
|
atTimes: tt.atTimes,
|
||||||
|
}
|
||||||
|
|
||||||
|
next := w.next(tt.lastRun)
|
||||||
|
assert.Equal(t, tt.expectedNextRun, next)
|
||||||
|
assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonthlyJob_next(t *testing.T) {
|
||||||
|
americaChicago, err := time.LoadLocation("America/Chicago")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
interval uint
|
||||||
|
days []int
|
||||||
|
daysFromEnd []int
|
||||||
|
atTimes []time.Time
|
||||||
|
lastRun time.Time
|
||||||
|
expectedNextRun time.Time
|
||||||
|
expectedDurationToNextRun time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"same day - before at time",
|
||||||
|
1,
|
||||||
|
[]int{1},
|
||||||
|
nil,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
|
||||||
|
5*time.Hour + 30*time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"same day - after at time, runs next available date",
|
||||||
|
1,
|
||||||
|
[]int{1, 10},
|
||||||
|
nil,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 10, 5, 30, 0, 0, time.UTC),
|
||||||
|
9 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"same day - after at time, runs next available date, following interval month",
|
||||||
|
2,
|
||||||
|
[]int{1},
|
||||||
|
nil,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 3, 1, 5, 30, 0, 0, time.UTC),
|
||||||
|
60 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"daylight savings time",
|
||||||
|
1,
|
||||||
|
[]int{5},
|
||||||
|
nil,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),
|
||||||
|
},
|
||||||
|
time.Date(2023, 11, 1, 0, 0, 0, 0, americaChicago),
|
||||||
|
time.Date(2023, 11, 5, 5, 30, 0, 0, americaChicago),
|
||||||
|
4*24*time.Hour + 6*time.Hour + 30*time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"negative days",
|
||||||
|
1,
|
||||||
|
nil,
|
||||||
|
[]int{-1, -3, -5},
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 29, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),
|
||||||
|
2 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day not in current month, runs next month (leap year)",
|
||||||
|
1,
|
||||||
|
[]int{31},
|
||||||
|
nil,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 3, 31, 5, 30, 0, 0, time.UTC),
|
||||||
|
29*24*time.Hour + 31*24*time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple days not in order",
|
||||||
|
1,
|
||||||
|
[]int{10, 7, 19, 2},
|
||||||
|
nil,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(2000, 1, 2, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 7, 5, 30, 0, 0, time.UTC),
|
||||||
|
5 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day not in next interval month, selects next available option, skips Feb, April & June",
|
||||||
|
2,
|
||||||
|
[]int{31},
|
||||||
|
nil,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
time.Date(1999, 12, 31, 5, 30, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 8, 31, 5, 30, 0, 0, time.UTC),
|
||||||
|
244 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m := monthlyJob{
|
||||||
|
interval: tt.interval,
|
||||||
|
days: tt.days,
|
||||||
|
daysFromEnd: tt.daysFromEnd,
|
||||||
|
atTimes: tt.atTimes,
|
||||||
|
}
|
||||||
|
|
||||||
|
next := m.next(tt.lastRun)
|
||||||
|
assert.Equal(t, tt.expectedNextRun, next)
|
||||||
|
assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationRandomJob_next(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
min time.Duration
|
||||||
|
max time.Duration
|
||||||
|
lastRun time.Time
|
||||||
|
expectedMin time.Time
|
||||||
|
expectedMax time.Time
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"min 1s, max 5s",
|
||||||
|
time.Second,
|
||||||
|
5 * time.Second,
|
||||||
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 1, 0, 0, 5, 0, time.UTC),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"min 100ms, max 1s",
|
||||||
|
100 * time.Millisecond,
|
||||||
|
1 * time.Second,
|
||||||
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(2000, 1, 1, 0, 0, 0, 100000000, time.UTC),
|
||||||
|
time.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
rj := durationRandomJob{
|
||||||
|
min: tt.min,
|
||||||
|
max: tt.max,
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
next := rj.next(tt.lastRun)
|
||||||
|
assert.GreaterOrEqual(t, next, tt.expectedMin)
|
||||||
|
assert.LessOrEqual(t, next, tt.expectedMax)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJob_LastRun(t *testing.T) {
|
||||||
|
testTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)
|
||||||
|
fakeClock := clockwork.NewFakeClockAt(testTime)
|
||||||
|
|
||||||
|
s, err := newTestScheduler(
|
||||||
|
WithClock(fakeClock),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
j, err := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithStartAt(WithStartImmediately()),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
lastRun, err := j.LastRun()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.Shutdown()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, testTime, lastRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithEventListeners(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventListeners []EventListener
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"no event listeners",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"afterJobRuns",
|
||||||
|
[]EventListener{
|
||||||
|
AfterJobRuns(func(_ uuid.UUID) {}),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"afterJobRunsWithError",
|
||||||
|
[]EventListener{
|
||||||
|
AfterJobRunsWithError(func(_ uuid.UUID, _ error) {}),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beforeJobRuns",
|
||||||
|
[]EventListener{
|
||||||
|
BeforeJobRuns(func(_ uuid.UUID) {}),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple event listeners",
|
||||||
|
[]EventListener{
|
||||||
|
AfterJobRuns(func(_ uuid.UUID) {}),
|
||||||
|
AfterJobRunsWithError(func(_ uuid.UUID, _ error) {}),
|
||||||
|
BeforeJobRuns(func(_ uuid.UUID) {}),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nil after job runs listener",
|
||||||
|
[]EventListener{
|
||||||
|
AfterJobRuns(nil),
|
||||||
|
},
|
||||||
|
ErrEventListenerFuncNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nil after job runs with error listener",
|
||||||
|
[]EventListener{
|
||||||
|
AfterJobRunsWithError(nil),
|
||||||
|
},
|
||||||
|
ErrEventListenerFuncNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nil before job runs listener",
|
||||||
|
[]EventListener{
|
||||||
|
BeforeJobRuns(nil),
|
||||||
|
},
|
||||||
|
ErrEventListenerFuncNil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var ij internalJob
|
||||||
|
err := WithEventListeners(tt.eventListeners...)(&ij)
|
||||||
|
assert.Equal(t, tt.err, err)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
if ij.afterJobRuns != nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if ij.afterJobRunsWithError != nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if ij.beforeJobRuns != nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
assert.Equal(t, len(tt.eventListeners), count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is the interface that wraps the basic logging methods
|
||||||
|
// used by gocron. The methods are modeled after the standard
|
||||||
|
// library slog package. The default logger is a no-op logger.
|
||||||
|
// To enable logging, use one of the provided New*Logger functions
|
||||||
|
// or implement your own Logger. The actual level of Log that is logged
|
||||||
|
// is handled by the implementation.
|
||||||
|
type Logger interface {
|
||||||
|
Debug(msg string, args ...any)
|
||||||
|
Error(msg string, args ...any)
|
||||||
|
Info(msg string, args ...any)
|
||||||
|
Warn(msg string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Logger = (*noOpLogger)(nil)
|
||||||
|
|
||||||
|
type noOpLogger struct{}
|
||||||
|
|
||||||
|
func (l noOpLogger) Debug(_ string, _ ...any) {}
|
||||||
|
func (l noOpLogger) Error(_ string, _ ...any) {}
|
||||||
|
func (l noOpLogger) Info(_ string, _ ...any) {}
|
||||||
|
func (l noOpLogger) Warn(_ string, _ ...any) {}
|
||||||
|
|
||||||
|
var _ Logger = (*slogLogger)(nil)
|
||||||
|
|
||||||
|
type slogLogger struct {
|
||||||
|
sl *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJSONSlogLogger(level slog.Level) Logger {
|
||||||
|
return NewSlogLogger(
|
||||||
|
slog.New(
|
||||||
|
slog.NewJSONHandler(
|
||||||
|
os.Stdout,
|
||||||
|
&slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSlogLogger(sl *slog.Logger) Logger {
|
||||||
|
return &slogLogger{sl: sl}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *slogLogger) Debug(msg string, args ...any) {
|
||||||
|
l.sl.Debug(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *slogLogger) Error(msg string, args ...any) {
|
||||||
|
l.sl.Error(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *slogLogger) Info(msg string, args ...any) {
|
||||||
|
l.sl.Info(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *slogLogger) Warn(msg string, args ...any) {
|
||||||
|
l.sl.Warn(msg, args...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,633 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Scheduler = (*scheduler)(nil)
|
||||||
|
|
||||||
|
type Scheduler interface {
|
||||||
|
Jobs() []Job
|
||||||
|
NewJob(JobDefinition, Task, ...JobOption) (Job, error)
|
||||||
|
RemoveByTags(...string)
|
||||||
|
RemoveJob(uuid.UUID) error
|
||||||
|
Start()
|
||||||
|
StopJobs() error
|
||||||
|
Shutdown() error
|
||||||
|
Update(uuid.UUID, JobDefinition, Task, ...JobOption) (Job, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ----------------- Scheduler -------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
type scheduler struct {
|
||||||
|
shutdownCtx context.Context
|
||||||
|
shutdownCancel context.CancelFunc
|
||||||
|
exec executor
|
||||||
|
jobs map[uuid.UUID]internalJob
|
||||||
|
location *time.Location
|
||||||
|
clock clockwork.Clock
|
||||||
|
started bool
|
||||||
|
globalJobOptions []JobOption
|
||||||
|
logger Logger
|
||||||
|
|
||||||
|
startCh chan struct{}
|
||||||
|
startedCh chan struct{}
|
||||||
|
stopCh chan struct{}
|
||||||
|
stopErrCh chan error
|
||||||
|
allJobsOutRequest chan allJobsOutRequest
|
||||||
|
jobOutRequestCh chan jobOutRequest
|
||||||
|
newJobCh chan internalJob
|
||||||
|
removeJobCh chan uuid.UUID
|
||||||
|
removeJobsByTagsCh chan []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type jobOutRequest struct {
|
||||||
|
id uuid.UUID
|
||||||
|
outChan chan internalJob
|
||||||
|
}
|
||||||
|
|
||||||
|
type allJobsOutRequest struct {
|
||||||
|
outChan chan []Job
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScheduler(options ...SchedulerOption) (Scheduler, error) {
|
||||||
|
schCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
exec := executor{
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
stopTimeout: time.Second * 10,
|
||||||
|
singletonRunners: make(map[uuid.UUID]singletonRunner),
|
||||||
|
logger: &noOpLogger{},
|
||||||
|
|
||||||
|
jobsIDsIn: make(chan uuid.UUID),
|
||||||
|
jobIDsOut: make(chan uuid.UUID),
|
||||||
|
jobOutRequest: make(chan jobOutRequest, 1000),
|
||||||
|
done: make(chan error),
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &scheduler{
|
||||||
|
shutdownCtx: schCtx,
|
||||||
|
shutdownCancel: cancel,
|
||||||
|
exec: exec,
|
||||||
|
jobs: make(map[uuid.UUID]internalJob),
|
||||||
|
location: time.Local,
|
||||||
|
clock: clockwork.NewRealClock(),
|
||||||
|
logger: &noOpLogger{},
|
||||||
|
|
||||||
|
newJobCh: make(chan internalJob),
|
||||||
|
removeJobCh: make(chan uuid.UUID),
|
||||||
|
removeJobsByTagsCh: make(chan []string),
|
||||||
|
startCh: make(chan struct{}),
|
||||||
|
startedCh: make(chan struct{}),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
stopErrCh: make(chan error, 1),
|
||||||
|
jobOutRequestCh: make(chan jobOutRequest),
|
||||||
|
allJobsOutRequest: make(chan allJobsOutRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
err := option(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.logger.Info("new scheduler created")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case id := <-s.exec.jobIDsOut:
|
||||||
|
s.selectExecJobIDsOut(id)
|
||||||
|
|
||||||
|
case j := <-s.newJobCh:
|
||||||
|
s.selectNewJob(j)
|
||||||
|
|
||||||
|
case id := <-s.removeJobCh:
|
||||||
|
s.selectRemoveJob(id)
|
||||||
|
|
||||||
|
case tags := <-s.removeJobsByTagsCh:
|
||||||
|
s.selectRemoveJobsByTags(tags)
|
||||||
|
|
||||||
|
case out := <-s.exec.jobOutRequest:
|
||||||
|
s.selectJobOutRequest(out)
|
||||||
|
|
||||||
|
case out := <-s.jobOutRequestCh:
|
||||||
|
s.selectJobOutRequest(out)
|
||||||
|
|
||||||
|
case out := <-s.allJobsOutRequest:
|
||||||
|
s.selectAllJobsOutRequest(out)
|
||||||
|
|
||||||
|
case <-s.startCh:
|
||||||
|
s.selectStart()
|
||||||
|
|
||||||
|
case <-s.stopCh:
|
||||||
|
s.stopScheduler()
|
||||||
|
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
s.stopScheduler()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// --------- Scheduler Channel Methods -----------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// The scheduler's channel functions are broken out here
|
||||||
|
// to allow prioritizing within the select blocks. The idea
|
||||||
|
// being that we want to make sure that scheduling tasks
|
||||||
|
// are not blocked by requests from the caller for information
|
||||||
|
// about jobs.
|
||||||
|
|
||||||
|
func (s *scheduler) stopScheduler() {
|
||||||
|
s.logger.Debug("stopping scheduler")
|
||||||
|
if s.started {
|
||||||
|
s.exec.stopCh <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, j := range s.jobs {
|
||||||
|
j.stop()
|
||||||
|
}
|
||||||
|
for id, j := range s.jobs {
|
||||||
|
<-j.ctx.Done()
|
||||||
|
|
||||||
|
j.ctx, j.cancel = context.WithCancel(s.shutdownCtx)
|
||||||
|
s.jobs[id] = j
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if s.started {
|
||||||
|
select {
|
||||||
|
case err = <-s.exec.done:
|
||||||
|
case <-time.After(s.exec.stopTimeout + 1*time.Second):
|
||||||
|
err = ErrStopExecutorTimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.stopErrCh <- err
|
||||||
|
s.started = false
|
||||||
|
s.logger.Debug("scheduler stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectAllJobsOutRequest(out allJobsOutRequest) {
|
||||||
|
outJobs := make([]Job, len(s.jobs))
|
||||||
|
var counter int
|
||||||
|
for _, j := range s.jobs {
|
||||||
|
outJobs[counter] = s.jobFromInternalJob(j)
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case out.outChan <- outJobs:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectRemoveJob(id uuid.UUID) {
|
||||||
|
j, ok := s.jobs[id]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.stop()
|
||||||
|
delete(s.jobs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectExecJobIDsOut(id uuid.UUID) {
|
||||||
|
j := s.jobs[id]
|
||||||
|
j.lastRun = j.nextRun
|
||||||
|
|
||||||
|
if j.limitRunsTo != nil {
|
||||||
|
j.limitRunsTo.runCount = j.limitRunsTo.runCount + 1
|
||||||
|
if j.limitRunsTo.runCount == j.limitRunsTo.limit {
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return
|
||||||
|
case s.removeJobCh <- id:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next := j.next(j.lastRun)
|
||||||
|
j.nextRun = next
|
||||||
|
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return
|
||||||
|
case s.exec.jobsIDsIn <- id:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.jobs[id] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectJobOutRequest(out jobOutRequest) {
|
||||||
|
if j, ok := s.jobs[out.id]; ok {
|
||||||
|
select {
|
||||||
|
case out.outChan <- j:
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(out.outChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectNewJob(j internalJob) {
|
||||||
|
if s.started {
|
||||||
|
next := j.startTime
|
||||||
|
if j.startImmediately {
|
||||||
|
next = s.now()
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIDsIn <- j.id:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if next.IsZero() {
|
||||||
|
next = j.next(s.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
id := j.id
|
||||||
|
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIDsIn <- id:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
j.nextRun = next
|
||||||
|
}
|
||||||
|
|
||||||
|
s.jobs[j.id] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectRemoveJobsByTags(tags []string) {
|
||||||
|
for _, j := range s.jobs {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if slices.Contains(j.tags, tag) {
|
||||||
|
j.stop()
|
||||||
|
delete(s.jobs, j.id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) selectStart() {
|
||||||
|
s.logger.Debug("scheduler starting")
|
||||||
|
go s.exec.start()
|
||||||
|
|
||||||
|
s.started = true
|
||||||
|
for id, j := range s.jobs {
|
||||||
|
next := j.startTime
|
||||||
|
if j.startImmediately {
|
||||||
|
next = s.now()
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIDsIn <- id:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if next.IsZero() {
|
||||||
|
next = j.next(s.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := id
|
||||||
|
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.exec.jobsIDsIn <- jobID:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
j.nextRun = next
|
||||||
|
s.jobs[id] = j
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.startedCh <- struct{}{}:
|
||||||
|
s.logger.Info("scheduler started")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ------------- Scheduler Methods ---------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
func (s *scheduler) now() time.Time {
|
||||||
|
return s.clock.Now().In(s.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) jobFromInternalJob(in internalJob) job {
|
||||||
|
return job{
|
||||||
|
id: in.id,
|
||||||
|
name: in.name,
|
||||||
|
tags: slices.Clone(in.tags),
|
||||||
|
jobOutRequest: s.jobOutRequestCh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) Jobs() []Job {
|
||||||
|
outChan := make(chan []Job)
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.allJobsOutRequest <- allJobsOutRequest{outChan: outChan}:
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs []Job
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case jobs = <-outChan:
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) NewJob(jobDefinition JobDefinition, task Task, options ...JobOption) (Job, error) {
|
||||||
|
return s.addOrUpdateJob(uuid.Nil, jobDefinition, task, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinition, taskWrapper Task, options []JobOption) (Job, error) {
|
||||||
|
j := internalJob{}
|
||||||
|
if id == uuid.Nil {
|
||||||
|
j.id = uuid.New()
|
||||||
|
} else {
|
||||||
|
currentJob := requestJobCtx(s.shutdownCtx, id, s.jobOutRequestCh)
|
||||||
|
if currentJob != nil && currentJob.id != uuid.Nil {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return nil, nil
|
||||||
|
case s.removeJobCh <- id:
|
||||||
|
<-currentJob.ctx.Done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
j.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ctx, j.cancel = context.WithCancel(s.shutdownCtx)
|
||||||
|
|
||||||
|
if taskWrapper == nil {
|
||||||
|
return nil, ErrNewJobTaskNil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := taskWrapper()
|
||||||
|
taskFunc := reflect.ValueOf(tsk.function)
|
||||||
|
for taskFunc.Kind() == reflect.Ptr {
|
||||||
|
taskFunc = taskFunc.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if taskFunc.Kind() != reflect.Func {
|
||||||
|
return nil, ErrNewJobTask
|
||||||
|
}
|
||||||
|
|
||||||
|
j.function = tsk.function
|
||||||
|
j.parameters = tsk.parameters
|
||||||
|
|
||||||
|
// apply global job options
|
||||||
|
for _, option := range s.globalJobOptions {
|
||||||
|
if err := option(&j); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply job specific options, which take precedence
|
||||||
|
for _, option := range options {
|
||||||
|
if err := option(&j); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := definition.setup(&j, s.location); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.newJobCh <- j:
|
||||||
|
}
|
||||||
|
|
||||||
|
return &job{
|
||||||
|
id: j.id,
|
||||||
|
name: j.name,
|
||||||
|
tags: slices.Clone(j.tags),
|
||||||
|
jobOutRequest: s.jobOutRequestCh,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) RemoveByTags(tags ...string) {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.removeJobsByTagsCh <- tags:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) RemoveJob(id uuid.UUID) error {
|
||||||
|
j := requestJobCtx(s.shutdownCtx, id, s.jobOutRequestCh)
|
||||||
|
if j == nil || j.id == uuid.Nil {
|
||||||
|
return ErrJobNotFound
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.removeJobCh <- id:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins scheduling jobs for execution based
|
||||||
|
// on each job's definition. Job's added to an already
|
||||||
|
// running scheduler will be scheduled immediately based
|
||||||
|
// on definition.
|
||||||
|
func (s *scheduler) Start() {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
case s.startCh <- struct{}{}:
|
||||||
|
<-s.startedCh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopJobs stops the execution of all jobs in the scheduler.
|
||||||
|
// This can be useful in situations where jobs need to be
|
||||||
|
// paused globally and then restarted with Start().
|
||||||
|
func (s *scheduler) StopJobs() error {
|
||||||
|
select {
|
||||||
|
case <-s.shutdownCtx.Done():
|
||||||
|
return nil
|
||||||
|
case s.stopCh <- struct{}{}:
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err := <-s.stopErrCh:
|
||||||
|
return err
|
||||||
|
case <-time.After(s.exec.stopTimeout + 2*time.Second):
|
||||||
|
return ErrStopSchedulerTimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown should be called when you no longer need
|
||||||
|
// the Scheduler or Job's as the Scheduler cannot
|
||||||
|
// be restarted after calling Shutdown.
|
||||||
|
func (s *scheduler) Shutdown() error {
|
||||||
|
s.shutdownCancel()
|
||||||
|
select {
|
||||||
|
case err := <-s.stopErrCh:
|
||||||
|
return err
|
||||||
|
case <-time.After(s.exec.stopTimeout + 2*time.Second):
|
||||||
|
return ErrStopSchedulerTimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update replaces the existing Job's JobDefinition with the provided
|
||||||
|
// JobDefinition. The Job's Job.ID() remains the same.
|
||||||
|
func (s *scheduler) Update(id uuid.UUID, jobDefinition JobDefinition, task Task, options ...JobOption) (Job, error) {
|
||||||
|
return s.addOrUpdateJob(id, jobDefinition, task, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ------------- Scheduler Options ---------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
// -----------------------------------------------
|
||||||
|
|
||||||
|
// SchedulerOption defines the function for setting
|
||||||
|
// options on the Scheduler.
|
||||||
|
type SchedulerOption func(*scheduler) error
|
||||||
|
|
||||||
|
// WithDistributedElector sets the elector to be used by multiple
|
||||||
|
// Scheduler instances to determine who should be the leader.
|
||||||
|
// Only the leader runs jobs, while non-leaders wait and continue
|
||||||
|
// to check if a new leader has been elected.
|
||||||
|
func WithDistributedElector(elector Elector) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if elector == nil {
|
||||||
|
return ErrWithDistributedElector
|
||||||
|
}
|
||||||
|
s.exec.elector = elector
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClock sets the clock used by the Scheduler
|
||||||
|
// to the clock provided. See https://github.com/jonboulle/clockwork
|
||||||
|
func WithClock(clock clockwork.Clock) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if clock == nil {
|
||||||
|
return ErrWithClockNil
|
||||||
|
}
|
||||||
|
s.clock = clock
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGlobalJobOptions sets JobOption's that will be applied to
|
||||||
|
// all jobs added to the scheduler. JobOption's set on the job
|
||||||
|
// itself will override if the same JobOption is set globally.
|
||||||
|
func WithGlobalJobOptions(jobOptions ...JobOption) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
s.globalJobOptions = jobOptions
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LimitMode defines the modes used for handling jobs that reach
|
||||||
|
// the limit provided in WithLimitConcurrentJobs
|
||||||
|
type LimitMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LimitModeReschedule causes jobs reaching the limit set in
|
||||||
|
// WithLimitConcurrentJobs or WithSingletonMode to be skipped
|
||||||
|
// and rescheduled for the next run time rather than being
|
||||||
|
// queued up to wait.
|
||||||
|
LimitModeReschedule = 1
|
||||||
|
|
||||||
|
// LimitModeWait causes jobs reaching the limit set in
|
||||||
|
// WithLimitConcurrentJobs or WithSingletonMode to wait
|
||||||
|
// in a queue until a slot becomes available to run.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Warning: do not use this mode if your jobs will continue to stack
|
||||||
|
// up beyond the ability of the limit workers to keep up. An example of
|
||||||
|
// what NOT to do:
|
||||||
|
//
|
||||||
|
// s, _ := gocron.NewScheduler(gocron.WithLimitConcurrentJobs)
|
||||||
|
// s.NewJob(
|
||||||
|
// gocron.DurationJob(
|
||||||
|
// time.Second,
|
||||||
|
// Task{
|
||||||
|
// Function: func() {
|
||||||
|
// time.Sleep(10 * time.Second)
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
LimitModeWait = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithLimitConcurrentJobs sets the limit and mode to be used by the
|
||||||
|
// Scheduler for limiting the number of jobs that may be running at
|
||||||
|
// a given time.
|
||||||
|
func WithLimitConcurrentJobs(limit uint, mode LimitMode) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
s.exec.limitMode = &limitModeConfig{
|
||||||
|
mode: mode,
|
||||||
|
limit: limit,
|
||||||
|
in: make(chan uuid.UUID, 1000),
|
||||||
|
}
|
||||||
|
if mode == LimitModeReschedule {
|
||||||
|
s.exec.limitMode.rescheduleLimiter = make(chan struct{}, limit)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLocation sets the location (i.e. timezone) that the scheduler
|
||||||
|
// should operate within. In many systems time.Local is UTC.
|
||||||
|
// Default: time.Local
|
||||||
|
func WithLocation(location *time.Location) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if location == nil {
|
||||||
|
return ErrWithLocationNil
|
||||||
|
}
|
||||||
|
s.location = location
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(logger Logger) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
if logger == nil {
|
||||||
|
return ErrWithLoggerNil
|
||||||
|
}
|
||||||
|
s.logger = logger
|
||||||
|
s.exec.logger = logger
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStopTimeout sets the amount of time the Scheduler should
|
||||||
|
// wait gracefully for jobs to complete before returning when
|
||||||
|
// StopJobs() or Shutdown() are called.
|
||||||
|
// Default: 10 * time.Second
|
||||||
|
func WithStopTimeout(timeout time.Duration) SchedulerOption {
|
||||||
|
return func(s *scheduler) error {
|
||||||
|
s.exec.stopTimeout = timeout
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,878 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/goleak"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestScheduler(options ...SchedulerOption) (Scheduler, error) {
|
||||||
|
// default test options
|
||||||
|
out := []SchedulerOption{
|
||||||
|
WithLogger(NewJSONSlogLogger(slog.LevelDebug)),
|
||||||
|
WithStopTimeout(time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
// append any additional options 2nd to override defaults if needed
|
||||||
|
out = append(out, options...)
|
||||||
|
return NewScheduler(out...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_OneSecond_NoOptions(t *testing.T) {
|
||||||
|
defer goleak.VerifyNone(t)
|
||||||
|
cronNoOptionsCh := make(chan struct{}, 10)
|
||||||
|
durationNoOptionsCh := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ch chan struct{}
|
||||||
|
jd JobDefinition
|
||||||
|
tsk Task
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"cron",
|
||||||
|
cronNoOptionsCh,
|
||||||
|
CronJob(
|
||||||
|
"* * * * * *",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {
|
||||||
|
cronNoOptionsCh <- struct{}{}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration",
|
||||||
|
durationNoOptionsCh,
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {
|
||||||
|
durationNoOptionsCh <- struct{}{}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.NewJob(tt.jd, tt.tsk)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
var runCount int
|
||||||
|
for runCount < 1 {
|
||||||
|
<-tt.ch
|
||||||
|
runCount++
|
||||||
|
}
|
||||||
|
err = s.Shutdown()
|
||||||
|
require.NoError(t, err)
|
||||||
|
stopTime := time.Now()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-tt.ch:
|
||||||
|
t.Fatal("job ran after scheduler was stopped")
|
||||||
|
case <-time.After(time.Millisecond * 50):
|
||||||
|
}
|
||||||
|
|
||||||
|
runDuration := stopTime.Sub(startTime)
|
||||||
|
assert.GreaterOrEqual(t, runDuration, time.Millisecond)
|
||||||
|
assert.LessOrEqual(t, runDuration, 1500*time.Millisecond)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_LongRunningJobs(t *testing.T) {
|
||||||
|
defer goleak.VerifyNone(t)
|
||||||
|
|
||||||
|
durationCh := make(chan struct{}, 10)
|
||||||
|
durationSingletonCh := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ch chan struct{}
|
||||||
|
jd JobDefinition
|
||||||
|
tsk Task
|
||||||
|
opts []JobOption
|
||||||
|
options []SchedulerOption
|
||||||
|
expectedRuns int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"duration",
|
||||||
|
durationCh,
|
||||||
|
DurationJob(
|
||||||
|
time.Millisecond * 500,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
durationCh <- struct{}{}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
[]SchedulerOption{WithStopTimeout(time.Second * 2)},
|
||||||
|
3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration singleton",
|
||||||
|
durationSingletonCh,
|
||||||
|
DurationJob(
|
||||||
|
time.Millisecond * 500,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
durationSingletonCh <- struct{}{}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[]JobOption{WithSingletonMode(LimitModeWait)},
|
||||||
|
[]SchedulerOption{WithStopTimeout(time.Second * 5)},
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler(tt.options...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.NewJob(tt.jd, tt.tsk, tt.opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
time.Sleep(1600 * time.Millisecond)
|
||||||
|
err = s.Shutdown()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var runCount int
|
||||||
|
timeout := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
close(timeout)
|
||||||
|
}()
|
||||||
|
Outer:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tt.ch:
|
||||||
|
runCount++
|
||||||
|
case <-timeout:
|
||||||
|
break Outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedRuns, runCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_Update(t *testing.T) {
|
||||||
|
defer goleak.VerifyNone(t)
|
||||||
|
|
||||||
|
durationJobCh := make(chan struct{})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialJob JobDefinition
|
||||||
|
updateJob JobDefinition
|
||||||
|
tsk Task
|
||||||
|
ch chan struct{}
|
||||||
|
runCount int
|
||||||
|
updateAfterCount int
|
||||||
|
expectedMinTime time.Duration
|
||||||
|
expectedMaxRunTime time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"duration, updated to another duration",
|
||||||
|
DurationJob(
|
||||||
|
time.Millisecond * 500,
|
||||||
|
),
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {
|
||||||
|
durationJobCh <- struct{}{}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
durationJobCh,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
time.Second * 1,
|
||||||
|
time.Second * 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
j, err := s.NewJob(tt.initialJob, tt.tsk)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
var runCount int
|
||||||
|
for runCount < tt.runCount {
|
||||||
|
select {
|
||||||
|
case <-tt.ch:
|
||||||
|
runCount++
|
||||||
|
if runCount == tt.updateAfterCount {
|
||||||
|
_, err = s.Update(j.ID(), tt.updateJob, tt.tsk)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = s.Shutdown()
|
||||||
|
require.NoError(t, err)
|
||||||
|
stopTime := time.Now()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-tt.ch:
|
||||||
|
t.Fatal("job ran after scheduler was stopped")
|
||||||
|
case <-time.After(time.Millisecond * 50):
|
||||||
|
}
|
||||||
|
|
||||||
|
runDuration := stopTime.Sub(startTime)
|
||||||
|
assert.GreaterOrEqual(t, runDuration, tt.expectedMinTime)
|
||||||
|
assert.LessOrEqual(t, runDuration, tt.expectedMaxRunTime)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_StopTimeout(t *testing.T) {
|
||||||
|
defer goleak.VerifyNone(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
jd JobDefinition
|
||||||
|
f any
|
||||||
|
opts []JobOption
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"duration",
|
||||||
|
DurationJob(
|
||||||
|
time.Millisecond * 100,
|
||||||
|
),
|
||||||
|
func(testDoneCtx context.Context) {
|
||||||
|
select {
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
case <-testDoneCtx.Done():
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration singleton",
|
||||||
|
DurationJob(
|
||||||
|
time.Millisecond * 100,
|
||||||
|
),
|
||||||
|
func(testDoneCtx context.Context) {
|
||||||
|
select {
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
case <-testDoneCtx.Done():
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]JobOption{WithSingletonMode(LimitModeWait)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testDoneCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
s, err := newTestScheduler(
|
||||||
|
WithStopTimeout(time.Millisecond * 100),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.NewJob(tt.jd, NewTask(tt.f, testDoneCtx), tt.opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
err = s.Shutdown()
|
||||||
|
assert.ErrorIs(t, err, ErrStopJobsTimedOut)
|
||||||
|
cancel()
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_Shutdown(t *testing.T) {
|
||||||
|
goleak.VerifyNone(t)
|
||||||
|
|
||||||
|
t.Run("start, stop, start, shutdown", func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler(
|
||||||
|
WithStopTimeout(time.Second),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
50*time.Millisecond,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithStartAt(
|
||||||
|
WithStartImmediately(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
require.NoError(t, s.StopJobs())
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
require.NoError(t, s.Shutdown())
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("calling Job methods after shutdown errors", func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler(
|
||||||
|
WithStopTimeout(time.Second),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
j, err := s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
100*time.Millisecond,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
WithStartAt(
|
||||||
|
WithStartImmediately(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
require.NoError(t, s.Shutdown())
|
||||||
|
|
||||||
|
_, err = j.LastRun()
|
||||||
|
assert.ErrorIs(t, err, ErrJobNotFound)
|
||||||
|
|
||||||
|
_, err = j.NextRun()
|
||||||
|
assert.ErrorIs(t, err, ErrJobNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_NewJob(t *testing.T) {
|
||||||
|
goleak.VerifyNone(t)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
jd JobDefinition
|
||||||
|
tsk Task
|
||||||
|
opts []JobOption
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"cron with timezone",
|
||||||
|
CronJob(
|
||||||
|
"CRON_TZ=America/Chicago * * * * * *",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cron with timezone, no seconds",
|
||||||
|
CronJob(
|
||||||
|
"CRON_TZ=America/Chicago * * * * *",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"random duration",
|
||||||
|
DurationRandomJob(
|
||||||
|
time.Second,
|
||||||
|
time.Second*5,
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"daily",
|
||||||
|
DailyJob(
|
||||||
|
1,
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weekly",
|
||||||
|
WeeklyJob(
|
||||||
|
1,
|
||||||
|
NewWeekdays(time.Monday),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(1, -1),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NewTask(
|
||||||
|
func() {},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.NewJob(tt.jd, tt.tsk, tt.opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.Start()
|
||||||
|
require.NoError(t, s.Shutdown())
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_NewJobErrors(t *testing.T) {
|
||||||
|
goleak.VerifyNone(t)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
jd JobDefinition
|
||||||
|
opts []JobOption
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"cron with timezone",
|
||||||
|
CronJob(
|
||||||
|
"bad cron",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrCronJobParse,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"random with bad min/max",
|
||||||
|
DurationRandomJob(
|
||||||
|
time.Second*5,
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrDurationRandomJobMinMax,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"daily job at times nil",
|
||||||
|
DailyJob(
|
||||||
|
1,
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrDailyJobAtTimesNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"daily job at time nil",
|
||||||
|
DailyJob(
|
||||||
|
1,
|
||||||
|
NewAtTimes(nil),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrDailyJobAtTimeNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"daily job hours out of range",
|
||||||
|
DailyJob(
|
||||||
|
1,
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(100, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrDailyJobHours,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"daily job minutes out of range",
|
||||||
|
DailyJob(
|
||||||
|
1,
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 100, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrDailyJobMinutesSeconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"daily job seconds out of range",
|
||||||
|
DailyJob(
|
||||||
|
1,
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrDailyJobMinutesSeconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weekly job at times nil",
|
||||||
|
WeeklyJob(
|
||||||
|
1,
|
||||||
|
NewWeekdays(time.Monday),
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrWeeklyJobAtTimesNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weekly job at time nil",
|
||||||
|
WeeklyJob(
|
||||||
|
1,
|
||||||
|
NewWeekdays(time.Monday),
|
||||||
|
NewAtTimes(nil),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrWeeklyJobAtTimeNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weekly job weekdays nil",
|
||||||
|
WeeklyJob(
|
||||||
|
1,
|
||||||
|
nil,
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrWeeklyJobDaysOfTheWeekNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weekly job hours out of range",
|
||||||
|
WeeklyJob(
|
||||||
|
1,
|
||||||
|
NewWeekdays(time.Monday),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(100, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrWeeklyJobHours,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weekly job minutes out of range",
|
||||||
|
WeeklyJob(
|
||||||
|
1,
|
||||||
|
NewWeekdays(time.Monday),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 100, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrWeeklyJobMinutesSeconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"weekly job seconds out of range",
|
||||||
|
WeeklyJob(
|
||||||
|
1,
|
||||||
|
NewWeekdays(time.Monday),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrWeeklyJobMinutesSeconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly job at times nil",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(1),
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrMonthlyJobAtTimesNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly job at time nil",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(1),
|
||||||
|
NewAtTimes(nil),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrMonthlyJobAtTimeNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly job days out of range",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(0),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrMonthlyJobDays,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly job days out of range",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
nil,
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrMonthlyJobDaysNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly job hours out of range",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(1),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(100, 0, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrMonthlyJobHours,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly job minutes out of range",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(1),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 100, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrMonthlyJobMinutesSeconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"monthly job seconds out of range",
|
||||||
|
MonthlyJob(
|
||||||
|
1,
|
||||||
|
NewDaysOfTheMonth(1),
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(1, 0, 100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
ErrMonthlyJobMinutesSeconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"WithName no name",
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
[]JobOption{WithName("")},
|
||||||
|
ErrWithNameEmpty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"WithStartDateTime is zero",
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
[]JobOption{WithStartAt(WithStartDateTime(time.Time{}))},
|
||||||
|
ErrWithStartDateTimePast,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"WithStartDateTime is in the past",
|
||||||
|
DurationJob(
|
||||||
|
time.Second,
|
||||||
|
),
|
||||||
|
[]JobOption{WithStartAt(WithStartDateTime(time.Now().Add(-time.Second)))},
|
||||||
|
ErrWithStartDateTimePast,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler(
|
||||||
|
WithStopTimeout(time.Millisecond * 50),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.NewJob(tt.jd, NewTask(func() {}), tt.opts...)
|
||||||
|
assert.ErrorIs(t, err, tt.err)
|
||||||
|
err = s.Shutdown()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_Singleton(t *testing.T) {
|
||||||
|
goleak.VerifyNone(t)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
duration time.Duration
|
||||||
|
limitMode LimitMode
|
||||||
|
runCount int
|
||||||
|
expectedMin time.Duration
|
||||||
|
expectedMax time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"singleton mode reschedule",
|
||||||
|
time.Millisecond * 100,
|
||||||
|
LimitModeReschedule,
|
||||||
|
3,
|
||||||
|
time.Millisecond * 600,
|
||||||
|
time.Millisecond * 1100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
jobRanCh := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
s, err := newTestScheduler(
|
||||||
|
WithStopTimeout(1 * time.Second),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = s.NewJob(
|
||||||
|
DurationJob(
|
||||||
|
tt.duration,
|
||||||
|
),
|
||||||
|
NewTask(func() {
|
||||||
|
time.Sleep(tt.duration * 2)
|
||||||
|
jobRanCh <- struct{}{}
|
||||||
|
}),
|
||||||
|
WithSingletonMode(tt.limitMode),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
var runCount int
|
||||||
|
for runCount < tt.runCount {
|
||||||
|
select {
|
||||||
|
case <-jobRanCh:
|
||||||
|
runCount++
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatalf("timed out waiting for jobs to run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop := time.Now()
|
||||||
|
require.NoError(t, s.Shutdown())
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, stop.Sub(start), tt.expectedMin)
|
||||||
|
assert.LessOrEqual(t, stop.Sub(start), tt.expectedMax)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduler_LimitMode(t *testing.T) {
|
||||||
|
goleak.VerifyNone(t)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
numJobs int
|
||||||
|
limit uint
|
||||||
|
limitMode LimitMode
|
||||||
|
duration time.Duration
|
||||||
|
expectedMin time.Duration
|
||||||
|
expectedMax time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"limit mode reschedule",
|
||||||
|
10,
|
||||||
|
2,
|
||||||
|
LimitModeReschedule,
|
||||||
|
time.Millisecond * 100,
|
||||||
|
time.Millisecond * 400,
|
||||||
|
time.Millisecond * 700,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"limit mode wait",
|
||||||
|
10,
|
||||||
|
2,
|
||||||
|
LimitModeWait,
|
||||||
|
time.Millisecond * 100,
|
||||||
|
time.Millisecond * 200,
|
||||||
|
time.Millisecond * 500,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s, err := newTestScheduler(
|
||||||
|
WithLimitConcurrentJobs(tt.limit, tt.limitMode),
|
||||||
|
WithStopTimeout(2*time.Second),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
jobRanCh := make(chan struct{}, 20)
|
||||||
|
|
||||||
|
for i := 0; i < tt.numJobs; i++ {
|
||||||
|
_, err = s.NewJob(
|
||||||
|
DurationJob(tt.duration),
|
||||||
|
NewTask(func() {
|
||||||
|
time.Sleep(tt.duration / 2)
|
||||||
|
jobRanCh <- struct{}{}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
s.Start()
|
||||||
|
|
||||||
|
var runCount int
|
||||||
|
for runCount < tt.numJobs {
|
||||||
|
select {
|
||||||
|
case <-jobRanCh:
|
||||||
|
runCount++
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatalf("timed out waiting for jobs to run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop := time.Now()
|
||||||
|
err = s.Shutdown()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, stop.Sub(start), tt.expectedMin)
|
||||||
|
assert.LessOrEqual(t, stop.Sub(start), tt.expectedMax)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func callJobFuncWithParams(jobFunc any, params ...any) error {
|
||||||
|
if jobFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f := reflect.ValueOf(jobFunc)
|
||||||
|
if f.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(params) != f.Type().NumIn() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
in := make([]reflect.Value, len(params))
|
||||||
|
for k, param := range params {
|
||||||
|
in[k] = reflect.ValueOf(param)
|
||||||
|
}
|
||||||
|
vals := f.Call(in)
|
||||||
|
for _, val := range vals {
|
||||||
|
i := val.Interface()
|
||||||
|
if err, ok := i.(error); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestJob(id uuid.UUID, ch chan jobOutRequest) *internalJob {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return requestJobCtx(ctx, id, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestJobCtx(ctx context.Context, id uuid.UUID, ch chan jobOutRequest) *internalJob {
|
||||||
|
resp := make(chan internalJob, 1)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ch <- jobOutRequest{
|
||||||
|
id: id,
|
||||||
|
outChan: resp,
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var j internalJob
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case jobReceived := <-resp:
|
||||||
|
j = jobReceived
|
||||||
|
}
|
||||||
|
return &j
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSliceDuplicatesInt(in []int) []int {
|
||||||
|
m := make(map[int]struct{})
|
||||||
|
|
||||||
|
for _, i := range in {
|
||||||
|
m[i] = struct{}{}
|
||||||
|
}
|
||||||
|
return maps.Keys(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAtTimesToDateTime(atTimes AtTimes, location *time.Location) ([]time.Time, error) {
|
||||||
|
if atTimes == nil {
|
||||||
|
return nil, errAtTimesNil
|
||||||
|
}
|
||||||
|
var atTimesDate []time.Time
|
||||||
|
for _, a := range atTimes() {
|
||||||
|
if a == nil {
|
||||||
|
return nil, errAtTimeNil
|
||||||
|
}
|
||||||
|
at := a()
|
||||||
|
if at.hours > 23 {
|
||||||
|
return nil, errAtTimeHours
|
||||||
|
} else if at.minutes > 59 || at.seconds > 59 {
|
||||||
|
return nil, errAtTimeMinSec
|
||||||
|
}
|
||||||
|
atTimesDate = append(atTimesDate, at.time(location))
|
||||||
|
}
|
||||||
|
slices.SortStableFunc(atTimesDate, func(a, b time.Time) int {
|
||||||
|
return a.Compare(b)
|
||||||
|
})
|
||||||
|
return atTimesDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type waitGroupWithMutex struct {
|
||||||
|
wg sync.WaitGroup
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *waitGroupWithMutex) Add(delta int) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.wg.Add(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *waitGroupWithMutex) Done() {
|
||||||
|
w.wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *waitGroupWithMutex) Wait() {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.wg.Wait()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
package gocron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoveSliceDuplicatesInt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []int
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"lots of duplicates",
|
||||||
|
[]int{
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||||
|
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||||
|
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||||
|
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||||
|
},
|
||||||
|
[]int{1, 2, 3, 4, 5},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := removeSliceDuplicatesInt(tt.input)
|
||||||
|
assert.ElementsMatch(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallJobFuncWithParams(t *testing.T) {
|
||||||
|
type f1 func()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
jobFunc any
|
||||||
|
params []any
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"nil jobFunc",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zero jobFunc",
|
||||||
|
f1(nil),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wrong number of params",
|
||||||
|
func(one string, two int) {},
|
||||||
|
[]any{"one"},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"function that returns an error",
|
||||||
|
func() error {
|
||||||
|
return fmt.Errorf("test error")
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
fmt.Errorf("test error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"function that returns no error",
|
||||||
|
func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := callJobFuncWithParams(tt.jobFunc, tt.params...)
|
||||||
|
assert.Equal(t, tt.expectedErr, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertAtTimesToDateTime(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
atTimes AtTimes
|
||||||
|
location *time.Location
|
||||||
|
expected []time.Time
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"atTimes is nil",
|
||||||
|
nil,
|
||||||
|
time.UTC,
|
||||||
|
nil,
|
||||||
|
errAtTimesNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"atTime is nil",
|
||||||
|
NewAtTimes(nil),
|
||||||
|
time.UTC,
|
||||||
|
nil,
|
||||||
|
errAtTimeNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"atTimes hours is invalid",
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(24, 0, 0),
|
||||||
|
),
|
||||||
|
time.UTC,
|
||||||
|
nil,
|
||||||
|
errAtTimeHours,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"atTimes minutes are invalid",
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(0, 60, 0),
|
||||||
|
),
|
||||||
|
time.UTC,
|
||||||
|
nil,
|
||||||
|
errAtTimeMinSec,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"atTimes seconds are invalid",
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(0, 0, 60),
|
||||||
|
),
|
||||||
|
time.UTC,
|
||||||
|
nil,
|
||||||
|
errAtTimeMinSec,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"atTimes valid",
|
||||||
|
NewAtTimes(
|
||||||
|
NewAtTime(0, 0, 3),
|
||||||
|
NewAtTime(0, 0, 0),
|
||||||
|
NewAtTime(0, 0, 1),
|
||||||
|
NewAtTime(0, 0, 2),
|
||||||
|
),
|
||||||
|
time.UTC,
|
||||||
|
[]time.Time{
|
||||||
|
time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Date(0, 0, 0, 0, 0, 1, 0, time.UTC),
|
||||||
|
time.Date(0, 0, 0, 0, 0, 2, 0, time.UTC),
|
||||||
|
time.Date(0, 0, 0, 0, 0, 3, 0, time.UTC),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := convertAtTimesToDateTime(tt.atTimes, tt.location)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
assert.Equal(t, tt.err, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue