1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-02-14 08:50:24 +02:00

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 (
"errors"
"fmt"
"slices"
"sync"
"time"
)
type job struct {
schedule *Schedule
run func()
}
// Cron is a crontab-like struct for tasks/jobs scheduling.
type Cron struct {
timezone *time.Location
ticker *time.Ticker
startTimer *time.Timer
jobs map[string]*job
jobs []*Job
tickerDone chan bool
interval time.Duration
mux sync.RWMutex
@ -40,7 +36,7 @@ func New() *Cron {
return &Cron{
interval: 1 * time.Minute,
timezone: time.UTC,
jobs: map[string]*job{},
jobs: []*Job{},
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).
// Check cron.NewSchedule() for the supported tokens.
func (c *Cron) Add(jobId string, cronExpr string, run func()) error {
if run == nil {
return errors.New("failed to add new cron job: run must be non-nil function")
func (c *Cron) Add(jobId string, cronExpr string, fn func()) error {
if fn == nil {
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)
if err != nil {
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,
run: run,
}
})
return nil
}
@ -108,7 +111,13 @@ func (c *Cron) Remove(jobId string) {
c.mux.Lock()
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.
@ -116,7 +125,7 @@ func (c *Cron) RemoveAll() {
c.mux.Lock()
defer c.mux.Unlock()
c.jobs = map[string]*job{}
c.jobs = []*Job{}
}
// Total returns the current total number of registered cron jobs.
@ -127,6 +136,19 @@ func (c *Cron) Total() int {
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).
//
// You can resume the ticker by calling Start().
@ -200,7 +222,7 @@ func (c *Cron) runDue(t time.Time) {
for _, j := range c.jobs {
if j.schedule.IsDue(moment) {
go j.run()
go j.Run()
}
}
}

View File

@ -2,6 +2,7 @@ package cron
import (
"encoding/json"
"slices"
"testing"
"time"
)
@ -98,6 +99,11 @@ func TestCronAddAndRemove(t *testing.T) {
// try to remove non-existing (should be no-op)
c.Remove("missing")
indexedJobs := make(map[string]*Job, len(c.jobs))
for _, j := range c.jobs {
indexedJobs[j.Id()] = j
}
// check job keys
{
expectedKeys := []string{"test3", "test2", "test5"}
@ -107,7 +113,7 @@ func TestCronAddAndRemove(t *testing.T) {
}
for _, k := range expectedKeys {
if c.jobs[k] == nil {
if indexedJobs[k] == nil {
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":{}}}`,
}
for k, v := range expectedSchedules {
raw, err := json.Marshal(c.jobs[k].schedule)
raw, err := json.Marshal(indexedJobs[k].schedule)
if err != nil {
t.Fatal(err)
}
@ -148,7 +154,7 @@ func TestCronMustAdd(t *testing.T) {
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")
}
}
@ -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) {
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"`
Months map[int]struct{} `json:"months"`
DaysOfWeek map[int]struct{} `json:"daysOfWeek"`
rawExpr string
}
// IsDue checks whether the provided Moment satisfies the current Schedule.
@ -130,6 +132,7 @@ func NewSchedule(cronExpr string) (*Schedule, error) {
Days: days,
Months: months,
DaysOfWeek: daysOfWeek,
rawExpr: cronExpr,
}, nil
}