Quartz Scheduling, Spring, Hibernate and Clustering - here be dragons!

22 February 2012

Setting up a scheduler should be easy, but add in Hibernate, Spring and a clustered environment and things get somewhat more tricky...

So, recently I needed to set up a job scheduler for a Java Web App. Quarz Scheduler came to my rescue here. Being a good (well-behaved) Java Developer, IOC is always a good idea, so Spring ticks that box. Hibernate was my choice for an ORM. The problem lies in that the application was to be hosted by the client on a clustered environment, and there is no shared file space.

This post is supposed to serve as a how-to for anyone in the same situation, hopefully it can save someone some time. There are lots of blogs etc that deal with some of these technologies together, but not all at once, and no-one mentioned the gotchas...

A note though - I use code and examples gratuitously from these blogs, so I will list some of them at the during the post. Without them, this would have taken forever to get done.

I am going to start from the position that your application is up and running to an extent, hibernate is set up (mine used open session in view). Setting up a basic quartz job is simple, so I will also skip that, but will give an example later. First up therefore I will start with a simple Spring quartz definition.

1. Simple Spring / Quartz Setup

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
</bean>

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="jobDetail"/>
    <!-- 10 seconds -->
    <property name="startDelay" value="10000"/>
    <!-- repeat every 50 seconds -->
    <property name="repeatInterval" value="50000"/>
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="complianceJobTrigger" />
        </list>
     </property>
</bean>
</beans>

Simple Yes? And it does work in a standalone environment. The Job file would then look something like:

public class ExampleJob extends QuartzJobBean {
    private int timeout;
    /**
     * Setter called after the ExampleJob is instantiated
     * with the value from the JobDetailBean (5)
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
        // do the actual work
    }
}

See: http://static.springsource.org/spring/docs/1.2.x/reference/scheduling.html

2. Persistent Storage

OK, next lets add the persistent storage:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
</bean>

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="jobDetail"/>
    <!-- 10 seconds -->
    <property name="startDelay" value="10000"/>
    <!-- repeat every 50 seconds -->
    <property name="repeatInterval" value="50000"/>
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="complianceJobTrigger" />
        </list>
    </property>
    <property name="quartzProperties">
        <props>
            <prop key="org.quartz.scheduler.instanceName">InstanceNameX</prop>
            <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
            <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>
            <prop key="org.quartz.threadPool.threadCount">3</prop>
            <prop key="org.quartz.threadPool.threadPriority">5</prop>
            <prop key="org.quartz.jobStore.misfireThreshold">60000</prop>
            <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
            <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.oracle.OracleDelegate</prop>
            <prop key="org.quartz.jobStore.useProperties">false</prop>
            <prop key="org.quartz.jobStore.dataSource">myDS</prop>
            <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop>
            <prop key="org.quartz.jobStore.isClustered">true</prop>
            <prop key="org.quartz.jobStore.clusterCheckinInterval">20000</prop>
            <prop key="org.quartz.dataSource.myDS.driver">oracle.jdbc.driver.OracleDriver</prop>
            <prop key="org.quartz.dataSource.myDS.URL">jdbc:oracle:thin:@lngmalu1.uk.db.com:1509:AppDB</prop>
            <prop key="org.quartz.dataSource.myDS.user">AppUser</prop>
            <prop key="org.quartz.dataSource.myDS.password">AppPass</prop>
            <prop key="org.quartz.dataSource.myDS.maxConnections">5</prop>
            <prop key="org.quartz.dataSource.myDS.validationQuery">select 0 from dual</prop>
        </props>
    </property>
</bean>

</beans>

This sets up Quartz to use a database to store the job references, it also allows it to work in a cluster, firing once per trigger, not on each machine. Property references can be found here. You will also need to create the tables required, the SQL script can be found in the Quartz download. While a property file is neater, I wanted to end up with one, self contained XML file.

So all should be good? Unfortunatley no... As stated in the documentation of MethodInvokingJobDetailFactoryBean. Note: JobDetails created via this FactoryBean are not serializable and thus not suitable for persistent job stores. You need to implement your own Quartz Job as a thin wrapper for each case where you want a persistent job to delegate to a specific service method.

So, take a few steps back. We will need to strip out the need for MethodInvokingJobDetailFactoryBean, and at the same time will add the capacity to call any Spring Service from the Bean. Basically, we want to create our own MethodInvokingJobDetailFactoryBean. Thanks to: http://toddkaufman.blogspot.com/2008_09_01_archive.html

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

<!-- Old JobDetail Definition
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
</bean>
-->

<!-- Runnable Job: this will be used to call my actual job. Must implement runnable -->
<bean name="myRunnableJob" class="example.MyRunnableJob"></bean>

<!-- This proxy tells the session and transaction when to start and stop -->
    <bean id="myRunnableJobProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target">
            <ref bean="myRunnableJob"/>
        </property>
        <property name="proxyInterfaces">
            <value>java.lang.Runnable</value>
        </property>
        <property name="interceptorNames">
            <value>hibernateInterceptor</value>
        </property>
    </bean>

<!-- New Clusterable Definition: note, the proxy is used now. -->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
    <property name="jobClass" value="example.GenericQuartzJob" />
    <property name="jobDataAsMap">
        <map>
            <entry key="batchProcessorName" value="myRunnableJobProxy" />
        </map>
    </property>
</bean>

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="jobDetail"/>
    <!-- 10 seconds -->
    <property name="startDelay" value="10000"/>
    <!-- repeat every 50 seconds -->
    <property name="repeatInterval" value="50000"/>
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="complianceJobTrigger" />
        </list>
    </property>
    <!-- Add this to make the Spring Context Available -->
    <property name="applicationContextSchedulerContextKey"><value>applicationContext</value></property>
    <property name="quartzProperties">
        <props>
            <prop key="org.quartz.scheduler.instanceName">InstanceNameX</prop>
            <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
            <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>
            <prop key="org.quartz.threadPool.threadCount">3</prop>
            <prop key="org.quartz.threadPool.threadPriority">5</prop>
            <prop key="org.quartz.jobStore.misfireThreshold">60000</prop>
            <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
            <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.oracle.OracleDelegate</prop>
            <prop key="org.quartz.jobStore.useProperties">false</prop>
            <prop key="org.quartz.jobStore.dataSource">myDS</prop>
            <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop>
            <prop key="org.quartz.jobStore.isClustered">true</prop>
            <prop key="org.quartz.jobStore.clusterCheckinInterval">20000</prop>
            <prop key="org.quartz.dataSource.myDS.driver">oracle.jdbc.driver.OracleDriver</prop>
            <prop key="org.quartz.dataSource.myDS.URL">jdbc:oracle:thin:@lngmalu1.uk.db.com:1509:AppDB</prop>
            <prop key="org.quartz.dataSource.myDS.user">AppUser</prop>
            <prop key="org.quartz.dataSource.myDS.password">AppPass</prop>
            <prop key="org.quartz.dataSource.myDS.maxConnections">5</prop>
            <prop key="org.quartz.dataSource.myDS.validationQuery">select 0 from dual</prop>
        </props>
    </property>
</bean>

</beans>

Before you continue, make sure you have added <property name="applicationContextSchedulerContextKey"><value>applicationContext</value></property> to the SchedulerFactoryBean. OK, a few things to note. I added a new bean, myRunnableJob. This is a simple file, implementing Runnable that actually implements my job. As an example

public class MyRunnableJob implements Runnable {
    //@Resource private SpringService myService;
    @Override public void run() {
        //Run the Job. This code will run in the Spring context so you can use injected dependencies here.
    }
}

Next is GenericQuartzJob. This class simply gets the job from the batchProcessorName and runs it.

public class GenericQuartzJob extends QuartzJobBean {
    private String batchProcessorName;
    public String getBatchProcessorName() {
        return batchProcessorName;
    }

    public void setBatchProcessorName(String name) {
        this.batchProcessorName = name;
    }

    protected void executeInternal(JobExecutionContext jobCtx) throws JobExecutionException {
        try {
            SchedulerContext schedCtx = jobCtx.getScheduler().getContext();
            ApplicationContext appCtx = (ApplicationContext) schedCtx.get("applicationContext");
            java.lang.Runnable proc = (java.lang.Runnable) appCtx.getBean(batchProcessorName);
            proc.run();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new JobExecutionException("Unable to execute batch job: " + batchProcessorName, ex);
        }
    }
}

OK. That solves that problem, we can now run simple jobs easily, and our config will persist correctly in the cluster. Unfortunatley, if, like me, you used Session In View, then you wont be able to access your DB though Hibernate.

3. Hibernate

What we will need to do is start a session before the job and flush after it. Enter Hibernate injectors.

First register the injector:

<bean class="org.springframework.orm.hibernate3.HibernateInterceptor" id="hibernateInterceptor">
    <property name="sessionFactory">
        <ref bean="sessionFactory">
    </ref></property>
</bean>

Next we need to define when the interceptor runs, to do this we use a ProxyFactoryBean for our job. Putting it all together

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

<!-- Old JobDetail Definition
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
</bean>
-->

<!-- Runnable Job: this will be used to call my actual job. Must implement runnable -->
<bean name="myRunnableJob" class="example.MyRunnableJob"></bean>

<!-- New Clusterable Definition -->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
        <property name="jobClass" value="example.GenericQuartzJob" />
        <property name="jobDataAsMap">
            <map>
                <entry key="batchProcessorName" value="myRunnableJob" />
            </map>
        </property>
    </bean>

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="jobDetail"/>
  <!-- 10 seconds -->
    <property name="startDelay" value="10000"/>
  <!-- repeat every 50 seconds -->
    <property name="repeatInterval" value="50000"/>
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
      <list>
         <ref bean="complianceJobTrigger" />
      </list>
     </property>
     <!-- Add this to make the Spring Context Available -->
     <property name="applicationContextSchedulerContextKey"><value>applicationContext</value></property>
     <property name="quartzProperties">
  <props>
      <prop key="org.quartz.scheduler.instanceName">InstanceNameX</prop>
      <prop key="org.quartz.scheduler.instanceId">AUTO</prop>

      <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>
   <prop key="org.quartz.threadPool.threadCount">3</prop>
   <prop key="org.quartz.threadPool.threadPriority">5</prop>

   <prop key="org.quartz.jobStore.misfireThreshold">60000</prop>

   <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
   <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.oracle.OracleDelegate</prop>
   <prop key="org.quartz.jobStore.useProperties">false</prop>
   <prop key="org.quartz.jobStore.dataSource">myDS</prop>
   <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop>

   <prop key="org.quartz.jobStore.isClustered">true</prop>
   <prop key="org.quartz.jobStore.clusterCheckinInterval">20000</prop>

   <prop key="org.quartz.dataSource.myDS.driver">oracle.jdbc.driver.OracleDriver</prop>
   <prop key="org.quartz.dataSource.myDS.URL">jdbc:oracle:thin:@lngmalu1.uk.db.com:1509:AppDB</prop>
   <prop key="org.quartz.dataSource.myDS.user">AppUser</prop>
   <prop key="org.quartz.dataSource.myDS.password">AppPass</prop>
   <prop key="org.quartz.dataSource.myDS.maxConnections">5</prop>
   <prop key="org.quartz.dataSource.myDS.validationQuery">select 0 from dual</prop>
  </props>
  </property>
</bean>

<bean id="hibernateInterceptor" class="org.springframework.orm.hibernate3.HibernateInterceptor">
        <property name="sessionFactory">
            <ref bean="sessionFactory"/>
        </property>
    </bean>
</bean>

Now the Hibernate session should be behaving itself.

4. The Dragon

There is one other gotcha, and it's a big one. To change an existing job, you will need to remove it from the database, as restarting the application will not clean out the database and reregister the job.


This post was originally posted on Richard's blog

Share

Add a comment