A következő címkéjű bejegyzések mutatása: Quartz. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: Quartz. Összes bejegyzés megjelenítése

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!

2012. március 20., kedd

Quartz - Feladatütemezés a Javából

A web-alkalmazások fejlesztése során előbb vagy utóbb megjelennek olyan feladatok amelyeket folyamatosan végre kellene hajtani meghatározott időközönként. Ilyen feladat lehet például a hírlevelek heti egyszeri kiküldése, a keresési indexek óránkénti frissítése vagy éppen az inaktív felhasználók havi törlése. Továbbá vannak olyan taszkok is, melyeket egyetlen egyszer kell elvégezni egy adott jövőbeli időpontban.

A Quartz egy tranzakciókezelést és klaszterezést is támogató nyílt forráskódú feladat ütemező, melyet egyaránt használhatunk Java SE ill. Java EE környezetben is, akár több ezer feladat ütemezett indításához. Mivel a hivatalos oldalon egy jól használható dokumentáció és sok minta példa segíti a Quartz megismerését, ezért a mostani leírásomat inkább egy áttekintő bejegyzésnek szántam. Először következzenek a Quartz 2 által használt fogalmak.

Job
Az a feladat, amit időzítve (N-szer) vagy ütemezetten (folyamatosan) végre kívánunk hajtani. A feladat mindig egy metódus meghívása lesz, mégpedig egy olyan osztálynak az execute() metódusa, ami implementálja a Job interface-t.

JobDetail
Egy job osztályból készített definíciós példány, ami a job-ot további tulajdonságokkal egészíti ki. Ilyen tulajdonság lehet a job neve, a job csoportja vagy éppen a data objectek (JobDataMap). Egy job-ból akár több JobDetails is létrehozható eltérő névvel, csoporttal és hozzárendelt adatokkal.

JobDataMap
A JobDataMap használatával data object-eket menthetünk el bármely JobDetail-hez, melyek a Job végrehajtása során elérhetők és módosíthatók az execute() metódus JobExecutionContext paraméterének felhasználásával. A JobDataMap-be mindig szerializálható objektumokat tegyünk, és ügyeljünk az eltérő osztály verzióikból származó problémákra is. Ajánlott a standard Java osztályokat használni (pl.:String, Long).

Trigger
Egy olyan esemény, ami kiváltja (tüzelés) a job végrehajtását. A kiváltó esemény lehet N-szer (SimpleTrigger) vagy folyamatosan ismétlődő (CronTrigger).

Scheduler
A jobok és a triggerek kezelését végzi, feladata pedig a jobok végrehajtása a triggerek tüzelésekor. A feladatok ütemezéséhez először is el kell indítani egy scheduler példányt, amit Java SE környezetben a kódból, programozott módon végezhetünk el, míg Java EE platformon általában az alkalmazásszerver indulásakor valósul meg. Az ütemező kikapcsolt/szünetelt állapotában, illetve az az alkalmazás szerver leállásakor a feladatok időzített végrehajtása is szünetel.

JobStore
A JobStore feladata a scheduler, a jobok és a triggerek működéséhez szükséges információk tárolása, melyet perzisztencia szerint az alábbi típusokba sorolhatunk:

1. TerracottaJobStore
  • Kereskedelmi termék.
  • Az adatok perzisztenciáját a Terracotta szerver biztosítja.
  • Klaszterezett és Nem klaszterezett módban is használható.

2. RamJobStore
  • Az időzítési információk memóriában történő tárolására szolgál.
  • Mivel nem perzisztens, az alkalmazás leállításával az időzítési információk elvesznek. 
 
3. JDBCJobStore
  • Az ütemezési információk tárolása perzisztens módon az erre kijelölt adatbázisban történik.
  • A szerver leállás miatt lekésett triggerek újratüzelése megoldható a következő induláskor.
  • A Quartz adatabázist létrehozó és inicializáló SQL szkriptek, a quartz tömörített állomány docs/dbTables/ könyvtára alatt találhatók.

3.1. A JDBCJobStore tranzakció típusai
  • JobStoreTX: A tranzakciókat a Quartz maga kezeli le, így standalone vagy JTA tranzakciót nem használó alkalmazásoknál érdemes használni.
  • JobStoreCMT: A menedzselt JTA tranzakcióra támaszkodik, így Enterprise környezetben ezt célszerű használni. Használatához, egy menedzselt és egy nem menedzselt datasource beállítása szükséges. A menedzselt datasource lehet lokális vagy XA típusú.

3.2. A JDBCJobStore klaszterezési lehetősége
  • Minden node ugyanazt a Quartz adatbázist használja.
  • A Quartz biztosítja az automatikus load balancing-ot. Bármely trigger tüzelésekor mindig csak az egyik node fogja végrehajtani a jobot.
  • Fail-over biztosítása. Ha az egyik node kiesik a többi node ezt detektálja, így a félbeszakadt jobok ismételten végrehajtásra kerülnek.


A folytatásban a Quartz különböző környezetekhez való bekonfigurálását fogom ismertetni.