Monday, December 01, 2008

Quick HOWTO : Quartz with JBoss

While searching for some job scheduler tool, I came across this Open Source tool, called, Quartz. Most importantly my requirement was to be able to use it from inside an application server like JBoss. After reading some tutorials, I gave it a go. Let me just brief my experience here.

Quick Intro
Fundamental entities with Quartz or with any other job schedulers are tasks and triggers invoking them. Quartz has got two kinds of triggers: SimpleTriggers and CronTriggers which are very much similar to Unix Cron triggers. There are two ways one can store the jobs in Quartz. RAMJobStore is the most simple one but the tasks stored here do not get persisted, i.e., jobs get forgotten once the server restarts. Other type of job stores are JDBCJobStore. As the name suggests with these job stores, jobs are stored in relational databases. Quartz supports most of the standard databases like MySQL, Oracle and etc. There are two kinds of JDBCJobStores available: JobStoreTX and JobStoreCMT. JobStoreTX is supposed to be used stand alone whereas JobStoreCMT is meant to be used from an application server. As my requirement was to use it with JBoss I used only JobStoreCMT. And, used it with MySQL as the back end database.

Quartz as a JBoss service
JBoss 4.2.2.GA already has got quartz bundled with it. But actually you can not do much with this as it supports only RAMJobStore. So to use it effectively we need to do some more. We have to copy quartz-1.6.2.jar and quartz-jboss-1.6.2.jar in the JBOSS_HOME/server/server_mode/lib directory. Then, we need to create a xml file to configure Quartz as a service in JBOSS_HOME/server/server_mode/deploy directory.

Create the data base
We need to create the database where all job and trigger information will get stored eventually. The sql scripts are bundled with Quartz download under docs/dbTables directory.

quartz-service.xml
As already mentioned we need to create the quartz-service file . Let me just enclose a sample file.

<server>

<mbean code="org.quartz.ee.jmx.jboss.QuartzService" name="user:service=QuartzService,name=QuartzService">
<!-- JNDI name for locating Scheduler, "Quartz" is default. -->

<attribute name="JndiName">Quartz</attribute>
<attribute name="Properties">
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#

# org.quartz.scheduler.classLoadHelper.class =

org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.xaTransacted = false

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 4

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = QUARTZ
org.quartz.jobStore.nonManagedTXDataSource = QUARTZ_NO_TX
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.dataSource.QUARTZ.jndiURL = java:QuartzDS
org.quartz.dataSource.QUARTZ_NO_TX.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.QUARTZ_NO_TX.URL = jdbc:mysql://localhost:3306/Quartz
org.quartz.dataSource.QUARTZ_NO_TX.user = root
#org.quartz.jobStore.maxMisfiresToHandleAtATime=0

</attribute>

</mbean>
</server>
The point to note here is that we'll need two datasource elements for this configuration. One is standard datasource managed by JBoss container (see the line org.quartz.jobStore.dataSource = QUARTZ) another one is not managed by the container, on which quiatz can call commit/rollback by itself. We have to confugure the container managed datasource as a standard jboss *-ds file, whereas we configure the non_managed_datasource inside this quartz-service.xml itself. Here one gotcha is for the container_managed datasource, we'll need to use XA datasource (I am not very sure why regular local datasource does not work. Maybe reason being Quartz works in a clustered environment, just a guess though). Let me just attach a sample mysqlquartz-ds file here:

<?xml version="1.0" encoding="UTF-8"?>
<datasources>
<xa-datasource>
<jndi-name>QuartzDS</jndi-name>

<xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
<xa-datasource-property name="URL">jdbc:mysql://localhost:3306/Quartz</xa-datasource-property>
<user-name>root</user-name>
<password></password>
<transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>
<max-pool-size>5</max-pool-size>
<min-pool-size>0</min-pool-size>

<blocking-timeout-millis>2000</blocking-timeout-millis>
<idle-timeout-minutes>2</idle-timeout-minutes>
<track-connection-by-tx>true</track-connection-by-tx>
<no-tx-separate-pools>false</no-tx-separate-pools>


<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>
<!-- <valid-connection-checker-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLValidConnectionChecker</valid-connection-checker-class-name> -->
<metadata>
<type-mapping>mySQL</type-mapping>
</metadata>
</xa-datasource>

</datasources>
Creating a job
Once you get the configuration right, other things are pretty simple.

InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup("Quartz");
Trigger trigger = TriggerUtils.makeDailyTrigger("myTrigger", 0, 0); //a trigger which gets fired on each midnight
trigger.setStartTime(new Date());

JobDetail job = new JobDetail("jobName", "jobGroup", Executor.class);

job.getJobDataMap().put("Name", "Abdul Sahid Khan");
job.getJobDataMap().put("Age", 125);

scheduler.scheduleJob(job, trigger);
So you get a handle to the Scheduler object using jndi lookup. Remember we set the jndi name in quartz-service.xml file. Then we create a trigger. There is various ways to create a trigger. TriggerUtils is a utils class provided by Quartz itself which gives mane factory methods to create simple triggers. Then we need to create a job. There is no Job class (well, there is an interface by that name but let us now pretend that it does not exist), instead we will create a JobDetail object. While creating JobDetail object we need to provide an Executor class which will be used when this particular job gets fired by the Scheduler. We can store job specific information in the datamap provided by JobDetail object. These information can be used in the Executor class.

Execute the job
The job gets executed by the class which was passed while creating the JobDetail object. This Executor class needs to implement earlier_ignored Job interface. And that mandates Executor to have a method called execute().

public class Executor implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
String jobName = context.getJobDetail().getName();
String groupName = context.getJobDetail().getGroup();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String name = (String) dataMap.get("Name");
Integer age = (Integer) dataMap.get("Age");
logger.info("Job received name: " + jobName + " , Group: " + groupName);
logger.info("Name: " + name + " , Age: " + age);
}
}
Delete the job
To delete a job we need to know the name and group of the job.

InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup("Quartz");
scheduler.deleteJob("jobName", "jobGroup");
Here one must remember that you can not delete a job from inside the execute method, that will create a lock which will cause JVM to crash. So delete should be handled as different process like create.

One last thing about Jboss
Sometime, business logic needs you to have your Executor class inside the your EAR/WAR application. Now that will create a dependency problem while starting the jboss server. Each time Quartz service starts, it tries to recover if there is any misfired jobs. But if Quartz starts before EAR/WAR deploys then we have ClassNotFound problem. To solve this we can use the deploy.last hack given by JBoss. You can create deploy.last directory and put the quartz-service.xml file inside that. That will ensure Quartz service will start after all the application gets deployed.

Reference:
Quartz wiki

15 comments:

Wolter said...

Excellent post! However, I'm having some trouble getting it to work on my end.

The first problem came due to initialization order. You'll need to add a "depends" tag to quartz-service.xml with the contents "jboss.jca:service=LocalTxCM,name=QuartzDS"

The second issue I haven't found a solution for. Upon attempting to look up "Quartz", it fails with NameNotFoundException: Quartz not bound

Sahid said...

Thank you crankytech for pointing out the work around for datasource. However I am not sure why your jndi lookup is failing. Maybe in your server "Quartz" is not created as a top level child of the jndi tree. Maybe actual name is something like: "/java/Quartz". Debugging will help perhaps..?

hardeep said...

Hi Sahid ... Its a nice post .. I am new to Quartz ..I am using it with Jboss 5.0.1. I am not able to configure DB. My quartz-server.xml has the following entry

org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.xaTransacted = false
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 4
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.OracleDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource=QUARTZ
org.quartz.dataSource.QUARTZ.jndiURL =java:PSSATO


and my ****-ds.xml has :



jndi-name PSSATO /jndi-name


xa-datasource-class oracle.jdbc.xa.client.OracleXADataSource /xa-datasource-class ......


but still I get this error while deployment :

DEPLOYMENTS IN ERROR:
Deployment "vfsfile:/Users/hardeep/jboss-5.0.1.GA/server/default/deploy/quartz-service.xml" is in error due to the following reason(s): org.quartz.SchedulerConfigException: Failure occured during job recovery. [See nested exception: org.quartz.JobPersistenceException: Failed to obtain DB connection from data source 'QUARTZ': java.sql.SQLException: Could not retrieve datasource via JNDI url 'java:PSSATO' javax.naming.NameNotFoundException: PSSATO not bound [See nested exception: java.sql.SQLException: Could not retrieve datasource via JNDI url 'java:PSSATO' javax.naming.NameNotFoundException: PSSATO not bound]]


I am not able to figure why the jndi name is not found....

hardeep said...
This comment has been removed by the author.
Unknown said...

Hi Sahid..
Nice Post...!!

imsomniac said...

I have succeed to deploy application.

But ctx.lookup("Quartz") returns null.I read from forums that Schedular is non serilizable class
so calling from outside results null instance.

Anonymous said...

Hello! I don't understand about this code:

InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup("Quartz");
Trigger trigger = TriggerUtils.makeDailyTrigger("myTrigger", 0, 0); //a trigger which gets fired on each midnight
trigger.setStartTime(new Date());

JobDetail job = new JobDetail("jobName", "jobGroup", Executor.class);

job.getJobDataMap().put("Name", "Abdul Sahid Khan");
job.getJobDataMap().put("Age", 125);

scheduler.scheduleJob(job, trigger);

Where do you put this code? In what class?

Sahid said...

You can write this code in any java class where from you can access the jndi context. Typically I use this in my ServletContextListener. So that when my application starts, all my jobs get created.

Anonymous said...

How can i configure for jboss 7?
It shows an error as "CronTrigger cannot be resolved as a type". Please suggest

n002213f said...

Good post, got me going.

Unknown said...

Thanks a lot for detailed installation guide provided.

I can seamlessly integrate Quartz with JBoss 4.2.2GA

I was migrating from JBoss 4.2.2GA to JBoss AS 7.0.2, I tried to integrated Quartz scheduler in jboss 7.0.2 AS without any success.
__________________________________

Can you put some light on how to integrate quartz on Jboss 7.

Thanks in advance.

Anonymous said...

For JBoss 7, to use the integratted quartz in your application, you need to add quartz to your app manifest, see the JBoss 7 guide

Anonymous said...

Hi
Can any one help me how to run my scheduler job code in jboss what are the step i have to follow to get it run my code. whjat aare the setting required in jboss side - can you provide me the detailed installation guide of jboss.

I have written java code and used Quartz scheduler intergrated my code in execute method().

But i want to run this schedule when server start first time.

help would be appriciated , thans in advance..

sandy said...

Excellent post dada!!!! Expecting more posts like this from you.

Joel said...

Hello Sahid, I cant use a DB for set the jobs and triggers, is it possible use another way for storage the necesary information? (txt, xls, xml, etc.)

Thanks