Quartz.net is an open source enterprise class scheduler. It is available with Apache License V2.0. It is a port of Quartz from Java community. It can be used as a separate windows service. It can also be hosted inside your application providing scheduling of jobs easier. For a UI application, you can use different schedulers to poll the servers. But the main usage should be for server applications running schedules and batches. This post should serve an introduction of common features of Quartz.net in order to get a feel of the API including the main features and their usage.
Quartz can be used as Hosted in your application. We can also use it as a separate process including hosted in a Windows Service. For our example we can use it as a scheduler hosted in our simple console application. Quartz is available as a Nuget package. You can use it when you want to host the scheduler in your application. You can also download the libraries package from Sourceforge.net.
Main Scheduling API Objects
If we take a moment to think about a scheduling API, there should be a main Scheduler which should be used to build and execute schedules. Each schedule should be a job executed based on a certain pre-configured trigger mechanism. So there are three types of objects. They are Scheduler, Job and Triggers. Quartz also follow the same conceptual model.
Quartz uses Triggers for "When to do" and Jobs for "What to do" needs". We also need a scheduler to register these jobs and triggers. The triggers would notify the scheduler when they are activated, which results in execution of jobs by the scheduler. There might be more than one jobs executed in a schedule. Similarly, we might need to execute the same job as a result of activation of more than one triggers.
As we discussed above, a job is simply an operation that we need to execute when a trigger is saturated. Quartz Job's interface is just IJob interface which just defines a contract for Execute operation. The jobs might be interrupt-able.
Following is a simple job which just prints the firing time on the console. It implements IJob interface. The firing time can be obtained from IJobExecutionContext passed as argument to Execute method.
Quartz has also provided implementation of some of the most frequent cross-cutting jobs. The details are as follows:
The scheduled job must be triggered based on some triggering event. All Quartz trigger interfaces are based on ITrigger.
The base trigger interface is inherited by a number of child interfaces providing more meaningful triggering details.
These interfaces are implemented by concrete classes in System.Impl.Triggers namespace. Here AbstractTrigger is the base class of these triggers. The available triggers are ConTriggerImpl, SimpleTriggerImpl, DailyTimeIntervalTriggerImpl and CalendarIntervalTriggerImpl.
As we discussed above, Triggers and Jobs can be used to create schedules. They can be registered with a Scheduler. Here Scheduler works as a Mediator which executes a Job based on saturation of a trigger's event. Job are specified using JobDetailImpl. This is a momento which would be used for persistence of jobs and their respective schedules. Here we have used an instance of MessageJob, defined above, and used it with CalendarIntervalTriggerImpl to create a schedule using Scheduler. As per Quartz documentation, every time a scheduler needs to execute the job, it creates a new instance of IJob. It then garbage collects the object later on.
The above code would result in the following output:
Calendars for Trigger's Exclusion List
Quartz provides a comprehensive list of calendars. These calendars allows us to provide exclusion list. All of them implement ICalendar through inheriting BaseCalendar. A calendar is associated with a trigger to provide exclusion list the the trigger inclusion list. A Calendar can be based on another ICalendar through CalendarBase property, which allows us to specify more sophisticated exclusion lists. It is easy to check if a given time is part of exclusion list through IsTimeInluded method of ICalendar. We can also get the next scheduled time after given time. In this case it just returns the next scheduled time if it is not in the exclusion list.
All the required calendars must be registered with the scheduler. A Trigger specifies the name of the calendar registered by the scheduler to provide the exclusion list. In the below code, we are just adding the calendar details to the trigger to include the exclusion details. The we are registering the actual calendar object with the key name with the scheduler. Here a DailyCalendar is use with start and ending time specified in 24-hour format.
Passing Data for Job Execution
As we discussed above, the scheduler uses JobFactory to create an instance of the IJob implementation. So we cannot directly instantiate the members of the job. Quartz provides JobDataMap for just that purpose. We can add key / value pairs to the JobDataMap associated with the JobDetail or Trigger. The JobDataMap values are assigned to the members of job after instantiation if key is same as one of the properties of Job. In the following example we are adding the JobDataMap to a JobDetail and Trigger. It must be remembered that multiple job details might use the same job with different JobDataMap, which can be used in other schedules.
Now we can modify the MessageJob to use the values passed by the scheduler. JobExecutionContext can be used to get the JobDataMap from Trigger and JobDetail. There is also a convenience JobDataMap which consolidates the values from both JobDataMap(s) where trigger ones override the JobDetails values. You can also notice additional attributes with the Job Definition. PersistJobDataAfterExecution would keep the new values added to the JobDataMap for JobDetails after job execution. DisallowConcurrentExecution avoids multiple execution of the job with the same JobDetails.
The above code would result in the following output. Please notice how JobDataMap values are injected into the properties.
Trigger Set for One Job
Quartz Scheduler allows the same JobDetails to be scheduled with multiple trigger. In the following code we are adding a SimpleTriggerImp and CronTriggerImpl. We are creating a JobDetails for MessageJob and then creating a schedule. Here CronTriggerImpl allows us to schedule based on the Cron expression as in Unix's Cron scheduler.
Here ISet<T> is the interface type provided by Quartz. The implementations including TreeSet<T>, HashSet<T> and ReadOnlySetISet<T>.
Quartz provides listeners to allow execution of actions based on certain events on job, schedule and trigger. There are three types of interfaces IScheduleListener, ITriggerListener and IJobListener for listening of events on schedule, triggers and jobs. There are abstract implementation of these interfaces including TriggerListenerSupport, ScheduleListenerSupport and JobListenerSupport. These abstract implementations are mostly for providing convenience to implementers of the listener interfaces with protected constructors and virtual members. This makes it easier as we just need to override the required members.
There are also BroadCast implementations of these interfaces including BroadCastJobListener, BroadCastSchedulerListener and BroadcastTriggerListener. This is based on Composite pattern where multiple listeners can be added to these broadcast listener's instance. We just need to discuss an instance of this broadcast listener with scheduler instead of requiring the individual listeners' registrations.
In the following example, we are using JobListnerSupport to create a custom Job listener. Here we just want to print the job details to console after they are executed by overriding JobExecuted method.
All listeners must be added to scheduler's ListenerManager. It includes, job, trigger and scheduler listeners.
The above code would result in the following output:
Quartz Builder API
In order to make developer's lives easier, Quartz provides several builder interfaces to create schedules, triggers and jobs. In the following code, we are using TriggerBuilder, JobBuilder and ScheduleBuilderto create a schedule. These are fluent interfaces. Look at how easy it has become to create a trigger, assign job details to it, specify schedule using a schedule builder, provide exclusions using calendar. Since a job has already been assigned to the trigger, we don't need to specify it again for creating a schedule. We need to add the job details to the scheduler separately.
In the above example, we used SimpleScheduleBuilder. There are also additional scheduler builders available to create the scheduler specific to your needs.
It is OK to create a job with no associated trigger assigned. The job details would just be dormant in the scheduler. But Scheduler throws SchedulerException if we add a trigger which would never get fired.
Quartz also provides other exception types. The hierarchy is as follows: