Mikroservis mimarisinde veya dış API entegrasyonlarında geçici hatalar kaçınılmazdır: ağ titremesi, servis yeniden başlama, rate limit aşımı... Bu geçici hataları akıllıca yönetmek için .NET ekosisteminde Polly standart haline geldi.

Polly v8: ResiliencePipeline

Polly v8 ile API tamamen yenilendi. Artık Policy yerine ResiliencePipeline kullanıyoruz:

// Polly v8 kurulumu
// dotnet add package Polly.Core
// dotnet add package Microsoft.Extensions.Http.Resilience

Retry: Geçici Hataları Tekrar Dene

// Temel retry
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(300),
        BackoffType = DelayBackoffType.Exponential, // 300ms, 600ms, 1200ms
        UseJitter = true, // Thundering herd'i önler
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>()
            .Handle<TimeoutException>(),
        OnRetry = args =>
        {
            logger.LogWarning("Deneme {Attempt} başarısız, tekrar deneniyor: {Ex}",
                args.AttemptNumber + 1, args.Outcome.Exception?.Message);
            return default;
        }
    })
    .Build();

// Kullanım
var result = await pipeline.ExecuteAsync(async ct =>
    await httpClient.GetStringAsync("/api/data", ct), cancellationToken);

Circuit Breaker: Arızalı Servisi Koru

Circuit breaker, ardı ardına gelen hataların tamamını denemek yerine belirli bir süre için isteği engeller. Hem istemciyi hem de arızalı servisi korur:

var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
    {
        // Son 10 istekte %50'den fazla hata olursa
        FailureRatio = 0.5,
        MinimumThroughput = 10,      // En az 10 istek gerekli
        SamplingDuration = TimeSpan.FromSeconds(30),

        // 30 saniye devre kırık (Open state) — hiç deneme yapılmaz
        BreakDuration = TimeSpan.FromSeconds(30),

        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(r => (int)r.StatusCode >= 500),

        OnOpened = args =>
        {
            logger.LogError("Circuit AÇILDI — servis devre dışı: {Duration}sn",
                args.BreakDuration.TotalSeconds);
            return default;
        },
        OnClosed = args =>
        {
            logger.LogInformation("Circuit KAPANDI — servis tekrar erişilebilir");
            return default;
        },
        OnHalfOpened = args =>
        {
            logger.LogInformation("Circuit YARI AÇIK — test isteği gönderiliyor");
            return default;
        }
    })
    .Build();

// Circuit açıksa BrokenCircuitException fırlatır
try
{
    var response = await pipeline.ExecuteAsync(
        ct => httpClient.GetAsync("/api/health", ct), ct);
}
catch (BrokenCircuitException)
{
    // Fallback: cache'den sun veya varsayılan değer döndür
    return GetFromCache() ?? new ServiceUnavailableResult();
}

Timeout: Sonsuz Beklemeyi Önle

var pipeline = new ResiliencePipelineBuilder()
    .AddTimeout(new TimeoutStrategyOptions
    {
        Timeout = TimeSpan.FromSeconds(10),
        OnTimeout = args =>
        {
            logger.LogWarning("İstek zaman aşımına uğradı: {Timeout}sn",
                args.Timeout.TotalSeconds);
            return default;
        }
    })
    .Build();

// TimeoutRejectedException fırlatır
try
{
    await pipeline.ExecuteAsync(async ct =>
    {
        await SlowExternalServiceAsync(ct);
    }, cancellationToken);
}
catch (TimeoutRejectedException)
{
    return Results.StatusCode(504); // Gateway Timeout
}

Pipeline Birleştirme: Gerçek Dünya Senaryosu

Güçlü yapılandırma her zaman birden fazla stratejiyi bir arada kullanır:

// Dış ödeme servisi için kapsamlı pipeline
var paymentPipeline = new ResiliencePipelineBuilder<PaymentResult>()
    // 1. Timeout — en içte: tek deneme için
    .AddTimeout(TimeSpan.FromSeconds(5))

    // 2. Retry — timeout sonrası tekrar dene
    .AddRetry(new RetryStrategyOptions<PaymentResult>
    {
        MaxRetryAttempts = 2,
        Delay = TimeSpan.FromSeconds(1),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        ShouldHandle = new PredicateBuilder<PaymentResult>()
            .Handle<HttpRequestException>()
            .Handle<TimeoutRejectedException>()
            .HandleResult(r => r.IsTransientError)
    })

    // 3. Circuit Breaker — en dışta: tüm deneme setini izle
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<PaymentResult>
    {
        FailureRatio = 0.5,
        MinimumThroughput = 5,
        BreakDuration = TimeSpan.FromSeconds(60)
    })

    .Build();

// Kullanım
public async Task<PaymentResult> ChargeAsync(PaymentRequest request, CancellationToken ct)
{
    try
    {
        return await paymentPipeline.ExecuteAsync(async token =>
            await _paymentGateway.ChargeAsync(request, token), ct);
    }
    catch (BrokenCircuitException)
    {
        logger.LogCritical("Ödeme servisi erişilemiyor — circuit açık");
        throw new PaymentServiceUnavailableException();
    }
    catch (TimeoutRejectedException)
    {
        logger.LogError("Ödeme isteği zaman aşımına uğradı");
        throw new PaymentTimeoutException();
    }
}

IHttpClientFactory ile Entegrasyon (.NET 8)

// Microsoft.Extensions.Http.Resilience paketi ile
builder.Services.AddHttpClient<PaymentApiClient>(client =>
{
    client.BaseAddress = new Uri("https://payment.example.com/");
})
.AddResilienceHandler("payment", pipeline =>
{
    pipeline
        .AddTimeout(TimeSpan.FromSeconds(5))
        .AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true,
            // 5xx ve ağ hataları için otomatik retry
        })
        .AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            MinimumThroughput = 10,
            BreakDuration = TimeSpan.FromSeconds(30)
        });
});

Hedging: Paralel Deneme (En Hızlı Yanıtı Al)

// Gecikme toleranssız senaryolar için: ilk yavaşlarsa paralel dene
var pipeline = new ResiliencePipelineBuilder<string>()
    .AddHedging(new HedgingStrategyOptions<string>
    {
        MaxHedgedAttempts = 2,
        Delay = TimeSpan.FromMilliseconds(500), // 500ms gecikmede paralel deneme başlar
        ShouldHandle = new PredicateBuilder<string>()
            .Handle<HttpRequestException>()
    })
    .Build();

// Primary request 500ms içinde yanıt vermezse
// ikinci request paralel başlar, hangisi önce dönerse kullanılır

Polly ile resilience eklemek uygulamanızın dış bağımlılıklara karşı ne kadar dayanıklı olduğunu doğrudan etkiler. Retry kaskad arızayı önler, circuit breaker arızalı servisi korur, timeout sonsuz beklemeyi engeller. Bu üçü birlikte kurulduğunda çoğu geçici hata son kullanıcıya yansımadan çözülür.