Update deps

This commit is contained in:
eleksir
2024-09-13 23:28:48 +03:00
parent 03926f47c4
commit 4c19fa2446
133 changed files with 10571 additions and 4471 deletions

View File

@@ -2,14 +2,20 @@ run:
timeout: 5m
issues-exit-code: 1
tests: true
skip-dirs:
- local
issues:
max-same-issues: 100
include:
- EXC0012
- EXC0014
exclude-dirs:
- local
exclude-rules:
- path: example_test.go
linters:
- revive
text: "seems to be unused"
fix: true
linters:
enable:
@@ -29,21 +35,10 @@ linters:
- 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
formats:
- format: colored-line-number
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

View File

@@ -1,4 +1,4 @@
.PHONY: fmt lint test mocks test_coverage
.PHONY: fmt lint test mocks test_coverage test_ci
GO_PKGS := $(shell go list -f {{.Dir}} ./...)
@@ -15,5 +15,8 @@ test:
test_coverage:
@go test -race -v $(GO_FLAGS) -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PKGS)
test_ci:
@TEST_ENV=ci go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)
mocks:
@go generate ./...

View File

@@ -98,7 +98,7 @@ 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.
- [**One time**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#OneTimeJob):
Jobs can be run once at a specific time. These are non-recurring jobs.
Jobs can be run at specific time(s) (either once or many times).
### Concurrency Limits
Jobs can be limited individually or across the entire scheduler.
@@ -120,6 +120,7 @@ other instances checking to see if a new leader needs to be elected.
(don't see what you need? request on slack to get a repo created to contribute it!)
- [**Locker**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedLocker):
A locker can be used to lock each run of a job to a single instance of gocron.
Locker can be at job or scheduler, if it is defined both at job and scheduler then locker of job will take precedence.
- Implementations: [go-co-op lockers](https://github.com/go-co-op?q=-lock&type=all&language=&sort=)
(don't see what you need? request on slack to get a repo created to contribute it!)

View File

@@ -2,14 +2,15 @@
## Supported Versions
The current plan is to maintain version 1 as long as possible incorporating any necessary security patches.
The current plan is to maintain version 2 as long as possible incorporating any necessary security patches. Version 1 is deprecated and will no longer be patched.
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
| 1.x.x | :heavy_multiplication_x: |
| 2.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.
We will do our best to address any vulnerabilities in an expeditious manner.

View File

@@ -33,15 +33,21 @@ var (
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")
ErrPanicRecovered = fmt.Errorf("gocron: panic recovered")
ErrWithClockNil = fmt.Errorf("gocron: WithClock: clock must not be nil")
ErrWithDistributedElectorNil = fmt.Errorf("gocron: WithDistributedElector: elector must not be nil")
ErrWithDistributedLockerNil = fmt.Errorf("gocron: WithDistributedLocker: locker must not be nil")
ErrWithDistributedJobLockerNil = fmt.Errorf("gocron: WithDistributedJobLocker: locker must not be nil")
ErrWithIdentifierNil = fmt.Errorf("gocron: WithIdentifier: identifier must not be nil")
ErrWithLimitConcurrentJobsZero = fmt.Errorf("gocron: WithLimitConcurrentJobs: limit must be greater than 0")
ErrWithLocationNil = fmt.Errorf("gocron: WithLocation: location must not be nil")
ErrWithLoggerNil = fmt.Errorf("gocron: WithLogger: logger must not be nil")
ErrWithMonitorNil = fmt.Errorf("gocron: WithMonitor: monitor 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")
ErrWithStopDateTimePast = fmt.Errorf("gocron: WithStopDateTime: end must not be in the past")
ErrStartTimeLaterThanEndTime = fmt.Errorf("gocron: WithStartDateTime: start must not be later than end")
ErrStopTimeEarlierThanStartTime = fmt.Errorf("gocron: WithStopDateTime: end must not be earlier than start")
ErrWithStopTimeoutZeroOrNegative = fmt.Errorf("gocron: WithStopTimeout: timeout must be greater than 0")
)

View File

@@ -2,29 +2,53 @@ package gocron
import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/jonboulle/clockwork"
"github.com/google/uuid"
)
type executor struct {
ctx context.Context
cancel context.CancelFunc
logger Logger
stopCh chan struct{}
jobsIn chan jobIn
// context used for shutting down
ctx context.Context
// cancel used by the executor to signal a stop of it's functions
cancel context.CancelFunc
// clock used for regular time or mocking time
clock clockwork.Clock
// the executor's logger
logger Logger
// receives jobs scheduled to execute
jobsIn chan jobIn
// sends out jobs for rescheduling
jobsOutForRescheduling chan uuid.UUID
jobsOutCompleted chan uuid.UUID
jobOutRequest chan jobOutRequest
stopTimeout time.Duration
done chan error
singletonRunners *sync.Map // map[uuid.UUID]singletonRunner
limitMode *limitModeConfig
elector Elector
locker Locker
monitor Monitor
// sends out jobs once completed
jobsOutCompleted chan uuid.UUID
// used to request jobs from the scheduler
jobOutRequest chan jobOutRequest
// used by the executor to receive a stop signal from the scheduler
stopCh chan struct{}
// the timeout value when stopping
stopTimeout time.Duration
// used to signal that the executor has completed shutdown
done chan error
// runners for any singleton type jobs
// map[uuid.UUID]singletonRunner
singletonRunners *sync.Map
// config for limit mode
limitMode *limitModeConfig
// the elector when running distributed instances
elector Elector
// the locker when running distributed instances
locker Locker
// monitor for reporting metrics
monitor Monitor
}
type jobIn struct {
@@ -131,8 +155,8 @@ func (e *executor) start() {
// 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 <- jIn
e.sendOutForRescheduling(&jIn)
e.limitMode.in <- jIn
}
} else {
// no limit mode, so we're either running a regular job or
@@ -303,6 +327,10 @@ func (e *executor) singletonModeRunner(name string, in chan jobIn, wg *waitGroup
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
cancel()
if j != nil {
// need to set shouldSendOut = false here, as there is a duplicative call to sendOutForRescheduling
// inside the runJob function that needs to be skipped. sendOutForRescheduling is previously called
// when the job is sent to the singleton mode runner.
jIn.shouldSendOut = false
e.runJob(*j, jIn)
}
@@ -330,15 +358,31 @@ func (e *executor) runJob(j internalJob, jIn jobIn) {
default:
}
if j.stopTimeReached(e.clock.Now()) {
return
}
if e.elector != nil {
if err := e.elector.IsLeader(j.ctx); err != nil {
e.sendOutForRescheduling(&jIn)
e.incrementJobCounter(j, Skip)
return
}
} else if j.locker != nil {
lock, err := j.locker.Lock(j.ctx, j.name)
if err != nil {
_ = callJobFuncWithParams(j.afterLockError, j.id, j.name, err)
e.sendOutForRescheduling(&jIn)
e.incrementJobCounter(j, Skip)
return
}
defer func() { _ = lock.Unlock(j.ctx) }()
} else if e.locker != nil {
lock, err := e.locker.Lock(j.ctx, j.name)
if err != nil {
_ = callJobFuncWithParams(j.afterLockError, j.id, j.name, err)
e.sendOutForRescheduling(&jIn)
e.incrementJobCounter(j, Skip)
return
}
defer func() { _ = lock.Unlock(j.ctx) }()
@@ -352,20 +396,35 @@ func (e *executor) runJob(j internalJob, jIn jobIn) {
}
startTime := time.Now()
err := callJobFuncWithParams(j.function, j.parameters...)
err := e.callJobWithRecover(j)
if e.monitor != nil {
e.monitor.RecordJobTiming(startTime, time.Now(), j.id, j.name, j.tags)
}
if err != nil {
_ = callJobFuncWithParams(j.afterJobRunsWithError, j.id, j.name, err)
if e.monitor != nil {
e.monitor.IncrementJob(j.id, j.name, j.tags, Fail)
}
e.incrementJobCounter(j, Fail)
} else {
_ = callJobFuncWithParams(j.afterJobRuns, j.id, j.name)
if e.monitor != nil {
e.monitor.IncrementJob(j.id, j.name, j.tags, Success)
e.incrementJobCounter(j, Success)
}
}
func (e *executor) callJobWithRecover(j internalJob) (err error) {
defer func() {
if recoverData := recover(); recoverData != nil {
_ = callJobFuncWithParams(j.afterJobRunsWithPanic, j.id, j.name, recoverData)
// if panic is occurred, we should return an error
err = fmt.Errorf("%w from %v", ErrPanicRecovered, recoverData)
}
}()
return callJobFuncWithParams(j.function, j.parameters...)
}
func (e *executor) incrementJobCounter(j internalJob, status JobStatus) {
if e.monitor != nil {
e.monitor.IncrementJob(j.id, j.name, j.tags, status)
}
}
@@ -448,4 +507,8 @@ func (e *executor) stop(standardJobsWg, singletonJobsWg, limitModeJobsWg *waitGr
e.logger.Debug("gocron: executor stopped")
}
waiterCancel()
if e.limitMode != nil {
e.limitMode.started = false
}
}

View File

@@ -24,8 +24,11 @@ type internalJob struct {
name string
tags []string
jobSchedule
lastScheduledRun time.Time
nextScheduled time.Time
// as some jobs may queue up, it's possible to
// have multiple nextScheduled times
nextScheduled []time.Time
lastRun time.Time
function any
parameters []any
@@ -35,10 +38,15 @@ type internalJob struct {
limitRunsTo *limitRunsTo
startTime time.Time
startImmediately bool
stopTime time.Time
// event listeners
afterJobRuns func(jobID uuid.UUID, jobName string)
beforeJobRuns func(jobID uuid.UUID, jobName string)
afterJobRunsWithError func(jobID uuid.UUID, jobName string, err error)
afterJobRunsWithPanic func(jobID uuid.UUID, jobName string, recoverData any)
afterLockError func(jobID uuid.UUID, jobName string, err error)
locker Locker
}
// stop is used to stop the job's timer and cancel the context
@@ -53,6 +61,13 @@ func (j *internalJob) stop() {
j.cancel()
}
func (j *internalJob) stopTimeReached(now time.Time) bool {
if j.stopTime.IsZero() {
return false
}
return j.stopTime.Before(now)
}
// task stores the function and parameters
// that are actually run when the job is executed.
type task struct {
@@ -91,7 +106,7 @@ type limitRunsTo struct {
// JobDefinition defines the interface that must be
// implemented to create a job from the definition.
type JobDefinition interface {
setup(*internalJob, *time.Location) error
setup(j *internalJob, l *time.Location, now time.Time) error
}
var _ JobDefinition = (*cronJobDefinition)(nil)
@@ -101,7 +116,7 @@ type cronJobDefinition struct {
withSeconds bool
}
func (c cronJobDefinition) setup(j *internalJob, location *time.Location) error {
func (c cronJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {
var withLocation string
if strings.HasPrefix(c.crontab, "TZ=") || strings.HasPrefix(c.crontab, "CRON_TZ=") {
withLocation = c.crontab
@@ -117,7 +132,7 @@ func (c cronJobDefinition) setup(j *internalJob, location *time.Location) error
)
if c.withSeconds {
p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
p := cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
cronSchedule, err = p.Parse(withLocation)
} else {
cronSchedule, err = cron.ParseStandard(withLocation)
@@ -149,7 +164,7 @@ type durationJobDefinition struct {
duration time.Duration
}
func (d durationJobDefinition) setup(j *internalJob, _ *time.Location) error {
func (d durationJobDefinition) setup(j *internalJob, _ *time.Location, _ time.Time) error {
if d.duration == 0 {
return ErrDurationJobIntervalZero
}
@@ -171,7 +186,7 @@ type durationRandomJobDefinition struct {
min, max time.Duration
}
func (d durationRandomJobDefinition) setup(j *internalJob, _ *time.Location) error {
func (d durationRandomJobDefinition) setup(j *internalJob, _ *time.Location, _ time.Time) error {
if d.min >= d.max {
return ErrDurationRandomJobMinMax
}
@@ -221,7 +236,7 @@ type dailyJobDefinition struct {
atTimes AtTimes
}
func (d dailyJobDefinition) setup(j *internalJob, location *time.Location) error {
func (d dailyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {
atTimesDate, err := convertAtTimesToDateTime(d.atTimes, location)
switch {
case errors.Is(err, errAtTimesNil):
@@ -250,7 +265,7 @@ type weeklyJobDefinition struct {
atTimes AtTimes
}
func (w weeklyJobDefinition) setup(j *internalJob, location *time.Location) error {
func (w weeklyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {
var ws weeklyJob
ws.interval = w.interval
@@ -286,7 +301,8 @@ type Weekdays func() []time.Weekday
// NewWeekdays provide the days of the week the job should run.
func NewWeekdays(weekday time.Weekday, weekdays ...time.Weekday) Weekdays {
return func() []time.Weekday {
return append(weekdays, weekday)
weekdays = append(weekdays, weekday)
return weekdays
}
}
@@ -314,7 +330,7 @@ type monthlyJobDefinition struct {
atTimes AtTimes
}
func (m monthlyJobDefinition) setup(j *internalJob, location *time.Location) error {
func (m monthlyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {
var ms monthlyJob
ms.interval = m.interval
@@ -437,31 +453,46 @@ type oneTimeJobDefinition struct {
startAt OneTimeJobStartAtOption
}
func (o oneTimeJobDefinition) setup(j *internalJob, _ *time.Location) error {
j.jobSchedule = oneTimeJob{}
return o.startAt(j)
func (o oneTimeJobDefinition) setup(j *internalJob, _ *time.Location, now time.Time) error {
sortedTimes := o.startAt(j)
slices.SortStableFunc(sortedTimes, ascendingTime)
// keep only schedules that are in the future
idx, found := slices.BinarySearchFunc(sortedTimes, now, ascendingTime)
if found {
idx++
}
sortedTimes = sortedTimes[idx:]
if !j.startImmediately && len(sortedTimes) == 0 {
return ErrOneTimeJobStartDateTimePast
}
j.jobSchedule = oneTimeJob{sortedTimes: sortedTimes}
return nil
}
// OneTimeJobStartAtOption defines when the one time job is run
type OneTimeJobStartAtOption func(*internalJob) error
type OneTimeJobStartAtOption func(*internalJob) []time.Time
// OneTimeJobStartImmediately tells the scheduler to run the one time job immediately.
func OneTimeJobStartImmediately() OneTimeJobStartAtOption {
return func(j *internalJob) error {
return func(j *internalJob) []time.Time {
j.startImmediately = true
return nil
return []time.Time{}
}
}
// OneTimeJobStartDateTime sets the date & time at which the job should run.
// This datetime must be in the future.
// This datetime must be in the future (according to the scheduler clock).
func OneTimeJobStartDateTime(start time.Time) OneTimeJobStartAtOption {
return func(j *internalJob) error {
if start.IsZero() || start.Before(time.Now()) {
return ErrOneTimeJobStartDateTimePast
}
j.startTime = start
return nil
return func(_ *internalJob) []time.Time {
return []time.Time{start}
}
}
// OneTimeJobStartDateTimes sets the date & times at which the job should run.
// At least one of the date/times must be in the future (according to the scheduler clock).
func OneTimeJobStartDateTimes(times ...time.Time) OneTimeJobStartAtOption {
return func(_ *internalJob) []time.Time {
return times
}
}
@@ -480,12 +511,25 @@ func OneTimeJob(startAt OneTimeJobStartAtOption) JobDefinition {
// -----------------------------------------------
// JobOption defines the constructor for job options.
type JobOption func(*internalJob) error
type JobOption func(*internalJob, time.Time) error
// WithDistributedJobLocker sets the locker to be used by multiple
// Scheduler instances to ensure that only one instance of each
// job is run.
func WithDistributedJobLocker(locker Locker) JobOption {
return func(j *internalJob, _ time.Time) error {
if locker == nil {
return ErrWithDistributedJobLockerNil
}
j.locker = locker
return nil
}
}
// WithEventListeners sets the event listeners that should be
// run for the job.
func WithEventListeners(eventListeners ...EventListener) JobOption {
return func(j *internalJob) error {
return func(j *internalJob, _ time.Time) error {
for _, eventListener := range eventListeners {
if err := eventListener(j); err != nil {
return err
@@ -498,7 +542,7 @@ func WithEventListeners(eventListeners ...EventListener) JobOption {
// 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 {
return func(j *internalJob, _ time.Time) error {
j.limitRunsTo = &limitRunsTo{
limit: limit,
runCount: 0,
@@ -510,8 +554,7 @@ func WithLimitedRuns(limit uint) JobOption {
// 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 {
return func(j *internalJob, _ time.Time) error {
if name == "" {
return ErrWithNameEmpty
}
@@ -524,7 +567,7 @@ func WithName(name string) JobOption {
// This is useful for jobs that should not overlap, and that occasionally
// (but not consistently) run longer than the interval between job runs.
func WithSingletonMode(mode LimitMode) JobOption {
return func(j *internalJob) error {
return func(j *internalJob, _ time.Time) error {
j.singletonMode = true
j.singletonLimitMode = mode
return nil
@@ -534,19 +577,19 @@ func WithSingletonMode(mode LimitMode) JobOption {
// WithStartAt sets the option for starting the job at
// a specific datetime.
func WithStartAt(option StartAtOption) JobOption {
return func(j *internalJob) error {
return option(j)
return func(j *internalJob, now time.Time) error {
return option(j, now)
}
}
// StartAtOption defines options for starting the job
type StartAtOption func(*internalJob) error
type StartAtOption func(*internalJob, time.Time) 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 {
return func(j *internalJob, _ time.Time) error {
j.startImmediately = true
return nil
}
@@ -555,25 +598,69 @@ func WithStartImmediately() StartAtOption {
// WithStartDateTime sets the first date & time at which the job should run.
// This datetime must be in the future.
func WithStartDateTime(start time.Time) StartAtOption {
return func(j *internalJob) error {
if start.IsZero() || start.Before(time.Now()) {
return func(j *internalJob, now time.Time) error {
if start.IsZero() || start.Before(now) {
return ErrWithStartDateTimePast
}
if !j.stopTime.IsZero() && j.stopTime.Before(start) {
return ErrStartTimeLaterThanEndTime
}
j.startTime = start
return nil
}
}
// WithStopAt sets the option for stopping the job from running
// after the specified time.
func WithStopAt(option StopAtOption) JobOption {
return func(j *internalJob, now time.Time) error {
return option(j, now)
}
}
// StopAtOption defines options for stopping the job
type StopAtOption func(*internalJob, time.Time) error
// WithStopDateTime sets the final date & time after which the job should stop.
// This must be in the future and should be after the startTime (if specified).
// The job's final run may be at the stop time, but not after.
func WithStopDateTime(end time.Time) StopAtOption {
return func(j *internalJob, now time.Time) error {
if end.IsZero() || end.Before(now) {
return ErrWithStopDateTimePast
}
if end.Before(j.startTime) {
return ErrStopTimeEarlierThanStartTime
}
j.stopTime = end
return nil
}
}
// WithTags sets the tags for the job. Tags provide
// a way to identify jobs by a set of tags and remove
// multiple jobs by tag.
func WithTags(tags ...string) JobOption {
return func(j *internalJob) error {
return func(j *internalJob, _ time.Time) error {
j.tags = tags
return nil
}
}
// WithIdentifier sets the identifier for the job. The identifier
// is used to uniquely identify the job and is used for logging
// and metrics.
func WithIdentifier(id uuid.UUID) JobOption {
return func(j *internalJob, _ time.Time) error {
if id == uuid.Nil {
return ErrWithIdentifierNil
}
j.id = id
return nil
}
}
// -----------------------------------------------
// -----------------------------------------------
// ------------- Job Event Listeners -------------
@@ -584,6 +671,18 @@ func WithTags(tags ...string) JobOption {
// listeners that can be used to listen for job events.
type EventListener func(*internalJob) error
// BeforeJobRuns is used to listen for when a job is about to run and
// then run the provided function.
func BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
return func(j *internalJob) error {
if eventListenerFunc == nil {
return ErrEventListenerFuncNil
}
j.beforeJobRuns = eventListenerFunc
return nil
}
}
// AfterJobRuns is used to listen for when a job has run
// without an error, and then run the provided function.
func AfterJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
@@ -608,14 +707,26 @@ func AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, jobName strin
}
}
// BeforeJobRuns is used to listen for when a job is about to run and
// then run the provided function.
func BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
// AfterJobRunsWithPanic is used to listen for when a job has run and
// returned panicked recover data, and then run the provided function.
func AfterJobRunsWithPanic(eventListenerFunc func(jobID uuid.UUID, jobName string, recoverData any)) EventListener {
return func(j *internalJob) error {
if eventListenerFunc == nil {
return ErrEventListenerFuncNil
}
j.beforeJobRuns = eventListenerFunc
j.afterJobRunsWithPanic = eventListenerFunc
return nil
}
}
// AfterLockError is used to when the distributed locker returns an error and
// then run the provided function.
func AfterLockError(eventListenerFunc func(jobID uuid.UUID, jobName string, err error)) EventListener {
return func(j *internalJob) error {
if eventListenerFunc == nil {
return ErrEventListenerFuncNil
}
j.afterLockError = eventListenerFunc
return nil
}
}
@@ -826,10 +937,33 @@ func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, firstPass
var _ jobSchedule = (*oneTimeJob)(nil)
type oneTimeJob struct{}
type oneTimeJob struct {
sortedTimes []time.Time
}
func (o oneTimeJob) next(_ time.Time) time.Time {
return time.Time{}
// next finds the next item in a sorted list of times using binary-search.
//
// example: sortedTimes: [2, 4, 6, 8]
//
// lastRun: 1 => [idx=0,found=false] => next is 2 - sorted[idx] idx=0
// lastRun: 2 => [idx=0,found=true] => next is 4 - sorted[idx+1] idx=1
// lastRun: 3 => [idx=1,found=false] => next is 4 - sorted[idx] idx=1
// lastRun: 4 => [idx=1,found=true] => next is 6 - sorted[idx+1] idx=2
// lastRun: 7 => [idx=3,found=false] => next is 8 - sorted[idx] idx=3
// lastRun: 8 => [idx=3,found=found] => next is none
// lastRun: 9 => [idx=3,found=found] => next is none
func (o oneTimeJob) next(lastRun time.Time) time.Time {
idx, found := slices.BinarySearchFunc(o.sortedTimes, lastRun, ascendingTime)
// if found, the next run is the following index
if found {
idx++
}
// exhausted runs
if idx >= len(o.sortedTimes) {
return time.Time{}
}
return o.sortedTimes[idx]
}
// -----------------------------------------------
@@ -849,6 +983,8 @@ type Job interface {
Name() string
// NextRun returns the time of the job's next scheduled run.
NextRun() (time.Time, error)
// NextRuns returns the requested number of calculated next run values.
NextRuns(int) ([]time.Time, error)
// RunNow runs the job once, now. This does not alter
// the existing run schedule, and will respect all job
// and scheduler limits. This means that running a job now may
@@ -894,7 +1030,39 @@ func (j job) NextRun() (time.Time, error) {
if ij == nil || ij.id == uuid.Nil {
return time.Time{}, ErrJobNotFound
}
return ij.nextScheduled, nil
if len(ij.nextScheduled) == 0 {
return time.Time{}, nil
}
// the first element is the next scheduled run with subsequent
// runs following after in the slice
return ij.nextScheduled[0], nil
}
func (j job) NextRuns(count int) ([]time.Time, error) {
ij := requestJob(j.id, j.jobOutRequest)
if ij == nil || ij.id == uuid.Nil {
return nil, ErrJobNotFound
}
lengthNextScheduled := len(ij.nextScheduled)
if lengthNextScheduled == 0 {
return nil, nil
} else if count <= lengthNextScheduled {
return ij.nextScheduled[:count], nil
}
out := make([]time.Time, count)
for i := 0; i < count; i++ {
if i < lengthNextScheduled {
out[i] = ij.nextScheduled[i]
continue
}
from := out[i-1]
out[i] = ij.next(from)
}
return out, nil
}
func (j job) Tags() []string {

View File

@@ -13,6 +13,7 @@ type JobStatus string
const (
Fail JobStatus = "fail"
Success JobStatus = "success"
Skip JobStatus = "skip"
)
// Monitor represents the interface to collect jobs metrics.

View File

@@ -44,6 +44,9 @@ type Scheduler interface {
// Update replaces the existing Job's JobDefinition with the provided
// JobDefinition. The Job's Job.ID() remains the same.
Update(uuid.UUID, JobDefinition, Task, ...JobOption) (Job, error)
// JobsWaitingInQueue number of jobs waiting in Queue in case of LimitModeWait
// In case of LimitModeReschedule or no limit it will be always zero
JobsWaitingInQueue() int
}
// -----------------------------------------------
@@ -53,25 +56,43 @@ type Scheduler interface {
// -----------------------------------------------
type scheduler struct {
shutdownCtx context.Context
shutdownCancel context.CancelFunc
exec executor
jobs map[uuid.UUID]internalJob
location *time.Location
clock clockwork.Clock
started bool
// context used for shutting down
shutdownCtx context.Context
// cancel used to signal scheduler should shut down
shutdownCancel context.CancelFunc
// the executor, which actually runs the jobs sent to it via the scheduler
exec executor
// the map of jobs registered in the scheduler
jobs map[uuid.UUID]internalJob
// the location used by the scheduler for scheduling when relevant
location *time.Location
// whether the scheduler has been started or not
started bool
// globally applied JobOption's set on all jobs added to the scheduler
// note: individually set JobOption's take precedence.
globalJobOptions []JobOption
logger Logger
// the scheduler's logger
logger Logger
startCh chan struct{}
startedCh chan struct{}
stopCh chan struct{}
stopErrCh chan error
allJobsOutRequest chan allJobsOutRequest
jobOutRequestCh chan jobOutRequest
runJobRequestCh chan runJobRequest
newJobCh chan newJobIn
removeJobCh chan uuid.UUID
// used to tell the scheduler to start
startCh chan struct{}
// used to report that the scheduler has started
startedCh chan struct{}
// used to tell the scheduler to stop
stopCh chan struct{}
// used to report that the scheduler has stopped
stopErrCh chan error
// used to send all the jobs out when a request is made by the client
allJobsOutRequest chan allJobsOutRequest
// used to send a jobs out when a request is made by the client
jobOutRequestCh chan jobOutRequest
// used to run a job on-demand when requested by the client
runJobRequestCh chan runJobRequest
// new jobs are received here
newJobCh chan newJobIn
// requests from the client to remove jobs by ID are received here
removeJobCh chan uuid.UUID
// requests from the client to remove jobs by tags are received here
removeJobsByTagsCh chan []string
}
@@ -108,6 +129,7 @@ func NewScheduler(options ...SchedulerOption) (Scheduler, error) {
stopTimeout: time.Second * 10,
singletonRunners: nil,
logger: &noOpLogger{},
clock: clockwork.NewRealClock(),
jobsIn: make(chan jobIn),
jobsOutForRescheduling: make(chan uuid.UUID),
@@ -122,7 +144,6 @@ func NewScheduler(options ...SchedulerOption) (Scheduler, error) {
exec: exec,
jobs: make(map[uuid.UUID]internalJob),
location: time.Local,
clock: clockwork.NewRealClock(),
logger: &noOpLogger{},
newJobCh: make(chan newJobIn),
@@ -292,20 +313,42 @@ func (s *scheduler) selectRemoveJob(id uuid.UUID) {
// Jobs coming back from the executor to the scheduler that
// need to evaluated for rescheduling.
func (s *scheduler) selectExecJobsOutForRescheduling(id uuid.UUID) {
select {
case <-s.shutdownCtx.Done():
return
default:
}
j, ok := s.jobs[id]
if !ok {
// the job was removed while it was running, and
// so we don't need to reschedule it.
return
}
j.lastScheduledRun = j.nextScheduled
next := j.next(j.lastScheduledRun)
if j.stopTimeReached(s.now()) {
return
}
scheduleFrom := j.lastRun
if len(j.nextScheduled) > 0 {
// always grab the last element in the slice as that is the furthest
// out in the future and the time from which we want to calculate
// the subsequent next run time.
slices.SortStableFunc(j.nextScheduled, ascendingTime)
scheduleFrom = j.nextScheduled[len(j.nextScheduled)-1]
}
if scheduleFrom.IsZero() {
scheduleFrom = j.startTime
}
next := j.next(scheduleFrom)
if next.IsZero() {
// the job's next function will return zero for OneTime jobs.
// since they are one time only, they do not need rescheduling.
return
}
if next.Before(s.now()) {
// in some cases the next run time can be in the past, for example:
// - the time on the machine was incorrect and has been synced with ntp
@@ -316,8 +359,8 @@ func (s *scheduler) selectExecJobsOutForRescheduling(id uuid.UUID) {
next = j.next(next)
}
}
j.nextScheduled = next
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
j.nextScheduled = append(j.nextScheduled, next)
j.timer = s.exec.clock.AfterFunc(next.Sub(s.now()), func() {
// set the actual timer on the job here and listen for
// shut down events so that the job doesn't attempt to
// run if the scheduler has been shutdown.
@@ -340,6 +383,17 @@ func (s *scheduler) selectExecJobsOutCompleted(id uuid.UUID) {
return
}
// if the job has nextScheduled time in the past,
// we need to remove any that are in the past.
var newNextScheduled []time.Time
for _, t := range j.nextScheduled {
if t.Before(s.now()) {
continue
}
newNextScheduled = append(newNextScheduled, t)
}
j.nextScheduled = newNextScheduled
// if the job has a limited number of runs set, we need to
// check how many runs have occurred and stop running this
// job if it has reached the limit.
@@ -390,7 +444,7 @@ func (s *scheduler) selectNewJob(in newJobIn) {
}
id := j.id
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
j.timer = s.exec.clock.AfterFunc(next.Sub(s.now()), func() {
select {
case <-s.shutdownCtx.Done():
case s.exec.jobsIn <- jobIn{
@@ -400,7 +454,8 @@ func (s *scheduler) selectNewJob(in newJobIn) {
}
})
}
j.nextScheduled = next
j.startTime = next
j.nextScheduled = append(j.nextScheduled, next)
}
s.jobs[j.id] = j
@@ -441,7 +496,7 @@ func (s *scheduler) selectStart() {
}
jobID := id
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
j.timer = s.exec.clock.AfterFunc(next.Sub(s.now()), func() {
select {
case <-s.shutdownCtx.Done():
case s.exec.jobsIn <- jobIn{
@@ -451,7 +506,8 @@ func (s *scheduler) selectStart() {
}
})
}
j.nextScheduled = next
j.startTime = next
j.nextScheduled = append(j.nextScheduled, next)
s.jobs[id] = j
}
select {
@@ -468,7 +524,7 @@ func (s *scheduler) selectStart() {
// -----------------------------------------------
func (s *scheduler) now() time.Time {
return s.clock.Now().In(s.location)
return s.exec.clock.Now().In(s.location)
}
func (s *scheduler) jobFromInternalJob(in internalJob) job {
@@ -501,6 +557,70 @@ func (s *scheduler) NewJob(jobDefinition JobDefinition, task Task, options ...Jo
return s.addOrUpdateJob(uuid.Nil, jobDefinition, task, options)
}
func (s *scheduler) verifyInterfaceVariadic(taskFunc reflect.Value, tsk task, variadicStart int) error {
ifaceType := taskFunc.Type().In(variadicStart).Elem()
for i := variadicStart; i < len(tsk.parameters); i++ {
if !reflect.TypeOf(tsk.parameters[i]).Implements(ifaceType) {
return ErrNewJobWrongTypeOfParameters
}
}
return nil
}
func (s *scheduler) verifyVariadic(taskFunc reflect.Value, tsk task, variadicStart int) error {
if err := s.verifyNonVariadic(taskFunc, tsk, variadicStart); err != nil {
return err
}
parameterType := taskFunc.Type().In(variadicStart).Elem().Kind()
if parameterType == reflect.Interface {
return s.verifyInterfaceVariadic(taskFunc, tsk, variadicStart)
}
if parameterType == reflect.Pointer {
parameterType = reflect.Indirect(reflect.ValueOf(taskFunc.Type().In(variadicStart))).Kind()
}
for i := variadicStart; i < len(tsk.parameters); i++ {
argumentType := reflect.TypeOf(tsk.parameters[i]).Kind()
if argumentType == reflect.Interface || argumentType == reflect.Pointer {
argumentType = reflect.TypeOf(tsk.parameters[i]).Elem().Kind()
}
if argumentType != parameterType {
return ErrNewJobWrongTypeOfParameters
}
}
return nil
}
func (s *scheduler) verifyNonVariadic(taskFunc reflect.Value, tsk task, length int) error {
for i := 0; i < length; i++ {
t1 := reflect.TypeOf(tsk.parameters[i]).Kind()
if t1 == reflect.Interface || t1 == reflect.Pointer {
t1 = reflect.TypeOf(tsk.parameters[i]).Elem().Kind()
}
t2 := reflect.New(taskFunc.Type().In(i)).Elem().Kind()
if t2 == reflect.Interface || t2 == reflect.Pointer {
t2 = reflect.Indirect(reflect.ValueOf(taskFunc.Type().In(i))).Kind()
}
if t1 != t2 {
return ErrNewJobWrongTypeOfParameters
}
}
return nil
}
func (s *scheduler) verifyParameterType(taskFunc reflect.Value, tsk task) error {
isVariadic := taskFunc.Type().IsVariadic()
if isVariadic {
variadicStart := taskFunc.Type().NumIn() - 1
return s.verifyVariadic(taskFunc, tsk, variadicStart)
}
expectedParameterLength := taskFunc.Type().NumIn()
if len(tsk.parameters) != expectedParameterLength {
return ErrNewJobWrongNumberOfParameters
}
return s.verifyNonVariadic(taskFunc, tsk, expectedParameterLength)
}
func (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinition, taskWrapper Task, options []JobOption) (Job, error) {
j := internalJob{}
if id == uuid.Nil {
@@ -535,23 +655,8 @@ func (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinition, taskW
return nil, ErrNewJobTaskNotFunc
}
expectedParameterLength := taskFunc.Type().NumIn()
if len(tsk.parameters) != expectedParameterLength {
return nil, ErrNewJobWrongNumberOfParameters
}
for i := 0; i < expectedParameterLength; i++ {
t1 := reflect.TypeOf(tsk.parameters[i]).Kind()
if t1 == reflect.Interface || t1 == reflect.Pointer {
t1 = reflect.TypeOf(tsk.parameters[i]).Elem().Kind()
}
t2 := reflect.New(taskFunc.Type().In(i)).Elem().Kind()
if t2 == reflect.Interface || t2 == reflect.Pointer {
t2 = reflect.Indirect(reflect.ValueOf(taskFunc.Type().In(i))).Kind()
}
if t1 != t2 {
return nil, ErrNewJobWrongTypeOfParameters
}
if err := s.verifyParameterType(taskFunc, tsk); err != nil {
return nil, err
}
j.name = runtime.FuncForPC(taskFunc.Pointer()).Name()
@@ -560,19 +665,19 @@ func (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinition, taskW
// apply global job options
for _, option := range s.globalJobOptions {
if err := option(&j); err != nil {
if err := option(&j, s.now()); err != nil {
return nil, err
}
}
// apply job specific options, which take precedence
for _, option := range options {
if err := option(&j); err != nil {
if err := option(&j, s.now()); err != nil {
return nil, err
}
}
if err := definition.setup(&j, s.location); err != nil {
if err := definition.setup(&j, s.location, s.exec.clock.Now()); err != nil {
return nil, err
}
@@ -591,13 +696,8 @@ func (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinition, taskW
case <-s.shutdownCtx.Done():
}
return &job{
id: j.id,
name: j.name,
tags: slices.Clone(j.tags),
jobOutRequest: s.jobOutRequestCh,
runJobRequest: s.runJobRequestCh,
}, nil
out := s.jobFromInternalJob(j)
return &out, nil
}
func (s *scheduler) RemoveByTags(tags ...string) {
@@ -656,6 +756,13 @@ func (s *scheduler) Update(id uuid.UUID, jobDefinition JobDefinition, task Task,
return s.addOrUpdateJob(id, jobDefinition, task, options)
}
func (s *scheduler) JobsWaitingInQueue() int {
if s.exec.limitMode != nil && s.exec.limitMode.mode == LimitModeWait {
return len(s.exec.limitMode.in)
}
return 0
}
// -----------------------------------------------
// -----------------------------------------------
// ------------- Scheduler Options ---------------
@@ -673,7 +780,7 @@ func WithClock(clock clockwork.Clock) SchedulerOption {
if clock == nil {
return ErrWithClockNil
}
s.clock = clock
s.exec.clock = clock
return nil
}
}
@@ -756,6 +863,14 @@ const (
// 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.
//
// Note: the limit mode selected for WithLimitConcurrentJobs takes initial
// precedence in the event you are also running a limit mode at the job level
// using WithSingletonMode.
//
// Warning: a single time consuming job can dominate your limit in the event
// you are running both the scheduler limit WithLimitConcurrentJobs(1, LimitModeWait)
// and a job limit WithSingletonMode(LimitModeReschedule).
func WithLimitConcurrentJobs(limit uint, mode LimitMode) SchedulerOption {
return func(s *scheduler) error {
if limit == 0 {

View File

@@ -88,12 +88,14 @@ func convertAtTimesToDateTime(atTimes AtTimes, location *time.Location) ([]time.
}
atTimesDate = append(atTimesDate, at.time(location))
}
slices.SortStableFunc(atTimesDate, func(a, b time.Time) int {
return a.Compare(b)
})
slices.SortStableFunc(atTimesDate, ascendingTime)
return atTimesDate, nil
}
func ascendingTime(a, b time.Time) int {
return a.Compare(b)
}
type waitGroupWithMutex struct {
wg sync.WaitGroup
mu sync.Mutex