Skip to content

Commit c5ad3de

Browse files
authored
feat: introduce matchers to filter jobs (#107)
1 parent b6a0644 commit c5ad3de

File tree

12 files changed

+358
-16
lines changed

12 files changed

+358
-16
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ type Scheduler interface {
2828
// ScheduleJob schedules a job using a specified trigger.
2929
ScheduleJob(jobDetail *JobDetail, trigger Trigger) error
3030

31-
// GetJobKeys returns the keys of all of the scheduled jobs.
32-
GetJobKeys() []*JobKey
31+
// GetJobKeys returns the keys of scheduled jobs.
32+
// For a job key to be returned, the job must satisfy all of the
33+
// matchers specified.
34+
// Given no matchers, it returns the keys of all scheduled jobs.
35+
GetJobKeys(...Matcher[ScheduledJob]) []*JobKey
3336

3437
// GetScheduledJob returns the scheduled job with the specified key.
3538
GetScheduledJob(jobKey *JobKey) (ScheduledJob, error)

examples/queue/file_system.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func main() {
5858

5959
<-ctx.Done()
6060

61-
scheduledJobs := jobQueue.ScheduledJobs()
61+
scheduledJobs := jobQueue.ScheduledJobs(nil)
6262
jobNames := make([]string, 0, len(scheduledJobs))
6363
for _, job := range scheduledJobs {
6464
jobNames = append(jobNames, job.JobDetail().JobKey().String())
@@ -289,7 +289,7 @@ func (jq *jobQueue) Remove(jobKey *quartz.JobKey) (quartz.ScheduledJob, error) {
289289
}
290290

291291
// ScheduledJobs returns the slice of all scheduled jobs in the queue.
292-
func (jq *jobQueue) ScheduledJobs() []quartz.ScheduledJob {
292+
func (jq *jobQueue) ScheduledJobs(matchers []quartz.Matcher[quartz.ScheduledJob]) []quartz.ScheduledJob {
293293
jq.mtx.Lock()
294294
defer jq.mtx.Unlock()
295295
logger.Trace("ScheduledJobs")
@@ -303,7 +303,7 @@ func (jq *jobQueue) ScheduledJobs() []quartz.ScheduledJob {
303303
data, err := os.ReadFile(fmt.Sprintf("%s/%s", dataFolder, file.Name()))
304304
if err == nil {
305305
job, err := unmarshal(data)
306-
if err == nil {
306+
if err == nil && isMatch(job, matchers) {
307307
jobs = append(jobs, job)
308308
}
309309
}
@@ -312,6 +312,16 @@ func (jq *jobQueue) ScheduledJobs() []quartz.ScheduledJob {
312312
return jobs
313313
}
314314

315+
func isMatch(job quartz.ScheduledJob, matchers []quartz.Matcher[quartz.ScheduledJob]) bool {
316+
for _, matcher := range matchers {
317+
// require all matchers to match the job
318+
if !matcher.IsMatch(job) {
319+
return false
320+
}
321+
}
322+
return true
323+
}
324+
315325
// Size returns the size of the job queue.
316326
func (jq *jobQueue) Size() int {
317327
jq.mtx.Lock()

matcher/job_group.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//nolint:dupl
2+
package matcher
3+
4+
import (
5+
"github.com/reugn/go-quartz/quartz"
6+
)
7+
8+
// JobGroup implements the quartz.Matcher interface with the type argument
9+
// quartz.ScheduledJob, matching jobs by their group name.
10+
// It has public fields to allow predicate pushdown in custom quartz.JobQueue
11+
// implementations.
12+
type JobGroup struct {
13+
Operator *StringOperator // uses a pointer to compare with standard operators
14+
Pattern string
15+
}
16+
17+
var _ quartz.Matcher[quartz.ScheduledJob] = (*JobGroup)(nil)
18+
19+
// NewJobGroup returns a new JobGroup matcher given the string operator and pattern.
20+
func NewJobGroup(operator *StringOperator, pattern string) quartz.Matcher[quartz.ScheduledJob] {
21+
return &JobGroup{
22+
Operator: operator,
23+
Pattern: pattern,
24+
}
25+
}
26+
27+
// JobGroupEquals returns a new JobGroup, matching jobs whose group name is identical
28+
// to the given string pattern.
29+
func JobGroupEquals(pattern string) quartz.Matcher[quartz.ScheduledJob] {
30+
return NewJobGroup(&StringEquals, pattern)
31+
}
32+
33+
// JobGroupStartsWith returns a new JobGroup, matching jobs whose group name starts
34+
// with the given string pattern.
35+
func JobGroupStartsWith(pattern string) quartz.Matcher[quartz.ScheduledJob] {
36+
return NewJobGroup(&StringStartsWith, pattern)
37+
}
38+
39+
// JobGroupEndsWith returns a new JobGroup, matching jobs whose group name ends
40+
// with the given string pattern.
41+
func JobGroupEndsWith(pattern string) quartz.Matcher[quartz.ScheduledJob] {
42+
return NewJobGroup(&StringEndsWith, pattern)
43+
}
44+
45+
// JobGroupContains returns a new JobGroup, matching jobs whose group name contains
46+
// the given string pattern.
47+
func JobGroupContains(pattern string) quartz.Matcher[quartz.ScheduledJob] {
48+
return NewJobGroup(&StringContains, pattern)
49+
}
50+
51+
// IsMatch evaluates JobGroup matcher on the given job.
52+
func (g *JobGroup) IsMatch(job quartz.ScheduledJob) bool {
53+
return (*g.Operator)(job.JobDetail().JobKey().Group(), g.Pattern)
54+
}

matcher/job_matcher_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package matcher_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/reugn/go-quartz/internal/assert"
8+
"github.com/reugn/go-quartz/job"
9+
"github.com/reugn/go-quartz/matcher"
10+
"github.com/reugn/go-quartz/quartz"
11+
)
12+
13+
func TestMatcher_JobAll(t *testing.T) {
14+
sched := quartz.NewStdScheduler()
15+
16+
dummy := job.NewFunctionJob(func(_ context.Context) (bool, error) {
17+
return true, nil
18+
})
19+
cron, err := quartz.NewCronTrigger("@daily")
20+
assert.IsNil(t, err)
21+
22+
jobKeys := []*quartz.JobKey{
23+
quartz.NewJobKey("job_monitor"),
24+
quartz.NewJobKey("job_update"),
25+
quartz.NewJobKeyWithGroup("job_monitor", "group_monitor"),
26+
quartz.NewJobKeyWithGroup("job_update", "group_update"),
27+
}
28+
29+
for _, jobKey := range jobKeys {
30+
err := sched.ScheduleJob(quartz.NewJobDetail(dummy, jobKey), cron)
31+
assert.IsNil(t, err)
32+
}
33+
sched.Start(context.Background())
34+
35+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobActive())), 4)
36+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobPaused())), 0)
37+
38+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobGroupEquals(quartz.DefaultGroup))), 2)
39+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobGroupContains("_"))), 2)
40+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobGroupStartsWith("group_"))), 2)
41+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobGroupEndsWith("_update"))), 1)
42+
43+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobNameEquals("job_monitor"))), 2)
44+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobNameContains("_"))), 4)
45+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobNameStartsWith("job_"))), 4)
46+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobNameEndsWith("_update"))), 2)
47+
48+
// multiple matchers
49+
assert.Equal(t, len(sched.GetJobKeys(
50+
matcher.JobNameEquals("job_monitor"),
51+
matcher.JobGroupEquals(quartz.DefaultGroup),
52+
matcher.JobActive(),
53+
)), 1)
54+
55+
assert.Equal(t, len(sched.GetJobKeys(
56+
matcher.JobNameEquals("job_monitor"),
57+
matcher.JobGroupEquals(quartz.DefaultGroup),
58+
matcher.JobPaused(),
59+
)), 0)
60+
61+
// no matchers
62+
assert.Equal(t, len(sched.GetJobKeys()), 4)
63+
64+
err = sched.PauseJob(quartz.NewJobKey("job_monitor"))
65+
assert.IsNil(t, err)
66+
67+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobActive())), 3)
68+
assert.Equal(t, len(sched.GetJobKeys(matcher.JobPaused())), 1)
69+
70+
sched.Stop()
71+
}
72+
73+
func TestMatcher_JobSwitchType(t *testing.T) {
74+
tests := []struct {
75+
name string
76+
m quartz.Matcher[quartz.ScheduledJob]
77+
}{
78+
{
79+
name: "job-active",
80+
m: matcher.JobActive(),
81+
},
82+
{
83+
name: "job-group-equals",
84+
m: matcher.JobGroupEquals("group1"),
85+
},
86+
{
87+
name: "job-name-contains",
88+
m: matcher.JobNameContains("name"),
89+
},
90+
}
91+
for _, tt := range tests {
92+
t.Run(tt.name, func(t *testing.T) {
93+
switch jm := tt.m.(type) {
94+
case *matcher.JobStatus:
95+
assert.Equal(t, jm.Suspended, false)
96+
case *matcher.JobGroup:
97+
if jm.Operator != &matcher.StringEquals {
98+
t.Fatal("JobGroup unexpected operator")
99+
}
100+
case *matcher.JobName:
101+
if jm.Operator != &matcher.StringContains {
102+
t.Fatal("JobName unexpected operator")
103+
}
104+
default:
105+
t.Fatal("Unexpected matcher type")
106+
}
107+
})
108+
}
109+
}
110+
111+
func TestMatcher_CustomStringOperator(t *testing.T) {
112+
var op matcher.StringOperator = func(_, _ string) bool { return true }
113+
assert.NotEqual(t, matcher.NewJobGroup(&op, "group1"), nil)
114+
}

matcher/job_name.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//nolint:dupl
2+
package matcher
3+
4+
import (
5+
"github.com/reugn/go-quartz/quartz"
6+
)
7+
8+
// JobName implements the quartz.Matcher interface with the type argument
9+
// quartz.ScheduledJob, matching jobs by their name.
10+
// It has public fields to allow predicate pushdown in custom quartz.JobQueue
11+
// implementations.
12+
type JobName struct {
13+
Operator *StringOperator // uses a pointer to compare with standard operators
14+
Pattern string
15+
}
16+
17+
var _ quartz.Matcher[quartz.ScheduledJob] = (*JobName)(nil)
18+
19+
// NewJobName returns a new JobName matcher given the string operator and pattern.
20+
func NewJobName(operator *StringOperator, pattern string) quartz.Matcher[quartz.ScheduledJob] {
21+
return &JobName{
22+
Operator: operator,
23+
Pattern: pattern,
24+
}
25+
}
26+
27+
// JobNameEquals returns a new JobName, matching jobs whose name is identical
28+
// to the given string pattern.
29+
func JobNameEquals(pattern string) quartz.Matcher[quartz.ScheduledJob] {
30+
return NewJobName(&StringEquals, pattern)
31+
}
32+
33+
// JobNameStartsWith returns a new JobName, matching jobs whose name starts
34+
// with the given string pattern.
35+
func JobNameStartsWith(pattern string) quartz.Matcher[quartz.ScheduledJob] {
36+
return NewJobName(&StringStartsWith, pattern)
37+
}
38+
39+
// JobNameEndsWith returns a new JobName, matching jobs whose name ends
40+
// with the given string pattern.
41+
func JobNameEndsWith(pattern string) quartz.Matcher[quartz.ScheduledJob] {
42+
return NewJobName(&StringEndsWith, pattern)
43+
}
44+
45+
// JobNameContains returns a new JobName, matching jobs whose name contains
46+
// the given string pattern.
47+
func JobNameContains(pattern string) quartz.Matcher[quartz.ScheduledJob] {
48+
return NewJobName(&StringContains, pattern)
49+
}
50+
51+
// IsMatch evaluates JobName matcher on the given job.
52+
func (n *JobName) IsMatch(job quartz.ScheduledJob) bool {
53+
return (*n.Operator)(job.JobDetail().JobKey().Name(), n.Pattern)
54+
}

matcher/job_status.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package matcher
2+
3+
import (
4+
"github.com/reugn/go-quartz/quartz"
5+
)
6+
7+
// JobStatus implements the quartz.Matcher interface with the type argument
8+
// quartz.ScheduledJob, matching jobs by their status.
9+
// It has public fields to allow predicate pushdown in custom quartz.JobQueue
10+
// implementations.
11+
type JobStatus struct {
12+
Suspended bool
13+
}
14+
15+
var _ quartz.Matcher[quartz.ScheduledJob] = (*JobStatus)(nil)
16+
17+
// JobActive returns a matcher to match active jobs.
18+
func JobActive() quartz.Matcher[quartz.ScheduledJob] {
19+
return &JobStatus{false}
20+
}
21+
22+
// JobPaused returns a matcher to match paused jobs.
23+
func JobPaused() quartz.Matcher[quartz.ScheduledJob] {
24+
return &JobStatus{true}
25+
}
26+
27+
// IsMatch evaluates JobStatus matcher on the given job.
28+
func (s *JobStatus) IsMatch(job quartz.ScheduledJob) bool {
29+
return job.JobDetail().Options().Suspended == s.Suspended
30+
}

matcher/string_operator.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package matcher
2+
3+
import "strings"
4+
5+
// StringOperator is a function to equate two strings.
6+
type StringOperator func(string, string) bool
7+
8+
// String operators.
9+
var (
10+
StringEquals StringOperator = stringsEqual
11+
StringStartsWith StringOperator = strings.HasPrefix
12+
StringEndsWith StringOperator = strings.HasSuffix
13+
StringContains StringOperator = strings.Contains
14+
)
15+
16+
func stringsEqual(source, target string) bool {
17+
return source == target
18+
}

quartz/job.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import (
55
)
66

77
// Job represents an interface to be implemented by structs which
8-
// represent a 'job' to be performed.
8+
// represent a task to be performed.
9+
// Some Job implementations can be found in the job package.
910
type Job interface {
1011
// Execute is called by a Scheduler when the Trigger associated
1112
// with this job fires.

quartz/job_key.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,13 @@ func (jobKey *JobKey) Equals(that *JobKey) bool {
4444
return jobKey.name == that.name &&
4545
jobKey.group == that.group
4646
}
47+
48+
// Name returns the name of the JobKey.
49+
func (jobKey *JobKey) Name() string {
50+
return jobKey.name
51+
}
52+
53+
// Group returns the group of the JobKey.
54+
func (jobKey *JobKey) Group() string {
55+
return jobKey.group
56+
}

quartz/matcher.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package quartz
2+
3+
// Matcher represents a predicate (boolean-valued function) of one argument.
4+
// Matchers can be used in various Scheduler API methods to select the entities
5+
// that should be operated.
6+
// Standard Matcher implementations are located in the matcher package.
7+
type Matcher[T any] interface {
8+
// IsMatch evaluates this matcher on the given argument.
9+
IsMatch(T) bool
10+
}

0 commit comments

Comments
 (0)