Published on

Spring Boot İle Scheduler Yapısı Oluşturma II

Authors

Giriş

Herkese selamlar! Bu yazıda, Spring Boot kullanarak scheduler yapısı oluşturma serimize devam ediyoruz. Bir önceki yazımda dinamik olarak scheduler yapısı nasıl kurulabilir bundan bahsetmiştik, şimdi ise projemizi biraz daha genişleterek yeni özellikler kazandıralım.

Yeni Özellikler

Önceki yazımızdaki projemizde bir API yazmıştık ve bu API'ye gelen istekte bize "cronExpression" ve "name" alanları geliyordu. Biz "cronExpression" a göre belirli aralıklarla bu "name" i output'a yazdırıyorduk.

Şimdi bu basit uygulamayı biraz daha kapsamlı bir hale getirelim. Bir job oluşturduğumuzda onu çalıştırmak, güncellemek ve durdurmak isteriz. Önceki uygulamamızda bunları karşılayan bir akış bulunmamaktaydı, şimdi bu eksiklikleri teker teker giderelim.

1. Çalışan Task'ın İptali

@Service
public class SchedulerService {

    private final TaskScheduler taskScheduler;

    public SchedulerService(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public void addTask(TaskDefinition taskDefinition) {
        MyJob myJob = new MyJob(taskDefinition.getName());
        CronTrigger cronTrigger = new CronTrigger(taskDefinition.getCronExpression());
        taskScheduler.schedule(myJob, cronTrigger);
    }
}

Önceki geliştirmemizde yukarıda görüldüğü gibi SchedulerService sınıfı içerisinde bir task'ın çalışması için TaskScheduler'ın schedule methodunu çağırıyorduk. Peki ya task'ı iptal etmek istersek ne yapacağız?

Aslında schedule methodu bize bir response dönüyor.

SchedulerService.java
ScheduledFuture<?> schedule = taskScheduler.schedule(myJob, cronTrigger)

Önceki yazımızda thread'lerle ilgili bahsettiğimiz konuda Spring, çalışan task'ları yönetmek için ScheduledThreadPool kullandığından bahsetmiştik. Java'da bir işlem thread'lerle yönetildiğinde Future adlı bir nesne döner. Future adından da anlaşılacağı gibi "gelecektir". Burada yapılan işlem henüz tamamlanmadı ama sana bir Future obje dönüyorum, tamamlandığında yapılacak bir işlemin varsa bunu kullanabilirsin der Java. Bu nedenle TaskScheduler da aslında çalışan task'ları işleme aldıktan sonra Future objesinden kalıtılmış, task'lar için özelleştirilen ScheduledFuture objesi döner.

Bu dönen ScheduledFuture objesinin cancel methodu var, bu method sayesinde çalışan bir task'ın, çalışmasını durdurabiliriz.

Bir T anında bunu gerçekleştirebilmek için bu nesneyi bir yerde saklamalıyız. Bunu yapmak için bir map oluşturabiliriz, bu map'in key değeri task'ın id'si, value değeri ise o task'ın scheduleFuture objesi olacak şekilde kurgulayabiliriz. Bunun için öncelikle her task'a unique bir id tanımlamalıyız, bu nedenle TaskDefinition sınıfını güncelliyoruz.

TaskDefinition.java
public class TaskDefinition {
    private String id;
    private String cronExpression;
    private String name;

    public TaskDefinition(String cronExpression, String name) {
        this.cronExpression = cronExpression;
        this.name = name;
    }

    public void initialize() {
        this.id = UUID.randomUUID().toString();
    }

    public String getId() {
        return id;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public String getName() {
        return name;
    }
}

Şimdi SchedulerService'imizi güncelleyebiliriz, içerisine bir map ekliyoruz, ve addTask methodunda TaskScheduler'dan dönen değeri ve task'ın id'sini bu map'e kaydediyoruz. TaskDefinition'ın initialize methodunu çağırmamızın nedeni ise içerisindeki id değerinin atanması ve bundan sonra ekleyebileceğimiz bazı kontrollerin bu method içerisinde yapılmasını sağlamak.

SchedulerService.java

@Service
public class SchedulerService {

    private final TaskScheduler taskScheduler;
    private final Map<String, ScheduledFuture<?>> tasksMap = new ConcurrentHashMap<>();

    public SchedulerService(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public void addTask(TaskDefinition taskDefinition) {
        taskDefinition.initialize();
        MyJob myJob = new MyJob(taskDefinition.getId());
        CronTrigger cronTrigger = new CronTrigger(taskDefinition.getCronExpression());
        ScheduledFuture<?> schedule = taskScheduler.schedule(myJob, cronTrigger);
        tasksMap.put(taskDefinition.getId(), schedule);
    }
}

Şimdi map'imizi de eklediğimize göre artık task'ları iptal edebiliriz, bunu yapabilmek için SchedulerService'in içerisine cancelTask methodunu ekliyoruz.

public void cancelTask(String taskId) {
    ScheduledFuture<?> scheduledFuture = tasksMap.get(taskId);
    if (Objects.nonNull(scheduledFuture)) {
        scheduledFuture.cancel(true);
    }
}

cancel methodu içerisine bir boolean ifade bekliyor, mayInterruptIfRunning. Aslında bu ifade task devam ediyorsa, bu aşamada kesilmesini istiyor muyuz istemiyor muyuz sorusuna karşılık geliyor. Bu case'de kesilmesinde bir sakınca görmediğimiz için true olarak geçiyoruz.

Şimdi SchedulerController'umuza yeni endpoint yazalım.

SchedulerController.java

@RestController
@RequestMapping("/api/v1/scheduler")
public class SchedulerController {

    private final SchedulerService schedulerService;

    public SchedulerController(SchedulerService schedulerService) {
        this.schedulerService = schedulerService;
    }

    @PostMapping
    void addTask(@RequestBody TaskDefinition taskDefinition) {
        schedulerService.addTask(taskDefinition);
    }

    @DeleteMapping("/{id}")
    void cancelTask(@PathVariable String id){
        schedulerService.cancelTask(id);
    }

Kodumuzu test edelim.

curl --location 'localhost:8080/api/v1/scheduler' \
--header 'Content-Type: application/json' \
--data '{
    "name":"abidino",
    "cronExpression" : "*/5 * * * * *"
}'

Yukarıdaki curl isteğini attığımızda oluşturduğumuz task 5 saniyede bir console'a aşağıdaki gibi bir çıktı yazacaktır. Burada ekrana zaman ve task ID yazılmaktadır.

07:32:40 ==> 25e19736-3eac-4eae-b6fa-4ab23c4a539f running
07:32:45 ==> 25e19736-3eac-4eae-b6fa-4ab23c4a539f running
07:32:50 ==> 25e19736-3eac-4eae-b6fa-4ab23c4a539f running

Bu task ID'yi alıp yeni oluşturduğumuz cancelTask endpoint'imize aşağıdaki gibi istek attığımızda ise çalışan job'umuz iptal edildiği için artık console'a herhangi bir şey yazılmadığını gözlemleyebiliriz.

curl --location --request DELETE 'localhost:8080/api/v1/scheduler/25e19736-3eac-4eae-b6fa-4ab23c4a539f'

2. Çalışan Task'ın Güncellenmesi

Task'ın oluşturulması, iptal edilmesi konusunu işledik. Şimdi ise güncellenmesini nasıl yapabiliriz diye düşünelim. Bunun için aslında yapmamız gereken şey, eski task'ı silip, gelen isteğe göre yeni bir task oluşturmak.

SchedulerService sınıfımıza updateTask methodu ekliyoruz.

SchedulerService.java
public void updateTask(String taskId, TaskDefinition taskDefinition) {
    ScheduledFuture<?> scheduledFuture = tasksMap.get(taskId);
    if (Objects.nonNull(scheduledFuture)) {
        scheduledFuture.cancel(true);
        addTask(taskDefinition);
        return;
    }
    throw new IllegalArgumentException("Task not found with this id : " + taskId);
}

SchedulerController sınıfımıza updateTask methodu ekliyoruz.

SchedulerController.java
    @PutMapping("/{id}")
    void updateTask(@PathVariable String id, @RequestBody TaskDefinition taskDefinition){
        schedulerService.updateTask(id, taskDefinition);
    }

Test etmek amacıyla bir adet task ekleyip daha sonra ise bunu updateTask endpoint'imizi çağırabiliriz. Önce mevcutta var olan task'ı iptal edip sonra ise verilen değerlere göre yeni task oluşturacaktır.

Github reposu icin buraya 👀 bakabilirsiniz.

Sonuç

Yazıyı okuduğunuz için teşekkür ederim, umarım faydalı olmuştur. Bu yazıda Spring Boot'da dinamik scheduler yapısını biraz daha geliştirdik serinin devam yazılarında görüşmek ūzere.

Herkese iyi çalışmalar. ✌🏼