Mikroservis Mimarisi & Dayanıklılık 8 dk okuma 3 okunma

Timeout ve Cancellation Pattern ile Gecikme Yönetimi

Dağıtık sistemlerde yavaşlayan servisler, tüm sistemin çökmesine yol açabilir. Timeout (Zaman Aşımı) ve Cancellation (İptal) Pattern'ları, uzun süren işlemleri belirli bir süre sonra durdurarak kaynakları korur. Bu yazıda, timeout stratejileri, CancellationToken kullanımı, iptal propagasyonu, Polly ile timeout politikaları, HTTP Client Factory entegrasyonu ve en iyi pratikler ele alınır.

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. Bir işlem belirli bir süre içinde tamamlanmazsa, otomatik olarak iptal edilir ve kaynaklar serbest bırakılı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.

Temel Akış:

text

İstek Gönder → Süre Başlar → İşlem Devam Ediyor → Süre Doldu → İşlem İptal → Hata Dön

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ı, dosya okuma
Total Timeout Tüm işlemin tamamlanması için beklenen süre İş mantığı, karmaşık işlemler
Idle Timeout Bağlantının boşta kalma süresi Connection pool yönetimi

2. Cancellation Pattern Nedir?

Cancellation Pattern, devam eden bir işlemin dışarıdan iptal edilmesini sağlayan bir desendir. .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)
{
    // İptal kontrolü - işlem başlamadan önce
    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. İptal Kontrolü (Döngülerde)

csharp

public async Task ProcessOrdersAsync(IEnumerable<Order> orders, CancellationToken cancellationToken)
{
    foreach (var order in orders)
    {
        // Her iterasyonda iptal kontrolü
        cancellationToken.ThrowIfCancellationRequested();
        
        await ProcessOrderAsync(order, cancellationToken);
    }
}

C. Timeout ile CancellationToken Birleştirme

csharp

public async Task<Order> GetOrderWithTimeoutAsync(int orderId)
{
    // 5 saniye sonra otomatik iptal edilecek token
    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}");
    }
}

D. Birden Fazla Token Birleştirme (Linked Tokens)

csharp

public async Task ProcessAsync(CancellationToken externalToken)
{
    // Hem dış token hem de 10 saniyelik timeout'u birleştir
    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.

csharp

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

Dezavantaj: Bazı işlemler doğal olarak daha uzun sürer; sabit timeout bunları haksız yere iptal eder.

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. Dinamik Timeout
İşlem geçmişine göre timeout süresi ayarlanır (ör. p99 gecikmeye göre).

csharp

// p99 gecikmenin 2 katı timeout
var timeout = TimeSpan.FromMilliseconds(_metrics.GetP99Latency() * 2);
using var cts = new CancellationTokenSource(timeout);

D. 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;
}

// Kullanım
var budget = new RequestBudget(TimeSpan.FromSeconds(10));
var order = await _orderService.GetAsync(orderId, budget.CreateToken());
var payment = await _paymentService.GetAsync(orderId, budget.CreateToken());
// Toplam süre 10 saniyeyi geçmez

5. Polly ile Timeout Politikası

Polly, .NET ekosisteminde timeout politikalarını uygulamak için güçlü araçlar sunar.

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. Optimistic Timeout (İyimser)
İşlemi iptal etmez, ancak iptal sinyali gönderir. İşlem, iptal sinyalini görmezden gelebilir.

csharp

var optimisticTimeout = Policy.TimeoutAsync(
    TimeSpan.FromSeconds(5),
    TimeoutStrategy.Optimistic);

C. Pessimistic Timeout (Kötümser)
İşlemi zorla iptal eder. İşlem, iptal sinyalini kontrol etmek zorundadır.

csharp

var pessimisticTimeout = Policy.TimeoutAsync(
    TimeSpan.FromSeconds(5),
    TimeoutStrategy.Pessimistic);

D. Polly v8 ile Resilience Pipeline

csharp

using Polly;
using Polly.Timeout;

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.

csharp

// Her retry denemesi 5 saniye timeout'a sahip
var pipeline1 = new ResiliencePipelineBuilder()
    .AddRetry(...)      // Dıştaki
    .AddTimeout(TimeSpan.FromSeconds(5))  // İçteki
    .Build();

// Toplam süre 5 saniye (retry'lar bu süreye sığmalı)
var pipeline2 = new ResiliencePipelineBuilder()
    .AddTimeout(TimeSpan.FromSeconds(5))  // Dıştaki
    .AddRetry(...)      // İçteki
    .Build();

6. HTTP Client Factory ile Timeout Entegrasyonu

csharp

// Program.cs
builder.Services.AddHttpClient("ExternalApi", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
    client.Timeout = TimeSpan.FromSeconds(30); // HttpClient seviyesinde timeout
})
.AddResilienceHandler("timeout-pipeline", builder =>
{
    builder
        .AddTimeout(TimeSpan.FromSeconds(10))  // Toplam istek timeout'u
        .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. Bu, CancellationToken'ın otomatik olarak yayılmasıyla sağlanır.

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)
            {
                // Uygulama kapanıyor - temiz bir şekilde çık
                break;
            }
        }
    }
}

C. İptal Sinyalini Görmezden Gelmeme

İptal edilen işlemler, OperationCanceledException fırlatmalı ve kaynaklarını finally bloğunda temizlemelidir.

csharp

public async Task ProcessAsync(CancellationToken cancellationToken)
{
    var connection = await OpenConnectionAsync(cancellationToken);
    try
    {
        await DoWorkAsync(connection, cancellationToken);
    }
    catch (OperationCanceledException)
    {
        _logger.LogInformation("İşlem iptal edildi.");
        throw; // İptal sinyalini yukarı ilet
    }
    finally
    {
        await connection.CloseAsync(); // Kaynak temizliği her durumda yapılır
    }
}

8. Timeout ve Cancellation En İyi Pratikler

Pratik Açıklama
Her Dış Çağrıya Timeout Koyun HTTP, veritabanı, dosya sistemi, mesaj kuyruğu - hepsinde timeout olmalı.
CancellationToken'ı Her Yere Geçirin Metot imzalarına CancellationToken parametresi ekleyin ve yayın.
İptal Sinyalini Kontrol Edin Uzun döngülerde periyodik olarak ThrowIfCancellationRequested() çağırın.
Timeout'ları Kademeli Yapın İç servislerin timeout'u, dış servislerin timeout'undan kısa olmalı.
Toplam Bütçe Belirleyin Bir isteğin toplam süresi sınırlı olmalı (budget-based).
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.
İzleyin ve Ölçün Timeout oranlarını izleyin; yüksekse timeout süresini gözden geçirin.

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 ve kullanıcılar bekliyor.

Çözüm: Kademeli timeout ve graceful degradation.

csharp

public class OrderDetailComposer
{
    public async Task<OrderDetailDto> ComposeAsync(int orderId, CancellationToken cancellationToken)
    {
        // Toplam bütçe: 8 saniye
        using var budgetCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
            cancellationToken, budgetCts.Token);

        // Kritik veriler - kısa timeout
        var order = await _orderService.GetAsync(orderId, linkedCts.Token);

        // Paralel çağrılar - her biri kendi timeout'una sahip
        var paymentTask = GetWithTimeoutAsync(
            () => _paymentService.GetAsync(orderId, linkedCts.Token),
            TimeSpan.FromSeconds(2)); // Ödeme kritik değil, kısa timeout

        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,  // null olabilir
            Shipment = await shipmentTask, // null olabilir
            Stock = await stockTask       // null olabilir
        };
    }

    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).

  • Kargo ve stok bilgileri 5 ve 3 saniyelik timeout'larla alınır.

  • Toplam sayfa 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