diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ba6fcb..9a06998 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [1.18.x, 1.22.x] + go-version: [1.20.x, 1.23.x] steps: - name: Setup Go uses: actions/setup-go@v5 @@ -27,7 +27,7 @@ jobs: run: go test -race ./... -coverprofile=coverage.out -covermode=atomic - name: Upload coverage to Codecov - if: ${{ matrix.go-version == '1.18.x' }} + if: ${{ matrix.go-version == '1.20.x' }} uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/go.mod b/go.mod index a8bc4ef..b64e93c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/reugn/go-quartz -go 1.18 +go 1.20 diff --git a/quartz/cron.go b/quartz/cron.go index ce6ca4a..a4a75c9 100644 --- a/quartz/cron.go +++ b/quartz/cron.go @@ -9,7 +9,7 @@ import ( "time" ) -// CronTrigger implements the quartz.Trigger interface. +// CronTrigger implements the [Trigger] interface. // Used to fire a Job at given moments in time, defined with Unix 'cron-like' schedule definitions. // // Examples: @@ -37,15 +37,15 @@ type CronTrigger struct { // Verify CronTrigger satisfies the Trigger interface. var _ Trigger = (*CronTrigger)(nil) -// NewCronTrigger returns a new CronTrigger using the UTC location. +// NewCronTrigger returns a new [CronTrigger] using the UTC location. func NewCronTrigger(expression string) (*CronTrigger, error) { return NewCronTriggerWithLoc(expression, time.UTC) } -// NewCronTriggerWithLoc returns a new CronTrigger with the given time.Location. +// NewCronTriggerWithLoc returns a new [CronTrigger] with the given [time.Location]. func NewCronTriggerWithLoc(expression string, location *time.Location) (*CronTrigger, error) { if location == nil { - return nil, illegalArgumentError("location is nil") + return nil, newIllegalArgumentError("location is nil") } expression = trimCronExpression(expression) fields, err := parseCronExpression(expression) @@ -141,13 +141,13 @@ func parseCronExpression(expression string) ([]*cronField, error) { } length := len(tokens) if length < 6 || length > 7 { - return nil, cronParseError("invalid expression length") + return nil, newCronParseError("invalid expression length") } if length == 6 { tokens = append(tokens, "*") } if (tokens[3] != "?" && tokens[3] != "*") && (tokens[5] != "?" && tokens[5] != "*") { - return nil, cronParseError("day field set twice") + return nil, newCronParseError("day field set twice") } return buildCronField(tokens) @@ -217,7 +217,7 @@ func parseField(field string, min, max int, translate ...[]string) (*cronField, if inScope(i, min, max) { return &cronField{[]int{i}}, nil } - return nil, invalidCronFieldError("simple", field) + return nil, newInvalidCronFieldError("simple", field) } // list values if strings.ContainsRune(field, listRune) { @@ -240,10 +240,10 @@ func parseField(field string, min, max int, translate ...[]string) (*cronField, if inScope(intVal, min, max) { return &cronField{[]int{intVal}}, nil } - return nil, invalidCronFieldError("literal", field) + return nil, newInvalidCronFieldError("literal", field) } - return nil, cronParseError(fmt.Sprintf("invalid field %s", field)) + return nil, newCronParseError(fmt.Sprintf("invalid field %s", field)) } func parseListField(field string, min, max int, glossary []string) (*cronField, error) { @@ -276,7 +276,7 @@ func parseListField(field string, min, max int, glossary []string) (*cronField, func parseRangeField(field string, min, max int, glossary []string) (*cronField, error) { t := strings.Split(field, string(rangeRune)) if len(t) != 2 { - return nil, invalidCronFieldError("range", field) + return nil, newInvalidCronFieldError("range", field) } from, err := normalize(t[0], glossary) if err != nil { @@ -287,7 +287,7 @@ func parseRangeField(field string, min, max int, glossary []string) (*cronField, return nil, err } if !inScope(from, min, max) || !inScope(to, min, max) { - return nil, invalidCronFieldError("range", field) + return nil, newInvalidCronFieldError("range", field) } rangeValues, err := fillRangeValues(from, to) if err != nil { @@ -300,7 +300,7 @@ func parseRangeField(field string, min, max int, glossary []string) (*cronField, func parseStepField(field string, min, max int, glossary []string) (*cronField, error) { t := strings.Split(field, string(stepRune)) if len(t) != 2 { - return nil, invalidCronFieldError("step", field) + return nil, newInvalidCronFieldError("step", field) } to := max var ( @@ -313,7 +313,7 @@ func parseStepField(field string, min, max int, glossary []string) (*cronField, case strings.ContainsRune(t[0], rangeRune): trange := strings.Split(t[0], string(rangeRune)) if len(trange) != 2 { - return nil, invalidCronFieldError("step", field) + return nil, newInvalidCronFieldError("step", field) } from, err = normalize(trange[0], glossary) if err != nil { @@ -331,10 +331,10 @@ func parseStepField(field string, min, max int, glossary []string) (*cronField, } step, err := strconv.Atoi(t[1]) if err != nil { - return nil, invalidCronFieldError("step", field) + return nil, newInvalidCronFieldError("step", field) } if !inScope(from, min, max) || !inScope(step, 1, max) || !inScope(to, min, max) { - return nil, invalidCronFieldError("step", field) + return nil, newInvalidCronFieldError("step", field) } stepValues, err := fillStepValues(from, step, to) if err != nil { diff --git a/quartz/error.go b/quartz/error.go index 9ba5c75..d56c116 100644 --- a/quartz/error.go +++ b/quartz/error.go @@ -8,33 +8,30 @@ import ( // Errors var ( ErrIllegalArgument = errors.New("illegal argument") - ErrIllegalState = errors.New("illegal state") ErrCronParse = errors.New("parse cron expression") - ErrJobNotFound = errors.New("job not found") - ErrQueueEmpty = errors.New("queue is empty") ErrTriggerExpired = errors.New("trigger has expired") + + ErrIllegalState = errors.New("illegal state") + ErrQueueEmpty = errors.New("queue is empty") + ErrJobNotFound = errors.New("job not found") + ErrJobAlreadyExists = errors.New("job already exists") + ErrJobIsSuspended = errors.New("job is suspended") + ErrJobIsActive = errors.New("job is active") ) -// illegalArgumentError returns an illegal argument error with a custom +// newIllegalArgumentError returns an illegal argument error with a custom // error message, which unwraps to ErrIllegalArgument. -func illegalArgumentError(message string) error { +func newIllegalArgumentError(message string) error { return fmt.Errorf("%w: %s", ErrIllegalArgument, message) } -// illegalStateError returns an illegal state error with a custom -// error message, which unwraps to ErrIllegalState. -func illegalStateError(message string) error { - return fmt.Errorf("%w: %s", ErrIllegalState, message) -} - -// cronParseError returns a cron parse error with a custom error message, +// newCronParseError returns a cron parse error with a custom error message, // which unwraps to ErrCronParse. -func cronParseError(message string) error { +func newCronParseError(message string) error { return fmt.Errorf("%w: %s", ErrCronParse, message) } -// jobNotFoundError returns a job not found error with a custom error message, -// which unwraps to ErrJobNotFound. -func jobNotFoundError(message string) error { - return fmt.Errorf("%w: %s", ErrJobNotFound, message) +// newIllegalStateError returns an illegal state error specifying it with err. +func newIllegalStateError(err error) error { + return fmt.Errorf("%w: %w", ErrIllegalState, err) } diff --git a/quartz/error_test.go b/quartz/error_test.go index d335c84..c80bcab 100644 --- a/quartz/error_test.go +++ b/quartz/error_test.go @@ -1,45 +1,32 @@ package quartz import ( - "errors" "fmt" "testing" "github.com/reugn/go-quartz/internal/assert" ) -func TestIllegalArgumentError(t *testing.T) { +func TestError_IllegalArgument(t *testing.T) { message := "argument is nil" - err := illegalArgumentError(message) - if !errors.Is(err, ErrIllegalArgument) { - t.Fatal("error must match ErrIllegalArgument") - } - assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrIllegalArgument, message)) -} + err := newIllegalArgumentError(message) -func TestIllegalStateError(t *testing.T) { - message := "job already exists" - err := illegalStateError(message) - if !errors.Is(err, ErrIllegalState) { - t.Fatal("error must match ErrIllegalState") - } - assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrIllegalState, message)) + assert.ErrorIs(t, err, ErrIllegalArgument) + assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrIllegalArgument, message)) } -func TestCronParseError(t *testing.T) { +func TestError_CronParse(t *testing.T) { message := "invalid field" - err := cronParseError(message) - if !errors.Is(err, ErrCronParse) { - t.Fatal("error must match ErrCronParse") - } + err := newCronParseError(message) + + assert.ErrorIs(t, err, ErrCronParse) assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrCronParse, message)) } -func TestJobNotFoundError(t *testing.T) { - message := "for key" - err := jobNotFoundError(message) - if !errors.Is(err, ErrJobNotFound) { - t.Fatal("error must match ErrJobNotFound") - } - assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrJobNotFound, message)) +func TestError_IllegalState(t *testing.T) { + err := newIllegalStateError(ErrJobAlreadyExists) + + assert.ErrorIs(t, err, ErrIllegalState) + assert.ErrorIs(t, err, ErrJobAlreadyExists) + assert.Equal(t, err.Error(), fmt.Sprintf("%s: %s", ErrIllegalState, ErrJobAlreadyExists)) } diff --git a/quartz/queue.go b/quartz/queue.go index dc4bff7..f8c9e96 100644 --- a/quartz/queue.go +++ b/quartz/queue.go @@ -2,7 +2,6 @@ package quartz import ( "container/heap" - "fmt" "sync" ) @@ -16,7 +15,7 @@ type scheduledJob struct { var _ ScheduledJob = (*scheduledJob)(nil) -// Job returns the scheduled job instance. +// JobDetail returns the details of the scheduled job. func (scheduled *scheduledJob) JobDetail() *JobDetail { return scheduled.job } @@ -45,11 +44,13 @@ type JobQueue interface { Push(job ScheduledJob) error // Pop removes and returns the next to run scheduled job from the queue. - // Implementations should return quartz.ErrQueueEmpty if the queue is empty. + // Implementations should return an error wrapping [ErrQueueEmpty] if the + // queue is empty. Pop() (ScheduledJob, error) // Head returns the first scheduled job without removing it from the queue. - // Implementations should return quartz.ErrQueueEmpty if the queue is empty. + // Implementations should return an error wrapping [ErrQueueEmpty] if the + // queue is empty. Head() (ScheduledJob, error) // Get returns the scheduled job with the specified key without removing it @@ -61,7 +62,7 @@ type JobQueue interface { // ScheduledJobs returns a slice of scheduled jobs in the queue. // The matchers parameter acts as a filter to build the resulting list. - // For a job to be returned in the result slice, it must satisfy all of the + // For a job to be returned in the result slice, it must satisfy all the // specified matchers. Empty matchers return all scheduled jobs in the queue. // // Custom queue implementations may consider using pattern matching on the @@ -151,8 +152,7 @@ func (jq *jobQueue) Push(job ScheduledJob) error { heap.Remove(&jq.delegate, i) break } - return illegalStateError(fmt.Sprintf("job with the key %s already exists", - job.JobDetail().jobKey)) + return newIllegalStateError(ErrJobAlreadyExists) } } heap.Push(&jq.delegate, job) @@ -164,7 +164,7 @@ func (jq *jobQueue) Pop() (ScheduledJob, error) { jq.mtx.Lock() defer jq.mtx.Unlock() if len(jq.delegate) == 0 { - return nil, ErrQueueEmpty + return nil, newIllegalStateError(ErrQueueEmpty) } return heap.Pop(&jq.delegate).(ScheduledJob), nil } @@ -174,7 +174,7 @@ func (jq *jobQueue) Head() (ScheduledJob, error) { jq.mtx.Lock() defer jq.mtx.Unlock() if len(jq.delegate) == 0 { - return nil, ErrQueueEmpty + return nil, newIllegalStateError(ErrQueueEmpty) } return jq.delegate[0], nil } @@ -189,7 +189,7 @@ func (jq *jobQueue) Get(jobKey *JobKey) (ScheduledJob, error) { return scheduled, nil } } - return nil, jobNotFoundError(jobKey.String()) + return nil, newIllegalStateError(ErrJobNotFound) } // Remove removes and returns the scheduled job with the specified key. @@ -202,11 +202,11 @@ func (jq *jobQueue) Remove(jobKey *JobKey) (ScheduledJob, error) { return heap.Remove(&jq.delegate, i).(ScheduledJob), nil } } - return nil, jobNotFoundError(jobKey.String()) + return nil, newIllegalStateError(ErrJobNotFound) } // ScheduledJobs returns a slice of scheduled jobs in the queue. -// For a job to be returned, it must satisfy all of the specified matchers. +// For a job to be returned, it must satisfy all the specified matchers. // Given an empty matchers it returns all scheduled jobs. func (jq *jobQueue) ScheduledJobs(matchers []Matcher[ScheduledJob]) ([]ScheduledJob, error) { jq.mtx.Lock() diff --git a/quartz/scheduler.go b/quartz/scheduler.go index 1c30d7d..a491798 100644 --- a/quartz/scheduler.go +++ b/quartz/scheduler.go @@ -3,7 +3,6 @@ package quartz import ( "context" "errors" - "fmt" "math" "sync" "time" @@ -170,16 +169,16 @@ func (sched *StdScheduler) ScheduleJob( trigger Trigger, ) error { if jobDetail == nil { - return illegalArgumentError("jobDetail is nil") + return newIllegalArgumentError("jobDetail is nil") } if jobDetail.jobKey == nil { - return illegalArgumentError("jobDetail.jobKey is nil") + return newIllegalArgumentError("jobDetail.jobKey is nil") } if jobDetail.jobKey.name == "" { - return illegalArgumentError("empty key name is not allowed") + return newIllegalArgumentError("empty key name is not allowed") } if trigger == nil { - return illegalArgumentError("trigger is nil") + return newIllegalArgumentError("trigger is nil") } nextRunTime := int64(math.MaxInt64) @@ -271,7 +270,7 @@ func (sched *StdScheduler) GetJobKeys(matchers ...Matcher[ScheduledJob]) ([]*Job // GetScheduledJob returns the ScheduledJob with the specified key. func (sched *StdScheduler) GetScheduledJob(jobKey *JobKey) (ScheduledJob, error) { if jobKey == nil { - return nil, illegalArgumentError("jobKey is nil") + return nil, newIllegalArgumentError("jobKey is nil") } sched.queueMtx.Lock() @@ -283,7 +282,7 @@ func (sched *StdScheduler) GetScheduledJob(jobKey *JobKey) (ScheduledJob, error) // DeleteJob removes the Job with the specified key if present. func (sched *StdScheduler) DeleteJob(jobKey *JobKey) error { if jobKey == nil { - return illegalArgumentError("jobKey is nil") + return newIllegalArgumentError("jobKey is nil") } sched.queueMtx.Lock() @@ -303,7 +302,7 @@ func (sched *StdScheduler) DeleteJob(jobKey *JobKey) error { // executed by the scheduler. func (sched *StdScheduler) PauseJob(jobKey *JobKey) error { if jobKey == nil { - return illegalArgumentError("jobKey is nil") + return newIllegalArgumentError("jobKey is nil") } sched.queueMtx.Lock() @@ -314,7 +313,7 @@ func (sched *StdScheduler) PauseJob(jobKey *JobKey) error { return err } if job.JobDetail().opts.Suspended { - return illegalStateError(fmt.Sprintf("job %s is suspended", jobKey)) + return newIllegalStateError(ErrJobIsSuspended) } job, err = sched.queue.Remove(jobKey) @@ -338,7 +337,7 @@ func (sched *StdScheduler) PauseJob(jobKey *JobKey) error { // ResumeJob restarts the suspended job with the specified key. func (sched *StdScheduler) ResumeJob(jobKey *JobKey) error { if jobKey == nil { - return illegalArgumentError("jobKey is nil") + return newIllegalArgumentError("jobKey is nil") } sched.queueMtx.Lock() @@ -349,7 +348,7 @@ func (sched *StdScheduler) ResumeJob(jobKey *JobKey) error { return err } if !job.JobDetail().opts.Suspended { - return illegalStateError(fmt.Sprintf("job %s is active", jobKey)) + return newIllegalStateError(ErrJobIsActive) } job, err = sched.queue.Remove(jobKey) diff --git a/quartz/scheduler_test.go b/quartz/scheduler_test.go index 01372fa..0f712a7 100644 --- a/quartz/scheduler_test.go +++ b/quartz/scheduler_test.go @@ -314,6 +314,7 @@ func TestScheduler_JobWithRetries(t *testing.T) { assert.IsNil(t, err) err = sched.ScheduleJob(jobDetail, quartz.NewRunOnceTrigger(time.Millisecond)) assert.ErrorIs(t, err, quartz.ErrIllegalState) + assert.ErrorIs(t, err, quartz.ErrJobAlreadyExists) jobDetail.Options().Replace = true err = sched.ScheduleJob(jobDetail, quartz.NewRunOnceTrigger(time.Millisecond)) assert.IsNil(t, err) @@ -402,8 +403,8 @@ func TestScheduler_MisfiredJob(t *testing.T) { sched.Start(context.Background()) - job := <-misfiredChan - assert.Equal(t, job.JobDetail().JobKey().Name(), "funcJob") + misfired := <-misfiredChan + assert.Equal(t, misfired.JobDetail().JobKey().Name(), "funcJob") sched.Stop() } @@ -477,6 +478,7 @@ func TestScheduler_PauseResumeErrors(t *testing.T) { err = sched.ResumeJob(jobDetail.JobKey()) assert.ErrorIs(t, err, quartz.ErrIllegalState) + assert.ErrorIs(t, err, quartz.ErrJobIsActive) err = sched.ResumeJob(quartz.NewJobKey("funcJob2")) assert.ErrorIs(t, err, quartz.ErrJobNotFound) @@ -484,6 +486,7 @@ func TestScheduler_PauseResumeErrors(t *testing.T) { assert.IsNil(t, err) err = sched.PauseJob(jobDetail.JobKey()) assert.ErrorIs(t, err, quartz.ErrIllegalState) + assert.ErrorIs(t, err, quartz.ErrJobIsSuspended) err = sched.PauseJob(quartz.NewJobKey("funcJob2")) assert.ErrorIs(t, err, quartz.ErrJobNotFound) @@ -496,20 +499,20 @@ func TestScheduler_PauseResumeErrors(t *testing.T) { func TestScheduler_ArgumentValidationErrors(t *testing.T) { sched := quartz.NewStdScheduler() - job := job.NewShellJob("ls -la") + j := job.NewShellJob("ls -la") trigger := quartz.NewRunOnceTrigger(time.Millisecond) expiredTrigger, err := quartz.NewCronTrigger("0 0 0 1 1 ? 2023") assert.IsNil(t, err) err = sched.ScheduleJob(nil, trigger) assert.ErrorContains(t, err, "jobDetail is nil") - err = sched.ScheduleJob(quartz.NewJobDetail(job, nil), trigger) + err = sched.ScheduleJob(quartz.NewJobDetail(j, nil), trigger) assert.ErrorContains(t, err, "jobDetail.jobKey is nil") - err = sched.ScheduleJob(quartz.NewJobDetail(job, quartz.NewJobKey("")), trigger) + err = sched.ScheduleJob(quartz.NewJobDetail(j, quartz.NewJobKey("")), trigger) assert.ErrorContains(t, err, "empty key name is not allowed") - err = sched.ScheduleJob(quartz.NewJobDetail(job, quartz.NewJobKeyWithGroup("job", "")), nil) + err = sched.ScheduleJob(quartz.NewJobDetail(j, quartz.NewJobKeyWithGroup("job", "")), nil) assert.ErrorContains(t, err, "trigger is nil") - err = sched.ScheduleJob(quartz.NewJobDetail(job, quartz.NewJobKey("job")), expiredTrigger) + err = sched.ScheduleJob(quartz.NewJobDetail(j, quartz.NewJobKey("job")), expiredTrigger) assert.ErrorIs(t, err, quartz.ErrTriggerExpired) err = sched.DeleteJob(nil) diff --git a/quartz/util.go b/quartz/util.go index d708bf5..946f4d2 100644 --- a/quartz/util.go +++ b/quartz/util.go @@ -56,7 +56,7 @@ func extractStepValues(parsed []string) ([]string, []string) { func fillRangeValues(from, to int) ([]int, error) { if to < from { - return nil, cronParseError("fill range values") + return nil, newCronParseError("fill range values") } length := (to - from) + 1 rangeValues := make([]int, length) @@ -68,7 +68,7 @@ func fillRangeValues(from, to int) ([]int, error) { func fillStepValues(from, step, max int) ([]int, error) { if max < from || step == 0 { - return nil, cronParseError("fill step values") + return nil, newCronParseError("fill step values") } length := ((max - from) / step) + 1 stepValues := make([]int, length) @@ -100,11 +100,11 @@ func translateLiteral(glossary []string, literal string) (int, error) { return i, nil } } - return 0, cronParseError(fmt.Sprintf("unknown literal %s", literal)) + return 0, newCronParseError(fmt.Sprintf("unknown literal %s", literal)) } -func invalidCronFieldError(t, field string) error { - return cronParseError(fmt.Sprintf("invalid %s field %s", t, field)) +func newInvalidCronFieldError(t, field string) error { + return newCronParseError(fmt.Sprintf("invalid %s field %s", t, field)) } // NowNano returns the current Unix time in nanoseconds.