Quartz scheduler

Content:

  1. Introduction
  2. Background
  3. Config
  4. Setup
  5. Run
  6. Other
  7. Advanced
  8. Alternatives
  9. Conclusion

Introduction:

This article will provide an intro to the Quartz scheduler.

We will cover both Quartz for JVM languages (Java, Kotlin, Scala, Groovy etc.) and Quartz.NET for CLR languages (C#, VB.NET, F# etc.).

Example code will be done in Java and C# respectively.

Basic Java/C# skills and an understanding of scheduled job concept are required.

Background:

Job scheduling aka the ability to get something run at a specific time in the future is an important feature in many systems.

Linux comes with cron. Windows comes with Task Scheduler. VMS comes with batch system.

The above all comes with the operating systems and are able to execute any shell command or program executable.

Quartz is different it is a scheduling system embedded in application code able to execute code within the application.

Quartz was created in 2005. The company behind was acquired by Terracotta in 2009. Terracotta was acquired by Software AG in 2011.

Quartz was ported to .NET (as Quartz.NET) in 2011.

Config:

The scheduling info can be store in memory or in files or in a database.

The example will use a database. This is probably the most realistic as durability/persistence is desireable for scheduling in a real application.

For the example we will use MySQL for Quartz and MS SQLServer for Quartz.NET.

The database tables must be created before use.

Database specific DDL for Quartz are available at GitHub.

Database specific DDL for Quartz.NET are available at GitHub.

org.quartz.scheduler.instanceName = DemoScheduler
# threadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
# jobStore
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = qrtz_
org.quartz.jobStore.dataSource = mysqlds
org.quartz.jobStore.isClustered = true
# dataSource
org.quartz.dataSource.mysqlds.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.mysqlds.URL = jdbc:mysql://localhost/test
org.quartz.dataSource.mysqlds.user = root
org.quartz.dataSource.mysqlds.password = hemmeligt
quartz.scheduler.instanceName = DemoScheduler
# threadPool
quartz.threadPool.threadCount = 5
# jobStore
quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz
quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz
quartz.jobStore.tablePrefix = QRTZ_
quartz.jobStore.dataSource = sqlsrvds
quartz.jobStore.clustered = true
quartz.serializer.type = json
# dataStore
quartz.dataSource.sqlsrvds.connectionString = Server=localhost;Database=Test;Integrated Security=true;Encrypt=false
quartz.dataSource.sqlsrvds.provider = SqlServer

Configuration supports clustered application aka multiple instances of the applications running in parallel.

We will see separate Setup, Run, List and Clean programs. This is just for simplicity. In a real world application all functionality would likely be inside the same program.

Setup:

We will see how to setup jobs running:

There are many other possibilities, but these are probably some of the more common on business applications.

package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class Ping implements Job {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    @Override
    public void execute(JobExecutionContext ctx) throws JobExecutionException {
        String p1 = ctx.getJobDetail().getJobDataMap().getString("p1");
        System.out.printf("%-20s %s : hi\n", p1, df.format(new Date()));
    }
}
package demo.sched;

import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;

import org.quartz.impl.StdSchedulerFactory;

import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.JobBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.TriggerBuilder.*;

public class Setup {
    public static void runNow(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, String... p) throws SchedulerException {
        Trigger trigger = newTrigger().withIdentity(nam + "_trigger", grp).startNow().build();
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.scheduleJob(job, trigger);
    }
    public static void runInterval(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, int secs, String... p) throws SchedulerException {
        Trigger trigger = newTrigger().withIdentity(nam + "_trigger", grp).startNow().withSchedule(simpleSchedule().withIntervalInSeconds(secs).repeatForever()).build();
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.scheduleJob(job, trigger);
    }
    public static void runIntervalLimited(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, int secs, int n, String... p) throws SchedulerException {
        Trigger trigger = newTrigger().withIdentity(nam + "_trigger", grp).startNow().withSchedule(simpleSchedule().withIntervalInSeconds(secs).withRepeatCount(n - 1)).build();
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.scheduleJob(job, trigger);
    }
    public static void runEveryDay(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, int hr, int min, String... p) throws SchedulerException {
        Trigger trigger = newTrigger().withIdentity(nam + "_trigger", grp).startNow().withSchedule(dailyAtHourAndMinute(hr, min)).build();  
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.scheduleJob(job, trigger);
    }
    public static void runEveryWeek(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, int day, int hr, int min, String... p) throws SchedulerException {
        Trigger trigger = newTrigger().withIdentity(nam + "_trigger", grp).startNow().withSchedule(weeklyOnDayAndHourAndMinute(day, hr, min)).build();  
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.scheduleJob(job, trigger);
    }
    public static void runEveryMonth(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, int day, int hr, int min, String... p) throws SchedulerException {
        Trigger trigger = newTrigger().withIdentity(nam + "_trigger", grp).startNow().withSchedule(monthlyOnDayAndHourAndMinute(day, hr, min)).build(); 
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.scheduleJob(job, trigger);
    }
    public static void main(String[] args) throws SchedulerException, InterruptedException {
        StdSchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();
        runNow(sched, "demo", "ping", Ping.class, "Now");
        runInterval(sched, "demo", "sping", Ping.class, 10, "Interval");
        runIntervalLimited(sched, "demo", "slping", Ping.class, 10, 3, "Interval Limited");
        runEveryDay(sched, "demo", "dping", Ping.class, 23, 0, "Every Day");
        sched.shutdown();
    }
}
using System;
using System.Threading.Tasks;

using Quartz;

namespace Jobs
{
    public class Ping : IJob
    {
        Task IJob.Execute(IJobExecutionContext ctx)
        {
            string p1 = ctx.JobDetail.JobDataMap.GetString("p1");
            Console.WriteLine("{0,-20} {1} : Hi", p1, DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
            return Task.CompletedTask;
        }
    }
}
using System;
using System.Threading.Tasks;

using Quartz;
using Quartz.Impl;

using Jobs;

namespace Setup
{
    public class Program
    {
        public static void RunNow(IScheduler sched, string grp, string nam, Type jobclz, params string[] p)
        {
            ITrigger trigger = TriggerBuilder.Create().WithIdentity(nam + "_trigger", grp).StartNow().Build();
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            DateTimeOffset dummy = sched.ScheduleJob(job, trigger).Result;
        }
        public static void RunInterval(IScheduler sched, string grp, string nam, Type jobclz, int secs, params string[] p)
        {
            ITrigger trigger = TriggerBuilder.Create().WithIdentity(nam + "_trigger", grp).StartNow().WithSimpleSchedule(s => s.WithIntervalInSeconds(secs).RepeatForever()).Build();
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            DateTimeOffset dummy = sched.ScheduleJob(job, trigger).Result;
        }
        public static void RunIntervalLimited(IScheduler sched, string grp, string nam, Type jobclz, int secs, int n, params string[] p)
        {
            ITrigger trigger = TriggerBuilder.Create().WithIdentity(nam + "_trigger", grp).StartNow().WithSimpleSchedule(s => s.WithIntervalInSeconds(secs).WithRepeatCount(n - 1)).Build();
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            DateTimeOffset dummy = sched.ScheduleJob(job, trigger).Result;
        }
        public static void RunEveryDay(IScheduler sched, string grp, string nam, Type jobclz, int hr, int min, params string[] p)
        {
            ITrigger trigger = TriggerBuilder.Create().WithIdentity(nam + "_trigger", grp).StartNow().WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(hr, min)).Build();
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            DateTimeOffset dummy = sched.ScheduleJob(job, trigger).Result;
        }
        public static void RunEveryWeek(IScheduler sched, string grp, string nam, Type jobclz, DayOfWeek day, int hr, int min, params string[] p)
        {
            ITrigger trigger = TriggerBuilder.Create().WithIdentity(nam + "_trigger", grp).StartNow().WithSchedule(CronScheduleBuilder.WeeklyOnDayAndHourAndMinute(day, hr, min)).Build();
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            DateTimeOffset dummy = sched.ScheduleJob(job, trigger).Result;
        }
        public static void RunEveryMonth(IScheduler sched, string grp, string nam, Type jobclz, int day, int hr, int min, params string[] p)
        {
            ITrigger trigger = TriggerBuilder.Create().WithIdentity(nam + "_trigger", grp).StartNow().WithSchedule(CronScheduleBuilder.MonthlyOnDayAndHourAndMinute(day, hr, min)).Build();
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            DateTimeOffset dummy = sched.ScheduleJob(job, trigger).Result;
        }
        public static void Main(string[] args)
        {
            StdSchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sched = sf.GetScheduler().Result;
            RunNow(sched, "demo", "ping", typeof(Ping), "Now");
            RunInterval(sched, "demo", "sping", typeof(Ping), 10, "Interval");
            RunIntervalLimited(sched, "demo", "slping", typeof(Ping), 10, 3, "Interval Limited");
            RunEveryDay(sched, "demo", "dping", typeof(Ping), 23, 0, "Every Day");
            sched.Shutdown();
        }
    }
}

Run:

package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class Ping implements Job {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    @Override
    public void execute(JobExecutionContext ctx) throws JobExecutionException {
        String p1 = ctx.getJobDetail().getJobDataMap().getString("p1");
        System.out.printf("%-20s %s : hi\n", p1, df.format(new Date()));
    }
}
package demo.sched;

import org.quartz.Scheduler;
import org.quartz.SchedulerException;

import org.quartz.impl.StdSchedulerFactory;

public class Run {
    public static void main(String[] args) throws SchedulerException, InterruptedException {
        StdSchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();
        sched.start();
        Thread.sleep(60 * 1000);
        sched.shutdown();
    }
}
using System;
using System.Threading.Tasks;

using Quartz;

namespace Jobs
{
    public class Ping : IJob
    {
        Task IJob.Execute(IJobExecutionContext ctx)
        {
            string p1 = ctx.JobDetail.JobDataMap.GetString("p1");
            Console.WriteLine("{0,-20} {1} : Hi", p1, DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
            return Task.CompletedTask;
        }
    }
}

Program.cs:

using System;
using System.Threading;
using System.Threading.Tasks;

using Quartz;
using Quartz.Impl;

namespace Run
{
    public class Program
    {
        public static void Main(string[] args)
        {
            StdSchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sched = sf.GetScheduler().Result;
            sched.Start();
            Thread.Sleep(60 * 1000);
            sched.Shutdown();
        }
    }
}

Other:

List:

package demo.sched;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;

public class List {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    private static String tf(Date d) {
        if(d != null) {
            return df.format(d);
        } else {
            return "                    ";
        }
    }
    public static void main(String[] args) throws SchedulerException, InterruptedException {
        StdSchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();
        for(String grp : sched.getJobGroupNames()) {
            for(JobKey job : sched.getJobKeys(GroupMatcher.jobGroupEquals(grp))) {
                for(Trigger trig : sched.getTriggersOfJob(job)) {
                    System.out.printf("grp=%-10s job=%-20s trig=%-20s class=%-30s prev=%s next=%s\n", grp, job.getName(), trig.getKey(), sched.getJobDetail(job).getJobClass().getName(), tf(trig.getPreviousFireTime()), tf(trig.getNextFireTime()));
                }
            }
        }
        sched.shutdown();
    }
}
using System;
using System.Threading.Tasks;

using Quartz;
using Quartz.Impl;
using Quartz.Impl.Matchers;

namespace List
{
    public class Program
    {
        public static string TF(DateTimeOffset? dto)
        {
            if(dto.HasValue)
            {
                return dto.Value.ToString("dd-MMM-yyyy HH:mm:ss");
            }
            else
            {
                return "                    ";
            }
        }
        public static void Main(string[] args)
        {

            StdSchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sched = sf.GetScheduler().Result;
            foreach(string grp in sched.GetJobGroupNames().Result)
            {
                foreach (JobKey job in sched.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(grp)).Result)
                {
                    foreach(ITrigger trig in sched.GetTriggersOfJob(job).Result)
                    {
                        Console.WriteLine("grp={0,-10} job={1,-20} trig={2,-20} class={3,-30} prev={4} next={5}", grp, job.Name, trig.Key, sched.GetJobDetail(job).Result.JobType.FullName, TF(trig.GetPreviousFireTimeUtc()), TF(trig.GetNextFireTimeUtc()));
                    }
                }
            }
            sched.Shutdown();
        }
    }
}

Clean:

package demo.sched;

import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;

public class Clean {
    public static void main(String[] args) throws SchedulerException, InterruptedException {
        StdSchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();
        for(String grp : sched.getJobGroupNames()) {
            for(JobKey job : sched.getJobKeys(GroupMatcher.jobGroupEquals(grp))) {
                sched.deleteJob(job);
            }
        }
        sched.shutdown();
    }
}
using System;
using System.Threading.Tasks;

using Quartz;
using Quartz.Impl;
using Quartz.Impl.Matchers;

namespace Clean
{
    public class Program
    {
        public static void Main(string[] args)
        {
            StdSchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sched = sf.GetScheduler().Result;
            foreach (string grp in sched.GetJobGroupNames().Result)
            {
                foreach (JobKey job in sched.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(grp)).Result)
                {
                    bool dummy = sched.DeleteJob(job).Result;
                }
            }
            sched.Shutdown();
        }
    }
}

Advanced:

Dependent jobs:

This is the case where job A is scheduled and when A is complete it triggers job B and when B is complete it triggers job C.

package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class A implements Job {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    @Override
    public void execute(JobExecutionContext ctx) throws JobExecutionException {
        System.out.printf("A: %s\n", df.format(new Date()));
    }
}
package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class B implements Job {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    @Override
    public void execute(JobExecutionContext ctx) throws JobExecutionException {
        System.out.printf("B: %s\n", df.format(new Date()));
    }
}
package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class C implements Job {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    @Override
    public void execute(JobExecutionContext ctx) throws JobExecutionException {
        System.out.printf("C: %s\n", df.format(new Date()));
    }
}
package demo.sched;

import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;

import org.quartz.impl.StdSchedulerFactory;
import org.quartz.listeners.JobChainingJobListener;

import static org.quartz.JobBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.TriggerBuilder.*;

public class Dep {
    public static void runIntervalLimited(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, int secs, int n, String... p) throws SchedulerException {
        Trigger trigger = newTrigger().withIdentity(nam + "_trigger", grp).startNow().withSchedule(simpleSchedule().withIntervalInSeconds(secs).withRepeatCount(n - 1)).build();
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.scheduleJob(job, trigger);
    }
    public static void runAfter(Scheduler sched, String grp, String nam, Class<? extends Job> jobclz, JobChainingJobListener chain, String dep, String... p) throws SchedulerException {
        JobDetail job = newJob(jobclz).withIdentity(nam + "_job", grp).storeDurably(true).build();
        for(int i = 0; i < p.length; i++) {
            job.getJobDataMap().put(String.format("p%d",  i + 1), p[i]);    
        }
        sched.addJob(job, true);
        chain.addJobChainLink(new JobKey(dep + "_job", grp), new JobKey(nam + "_job", grp));
    }
    public static void main(String[] args) throws SchedulerException, InterruptedException {
        StdSchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();
        JobChainingJobListener chain = new JobChainingJobListener("demo_chain");
        sched.getListenerManager().addJobListener(chain);
        runIntervalLimited(sched, "demo", "A", A.class, 10, 3);
        runAfter(sched, "demo", "B", B.class, chain, "A");
        runAfter(sched, "demo", "C", C.class, chain, "B");
        sched.start();
        Thread.sleep(40 * 1000);
        sched.shutdown();
    }
}
using System;
using System.Threading;
using System.Threading.Tasks;

using Quartz;
using Quartz.Impl;
using Quartz.Listener;

namespace Dep
{
    public class Program
    {
        public class A : IJob
        {
            Task IJob.Execute(IJobExecutionContext ctx)
            {
                Console.WriteLine("A : {0}", DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
                return Task.CompletedTask;
            }
        }
        public class B : IJob
        {
            Task IJob.Execute(IJobExecutionContext ctx)
            {
                Console.WriteLine("B : {0}", DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
                return Task.CompletedTask;
            }
        }
        public class C : IJob
        {
            Task IJob.Execute(IJobExecutionContext ctx)
            {
                Console.WriteLine("C : {0}", DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
                return Task.CompletedTask;
            }
        }
        public static void RunIntervalLimited(IScheduler sched, string grp, string nam, Type jobclz, int secs, int n, params string[] p)
        {
            ITrigger trigger = TriggerBuilder.Create().WithIdentity(nam + "_trigger", grp).StartNow().WithSimpleSchedule(s => s.WithIntervalInSeconds(secs).WithRepeatCount(n - 1)).Build();
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            DateTimeOffset dummy = sched.ScheduleJob(job, trigger).Result;
        }
        public static void RunAfter(IScheduler sched, string grp, string nam, Type jobclz, JobChainingJobListener chain, String dep, params string[] p)
        {
            IJobDetail job = JobBuilder.Create(jobclz).WithIdentity(nam + "_job", grp).StoreDurably().Build();
            for (int i = 0; i < p.Length; i++)
            {
                job.JobDataMap.Add(string.Format("p{0}", i + 1), p[i]);
            }
            sched.AddJob(job, true);
            chain.AddJobChainLink(new JobKey(dep + "_job", grp), new JobKey(nam + "_job", grp));
        }
        public static void Main(string[] args)
        {
            StdSchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sched = sf.GetScheduler().Result;
            JobChainingJobListener chain = new JobChainingJobListener("demo_chain");
            sched.ListenerManager.AddJobListener(chain);
            RunIntervalLimited(sched, "demo", "A", typeof(A), 10, 3);
            RunAfter(sched, "demo", "B", typeof(B), chain, "A");
            RunAfter(sched, "demo", "C", typeof(C), chain, "B");
            sched.Start();
            Thread.Sleep(40 * 1000);
            sched.Shutdown();
        }
    }
}

Alternatives:

In recent years alternatives to Quartz and Quartz.NET has been created.

For CLR languages HangFire was first released in 2014.

For JVM languages JobRunr was first released in 2020.

Setup:

package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class Ping {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    public void execute(String p1) {
        System.out.printf("%-20s %s : hi\n", p1, df.format(new Date()));
    }
}
package demo.sched;

import java.time.DayOfWeek;
import java.time.Duration;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.jobs.lambdas.JobLambda;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.scheduling.cron.Cron;
import org.jobrunr.storage.sql.common.SqlStorageProviderFactory;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class Setup {
    public static void runNow(JobScheduler sched, JobLambda job) {
        sched.enqueue(job);
    }
    public static void runInterval(JobScheduler sched, String nam, JobLambda job, int secs) {
        sched.scheduleRecurrently(nam, Duration.ofSeconds(secs), job);
    }
    public static void runEveryDay(JobScheduler sched, String nam, JobLambda job, int hr, int min) {
        sched.scheduleRecurrently(nam, Cron.daily(hr, min), job);
    }
    public static void runEveryWeek(JobScheduler sched, String nam, JobLambda job, int day, int hr, int min) {
        sched.scheduleRecurrently(nam, Cron.weekly(DayOfWeek.of(day), hr, min), job);
    }
    public static void runEveryMonth(JobScheduler sched, String nam, JobLambda job, int day, int hr, int min) {
        sched.scheduleRecurrently(nam, Cron.monthly(day, hr, min), job);
    }
    public static void main(String[] args) throws InterruptedException {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:mysql://localhost/test");
        cfg.setUsername("root");
        cfg.setPassword("hemmeligt");
        HikariDataSource ds = new HikariDataSource(cfg);
        JobScheduler sched = JobRunr.configure()
                                    .useStorageProvider(SqlStorageProviderFactory.using(ds))
                                    .initialize()
                                    .getJobScheduler();
        runNow(sched, () -> new Ping().execute("Now"));
        runInterval(sched, "sping", () -> new Ping().execute("Interval"), 10);
        runEveryDay(sched, "dping", () -> new Ping().execute("Every Day"), 23, 0);
        sched.shutdown();
    }
}
package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class Ping {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    public void execute(String p1) {
        System.out.printf("%-20s %s : hi\n", p1, df.format(new Date()));
    }
}
package demo.sched;

import java.time.DayOfWeek;
import java.time.Duration;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.jobs.lambdas.JobLambda;
import org.jobrunr.scheduling.BackgroundJob;
import org.jobrunr.scheduling.cron.Cron;
import org.jobrunr.storage.sql.common.SqlStorageProviderFactory;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class Setup2 {
    public static void runNow(JobLambda job) {
        BackgroundJob.enqueue(job);
    }
    public static void runInterval(String nam, JobLambda job, int secs) {
        BackgroundJob.scheduleRecurrently(nam, Duration.ofSeconds(secs), job);
    }
    public static void runEveryDay(String nam, JobLambda job, int hr, int min) {
        BackgroundJob.scheduleRecurrently(nam, Cron.daily(hr, min), job);
    }
    public static void runEveryWeek(String nam, JobLambda job, int day, int hr, int min) {
        BackgroundJob.scheduleRecurrently(nam, Cron.weekly(DayOfWeek.of(day), hr, min), job);
    }
    public static void runEveryMonth(String nam, JobLambda job, int day, int hr, int min) {
        BackgroundJob.scheduleRecurrently(nam, Cron.monthly(day, hr, min), job);
    }
    public static void main(String[] args) throws InterruptedException {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:mysql://localhost/test");
        cfg.setUsername("root");
        cfg.setPassword("hemmeligt");
        HikariDataSource ds = new HikariDataSource(cfg);
        BackgroundJob.setJobScheduler(JobRunr.configure()
                                             .useStorageProvider(SqlStorageProviderFactory.using(ds))
                                             .initialize()
                                             .getJobScheduler());
        runNow(() -> new Ping().execute("Now"));
        runInterval("sping", () -> new Ping().execute("Interval"), 10);
        runEveryDay("dping", () -> new Ping().execute("Every Day"), 23, 0);
    }
}
using System;
using System.Threading.Tasks;

namespace Jobs
{
    public class Ping
    {
        public void Execute(string p1)
        {
            Console.WriteLine("{0,-20} {1} : Hi", p1, DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
        }
    }
}
using System;
using System.Linq.Expressions;
using System.Threading;

using Hangfire;
using Hangfire.Server;

using Jobs;

namespace Setup
{
    public class Program
    {
        public static void RunNow(Expression<Action> job)
        {
            BackgroundJob.Enqueue(job);
        }
        public static void RunInterval(string nam, Expression<Action> job, int secs)
        {
            RecurringJob.AddOrUpdate(nam, job, string.Format("*/{0} * * * * *", secs)); // implementtaion specific hack possible because NCrontab also supports 6 args besides the standard 5
        }
        public static void RunEveryDay(string nam, Expression<Action> job, int hr, int min)
        {
            RecurringJob.AddOrUpdate(nam, job, Cron.Daily(hr, min));
        }
        public static void RunEveryWeek(string nam, Expression<Action> job, int day, int hr, int min)
        {
            RecurringJob.AddOrUpdate(nam, job, Cron.Weekly((DayOfWeek)day, hr, min));
        }
        public static void RunEveryMonth(string nam, Expression<Action> job, int day, int hr, int min)
        {
            RecurringJob.AddOrUpdate(nam, job, Cron.Monthly(day, hr, min));
        }
        public static void Main(string[] args)
        {
            GlobalConfiguration.Configuration.UseSqlServerStorage("Server=localhost;Database=Test;Integrated Security=true;Encrypt=false");
            using (IBackgroundProcessingServer sched = new BackgroundJobServer(new BackgroundJobServerOptions { WorkerCount = 5, SchedulePollingInterval = TimeSpan.FromSeconds(1)  }))
            {
                RunNow(() => new Ping().Execute("Now"));
                RunInterval("sping", () => new Ping().Execute("Interval"), 10);
                RunEveryDay("dping", () => new Ping().Execute("Every Day"), 23, 0);
            }
        }
    }
}

Run:

package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class Ping {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    public void execute(String p1) {
        System.out.printf("%-20s %s : hi\n", p1, df.format(new Date()));
    }
}
package demo.sched;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.server.BackgroundJobServerConfiguration;
import org.jobrunr.storage.sql.common.SqlStorageProviderFactory;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class Run {
    public static void main(String[] args) throws InterruptedException {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:mysql://localhost/test");
        cfg.setUsername("root");
        cfg.setPassword("hemmeligt");
        HikariDataSource ds = new HikariDataSource(cfg);
        JobScheduler sched = JobRunr.configure()
                                    .useStorageProvider(SqlStorageProviderFactory.using(ds))
                                    .useBackgroundJobServer(BackgroundJobServerConfiguration.usingStandardBackgroundJobServerConfiguration().andWorkerCount(5).andPollIntervalInSeconds(5))
                                    .initialize()
                                    .getJobScheduler();
        Thread.sleep(60 * 1000);
        sched.shutdown();
    }
}
package demo.sched;

import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class Ping {
    private static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
    public void execute(String p1) {
        System.out.printf("%-20s %s : hi\n", p1, df.format(new Date()));
    }
}
package demo.sched;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.scheduling.BackgroundJob;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.server.BackgroundJobServerConfiguration;
import org.jobrunr.storage.sql.common.SqlStorageProviderFactory;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class Run2 {
    public static void main(String[] args) throws InterruptedException {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:mysql://localhost/test");
        cfg.setUsername("root");
        cfg.setPassword("hemmeligt");
        HikariDataSource ds = new HikariDataSource(cfg);
        JobScheduler sched = JobRunr.configure()
                                    .useStorageProvider(SqlStorageProviderFactory.using(ds))
                                    .useBackgroundJobServer(BackgroundJobServerConfiguration.usingStandardBackgroundJobServerConfiguration().andWorkerCount(5).andPollIntervalInSeconds(5))
                                    .initialize()
                                    .getJobScheduler();
        BackgroundJob.setJobScheduler(sched);
        Thread.sleep(60 * 1000);
        sched.shutdown();
    }
}
using System;
using System.Threading.Tasks;

namespace Jobs
{
    public class Ping
    {
        public void Execute(string p1)
        {
            Console.WriteLine("{0,-20} {1} : Hi", p1, DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss"));
        }
    }
}
using System;
using System.Linq.Expressions;
using System.Net.NetworkInformation;
using System.Threading;

using Hangfire;
using Hangfire.Server;

using Jobs;

namespace Run
{
    public class Program
    {
        public static void Main(string[] args)
        {
            GlobalConfiguration.Configuration.UseSqlServerStorage("Server=localhost;Database=Test;Integrated Security=true;Encrypt=false");
            using (IBackgroundProcessingServer sched = new BackgroundJobServer(new BackgroundJobServerOptions { WorkerCount = 5, SchedulePollingInterval = TimeSpan.FromSeconds(1) }))
            {
                Thread.Sleep(60 * 1000);
            }

        }
    }
}

Clean:

package demo.sched;

import java.time.Duration;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.jobs.lambdas.JobLambda;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.storage.sql.common.SqlStorageProviderFactory;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class Clean {
    public static void runNow(JobScheduler sched, JobLambda job) {
        sched.enqueue(job);
    }
    public static void runInterval(JobScheduler sched, String nam, JobLambda job, int secs) {
        sched.scheduleRecurrently(nam, Duration.ofSeconds(secs), job);
    }
    public static void main(String[] args) throws InterruptedException {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:mysql://localhost/test");
        cfg.setUsername("root");
        cfg.setPassword("hemmeligt");
        HikariDataSource ds = new HikariDataSource(cfg);
        JobScheduler sched = JobRunr.configure()
                                    .useStorageProvider(SqlStorageProviderFactory.using(ds))
                                    .initialize()
                                    .getJobScheduler();
        sched.deleteRecurringJob("sping");
        sched.deleteRecurringJob("dping");
        sched.shutdown();
    }
}
package demo.sched;

import java.time.Duration;

import org.jobrunr.configuration.JobRunr;
import org.jobrunr.jobs.lambdas.JobLambda;
import org.jobrunr.scheduling.BackgroundJob;
import org.jobrunr.scheduling.JobScheduler;
import org.jobrunr.storage.sql.common.SqlStorageProviderFactory;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class Clean2 {
    public static void runNow(JobScheduler sched, JobLambda job) {
        sched.enqueue(job);
    }
    public static void runInterval(JobScheduler sched, String nam, JobLambda job, int secs) {
        sched.scheduleRecurrently(nam, Duration.ofSeconds(secs), job);
    }
    public static void main(String[] args) throws InterruptedException {
        HikariConfig cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:mysql://localhost/test");
        cfg.setUsername("root");
        cfg.setPassword("hemmeligt");
        HikariDataSource ds = new HikariDataSource(cfg);
        BackgroundJob.setJobScheduler(JobRunr.configure()
                                             .useStorageProvider(SqlStorageProviderFactory.using(ds))
                                             .initialize()
                                             .getJobScheduler());
        BackgroundJob.deleteRecurringJob("sping");
        BackgroundJob.deleteRecurringJob("dping");
    }
}
using System;

using Hangfire.Server;
using Hangfire;

namespace Clean
{
    public class Program
    {
        public static void Main(string[] args)
        {
            GlobalConfiguration.Configuration.UseSqlServerStorage("Server=localhost;Database=Test;Integrated Security=true;Encrypt=false");
            using (IBackgroundProcessingServer sched = new BackgroundJobServer(new BackgroundJobServerOptions { WorkerCount = 5, SchedulePollingInterval = TimeSpan.FromSeconds(1) }))
            {
                RecurringJob.RemoveIfExists("sping");
                RecurringJob.RemoveIfExists("dping");
            }                
        }
    }
}

Conclusion:

Quartz and Quartz.NET are very powerful and flexible schedulers for Java (any JVM language) applications and C# (any CLR language) applications respectively.

Newer alternatives with a more modern style do exist, but to me the benefits (not needing to have job implement specific interface due to scheduling an adhoc lambda expression calling the job, not needing to create database tables upfront etc.) seems rather superficial and they seem to be lacking in advanced functionality.

Article history:

Version Date Description
1.0 May 15th 2025 Initial version
1.1 May 21st 2025 Add dependency section
1.2 May 25th 2025 Add alternatives section

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj