Web uygulamasına zamanlayıcı eklerken her denetleyiciye “scheduler başladı mı?” kontrolü dağıtmak istemem. Uygulamanın açılışı ve kapanışı bir sahibin sorumluluğunda olmalı. Arşivimdeki hosted service yapıları, Quartz'ın yaşam döngüsünü Generic Host'a bağlama fikrini anlatmak için iyi bir örnek.
İşi kaydetmek ve scheduler'ı başlatmak ayrı adımlar
AddQuartz iş tanımlarını ve tetikleyicileri DI yapılandırmasına ekler. AddQuartzHostedService ise zamanlayıcının host açılırken başlayıp host dururken kapanmasını sağlar. Bu örnekte heartbeat işi hemen, sonra yaklaşık on saniyelik aralıklarla çalışacak şekilde tanımlı. Quartz.Extensions.Hosting 3.x ve Microsoft.Extensions.Hosting bağlamında düşünülmeli.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
var builder=Host.CreateApplicationBuilder();
builder.Services.AddQuartz(options=>{
var key=new JobKey("heartbeat");
options.AddJob<Heartbeat>(job=>job.WithIdentity(key));
options.AddTrigger(t=>t.ForJob(key).StartNow()
.WithSimpleSchedule(s=>s.WithIntervalInSeconds(10).RepeatForever()));
});
builder.Services.AddQuartzHostedService(options=>options.WaitForJobsToComplete=true);
await builder.Build().RunAsync();
public class Heartbeat:IJob
{
public Task Execute(IJobExecutionContext context)
{ context.CancellationToken.ThrowIfCancellationRequested(); return Task.CompletedTask; }
}
Heartbeat yalnız iptal kontrolü yapıp tamamlanıyor; bu kodda gözle görülür heartbeat logu veya veritabanı kaydı yok. Gerçek çalışmayı sınamak için Execute içine test edilebilir bir işlem veya kontrollü sayaç konabilir. “Konsolda çıktı yok” ile “scheduler hiç başlamadı” aynı teşhis değildir.
Neden host içinde sahiplik önemli?
Uygulama ömrü tek bir RunAsync çağrısıyla yönetiliyor. Başlatma, durdurma ve Ctrl+C gibi kapanış sinyalleri aynı çatıya bağlanıyor. Scheduler'ı bir request içinde başlatırsam her istekte yeni bir örnek oluşturma veya son referansı kaybetme riski doğar. Hosted service, bu uzun ömürlü işin bir HTTP talebine ait olmadığını kodda gösterir.
Bu ayrım servis yaşam sürelerini de görünür kılar. Uzun ömürlü bir nesnenin scoped DbContext'i constructor'da yakalayıp sürekli kullanması doğru bir tasarım değildir. İşin scoped bağımlılıkları uygun çalışma scope'u içinde çözülmelidir. Quartz sürümü ve job factory yapılandırması DI davranışını belirler; kullandığım paket sözleşmesini doğrularım.
Kapanışı beklemek ile iptali tasarlamak
WaitForJobsToComplete=true, kapanışta devam eden işleri bekleme niyetidir. İşin ilk satırında ThrowIfCancellationRequested çağırmak ise yalnız o anki durumu sınar. Sonraki HTTP veya SQL çağrısı token almıyorsa kapanış sırasında uzun süre çalışmaya devam edebilir. Aynı token bütün iptal edilebilir I/O zincirine taşınmalı.
İşin normal süresi, kendi timeout'ları ve host kapanış bütçesi uyumlu olmalı. Uygulama durdurulduğunda süreç sonsuza dek açık kalıyorsa “tamamlanmayı bekliyor” demek teşhis olabilir ama çözüm değildir. Bir işi kesilebilir noktalara ayırmak ve kaldığı yerden güvenilir biçimde devam ettirmek gerekebilir.
Yeniden başlatma, kaldığı yerden devam garantisi değildir
Bu örnekte kalıcı Quartz store yapılandırılmıyor. Uygulama kapalıyken bellekteki zamanlama bir iş günlüğü gibi saklanmaz. Kalıcı tetikleyici eklemek de işin kendi ilerleme kaydını otomatik oluşturmaz. “Son hangi dönemi tamamladım?” bilgisini iş verisi olarak saklamak ile “bir sonraki tetikleme ne zaman?” bilgisini saklamak farklıdır.
Web servisini iki kopyaya çıkarmak da tek kopya zamanlayıcıyı kendiliğinden iki kopya iş güvenliğine dönüştürmez. Kalıcı store, cluster veya veri düzeyinde tekilleştirme seçenekleri iş gereksinimine göre ele alınır. Küçük bir kişisel uygulamada bu özelliklere ihtiyaç yoksa sırf kurumsal görünmesi için eklemem.
Yaşam döngüsünü gerçekten durdurarak kontrol etmek
Testte host'u başlatır, bir çalışmayı gözler, ardından kapanış isteği gönderirim. Yeni iş başlamaması, devam eden işin seçilen politikayla bitmesi veya iptal olması ve süreçte açık kaynak kalmaması beklenir. Sonra host yeniden açılır; tanımların tekilleştiği ve tamamlanmış işin gereksiz tekrarlanmadığı kontrol edilir. Başlangıç kaydının başarılı olması bu davranışların hepsinin yerine geçmez.
Quartz hosted service entegrasyonu; .NET Generic Host yaşam döngüsü.