Mikroservis Mimarisi & Dayanıklılık 6 dk okuma 2 okunma

Timeout ve Cancellation Pattern ile Gecikme Yönetimi

Timeout ve Cancellation Pattern'ları, yavaşlayan servislerin tüm sistemi çökertmesini engeller. Timeout, bir işlemin belirli süre içinde tamamlanmasını zorunlu kılar; CancellationToken ise iptal sinyalinin tüm katmanlara yayılmasını sağlar. Kademeli timeout, toplam bütçe ve Polly entegrasyonu ile gecikme yönetimi sağlanır. Monotonik saat kullanımı ve graceful shutdown kritik öneme sahiptir.

Timeout ve Cancellation Pattern ile Gecikme Yönetimi

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, finally blokları 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.

Tüm yazılar Soru ya da yorumun mu var? Yaz