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


