mirror of https://github.com/go-co-op/gocron.git
781 lines
21 KiB
Go
781 lines
21 KiB
Go
//go:generate mockgen -source=job.go -destination=mocks/job.go -package=gocronmocks
|
|
package gocron
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/robfig/cron/v3"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
// 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, jobName string)
|
|
beforeJobRuns func(jobID uuid.UUID, jobName string)
|
|
afterJobRunsWithError func(jobID uuid.UUID, jobName string, 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, jobName string)) EventListener {
|
|
return func(j *internalJob) error {
|
|
if eventListenerFunc == nil {
|
|
return ErrEventListenerFuncNil
|
|
}
|
|
j.afterJobRuns = eventListenerFunc
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, jobName string, 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, jobName string)) 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
|
|
}
|