Timeout ve Cancellation Pattern ile Gecikme Yönetimi
Dağıtık sistemlerin en sinsi düşmanı, yavaşlıktır. Bir servis çöktüğünde fark edilir ve hızlıca müdahale edilir; ancak bir servis yavaşladığında, istekler birikir, thread havuzları tükenir, bellek şişer ve zincirleme bir çöküş (cascade failure) yaşanır. İşte Timeout (Zaman Aşımı) ve Cancellation (İptal) Pattern'ları, bu sessiz katili durdurmak için tasarlanmıştır.
1. Timeout Pattern Nedir?
Timeout Pattern, bir işlemin belirli bir süre içinde tamamlanmasını zorunlu kılan bir dayanıklılık desenidir. Süre aşılırsa, işlem iptal edilir ve bir hata döner. Amaç, yavaş işlemlerin kaynakları süresiz olarak meşgul etmesini engellemektir.
Timeout Türleri:
| Tür | Açıklama | Kullanım Alanı |
|---|---|---|
| Connection Timeout | Bağlantı kurulması için beklenen süre | Ağ bağlantısı kurulumu |
| Read Timeout | Yanıtın okunması için beklenen süre | HTTP çağrıları |
| Total Timeout | Tüm işlemin tamamlanması için beklenen süre | İş mantığı |
| Idle Timeout | Bağlantının boşta kalma süresi | Connection pool |
2. Cancellation Pattern Nedir?
Cancellation Pattern, devam eden bir işlemin dışarıdan iptal edilmesini sağlar. .NET'te bu, CancellationToken yapısı ile gerçekleştirilir.
Kullanım Senaryoları:
-
Kullanıcı isteği iptal ettiğinde (tarayıcı sekmesini kapattı).
-
Bir timeout süresi dolduğunda.
-
Uygulama kapanırken (graceful shutdown).
-
Bir üst işlem iptal edildiğinde (propagation).
CancellationToken'ın Sağladıkları:
-
İşbirliği (Cooperative): İptal, zorla değil, işbirliği ile yapılır. İşlem, token'ı periyodik olarak kontrol eder ve iptal isteğini görürse durur.
-
Yayılım (Propagation): Bir üst işlem iptal edildiğinde, alt işlemlere otomatik olarak yayılır.
-
Kaynak Temizliği: İptal edilen işlemler,
finallyblokları ile kaynaklarını temizler.
3. .NET'te CancellationToken Kullanımı
A. Temel Kullanım
csharp
public async Task<Order> GetOrderAsync(int orderId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var order = await _dbContext.Orders
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
if (order == null)
throw new NotFoundException($"Sipariş bulunamadı: {orderId}");
return order;
}
B. Timeout ile CancellationToken Birleştirme
csharp
public async Task<Order> GetOrderWithTimeoutAsync(int orderId)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
return await GetOrderAsync(orderId, cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new TimeoutException($"Sipariş getirme işlemi 5 saniyede tamamlanamadı: {orderId}");
}
}
C. Birden Fazla Token Birleştirme (Linked Tokens)
csharp
public async Task ProcessAsync(CancellationToken externalToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
externalToken, timeoutCts.Token);
await DoWorkAsync(linkedCts.Token);
}
4. Timeout Stratejileri
A. Sabit Timeout (Fixed Timeout)
Her istek için aynı süre tanınır. Basittir ancak bazı işlemler doğal olarak daha uzun sürer.
csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
B. Kademeli Timeout (Tiered Timeout)
Farklı işlem türleri için farklı timeout süreleri tanımlanır.
csharp
public static class TimeoutPolicies
{
public static readonly TimeSpan FastQuery = TimeSpan.FromSeconds(2);
public static readonly TimeSpan NormalQuery = TimeSpan.FromSeconds(10);
public static readonly TimeSpan LongRunningReport = TimeSpan.FromMinutes(5);
public static readonly TimeSpan ExternalApiCall = TimeSpan.FromSeconds(15);
}
C. Bütçe Tabanlı Timeout (Budget-Based)
İstek başlangıcında bir toplam bütçe belirlenir; her servis çağrısı bu bütçeden harcar.
csharp
public class RequestBudget
{
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private readonly TimeSpan _totalBudget;
public RequestBudget(TimeSpan totalBudget) => _totalBudget = totalBudget;
public TimeSpan Remaining => _totalBudget - _stopwatch.Elapsed;
public CancellationToken CreateToken() => new CancellationTokenSource(Remaining).Token;
}
var budget = new RequestBudget(TimeSpan.FromSeconds(10));
var order = await _orderService.GetAsync(orderId, budget.CreateToken());
var payment = await _paymentService.GetAsync(orderId, budget.CreateToken());
5. Polly ile Timeout Politikası
A. Basit Timeout
csharp
using Polly;
using Polly.Timeout;
var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(5));
try
{
var response = await timeoutPolicy.ExecuteAsync(async (ct) =>
await _httpClient.GetAsync("https://api.example.com/data", ct));
}
catch (TimeoutRejectedException ex)
{
Console.WriteLine("İşlem zaman aşımına uğradı.");
}
B. Polly v8 ile Resilience Pipeline
csharp
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(10),
OnTimeout = args =>
{
Console.WriteLine($"İşlem {args.Timeout.TotalSeconds} saniyede tamamlanamadı.");
return default;
}
})
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true
})
.Build();
Dikkat: Polly'de politika sırası önemlidir. Timeout, retry'dan önce eklenirse her retry denemesi kendi timeout'una sahip olur. Retry, timeout'tan önce eklenirse toplam süre timeout ile sınırlanır.
6. HTTP Client Factory ile Timeout Entegrasyonu
csharp
builder.Services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddResilienceHandler("timeout-pipeline", builder =>
{
builder
.AddTimeout(TimeSpan.FromSeconds(10))
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(60)
});
});
Önemli: HttpClient.Timeout ile Polly timeout'u birlikte kullanıldığında, HttpClient.Timeout genellikle daha uzun olmalıdır; aksi halde Polly timeout'u devreye giremeden HttpClient kendi timeout'unu fırlatır.
7. İptal Propagasyonu (Cancellation Propagation)
Bir üst işlem iptal edildiğinde, alt işlemlerin de iptal edilmesi gerekir.
A. ASP.NET Core'da İptal Propagasyonu
csharp
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id, CancellationToken cancellationToken)
{
// ASP.NET Core, HTTP isteği iptal edildiğinde (istemci bağlantıyı kestiğinde)
// cancellationToken otomatik olarak iptal edilir.
var order = await _orderService.GetAsync(id, cancellationToken);
var customer = await _customerService.GetAsync(order.CustomerId, cancellationToken);
return Ok(new { order, customer });
}
B. Arka Plan İşlemlerinde İptal
csharp
public class OrderProcessor : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var order = await _queue.DequeueAsync(stoppingToken);
await ProcessOrderAsync(order, stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
}
8. Timeout ve Cancellation En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Her dış çağrıya timeout koyun | HTTP, veritabanı, dosya sistemi, mesaj kuyruğu |
| CancellationToken'ı her yere geçirin | Metot imzalarına ekleyin ve yayın |
| İptal sinyalini kontrol edin | Uzun döngülerde ThrowIfCancellationRequested() çağırın |
| Timeout'ları kademeli yapın | İç servislerin timeout'u, dış servislerden kısa olmalı |
| Toplam bütçe belirleyin | Bir isteğin toplam süresi sınırlı olmalı |
| Polly sırasına dikkat edin | Timeout ve retry sırası, beklenen davranışı belirler |
| HttpClient.Timeout'u ayarlayın | Polly timeout'undan daha uzun olmalı |
| OperationCanceledException'ı yakalayın | İptal durumunu loglayın, ancak yutmayın |
| Graceful shutdown sağlayın | Uygulama kapanırken devam eden işlemleri iptal edin |
9. Timeout ve Diğer Dayanıklılık Desenleri
| Desen | Timeout ile İlişkisi |
|---|---|
| Retry | Her retry denemesi kendi timeout'una sahip olmalı |
| Circuit Breaker | Timeout'lar circuit breaker'ı tetikleyebilir (yavaşlık = hata) |
| Bulkhead | Timeout'lar, bulkhead havuzlarının tükenmesini önler |
| Fallback | Timeout durumunda yedek yanıt dönülebilir |
10. Gerçek Dünya Senaryosu
Senaryo: Bir e-ticaret uygulamasında, sipariş detay sayfası üç harici servisten veri çekiyor: Ödeme Servisi (yavaş olabilir), Kargo Servisi (hızlı) ve Stok Servisi (orta).
Problem: Ödeme Servisi yavaşladığında, tüm sayfa yüklenemiyor.
Çözüm: Kademeli timeout ve graceful degradation.
csharp
public class OrderDetailComposer
{
public async Task<OrderDetailDto> ComposeAsync(int orderId, CancellationToken cancellationToken)
{
using var budgetCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, budgetCts.Token);
var order = await _orderService.GetAsync(orderId, linkedCts.Token);
var paymentTask = GetWithTimeoutAsync(
() => _paymentService.GetAsync(orderId, linkedCts.Token),
TimeSpan.FromSeconds(2));
var shipmentTask = GetWithTimeoutAsync(
() => _shipmentService.GetAsync(orderId, linkedCts.Token),
TimeSpan.FromSeconds(5));
var stockTask = GetWithTimeoutAsync(
() => _stockService.GetAsync(orderId, linkedCts.Token),
TimeSpan.FromSeconds(3));
await Task.WhenAll(paymentTask, shipmentTask, stockTask);
return new OrderDetailDto
{
Order = order,
Payment = await paymentTask,
Shipment = await shipmentTask,
Stock = await stockTask
};
}
private async Task<T?> GetWithTimeoutAsync<T>(Func<Task<T>> action, TimeSpan timeout) where T : class
{
try
{
using var cts = new CancellationTokenSource(timeout);
return await action().WaitAsync(cts.Token);
}
catch (OperationCanceledException)
{
_logger.LogWarning("Servis çağrısı zaman aşımına uğradı: {Timeout}", timeout);
return null;
}
}
}
Bu yapılandırma sayesinde Ödeme Servisi 2 saniyede yanıt vermezse, sayfa yine de yüklenir (ödeme bilgisi boş görünür); toplam yükleme süresi 8 saniyeyi geçmez.
Sonuç
Timeout ve Cancellation Pattern'ları, dağıtık sistemlerde gecikme yönetiminin temel taşlarıdır. Her dış çağrıya timeout koyun, CancellationToken'ı her yere geçirin, kademeli timeout stratejileri uygulayın, toplam bütçe belirleyin ve iptal sinyalini düzgün yönetin. Polly, .NET ekosisteminde timeout politikalarını uygulamak için olgun ve güçlü bir kütüphanedir. Retry, Circuit Breaker, Bulkhead ve Fallback ile birlikte kullanıldığında, sisteminiz yavaşlayan servislere karşı çok daha dirençli hale gelir. Unutmayın: Yavaşlık, çöküşün sessiz habercisidir; timeout, bu sessizliği bozan alarmdır.