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
 }
}

6 megjegyzés:

  1. WebSphere 6.1

    Reg hallottam rola, csak kivancsisagbol: ez valami melohelyi project?

    VálaszTörlés
    Válaszok
    1. Hali!

      Pár helyen használunk még WebSphere 6.1 -et, leginkább banki és nagyvállalati szektorban, bár a migrációk mindenhol elkezdődtek. Jaja, kb. 1 évvel ezelőtti projektben kódoltam le a fenti sorokat.

      Törlés
  2. Java EE 6 -ban a @Schedule annotációval hasonló célt tudsz megvalósítani. Jól tévedek? Bár WebSphere 6.1 -nél gondolom ez nem volt opció...

    VálaszTörlés
    Válaszok
    1. Igen a @Schedule esetében is erről van szó! Egyébként WAS 6.1 alatt is van egy beépített időzítő szolgáltatás, de ennek a használatát inkább nem erőltetném.

      Törlés
  3. Kedves Bakai Balázs!

    Az interneten - google - bogarászva bukkantam rá erre az oldalra.
    Egy jellemzően IT területre specializálódott fejvadász cég munkatársa vagyok, egy IT megbízásából keresünk most Websphere Apllication Server specialistát. Sajnos elég kevés ezen a téren az aktív álláskereső, így mindenhol próbálok vadászni.
    Azért szeretném felvenni Önnel a kapcsolatot, mert lehetséges, hogy ismerősi körében van olyan, aki Websphere-ben tapasztalt és esetleg meg lehetne keresni ezzel az állásajánlattal.
    Tudna nekem ebben segíteni?
    Segítségét előre is köszönöm. (elérhetőségem lentebb)

    Üdvözlettel,
    Éles Klára

    Senior HR Consultant

    Tech People Hungary Kft.
    1053 Budapest, Reáltanoda u. 11. I.em. 1.
    Tel/Fax: +36 1 789-7416
    Mobile: +36 30 730-2543
    E-mail: ke@tech-people.com
    www.tech-people.com

    VálaszTörlés
    Válaszok
    1. Köszönöm a bizalmat! A lehetőségekkel kapcsolatban hamarosan felveszem önnel a kapcsolatot!

      Törlés