Asp.Net Core Polly ile Resiliency Patterns ( Esneklik-Dayanıklılık) Retry Pattern İncelemesi

Adem Olguner
Devops Türkiye☁️ 🐧 🐳 ☸️
6 min readMar 7, 2024

Eğer bu satırları okuyorsanız, muhtemelen Microservices dünyasına adım atmışsınız demektir. Bu süreçte bir dizi zorlukla karşılaşmış olabilir ve bu sorunlara çözüm arayışına girmiş olmalısınız. Microservices kavramı, ilk duyulduğunda oldukça heyecan verici gelebilir ancak bu yolculuğa adım attıkça, zaman zaman karmaşık ve sınırsız gibi görünen sorunlarla karşılaşmanın kaçınılmaz olduğunu fark etmiş olabilirsiniz.

Hizmetler arası iletişim,ağ hataları, gecikmeler, bağlantı sorunları, hata yönetimi, veritabanları yönetimi, veri bütünlüğü, tutarlılık ve güvenilirlik, veri paylaşımı, veri güncelleme, veri senkronizasyonu, yetkilendirme, kimlik doğrulama, loglama ve izleme mekanizması, versiyon yönetimi vb… bu liste daha çok uzar gider. Teknik terimlere boğmadan kuşbakışı bu ve daha fazlası ile uğraşmak.

Resiliency Patterns (Esneklik-Dayanıklılık)

Resiliency işlemlerinin asıl amacı, uygulamalarımızın dayanıklılığını arttırmayı hedefler. Dayanıklılıktan kasıt sistem veya sistemlerde meydana gelebilecek hatalar ile başa çıkmak, hizmet kesintilerine karşı uygulamalarımızın down olmamasını, çalışabilirliğini sağlamak, devamlılığını sürdürmek.

Gelin bu tanımlamayı bir hikaye üzerinden giderek daha akılda kalıcı olmasını sağlayalım

Bir zamanlar Güzelyalı adında huzurlu bir kasaba vardı. Kasaba sakinleri, günlük işlerini gerçekleştiren ve birbirleriyle etkileşimde bulunan bir topluluktu. Ancak, Güzelyalı’da sıklıkla karşılaşılan bir sorun vardı: doğanın tuhaf olaylarına bağlı olarak ortaya çıkan beklenmeyen kesintiler.

Kasaba lideri, bu durumu çözmek ve Güzelyalı sakinlerine sürekli bir hizmet sunmak için bir çözüm bulmak istedi. Bu noktada, bir bilge kasaba sakini olan Uncle Adem, Resiliency Patterns adında bir büyü kitabını hatırladı.

Uncle Adem, kasaba liderine bu sihirli desenlerin kasabanın dayanıklılığını artırabileceğini ve beklenmeyen durumlarla başa çıkma yeteneğini güçlendirebileceğini anlattı. Kasaba lideri, Uncle Adem’nın önerilerini dikkate alarak Güzelyalı’yı daha dayanıklı hale getirmeye karar verdi.

İlk olarak, Retry Pattern kullanılarak, kasabada ortaya çıkan küçük kesintilerin otomatik olarak düzeltilebileceği bir sistem kuruldu. Bir hizmette hata oluştuğunda, sistem otomatik olarak belirli aralıklarla tekrar deneme stratejisini uygulayarak normal duruma dönme çabasına girdi.

Ardından, Circuit Breaker Pattern ile kasabadaki bazı hizmetler izole edildi. Bir hizmette ciddi bir hata oluştuğunda, bu hizmet geçici olarak devre dışı bırakılarak diğer hizmetlerin etkilenmesi önlendi.

Uncle Adem, kasabanın her tarafına Bulkhead Pattern uygulayarak, gelen talepleri kontrollü bir şekilde yönetmeyi önerdi. Bu, beklenmedik bir yük artışı durumunda sistemde aşırı yüklenmeyi önlemek amacını taşıyordu.

Böylece, Resiliency Patterns sayesinde Güzelyalı, doğanın ve beklenmedik olayların yol açtığı sorunlara karşı daha dayanıklı hale geldi. Kasaba sakinleri, günlük yaşamlarını daha az kesintiyle sürdürmeyi başardılar ve Güzelyalı, bu sihirli desenlerle daha güçlü ve dirençli bir kasaba haline geldi.

Resiliency Patternler türleri sadece bunlar değil tabiki. Hikayeleştirme yaparak daha hatırlanabilir bir anlatım ortaya çıkarmaya çalıştım. Bu yazı serisi ile Resiliency Patternleri tek tek inceleyeceğiz.

Resiliency Pattern Policy Tipleri;

Retry
Circuit Breaker
Fallback
Timeout
Cache
Bulkhead

Retry Pattern

Retry Pattern, hata durumlarında bir operasyonu tekrar deneyerek başarılı bir sonuç elde etme stratejisidir. Bu desen, iç ve dış servis çağrıları veya ağ bağlantıları gibi durumlarda kullanışlıdır. Tekrar deneme, kaç kere ve hangi sıklıkta gibi soruların cevabını kodlarımız yazarken inceleyeceğiz.

https://learn.microsoft.com/en-us/dotnet/architecture/cloud-native/application-resiliency-patterns

Neden Retry Pattern Kullanmalıyım?

Retry pattern bize ne sağlıyor, uygulamamız yaşam döngüsü içerisinde farklı bir api ye (internal veya external) istekte bulunduğumuzda, bu isteği karşılayan tarafta farklı durumlardan dolayı hata (exception) ile karşılaşıldı ve bu durumda api isteğimiz burada kesilecektir, bu pek karşılaşmak istediğimiz bir durum değildir. Peki burada hata durumlarının-çeşitlerinin bir önemi var mı? sorusu akla geliyor.

Hata durumlarının-çeşitlerinin bir önemi var mı?

Belkide en önemli soru diyebiliriz. Tekrar deneme tasarımı ile ne tür hata durumlarında denemeliyiz? BadRequest(400), Unauthorized(401) yada Forbidden(403) aldığımız, bir hata durumunda tekrar denemek çözüm olmayacaktır aksine network trafiği oluşturacağız. Bu durumda doğru hata (exception) tipleri için Retry Pattern uygulamalıyız. Genel olarak network kaynaklı hatalarda erişememe, zaman aşımı (timeout) gibi hata (exception) türleri için kullanmak daha sağlıklı olacaktır.

private static readonly HttpStatusCode[] HttpStatusCodesWorthRetrying =
[
HttpStatusCode.RequestTimeout, // 408
HttpStatusCode.InternalServerError, // 500
HttpStatusCode.BadGateway, // 502
HttpStatusCode.ServiceUnavailable, // 503
HttpStatusCode.GatewayTimeout // 504
];

Not: Bu durumların dışında Custom Exception’lar içinde yapılabilir. Kodumuzu yazarken bu noktalara değineceğiz.

Retry pattern ile tekrar deneme işlemi yaparken bazı hususlarda (Idempotency) kontrol etmek gerekecektir. Idempotency nedir ne değildir bu konuya girmeyeceğim bu konuda takım arkadaşım Serdar Usta (Hepsiburada/Bilgi Teknolojileri)’nın detaylıca kaleme aldığı Idempotency yazısını okuyabilirsiniz.

Kısaca değinmek gerekirse; ödeme işlemi için farklı bir takımın kontrolünde olan bir servise ait istek yaptığımızı düşünelim. Yaptığımızı istekte kullanıcı bakiyesinden para kesildiğini düşünelim. Kullanıcı hesabından para kesiliyor ancak işlem devam ederken timeout aldık ve Retry Pattern ile tekrar denedik ve yine kullanıcı hesabından para kesildi. Kullanıcı mağduriyeti yaratıldı. Burada hizmet aldığınız takımdan bu işlem ile alakalı idempotent yapının olup olmadığını bilmek ve dikkat etmek gerekiyor.

Let’s code

Polly.NET

Açık kaynak olarak geliştirilen ve içinde hazır Resiliency Pattern’ler (Retry, Circuit Breaker, BulkHead Isolation, Timeout ve Fallback) barındıran bir .NET kütüphanesidir. NuGet üzerinden kurulum sağlayabiliriz.

<PackageReference Include="Polly" Version="8.3.0" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.2" />

Web Api Projesi Oluşturalım

Uygulamamız bir oyun uygulaması için gerekli olan entegrasyonların olduğu bir Api uygulaması olacaktır. Bu uygulama kendi içerisindeki iş kuralları ile beraber internal-external servislerden de beslenen bir uygulama olacaktır. Oyunun kendi işlem akışları devam ederken external bir servis olan Wallet servisine istek atacağız ve kullanıcı güncel bakiyesini öğreneceğiz, bu bakiye ile oyun oynamaya devam edecektir.

Uygulama API’nin adı: SpinningWheels-Api olsun
Bakiye bilgisi için kullanacağımız API’nin adı Wallet-Api olsun.

Oyun arayüzümüz

Uygulama Katmanı ve Proje

Retry Pattern kullanmadan önce client isteğinin request ve response durumu bu şekilde olacaktır.

Retry Pattern — Uygulamadan önce

Uygulamalar arası client entegrasyonunu ekleyelim. AddHttpClient() eklendikten sonra şu an için sadece wallet-client olduğu için sadece bu clientı ekliyoruz. Tüm clint eklenme olayını ayrı ayrı metotlar ile yapmak kod karmaşası açısından ve her bir client için farklı farklı ihtiyaç duyulabilen entegrasyonlar eklenebilir düşüncesi ile ayro metot olarak tasarlanmıştır.

private static IServiceCollection AddClientDependencies(this IServiceCollection services, 
IConfiguration configuration)
{
var httpClients = services.AddHttpClient();
httpClients.RegisterWalletClient(configuration);
return services;
}

private static void RegisterWalletClient(this IServiceCollection services,
IConfiguration configuration)
{
services
.AddWalletClient(configuration)
.AddTransientHttpErrorPolicy(_=>ResiliencyStrategies.GetRetryPolicy());
}

.AddWalletClient(configuration) ile wallet için yaptığımız client entegrasyonunu ekliyoruz ve ardından Retry Pattern entegrasyonunu ekliyoruz.

.AddTransientHttpErrorPolicy(_=>ResiliencyStrategies.GetRetryPolicy());

public static class ResiliencyStrategies
{
private static readonly HttpStatusCode[] HttpStatusCodesWorthRetrying =
[
HttpStatusCode.RequestTimeout, // 408
HttpStatusCode.InternalServerError, // 500
HttpStatusCode.BadGateway, // 502
HttpStatusCode.ServiceUnavailable, // 503
HttpStatusCode.GatewayTimeout // 504
];

public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(r => HttpStatusCodesWorthRetrying.Contains(r.StatusCode))
.WaitAndRetryAsync(
retryCount : 6,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)), //TimeSpan.FromSeconds(10)
onRetryAsync: (exception, timespan, retryAttempt, context) =>
{
Console.WriteLine($"Retry #{retryAttempt} due to {exception.Exception.Message}. " +
$"Waiting for {timespan.TotalSeconds} seconds.");
return Task.CompletedTask;
});
}
}

HandleTransientHttpError : Geçici olarak gerçekleşen (transient) HTTP hatalarını handle eder. InternalServiceError (5xx) veya RequestTimeout [408] durumlarında ele alınır. Bu hata türklerine ek olarak farklı hata tiplerini de ekleyebiliriz.

OrResult : Bu metot ile ekstra koşullar eklenilir. Ben örnek olarak sadece statusCode ekledim ancak farklı koşullar eklenebilir.

WaitAndRetryAsync : Deneme sayısı ve bekleme süresi alınarak, istek otomatik olarak tekrar gerçekleştirilir. Burada tekrar süresi (delay) sabit bir değer olabileceği gibi farklı delay tanımlamaları yapılabilir. biz burada örnek olarak 6 tekrar ve 3'ün katları (3–9–27–81…) olacak şekilde ayarladık.

Retry Pattern — Uygulandıktan sonra

Not: Retry Pattern genel olarak kısa süreli oluşabilecek ağ problemi ve network kaynaklı timeout işlemleri için kullanılabilir. Client isteği için genelde bir timeout süresi olacaktır bu api katmanında 10 sn desek gateway katmanında max 60 sn olarak tanımlansa da burada client isteği retry sürecinde max 60 sn içerisinde cevap verebilir olmalı aksi durumda TimeoutException hatası alınır ve uygulamamız exception verir, uygulama dayanıklılık ve esnekliği tam olarak sağlanmamış olur. Burada TimeoutException hatasının oluşma ihtimalini göz önünde bulundurup handle edebilmemiz gerekiyor. Buna çözüm olarak daha sonraki yazımızda değineceğimiz TimeoutException Pattern ile çözüm geliştirmiş olacağız.

TimeoutException handle edemediğimizde uygulama hata alıcaktır

SpinningWheels-Api çalıştırıp 2. ci retry denemesini yaparken Wallet-Api’yi ayağa kaldırdığımızda erişim sağlamaya başlayacak ve request işlemi kaldığı yerden kesintisiz sağlamış olucaktır.

Polly kütüphanesi ile Resiliency Pattern’leri incelemeye çalıştık. İlk olarak Retry Pattern ile düzenlemeye ve konfigüre etmeye çalıştık. Bir sonraki yazımızda Circuit Pattern ile devam edeceğiz. Kısa zaman sonra görüşmek üzere.

Kod örneklerine buradan ulaşabilirsiniz.

Saygı ve sevgilerimle
Adem OLGUNER.

Kaynaklar:
. https://learn.microsoft.com/en-us/dotnet/architecture/cloud-native/application-resiliency-patterns
. https://github.com/App-vNext/Polly

--

--