Friday, November 11, 2011

Integrating Quartz with JBoss

Quartz Overview

Quartz is a full-featured, open source job scheduling service that can be integrated with, or used along side virtually any Java EE or Java SE application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that are programmed to fulfill the requirements of your application. The Quartz Scheduler includes many enterprise-class features, such as JTA transactions and clustering.
What Can Quartz Do For You?
If your application has tasks that need to occur at given moments in time, or if your system has recurring maintenance jobs then Quartz may be your ideal solution.
Sample uses of job scheduling with Quartz:
  • Driving Process Work flow: As a new order is initially placed, schedule a Job to fire in exactly 2 hours, that will check the status of that order, and trigger a warning notification if an order confirmation message has not yet been received for the order, as well as changing the order's status to 'awaiting intervention'.
  • System Maintenance: Schedule a job to dump the contents of a database into an XML file every business day (all weekdays except holidays) at 11:30 PM.
  • Providing reminder services within an application.

Features

Runtime Environments
  • Quartz can run embedded within another free standing application
  • Quartz can be instantiated within an application server (or servlet container), and participate in XA transactions
  • Quartz can run as a stand-alone program (within its own Java Virtual Machine), to be used via RMI
  • Quartz can be instantiated as a cluster of stand-alone programs (with load-balance and fail-over capabilities)
Job Scheduling
Jobs are scheduled to run when a given Trigger occurs. Triggers can be created with nearly any combination of the following directives:
  • at a certain time of day (to the millisecond)
  • on certain days of the week
  • on certain days of the month
  • on certain days of the year
  • not on certain days listed within a registered Calendar (such as business holidays)
  • repeated a specific number of times
  • repeated until a specific time/date
  • repeated indefinitely
  • repeated with a delay interval
Jobs are given names by their creator and can also be organized into named groups. Triggers may also be given names and placed into groups, in order to easily organize them within the scheduler. Jobs can be added to the scheduler once, but registered with multiple Triggers. Within a J2EE environment, Jobs can perform their work as part of a distributed (XA) transaction.
Job Execution
  • Jobs can be any Java class that implements the simple Job interface, leaving infinite possibilities for the work your Jobs can perform.
  • Job class instances can be instantiated by Quartz, or by your application's framework.
  • When a Trigger occurs, the scheduler notifies zero or more Java objects implementing the JobListener and TriggerListener interfaces (listeners can be simple Java objects, or EJBs, or JMS publishers, etc.). These listeners are also notified after the Job has executed.
  • As Jobs are completed, they return a JobCompletionCode which informs the scheduler of success or failure. The JobCompletionCode can also instruct the scheduler of any actions it should take based on the success/fail code - such as immediate re-execution of the Job.
Job Persistence
  • The design of Quartz includes a JobStore interface that can be implemented to provide various mechanisms for the storage of jobs.
  • With the use of the included JDBCJobStore, all Jobs and Triggers configured as "non-volatile" are stored in a relational database via JDBC.
  • With the use of the included RAMJobStore, all Jobs and Triggers are stored in RAM and therefore do not persist between program executions - but this has the advantage of not requiring an external database.

Integrating Quartz with Jboss


Pre-requisite required for integration
  1. Jboss ( 4.4.2 )
  2. Quartz 1.6.6 ( It can be downloaded from http://quartz-scheduler.org/downloads/catalog )
  3. Additional Jars and libs
  • jboss-xa-jdbc.rar
  • commons-dbcp-all-1.3-r699049.jar
Placing the additional jars/libs required
  1. Copy jboss-xa-jdbc.rar to JBOSS_HOME\server\default\deploy directory
  2. Place commons-dbcp-all-1.3-r699049.jar to JBOSS_HOME\server\default\lib directory
Steps to integrate.
JBoss 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.6.jar and quartz-jboss-1.6.6.jar in the JBOSS_HOME/server/default/lib directory. Then, we need to create a xml file to configure Quartz as a service in JBOSS_HOME/server/default/deploy directory ( xml file can be found with Quartz download lib 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.
             name="user:service=QuartzService,name=QuartzService">      jboss.jca:service=DataSourceBinding,name=QuartzDS               true                         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 = 100    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.tablePrefix = QRTZ_    org.quartz.dataSource.QUARTZ.jndiURL = java:QuartzDS        org.quartz.jobStore.nonManagedTXDataSource = QUARTZ    #org.quartz.dataSource.QUARTZ_NO_TX.driver = oracle.jdbc.driver.OracleDriver    #org.quartz.dataSource.QUARTZ_NO_TX.URL = jdbc:oracle:thin:@(DESCRIPTION =(ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.103)(PORT = 1521)))(CONNECT_DATA =(sid =rajni)(SERVER=shared)))    #org.quartz.dataSource.QUARTZ_NO_TX.user = mercury_dcd    #org.quartz.dataSource.QUARTZ_NO_TX.password = mercury_dcd        org.quartz.jobStore.maxMisfiresToHandleAtATime=60000        org.quartz.plugin.triggHistory.class = \org.quartz.plugins.history.LoggingTriggerHistoryPlugin    org.quartz.plugin.triggHistory.triggerFiredMessage = \Trigger \{1\}.\{0\} fired job \{6\}.\{5\} at: \{4, date, HH:mm:ss MM/dd/yyyy}    org.quartz.plugin.triggHistory.triggerCompleteMessage = \  Trigger \{1\}.\{0\} completed firing job \{6\}.\{5\} at \{4, date, HH:mm:ss MM/dd/yyyy\}.                        
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 quartz 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).
Adding a new job to Quartz requires the following details.
Sample oracle-ds file:

      QuartzDS    oracle.jdbc.xa.client.OracleXADataSource     jdbc:oracle:thin:@(DESCRIPTION =(ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.103)(PORT = 1521)))(CONNECT_DATA =(sid =rajni)(SERVER=shared)))    mercury_dcd    mercury_dcd    TRANSACTION_READ_COMMITTED    5    0     2000    2    true    false    org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter                 mySQL      
Run.bat / Run.sh

Setting classpath to be able to use Quartz libs from Application war
JBOSS_HOME\jboss-4.2.2.GA\server\default\lib\quartz-1.6.6.jar;
JBOSS_HOME\jboss-4.2.2.GA\server\default\lib\commons-dbcp-all-1.3-r699049.jar;
JBOSS_HOME\lib\custom\classes\Executor.class; ( Is discussed below )
JBOSS_HOME\jboss-4.2.2.GA\lib\custom\classes\QuartzScheduler.class; ( Is discussed below )
Note:
  1. On completion of above steps, restart Jboss server and see that the Quartz service is deployed successfully

Working with Quartz

To simplify the pain in creating and manage the jobs within Quartz, I have created a class that manages all the processes like adding job, deleting job, rescheduling job, pause/resume job, metadata and start/stop scheduler.
Currently this class support only CRON TRIGGERS.
  1. Add a new job to Quartz scheduler

Function :
public String addQrtzCronJob(HashMap jobHashMap)
{
try
{
//Getting the schedular context
InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup(((String)jobHashMap.get("dsXName")).toString());
//checking schedular shutdown status
if(scheduler.isShutdown())
{
createLog("\n"+new Date()+" {status:'unsuccess',error:'Schedular for source "+((String)jobHashMap.get("dsXName")).toString()+" already shutdown.',msg:''} ");
return "{status:'unsuccess',error:'Schedular for source "+((String)jobHashMap.get("dsXName")).toString()+" already shutdown.',msg:''}";
}
// defines job and execution class
JobDetail job = new JobDetail(((String)jobHashMap.get("jobName")).toString(), ((String)jobHashMap.get("jobGroup")).toString(), Class.forName(((String)jobHashMap.get("exeClass")).toString()) );
//Passing params to exector class
job.getJobDataMap().put("FUNCTION",((String)jobHashMap.get("funcName")).toString());
job.getJobDataMap().put("PARAMS",((String)jobHashMap.get("funcParams")).toString());
job.getJobDataMap().put("LOGPATH",(String)getQuartzCfgPath());
//Defines cron trigger with cron expression
CronTrigger trigger = new CronTrigger(((String)jobHashMap.get("trgName")).toString(), ((String)jobHashMap.get("tgrGroup")).toString(), ((String)jobHashMap.get("jobName")).toString(),
((String)jobHashMap.get("jobGroup")).toString(), ((String)jobHashMap.get("cronExpr")).toString());
//Setting mis fire instrunction for trigger
trigger.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW);
// Adds the job to schedular
scheduler.addJob(job, true);
// Schedules the job
Date ft = scheduler.scheduleJob(trigger);
createLog("\n"+new Date()+job.getFullName() + " has been scheduled to run at: " + ft+ " and repeat based on expression: "+ trigger.getCronExpression()+" ::getQuartzLogPath()="+getQuartzLogPath());
return "{status:'success',error:'',msg:'Job added successfully'}";
}
catch(Exception e)
{
createLog("\n"+new Date()+" {status:'unsuccess',error:'"+e.toString()+"',msg:''} ");
return "{status:'unsuccess',error:'"+e.toString()+"',msg:''}";
}
}
      ExeClass : The class that is to be called on execution by default it should be “custom.classes.Executor”
      dsXName : The Data source name
      jobName : Job name for the given job
      jobGroup : Job group associated with the job, if job group is null, its takes the default job group.
      trgName : Trigger name for the given job
      tgrGroup : Trigger group name
      cronExpr : CRON Expression to specify the time/period for job execution.
      FuncName : This is the name of function which is to be called when the job executes
      funcParams : Params are the extra information passed to function
  1. Delete a job

    Function:
    public String deleteQrtzCronJob(String dsXName,String jobName,String jobGroup)
    {
    try
    {
    createLog("\n"+new Date()+" deleteQrtzCronJob called dsXName="+dsXName+",jobName="+jobName+",jobGroup="+jobGroup);
    InitialContext ctx = new InitialContext();
    Scheduler scheduler = (Scheduler) ctx.lookup(dsXName);
    boolean deleteJob = scheduler.deleteJob(jobName, jobGroup);
    if(deleteJob)
    {
    createLog("\n"+new Date()+" {status:'success',error:'',msg:'Job deleted successfully.'}");
    return "{status:'success',error:'',msg:'Job deleted successfully.'}";
    }
    else
    {
    createLog("\n"+new Date()+" {status:'unsuccess',error:'Job cannot be deleted',msg:''}");
    return "{status:'unsuccess',error:'Job cannot be deleted',msg:''}";
    }
    }
    catch(Exception e)
    {
    createLog("\n"+new Date()+" {status:'success',error:'"+e.toString()+"',msg:''} ");
    return "{status:'unsuccess',error:'"+e.toString()+"',msg:''}";
    }
    }
    Deleting a job within Quartz requires the following details.
    1. dsXName : The Data source name
    2. jobName : Job name to be deleted
    3. jobGroup : The job group associated with the job to be deleted
  2. Reschedule a job

Function :
public String reScheduleQrtzCronJob(HashMap jobHashMap)
{
try
{
//Getting the schedular context
InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup(((String)jobHashMap.get("dsXName")).toString());
//checking schedular shutdown status
if(scheduler.isShutdown())
{
createLog("\n"+new Date()+" {status:'unsuccess',error:'Schedular for source "+((String)jobHashMap.get("dsXName")).toString()+" already shutdown.',msg:''}");
return "{status:'unsuccess',error:'Schedular for source "+((String)jobHashMap.get("dsXName")).toString()+" already shutdown.',msg:''}";
}
//Defines cron trigger with cron expression
CronTrigger trigger = new CronTrigger(((String)jobHashMap.get("trgName")).toString(), ((String)jobHashMap.get("tgrGroup")).toString(), ((String)jobHashMap.get("jobName")).toString(),
((String)jobHashMap.get("jobGroup")).toString(), ((String)jobHashMap.get("cronExpr")).toString());
//Setting mis fire instrunction for trigger
trigger.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW);
// Adds the job to schedular
Date ft = scheduler.rescheduleJob(((String)jobHashMap.get("trgName")).toString(), ((String)jobHashMap.get("tgrGroup")).toString(), trigger);
createLog("\n"+new Date()+" {status:'success',error:'',msg:'Job rescheduled successfully with expression'"+((String)jobHashMap.get("cronExpr")).toString()+"} ");
return "{status:'success',error:'',msg:'Job rescheduled successfully with expression'"+((String)jobHashMap.get("cronExpr")).toString()+"}";
}
catch(Exception e)
{
createLog("\n"+new Date()+" {status:'success',error:'"+e.toString()+"',msg:''} ");
return "{status:'unsuccess',error:'"+e.toString()+"',msg:''}";
}
}

    Rescheduling a job within Quartz requires the following details.
      ExeClass : The class that is to be called on execution by default it should be “custom.classes.Executor”
      dsXName : The Data source name
      jobName : Job name for the given job
      jobGroup : Job group associated with the job, if job group is null, its takes the default job group.
      trgName : Trigger name for the given job
      tgrGroup : Trigger group name
      cronExpr : CRON Expression to specify the time/period for job execution.

  1. Pause a job

    Function :
public String pauseQrtzCronTrigger(String dsXName,String trgName,String trgGroup)
{
try
{
createLog("\n"+new Date()+" pauseQrtzCronTrigger called dsXName="+dsXName+",trgName="+trgName+",trgGroup="+trgGroup);
InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup(dsXName);
scheduler.pauseTrigger(trgName, trgGroup);
createLog("\n"+new Date()+" {status:'success',error:'',msg:'Trigger stop/paused successfully.'}");
return "{status:'success',error:'',msg:'Trigger stop/paused successfully.'}";
}
catch(Exception e)
{
createLog("\n"+new Date()+" pauseQrtzCronTrigger exception : {status:'unsuccess',error:'"+e.toString()+"',msg:''} ");
return "{status:'unsuccess',error:'"+e.toString()+"',msg:''}";
}
}
    Pause a job within Quartz requires the following details.
    1. dsXName : The Data source name
    2. jobName : Job name to be paused
      jobGroup : The job group associated with the job to be paused
  1. Resume a job

    Function :
public String resumeQrtzCronJob(String dsXName,String jobName,String jobGroup)
{
try
{
createLog("\n"+new Date()+" resumeQrtzCronJob called dsXName="+dsXName+",jobName="+jobName+",jobGroup="+jobGroup);
InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup(dsXName);
scheduler.resumeJob(jobName, jobGroup);
createLog("\n"+new Date()+" {status:'success',error:'',msg:'Job resumed successfully.'}");
return "{status:'success',error:'',msg:'Job resumed successfully.'}";
}
catch(Exception e)
{
createLog("\n"+new Date()+" resumeQrtzCronJob exception : {status:'unsuccess',error:'"+e.toString()+"',msg:''} ");
return "{status:'unsuccess',error:'"+e.toString()+"',msg:''}";
}
}

    Resume a job within Quartz requires the following details.
    1. dsXName : The Data source name
    2. jobName : Job name to be resumed
    3. jobGroup : The job group associated with the job to be resumed
  1. Start scheduler

    Function :
public String startQrtzSchedular(String dsXName)
{
try
{
createLog("\n"+new Date()+" startQrtzSchedular called dsXName="+dsXName);
InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup(dsXName);
if(!scheduler.isShutdown())
{
createLog("\n"+new Date()+"checking shutdown status {status:'unsuccess',error:'Schedular for source "+dsXName+" already running.',msg:''}");
return "{status:'unsuccess',error:'Schedular for source "+dsXName+" already running.',msg:''}";
}
scheduler.start();
return "{status:'success',error:'',msg:'Scheduler started successfully'}";
}
catch(Exception e)
{
createLog("\n"+new Date()+" {status:'success',error:'"+e.toString()+"',msg:''} ");
return "{status:'unsuccess',error:'"+e.toString()+"',msg:''}";
}
}

    Start Scheduler with Quartz requires the following details.
      dsXName : The Data source name
  1. Stop scheduler


    Function :
public String stopQrtzSchedular(String dsXName)
{
try
{
createLog("\n"+new Date()+"stopQrtzSchedular called with dsXName="+dsXName);
InitialContext ctx = new InitialContext();
Scheduler scheduler = (Scheduler) ctx.lookup(dsXName);
if(scheduler.isShutdown())
{
createLog("\n"+new Date()+" {status:'unsuccess',error:'Schedular for source "+dsXName+" already shutdown.',msg:''} ");
return "{status:'unsuccess',error:'Schedular for source "+dsXName+" already shutdown.',msg:''}";
}
scheduler.shutdown(true);
return "{status:'success',error:'',msg:'Scheduler shutdown successfully'}";
}
catch(Exception e)
{
createLog("\n"+new Date()+" execption in stopQrtzSchedular {status:'success',error:'"+e.toString()+"',msg:''} ");
return "{status:'unsuccess',error:'"+e.toString()+"',msg:''}";
}
}

    Stop Scheduler with Quartz requires the following details.
      1. dsXName : The Data source name

No comments: