Job Chaining in Quartz

Job chaining is a topic that gets raised from time to time on the Quartz users' forum. In fact, it has been asked enough that it's part of the Quartz FAQ (see www.opensymphony.com/quartz/faq.html#chaining). Whether they realize it not, most of the users who are asking whether Quartz supports job chaining are really asking, "How can I add workflow to Quartz?" But before we dive deep into OSWorkflow, let's look at how you might accomplish job chaining with the facilities that come in the Quartz framework.

Job Chaining Isn't Workflow

For clarity, we should make the distinction that job chaining is one Quartz job either conditionally or unconditionally scheduling another job when the first one finishes. Using the Quartz framework alone to accomplish this is laden with problems and limitations. It's worth going through the exercise, however, so that you can fully understand those limitations.

We're going to make some of you mad with this statement, but as you'll learn from this material, job chaining in Quartz is not workflow. It might have some resemblance to workflow, and it might smell like workflow or feel like workflow, but it's definitely not workflow as you'll soon come to know it. You can think of it as the "lazy man's workflow"sort of workflow on a shoestring budget. In all seriousness, workflow systems such as OSWorkflow offer much more functionality than you'll get out of Quartz job chaining. This is not a knock on Quartz: Quartz was designed for job scheduling, and it does that very well. Workflow frameworks such as OSWorkflow are designed to do workflow. Both are great tools.

Quartz takes two main approaches to job chaining. One uses Quartz listeners; the other uses the JobDataMap.

Job Chaining with Listeners

The first approach to job chaining is to use Quartz listeners. This is done by creating either a JobListener or a TRiggerListener that, when notified by the Scheduler, schedules the next job for execution. The method jobWasExecuted() on the JobListener or the triggerComplete() method on the triggerListener can be used as the location to "chain" the next job. Let's suppose that you have a job called ImportantJob that performs some important logic for your business. You create it like any other Quartz job that you created so far. Listing 14.1 shows the outline of the job that represents some important job your Quartz application needs to perform.

Listing 14.1. ImportantJob Represents a Job That You Might Need to Perform for Your Business

public class ImportantJob implements Job { public void execute(JobExecutionContext context) { // Do something important in this Job } }

Notice that there's nothing special about the job in Listing 14.1. Let's further suppose that you needed to chain a second job to the completion of the ImportantJob shown in Listing 14.1. You could choose any job, but let's make it one that prints some details about the job that ran before it. Call it PrintJobResultJob (see Listing 14.2).

Listing 14.2. PrintJobResultJob Prints Information About the Chained Job That Ran Before It

public class PrintJobResultJob implements Job { Log logger = LogFactory.getLog(PrintJobResultJob.class); public void execute(JobExecutionContext context) { // Get the JobResult for the previous chained Job JobResult jobResult = (JobResult) context.getJobDataMap().get("JOB_RESULT"); // If no Job was chained before this one, do nothing if (jobResult != null) { logger.info(jobResult); } } }

The PrintJobResultJob is designed to look in its JobDataMap and see if a JobResult object is present. The class JobResult is not part of the Quartz framework, but you can easily create it to represent the result of a job execution. In many instances, creating something like a JobResult class can be helpful. Listing 14.3 shows the JobResult class for our example.

Listing 14.3. The JobResult Represents the Result of a Job Execution

public class JobResult { private boolean success; private String jobName; private long startedTime; private long finishedTime; public JobResult(){ startedTime = System.currentTimeMillis(); } // getters and setters not shown in this listing public String toString() { StringBuffer buf = new StringBuffer(); buf.append(jobName); buf.append(" executed in "); buf.append(finishedTime - startedTime); buf.append(" (msecs) "); if (success) { buf.append("and was successful. "); } else { buf.append("but was NOT successful. "); } return buf.toString(); } }

The JobResult class in Listing 14.3 contains several pieces of information about the result of a job execution: the time it started, the time it finished, and a flag that indicates whether the execution was successful. You can obviously put whatever fields you need in your version; this is just a simple example.

The next step in this job-chaining example is to create the listener class to perform the actual chaining. For this example, we're going to use a JobListener, but a triggerListener would work as well. Listing 14.5 shows the job-chaining JobListener.

Listing 14.5. A Nonglobal JobListener That Performs Job Chaining

public class JobChainListener implements org.quartz.JobListener { Log logger = LogFactory.getLog(JobChainListener.class); public Class nextJobClass; public String listenerName; public JobChainListener() { super(); } public JobChainListener(String listenerName, Class nextJob) { setName(listenerName); this.nextJobClass = nextJob; } public String getName() { return listenerName; } public void setName(String name) { this.listenerName = name; } public void jobToBeExecuted(JobExecutionContext context) { // Do nothing in this example } public void jobExecutionVetoed(JobExecutionContext context) { // Do nothing in this example } public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { Scheduler scheduler = context.getScheduler(); try { // Create the chained JobDetail JobDetail jobDetail = new JobDetail("ChainedJob", null, nextJobClass); // Create a one-time trigger that fires immediately Trigger trigger = TriggerUtils.makeSecondlyTrigger(0, 0); trigger.setName("FireNowTrigger"); trigger.setStartTime(new Date()); // Update the JobResult for the next Job JobResult jobResult = (JobResult) context.getJobDataMap().get( "JOB_RESULT"); jobResult.setFinishedTime(System.currentTimeMillis()); jobResult.setSuccess(true); // Pass JobResult to next job through its JobDataMap jobDetail.getJobDataMap().put( "JOB_RESULT", jobResult); // Schedule the next job to fire immediately scheduler.scheduleJob(jobDetail, trigger); logger.info(nextJobClass.getName() + " has been scheduled executed"); } catch (Exception ex) { logger.error("Couldn't chain next Job", ex); return; } } }

As you can see from Listing 14.5, the work of chaining the next job is done in the jobWasExecuted() method. As you learned in Chapter 7, "Implementing Quartz Listeners," the Scheduler calls the jobWasExecuted() method when a job is finished executing. This makes the perfect method to chain the jobs together. In the jobWasExecuted() method from Listing 14.5, several things are going on.

First, a new JobDetail and trigger are created for the chained job. Then the JobResult is retrieved from the JobDataMap of the first job, and the finishedTime and success fields are set. For the chained (next) job to have access to the JobResult object, it is loaded into the JobDataMap of the chained job.

Passing Data from One Job to Another

The idea of a JobResult was used in this example to illustrate that although passing data from one job to the chained job is possible, it can be cumbersome. In the case of the current example, the data is passed in the listener. This is the only place it can happen because this is where the chaining occurs. If you need to chain three jobs, entanglement just gets worse.

The last part of Listing 14.5 schedules the new job with the Scheduler. Because the trigger for the new job was set to fire right away, the PrintJobResultJob job will execute immediately. Looking back at Listing 14.2, you can see that when the execute() method on the PrintJobResultJob class is called, it retrieves the JobResult object and calls the toString() method. Again, this is a very simple example to show you how to chain jobs. Your jobs would obviously have more complicated logic than those shown here. The job chaining, however, would work the same.

So the good news is that it's not that difficult to use a listener class to chain jobs. The bad news is that there are some serious design issues with this approach. First, although we were smart enough to pass in the name of the next job to the listener class, there's a fairly tight coupling in this code. The listener needs to be told about the chained job at creation, so the listener becomes tightly coupled to a specific chained job for the life cycle of the listener. You would need to set up a listener for each chained joband what about when you need to chain more than two jobs? Things can get out of hand very quickly.

Variations On The Listener Approach

Of course, there are some variable ways in which you can implement your listener to function. For example, you could create a single JobDetail instance, set its durability flag to true, and prestore it in the Scheduler. Then the listener would not need to create the JobDetail and trigger each time; it would only need to create a JobDataMap, place the JobResult within it, and call scheduler.triggerJob(jobName, groupName, jobDataMap); the existing job would be executed and passed the JobResult.

 

Job Chaining with the JobDataMap

Another approach for chaining jobs is to use the JobDataMap to store the next job to execute. In the earlier listener example, we used the JobDataMap to store and pass the JobResult, but this new approach gets rid of the listener and uses the JobDataMap to do it all.

Because we've ridden ourselves of the listener class, this means that the job itself must handle the chaining of the next job. The behavior can be abstracted to a base job class, if you want. The example here doesn't do that, however, to keep it as simple as possible.

Listing 14.5 shows the new ImportantJob class. After it completes, the next job in the chain gets scheduled. You might even decide to schedule the next job based on some flag or condition of the last execution. For example, if some flag were set to true, you would execute Job A; if the flag were set to false, you would execute Job B.

The next job in the chain is stored in the JobDataMap using a key of your choice. In this example, we used the value NEXT_JOB. One of the problems with this approach is that something must store the next job in the JobDataMap before the job execution.

Listing 14.5. Job Chaining Can Also Be Done Using the JobDataMap

public class ImportantJob implements Job { static Log logger = LogFactory.getLog(JobChainListener.class); public void execute(JobExecutionContext context) { // Do something important in this Job // Set some condition based on this Job execution boolean success = true; // schedule the next Job if condition was successful if (success) { scheduleNextJob(context); } else { logger.info("Job was NOT chained"); } } protected void scheduleNextJob(JobExecutionContext context) { JobDataMap jobDataMap = context.getJobDataMap(); String nextJob = jobDataMap.getString("NEXT_JOB"); if (nextJob!= null && nextJob.length() > 0) { try { Class jobClass = Class.forName(nextJob); scheduleJob(jobClass, context.getScheduler()); } catch (Exception ex) { logger.error("error scheduling chained job", ex); } } } protected void scheduleJob(Class jobClass, Scheduler scheduler) { JobDetail jobDetail = new JobDetail(jobClass.getName(), null, jobClass); // Create a fire now, one time trigger Trigger trigger = TriggerUtils.makeSecondlyTrigger(0, 0); trigger.setName(jobClass.getName() + "Trigger"); trigger.setStartTime(new Date()); // Schedule the next job to fire immediately try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException ex) { logger.error("error chaining Job " + jobClass.getName(), ex); } } }

After the name of the job is retrieved from the JobDataMap, a JobDetail and trigger are created, and the job is scheduled. As with the listener example from before, the code that schedules the first job needs to add the initial chained job to the JobDataMap for all this to work. This can be done when the job is first added to the Scheduler and the Scheduler is started. Listing 14.6 shows this.

Listing 14.6. The First Chained Job Needs to Be Configured When the First Job Is Scheduled

public class NewScheduler { static Log logger = LogFactory.getLog(NewScheduler.class); public static void main(String[] args) { try { // Create and start the Scheduler Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); JobDetail jobDetail = new JobDetail("ImportantJob", null, ImportantJob.class); // Set up the first chained Job JobDataMap dataMap = jobDetail.getJobDataMap(); dataMap.put("NEXT_JOB", "org.cavaness.quartzbook.chapter14.ChainedJob"); // Create the trigger and scheduler the Job Trigger trigger = TriggerUtils.makeSecondlyTrigger(10000, 0); trigger.setName("FireOnceTrigger"); trigger.setStartTime(new Date()); scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException ex) { logger.error( ex ); } } }

The code in Listing 14.6 is used to schedule the first job and, at the same time, set up the next job in the chain. If there were a third job, it would have to be set up in the JobDataMap for the second job. You can obviously see how unwieldy this approach becomes when you have more than two jobs in a chain. OSWorkflow helps with this and a whole mess of other problems.

Using the JobInitializationPlugin with Job Chaining

If you are specifying your job information in the quartz_jobs.xml file and are using the JobInitializationPlugin to load that information, this approach might not be so bad. That's because you can specify the job chain very easily in the XML file. For example, consider the quartz_jobs.xml in Listing 14.7.

Listing 14.7. Job Chaining Is a Little More Manageable When Using the JobInitializationPlugin

ImportantJob org.cavaness.quartzbook.chapter14.ImportantJob NEXT_JOB org.cavaness.quartzbook.chapter14.ChainedJob FireOnceTrigger DEFAULT ImportantJob DEFAULT 2005-07-19 8:31:00 PM 0

The result of the job information in Listing 14.7 is identical to that of the previous example, only better. If you needed to change which job gets chained to the ImportantJob, you would only need to change the XML file. In the previous job-chaining examples, code would have had to be changed and recompiled.

Категории