Sunday, June 18, 2006

Scheduling durable, non-volatile job in OpenSymphony Quartz

Previously, I managed to get Quartz to run when Tomcat runs. I am also using Postgresql as the jobstore. Now I need to schedule a job to run in Quartz. Since I want the job to persist between Quartz runs, I have to tag the job with the empty interface StatefulJob:
package my.com.petah.common.util.cron;

import org.apache.commons.logging.*;
import org.quartz.*;
import java.text.*;
import java.util.*;
import java.util.Calendar;

public class ConsolidateAttendance
implements Job, StatefulJob {

private static Log log
= LogFactory.getLog(ConsolidateAttendance.class);

private static DateFormat df
= new SimpleDateFormat("yyyymmdd HH:mm");

private static Date parse(String val) {
if (val == null) return null;
try { return df.parse(val); }
catch (ParseException e) { return null; }
}

public void execute(JobExecutionContext ctx)
throws JobExecutionException {
JobDetail job = ctx.getJobDetail();
JobDataMap props = job.getJobDataMap();
Date last = parse(props.getString("last-run"));
Calendar cal = Calendar.getInstance();
cal.clear(Calendar.SECOND);
cal.clear(Calendar.MILLISECOND);
Date curr = cal.getTime();
if (log.isInfoEnabled()) {
log.info(
job.getFullName()
+ " executes. Last run:"
+ last + " current:" + curr);
}
doExecute(ctx, last, curr);
props.put("last-run", df.format(curr));
}

public void doExecute(
JobExecutionContext ctx, Date last, Date curr) {

// job logic here
}
}
Note that I am using the Jakarta commons logging. I also need to convert the time when the job ran previously into a String because, through configuration parameter org.quartz.jobStore.useProperties , I have chosen to store job property values as Strings to avoid class versioning problems. The main logic of the job should be placed in the doExecuteMethod.

In creating the job and scheduling it, it seems reasonable to do it outside of the webapp in Tomcat. This can be done through RMI. The additional parameters needed in quartz.properties for this purpose is given below:

org.quartz.scheduler.rmi.export: true
org.quartz.scheduler.rmi.registryHost: localhost
org.quartz.scheduler.rmi.registryPort: 1099
org.quartz.scheduler.rmi.createRegistry: true

Now to get the RMI client program to work properly requires a bit of experimentation because Quartz documentation is a bit inadequate. For example, in all of the examples, Scheduler class has been used. But with RMI, we actually need to use QuartzScheduler_Stub class instead. This class is method-for-method equivalent to QuartzScheduler class. Methods signatures between Scheduler and QuartzScheduler are different because the latter typically would require a mysterious SchedulingContext as its first parameter. We also need to bind with QuartzScheduler_Stub with name "QuartzScheduler_$_NON_CLUSTERED" in the RMI registry. See the start of the main function below:

package my.com.petah.common.util.cron;

import org.apache.commons.logging.*;
import org.quartz.core.*;
import org.quartz.*;
import java.rmi.registry.*;

public class CronInit {

static Log log
= LogFactory.getLog(CronInit.class);

public static void main(String args[])
throws Exception {
Registry reg = LocateRegistry.getRegistry();
if (log.isInfoEnabled()) {
String n[] = reg.list();
for (int i = 0; i < n.length; ++i)
log.info("rmi registry object:" + n[i]);
}
SchedulingContext ctx = new SchedulingContext();
ctx.setInstanceId("petah.scheduler");
QuartzScheduler_Stub sched
= (QuartzScheduler_Stub) reg.lookup(
"QuartzScheduler_$_NON_CLUSTERED");
if (log.isInfoEnabled()) {
log.info(
"scheduler retrieved:"
+ sched.getSchedulerName());
String g[] = sched.getJobGroupNames(ctx);
for (int i = 0; i < g.length; ++i) {
String n[] = sched.getJobNames(ctx, g[i]);
for (int j = 0; j < n.length; ++j)
log.info("defined job: " + g[i] + "." + n[j]);
}
}

Note that you can give any string as the instance id for the mysterious SchedulingContext object. Once the scheduler has been successfully retrieved, the code prints the jobs already defined in the scheduler.

Next, I need to create the job. I want the job to still exist even when it is not scheduled, i.e., it does not have a trigger attached to it. The job is said to be durable. Also, I want the job to persist between Quartz runs. It is said to be non-volatile in this case. However, before I create a job, I check whether the job is already there.


JobDetail job = sched.getJobDetail(
ctx, "consolidate-attendance",
"daily-attendance-group");
if (job != null) {
if (log.isInfoEnabled())
log.info(job.getFullName()
+ " already defined.");
} else {
job = new JobDetail("consolidate-attendance",
"daily-attendance-group", ConsolidateAttendance.class,
/* volatile */ false, /* durable */ true,
/* recover */ false);
sched.addJob(ctx, job, false);
}

The JobDetail object created above ties the ConsolidateAttendance class defined earlier with a job with name consolidate-attendance in the group daily-attendance-group. Actually, you do not have to explicitly add the job the scheduler. You could have schedule it with the trigger to be defined shortly. But I want to test whether I could add a "dangling" job and then schedule it later.

Next, the trigger. Again, I check whether trigger by the given name is there already before creating it.


Trigger trig = sched.getTrigger(
ctx, "every-some-minutes",
"debug-trigger-group");
if (trig != null) {
if (log.isInfoEnabled())
log.info(trig.getFullName()
+ " already defined.");
} else {
trig = new CronTrigger(
"every-some-minutes",
"debug-trigger-group", "0 0/5 * * * ?");
}

Note that a trigger has a name and a group too. In the above, the name is "every-some-minutes" and the group is "debug-trigger-group". The cron expression "0 0/5 * * * ?" says that the trigger will fire every 5 minutes starting from the zeroth minute of the hour. I was hoping I could add a trigger to the scheduler similar to what is possible with a job but there is no way to do this.

Now the job could be scheduled by attaching it to the trigger. I first check whether the trigger has already got a job tied to it already.


String jobGroup = trig.getJobGroup();
String jobName = trig.getJobName();
if (jobGroup != null && jobName != null) {
if (log.isInfoEnabled()) {
log.info("trigger " + trig.getFullName()
+ " already attached to job "
+ jobGroup + "." + jobName);
}
} else {
trig.setJobGroup("daily-attendance-group");
trig.setJobName("consolidate-attendance");
sched.scheduleJob(ctx, trig);
if (log.isInfoEnabled()) {
log.info("job " + job.getFullName()
+ " scheduled with trigger "
+ trig.getFullName());
}
}
}
}

Note that there are two versions of the method scheduleJob. You cannot use the version that accepts both a job and a trigger because internally the scheduler would try to add the job to itself. Since I have already done this, the method would fail. You need to tie the job to the trigger through methods in the Trigger class and use the scheduleJob that only accepts a trigger.

That is it. Running the CronInit class above on the same machine where the webapp is running would create the job and the schedule it with the trigger. Below is a sample of the log from the scheduler:

...
Connected to server
INFO: Server startup in 5315 ms
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:00:00 MYT 2006
current:Sun Jun 18 22:05:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:05:00 MYT 2006
current:Sun Jun 18 22:10:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:10:00 MYT 2006
current:Sun Jun 18 22:15:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:15:00 MYT 2006
current:Sun Jun 18 22:20:00 MYT 2006
INFO: daily-attendance-group.consolidate-attendance executes.
Last run:Wed Jan 18 22:20:00 MYT 2006
current:Sun Jun 18 22:25:00 MYT 2006
...

7 comments:

Anonymous said...

Can you please let me know what is the maximum limit of DATE which we can give, while using quartz.jar.

Ex->22-Jan-7000 so which is the maximum year which we can give to schedule.

Can you please mail me the details on mayur.bhatnagar@gmail.com

Ron said...

What if you are trying to connect to a clustered scheduler?

myusri said...

Oh, I have not done any serious Java development for sometime already. I figure if you want to use clustered scheduler, you will not start it from within Tomcat? To connect to it through RMI you probably need to bind to the clustered instance instead. There could be more things involved. Hopefully you have found your solution already.

Unknown said...

hi this is karan,
thx for ur effort..
Can u help me in storing the log of fired,misfired or completed trigger..
i mean how can i store all this info in database table for future refrence that which trigger fired when..
mail me at karan.gondara@gmail.com
or jst post a reply over here.
thx a lot

myusri said...

Hi Karan,

In the example I have above, I store the time when a job gets triggered in a job property. Alternatively, you should be able to store the time in a column of a database table of your choice.

Anonymous said...

QuartzScheduler_Stub?

I think that you have to replace QuartzScheduler_Stub for RemotableQuartzScheduler interfase.

myusri said...

Thanks for the suggestion. Maybe I will try it when I've mustered enough energy to start looking at Java web development again.

The code I posted is like 2 years and a month old...