Durable Task Gerçek Dünyada: Hangi Sorunları Çözer?
Durable Task hakkında en sık sorulan soru, replay mekanizmasının nasıl çalıştığı ya da scheduler’ın durumu nasıl kalıcı hâle getirdiği değil; çok daha pratik bir şey: bu teknoloji hangi gerçek problemi çözüyor? Ne zaman sıradan C# kodu, bir kuyruk, bir veritabanı veya arka plan işçisi yeterli oluyor, ne zaman dayanıklı yürütmeye (durable execution) geçmek gerekiyor? Tamir Dresher’ın.NET Blog’daki yazısı bu soruya “hello world” orkestrasyonlarıyla değil, işlerin gerçekten karmaşıklaştığı senaryolarla yanıt veriyor. Aşağıda yazının teknik çerçevesini, sorumluluk sınırlarını ve Durable Task’ın çözmediği kısımları özetliyorum.
Sorun: İşçi öldüğünde “nerede kalmıştım” bilgisi de ölüyor
Bir müşteri için Azure ortamı hazırlayan arka plan işçisi düşünün. Ağı oluşturuyor, veritabanı dağıtımını başlatıyor, depolama hesabını yaratıyor ve uygulamayı dağıtıyor. Sonra işçi process’i kayboluyor. Azure tarafında işler hâlâ devam ediyor olabilir; ancak yerel değişkenler ve continuation’lar yok oldu. Metodu yeniden başlatmak kolay kısım. Zor olan, hangi işlemlerin tamamlandığını ve hangilerini tekrarlamanın güvenli olduğunu anlamak.
Elbette bunu kendiniz kurabilirsiniz: bir veritabanı, kuyruk, outbox tablosu, zamanlanmış işler ve mutabakat (reconciliation) mantığı. Bazen doğru yanıt tam olarak budur. Ama yalnızca “sırada ne var ve nasıl devam edeceğim” bilgisini hatırlamak için koca bir sistem inşa etmeye başladığınızda, dayanıklı orkestrasyon anlamlı hâle gelir.
Yazıdaki örnekler Durable Task.NET SDK v1.26.0 ve Durable Task Scheduler (DTS) üzerinden kurgulanmış. Kod parçaları orkestrasyon sınırlarına odaklanıyor; adapter’lar, DTO tanımları ve kayıt işlemleri kasıtlı olarak çıkarılmış. Yani bunlar derlenip test edilmiş bir örnek değil, tasarımı anlatan iskeletler. Özellikle de şunu varsaymamak gerekiyor: durable history, dış sistemlerde exactly-once etki garantisi vermez.
Sorumluluk sınırı: Durable Task ne yapar, uygulama ne yapar
Yazıdaki beş senaryo (fatura onayları, Azure tenant provisioning, abonelik ödemeleri, sırasız gelen webhook’larla ilerleyen son dakika haberleri ve olay inceleyen bir AI ajanı) aynı sınırı paylaşıyor: Durable Task sürecin koordinasyonunu üstlenir, dış sonucun otoritesi uygulamada kalır.
| Senaryo | Durable Task’ın katkısı | Uygulamanın sorumluluğu |
|---|---|---|
| İnsan onayı | Dayanıklı bekleme ve devam etme | Yetkilendirme ve otoriter karar |
| Provisioning | İş seviyesinde sıralama | ARM mutabakatı ve güvenli temizlik |
| Ödemeler | Zamanlayıcı, callback ve kurtarma politikası | Ödeme defteri ve belirsiz kalan tahsilatlar |
| Webhook’lar | Dayanıklı koordinasyon | Hedef düzeyinde revizyon fencing’i |
| Ajanlar | Dayanıklı onay ve kurtarma akışı | Öneri kimliği ve güvenli araç çalıştırma |
Fatura onayı: Beklemeyi neden worker hatırlasın?
Bir fatura işleme servisi belgeyi ayrıştırır, sonucu doğrular ve faturayı ya muhasebeye yazar, ya reddeder ya da insan incelemesine gönderir. Sekiz faturalık bir partide yalnızca biri onay bekliyorsa, diğer yedisinin beklemesi için bir neden yoktur.
Sıradan C# ile yazılan işlem yolunda yapısal bir güvenilmezlik yoktur; OpenInvoiceReviewAsync bir inceleme kaydı ve bildirim outbox’ı oluşturabilir. Ancak biri faturayı ertesi gün onayladığında başka bir handler doğru belge kuşağını yüklemek, son tarihi kontrol etmek, onaylanan yükü geri getirmek ve kaldığı yerden devam etmek zorundadır. İşçi, ayrıştırma ile doğrulama arasında ölürse, uygulamanın yeniden ayrıştırma mı yapacağına yoksa kayıtlı sonucu mu okuyacağına karar vermesi gerekir. Durable Task’ın doğrudan ifade edebildiği şey tam olarak bu “devam protokolü”.
Sınırlı bir parti, her faturaya kendi dayanıklı geçmişini veren bağımsız alt orkestrasyonlarla başlatılabilir:
public override async Task<InvoiceResult[]> RunAsync(
TaskOrchestrationContext context, InvoiceBatch batch)
{
if (batch.Invoices is null || batch.Invoices.Length is <1or> 8
|| batch.Invoices.Select(x => x.InvoiceKey).Distinct().Count()
!= batch.Invoices.Length)
throw new ArgumentException("Supply 1 to 8 distinct invoices.");
return await Task.WhenAll(batch.Invoices.Select(invoice =>
context.CallSubOrchestratorAsync<InvoiceResult>(
nameof(ProcessInvoice), invoice)));
}
Buradaki sekiz sınırı bir DTS kotası değil, uygulamanın kendi kabul limitidir; daha büyük partiler sayfalanabilir ve tenant/global eşzamanlılık limitlerine tabi tutulabilir. Tamamlanan bir çocuk orkestrasyon kendi faturasını muhasebeye yazarken, bir diğeri onay beklemeye devam edebilir. Task.WhenAll tüm çocukları bekler; fakat biri başarısız olduğunda kardeşinin muhasebe kaydını geri almaz.
Ayrıştırma ve doğrulama birer activity’dir. Başarılı sonuçları orkestrasyon geçmişine yazıldıktan sonra uyumlu bir replay, activity’leri tekrarlamak yerine kayıtlı sonuçlarla aynı kod yolunu izler. Bu, askıya alınmış bir CLR stack’inin geri yüklenmesi değil, orchestrator kodunun geçmişe karşı yeniden çalıştırılmasıdır.
İnceleme gerektiğinde bekleme şöyle ifade edilir:
var review = new InvoiceReview(invoice, assessment.PayloadReference);
await context.CallActivityAsync("OpenInvoiceReview", review);
TimeSpan remaining =
invoice.ReviewDeadlineUtc - context.CurrentUtcDateTime;
if (remaining > TimeSpan.Zero)
{
try
{
InvoiceReviewHint hint =
await context.WaitForExternalEvent<InvoiceReviewHint>(
$"invoice-review:{invoice.ReviewId}", remaining);
if (hint.ProcessingId != invoice.ProcessingId
|| hint.ReviewId != invoice.ReviewId)
throw new InvalidOperationException("Review identity mismatch.");
}
catch (OperationCanceledException)
{
// The durable wait timed out. Read the committed decision below.
}
}
InvoiceDecision decision =
await context.CallActivityAsync<InvoiceDecision>(
"ResolveInvoiceReview", review);
Zamanlayıcı dayanıklıdır: fatura beklerken hiçbir thread işgal edilmez ve yerine gelen bir worker, çocuğu aynı bekleme noktasına kadar replay edebilir. Ancak dışarıdan gelen olay yalnızca bir ipucudur. Kimliği doğrulanmış bir uç nokta, çocuğa sinyal göndermeden önce tek bir nihai inceleme kararını uygulamanın inceleme deposuna atomik biçimde yazmalıdır. ResolveInvoiceReview, zamanlayıcı tetiklendiğinde dahi bu otoriter kaydı okur veya kapatır. Onay son tarihten hemen önce commit edildiyse ama ipucu hemen sonra geldiyse, sonucu bildirim zamanlaması değil kayıtlı karar belirler.
Muhasebeye yalnızca geçerli bir otomatik değerlendirme ya da commit edilmiş bir insan onayı ilerleyebilir. İlgili activity kararlı bir posting key ve değişmez istek parmak izi kullanır; muhasebe sistemi kaydı kabul ettiği hâlde onay yanıtı kaybolduysa entegrasyonun aynı posting kimliğiyle mutabakat yapması gerekir. Durable history tek başına “tam olarak bir kez muhasebeleştirme” sözü veremez.
Tenant provisioning: Bu işi ARM zaten yapmıyor mu?
İkinci örnek bir SaaS tenant’ının devreye alınması. Dikkat edilmesi gereken ilk nokta: TenantCreated olayı tenant kaydının kabul edildiği anlamına gelir, ortamının hazır olduğu anlamına değil. Tasarım; özel bir sanal ağ, PostgreSQL sunucusu, private erişimli depolama ve Container App oluşturuyor, ardından DNS’i yapılandırıp hazırlık kontrolünden sonra tenant’ı etkinleştiriyor. Örnekteki yardımcı metotlar Azure SDK çağrıları değil, Bicep/ARM işlemlerini sarmalayan uygulama soyutlamalarıdır.
DTS eklemeden önce rahatsız edici soruyu sormak gerekiyor: bu işi ARM zaten çözmüyor mu? Tek ve iyi tanımlanmış bir dağıtım için çözüyor olabilir. ARM/Bicep bağımlılık sıralamasını, paralel dağıtımı, kararlı kimliklerle idempotent yeniden uygulamayı ve sunucu tarafında dağıtım durumunu zaten destekler. Bir worker çökmesi ARM dağıtımını silmez; dağıtımı sorgulamak ve aynı arzu edilen durumu yeniden uygulamak yeterli olabilir.
Daha geniş onboarding süreci ise farklıdır: uygulamanın tenant isteğini kabul etmesi, ilerlemeyi raporlaması, hazırlık durumuyla etkinleştirmeyi eşgüdümlemesi, varsa harici onay adımlarından sağ çıkması ve kısmen hazırlanmış bir ortamla ne yapacağına karar vermesi gerekir. İş seviyesindeki bir workflow’un değer kattığı yer burasıdır.
Kaynak hazırlığı ile tenant etkinleştirmeyi ayırmak
Önce API yetkilendirme yapar, değişmez bir onboarding isteğini dayanıklı biçimde kabul eder ve 202 Accepted ile birlikte bir istek kimliği ve durum URL’i döner. Kabul edilmiş olmak hazır olmak değildir. Uygulama; tenant’ı, ortamı, değişmez spesifikasyonu ve sahiplik kuşağını (ownership generation) kaydeder. Aynı idempotency key aynı spesifikasyonla tekrar kullanılırsa mevcut istek döner; spesifikasyon değiştirilirse istek reddedilir.
Scheduler’a devretme, uygulamanın veritabanıyla sihirli biçimde atomik değildir. Başlatma niyetini kabul kaydının yanındaki bir outbox’a yazmak, sonra dispatch edip workflow kimliğini mutabık kılmak gerekir. Sürekli yeni instance zamanlamak, var olanı sürdürmekle aynı şey değildir.
Dış orkestrasyon iş bağımlılığını açıkça ifade eder:
public override async Task<TenantActivation> RunAsync(
TaskOrchestrationContext context, EnvironmentPlan plan)
{
EnvironmentResult resources =
await context.CallSubOrchestratorAsync<EnvironmentResult>(
nameof(ProvisionEnvironment), plan);
return await context.CallActivityAsync<TenantActivation>(
"ActivateTenant", new TenantActivationRequest(plan, resources));
}
Çocuk orkestrasyon kaynak hazırlığında biter. Etkinleştirme, tenant sahipliğini ve kaynakların gerçek hazırlığını yeniden doğrulayan ayrı ve idempotent bir activity’dir. Etkinleştirme başarısız olursa dış workflow görünür biçimde başarısız olur; çocuğun hazırladığı kaynakları otomatik olarak silmez. Etkinleştirmeyi onarmak, provisioning’i geri almaktan farklı bir problemdir.
Çocuk orkestrasyonun içinde aşamalar dalgalar hâlinde ilerler: ağ; paralel olarak veritabanı ve depolama; uygulama; DNS; hazırlık kontrolü. Her aşama activity’si bir niyet kaydeder ve sağlayıcı işlemini başlatır ya da mevcut işlemle mutabakat kurar. Ayrı bir okuma activity’si tek ve sınırlı bir durum sorgusu yapar; orchestrator ise yoklamalar arasına dayanıklı bir timer koyar:
DateTime now = context.CurrentUtcDateTime;
if (now >= deadline)
return Failed("PollingDeadlineExceeded");
TimeSpan delay = TimeSpan.FromSeconds(progress.PollAfterSeconds);
DateTime wakeAt = delay >= deadline - now
? deadline
: now.Add(delay);
await context.CreateTimer(wakeAt, CancellationToken.None);
Orchestrator içinde ne HTTP isteği ne de Task.Delay çalışır; I/O’yu activity yapar, geçmişe yalnızca activity sonucu ve timer kararı yazılır. 45 dakikalık yoklama bütçesi ek yoklamayı sınırlar; ancak zaten kabul edilmiş bir Azure işlemini iptal etmez.
İki farklı hata penceresi
Veritabanı dağıtımı Azure tarafından kabul edildi ama activity tamamlanma bilgisi DTS’e ulaşmadan worker öldüyse: DTS’te kayıtlı başarılı bir sonuç olmadığı için activity yeniden teslim edilebilir. Yeniden teslim edilen adapter, uygulamanın işlem günlüğüne ve kararlı Azure dağıtım/kaynak kimliğine bakar; dağıtım hâlâ sürüyorsa alakasız yeni bir veritabanı yaratmak yerine sorgulamaya devam eder. Sağlayıcı nihai tamamlanmayı döndürdüğünde sonuç kaydedilir ve çocuk orkestrasyon uygulama dağıtımına ilerler. Tenant ise yalnızca doğrulanmış hazırlık kanıtı döndükten sonra etkinleştirilir.
Buna karşılık activity tamamlanması kaydedildikten sonra worker çöktüyse, uyumlu replay sırasında orchestrator bu sonucu geçmişten okur ve tamamlanmış activity’yi yeniden çağırmaz. Bunlar farklı hata pencereleridir: birincisini adapter’ınızın mutabakat protokolü, ikincisini dayanıklı yürütme geçmişi çözer.
Telafi bir “geri al” düğmesi değildir
Başarısız bir workflow, Azure’ın çalışmayı bıraktığı anlamına gelmez. Veritabanı ile depolama paralel oluşturulurken depolama başarısız olsa bile Azure veritabanını hâlâ oluşturuyor olabilir. Görülen her şeyi hemen silerseniz, o veritabanı temizlik kontrolünden sonra tamamlanabilir; sonuçta hata ve temizlik raporlanmışken ücret üretmeye devam eden sahipsiz bir veritabanı kalır.
Temizlikten önce uygulamanın yapması gerekenler:
- Bu başarısız onboarding denemesinin yeni provisioning işlemi göndermesini engellemek.
- Daha önce gönderilmiş işlemleri (yanıtı kaybolanlar dâhil) kontrol edip gerçek durumlarıyla mutabakat kurmak. Timeout’un veya çöken bir worker’ın Azure’ı durdurduğunu varsaymamak.
- Yalnızca bu deneme için oluşturulmuş kaynakları, ancak bekleyen işlemlerin silmeden sonra bunları yeniden yaratamayacağı tespit edildikten sonra silmek.
Otomatik temizliğin katı bir sınırı var: bu, tenant etkinleştirilmeden ve müşteri verisi kabul edilmeden önceki ilk onboarding denemesinin başarısızlığı olmalıdır. Mevcut paylaşımlı kaynaklar ve aktif müşteri verisi asla temizlik hedefi değildir. Her silmeden önce güncel sahiplik ve sağlayıcı kimliği yeniden doğrulanmalı, uygun kaynaklar ters bağımlılık sırasıyla silinmelidir: DNS, uygulama, veritabanı/depolama, sonra ağ. Hâlâ neyin çalıştığı veya neyin güvenle sahiplenildiği tespit edilemiyorsa, tahmin etmek yerine kaynaklar korunmalı ve mutabakat için NeedsIntervention durumu yüzeye çıkarılmalıdır. Hem özgün provisioning hatası hem de temizlik hataları kayıt altına alınmalıdır. Durable Task bu kurtarma sürecini koordine edebilir; hangi Azure kaynağının silinmesinin güvenli olduğuna karar veremez.
Fencing ve uçuştaki işlemleri boşaltmak
Temizliğin neden tamamlanmış adımları tersine çevirmekten zor olduğunu iki kavram açıklıyor:
Fencing, eski denemelerin yeni değişiklik yapmasını engeller; ancak bu, kısıtlamayı uygulayan tarafın kuralı zorlamasına bağlıdır. Uygulamanız eski bir provisioning kuşağından gelen gönderimleri reddedebilir, fakat bu Azure’ın zaten kabul ettiği istekleri iptal etmez.
Uçuştaki işlemleri boşaltma ve mutabakat, temizliği güvenli ilan etmeden önce daha önce gönderilmiş işleri (onayı kaybolanlar dâhil) hesaba katmak demektir. Dağıtık sistem tartışmalarında ilgili bekleyen işin kalmadığı duruma bazen quiescence (durağanlık) denir. Burada anlatılan pratik bir temizlik gereksinimidir; biçimsel eşzamanlılık özelliği olan “quiescent consistency” değildir ve Azure’ın otomatik olarak sağladığı bir garanti de değildir. Arka plan için Martin Kleppmann’ın fencing token açıklamasına ve Microsoft’un ARM dağıtım iptali API’sine bakılabilir. İptal bile kaynakları kısmen dağıtılmış bırakabilir.
Son bir ayrıntı: Task.WhenAll provisioning dalgasının tamamını bekler, ancak activity’lerden biri exception fırlatırsa yürütme sonuçları kontrol eden koda ulaşmaz. Yazıda beklenen hatalar sonuç olarak ele alınıyor, beklenmeyen hataların ise orkestrasyonu normal biçimde başarısız kılmasına izin veriliyor. Bu ayrım temizlik için de önemli: tüm provisioning görevlerinin bitmesini beklemek, neyin başarılı neyin başarısız olduğunu ve dolayısıyla neyin temizlenmesi gerektiğini gösterir.
Çıkarım
Durable Task’ın asıl kazancı, ayrıştırmayı, doğrulamayı, onay beklemesini ve muhasebeleştirmeyi hatırlamak için ayrı ayrı handler’lar yazmak yerine sürecin tamamını tek bir yerde tarif edebilmek ve nerede kalındığını runtime’a bıraktırabilmek. Buna karşılık yetkilendirme, kararların kalıcı kaydı, dış çağrıların tekrarlanabilir (idempotent) olması ve sağlayıcı tarafındaki mutabakat hâlâ uygulamanın sorumluluğunda. Mevcut continuation handler’larınız bunları zaten temiz biçimde yapıyorsa, yeni bir runtime eklemeniz gerekmeyebilir.
Kaynaklar ve İleri Okuma
- Durable Task in the Real World: What Problems Does It Actually Solve? —.NET Blog (Tamir Dresher)
- Durable Task.NET SDK v1.26.0 sürüm notları (GitHub)
- microsoft/durabletask-dotnet deposu
- Azure Durable Task dokümantasyonu
- Durable Task orkestrasyonları
- Durable Task entity’leri
- Durable Task singleton’ları
- Durable Task Scheduler (DTS)
- Microsoft Agent Framework ile durable agent’lar
- agent-framework-durable-extension deposu
- Martin Kleppmann: How to do distributed locking (fencing token’lar)
- ARM Deployments – Cancel REST API
- .NET Blog RSS akışı







Yorum gönder