Makale Özeti

Bu makalemizde Quartz.NET içerisindeki görevleri daha detaylı olarak inceleyeceğiz.

Makale

   Önceki yazımızda Quartz.NET kütüphanesine giriş yapmıştık. Görevlerin, IJob arayüzüyle nasıl tanımlanabileceği üzerinde durmuş, SimpleTrigger ile tetiklenecek basit bir görev oluşturmuştuk. Bu makalede ise Quartz.NET görevlerini daha detaylı inceleyeceğiz:

Görevlerin birden fazla tetikleyici ile zamanlanması

   Quartz.NET’in en esnek özelliklerinden biri de görevlerin ve tetikleyicilerin ayrı olmasıdır. Böylelikle aynı görevin birden fazla tetikleyici ile zamanlanması mümkün olabilmektedir. Önceki makalede, oluşturmuş olduğumuz MyJob isimli görevi bir SimpleTrigger ile zamanlamıştık. Şimdi aynı işlemi birden fazla SimpleTrigger ile gerçekleştirelim:

public class MyJob : IJob {
    public void Execute(JobExecutionContext context) {
        Console.WriteLine("Görev çalıştırıldı : {0}",
                      DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
    }
}

class Program {

    static void Main(string[] args) {
        ISchedulerFactory schedFact = new StdSchedulerFactory();

        //Yeni bir zamanlayıcı oluşturulup çalıştırılıyor
        IScheduler sched = schedFact.GetScheduler();
        sched.Start();

        //Oluşturduğumuz görev(MyJob) hazırlanıyor
        JobDetail jobDetail = new JobDetail("myJob", null, typeof(MyJob));

        //Başlatıldıktan 20 sn sonra çalışacak bir SimpleTrigger oluşturuluyor.
        Trigger trigger1 = new SimpleTrigger("trigger1", null, DateTime.UtcNow.AddSeconds(20), null, 0, TimeSpan.Zero);
        //Başlatıldıktan 40 sn sonra çalışacak bir SimpleTrigger oluşturuluyor.
        Trigger trigger2 = new SimpleTrigger("trigger2", null, DateTime.UtcNow.AddSeconds(40), null, 0, TimeSpan.Zero);

        //Görev tetikleyici ile zamanlanıyor
        sched.ScheduleJob(jobDetail, trigger1);

        trigger2.JobName = jobDetail.Name;
        sched.ScheduleJob(trigger2);

        ManualResetEvent resetEvent = new ManualResetEvent(false);
        resetEvent.WaitOne();
    }
}

   Örnekte görüldüğü gibi; tanımlandıktan 20 sn ve tanımlandıktan 40 sn sonra çalışacak iki tetikleyici oluşturulmuş ve görevimiz ile ilişkilendirilmiştir. Burada ikinci tetikleyicinin nasıl eklendiğini gösteren kod bölümüne dikkat etmemiz gerekmektedir. İlk tanımlama ile birlikte görevimiz Quartz.NET zamanlayıcısına eklenmiştir. Eğer daha sonraki tetikleyicileri tanımlarken “sched.ScheduleJob(jobDetail, trigger2); ” şeklinde bir ifade kullanırsak görev yeniden eklenmeye çalışılacak ve zaten ekli olduğunu belirten bir Exception oluşacaktır. Bu nedenle, ikinci tetikleyicimiz aşağıdaki şekilde ilişkilendirilmektedir:

trigger2.JobName = jobDetail.Name;
sched.ScheduleJob(trigger2);

Tetikleyicilerde Öncelik

   Bir görev için tanımlamış olduğumuz tetikleyicilerin aynı anda çalışması söz konusu olabilir. Bu gibi durumlarda tetikleyiciler arasında öncelik tanımlanarak çalışmalar istenilen sırada gerçekleştirilebilir.

trigger1.Priority = 3;
trigger2.Priority = 10;

   Örnekte; iki tetikleyiciye de birer öncelik değeri verilmiştir. Eğer bu tetikleyiciler aynı anda çalışırsa öncelik değeri yüksek olan(10) ilk sırada işletilecektir. Öncelik için herhangi bir değer atanmazsa varsayılan olarak 5 kullanılmaktadır.

Görevlere Veri Aktarılması

   Tanımlanan görevler çalışma anında erişebileceği bazı özel verilere ihtiyaç duyabilmektedir. JobDataMap sınıfı ile bu verileri görevlere aktarabilmemiz mümkündür. Bu işlem görevin tanımlanması sırasında yapılabilir. Tanımlanmış her bir görev bir JobDataMap örneğine sahiptir. JobDataMap, IDictionary arayüzünden türetilmiş bir sınıftır ve görevlerle ilgili verileri aktarabilmek için bir araç sunmaktadır.Şimdi bunu bir örnek üzerinden inceleyelim:

JobDetail jobDetail = new JobDetail("myJob", null, typeof(MyJob));
jobDetail.JobDataMap["Mesaj"] = "Bu mesaj görevin çalışması sırasında kullanılacaktır";

   Örnekte de görüldüğü JobDetail üzerinde yer alan JobDataMap ile verilerimizi anahtar/değer ikilileri şeklinde saklıyoruz. Böylelikle çalışan görevler bu verilere ulaşabiliyor. Sakladığımız verilere nasıl ulaşabileceğimizi de aşağıdaki kod bölümünde görebilirsiniz:

public class MyJob : IJob {
    public void Execute(JobExecutionContext context) {
        string mesaj = (string)context.JobDetail.JobDataMap["Mesaj"];
        Console.WriteLine(mesaj);
    }
}

Stateful Görevler

   Yukarıda JobDataMap üzerinden görevlere veri aktarılabileceğini görmüştük. Bu veriler görevimiz çalıştırılırken Execute metoduna JobExecutionContext ile birlikte gönderilmekte ve görev tarafından kullanılabilmektedir. Ancak; görevin çalıştırılması sırasında JobDataMap üzerinde yapılan değişiklikler görevin sonraki çalışmaları tarafından görülememektedir. Her çalışma sırasında başlangıçta atamış olduğumuz verilere ulaşılmaktadır. Bu yüzden IJob arayüzü ile tanımladığımız görevler non-stateful görevlerdir. Bu durumu aşağıdaki örnekle açıklamaya çalışalım:

public class MyJob : IJob {
    public void Execute(JobExecutionContext context) {
        int sayi = context.JobDetail.JobDataMap.GetInt("sayi");

        Console.WriteLine("Görev çalıştırıldı : {0}, sayı={1}", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"), sayi);
        sayi++;
        context.JobDetail.JobDataMap["sayi"] = sayi;
    }
}

class Program {
    static void Main(string[] args) {
        ISchedulerFactory schedFact = new StdSchedulerFactory();

        //Yeni bir zamanlayıcı oluşturulup çalıştırılıyor
        IScheduler sched = schedFact.GetScheduler();
        sched.Start();

        //Oluşturduğumuz görev(MyJob) hazırlanıyor
        JobDetail jobDetail = new JobDetail("myJob", null, typeof(MyJob));
        jobDetail.JobDataMap["sayi"] = 1;

        Trigger trigger1 = new SimpleTrigger("trigger1", null, DateTime.UtcNow.AddSeconds(2), null, 0, TimeSpan.Zero);
        Trigger trigger2 = new SimpleTrigger("trigger2", null, DateTime.UtcNow.AddSeconds(4), null, 0, TimeSpan.Zero);

        //Görev tetikleyici ile zamanlanıyor
        sched.ScheduleJob(jobDetail, trigger1);

        trigger2.JobName = jobDetail.Name;
        sched.ScheduleJob(trigger2);

        ManualResetEvent resetEvent = new ManualResetEvent(false);
        resetEvent.WaitOne();
    }
}

   Örnekte görüldüğü gibi görevimiz iki tetikleyici ile zamanlanmıştır. Ayrıca “sayi” isimli verimiz göreve aktarılmıştır. Görev çalıştığında sayının değerini yazdırmakta ve ardından 1 arttırmaktadır. Görevimiz ikinci kez çalıştığında bu değişikliği görmeyecek ve ekrana yeniden 1 değerini yazdıracaktır. İlgili kodu çalıştırdığınızda karşınıza çıkan sonuç aşağıdaki gibi olacaktır:

   Burada dikkat etmemiz gereken nokta, referans tipleridir. JobDataMap içerisinde sakladığımız nesne bir referans tipi ise ve bu nesnenin değer tipi olan bir alanında değişiklik yaparsak bu değişiklik görülecektir. Çünkü referans tipleri için bir kopya değil doğrudan referansın kendisi saklanmaktadır. Bunu bir örnekle açıklamaya çalışalım. JobDataMap içerisinde saklanacak nesnemiz için bir sınıf oluşturalım:

public class MyClass {
    public int Sayi { get; set; }
}
   Şimdi bu sınıfımızın bir örneğini JobDataMap içerisinde saklayalım:
jobDetail.JobDataMap["sayi"] = new MyClass { Sayi = 1 };
   Görevimizin Execute metodunda MyClass sınıfının Sayi özelliğini değiştirelim:
MyClass mc = context.JobDetail.JobDataMap["sayi"] as MyClass;
Console.WriteLine("Görev çalıştırıldı : {0}, sayı={1}", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"), mc.Sayi);
//Değişiklik görülecektir.
mc.Sayi++;
   Görevimizde bu şekilde bir değişiklik yapıp çalıştırırsak aşağıdaki sonucu elde edeceğiz:

   Görüldüğü gibi görevimiz Stateful olmamasına rağmen referans tipinin üzerinde yaptığımız değişiklik korundu. Ancak JobDataMap üzerindeki referansı değiştirmeye çalışırsak bu değişiklik yansımayacaktır. Görevin Execute metodunda aşağıdaki değişikliği yaptığımızı varsayalım:

MyClass mc = context.JobDetail.JobDataMap["sayi"] as MyClass;
Console.WriteLine("Görev çalıştırıldı : {0}, sayı={1}", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"), mc.Sayi);
//Değişiklik görülmeyecektir.
context.JobDetail.JobDataMap["sayi"] = new MyClass { Sayi = 5 };

   Kodumuzu bu şekilde çalıştırırsak değişikliğin yansımadığını göreceğiz. Çünkü bir sonraki çalıştırmada MyClass nesnesinin eski referansı yeniden yüklenecektir:

   Yukarıdaki örneklerle anlatmak istediğimiz nokta; Quartz.NET'in görevin çalıştırılması sırasında JobDataMap’in bir kopyasını oluşturması ve göreve aktarmasıdır. Eğer her koşulda (değer ve referans tipleri için) görev içerisinde yaptığımız değişikliklerin sonraki çalışmalarda görülebilmesini istiyorsak Stateful görevler kullanmalıyız. Quartz.NET, Stateful görevler tanımlamamız için IStatefulJob arayüzünü sunmaktadır. Eğer görevinizi IStatefulJob arayüzünden türetirseniz, JobDataMap üzerinde yapmış olduğunuz tüm değişiklikler görevin sonraki çalışmaları tarafından da görülecektir. MyJob sınıfımızda gerekli değişiklikleri yaparak bu durumu görebiliriz:

public class MyJob : IStatefulJob {
    public void Execute(JobExecutionContext context) {
        int sayi = context.JobDetail.JobDataMap.GetInt("sayi");

        Console.WriteLine("Görev çalıştırıldı : {0}, sayı={1}", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"), sayi);
        sayi++;
        context.JobDetail.JobDataMap["sayi"] = sayi;
    }
}

   Ancak Stateful görevlerin çalıştırılması ile ilgili bir farklılık söz konusudur. Görev çalışırken başka bir tetikleyici tarafından da çalıştırılabilmesi mümkün değildir. Sıradaki tetikleyiciler bloklanacak ve görevin o anki çalışmasını sonlandırmasını bekleyeceklerdir.

   Bu makalede Quartz.NET içerisindeki görevlerle ilgili detaylar üzerinde durmaya çalıştık. Bir sonraki makalede CronTrigger sınıfını inceleyeceğiz.

Cemil ABİŞ

http://www.cemilabis.com
http://twitter.com/cemilabis