Coder Social home page Coder Social logo

reugn / go-quartz Goto Github PK

View Code? Open in Web Editor NEW
1.6K 20.0 80.0 201 KB

Minimalist and zero-dependency scheduling library for Go

Home Page: https://pkg.go.dev/github.com/reugn/go-quartz/quartz

License: MIT License

Go 100.00%
scheduler job-scheduler quartz golang go job cron crontab zero-dependency jobqueue

go-quartz's Introduction

go-quartz

Build PkgGoDev Go Report Card codecov

A minimalistic and zero-dependency scheduling library for Go.

About

Inspired by the Quartz Java scheduler.

Library building blocks

Scheduler interface

type Scheduler interface {
	// Start starts the scheduler. The scheduler will run until
	// the Stop method is called or the context is canceled. Use
	// the Wait method to block until all running jobs have completed.
	Start(context.Context)

	// IsStarted determines whether the scheduler has been started.
	IsStarted() bool

	// ScheduleJob schedules a job using a specified trigger.
	ScheduleJob(jobDetail *JobDetail, trigger Trigger) error

	// GetJobKeys returns the keys of scheduled jobs.
	// For a job key to be returned, the job must satisfy all of the
	// matchers specified.
	// Given no matchers, it returns the keys of all scheduled jobs.
	GetJobKeys(...Matcher[ScheduledJob]) ([]*JobKey, error)

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

	// DeleteJob removes the job with the specified key from the
	// scheduler's execution queue.
	DeleteJob(jobKey *JobKey) error

	// PauseJob suspends the job with the specified key from being
	// executed by the scheduler.
	PauseJob(jobKey *JobKey) error

	// ResumeJob restarts the suspended job with the specified key.
	ResumeJob(jobKey *JobKey) error

	// Clear removes all of the scheduled jobs.
	Clear() error

	// Wait blocks until the scheduler stops running and all jobs
	// have returned. Wait will return when the context passed to
	// it has expired. Until the context passed to start is
	// cancelled or Stop is called directly.
	Wait(context.Context)

	// Stop shutdowns the scheduler.
	Stop()
}

Implemented Schedulers

  • StdScheduler

Trigger interface

type Trigger interface {
	// NextFireTime returns the next time at which the Trigger is scheduled to fire.
	NextFireTime(prev int64) (int64, error)

	// Description returns the description of the Trigger.
	Description() string
}

Implemented Triggers

  • CronTrigger
  • SimpleTrigger
  • RunOnceTrigger

Job interface

Any type that implements it can be scheduled.

type Job interface {
	// Execute is called by a Scheduler when the Trigger associated with this job fires.
	Execute(context.Context) error

	// Description returns the description of the Job.
	Description() string
}

Several common Job implementations can be found in the job package.

Cron expression format

Field Name Mandatory Allowed Values Allowed Special Characters
Seconds YES 0-59 , - * /
Minutes YES 0-59 , - * /
Hours YES 0-23 , - * /
Day of month YES 1-31 , - * ? /
Month YES 1-12 or JAN-DEC , - * /
Day of week YES 1-7 or SUN-SAT , - * ? /
Year NO empty, 1970- , - * /

Distributed mode

The scheduler can use its own implementation of quartz.JobQueue to allow state sharing.
An example implementation of the job queue using the file system as a persistence layer can be found here.

Logger

To set a custom logger, use the logger.SetDefault function.
The argument must implement the logger.Logger interface.

The following example shows how to disable library logs.

import "github.com/reugn/go-quartz/logger"

logger.SetDefault(logger.NewSimpleLogger(nil, logger.LevelOff))

Examples

package main

import (
	"context"
	"net/http"
	"time"

	"github.com/reugn/go-quartz/job"
	"github.com/reugn/go-quartz/quartz"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// create scheduler
	sched := quartz.NewStdScheduler()

	// async start scheduler
	sched.Start(ctx)

	// create jobs
	cronTrigger, _ := quartz.NewCronTrigger("1/5 * * * * *")
	shellJob := job.NewShellJob("ls -la")

	request, _ := http.NewRequest(http.MethodGet, "https://worldtimeapi.org/api/timezone/utc", nil)
	curlJob := job.NewCurlJob(request)

	functionJob := job.NewFunctionJob(func(_ context.Context) (int, error) { return 42, nil })

	// register jobs to scheduler
	sched.ScheduleJob(quartz.NewJobDetail(shellJob, quartz.NewJobKey("shellJob")),
		cronTrigger)
	sched.ScheduleJob(quartz.NewJobDetail(curlJob, quartz.NewJobKey("curlJob")),
		quartz.NewSimpleTrigger(time.Second*7))
	sched.ScheduleJob(quartz.NewJobDetail(functionJob, quartz.NewJobKey("functionJob")),
		quartz.NewSimpleTrigger(time.Second*5))

	// stop scheduler
	sched.Stop()

	// wait for all workers to exit
	sched.Wait(ctx)
}

More code samples can be found in the examples directory.

License

Licensed under the MIT License.

go-quartz's People

Contributors

alexandear avatar andreydomas avatar joaquinrovira avatar neomantra avatar oreillysean avatar reugn avatar rfyiamcool avatar tychoish avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-quartz's Issues

Is this distributed

Excuse me, the implementation of this library distributed scheduling is distributed?

Job with cron for particular time executes many times

I have a cron expression 4 47 11 5 10 ? 2023 and I expect the job will be executed once at 2023-10-05T11:47:04Z.
The job executes at given time but many times. Number of executions is various in different tries but usually is about one million times.
Version of code is v0.7.0
Go version is 1.20
Could you please fix (or explain) the issue?

The code to reproduce is

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/reugn/go-quartz/quartz"
)

type scheduleJob struct {
	scheduleID string
	counter    int
}

func (sj *scheduleJob) Description() string {
	return fmt.Sprintf("Schedule: %s", sj.scheduleID)
}

func (sj *scheduleJob) Key() int {
	return quartz.HashCode(sj.scheduleID)
}

func (sj *scheduleJob) Execute(ctx context.Context) {
	sj.counter++
	fmt.Printf("Execute %d time at %v\n", sj.counter, time.Now().UTC())
}

func main() {
	// at 2023-10-05T11:47:04Z
	cron := "4 47 11 5 10 ? 2023"
	ctx := context.Background()
	sched := quartz.NewStdScheduler()
	sched.Start(ctx)
	trigger, err := quartz.NewCronTrigger(cron)
	if err != nil {
		fmt.Printf("Bad expression: %s", err)
	}

	job := &scheduleJob{scheduleID: "aa"}
	err = sched.ScheduleJob(ctx, job, trigger)
	if err != nil {
		fmt.Printf("Failed to schedule: %s", err)
	}

	time.Sleep(5 * time.Minute)
}

Jobs are taken out of the queue when being re-scheduled

Description
I found this when stress testing quartz.
I saw this code takes a job being triggered out of the jobs queue in order to apply the next run time and reschedule.

When running a job, if the scheduler is running that part of the code and, in a different routine, we try to get that job (for deleting it, modifying or whatever) we get a job not found error.
The scheduler will push the job again to the queue with the new next run time, but the other routine could be creating a duplicate job because it could not find the current one.

The queue is just pushing jobs, so my understanding is that job duplication is possible.

For example, this simple code might be duplicating jobs if GetScheduledJob is called right at that part of the code mentioned above and the job is out of the queue for being rescheduled.

jobKey := quartz.NewJobKey("test")
jobDetail, err = scheduler.GetScheduledJob(jobKey)
if err != nil {
  // job was not found
  scheduler.ScheduleJob(quartz.NewJobDetail(NewTestJob("test"), jobKey), quartz.NewSimpleTrigger(time.Millisecond*JobCycle))
}

This is an example to recreate the issue: (at least I'm able to recreate in my system)

package main

import (
   "context"
   "fmt"
   "os"
   "time"

   "github.com/reugn/go-quartz/quartz"
)

const (
   JobCycle       = 10
   ChangeJobCycle = 30
   GetRetries     = 3
)

// --------------------- TestJob -----------------------------
var _ quartz.Job = &TestJob{}

type TestJob struct {
   Name string
}

func testJobDescription(name string) string {
   return fmt.Sprintf("testjob-%s", name)
}

func TestJobKey(name string) *quartz.JobKey {
   return quartz.NewJobKey(name)
}

func NewTestJob(name string) *TestJob {
   return &TestJob{
   	Name: name,
   }
}

func (j *TestJob) Execute(ctx context.Context) error {

   j.doTheJob(ctx)

   return nil
}

func (j *TestJob) Description() string {
   return testJobDescription(j.Name)
}

func (j *TestJob) doTheJob(_ context.Context) {
   fmt.Printf("This is the super job, my name is: %s\n", j.Name)
}

// --------------------- END TestJob -----------------------------

func main() {
   ctx, cancel := context.WithCancel(context.Background())
   defer cancel()
   scheduler := quartz.NewStdScheduler()

   scheduler.Start(ctx)

   name := "TEST-JOB"
   jobKey := quartz.NewJobKey(name)
   // Schedule the job.
   scheduler.ScheduleJob(quartz.NewJobDetail(NewTestJob(name), jobKey), quartz.NewSimpleTrigger(time.Millisecond*JobCycle))

   // This will change the job name every ChangeJobCycle milliseconds
   go changeJob(ChangeJobCycle, scheduler, name)

   scheduler.Wait(ctx)
}

func getScheduledJobWithRetries(scheduler quartz.Scheduler, key *quartz.JobKey, retries int) (quartz.Job, error) {
   var err error
   var jobDetail quartz.ScheduledJob
   for retries > 0 {
   	jobDetail, err = scheduler.GetScheduledJob(key)
   	if err == nil {
   		return jobDetail.JobDetail().Job(), err
   	}
   	// This should not be executed, as the job is scheduled initally and it's never intentionally deleted
   	fmt.Printf("RETRYING because of error: [%v]. Number of retries left: [%d]\n", err, retries)
   	time.Sleep(1 * time.Millisecond)
   	retries--
   }
   return nil, err
}

func changeJob(waitTime time.Duration, scheduler quartz.Scheduler, name string) {
   for {
   	time.Sleep(waitTime * time.Millisecond)
   	jobKey := quartz.NewJobKey(name)
   	job, err := getScheduledJobWithRetries(scheduler, jobKey, GetRetries)
   	if err != nil {
   		fmt.Printf("Could not get job after %d retries: %v\n", GetRetries, err)
   		os.Exit(2)
   	}
   	myjob, ok := job.(*TestJob)
   	if !ok {
   		fmt.Printf("The job %s was not a TestJob\n", job.Description())
   		return
   	}
   	fmt.Printf("\n*** I got the job with name: %s, changing the name ***\n\n", myjob.Name)
   	myjob.Name = time.Now().String()
   }
}

If you run that code you will see it retries calling GetScheduledJob, which should never happen because the job is scheduled and never deleted. The example just gets the job every ChangeJobCycle milliseconds and changes the Name of the job.

In order to see the retries without the rest of the noise you can do:

$ go run main.go | grep "RETRYING"

I'm getting a few lines:

RETRYING because of error: [job not found: default::TEST-JOB]. Number of retries left: [3]
RETRYING because of error: [job not found: default::TEST-JOB]. Number of retries left: [3]
RETRYING because of error: [job not found: default::TEST-JOB]. Number of retries left: [3]
RETRYING because of error: [job not found: default::TEST-JOB]. Number of retries left: [3]
RETRYING because of error: [job not found: default::TEST-JOB]. Number of retries left: [3]

I was able to recreate this in master and also in 0.9.0 (I haven't checked other versions)

Expected behaviour
I would expect to get the job even if it's being executed (or rescheduled) at that moment.

Suggestion
Apply next time to run without taking the job out of the queue. Maybe using a pointer for priority just like for job in scheduledJob and just changing the priority value.

No Restriction on Jobs with Same Key

Tried to insert 2 jobs with same key (int), both of them were inserted. But the GetScheduledJob by ID only returns one job.
I expect the second will override the first one.

Persistent Job store

Is there a storage layer for this ?

I was thinking of adding a JSON based storage and some sort of web based gui to configure that json.

essentially itโ€™s really a config .

then there is the question of if the config is changed via the Jain , how we update the running program without restarting .

what do you think ?

Do you have other plans for this area perhaps ?

NewFuncionJob not declared by package quartz

I have a problem
my editor prompts me that I do not have this method

2023-02-01_09-01

an error occurred when I tried to run the code

# command-line-arguments
./main.go:16:33: not enough arguments in call to sched.ScheduleJob
	have (*quartz.FunctionJob[int], *quartz.SimpleTrigger)
	want (context.Context, quartz.Job, quartz.Trigger)

the code

package main

import (
	"context"
	"time"

	"github.com/reugn/go-quartz/quartz"
)

func main() {
	ctx := context.Background()
	sched := quartz.NewStdScheduler()
	sched.Start(ctx)

	functionJob := quartz.NewFunctionJob(func(_ context.Context) (int, error) { return 42, nil })
	sched.ScheduleJob(functionJob, quartz.NewSimpleTrigger(time.Second*5))
	sched.Stop()
	sched.Wait(ctx)

}

OS: arch linux
go version: 1.19

Scheduler does not schedule jobs without giving any error

I am scheduling 4 jobs.
2 Jobs every 1 hour. (Job names: PLUGIN_DISCOVERY_JOB__18998__DISCOVERY__ and PLUGIN_DISCOVERY_JOB__19003__DISCOVERY__)
1 Job every 6 hours. (Job name: Sync_defs)
1 Job every 5 minutes. (Job name: MON_JOB__39f6f2a3-e0db-4036-90fe-74b67d1af4af__PING__5)

Following are the logs.


2021-09-29T23:45:00.022Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-29T23:45:00.029Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-29T23:45:00.029Z	INFO	portedapp/ported-app-processor.go:67	Job Name : MON_JOB__39f6f2a3-e0db-4036-90fe-74b67d1af4af__PING__5, Target : ping-adapter
2021-09-29T23:50:00.000Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-29T23:50:00.007Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-29T23:50:00.007Z	INFO	portedapp/ported-app-processor.go:67	Job Name : MON_JOB__39f6f2a3-e0db-4036-90fe-74b67d1af4af__PING__5, Target : ping-adapter
2021-09-29T23:55:00.020Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-29T23:55:00.026Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-29T23:55:00.027Z	INFO	portedapp/ported-app-processor.go:67	Job Name : MON_JOB__39f6f2a3-e0db-4036-90fe-74b67d1af4af__PING__5, Target : ping-adapter
2021-09-30T00:00:00.000Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-30T00:00:00.008Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-30T00:00:00.008Z	INFO	portedapp/ported-app-processor.go:67	Job Name : Sync_defs, Target : snmp-adapter
2021-09-30T00:06:00.023Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-30T00:06:00.029Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-30T00:06:00.029Z	INFO	portedapp/ported-app-processor.go:67	Job Name : PLUGIN_DISCOVERY_JOB__18998__DISCOVERY__, Target : snmp-adapter
2021-09-30T00:38:00.000Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-30T00:38:00.006Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-30T00:38:00.007Z	INFO	portedapp/ported-app-processor.go:67	Job Name : PLUGIN_DISCOVERY_JOB__19003__DISCOVERY__, Target : snmp-adapter
2021-09-30T01:00:00.001Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-30T01:00:00.008Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-30T01:00:00.008Z	INFO	portedapp/ported-app-processor.go:67	Job Name : MON_JOB__39f6f2a3-e0db-4036-90fe-74b67d1af4af__PING__5, Target : ping-adapter
2021-09-30T01:05:00.024Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-30T01:05:00.031Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-30T01:05:00.031Z	INFO	portedapp/ported-app-processor.go:67	Job Name : MON_JOB__39f6f2a3-e0db-4036-90fe-74b67d1af4af__PING__5, Target : ping-adapter
2021-09-30T01:06:00.024Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-30T01:06:00.029Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-30T01:06:00.029Z	INFO	portedapp/ported-app-processor.go:67	Job Name : PLUGIN_DISCOVERY_JOB__18998__DISCOVERY__, Target : snmp-adapter
2021-09-30T01:10:00.024Z	INFO	portedapp/ported-app-processor.go:36	Received Ported App Scheduling request..!!
2021-09-30T01:10:00.030Z	INFO	portedapp/ported-app-processor.go:66	Successfully scheduled Job..!! 
2021-09-30T01:10:00.030Z	INFO	portedapp/ported-app-processor.go:67	Job Name : MON_JOB__39f6f2a3-e0db-4036-90fe-74b67d1af4af__PING__5, Target : ping-adapter

Code snippet

type ScheduledJob struct {
	JobName string
	Config  string
}

func (sh *ScheduledJob) Description() string {
	return sh.JobName
}

func (sh *ScheduledJob) Key() int {
	return quartz.HashCode(sh.JobName)
}

func (sh *ScheduledJob) Execute() {
	logger.SugarLogger.Info("Received Ported App Scheduling request..!!")
	var request model.GenericRequest
	err := json.Unmarshal([]byte(sh.Config), &request)
	if err != nil {
		logger.SugarLogger.Error("Error while reading generic request message. Error: ", err)
	}
	metaData := request.MetaData
	metaData.Source = "Scheduler"

	jobConfig := request.JobConfiguration

	requestJson, err := json.Marshal(request)
	if err != nil {
		logger.SugarLogger.Error("Error while reading metadata message. Error: ", err)
	}

	// publish request json to app
	target := metaData.Target
	publishUrl := httpclient.GetPublishUrl(target)
	_, err = httpclient.PostMessage(string(requestJson), publishUrl)
	if err != nil {
		logger.SugarLogger.Error("Error while publish message to app. App Name : ", target, ", Error:", err)
	}

	logger.SugarLogger.Info("Successfully scheduled Job..!! ")
	logger.SugarLogger.Info("Job Name : ", jobConfig.JobName, ", Target : ", target)
}

Version: v0.3.6

In the above logs From 5 minute job not scheduled job from 2021-09-29T23:55:00.027Z to 2021-09-30T01:00:00.001Z Here 1 hour 5 minutes not scheduled any jobs.

Observation : Here request is not coming to Execute() method
It seems there might be issue in NextTriggerTime calculation. If you observer trigger times 23:55 and it is resumed back at 1:00.

DeleteJob issue, when deleting job already waiting for execution

H,

first of all: thanks for this library, it is great.

One remark regarding DeleteJob function:

DeleteJob removes a job from the queue, but doesn't cancel its execution, when it was the most recent job and it is already waiting for the timer event in the execution loop.
In this situation, after DeleteJob, the timer, set in startExecutionLoop is stil active and fires at calculated time (defined for in the meantime deleted job), and executes next current most recent job at wrong time.

In the example below:

  1. Two jobs are defined ("First job" with 15s interval and "Second job" with 30s interval)
  2. "First job" is most recent and starts timer in startExecutionLoop, which will be fired after ca. 15s
  3. After 5s, before timer fires, job "First job" will be deleted.
  4. Because DeleteJob doesn't do reset(), timer is still active and fires calling executeAndReschedule()
  5. executeAndReschedule gets most recent item from the heap (heap.Pop(sched.queue).(*item)), which is now "Second job" and executes it.
  6. In this case, the calculation in (st *SimpleTrigger) NextFireTime() is also wrong: it adds the value of interval to the calculated previous start time (priority). However, it should use now() instead.

Example, based on sample code:
func sampleScheduler(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()

sched := quartz.NewStdScheduler()

sched.Start(ctx)

_ = sched.ScheduleJob(ctx, &PrintJob{"First job"}, quartz.NewSimpleTrigger(time.Second*15))
_ = sched.ScheduleJob(ctx, &PrintJob{"Second job"}, quartz.NewSimpleTrigger(time.Second*30))


var wg1 sync.WaitGroup
wg1.Add(1)
go func() {
	defer wg1.Done()
	time.Sleep(time.Second * 5)
	fmt.Printf("delete job %d\n", quartz.HashCode("First job"))
	err := sched.DeleteJob(quartz.HashCode("First job"))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Job deleted")
	jobs := sched.GetJobKeys()
	for i, j := range jobs {
		fmt.Printf("%d job %d\n", i, j)
	}

}()

wg1.Wait()

jobs := sched.GetJobKeys()
for i, j := range jobs {
	fmt.Printf("%d job %d\n", i, j)
}

wg1.Add(1)
go func() {
	defer wg1.Done()
	fmt.Println("waiting 30 seconds...")
	time.Sleep(time.Second * 30)
}()

wg1.Wait()
fmt.Println("Stopping...")
sched.Stop()
sched.Wait(ctx)

}

Solution:
Call reset() in the DeleteJob function. However, sched.reset(ctx) requires context, which is not available in the DeleteJob
IMHO, this solution works properly:

func (sched *StdScheduler) DeleteJob2(ctx context.Context, key int) error {
sched.mtx.Lock()
defer sched.mtx.Unlock()

for i, item := range *sched.queue {
	if item.Job.Key() == key {
		sched.queue.Remove(i)
		sched.reset(ctx)
		return nil
	}
}

return errors.New("no Job with the given Key found")

}

day and month are case sensitive

Days and months are case sensitive in go-quartz while they are not in quartz java (and are capitalized in both documentations).

Example (every monday at 2am):
"0 0 2 ? * MON" <= invalid cron expression
"0 0 2 ? * Mon" <= OK
"0 0 2 ? * 2" <= OK

I expect it to be case insensitive

Day of week bug in CronTrigger.NextFireTime

CronTrigger.NextFireTime produces wrong fire time when day of week in cron expression is smaller than current day of week.

Example:

package main

import (
	"fmt"
	"github.com/reugn/go-quartz/quartz"
	"time"
)

func main() {
	const dateLayout = "Mon Jan 2 15:04:05 2006"
	var currTime int64 = 1615363744528064087 // time.Now().UnixNano()
	fmt.Printf("Current time: %s\n\n", time.Unix(currTime/int64(time.Second), 0).UTC().Format(dateLayout))
	for i, dayOfWeek := range []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} {
		for _, dayOfWeekFormat := range []string{fmt.Sprint(i), dayOfWeek} {
			cronExpr := fmt.Sprintf("0 0 0 * * %s", dayOfWeekFormat)
			fmt.Println("Cron expression:", cronExpr)
			cronTrigger, err := quartz.NewCronTrigger(cronExpr)
			if err != nil {
				panic(err)
			}
			nextFireTime, err := cronTrigger.NextFireTime(currTime)
			if err != nil {
				panic(err)
			}
			fmt.Printf("Next fire time: %s\n\n", time.Unix(nextFireTime/int64(time.Second), 0).UTC().Format(dateLayout))
		}
	}
}

prints:

Current time: Wed Mar 10 08:09:04 2021

Cron expression: 0 0 0 * * 0
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * Sun
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * 1
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * Mon
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * 2
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * Tue
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * 3
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * Wed
Next fire time: Wed Mar 17 00:00:00 2021

Cron expression: 0 0 0 * * 4
Next fire time: Thu Mar 11 00:00:00 2021

Cron expression: 0 0 0 * * Thu
Next fire time: Thu Mar 11 00:00:00 2021

Cron expression: 0 0 0 * * 5
Next fire time: Fri Mar 12 00:00:00 2021

Cron expression: 0 0 0 * * Fri
Next fire time: Fri Mar 12 00:00:00 2021

Cron expression: 0 0 0 * * 6
Next fire time: Sat Mar 13 00:00:00 2021

Cron expression: 0 0 0 * * Sat
Next fire time: Sat Mar 13 00:00:00 2021

NextFireTime question

var (
loc, _ = time.LoadLocation("Asia/Shanghai")
)
cTrigger, _ := quartz.NewCronTriggerWithLoc("0 * * ? * 4", loc)
nextTime, _ := cTrigger.NextFireTime(time.Now().UnixNano())

Today is 2022-11-09 17:05:00, and the next execution time is 2022-11-16 17:05:00, shouldn't it be 2022-11-09 17:06:00?

CronTrigger.NextFireTime() can return invalid results

Issue

TL;DR: The current CronTrigger.NextFireTime() method needs to be fixed.

The current implementation of go-quartz library shows a subtle error that leads to bad scheduling. I have tested the library using the Cronmaker tool and compared the expected dates with the scheduled dates calculated by go-quartz for some tests. As a result, I have detected that the test cases are incorrect, and the current implementation skips some dates. This error is subtle, and the rest of the dates are correct. Therefore, the current tests are misleading and should be corrected with some verifiable reference values.

All experiments start from the reference date Sat Apr 22 12:00:00 2023 and calculate the next 50 scheduled dates. These are then contrasted against the output of Cronmaker.

TestCronExpression8

Taken directly from cron_test.go. The test begins from the initial date of Mon Apr 15 18:00:00 2019. However, for demonstration purposes I have changed the reference date to the one described above. The cron expression is */3 */51 */12 */2 */4 ? *. Includes means that only the following are valid values:

Chunk Valid values
Seconds 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57
Minutes 0, 51
Hours 0, 12
Day of Month 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31
Month Jan, May, Sep

As we can see below, the current implementation skips from Sat Apr 22 12:00:00 2023 to Tue May 23 12:00:03 2023. It misses days 1-21 of May. The error is subtle and - beyond the initial miscalculation - the rest are correct. This means that the current tests are misleading and should be corrected with some verifiable reference values. The current ones seem to have been set incorrectly.

Current Cronmaker
Tue May 23 12:00:03 2023
Tue May 23 12:00:06 2023
Tue May 23 12:00:09 2023
Tue May 23 12:00:12 2023
Tue May 23 12:00:15 2023
Tue May 23 12:00:18 2023
Tue May 23 12:00:21 2023
Tue May 23 12:00:24 2023
Tue May 23 12:00:27 2023
Tue May 23 12:00:30 2023
Tue May 23 12:00:33 2023
Tue May 23 12:00:36 2023
Tue May 23 12:00:39 2023
Tue May 23 12:00:42 2023
Tue May 23 12:00:45 2023
Tue May 23 12:00:48 2023
Tue May 23 12:00:51 2023
Tue May 23 12:00:54 2023
Tue May 23 12:00:57 2023
Tue May 23 12:51:00 2023
Tue May 23 12:51:03 2023
Tue May 23 12:51:06 2023
Tue May 23 12:51:09 2023
Tue May 23 12:51:12 2023
Tue May 23 12:51:15 2023
Tue May 23 12:51:18 2023
Tue May 23 12:51:21 2023
Tue May 23 12:51:24 2023
Tue May 23 12:51:27 2023
Tue May 23 12:51:30 2023
Tue May 23 12:51:33 2023
Tue May 23 12:51:36 2023
Tue May 23 12:51:39 2023
Tue May 23 12:51:42 2023
Tue May 23 12:51:45 2023
Tue May 23 12:51:48 2023
Tue May 23 12:51:51 2023
Tue May 23 12:51:54 2023
Tue May 23 12:51:57 2023
Thu May 25 00:00:00 2023
Thu May 25 00:00:03 2023
Thu May 25 00:00:06 2023
Thu May 25 00:00:09 2023
Thu May 25 00:00:12 2023
Thu May 25 00:00:15 2023
Thu May 25 00:00:18 2023
Thu May 25 00:00:21 2023
Thu May 25 00:00:24 2023
Thu May 25 00:00:27 2023
Thu May 25 00:00:30 2023
Mon May 1 00:00:00 2023
Mon May 1 00:00:03 2023
Mon May 1 00:00:06 2023
Mon May 1 00:00:09 2023
Mon May 1 00:00:12 2023
Mon May 1 00:00:15 2023
Mon May 1 00:00:18 2023
Mon May 1 00:00:21 2023
Mon May 1 00:00:24 2023
Mon May 1 00:00:27 2023
Mon May 1 00:00:30 2023
Mon May 1 00:00:33 2023
Mon May 1 00:00:36 2023
Mon May 1 00:00:39 2023
Mon May 1 00:00:42 2023
Mon May 1 00:00:45 2023
Mon May 1 00:00:48 2023
Mon May 1 00:00:51 2023
Mon May 1 00:00:54 2023
Mon May 1 00:00:57 2023
Mon May 1 00:51:00 2023
Mon May 1 00:51:03 2023
Mon May 1 00:51:06 2023
Mon May 1 00:51:09 2023
Mon May 1 00:51:12 2023
Mon May 1 00:51:15 2023
Mon May 1 00:51:18 2023
Mon May 1 00:51:21 2023
Mon May 1 00:51:24 2023
Mon May 1 00:51:27 2023
Mon May 1 00:51:30 2023
Mon May 1 00:51:33 2023
Mon May 1 00:51:36 2023
Mon May 1 00:51:39 2023
Mon May 1 00:51:42 2023
Mon May 1 00:51:45 2023
Mon May 1 00:51:48 2023
Mon May 1 00:51:51 2023
Mon May 1 00:51:54 2023
Mon May 1 00:51:57 2023
Mon May 1 12:00:00 2023
Mon May 1 12:00:03 2023
Mon May 1 12:00:06 2023
Mon May 1 12:00:09 2023
Mon May 1 12:00:12 2023
Mon May 1 12:00:15 2023
Mon May 1 12:00:18 2023
Mon May 1 12:00:21 2023
Mon May 1 12:00:24 2023
Mon May 1 12:00:27 2023

Every 15 seconds on Weekdays

This is the cron expression where I found the issue. I am using go-quartz to schedule weekly reservations automatically. The demand is quite high so the availability runs out quickly. The app never seems to trigger on time. It has left me more often than not without some of the time slots I selected. The cron expression is the following, */15 * * ? * 1-7. I slightly modified the cron for this description to make the error more noticeable. In this experiment the error is unmistakable. Instead of every 15 seconds during the day, it skips to the next day every minute.

Current Cronmaker
Sat Apr 22 12:00:00 2023
Sat Apr 22 12:00:15 2023
Sat Apr 22 12:00:30 2023
Sat Apr 22 12:00:45 2023
Sun Apr 23 12:00:00 2023
Sun Apr 23 12:00:15 2023
Sun Apr 23 12:00:30 2023
Sun Apr 23 12:00:45 2023
Mon Apr 24 12:00:00 2023
Mon Apr 24 12:00:15 2023
Mon Apr 24 12:00:30 2023
Mon Apr 24 12:00:45 2023
Tue Apr 25 12:00:00 2023
Tue Apr 25 12:00:15 2023
Tue Apr 25 12:00:30 2023
Tue Apr 25 12:00:45 2023
Wed Apr 26 12:00:00 2023
Wed Apr 26 12:00:15 2023
Wed Apr 26 12:00:30 2023
Wed Apr 26 12:00:45 2023
Thu Apr 27 12:00:00 2023
Thu Apr 27 12:00:15 2023
Thu Apr 27 12:00:30 2023
Thu Apr 27 12:00:45 2023
Fri Apr 28 12:00:00 2023
Fri Apr 28 12:00:15 2023
Fri Apr 28 12:00:30 2023
Fri Apr 28 12:00:45 2023
Sat Apr 29 12:00:00 2023
Sat Apr 29 12:00:15 2023
Sat Apr 29 12:00:30 2023
Sat Apr 29 12:00:45 2023
Sun Apr 30 12:00:00 2023
Sun Apr 30 12:00:15 2023
Sun Apr 30 12:00:30 2023
Sun Apr 30 12:00:45 2023
Mon May 1 12:00:00 2023
Mon May 1 12:00:15 2023
Mon May 1 12:00:30 2023
Mon May 1 12:00:45 2023
Tue May 2 12:00:00 2023
Tue May 2 12:00:15 2023
Tue May 2 12:00:30 2023
Tue May 2 12:00:45 2023
Wed May 3 12:00:00 2023
Wed May 3 12:00:15 2023
Wed May 3 12:00:30 2023
Wed May 3 12:00:45 2023
Thu May 4 12:00:00 2023
Thu May 4 12:00:15 2023
Thu May 4 12:00:30 2023
Sat Apr 22 12:00:15 2023
Sat Apr 22 12:00:30 2023
Sat Apr 22 12:00:45 2023
Sat Apr 22 12:01:00 2023
Sat Apr 22 12:01:15 2023
Sat Apr 22 12:01:30 2023
Sat Apr 22 12:01:45 2023
Sat Apr 22 12:02:00 2023
Sat Apr 22 12:02:15 2023
Sat Apr 22 12:02:30 2023
Sat Apr 22 12:02:45 2023
Sat Apr 22 12:03:00 2023
Sat Apr 22 12:03:15 2023
Sat Apr 22 12:03:30 2023
Sat Apr 22 12:03:45 2023
Sat Apr 22 12:04:00 2023
Sat Apr 22 12:04:15 2023
Sat Apr 22 12:04:30 2023
Sat Apr 22 12:04:45 2023
Sat Apr 22 12:05:00 2023
Sat Apr 22 12:05:15 2023
Sat Apr 22 12:05:30 2023
Sat Apr 22 12:05:45 2023
Sat Apr 22 12:06:00 2023
Sat Apr 22 12:06:15 2023
Sat Apr 22 12:06:30 2023
Sat Apr 22 12:06:45 2023
Sat Apr 22 12:07:00 2023
Sat Apr 22 12:07:15 2023
Sat Apr 22 12:07:30 2023
Sat Apr 22 12:07:45 2023
Sat Apr 22 12:08:00 2023
Sat Apr 22 12:08:15 2023
Sat Apr 22 12:08:30 2023
Sat Apr 22 12:08:45 2023
Sat Apr 22 12:09:00 2023
Sat Apr 22 12:09:15 2023
Sat Apr 22 12:09:30 2023
Sat Apr 22 12:09:45 2023
Sat Apr 22 12:10:00 2023
Sat Apr 22 12:10:15 2023
Sat Apr 22 12:10:30 2023
Sat Apr 22 12:10:45 2023
Sat Apr 22 12:11:00 2023
Sat Apr 22 12:11:15 2023
Sat Apr 22 12:11:30 2023
Sat Apr 22 12:11:45 2023
Sat Apr 22 12:12:00 2023
Sat Apr 22 12:12:15 2023
Sat Apr 22 12:12:30 2023

some questions about NextFireTime

I use the test code:

now := time.Now()

fmt.Println(utils.TimeFormat(now))

loc, _ := time.LoadLocation("Asia/Shanghai")

ctrigger1, _ := quartz.NewCronTriggerWithLoc("0 * * ? * *", loc)

ne1, _ := ctrigger1.NextFireTime(now.UnixNano())

fmt.Println(utils.TimeFormat(time.Unix(ne1/int64(time.Second), 0)))

ctrigger2, _ := quartz.NewCronTriggerWithLoc("0 * * ? * MON", loc)

ne2, _ := ctrigger2.NextFireTime(now.UnixNano())

fmt.Println(utils.TimeFormat(time.Unix(ne2/int64(time.Second), 0)))

ctrigger3, _ := quartz.NewCronTriggerWithLoc("0 * * ? * 1", loc)

ne3, _ := ctrigger3.NextFireTime(now.UnixNano())

fmt.Println(utils.TimeFormat(time.Unix(ne3/int64(time.Second), 0)))

then I got:

2022-06-27 18:32:23
2022-06-27 18:33:00
2022-07-04 18:32:00
2022-07-03 18:32:00

the third and the fourth output are not as I expected, is this correct?

RunOnceTrigger cannot be properly unmarshalled

Expected Behaviour:
When using runOnceTrigger the job should fire after the given delay and expire immediately.

Current behaviour:
When using runOnceTrigger with the delay of 5secs, the job is never expired.

Steps to reproduce:
Use the following code to schedule the job in the jobQueue example given in the repository.

if err := scheduler.ScheduleJob(jobDetail1, quartz.NewRunOnceTrigger(5*time.Second)); err != nil {
	logger.Warnf("Failed to schedule job: %s", jobDetail1.JobKey())
}

Possible Cause:
The RunOnceTrigger has the expired field as private, because of which we cannot create expired RunOnceTriggers. When using persistent storage, the user needs to serialise and recreate the trigger. Allowing users to create expired RunOnceTrigger can solve the problem.

Cannot use alternative Logger implementations

From the code in default_logger.go it seems you had a good intention to allow users to replace the default logger with their own implementation of the Logger interface. However, doing so will panic with:

sync/atomic: store of inconsistently typed value into Value

The reason is that atomic.Value doesn't work quite like you seem to think (I agree it's not entirely intuitive). From the documentation of Value.Store:

// Store sets the value of the Value v to val.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).

What this means is that once you set the value to an instance of SimpleLogger in your init() function, the value will only accept SimpleLogger instances. This means that in my case I cannot install my own implementation of the Logger interface.

How to fix this? I would suggest using a simple Logger var instead of atomic.Value. This does mean no thread safety, but is it really necessary here? Users usually set up logging when they start the application and don't change it later. Most logging libraries are not thread safe in this way.

If you do want to insist on thread safety (perhaps to ensure behavior doesn't change for users who expect it) I would suggest wrapping access to the var in a simple mutex.

I'd be happy to submit a PR for either of these solutions, just let me know. Thanks for a very useful library!

How can I identify a Job from JobKey?

There's not any method giving name of JobKey. So I'm not able to implement JobQueue.Remove(JobKey) and JobQueue.Push(ScheduledJob) methods for customizing a persistent JobQueue.
And another question: I think a persistent JobQueue based on widely used database example is really required. I'm using JobQueue in production, but I'm not very certain how implement this properly. Thanks.

ability to Start after all jobs were added

First of all, thanks for the library.

I see that it is required to call Start before ScheduleJob. I had a few experiments trying to schedule before calling start and they were unsuccessful. And this leads me to a question: what is required to be changed to allow scheduling before the start? The reason for that would be that I want all components to be composed, then started to produce some work at a predictable point in time. So I have this central structure that is responsible for the Start moment.

CronTrigger use only UTC

        cronTime := "0 30 21 * * ?"
	ct, err := quartz.NewCronTrigger(cronTime)
	if err != nil {
		return err
	}

	currentTime := time.Now().UnixNano()
	fmt.Println("CurrentTime:", time.Unix(currentTime/int64(time.Second), 0).String())

	nft, err := ct.NextFireTime(currentTime)
	if err != nil {
		return err
	}

         fmt.Println("NextFireTime:", time.Unix(nft/int64(time.Second), 0).String())

Result:

CurrentTime: 2022-03-23 09:09:25 +0300 MSK
NextFireTime: 2022-03-24 00:30:00 +0300 MSK

Looks like cron trigger set job time in UTC. Is there some option to set job in local time?

Trigger start time functionality suggestion

The start time can be set in other packages (java, .net). We should be able to do that with this package as well.

Edit: I guess you can do it with NextFireTime. It didn't seem very understandable to me.

Some of the cron expressions are not supported

All cron expressions not supported

Example

package main

import (
	"fmt"
	"strconv"
	"time"

	"github.com/reugn/go-quartz/quartz"
)


type ScheduledJob struct {
	jobid  int
	config string
}


func NewScheduledJob(jobid int, config string) *ScheduledJob {
	var job ScheduledJob
	job.jobid = jobid
	job.config = config
	return &job
}

func (sh *ScheduledJob) Description() string {
	return sh.config
}

func (sh *ScheduledJob) Key() int {
	return sh.jobid
}

func (sh *ScheduledJob) Execute() {
	fmt.Printf("Job id %s Config %s", strconv.Itoa(sh.jobid), sh.config)
	time.Sleep(2 * time.Second)
}

func main() {
	sched := quartz.NewStdScheduler()
	sched.Start()

	cronTrigger, err := quartz.NewCronTrigger("0 12 */2 * * ? *")

	if err != nil {
		fmt.Println(err)
		return
	}
	job := NewScheduledJob(10, "My config")
	sched.ScheduleJob(job, cronTrigger)

	<-make(chan int)
}

Error

Invalid cron expression: Cron step min/max validation error

Above cron expression means every 2 hours

Additional cron expressions that are fail

Every 3 hours : 0 24 */3 * * ? *
Every 4 hours : 0 43 */5 * * ? *
Every 6 hours : 0 54 */6 * * ? * 
Every 12 hours : 0 51 */12 * * ? *
Every 2 days 3rd hour: 0 0 3 */2 * ? *
Every 6 days 10th hour: 0 0 10 */6 * ? *
Every Month 2nd Monday 6th Hour: 0 0 6 ? * 2#2 *
Every Month 4th Tuesday 15th hour: 0 0 15 ? * 3#4 *

etc

add support for contexts in scheduler

It would be nice to be able to have a context that is canceled when the job finishes executing to make it difficult/impossible for jobs to spawn go routines that last longer than the job execution itself. This change would also allow quartz schedulers to integrate with existing process/task lifescale mechanisms that use contexts.

I've prototyped this in this branch: https://github.com/tychoish/go-quartz/tree/dev-contexts.

If this is something that you/the project is interested in, I would be willing to carry this over the line.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.