2012. április 18., szerda

Quartz - Bevetés Java EE környezetben

Egy klaszterezett WebSphere 6.1 alkalmazás szerver alatt futó webalkalmazáshoz fogok egy olyan modult elkészíteni, amely minden nap 21:00 órakor végrehajtja a feliratkozott felhasználóknak a hírlevél elküldését. Az ismertetésre kerülő példa más alkalmazás szerveren vagy egy Apache Tomcat-en is - kisebb módosításokkal - bevethető!


1. Döntések az ütemezés végrehajtásához

Először is el kell döntenünk, hogy az időzítési adatok tárolását perzisztens vagy nem perzisztens módon kívánjuk végrehajtani. Bár a RamJobStore felkonfigurálása egyszerűbb, a biztonságosabb kezelés érdekében (pl.: szerver leállás miatti lekésett triggerek újratüzelése) érdemes a JdbcJobStore-t választani. A következő lépés, hogy eldöntsük melyik tranzakció típust használjuk a Quartz táblákat tartalmazó adatbázis eléréséhez. A JobStoreTX-re esett a választás, mivel az elkészített alkalmazás nem használja a WebSphere által kezelt JTA tranzakciókezelést. A WebSphere admin konzolon korábban felvett adatforrásra a jdbc/wasdb JNDI névvel fogok hivatkozni a quartz konfigban, nem pedig properties bejegyzésekkel.

Mivel az alkalmazás egy klaszter több szerverére is telepítve lett, az ütemezés megfelelő végrehajtásához dönteni kell a Quartz klaszterezési lehetőségének kihasználása ill. az ütemezett feladatoknak a WebSphere klaszter egyik szerverén történő végrehajtása mellett. (Az utóbbit pl. az egyik szerver hoszt nevére való szűréssel tehetnénk meg.) A választásom a Quartz beépített klaszterezésére esett, mivel ez hibatűrő (az egyik szerver leállásakor a másik átveszi az időzített feladatok végrehajtását) és biztosítja a megfelelő terheléselosztást is.

2. A Quartz konfigurálása

Az alkalmazás Oracle adatbázist használ, ezért a quartz telepítési csomagban található (docs/table/tables_oracle.sql) szkriptet kell lefuttatni a Quartz által használt táblák létrehozásához. Végül az előzetes döntések alapján elkészített quartz.properties állományt tegyük ki az alkalmazás classpath-ára.
#Main Scheduler
org.quartz.scheduler.instanceName=WebSphereClusteredScheduler
org.quartz.scheduler.instanceId=AUTO

#ThreadPool
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=1

#JobStore
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.misfireThreshold=60000
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.dataSource=wasDs
org.quartz.dataSource.wasDs.jndiURL=jdbc/wasdb
org.quartz.jobStore.acquireTriggersWithinLock=true
org.quartz.jobStore.txIsolationLevelSerializable=true

#Clustered config
org.quartz.jobStore.isClustered=true 
org.quartz.jobStore.clusterCheckinInterval=10000

#Update check
org.quartz.scheduler.skipUpdateCheck=true

3. A hírlevélküldési feladat időzítése

A konténerben futó webalkalmazások esetén, a Quartz időzítő automatikus indításához és leállításához a web.xml fájlban kell felvenni egy listenert vagy egy startup servletet. A hírlevél küldéshez tartozó job és trigger inicializálását érdemes az alkalmazás indulásakor elvégezni, így ehhez célszerű létrehozni egy saját szervletet, SchedulerInitializer névvel. Bár a szervlet init() metódusa a klaszter minden szerverén lefut, az inicializálást csak az egyik szerveren kell végrehajtani, így elkerülhető az ütemezési feladatok többszöröződése.
 
  QuartzInitializer 
  
   org.quartz.ee.servlet.QuartzInitializerServlet
  
  
   start-scheduler-on-loadtrue
  
    shutdown-on-unloadtrue
  2


 
  
     SchedulerInitializer
    
   
     test.SchedulerInitializerServlet
    
   3
Az init() metódusban történik a hírlevél küldési job és trigger szükség szerinti létrehozása, majd ellenőrzésképpen az infók kiíratása. Az alábbi Clean Coding elvek figyelembe vételével megalkotott kódrészlet a Quartz 2.1 -re épül, így az ennek megfelelő API-t használtam.
public class SchedulerInitializerServlet extends HttpServlet{
 private Scheduler sched = null;
 private static final String GROUP_NAME="NG";
 private static final String NEWSLETTER_JOB_NAME="NLJ";
 private static final String NEWSLETTER_TRIGGER_NAME="NLT";
 private static final String CRON="0 0 21 * * ?";
 
 @Override
 public void init(ServletConfig cfg) throws ServletException{
   super.init(cfg); 
   startScheduling();
   listOfJobsAndTriggers();
 }

 private void startScheduling() throws Exception{
   initScheduler(); 
   if(!isSavedNewsletterJob()) 
     sched.scheduleJob(createNewsletterJobDetail(), 
     createNewsletterTrigger(CRON)); 
 }

 private void initScheduler() throws SchedulerException {
   if (sched == null)
     sched = StdSchedulerFactory.getDefaultScheduler(); 
   if (!sched.isStarted())
     sched.start();
 }

 private boolean isSavedNewsletterJob(){
   return sched.checkExists(
     JobKey.jobKey(NEWSLETTER_JOB_NAME,GROUP_NAME));
 }

 private JobDetail createNewsletterJobDetail() {
   return JobBuilder.newJob(NewsletterSenderJob.class)
     .withIdentity(NEWSLETTER_JOB_NAME, GROUP_NAME)
     .build();
 }
 
 private Trigger createNewsletterTrigger(String cron) { 
   return TriggerBuilder.newTrigger()
     .withIdentity(NEWSLETTER_TRIGGER_NAME, GROUP_NAME)
     .withSchedule(CronScheduleBuilder.cronSchedule(cron))
     .startNow()
     .build();
 }

 private void listOfJobsAndTriggers() throws Exception{
   for(String grp: sched.getJobGroupNames())
     for(JobKey jk : sched.getJobKeys(jobGroupEquals(grp)))
       log.info("Found job identified by:"+jk);
 
   for(String grp: sched.getTriggerGroupNames()) {
     for(TriggerKey tk : sched.getTriggerKeys(
      triggerGroupEquals(grp))) {
         log.info("Found trigger identified by:"+tk); 
 }
 
}
A hírlevél kiküldéséhez definiált CronTrigger tüzelésekor, a Job interface-t implementáló NewsletterSenderJob osztályból mindig egy új példány fog létrejönni, melynek az execute() metódusa lesz végrehajtva. A @DisallowConcurrentExecution annotáció biztosítja, hogy - JobDetails-enként - egyszerre csak egy példány fusson a NewsletterSender job-ból. Habár a @PersistJobDataAfterExecution működését jelenleg nem használom ki, az annotáció hozzáadásával elérhetővé válik, hogy a JobDetails-hez rendelt data objectek módosításai automatikusan mentésre kerüljenek.
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class NewsletterSenderJob implements Job{ 
 public NewsletterSenderJob() {}
 public void execute(JobExecutionContext context) 
   throws JobExecutionException {
     //newsletter sender logic
 }
}

2012. április 2., hétfő

Quartz - A feladatütemező konfigurálása

Az előző cikkben a Quartz által használt fogalmakról írtam, a mostani bejegyzésemben pedig a Quartz eltérő környezetekhez való felkonfigurálásáról fogok blogolni.

A Quartz konfigurációs beállításait a classpath-ra elhelyezett quartz.properties fájlban adhatjuk meg, ami alapján a StdSchedulerFactory legyártja az időzítéshez használt Scheduler példányt. A RAMJobStore használatához elég a quartz.properties fájlt megírni, a JDBCJobStore használatához ezen felül még létre kell hozni a szükséges adatbázis táblákat is (/docs/dbTable mappa). A következőkben a quartz.properties fájl minimális konfigurációját fogom ismertetni néhány lehetséges környezethez.

RAMJobStore – Standalone környezethez

Az időzítési információkat az alkalmazás minden indulásakor inicializálni kell, továbbá a lekésett triggerekhez tartozó jobok újbóli végrehajtására nincs lehetőség.
#Configure MainScheduler
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.threadPool.threadCount = 3

#Configure JobStore
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

JDBCJobStore/JobStoreTX – Standalone környezethez

A konfiguráció egy standalone java alkalmazás által használt beállításokat tartalmazza. Az ütemező adatainak perzisztens tárolásához egy MySQL adatbázis került bekonfigurálásra, melynek az attribútumait a datasource résznél property-kkel definiáltam.
#Configure Main 
Schedulerorg.quartz.scheduler.instanceName = MyTestScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.threadPool.threadCount = 3

#Configure JobStore
org.quartz.jobStore.useProperties = true
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource=myTestDS

#Configure Datasources (by properties)
org.quartz.dataSource.myTestDS.driver=com.mysql.jdbc.Driver
org.quartz.dataSource.myTestDS.URL=jdbc:mysql://localhost:3306/testQuartz
org.quartz.dataSource.myTestDS.user=testQuartz
org.quartz.dataSource.myTestDS.password=testQuartz
org.quartz.dataSource.myTestDS.maxConnections=30

JDBCJobStore/JobStoreCMT – Standalone környezethez

A konfiguráció egy Java EE alkalmazás ütemezőjének beállításait mutatja. Az ütemező adatainak a tárolásához szintén egy MySQL adatbázis lett bekonfigurálva. A működéshez két datasource-t kell beállítani, egy JTA tranzakciókban résztvevő (myTestDS) és egy nem tranzakcionális (myTestDSNonManagedTX) datasource-t. Az adatforrásokat az előző példával ellentétben most nem property-vel, hanem JNDI-al adtam meg.
#Configure Main Scheduler
org.quartz.scheduler.instanceName = MyTestScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.threadPool.threadCount = 3

#Configure JobStore
org.quartz.jobStore.useProperties = true
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = myTestDS
org.quartz.jobStore.nonManagedTXDataSource = myTestDSNonManagedTX 

#Configure Datasources (by jndi)
org.quartz.dataSource.myTestDS.jndiURL=java:/dbDatasource
org.quartz.dataSource.myTestDSNonManagedTX.jndiURL=java:/dbDatasourceNonManaged

JDBCJobStore/JobStoreTX – Clustered környezethez

Klaszterezett környezetben minden node-nak ugyanazt a quartz.properties beállításokat kell használnia, csupán a ThreadPool méret és az InstanceId lehet eltérő. A node-oknak egyedi instanceId-vel kell rendelkezniük, ami az AUTO értékkel könnyen megvalósítható.
#Configure Main Scheduler
org.quartz.scheduler.instanceName = MyClusteredTestScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.threadPool.threadCount = 3

#Configure JobStore
org.quartz.jobStore.useProperties = true
org.quartz.jobStore.acquireTriggersWithinLock=true
org.quartz.jobStore.txIsolationLevelSerializable=true
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = myTestDS

#Clustered Configuration
org.quartz.jobStore.isClustered = true 
org.quartz.jobStore.clusterCheckinInterval = 10000

#Configure Datasources (by properties)
org.quartz.dataSource.myTestDS.driver=com.mysql.jdbc.Driver
org.quartz.dataSource.myTestDS.URL=jdbc:mysql://localhost:3306/testQuartz
org.quartz.dataSource.myTestDS.user=testQuartz
org.quartz.dataSource.myTestDS.password=testQuartz
org.quartz.dataSource.myTestDS.maxConnections=30
A befejező részben egy WebSphere alatt futó webalkalmazáshoz fogok időzített feladatokat kódolni, ne hagyd ki a következő bejegyzésemet sem!