Michael Lipton, Software Engineer, IBM
Soobaek Jang (sjang@us.ibm.com), IT Architect/Integration, IBM
21 Nov 2006
Quartz is an open source project that offers an extensive set of job scheduling features. In this article, software engineer Michael Lipton and IT architect Soobaek Jang introduce the Quartz API, starting with a general overview of the framework and concluding with a series of code examples that illustrate its fundamental features. After reading this article and following the code examples, you should feel capable of incorporating the basic features of Quartz into any Java™ application.
As modern Web applications continue to grow in scope and complexity, each underlying component of the applications must similarly grow. Job scheduling is a common requirement for Java applications in modern systems, and so is a constant preoccupation for Java developers. While current scheduling technology has evolved from more primitive methods of database trigger flags and separate scheduler threads, job scheduling is still a non-trivial problem. One of the most desirable solutions to this problem is the Quartz API from OpenSymphony.
Quartz is an open source job scheduling framework that provides simple but powerful mechanisms for job scheduling in Java applications. Quartz allows developers to schedule jobs by time interval or by time of day. It implements many-to-many relationships for jobs and triggers and can associate multiple jobs with different triggers. Applications that incorporate Quartz can reuse jobs from different events and also group multiple jobs for a single event. While you can configure Quartz through a property file (in which you can specify a data source for JDBC transactions, global job and/or trigger listeners, plug-ins, thread pools, and more) it is not at all integrated with the application server's context or references. One result of this is that jobs do not have access to the Web server's internal functions; in the case of the WebSphere application server, for example, Quartz-scheduled jobs cannot interfere with the server's Dyna-cache and data sources.
This article introduces the Quartz API using a series of code examples to illustrate mechanisms such as jobs, triggers, job stores, and properties.
Getting started
To start using Quartz, you need to configure your project with the Quartz API. The procedure is as follows:
1. Download the Quartz API.
2. Extract and place the quartz-x.x.x.jar into your project folder, or put the file into your project classpath.
3. Place the jar files from the core and/or optional folder into your project folder or project classpath.
4. If using JDBCJobStore, place all JDBC jar files into your project folder or project classpath.
For your convenience, we have compiled all the necessary files, including the DB2 JDBC files, into a single zip. See the Downloads section to download the code.
Now let's look at the main components of the Quartz API.
Jobs and triggers
The two fundamental units of Quartz's scheduling package are jobs and triggers. A job is an executable task that can be scheduled, while a trigger provides a schedule for a job. While these two entities could easily have been combined, their separation in Quartz is both intentional and beneficial.
By keeping the work to be performed separate from its scheduling, Quartz allows you to change the scheduled trigger for a job without losing the job itself, or the context around it. Also, any singular job can have many triggers associated with it.
Example 1: Jobs
You can make a Java class executable by implementing the org.quartz.job interface. An example of a Quartz job is given in Listing 1. This class overriddes the execute(JobExecutionContext context) method with a very simple output statement. The method can contain any code we might wish to execute. (All the code samples are based on Quartz 1.5.2, the stable release at the time of this writing.)
Listing 1. SimpleQuartzJob.java
package com.ibm.developerworks.quartz;
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class SimpleQuartzJob implements Job {
public SimpleQuartzJob() {
}
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("In SimpleQuartzJob - executing its JOB at "
+ new Date() + " by " + context.getTrigger().getName());
}
}
Notice that the execute method takes a JobExecutionContext object as an argument. This object provides the runtime context around the job instance. Specifically, it gives access to the scheduler and trigger, which collaborated to initiate execution of the job as well as the job's JobDetail object. Quartz separates the execution and the surrounding state of a job by placing the state in a JobDetail object and having the JobDetail constructor initiate an instance of a job. The JobDetail object stores the job's listeners, group, data map, description, and other properties of the job.
Example 2: Simple triggers
A trigger develops a schedule for job execution. Quartz offers a few different trigger options of varying complexity. The SimpleTrigger in Listing 2 introduces the fundamentals of triggers:
Listing 2. SimpleTriggerRunner.java
public void task() throws SchedulerException
{
// Initiate a Schedule Factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
// Retrieve a scheduler from schedule factory
Scheduler scheduler = schedulerFactory.getScheduler();
// current time
long ctime = System.currentTimeMillis();
// Initiate JobDetail with job name, job group, and executable job class
JobDetail jobDetail =
new JobDetail("jobDetail-s1", "jobDetailGroup-s1", SimpleQuartzJob.class);
// Initiate SimpleTrigger with its name and group name
SimpleTrigger simpleTrigger =
new SimpleTrigger("simpleTrigger", "triggerGroup-s1");
// set its start up time
simpleTrigger.setStartTime(new Date(ctime));
// set the interval, how often the job should run (10 seconds here)
simpleTrigger.setRepeatInterval(10000);
// set the number of execution of this job, set to 10 times.
// It will run 10 time and exhaust.
simpleTrigger.setRepeatCount(100);
// set the ending time of this job.
// We set it for 60 seconds from its startup time here
// Even if we set its repeat count to 10,
// this will stop its process after 6 repeats as it gets it endtime by then.
//simpleTrigger.setEndTime(new Date(ctime + 60000L));
// set priority of trigger. If not set, the default is 5
//simpleTrigger.setPriority(10);
// schedule a job with JobDetail and Trigger
scheduler.scheduleJob(jobDetail, simpleTrigger);
// start the scheduler
scheduler.start();
}
Listing 2 starts by instantiating a SchedulerFactory and getting the scheduler. As we discussed earlier, the JobDetail object is created by taking the Job as an argument to its constructor. As implied by its name, the SimpleTrigger instance is quite primitive. After creating the object, we set a few basic properties scheduling the job for execution immediately and then to repeat every 10 seconds until the job had been executed 100 times.
There are a number of other ways to manipulate a SimpleTrigger. In addition to a specified number of repeats and a specified repeat interval, you may schedule jobs to execute at a specific calendar time, given a maximum time of execution, or given a priority, which we discuss below. The maximum time of execution overrides a specified number of repeats, thus ensuring that a job does not run past the maximum time.
Example 3: Cron triggers
A CronTrigger allows for more specific scheduling than a SimpleTrigger and is still not very complex. Based on cron expressions, CronTriggers allow for calendar-like repeat intervals rather than uniform repeat intervals -- a major improvement over SimpleTriggers.
Cron expressions consist of the following seven fields:
* Seconds
* Minutes
* Hours
* Day-of-month
* Month
* Day-of-week
* Year (optional field)
Special characters
Cron triggers utilize a series of special characters, as follows:
* The backslash (/) character denotes value increments. For example, "5/15" in the seconds field means every 15 seconds starting at the fifth second.
* The question (?) character and the letter-L (L) character are permitted only in the day-of-month and day-of-week fields. The question character indicates that the field should hold no specific value. Therefore, if you specify the day-of-month, you can insert a "?" in the day-of-week field to indicate that the day-of-week value is inconsequential. The letter-L character is short for last. Placed in the day-of-month field, this schedules execution for the last day of the month. In the day-of-week field, an "L" is equivalent to a "7" if placed by itself or means the last instance of the day-of-week in the month. So "0L" would schedule execution for the last Sunday of the month.
* The letter-W (W) character in the day-of-month field schedules execution on the weekday nearest to the value specified. Placing "1W" in the day-of month field schedules execution for the weekday nearest the first of the month.
* The pound (#) character specifies a particular instance of a weekday for a given month. Placing "MON#2" in the day-of-week field schedules a task on the second Monday of the month.
* The asterisk (*) character is a wildcard character and indicates that every possible value can be taken for that specific field.
All of these definitions may seem daunting, but cron expressions become simple after just a few minutes of practice.
Listing 3 shows an example of a CronTrigger. Notice that the instantiation of the SchedulerFactory, Scheduler, and JobDetail are identical to that found in the SimpleTrigger example. In this case, we have only changed the trigger. The cron expression we have specified here ("0/5 * * * * ?") schedules the task for execution every 5 seconds.
Listing 3. CronTriggerRunner.java
public void task() throws SchedulerException
{
// Initiate a Schedule Factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
// Retrieve a scheduler from schedule factory
Scheduler scheduler = schedulerFactory.getScheduler();
// current time
long ctime = System.currentTimeMillis();
// Initiate JobDetail with job name, job group, and executable job class
JobDetail jobDetail =
new JobDetail("jobDetail2", "jobDetailGroup2", SimpleQuartzJob.class);
// Initiate CronTrigger with its name and group name
CronTrigger cronTrigger = new CronTrigger("cronTrigger", "triggerGroup2");
try {
// setup CronExpression
CronExpression cexp = new CronExpression("0/5 * * * * ?");
// Assign the CronExpression to CronTrigger
cronTrigger.setCronExpression(cexp);
} catch (Exception e) {
e.printStackTrace();
}
// schedule a job with JobDetail and Trigger
scheduler.scheduleJob(jobDetail, cronTrigger);
// start the scheduler
scheduler.start();
}
Advanced Quartz
You can access a vast amount of functionality using only jobs and triggers as detailed above. Quartz is a comprehensive and flexible scheduling package, however, offering much more functionality to those who choose to explore it. The next sections discuss some of the advanced features of Quartz.
Job stores
Quartz offers two different means by which to store the data associated with jobs and triggers in memory or a database. The former, an instance of the RAMJobStore class, is the default setting. This job store is the easiest to use and offers the best performance because all data is stored in memory. This method's major deficiency is lack of data persistence. Because the data is stored in RAM, all information will be lost upon an application or system crash.
To remedy this problem, Quartz offers the JDBCJobStore. As the name infers, this job store places all data in a database through JDBC. The trade-off for data persistence is a lower level of performance, as well as a higher level of complexity.
Setting up JDBCJobStore
You have seen a RAMJobStore instance at work in the previous examples. Because it is the default job store, it is clear that no additional setup is required to use it. Using the JDBCJobStore requires some initialization, however.
Setting up the JDBCJobStore for use in your applications requires two steps: First you must create the database tables to be used by the job store. The JDBCJobStore is compatible with all major databases, and Quartz offers a series of table-creation SQL scripts that ease the setup process. You will find table-creation SQL scripts in the "docs/dbTables" directory of the Quartz distribution. Second, you must define some properties, shown in Table 1:
Table 1. JDBCJobStore properties
Property name Value
org.quartz.jobStore.class org.quartz.impl.jdbcjobstore.JobStoreTX (or JobStoreCMT)
org.quartz.jobStore.tablePrefix QRTZ_ (optional, customizable)
org.quartz.jobStore.driverDelegateClass org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource qzDS (customizable)
org.quartz.dataSource.qzDS.driver com.ibm.db2.jcc.DB2Driver (could be any other database driver)
org.quartz.dataSource.qzDS.url jdbc:db2://localhost:50000/QZ_SMPL (customizable)
org.quartz.dataSource.qzDS.user db2inst1 (place userid for your own db)
org.quartz.dataSource.qzDS.password pass4dbadmin (place your own password for user)
org.quartz.dataSource.qzDS.maxConnections 30
Listing 4 illustrates the data persistence offered by the JDBCJobStore. As in previous examples, we started by initializing the SchedulerFactory and the Scheduler. Next, rather than initialize a job and trigger, we fetched the list of trigger group names and then, for each trigger group name, the list of trigger names. Note that each existing job should be rescheduled using the Scheduler.reschedule() method. Simply reinitializing a job that was terminated in a previous application run does not accurately load the trigger's properties.
Listing 4. JDBCJobStoreRunner.java
public void task() throws SchedulerException
{
// Initiate a Schedule Factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
// Retrieve a scheduler from schedule factory
Scheduler scheduler = schedulerFactory.getScheduler();
String[] triggerGroups;
String[] triggers;
triggerGroups = scheduler.getTriggerGroupNames();
for (int i = 0; i < triggerGroups.length; i++) {
triggers = scheduler.getTriggerNames(triggerGroups[i]);
for (int j = 0; j < triggers.length; j++) {
Trigger tg = scheduler.getTrigger(triggers[j], triggerGroups[i]);
if (tg instanceof SimpleTrigger && tg.getName().equals("simpleTrigger")) {
((SimpleTrigger)tg).setRepeatCount(100);
// reschedule the job
scheduler.rescheduleJob(triggers[j], triggerGroups[i], tg);
// unschedule the job
//scheduler.unscheduleJob(triggersInGroup[j], triggerGroups[i]);
}
}
}
// start the scheduler
scheduler.start();
}
Running JDBCJobStore
When we run our example for the first time, the trigger is initialized in the database. Figure 1 shows the database after the trigger has been initialized but before the trigger has been fired. Therefore the REPEAT_COUNT is set to 100, based on the setRepeatCount() method in Listing 4 and TIMES_TRIGGERED is 0. After letting the application run for a while, it is stopped.
Figure 1. Data in the database using JDBCJobStore (before run)
Before run with JDBCJobStore
Figure 2 shows the database after the application has been stopped. In this figure, TIMES_TRIGGERED is set to 19, which denotes the number of times that the job was run.
Figure 2. The same data after 19 iterations
After first 19 iterations
When we start the application again, the REPEAT_COUNT is updated. This is apparent in Figure 3. Here we see that REPEAT_COUNT is updated to 81, so the new REPEAT_COUNT is equal to the previous REPEAT_COUNT value minus the previous TIMES_TRIGGERED value. Furthermore, we see that in Figure 3 the new TIMES_TRIGGERED value is 7, indicating that the job has been triggered seven more times since the application was restarted.
Figure 3. Data after the second run of 7 iterations
After the second run for 7 iterations
After stopping the application again, the REPEAT_COUNT value is again updated. This is shown in Figure 4, where the application has been stopped and not yet restarted. Again, the REPEAT_COUNT value is updated by subtracting the previous TIMES_TRIGGERED value from the previous REPEAT_COUNT value.
Figure 4. Initial data before running the trigger again
Initial data before running the trigger again
Using properties
As you have seen with the JDBCJobStore, you can use a number of properties to fine-tune the behavior of Quartz. You should specify these properties in the quartz.properties file. See Resources for a listing of configurable properties. Listing 5 shows a sample of properties used for the JDBCJobStore example:
Listing 5. quartz.properties
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
# Using RAMJobStore
## if using RAMJobStore, please be sure that you comment out the following
## - org.quartz.jobStore.tablePrefix,
## - org.quartz.jobStore.driverDelegateClass,
## - org.quartz.jobStore.dataSource
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
# Using JobStoreTX
## Be sure to run the appropriate script(under docs/dbTables) first to create tables
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# Configuring JDBCJobStore with the Table Prefix
org.quartz.jobStore.tablePrefix = QRTZ_
# Using DriverDelegate
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# Using datasource
org.quartz.jobStore.dataSource = qzDS
# Define the datasource to use
org.quartz.dataSource.qzDS.driver = com.ibm.db2.jcc.DB2Driver
org.quartz.dataSource.qzDS.URL = jdbc:db2://localhost:50000/dbname
org.quartz.dataSource.qzDS.user = dbuserid
org.quartz.dataSource.qzDS.password = password
org.quartz.dataSource.qzDS.maxConnections = 30
In conclusion
The Quartz job scheduling framework offers the best of both worlds: an API that is both comprehensively powerful and easy to use. Quartz can be used for simple job triggering as well as complex JDBC persistent job storage and execution. OpenSymphony has successfully filled a void in the open source universe by making the otherwise tedious chore of job scheduling trivial for developers.
No comments:
Post a Comment