Neden Bir Distributed Job Scheduler Yazdım?
.NET tarafında arka plan işleri denince akla gelen ilk iki isim belli: Hangfire ve Quartz.NET. İkisi de olgun, ikisi de yıllardır sahada. Peki ortada bunlar varken neden oturup bir job scheduler yazdım?
Kısa cevap: ikisiyle de bir sorunum yoktu. Sorun, işlerin nerede çalıştığıyla başladı. Ama orada bitmedi — asıl uzun hikâye, o ilk sorunu çözdükten sonra ortaya çıkan eksiklerdi.
Bu yazıda önce o ilk sorunu anlatacağım, sonra Milvaion'ın buna nasıl bir cevap verdiğine bakacağız.
Problem: Job'lar Uygulamanın İçinde Çalışıyor
Hangfire'ı ya da Quartz'ı bir projeye eklediğinde, scheduler senin uygulamanın process'inin içinde yaşar. Zamanı gelen job, aynı process'te, aynı thread pool'da çalışır.
Bu, işlerin çoğu için gayet iyidir. Ta ki şunlardan biri olana kadar:
- Uzun süren bir job diğerlerini bekletir. Gece çalışan rapor job'ın iki saat sürüyorsa, o iki saat boyunca thread pool'dan pay alır.
- Patlayan bir job scheduler'ı da götürür. Job'ın içinde yakalanmamış bir exception ya da bir memory leak, sadece o job'ı değil uygulamanın tamamını etkiler.
- Farklı işler farklı donanım ister. ML modeli çalıştıran bir job GPU isterken, e-posta gönderen bir job'ın 128 MB RAM'i vardır. İkisi aynı process'te yaşıyorsa, en pahalı olanın ihtiyacına göre ölçeklenirsin.
- Job'lar API ile birlikte ölçeklenir. API'ye gelen trafik arttı diye pod sayısını üçe çıkardığında, job'ların da üçe çıkar. Genelde istediğin bu değildir.
Benim durumumda dört maddenin dördü de vardı. Bir noktada şunu fark ettim: aslında istediğim şey job'ların nerede çalışacağına ayrı karar verebilmekti.
Milvaion Nedir?
Milvaion, .NET 10 üzerine yazılmış, açık kaynak (Apache 2.0) bir distributed job scheduling sistemi. Temel fikri tek cümleyle şu:
Job'ın ne zaman çalışacağına karar veren yer ile onu çalıştıran yer aynı process olmak zorunda değil.
Bunu ikiye ayırıyor:
- Scheduler (API): Cron ifadelerini okur, zamanı gelen job'ı tespit eder, kuyruğa atar. Dashboard'u da o barındırır.
- Worker: Kuyruktan mesajı alır, senin
IAsyncJobkodunu çalıştırır, sonucu geri bildirir.
Aralarında RabbitMQ, Redis ve PostgreSQL var.

Bu ayrımın pratikte karşılığı şu: worker'ı ayrı deploy edersin, ayrı ölçeklersin, ayrı donanıma koyarsın. GPU isteyen job'lar için GPU'lu worker, e-posta için 128 MB'lık worker. Bir worker çökerse scheduler etkilenmez, kuyruktaki mesaj bekler.
Temel Kavramlar
Milvaion'da dolaşırken karşına çıkacak dört kelime var, baştan netleştirelim:
| Kavram | Ne Demek |
|---|---|
| Job | Tekrarlayan ya da tek seferlik bir çalışma tanımı. "Her sabah 9'da rapor gönder" bir job'dır. |
| Worker Job | IAsyncJob implement eden C# sınıfın. Asıl işi yapan kod. |
| Occurrence | Bir job'ın tek bir çalışması. Statüsü, süresi ve logları vardır. |
| Worker | Job'ları çalıştıran process. |
Job bir tanım, occurrence ise o tanımın bir kez koşması. Dashboard'da "bu job 340 kere çalışmış, 3'ü patlamış" derken bahsedilen şey occurrence'lar.
Nasıl Çalışıyor?
Akış şöyle işliyor:
- Worker Auto Discovery senin worker'ını ve içindeki job sınıflarını otomatik keşfeder.
- Dashboard'dan ya da REST API'den bir job oluşturursun.
- Scheduler bunu PostgreSQL'e yazar ve bir sonraki çalışma zamanıyla Redis ZSET'e ekler.
- Dispatcher Redis'i kontrol eder, zamanı gelenleri bulur.
- Zamanı gelen job'lar routing key ile RabbitMQ'ya publish edilir.
- Worker mesajı alır, senin
IAsyncJobkodunu çalıştırır. - Worker durum ve logları RabbitMQ üzerinden geri bildirir.
- Scheduler sonucu kaydeder ve SignalR ile dashboard'a anlık olarak yansıtır.
Zamanlama için Redis ZSET kullanmamın sebebi, "şu ana kadar zamanı gelmiş job'ları getir" sorgusunun ZSET'te son derece ucuz olması. Veritabanını her saniye yoklamak yerine Redis'e soruyorum.
Bir Worker Yazmak
Teoriyi bir kenara bırakıp koda bakalım. Önce template'i kuralım:
dotnet new install Milvasoft.Templates.Milvaion
dotnet new milvaion-console-worker -n MyCompany.MyWorker
Sonra bir job yazalım:
using Milvasoft.Milvaion.Sdk.Worker.Abstractions;
public class SendReportJob(IReportService reportService) : IAsyncJob
{
public async Task ExecuteAsync(IJobContext context)
{
context.LogInformation("Rapor hazırlanıyor...");
// Job'a dashboard'dan girilen JSON payload'ı tipli olarak alıyoruz
var data = context.GetData<ReportRequest>();
await reportService.GenerateAsync(data, context.CancellationToken);
context.LogInformation("Rapor gönderildi.");
}
}
Dikkat edilecek üç nokta var:
- Constructor injection çalışıyor. Worker normal bir .NET host olduğu için, DI container'ına ne koyduysan job'ın içinde kullanabilirsin.
context.LogInformationile yazdığın loglar dashboard'da o occurrence'ın altında, canlı olarak görünür. Teknik loglar ayrıca Seq'e gider.context.CancellationTokenönemli. Job'ı dashboard'dan iptal ettiğinde ya da timeout'a düştüğünde tetiklenen token bu. Uzun süren işlerde bunu aşağıya geçirmezsen job iptal edilemez.
Worker'ı çalıştırdığında auto discovery devreye girer ve SendReportJob dashboard'da seçilebilir hale gelir. Ayrıca bir kayıt işlemi yapmana gerek yok.
Asıl Mesele Distributed Olması Değildi
Buraya kadar anlattığım şey Milvaion'ın çıkış noktasıydı. Ama dürüst olayım: eğer tek yaptığı job'ları ayrı process'te çalıştırmak olsaydı, muhtemelen bu yazıyı yazmaya değmezdi.
Scheduler'ı ayırdıktan sonra fark ettiğim şey şu oldu — asıl zamanı yiyen kısım job'ın nerede çalıştığı değil, çalıştıktan sonra ne olduğuydu. Job patladı, e-posta geldi, sonra ne? Logu nerede? Hangi worker'daydı? Dün de patlamış mıydı? Bunu kim değiştirmiş?
Hangfire'da ve Quartz'ta bu soruların cevabı genelde "Seq'e bak" ya da "veritabanına gir" gibi daha teknik bilgi sahibi insanlara yönelik oluyor. Fakat örneğin mevcut şirketimde IT Operasyon ekibimizin Hangfire altyapısında çalışan mevcut scheduler'ımızı anlamakta ve kullanmakta zorlandığını gördüm. Zamanla ortaya çıkan özelliklerin çoğu bu boşluğu kapatmak için yazıldı. Bu tarz tam kapsamlı piyasada gördüğüm sadece temporal.io oldu. Bende o devasa ekosisteme girmek ve öğrenmek yerine sfırdan, daha lightweight, developer friendly ve kontrolün tamamen bende olduğu bir tool geliştireyim dedim :)
Dashboard
Bu, üzerinde en çok vakit harcadığım kısımlardan biri. Quartz.NET'in hazır bir arayüzü yok — üçüncü parti bir şey kurarsın ya da kendin yazarsın. Hangfire'ın var ve gayet iş görüyor, ama kapsamı belli: job listesi, retry, recurring job'lar.
Milvaion'ın dashboard'ı şunları yapıyor:
- Canlı log akışı. Job çalışırken
context.LogInformationsatırları SignalR üzerinden ekrana düşüyor. Bitmesini bekleyip Seq'e gitmiyorsun. - Occurrence geçmişi. Her çalışmanın süresi, statüsü, exception'ı, hangi worker'da koştuğu. Filtrelenebilir, cursor pagination'lı — 30 bin kayıtlı bir job'da da açılıyor.
- Worker sağlığı. Hangi worker ayakta, kaç job çalıştırıyor, kapasitesi ne, heartbeat'i ne zaman geldi. Mcp server ile bu soruların cevabını yapay zeka ile alabilir ve tüm sistemi yönetebilirsin ;)
- Job'ı ekrandan düzenleme. Cron'u değiştir, duraklat, tetikle, job data'sını güncelle. Deploy gerekmiyor.
- Enterprise yönetim. RBAC based role yönetimi, kullanıcı yönetimi, auditing, api key yönetimi, mcp server, failure takibi gibi tüm sistemi arayüz üzerinden no-code yönetmenizi sağlayan bir çok ekran mevcut.
Bunlar tek tek "vay be" dedirtecek şeyler değil. Ama bir job gece 3'te patladığında hepsinin aynı ekranda olmasıyla olmaması arasında ciddi fark var.
Workflow Engine (DAG)
Hangfire'da ContinueJobWith var, yani "bu bitince şunu çalıştır" zinciri kurabiliyorsun. Quartz'ta bunun karşılığı yok, kendin kurgularsın.
Aslında workflow engine geliştirmeyi hiç istemiyordum çünkü n8n gibi robust toollar zaten piyasada mevcut fakat sisteme job chaning eklenmeliydi ve job chaning'in ucu eninde sonunda buraya çıkıyor. E madem job chaning yapacağız düzgün yapalım dedim :)
Milvaion'da görsel bir DAG builder var. Adımları sürükleyip bağlıyorsun; condition node ile koşula göre dallanıyor, merge node ile dallar birleşiyor, adımlar arasında data mapping ile bir adımın çıktısını diğerine bağlıyorsun. Bir adım patladığında hangi adımların çalışmadığı ekranda duruyor.
"Önce veriyi çek, başarılıysa işle, hata varsa alarm at, ikisinden sonra raporu gönder" gibi bir akış, üç ayrı job ve aralarında elle yazılmış kontrol koduyla uğraşmadan kuruluyor.
Alerting
Job patladığında birinin haberi olması lazım. Milvaion'da alarm kanalları built-in: Google Chat, Microsoft Teams, Slack, e-posta ve uygulama içi bildirim.
Hangfire ve Quartz tarafında bunu genelde kendin yazarsın — bir IJobFilter ya da bir listener, sonra HTTP client, sonra rate limiting derken küçük bir proje olur.
Auto Disable
Bu, en sevdiğim küçük özellik. Sürekli patlayan bir job'ı, belirlediğin eşikten sonra otomatik olarak devre dışı bırakıyor.
Neden önemli: connection string'i bozulmuş bir job her 5 dakikada bir çalışıp gece boyunca 96 kere patlar ve 96 alarm üretir. Sabah geldiğinde inbox'ında 96 mail olur ve içlerinden gerçekten önemli olanı bulamazsın. Auto disable "3 kere üst üste patladıysa dur, birine haber ver" diyor.
Eşiği ve failure window'u job bazında ayarlıyorsun — yani "son 30 dakika içinde 3 kere" diyebiliyorsun ki geçen haftadan kalma hatalar sayıya dahil olmasın.
Zombie Detection ve Timeout'lar
Worker çöktüğünde Running statüsünde asılı kalan occurrence'lar oluşur. Milvaion bunları tespit edip Failed'a çekiyor. İki ayrı timeout var:
- Execution timeout: Job şu kadar saniyeden uzun sürerse worker cancellation token'ı tetikler.
- Zombie timeout: Job şu kadar dakikadır kuyrukta ya da çalışıyor görünüyorsa, artık ölmüş kabul edilir.
Concurrent Execution Policy
Bir job'ın önceki koşusu hâlâ devam ederken zamanı tekrar geldiğinde ne olacak? Milvaion'da bunu job bazında seçiyorsun: yenisini atla, sıraya al ya da paralel çalıştır.
Hangfire'da bunu DisableConcurrentExecution attribute'u ile kod tarafında yaparsın, Quartz'ta [DisallowConcurrentExecution] ile. İkisi de derleme zamanı kararı; Milvaion'da ekrandan değiştiriyorsun.
RBAC, Kullanıcılar ve API Key'ler
Dashboard'ı ekibe açtığın anda "kim neyi silebilir" sorusu geliyor. Milvaion'da rol tabanlı yetkilendirme var ve granularity job/worker/dashboard seviyesinde. Birine sadece "görüntüleme" yetkisi verip production'a dokunamamasını sağlayabiliyorsun.
Bunun yanında API key desteği var: CI pipeline'ları, script'ler ve otomasyon için, kullanıcı hesabı olmadan. Key'in ne yapabileceğini yine permission bazında kısıtlıyorsun, iptal edilebiliyor, son kullanma tarihi verilebiliyor.
Hangfire'ın dashboard'ı varsayılan olarak yetkilendirmesiz gelir; IDashboardAuthorizationFilter yazarsın. Quartz'ta ortada bir dashboard olmadığı için soru da yok.
Metrik Raporları
Arka planda çalışan bir reporter worker düzenli olarak rapor üretiyor: hata oranı trendi, süre yüzdelikleri (p50/p95/p99), en yavaş job'lar, worker throughput'u ve doluluk oranı.
Bunlar dashboard'da grafik olarak duruyor. "Bu job son bir haftada yavaşladı mı" sorusuna bakarak cevap verebiliyorsun, sorgu yazmadan.
MCP Server — Scheduler'a Soru Sormak
Bu, en yeni eklenen şey. Milvaion bir MCP (Model Context Protocol) server olarak da çalışıyor. Claude Code, Cursor veya GitHub Copilot'ı Milvaion'a bağlayıp scheduler'ına düz cümleyle soru sorabiliyorsun.
40'tan fazla tool var: job listeleme, execution geçmişi, log okuma, dead letter kayıtları, worker sağlığı, tetikleme, duraklatma, düzenleme. Her biri kendi yetkisinin arkasında — sadece okuma yetkisi verdiğin bir API key'le asistan her şeyi inceleyebilir ama hiçbir şeyi değiştiremez.
Önemli bir not: Milvaion burada veri kaynağı. Sunucu tarafında hiçbir model sağlayıcı anahtarı tutulmuyor, dışarıya hiçbir çağrı yapılmıyor. Model senin editöründe, senin aboneliğinle çalışıyor.
Pratikte şuna benziyor: "dün gece hangi job'lar patladı ve neden" diye soruyorsun, asistan list_failures ile başlayıp get_occurrence ile logları çekiyor ve sana özetliyor.
Hangfire ve Quartz'ı İzlemek
Sonuncusu biraz ters köşe: Milvaion, Hangfire ve Quartz.NET kurulumlarını izleyebiliyor.
İki satır kodla mevcut scheduler'ını read-only şekilde Milvaion'a bağlıyorsun. Job'ların yine Hangfire'da çalışmaya devam ediyor, ama execution geçmişi, loglar, metrikler ve alarmlar Milvaion dashboard'ında toplanıyor. Migration yok, job kodun değişmiyor.
Bunu şunun için yazdım: Milvaion'un amacı hiçbir zaman sektör standardı olmuş Quartz ve Hangfire'a rakip olmak değildi. Bu rekabete girmekte yeni çıkmış bir open-source tool açısından anlamsız zaten. Hali hazırda sistemlerinde bu kütüphaneleri kullanan kişilerin Milvaion migration'ı zor olacak, bu yüzden bu geçişi organik bir şekilde yapabilmek ve Milvaion'un yeteneklerini kullanıcılara gösterecek bir onboarding amaçlı yazıldı bu entegrasyon.
Peki Ya Güvenilirlik?
Dağıtık bir sisteme geçtiğinde "mesaj kayboldu mu?" sorusu kaçınılmaz olarak gelir. Milvaion tarafında bunun cevapları şunlar:
- At-least-once delivery: RabbitMQ manuel ACK kullanılıyor. Worker işi bitirmeden ACK göndermiyor, dolayısıyla worker ortasında çökerse mesaj kuyruğa geri düşer.
- Otomatik retry: Exponential backoff ile. Kaç kere deneneceğini job bazında ayarlayabilirsin.
- Dead Letter Queue: Retry hakkı biten job'lar DLQ'ya düşer, kaybolmaz. Dashboard'da ayrı bir ekranda listelenir.
- Offline resilience: Worker RabbitMQ'ya ulaşamazsa sonuçları lokal SQLite'a yazar, bağlantı gelince gönderir.
Dikkat Edilmesi Gerekenler
Dürüst olmak gerekirse Milvaion her senaryo için doğru araç değil. Şu durumlarda başka bir şey kullanmalısın:
- Tek uygulama, tek sunucu. PostgreSQL + Redis + RabbitMQ üçlüsünü ayağa kaldırmanın maliyeti, kazandığından fazlaysa Hangfire çok daha doğru tercih.
- Sub-second zamanlama. Dispatcher en az saniyede bir kontrol ediyor. Milisaniye hassasiyeti gerekiyorsa buraya bakma.
- Event işleme. "Şu event gelince şunu çalıştır" ihtiyacın varsa bu bir scheduler işi değil; Kafka ya da benzeri bir şey daha uygun.
- .NET Framework. Milvaion .NET 10 hedefliyor. Legacy bir uygulaman varsa Hangfire ve Quartz çok daha geniş TFM desteği sunuyor.
Buna karşılık şu durumlarda gerçekten işe yarıyor: job'ların birden fazla servise dağılmışsa, uzun süren işlerin varsa, farklı job'lar farklı donanım istiyorsa, modern bir uygulama ve mimari istiyorsan, ya da denetim için tam bir çalışma geçmişi tutman gerekiyorsa.
Sonuç
Milvaion'ı yazma sebebim "daha iyi bir Hangfire" yapmak değildi. Zamanlama ile çalıştırmayı ayırmak istedim, çünkü elimdeki problem tam olarak oydu.
Ama işin bana öğrettiği şey şu oldu: o ayrımı yapmak işin sadece başlangıcıydı. Asıl değer, job'ları çalıştırdıktan sonra onlara ne olduğunu görebilmekte — dashboard'da, alarmlarda, raporlarda, auto disable gibi küçük ama gece uykunu kurtaran detaylarda.
Ortaya çıkan şey şu an .NET 10 üzerinde çalışıyor, Apache 2.0 lisanslı ve GitHub'da açık kaynak. Docker Compose ile beş dakikada ayağa kalkıyor:
git clone https://github.com/Milvasoft/milvaion.git
cd milvaion
docker compose up -d
Dashboard http://localhost:5000 adresinde seni bekliyor olacak.
Tüm özellikler için hemencecik ayaklandırıp inceleyebilirsin ya da dökümana bir göz at derim.
Bu yazıda temel mimariden ve özelliklerden bahsettim. Serinin bir sonraki yazısında, mevcut Hangfire ya da Quartz.NET kurulumunu hiç değiştirmeden Milvaion'ın izleme yeteneklerini nasıl ekleyebileceğine bakacağız — çünkü Milvaion'ı benimsemek için migration yapmak zorunda değilsin.
Bir sonraki yazıda görüşmek dileğiyle…