Zamanlanmış Görevlerin Quartz ile Dinamik Yönetimi — 1.Bölüm

Esat KOÇ
Akbank Teknoloji
Published in
5 min readMar 31, 2023

Yeni yazımızda zamanlanmış görevlerin dinamik bir şekilde nasıl yönetildiğini size anlatmak istiyoruz. Akbank Teknoloji Ekibi olarak görev yönetimlerimizi Quartz Job Scheduler (kısaca Quartz) ile yürütüyoruz. Quartz, basit yapılardan tutun da büyük kurumsal uygulamalara kadar farklı alanlarda kullanılabilen, zengin özelliklere sahip Java tabanlı zamanlanmış bir görev yönetim kütüphanesidir. (.NET dünyasındaki karşılığı Quartz.NET’dir.)

Eğer, belirli işlerin (çoğunlukla uzun zaman alacak işlerin) istediğimiz zamanda otomatik bir şekilde arka planda çalışmasını istiyorsak Quartz kullanabiliriz. Örneğin;

  • Şifresinin süresi geçmiş kullanıcılar için hatırlatıcı epostaların gönderilmesi,
  • Belirli periyotlarda dosyaların transfer edilmesi, silinmesi vb. işlerin yapılması,
  • Veritabanlarının taranarak raporlar oluşturulması,
  • Bir e-ticaret uygulamasının sipariş kuyruğuna yeni ekleme yapıldığında sipariş modülüne bilgilendirme yapılması gibi sayısız örnek için bir görev yönetim aracına ihtiyacınız olabilir.

İşte iki bölümden oluşan bu serinin ilk bölümünde, öncelikle Quartz kütüphanesinin temel bileşenlerini öğrenecek, daha sonra örnek bir Spring uygulamasında belirli zamanlarda çalışmasını istediğimiz iş parçacıklarını nasıl çalıştıracağımızı göreceğiz. Serinin ikinci bölümünde ise ilk bölümde öğrendiklerimizi pratiğe dökeceğimiz mini bir ihale sistemi yazacağız.

Temel Quartz elemanları

Quartz ile görev yönetimi yapabilmek için kullanılan temel bileşenler vardır. Bunları Job, JobDetail, Trigger ve Scheduler olarak sıralayabiliriz. Şimdi tek tek bu bileşenlerin üzerinden geçip nasıl çalıştıklarına göz atalım.

Job

Belirli zamanlarda çalıştırmak istediğimizde, yapılması gereken işin ne olduğunu söyleyen sınıftır.

Pure Java’da implemente ederken, Job interface’ini implemente eden bir sınıf yazmamız gerekir.

package org.quartz;

public interface Job {

public void execute(JobExecutionContext context)
throws JobExecutionException;
}

Spring uygulamasında ise, QuartzJobBean interface’ini implemente edebiliriz.

// HelloJob.java
@Component
public class HelloJob extends QuartzJobBean {

@Override
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
// Your job implementations
}

JobDetail

Job ile ilgili diğer bilgileri tutan sınıftır. Job’ın tanımı, çalıştıracağı execute metodunun yer aldığı sınıf, varsa özel bir key ve buna benzer diğer bilgiler bu sınıfta tutulur. JobDetail oluşturmak için Quartz’ın builder pattern’i ile bize sunduğu bir yapı mevcuttur.

JobDetail jobDetail = newJob(HelloJob.class)
.withIdentity("myJob", "myApp")
.storeDurably()
.build();

Burada HelloJob sınıfı için detay bilgileri içeren JobDetail yazarak daha sonra erişebilmek için withIdentity()bir key bilgisi JobKey verdik. Bu key bilgisi, name ve group parçalarından oluşur.

Yazdığımız JobDetail’in bellekte değil de kalıcı olarak tutulmasını istediğimiz içinse storeDurably() parametresini ekledik. Bu parametrenin geçerli olabilmesi için store tipinin de ayarlanması gerekir. Bu konuya daha sonra tekrar değineceğiz.

Trigger

Yaptığımız job’un ne zaman çalışacağını belirtebilmek için yazacağımız bileşene Trigger bileşeni diyoruz. Quartz’ içinde kullanabileceğimiz iki türlü Trigger mevcuttur. Bunlar SimpleTrigger ve CronTrigger

SimpleTrigger ile job’ın çalışmasını istediğimiz aralık bilgilerini ve çalışma sayısını verebiliyoruz. Aşağıdaki örnekte, her 2 saniyede bir sonsuza kadar Job’ı çalıştıracak bir Trigger yazıyoruz.

Trigger simpleTrigger = newTrigger()
.withIdentity("myTrigger", "group1")
.forJob(jobDetail) // hangi job'ı çalıştıracağı
.startNow()
.withSchedule(
simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever()
)
.build();

CronTrigger ‘la ise Job’ın çalışma zamanını takvim elementleri (saniye, dakika, saat, gün, ay, yıl) ile ilişkilendirebiliyoruz. Bunun için cron ifadesi denilen bir yapıyı kullanmamız gerekiyor. Bu yapıyı öğrenmek için aşağıdaki linki inceleyebilirsiniz. SimpleTrigger’dan çok daha esnek bir yapı sunuyor.

Aşağıdaki örnekte ise haftanın yalnızca çarşamba ve cuma günlerinin ilk dakikalarının ilk saniyesinden başlayarak her 20 saniyede bir Job’ı çalıştıracak bir Trigger yazıyoruz.

    Trigger cronTrigger = TriggerBuilder
.newTrigger()
.forJob(jobDetail) // hangi job'ı çalıştıracağı
.withIdentity(triggerKey)
.withSchedule(
CronScheduleBuilder
.cronSchedule("0/20 * * ? * WED,FRI")
.inTimeZone(TimeZone.getTimeZone("Europe/Istanbul"))
)
.build();

Scheduler

Scheduler, Job’ların dinamik olarak yönetilmesini sağlayan, yani Job’u ilgili jobDetail ve Trigger’i ile schedule, reschedule, pause ve resume eden sınıftır.

Yukarıda yazdığımız jobDetail ve Trigger’ını aşağıdaki gibi schedule ediyoruz. Spring uygulamalarında dependency injection ile Schedulerbağımlılığını istediğimiz component’a verebiliriz.

// MyService.java
@Service
public class MyService {

private final Scheduler scheduler;

public MyService(Scheduler scheduler) {
this.scheduler = scheduler;
}

public void scheduleJob() {
scheduler.scheduleJob(jobDetail, cronTrigger);
}
}

Spring Boot kullandığımız için istediğimiz component’e otomatik olarak yapılandırılmış Scheduler ‘i bağımlılık olarak verebiliyoruz. (Aynı uygulama içerisinde birden fazla Scheduler kullanılmak istenirse veya Spring Boot kullanılmıyorsa söz konusu sınıflar Bean olarak ayrıca yaratılıp enjekte edilmelidir)

Store Tipi

Quartz, Job’ların ve Trigger’ların saklanma mekanizması olarak ram store ve jdbc store olmak üzere iki ayrı yapı sunar.

Ram Store: Job bilgileri ram bellekte tutulur. Performans açısından oldukça hızlı olmakla beraber uygulama kapandığında tüm bilgiler silinir. (Varsayılan konfigürasyondur.)

#application.properties
spring.quartz.job-store-type=memory

Jdbc Store: Job bilgileri veri tabanında Quartz için oluşturulmuş özel tablolarda tutulur. İki türlü jdbc store yapısı mevcuttur. Birincisi Quartz’ın kendi transaction manager’ını kullandığı JobStoreTX yapısı, ikincisi ise Quartz’ın uygulamanın transaction manager’ını kullandığı JobStoreCMT yapısıdır.

Spring Boot kullanıldığında otomatik olarak Quartz da Spring tarafından yönetilen transaction yapısına dahil olmuş olur (jobStoreCMT).

spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate

# quartz tablolarına prefix verilmesi için
spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_

# datasource bilgileri
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=${USERNAME}
spring.datasource.password=${PASSWORD}
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect

Quartz tablolarının oluşturulması için gerekli sql script’lerini veri tabanı özelinde aşağıdaki GitHub linkinde bulabilirsiniz.

https://github.com/quartz-scheduler/quartz/tree/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore

Misfire Durumları

Quartz Trigger’ları bazı sebeplerden dolayı (Network gecikmesi, uygulama kapanması vb.) istenilen zamanda Job’ı tetikleyemezse misfire durumu oluşmuş demektir.

Öncelikle, Job’ın milisaniye cinsinden ne kadar süre içerisinde tetiklenmemesi durumunda misfire oluşacağını Quartz’a aşağıdaki parametre ile bildirmemiz gerekir. Default değerimiz 60000 ms’dir. (60 saniye.)

spring.quartz.properties.org.quartz.jobStore.misfireThreshold=5000

Peki misfire durumu oluşunca Quartz ne yapar ? Misfire durumlarında eğer Trigger’ımız cron tipinde ise varsayılan olarak MISFIRE_INSTRUCTION_FIRE_NOWsabitini kullanarak misfire olmuş Job’ı hemen tetikler. Diğer misfire sabitleri ise şöyledir:

MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
MISFIRE_INSTRUCTION_FIRE_NOW
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT (yalnızca SimpleTrigger)
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT (yalnızca SimpleTrigger)
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT (yalnızca SimpleTrigger)
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT (yalnızca SimpleTrigger)
MISFIRE_INSTRUCTION_DO_NOTHING (yalnızca CronTrigger)

Ancak farklı davranmasını istiyorsak, Trigger yaratılırken bu yapılandırmayı da özelleştirebiliriz.

    CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(triggerKey)
.withSchedule(
CronScheduleBuilder
.cronSchedule("0/20 * * * * ?")
// Misfire instruction
.withMisfireHandlingInstructionFireAndProceed()
)
.build();

Paralel Çalışma

Quartz, aynı quartz tablolarının birden fazla Scheduler instance’ı tarafından kullanımına izin verir. Bu durumda, aynı işin tüm instance’lar tarafından yapılmasını engellemek için bazı konfigürasyonlar yapmamız gerekir. Aksi takdirde, istenmeyen durumlara yol açabiliriz.

Öncelikle, application.properties dosyasında aşağıdaki parametreleri vermemiz gerekir.

# How many job will be running at the same time. Default is 10
spring.quartz.properties.org.quartz.threadPool.threadCount=5

# Giving different scheduler to different name
spring.quartz.scheduler-name=demoapp

# Same scheduler will be running in different instance
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO

Daha sonra da ilgili job için execute metodunu yazdığımız Job sınıfına annotasyon olarak @DisallowConcurrentExecution veriyoruz.

@Component
@DisallowConcurrentExecution
public class DetermineAuctionWinnerJob extends QuartzJobBean {

@Override
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
// your job logic

}
}

Özet

Yazımızın bu ilk bölümünde, Quartz kütüphanesinin temel bileşenlerinin üzerinden geçerek nasıl kullanıldığı öğrendik. Öğrendiklerimizi uygulayabilmek için ikinci bölümde Spring Boot uygulaması ile mini bir ihale sistemi yazacağız.

İkinci bölümümüzde görüşmek üzere…

Referanslar

--

--