added Cron.Jobs() method

This commit is contained in:
Gani Georgiev 2024-12-22 16:05:38 +02:00
parent f27d9f1dc9
commit bae5421d62
5 changed files with 163 additions and 22 deletions

View File

@ -11,21 +11,17 @@ package cron
import ( import (
"errors" "errors"
"fmt" "fmt"
"slices"
"sync" "sync"
"time" "time"
) )
type job struct {
schedule *Schedule
run func()
}
// Cron is a crontab-like struct for tasks/jobs scheduling. // Cron is a crontab-like struct for tasks/jobs scheduling.
type Cron struct { type Cron struct {
timezone *time.Location timezone *time.Location
ticker *time.Ticker ticker *time.Ticker
startTimer *time.Timer startTimer *time.Timer
jobs map[string]*job jobs []*Job
tickerDone chan bool tickerDone chan bool
interval time.Duration interval time.Duration
mux sync.RWMutex mux sync.RWMutex
@ -40,7 +36,7 @@ func New() *Cron {
return &Cron{ return &Cron{
interval: 1 * time.Minute, interval: 1 * time.Minute,
timezone: time.UTC, timezone: time.UTC,
jobs: map[string]*job{}, jobs: []*Job{},
tickerDone: make(chan bool), tickerDone: make(chan bool),
} }
} }
@ -82,23 +78,30 @@ func (c *Cron) MustAdd(jobId string, cronExpr string, run func()) {
// //
// cronExpr is a regular cron expression, eg. "0 */3 * * *" (aka. at minute 0 past every 3rd hour). // cronExpr is a regular cron expression, eg. "0 */3 * * *" (aka. at minute 0 past every 3rd hour).
// Check cron.NewSchedule() for the supported tokens. // Check cron.NewSchedule() for the supported tokens.
func (c *Cron) Add(jobId string, cronExpr string, run func()) error { func (c *Cron) Add(jobId string, cronExpr string, fn func()) error {
if run == nil { if fn == nil {
return errors.New("failed to add new cron job: run must be non-nil function") return errors.New("failed to add new cron job: fn must be non-nil function")
} }
c.mux.Lock()
defer c.mux.Unlock()
schedule, err := NewSchedule(cronExpr) schedule, err := NewSchedule(cronExpr)
if err != nil { if err != nil {
return fmt.Errorf("failed to add new cron job: %w", err) return fmt.Errorf("failed to add new cron job: %w", err)
} }
c.jobs[jobId] = &job{ c.mux.Lock()
defer c.mux.Unlock()
// remove previous (if any)
c.jobs = slices.DeleteFunc(c.jobs, func(j *Job) bool {
return j.Id() == jobId
})
// add new
c.jobs = append(c.jobs, &Job{
id: jobId,
fn: fn,
schedule: schedule, schedule: schedule,
run: run, })
}
return nil return nil
} }
@ -108,7 +111,13 @@ func (c *Cron) Remove(jobId string) {
c.mux.Lock() c.mux.Lock()
defer c.mux.Unlock() defer c.mux.Unlock()
delete(c.jobs, jobId) if c.jobs == nil {
return // nothing to remove
}
c.jobs = slices.DeleteFunc(c.jobs, func(j *Job) bool {
return j.Id() == jobId
})
} }
// RemoveAll removes all registered cron jobs. // RemoveAll removes all registered cron jobs.
@ -116,7 +125,7 @@ func (c *Cron) RemoveAll() {
c.mux.Lock() c.mux.Lock()
defer c.mux.Unlock() defer c.mux.Unlock()
c.jobs = map[string]*job{} c.jobs = []*Job{}
} }
// Total returns the current total number of registered cron jobs. // Total returns the current total number of registered cron jobs.
@ -127,6 +136,19 @@ func (c *Cron) Total() int {
return len(c.jobs) return len(c.jobs)
} }
// Jobs returns a shallow copy of the currently registered cron jobs.
func (c *Cron) Jobs() []*Job {
c.mux.RLock()
defer c.mux.RUnlock()
copy := make([]*Job, len(c.jobs))
for i, j := range c.jobs {
copy[i] = j
}
return copy
}
// Stop stops the current cron ticker (if not already). // Stop stops the current cron ticker (if not already).
// //
// You can resume the ticker by calling Start(). // You can resume the ticker by calling Start().
@ -200,7 +222,7 @@ func (c *Cron) runDue(t time.Time) {
for _, j := range c.jobs { for _, j := range c.jobs {
if j.schedule.IsDue(moment) { if j.schedule.IsDue(moment) {
go j.run() go j.Run()
} }
} }
} }

View File

@ -2,6 +2,7 @@ package cron
import ( import (
"encoding/json" "encoding/json"
"slices"
"testing" "testing"
"time" "time"
) )
@ -98,6 +99,11 @@ func TestCronAddAndRemove(t *testing.T) {
// try to remove non-existing (should be no-op) // try to remove non-existing (should be no-op)
c.Remove("missing") c.Remove("missing")
indexedJobs := make(map[string]*Job, len(c.jobs))
for _, j := range c.jobs {
indexedJobs[j.Id()] = j
}
// check job keys // check job keys
{ {
expectedKeys := []string{"test3", "test2", "test5"} expectedKeys := []string{"test3", "test2", "test5"}
@ -107,7 +113,7 @@ func TestCronAddAndRemove(t *testing.T) {
} }
for _, k := range expectedKeys { for _, k := range expectedKeys {
if c.jobs[k] == nil { if indexedJobs[k] == nil {
t.Fatalf("Expected job with key %s, got nil", k) t.Fatalf("Expected job with key %s, got nil", k)
} }
} }
@ -121,7 +127,7 @@ func TestCronAddAndRemove(t *testing.T) {
"test5": `{"minutes":{"1":{}},"hours":{"2":{}},"days":{"3":{}},"months":{"4":{}},"daysOfWeek":{"5":{}}}`, "test5": `{"minutes":{"1":{}},"hours":{"2":{}},"days":{"3":{}},"months":{"4":{}},"daysOfWeek":{"5":{}}}`,
} }
for k, v := range expectedSchedules { for k, v := range expectedSchedules {
raw, err := json.Marshal(c.jobs[k].schedule) raw, err := json.Marshal(indexedJobs[k].schedule)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -148,7 +154,7 @@ func TestCronMustAdd(t *testing.T) {
c.MustAdd("test2", "* * * * *", func() {}) c.MustAdd("test2", "* * * * *", func() {})
if _, ok := c.jobs["test2"]; !ok { if !slices.ContainsFunc(c.jobs, func(j *Job) bool { return j.Id() == "test2" }) {
t.Fatal("Couldn't find job test2") t.Fatal("Couldn't find job test2")
} }
} }
@ -208,6 +214,42 @@ func TestCronTotal(t *testing.T) {
} }
} }
func TestCronJobs(t *testing.T) {
t.Parallel()
c := New()
calls := ""
if err := c.Add("a", "1 * * * *", func() { calls += "a" }); err != nil {
t.Fatal(err)
}
if err := c.Add("b", "2 * * * *", func() { calls += "b" }); err != nil {
t.Fatal(err)
}
// overwrite
if err := c.Add("b", "3 * * * *", func() { calls += "b" }); err != nil {
t.Fatal(err)
}
jobs := c.Jobs()
if len(jobs) != 2 {
t.Fatalf("Expected 2 jobs, got %v", len(jobs))
}
for _, j := range jobs {
j.Run()
}
expectedCalls := "ab"
if calls != expectedCalls {
t.Fatalf("Expected %q calls, got %q", expectedCalls, calls)
}
}
func TestCronStartStop(t *testing.T) { func TestCronStartStop(t *testing.T) {
t.Parallel() t.Parallel()

25
tools/cron/job.go Normal file
View File

@ -0,0 +1,25 @@
package cron
// Job defines a single registered cron job.
type Job struct {
fn func()
schedule *Schedule
id string
}
// Id returns the cron job id.
func (j *Job) Id() string {
return j.id
}
// Expr returns the plain cron job schedule expression.
func (j *Job) Expr() string {
return j.schedule.rawExpr
}
// Run runs the cron job function.
func (j *Job) Run() {
if j.fn != nil {
j.fn()
}
}

49
tools/cron/job_test.go Normal file
View File

@ -0,0 +1,49 @@
package cron
import "testing"
func TestJobId(t *testing.T) {
expected := "test"
j := Job{id: expected}
if j.Id() != expected {
t.Fatalf("Expected job with id %q, got %q", expected, j.Id())
}
}
func TestJobExpr(t *testing.T) {
expected := "1 2 3 4 5"
s, err := NewSchedule(expected)
if err != nil {
t.Fatal(err)
}
j := Job{schedule: s}
if j.Expr() != expected {
t.Fatalf("Expected job with cron expression %q, got %q", expected, j.Expr())
}
}
func TestJobRun(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Shouldn't panic: %v", r)
}
}()
calls := ""
j1 := Job{}
j2 := Job{fn: func() { calls += "2" }}
j1.Run()
j2.Run()
expected := "2"
if calls != expected {
t.Fatalf("Expected calls %q, got %q", expected, calls)
}
}

View File

@ -35,6 +35,8 @@ type Schedule struct {
Days map[int]struct{} `json:"days"` Days map[int]struct{} `json:"days"`
Months map[int]struct{} `json:"months"` Months map[int]struct{} `json:"months"`
DaysOfWeek map[int]struct{} `json:"daysOfWeek"` DaysOfWeek map[int]struct{} `json:"daysOfWeek"`
rawExpr string
} }
// IsDue checks whether the provided Moment satisfies the current Schedule. // IsDue checks whether the provided Moment satisfies the current Schedule.
@ -130,6 +132,7 @@ func NewSchedule(cronExpr string) (*Schedule, error) {
Days: days, Days: days,
Months: months, Months: months,
DaysOfWeek: daysOfWeek, DaysOfWeek: daysOfWeek,
rawExpr: cronExpr,
}, nil }, nil
} }