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-load true shutdown-on-unload true 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 } }