2023-12-11 16:39:59 +00:00
|
|
|
//go:generate mockgen -destination=mocks/job.go -package=gocronmocks . Job
|
2023-11-08 17:11:42 +00:00
|
|
|
package gocron
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/jonboulle/clockwork"
|
|
|
|
|
"github.com/robfig/cron/v3"
|
2023-11-09 15:49:21 +00:00
|
|
|
"golang.org/x/exp/slices"
|
2023-11-08 17:11:42 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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
|
2024-04-06 01:56:22 +00:00
|
|
|
|
|
|
|
|
// as some jobs may queue up, it's possible to
|
|
|
|
|
// have multiple nextScheduled times
|
|
|
|
|
nextScheduled []time.Time
|
|
|
|
|
|
2024-03-26 14:55:21 +00:00
|
|
|
lastRun time.Time
|
2023-11-08 17:11:42 +00:00
|
|
|
function any
|
|
|
|
|
parameters []any
|
|
|
|
|
timer clockwork.Timer
|
|
|
|
|
singletonMode bool
|
|
|
|
|
singletonLimitMode LimitMode
|
|
|
|
|
limitRunsTo *limitRunsTo
|
|
|
|
|
startTime time.Time
|
|
|
|
|
startImmediately bool
|
|
|
|
|
// event listeners
|
2023-11-14 17:39:27 +00:00
|
|
|
afterJobRuns func(jobID uuid.UUID, jobName string)
|
|
|
|
|
beforeJobRuns func(jobID uuid.UUID, jobName string)
|
|
|
|
|
afterJobRunsWithError func(jobID uuid.UUID, jobName string, err error)
|
2024-06-20 12:01:36 +00:00
|
|
|
afterJobRunsWithPanic func(jobID uuid.UUID, jobName string, recoverData any)
|
2024-06-19 11:33:50 +00:00
|
|
|
afterLockError func(jobID uuid.UUID, jobName string, err error)
|
2024-04-22 18:52:27 +00:00
|
|
|
|
|
|
|
|
locker Locker
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2024-06-20 18:56:48 +00:00
|
|
|
p := cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
2023-11-08 17:11:42 +00:00
|
|
|
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 {
|
2024-03-25 20:58:47 +00:00
|
|
|
if d.duration == 0 {
|
|
|
|
|
return ErrDurationJobIntervalZero
|
|
|
|
|
}
|
2023-11-08 17:11:42 +00:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// DailyJob runs the job on the interval of days, and at the set times.
|
|
|
|
|
// By default, the job will start the next available day, considering the last run to be now,
|
|
|
|
|
// and the time and day based on the interval and times you input. This means, if you
|
|
|
|
|
// select an interval greater than 1, your job by default will run X (interval) days from now
|
|
|
|
|
// if there are no atTimes left in the current day. You can use WithStartAt to tell the
|
|
|
|
|
// scheduler to start the job sooner.
|
2023-11-08 17:11:42 +00:00
|
|
|
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)
|
2023-12-17 14:03:44 +00:00
|
|
|
ws.daysOfWeek = daysOfTheWeek
|
2023-11-08 17:11:42 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// Weekdays defines a function that returns a list of week days.
|
2023-11-08 17:11:42 +00:00
|
|
|
type Weekdays func() []time.Weekday
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// NewWeekdays provide the days of the week the job should run.
|
2023-11-08 17:11:42 +00:00
|
|
|
func NewWeekdays(weekday time.Weekday, weekdays ...time.Weekday) Weekdays {
|
|
|
|
|
return func() []time.Weekday {
|
2024-05-02 03:30:07 +00:00
|
|
|
weekdays = append(weekdays, weekday)
|
|
|
|
|
return weekdays
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// WeeklyJob runs the job on the interval of weeks, on the specific days of the week
|
|
|
|
|
// specified, and at the set times.
|
|
|
|
|
//
|
|
|
|
|
// By default, the job will start the next available day, considering the last run to be now,
|
|
|
|
|
// and the time and day 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) weeks from now
|
|
|
|
|
// if there are no daysOfTheWeek left in the current week. You can use WithStartAt to tell the
|
|
|
|
|
// scheduler to start the job sooner.
|
2023-11-08 17:11:42 +00:00
|
|
|
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
|
2023-11-22 12:43:50 +00:00
|
|
|
// X (interval) months from now if there are no daysOfTheMonth left in the current month.
|
2023-11-08 17:11:42 +00:00
|
|
|
// 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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-19 03:13:37 +00:00
|
|
|
var _ JobDefinition = (*oneTimeJobDefinition)(nil)
|
|
|
|
|
|
|
|
|
|
type oneTimeJobDefinition struct {
|
|
|
|
|
startAt OneTimeJobStartAtOption
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o oneTimeJobDefinition) setup(j *internalJob, _ *time.Location) error {
|
|
|
|
|
j.jobSchedule = oneTimeJob{}
|
|
|
|
|
return o.startAt(j)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OneTimeJobStartAtOption defines when the one time job is run
|
|
|
|
|
type OneTimeJobStartAtOption func(*internalJob) error
|
|
|
|
|
|
|
|
|
|
// OneTimeJobStartImmediately tells the scheduler to run the one time job immediately.
|
|
|
|
|
func OneTimeJobStartImmediately() OneTimeJobStartAtOption {
|
|
|
|
|
return func(j *internalJob) error {
|
|
|
|
|
j.startImmediately = true
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OneTimeJobStartDateTime sets the date & time at which the job should run.
|
|
|
|
|
// This datetime must be in the future.
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OneTimeJob is to run a job once at a specified time and not on
|
|
|
|
|
// any regular schedule.
|
|
|
|
|
func OneTimeJob(startAt OneTimeJobStartAtOption) JobDefinition {
|
|
|
|
|
return oneTimeJobDefinition{
|
|
|
|
|
startAt: startAt,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-08 17:11:42 +00:00
|
|
|
// -----------------------------------------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// ----------------- Job Options -----------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// JobOption defines the constructor for job options.
|
2023-11-08 17:11:42 +00:00
|
|
|
type JobOption func(*internalJob) error
|
|
|
|
|
|
2024-04-22 18:52:27 +00:00
|
|
|
// 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) error {
|
|
|
|
|
if locker == nil {
|
|
|
|
|
return ErrWithDistributedJobLockerNil
|
|
|
|
|
}
|
|
|
|
|
j.locker = locker
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// WithEventListeners sets the event listeners that should be
|
|
|
|
|
// run for the job.
|
2023-11-08 17:11:42 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// WithSingletonMode keeps the job from running again if it is already running.
|
|
|
|
|
// This is useful for jobs that should not overlap, and that occasionally
|
|
|
|
|
// (but not consistently) run longer than the interval between job runs.
|
2023-11-08 17:11:42 +00:00
|
|
|
func WithSingletonMode(mode LimitMode) JobOption {
|
|
|
|
|
return func(j *internalJob) error {
|
|
|
|
|
j.singletonMode = true
|
|
|
|
|
j.singletonLimitMode = mode
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// WithStartAt sets the option for starting the job at
|
|
|
|
|
// a specific datetime.
|
2023-11-08 17:11:42 +00:00
|
|
|
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.
|
2023-11-22 12:43:50 +00:00
|
|
|
// This datetime must be in the future.
|
2023-11-08 17:11:42 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// 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.
|
2023-11-08 17:11:42 +00:00
|
|
|
func WithTags(tags ...string) JobOption {
|
|
|
|
|
return func(j *internalJob) error {
|
|
|
|
|
j.tags = tags
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// ------------- Job Event Listeners -------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// EventListener defines the constructor for event
|
|
|
|
|
// listeners that can be used to listen for job events.
|
2023-11-08 17:11:42 +00:00
|
|
|
type EventListener func(*internalJob) error
|
|
|
|
|
|
2024-06-20 12:01:36 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-02 20:44:39 +00:00
|
|
|
// AfterJobRuns is used to listen for when a job has run
|
|
|
|
|
// without an error, and then run the provided function.
|
2023-11-14 17:39:27 +00:00
|
|
|
func AfterJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
|
2023-11-08 17:11:42 +00:00
|
|
|
return func(j *internalJob) error {
|
|
|
|
|
if eventListenerFunc == nil {
|
|
|
|
|
return ErrEventListenerFuncNil
|
|
|
|
|
}
|
|
|
|
|
j.afterJobRuns = eventListenerFunc
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 12:43:50 +00:00
|
|
|
// AfterJobRunsWithError is used to listen for when a job has run and
|
|
|
|
|
// returned an error, and then run the provided function.
|
2023-11-14 17:39:27 +00:00
|
|
|
func AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, jobName string, err error)) EventListener {
|
2023-11-08 17:11:42 +00:00
|
|
|
return func(j *internalJob) error {
|
|
|
|
|
if eventListenerFunc == nil {
|
|
|
|
|
return ErrEventListenerFuncNil
|
|
|
|
|
}
|
|
|
|
|
j.afterJobRunsWithError = eventListenerFunc
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-20 12:01:36 +00:00
|
|
|
// 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 {
|
2023-11-08 17:11:42 +00:00
|
|
|
return func(j *internalJob) error {
|
|
|
|
|
if eventListenerFunc == nil {
|
|
|
|
|
return ErrEventListenerFuncNil
|
|
|
|
|
}
|
2024-06-20 12:01:36 +00:00
|
|
|
j.afterJobRunsWithPanic = eventListenerFunc
|
2023-11-08 17:11:42 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-19 11:33:50 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-08 17:11:42 +00:00
|
|
|
// -----------------------------------------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// ---------------- 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 {
|
2023-12-17 14:03:44 +00:00
|
|
|
firstPass := true
|
|
|
|
|
next := d.nextDay(lastRun, firstPass)
|
|
|
|
|
if !next.IsZero() {
|
|
|
|
|
return next
|
|
|
|
|
}
|
|
|
|
|
firstPass = false
|
|
|
|
|
|
|
|
|
|
startNextDay := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(d.interval), 0, 0, 0, lastRun.Nanosecond(), lastRun.Location())
|
|
|
|
|
return d.nextDay(startNextDay, firstPass)
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-17 14:03:44 +00:00
|
|
|
func (d dailyJob) nextDay(lastRun time.Time, firstPass bool) time.Time {
|
2023-11-08 17:11:42 +00:00
|
|
|
for _, at := range d.atTimes {
|
2024-03-26 14:55:21 +00:00
|
|
|
// sub the at time hour/min/sec onto the lastScheduledRun's values
|
2023-11-08 17:11:42 +00:00
|
|
|
// 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())
|
|
|
|
|
|
2023-12-17 14:03:44 +00:00
|
|
|
if firstPass && atDate.After(lastRun) {
|
2023-11-08 17:11:42 +00:00
|
|
|
// checking to see if it is after i.e. greater than,
|
2024-03-26 14:55:21 +00:00
|
|
|
// and not greater or equal as our lastScheduledRun day/time
|
2023-11-08 17:11:42 +00:00
|
|
|
// will be in the loop, and we don't want to select it again
|
|
|
|
|
return atDate
|
2023-12-17 14:03:44 +00:00
|
|
|
} else if !firstPass && !atDate.Before(lastRun) {
|
2023-12-13 21:54:12 +00:00
|
|
|
// now that we're looking at the next day, it's ok to consider
|
2024-03-26 14:55:21 +00:00
|
|
|
// the same at time that was last run (as lastScheduledRun has been incremented)
|
2023-12-13 21:54:12 +00:00
|
|
|
return atDate
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-08 17:11:42 +00:00
|
|
|
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 {
|
2023-12-17 14:03:44 +00:00
|
|
|
firstPass := true
|
|
|
|
|
next := w.nextWeekDayAtTime(lastRun, firstPass)
|
2023-11-08 17:11:42 +00:00
|
|
|
if !next.IsZero() {
|
|
|
|
|
return next
|
|
|
|
|
}
|
2023-12-17 14:03:44 +00:00
|
|
|
firstPass = false
|
2023-11-08 17:11:42 +00:00
|
|
|
|
|
|
|
|
startOfTheNextIntervalWeek := (lastRun.Day() - int(lastRun.Weekday())) + int(w.interval*7)
|
|
|
|
|
from := time.Date(lastRun.Year(), lastRun.Month(), startOfTheNextIntervalWeek, 0, 0, 0, 0, lastRun.Location())
|
2023-12-17 14:03:44 +00:00
|
|
|
return w.nextWeekDayAtTime(from, firstPass)
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-17 14:03:44 +00:00
|
|
|
func (w weeklyJob) nextWeekDayAtTime(lastRun time.Time, firstPass bool) time.Time {
|
2023-11-08 17:11:42 +00:00
|
|
|
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 {
|
2024-03-26 14:55:21 +00:00
|
|
|
// sub the at time hour/min/sec onto the lastScheduledRun's values
|
2023-11-08 17:11:42 +00:00
|
|
|
// 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())
|
|
|
|
|
|
2023-12-17 14:03:44 +00:00
|
|
|
if firstPass && atDate.After(lastRun) {
|
2023-11-08 17:11:42 +00:00
|
|
|
// checking to see if it is after i.e. greater than,
|
2024-03-26 14:55:21 +00:00
|
|
|
// and not greater or equal as our lastScheduledRun day/time
|
2023-11-08 17:11:42 +00:00
|
|
|
// will be in the loop, and we don't want to select it again
|
|
|
|
|
return atDate
|
2023-12-17 14:03:44 +00:00
|
|
|
} else if !firstPass && !atDate.Before(lastRun) {
|
|
|
|
|
// now that we're looking at the next week, it's ok to consider
|
2024-03-26 14:55:21 +00:00
|
|
|
// the same at time that was last run (as lastScheduledRun has been incremented)
|
2023-12-17 14:03:44 +00:00
|
|
|
return atDate
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
|
2024-01-21 22:04:16 +00:00
|
|
|
daysFromEnd := m.handleNegativeDays(lastRun, daysList, m.daysFromEnd)
|
|
|
|
|
next := m.nextMonthDayAtTime(lastRun, daysFromEnd, true)
|
2023-11-08 17:11:42 +00:00
|
|
|
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() {
|
2024-01-21 22:04:16 +00:00
|
|
|
daysFromEnd = m.handleNegativeDays(from, daysList, m.daysFromEnd)
|
|
|
|
|
next = m.nextMonthDayAtTime(from, daysFromEnd, false)
|
2023-11-08 17:11:42 +00:00
|
|
|
from = from.AddDate(0, int(m.interval), 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return next
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-21 22:04:16 +00:00
|
|
|
func (m monthlyJob) handleNegativeDays(from time.Time, days, negativeDays []int) []int {
|
|
|
|
|
var out []int
|
|
|
|
|
// getting a list of the days from the end of the following month
|
|
|
|
|
// -1 == the last day of the month
|
|
|
|
|
firstDayNextMonth := time.Date(from.Year(), from.Month()+1, 1, 0, 0, 0, 0, from.Location())
|
|
|
|
|
for _, daySub := range negativeDays {
|
|
|
|
|
day := firstDayNextMonth.AddDate(0, 0, daySub).Day()
|
|
|
|
|
out = append(out, day)
|
|
|
|
|
}
|
|
|
|
|
out = append(out, days...)
|
|
|
|
|
slices.Sort(out)
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-17 14:03:44 +00:00
|
|
|
func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, firstPass bool) time.Time {
|
2023-11-08 17:11:42 +00:00
|
|
|
// 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 {
|
2024-03-26 14:55:21 +00:00
|
|
|
// sub the day, and the at time hour/min/sec onto the lastScheduledRun's values
|
2023-11-08 17:11:42 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-17 14:03:44 +00:00
|
|
|
if firstPass && atDate.After(lastRun) {
|
2023-11-08 17:11:42 +00:00
|
|
|
// checking to see if it is after i.e. greater than,
|
2024-03-26 14:55:21 +00:00
|
|
|
// and not greater or equal as our lastScheduledRun day/time
|
2023-11-08 17:11:42 +00:00
|
|
|
// will be in the loop, and we don't want to select it again
|
|
|
|
|
return atDate
|
2023-12-17 14:03:44 +00:00
|
|
|
} else if !firstPass && !atDate.Before(lastRun) {
|
|
|
|
|
// now that we're looking at the next month, it's ok to consider
|
2024-03-26 14:55:21 +00:00
|
|
|
// the same at time that was lastScheduledRun (as lastScheduledRun has been incremented)
|
2023-12-17 14:03:44 +00:00
|
|
|
return atDate
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-19 03:13:37 +00:00
|
|
|
var _ jobSchedule = (*oneTimeJob)(nil)
|
|
|
|
|
|
|
|
|
|
type oneTimeJob struct{}
|
|
|
|
|
|
|
|
|
|
func (o oneTimeJob) next(_ time.Time) time.Time {
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-08 17:11:42 +00:00
|
|
|
// -----------------------------------------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// ---------------- Job Interface ----------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Job provides the available methods on the job
|
|
|
|
|
// available to the caller.
|
|
|
|
|
type Job interface {
|
2023-12-20 21:13:38 +00:00
|
|
|
// ID returns the job's unique identifier.
|
2023-11-08 17:11:42 +00:00
|
|
|
ID() uuid.UUID
|
2023-12-20 21:13:38 +00:00
|
|
|
// LastRun returns the time of the job's last run
|
2023-11-08 17:11:42 +00:00
|
|
|
LastRun() (time.Time, error)
|
2023-12-20 21:13:38 +00:00
|
|
|
// Name returns the name defined on the job.
|
2023-11-08 17:11:42 +00:00
|
|
|
Name() string
|
2023-12-20 21:13:38 +00:00
|
|
|
// NextRun returns the time of the job's next scheduled run.
|
2023-11-08 17:11:42 +00:00
|
|
|
NextRun() (time.Time, error)
|
2024-05-06 19:07:44 +00:00
|
|
|
// NextRuns returns the requested number of calculated next run values.
|
|
|
|
|
NextRuns(int) ([]time.Time, error)
|
2023-12-20 21:13:38 +00:00
|
|
|
// RunNow runs the job once, now. This does not alter
|
|
|
|
|
// the existing run schedule, and will respect all job
|
2024-03-05 16:55:42 +00:00
|
|
|
// and scheduler limits. This means that running a job now may
|
|
|
|
|
// cause the job's regular interval to be rescheduled due to
|
|
|
|
|
// the instance being run by RunNow blocking your run limit.
|
2023-12-19 03:13:37 +00:00
|
|
|
RunNow() error
|
2023-12-20 21:13:38 +00:00
|
|
|
// Tags returns the job's string tags.
|
|
|
|
|
Tags() []string
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2023-12-19 03:13:37 +00:00
|
|
|
runJobRequest chan runJobRequest
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (j job) ID() uuid.UUID {
|
|
|
|
|
return j.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (j job) Name() string {
|
|
|
|
|
return j.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (j job) NextRun() (time.Time, error) {
|
|
|
|
|
ij := requestJob(j.id, j.jobOutRequest)
|
|
|
|
|
if ij == nil || ij.id == uuid.Nil {
|
|
|
|
|
return time.Time{}, ErrJobNotFound
|
|
|
|
|
}
|
2024-04-06 01:56:22 +00:00
|
|
|
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
|
2023-11-08 17:11:42 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-06 19:07:44 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-08 17:11:42 +00:00
|
|
|
func (j job) Tags() []string {
|
|
|
|
|
return j.tags
|
|
|
|
|
}
|
2023-12-19 03:13:37 +00:00
|
|
|
|
|
|
|
|
func (j job) RunNow() error {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
resp := make(chan error, 1)
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case j.runJobRequest <- runJobRequest{
|
|
|
|
|
id: j.id,
|
|
|
|
|
outChan: resp,
|
|
|
|
|
}:
|
|
|
|
|
case <-time.After(100 * time.Millisecond):
|
|
|
|
|
return ErrJobRunNowFailed
|
|
|
|
|
}
|
|
|
|
|
var err error
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return ErrJobRunNowFailed
|
|
|
|
|
case errReceived := <-resp:
|
|
|
|
|
err = errReceived
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|