Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

.NET’te Background Job ve Scheduling: TickerQ, Hangfire ve Quartz.NET Karşılaştırması


Merhaba,

Bu içeriğimizde son dönemlerde zamanlanmış işler (scheduled jobs) için ciddi şekilde ilginç bir alternatif haline gelmiş olan TickerQ’yu inceleyecek ve özellikle Quartz.NET‘in ve Hangfire‘nin yıllardır çözemediği hangi problemlere dair çözümlerle geldiğini değerlendiriyor olacağız. Ayrıca, bu teknolojiler arasında kullanım durumlarına göre hangilerini seçmemiz gerektiğine dair mukayesede bulunacağız. O halde hazırsanız başlayalım…

Her şeyden Önce Problem(ler) Nelerdir?

Çoğu kişi için scheduler denince akıllarda şu canlanmaktadır; “her gece 02:00’da rapor üret”

Ancak scheduler bundan çok daha fazlasına karşılık gelmektedir; bir işlemden 10 dakika sonra e-posta göndermeden tutun sipariş verildikten 24 saat sonra ödeme gelmediyse iptal edilmesine kadar davranışsal planlamaların ötesinde, belirli bir zamanda çalışacak işlerin delayed event olarak planlanması, periyodik görevlerin ne zaman çalışacağını belirlemek üzere cron yapılandırmalarının tanımlanması ve hatta bu planlamaların sürdürülebilirliğini sağlamak amacıyla persistence mekanizmalarının kullanılması gibi yetenekler de scheduler’ın temel sorumluluk alanına girmektedir. Dolayısıyla bunların tamamı, scheduler’ın ihtiyaç duyduğu yardımcı kabiliyetlerdir.

Ancak scheduler’ın bu sorumlulukları yerine getirebilmesi, beraberinde çözülmesi gereken birtakım tasarım problemlerini ve çalışma zamanı risklerini de ortaya çıkarmaktadır. Şimdi gelin bu riskleri tek tek masaya yatırarak farkındalık oluşturmaya çalışalım;

  • Uygulamanın yeniden başlarsa ne olacak?
    Diyelim ki; 02:00’da rapor üretilecek. Ya uygulama 01:59’da çöker ve 02:00’da çalışmayarak 02:15’te tekrar ayağa kalkarsa! Böyle bir durumda ideal olan scheduler’ın planlanan işi kaybetmemesi gerekmektedir. Dolayısıyla schedule bilgisinin sadece RAM’de tutulması yeterli değildir.
  • Uygulamanın birden fazla instance’ı varsa?
    Load Balancer

    ┌──────────┼──────────┐
    ↓ ↓ ↓
    API #1 API #2 API #3
    Modern uygulamalar genellikle yandaki gibi load balancer eşliğinde ölçeklendirilerek tasarlanmaktadır. Haliyle tüm instance’lar da scheduler yapılandırılmaktadır. Böyle bir tasarımdaki en büyük risk aynı işin ölçeklendirilmenin yapıldığı instance sayısı kadar çalışabilme ihtimalidir.

    Dolayısıyla burada scheduler’ın çözmesi gereken bir problem ortaya çıkmaktadır: “bu işi hangi instance çalıştıracaktır?”

  • Job başarısız olursa ne olacak?
    Scheduler tarafından çalıştırılan iş exception fırlatırsa eğer bu taktirde yine ideal olan işin tekrar çalıştırılması olacaktır. Böyle durumlarda scheduler’ın kendisinden ziyade job execution/retry politikaları devreye girecektir.
  • Job’ın state’i bir yerde tutulmalıdır!
    Job’ın ne zaman çalışacağı, en son ne zaman çalıştığı, başarılı mıydı, başarısız mıydı, kaç kere retry edildiği yahut bir sonraki çalışmanın ne zaman olduğu gibi soruların cevaplarını bir yerden alabiliyor olmamız gerekmektedir. Eğer ki bu soruların cevapları RAM’de tutuluyorsa bu ciddi bir risk teşkil etmektedir. Haliyle SQL Server, PostgreSQL yahut Redis gibi persistence mekanizmalarıyla kalıcı hale getirilmelidir!

Peki tüm bunlar neden problemdir?
En saf haliyle başlangıçtaki amacımız her gece 02:00’da rapor üretmekti. Ancak olay öyle bir noktaya geliyor ki production’da Schedule → Persistence → Concurrency → Distributed Execution → Retry → Failure Handling → Recovery → Monitoring gibi bir sisteme ihtiyaç olduğu ortaya çıkıyor!

Evet, Quartz.NET ve Hangfire’ın tam da çözdüğü problem bu ihtiyaç silsilesidir…

Peki Quartz.NET ve Hangfire neye yetmiyor?
Aslında Quartz.NET ve Hangfire’ın çözemediği temel bir scheduling problemi yoktur! İkisi de bu tarz ihtiyaç süreçlerinde tercih edilebilecek kadar olgun çözümlerdir. Zaten TickerQ’nun da ortaya çıkışını “Quartz.NET ve Hangfire yetersizdi, TickerQ geldi” şeklinde anlatmak hem niyet hem de teknik olarak oldukça yanıltıcı olacaktır! Doğrusu şudur; Quartz.NET ve Hangfire’ın çözdüğü problemlere karşın TickerQ daha modern bir yaklaşım sergileyerek çözüm getirmektedir…

Şöyle ki; Quartz.NET ve Hangfire geleneksel reflection altyapısına sahipken, TickerQ’nun yapısı ise .NET dünyasında değişiklik gösteren source generators, native AOT vs. gibi beklentilere ayak uydurabilmektedir…

TickerQ’nun Quartz.NET ve Hangfire’dan Farkı Nedir?

TickerQ, Quartz.NET ve Hangfire ile aynı temel probleme çözüm getirmektedir. Yani üçü de, bir işi şimdi değil, belirli bir zamanda veya belirli aralıklarla güvenli şekilde çalıştırmaya odaklıdırlar. Ama çözüm süreçlerindeki yaklaşımları oldukça farklıdır:

Quartz.NET Hangfire TickerQ
Ana fikir Güçlü scheduler Persistent background jobs Modern .NET scheduler
Job keşfi Runtime/reflection Runtime/reflection Compile-time source generator
Native AOT
Persistence ADO.NET JobStore SQL/Redis vb. EF Core / Redis
Dashboard Yerleşik değil
Cron Çok güçlü Güçlü
Delayed job
Retry ✅ / yapılandırılabilir ✅ çok güçlü
Job chaining Listener vb. Continuations Parent/child
Modern .NET yaklaşımı Orta Orta Çok güçlü
Olgunluk 🟢 Çok yüksek 🟢 Çok yüksek 🟡 Yeni

TickerQ’nun en büyük numarası source generator’dır! Kodu otomatik üretmekte ve compile etmektedir.

Quartz.NET ve Hangfire etrafında yıllarca oluşmuş (persistence, recovery, concurrency, retry, clustering, scheduling semantics, monitoring, job lifecycle vs. gibi) kocaman bir ekosistem mevcuttur. Haliyle bu kütüphanelerin çözdüğü scheduling/background-job problemlerini TickerQ, modern .NET’in compile-time ve AOT dünyasına daha uygun bir mimariyle çözmeye çalışmakta ve bu ekosistemin önemli bir bölümünü sunmaktadır.

Quartz.NET, Hangfire ve TickerQ Tam Olarak Aynı Şey mi?

Hayır, aynı şey değiller! Ama aynı problem alanında kesişmektedirler.

Quartz.NET, Hangfire ve TickerQ birbirlerinin birebir alternatifleri değildir; fakat üçünün de “zamanlanmış/arka plan işlerini güvenilir biçimde çalıştırma” yetenekleri vardır.

Dolayısıyla üçü de scheduler mı? diye sorarsanız, evet üçünün de scheduler yeteni vardır. Ancak üçü aynı scheduler’ın üç farklı implementasyonu değildir!

Avantajları & Dezavantajları

Quartz.NET Hangfire TickerQ
En güçlü tarafı Çok gelişmiş scheduling Kullanım kolaylığı + dashboard Modern .NET + source generation
En büyük avantajı Scheduling konusunda çok olgun Background job yönetimi çok rahat Reflection’sız, AOT uyumlu yaklaşım
En büyük dezavantajı Karmaşık ve ağır gelebilir Scheduler’dan çok job platformu yaklaşımı Daha genç ekosistem
Öğrenme 🔴 Zor 🟢 Kolay 🟢 Kolay / Orta
Dashboard ✅ Çok iyi
Cron ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Persistence ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
Retry ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
Native AOT
Ekosistem ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
Production geçmişi ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐

Yukarıdaki tablodan da görüleceği üzere scheduling işlemlerinde kullanabileceğimiz bu kütüphanelerin avantaj ve dezavantajlarını gayet güzel bir şekilde mukayese etmiş bulunuyoruz. Ancak konuya dair daha iyi sindirilebilmesi için bir kaç kelam söylemekte fayda görmekteyim. Öncelikle Quartz.NET’i ele alalım. Bu kütüphanenin en büyük avantajı çok olgun olmasıdır. Yıllardır mevzu bahis olan problemler üzerine odaklanması ve scheduling senaryolarında sayısız kez varlık göstermesi bu kütüphaneyi ister istemez çok güçlü kılmaktadır. Ancak bu gücün en büyük bedeli ise getirdiği karmaşıklıktır. Basit bir iş için bile Quartz.NET’te abstraction’larla uğraşmak ikrah ettirecek derecede kaçınılmaz olacaktır.

Hangfire’a gelirsek eğer kullanımının kolay olması en büyük avantajıdır. Üstelik çok güzel bir dashboard’a sahiptir. Ancak bu kolaylık Quartz.NET’e nazaran biraz yüzeysel kalmasına sebebiyet vermekte ve scheduling süreçlerinde derin yapılandırmalara kadar inememektedir. Yani demek istediğim o ki, çok karmaşık scheduling politikalarının söz konusu olduğu senaryolarda Hangfire’a nazaran Quartz daha rahat eşlik edebilmektedir.

TickerQ ise modern .NET yaklaşımlarıyla yapılandırılmış yeni bir scheduling kütüphanesidir. Önceki satırlarda vurgulandığı gibi en büyük avantajı source generator altyapısına sahip olmasıdır. Yani job’ları runtime’da reflection ile keşfetmektense compile-time’da bulup, çalıştırabilmektedir. Bu da süreçte bizlere ciddi performans getirmekte ve maliyeti düşürmektedir.

TickerQ kesinlikle Quartz.NET’ten daha hızlıdır!

Ancak TickerQ’nun henüz yeni tasarlanmış olması daha küçük community ve daha az tecrübe getirmektedir. Bu tecrübe yoksunluğundan kaynaklı Quartz.NET ve Hangfire’da olduğu gibi türlü ihtiyaç senaryolarına karşı örnek olayların bulunabilmesi pek mümkün değildir!

Bundan kaynaklı TickerQ’nun seçiminde kıstasın performans odaklı değil, mimarisel yaklaşım odaklı olması gerektiğine dikkatinizi çekerim…

Asp.NET Core’da TickerQ Nasıl Kullanılır?

TickerQ’yu scheduling işlemlerinde kullanabilmek için tabi ki de öncelikle TickerQ kütüphanesinin yüklenmesi ve ardından aşağıdaki yapılandırma ile projeye entegrasyonu gerçekleştirilmelidir:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTickerQ();

var app = builder.Build();

.
.
.

app.UseTickerQ();

app.Run();

Bu temel yapılandırmadan sonra genel mantık job tanımlama ve çalıştırma üzerine kuruludur. Dolayısıyla en yalın haliyle aşağıdaki gibi bir job tanımlamasını gerçekleştirebiliriz:

    public class OrderJobs
    {
        [TickerFunction(FunctionNames.SendOrderEmail)]
        public async Task SendOrderEmailAsync(TickerFunctionContext<SendOrderEmailRequest> tickerFunctionContext, CancellationToken cancellationToken)
        {
            await Task.Delay(500, cancellationToken);

            await Console.Out.WriteLineAsync(@$"Sipariş e-postası gönderildi. Job Id : {tickerFunctionContext.Id}
Order Id : {tickerFunctionContext.Request.OrderId} | E-posta : {tickerFunctionContext.Request.Email}");
        }
    }

Burada dikkat edilirse eğer TickerQ’da tanımlanan job’lar TickerFunction attribute’u ile işaretlenmektedir. TickerQ’nun source generator’ı bu attribute’u compile-time’da keşfedecektir ve ilgili job’ın dependency injection üzerinden resolve edilmesi ve çağrılabilmesi için gerekli olan wiring’i üretecektir.

Ardından yapılması gereken job’ın aşağıdaki gibi çalıştırılmasıdır:

app.MapPost("/orders/schedule-email", async (SendOrderEmailRequest sendOrderEmailRequest, ITimeTickerManager<TimeTickerEntity> timeTickerManager) =>
{
    var result = await timeTickerManager.AddAsync(new TimeTickerEntity
    {
        Function = FunctionNames.SendOrderEmail,
        ExecutionTime = DateTime.UtcNow.AddSeconds(15),
        Request = TickerHelper.CreateTickerRequest(sendOrderEmailRequest)
    });

    return Results.Ok(new
    {
        sendOrderEmailRequest.OrderId,
        jobId = result.Result?.Id
    });
});

Dikkat ederseniz çalıştırılacak job’ın ne zaman çalışacağı da ExecutionTime property’siyle belirlenmektedir..NET’te Background Job ve Scheduling: TickerQ, Hangfire ve Quartz.NET KarşılaştırmasıGörüldüğü üzere ilgili endpoint’e istek attığımız zaman job hedeflenen zamanda çalıştırılacak şekilde yapılandırılmaktadır.

Peki hoca! Belirlenen periyotta bir çalışan job nasıl yapılandırabiliriz? diye sorduğunuzu duyar gibiyim… Diyelim ki her gece saat 02:00’da eski sipariş kayıtları temizlensin. Bunun için aşağıdaki gibi bir job’ın tanımlanması gerekmektedir:

    public class OrderJobs
    {
        .
        .
        .

        [TickerFunction(FunctionNames.CleanupOrders, cronExpression: "0 0 2 * * *")]
        public async Task CleanupOrdersAsync(TickerFunctionContext<SendOrderEmailRequest> tickerFunctionContext, CancellationToken cancellationToken)
        {
            await Console.Out.WriteLineAsync(@$"Sipariş temizleme başladı. {DateTime.UtcNow}");

            await Task.Delay(500, cancellationToken);

            await Console.Out.WriteLineAsync(@$"Sipariş temizleme tamamlandı. {DateTime.UtcNow}");
        }
    }

Burada, job’a attribute üzerinden tanımlanmış olan cronExpression ile bu job’ın hangi periyotta bir çalışacağını cron formatında ifade etmekteyiz. Ayrıca burada dikkat ederseniz eğer attribute üzerinden verilen bu cron değeri ile bu recurring schedule’ı ekstradan herhangi bir yapılandırmaya gerek duymaksızın startup sırasında otomatik seed edebilmekteyiz.

Peki hoca! Tüm çalışmada memory kullanılmaktadır öyle değil mi?
Evet, yani bu vaziyette uygulama kapandığı taktirde planlanan tüm işler kaybolacaktır. Haliyle Quartz.NET’te yahut Hangfire’da olduğu gibi TickerQ’yu kullanırken de bir persistence provider kullanılması gerekmektedir.

Bunun için TickerQ.EntityFrameworkCore kütüphanesinden istifade edilebilir. Bu kütüphane ile adı üzerinde TickerQ’ya uygun Entity Framework Core yapılanmasında bulunabilir ve hızlıca veritabanı işlemlerini yürütebiliriz. Tabi veritabanı işlemleri için de uygun paketin yüklenmesi gerekecektir. Misal olarak SQL Server kullanılacaksa Microsoft.EntityFrameworkCore.SqlServer paketinin yüklenmesi gerekmektedir.

Şimdi burada yapılması gereken oldukça basittir. Şöyle ki; Program.cs‘de aşağıdaki yapılandırmada bulunulmalıdır:

builder.Services.AddTickerQ(options =>
{
    options.AddOperationalStore(efConfiguration =>
    {
        efConfiguration.UseTickerQDbContext<TickerQDbContext>(optionsAction =>
        {
            optionsAction.UseSqlServer(builder.Configuration.GetConnectionString("SQLServer"), sqlServerOptionsAction => sqlServerOptionsAction.MigrationsAssembly(typeof(Program).Assembly.GetName().Name));
        });
    });
});

Bu yapılandırmanın ardından add-migration mig_1 ve update-database talimatlarının verilmesiyle veritabanı migrate edilmiş olacaktır..NET’te Background Job ve Scheduling: TickerQ, Hangfire ve Quartz.NET KarşılaştırmasıBu yapılandırmadan sonra aşağıdaki gibi bir çalışma neticesinde oluşturulan job veritabanına kaydedilecek ve uygulama kapansa dahi, daha sonradan ayağa kaldırıldığında kaldığı yerden devam edecektir:

app.MapPost("/orders/schedule-email/persistence", async (SendOrderEmailRequest sendOrderEmailRequest, ITimeTickerManager<TimeTickerEntity> timeTickerManager) =>
{
    var result = await timeTickerManager.AddAsync(new TimeTickerEntity
    {
        Function = FunctionNames.SendOrderEmail,
        ExecutionTime = DateTime.UtcNow.AddSeconds(5),
        Request = TickerHelper.CreateTickerRequest(sendOrderEmailRequest),
        Retries = 3,
        RetryIntervals = new[] { 30, 120, 600 }
    });

    return Results.Ok(new
    {
        sendOrderEmailRequest.OrderId,
        jobId = result.Result?.Id
    });
});

.NET’te Background Job ve Scheduling: TickerQ, Hangfire ve Quartz.NET Karşılaştırması

Concurrency Nasıl Sağlanır?

Diyelim ki, aynı anda 10.000 sipariş geldi. Ee haliyle tüm bu siparişlerin email job’ı devreye girecektir ve hepsinin aynı anda girmesi ciddi bir darboğaz yaratacaktır. İşte böyle bir durumda TickerQ’da fonksiyon bazında concurrency sınırını belirleyebilmekte ve sistemi ölçeklendirebilmekteyiz. Bunun için TickerFunction attribute’unda aşağıdaki gibi maxConcurrency parametresini kullanabiliriz:

[TickerFunction(FunctionNames.SendOrderEmail, maxConcurrency: 3)]

Bu durumda;

send-order-email #01 ──┐
send-order-email #02 ──┤ ← 3 işlem eşzamanlı
send-order-email #03 ──┘


send-order-email #04 ──┐
send-order-email #05 ──┤ ← önceki 3’ünün bitmesini bekler
send-order-email #06 ──┘


send-order-email #07 ──┐
send-order-email #08 ──┤ ← önceki 3’ünün bitmesini bekler
send-order-email #09 ──┘

şeklinde çalışma sergileyecektir.

Dashboard Ekleyelim

Ve son olarak da job’ların çalışmasını anlık olarak gözlemleyebileceğimiz ve TickerQ’nun en güzel özelliklerinden biri olan Dashboard’un yüklenmesini ele alalım. İlk olarak TickerQ.Dashboard paketinin yüklenmesi gerekmekte ve ardından aşağıdaki yapılandırmalar Program.cs‘de gerçekleştirilmelidir:

builder.Services.AddTickerQ(options =>
{
    .
    .
    .

    options.AddDashboard(configure => configure.WithBasicAuth(username: "admin", password: "admin"));
    options.AddOperationalStore(configurator => configurator.UseTickerQDbContext<TickerQDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("SQLServer"))));
});

Bu yapılandırmadan sonra https://localhost:{port}/tickerq/dashboard/ adresi üzerinden dashboard’a erişebilir ve job’ların sürecine dair gözlemde bulunabiliriz..NET’te Background Job ve Scheduling: TickerQ, Hangfire ve Quartz.NET Karşılaştırması.NET’te Background Job ve Scheduling: TickerQ, Hangfire ve Quartz.NET Karşılaştırması.NET’te Background Job ve Scheduling: TickerQ, Hangfire ve Quartz.NET Karşılaştırması

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Örnek çalışmaya aşağıdaki GitHub reposundan erişebilirsiniz.
https://github.com/gncyyldz/TickerQ.Example

Bu repository, ilgili konuya dair örnek çalışmanın kaynak kodlarını ve mimari yapısını içermektedir. Detaylar için GitHub üzerinden incelemede bulunabilirsiniz.


GitHub’da Görüntüle →

Bunlar da hoşunuza gidebilir...

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir