.NET Core 5.0 – Retry Pattern, Timeout ve Circuit Breaker | Polly
Merhaba,
Client ve server arasındaki haberleşmede request/response esasına dayanan bir proje düşleyelim… Client’ın, ihtiyacı doğrultusunda server’a request tabanlı isteklerde bulunabildiğini ve elde edilen response neticesinde gelen verileri işleyebildiğini… Evet, biliyorum. Bu durum günümüzün en temel esası. Haliyle düşlenecek ne var burada hoca! dediğinizi de duyar gibiyim… Tabi ki de, sizleri bilinen bir kültür üzerinden beyhude düşlere sürüklemek gibi bir niyetim yok. Hatta biliyorum ki, bir çoğunuz bu yaklaşımı gerektiği taktirde gözünüz kapalı kullanabilmekte ve client ile server iletişimini sağlayabilmektedir. Ama şunu da biliyorum ki, bu yaklaşımı kullanan azımsanmayacak bir kitlenin client ile server arasındaki haberleşme esnasında server’da yaşanabilecek olası hatalara karşı önlem almadığını da…
Evet, bu içeriğimizde client ile server arasındaki request/response temelli haberleşme süreçlerinde nelere dikkat edilmesi gerektiğini ele alacak ve yapılan request neticesinde server’da meydana gelebilecek olası bir hatadan yahut kesintiden kaynaklı yaşanan başarısızlık durumlarında client’ın ne gibi davranışlar göstermesi gerektiği üzerine farkındalık yaratıyor olacağız.
Şimdi, ilk olarak yukarıda sizlere yansıtmaya çalıştığım farkındalığı net masaya yatırarak başlayalım. Bunun için aşağıdaki materyal konuya dair gayet yeterli malzeme verecektir;
HttpClient httpClient = new HttpClient(); var message = await httpClient.GetAsync("https://localhost:5001/api/products"); var content = message.Content.ReadAsStringAsync(); Console.WriteLine(content.Result);
Görüldüğü üzere, bu kod herhangi bir uygulamanın herhangi bir noktasında çalıştırıldığı vakit, belirtilen endpoint’e bir GET isteğinde bulunacak ve sonucu elde edecektir. Bu gayet aşikar! Lakin bu kodu kullanan client’ın yaptığı request neticesinde server’da bir hata meydana gelirse ne olacak? Hiç bunu düşündünüz mü?
Hatta ilgili API’ın detayını da aşağıya alalım ve gelen istekte yaşanacak sıkıntıyı garantiye alalım…
[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { [HttpGet] public string Get() { throw new Exception("Hata!"); } }
Dikkat ederseniz API içeride bir Exception fırlatmaktadır. Bu durum, connection kopması yahut sistem çökmesi gibi run time’da alınan farklı bir hatadan da kaynaklanıyor olabilirdi. En nihayetinde request gönderilen server’ın süreçte yaşadığı bir aksaklığa karşı client’ın alacağı tutuma odaklanırsak eğer client’ın genel geçer davranışı aşağıdaki gibi olacaktır.
İşte… Client’ın yaptığı request neticesinde eğer server’da bir hata meydana gelirse client bundan haberdar edilmekte lakin kodun akışı devam edeceğinden dolayı bu isteğin bir daha esamesi okunmamaktadır. Lakin bu istek bizim için önemliyse ve server’da ki yaşanan istisnai durum gelip geçici bir hataysa! Böyle bir ahvalde isteği es geçmektense, bir müddet sonra tekrar denemek daha akıllıca ve amaca dönük olmayacak mı? Hatta server’da yaşanan sorunun ne kadar süre sonra neticeleneceği kesin bilinemeyeceğinden dolayı belirli bir süre sonra tekrar edecek olan request’i, izafi olarak ayarlanmış bir kaç periyot eşliğinde tekrarlı yapmak ve tüm bu isteklere rağmen hala hata çözülemediyse o zaman isteği es geçmek en doğrusu olmayacak mı?
Evet… Sanırım aynı fikirdeyiz 🙂 Tabi ki de doğrusu bu olacaktır.
Peki böyle bir durumda ne yapmalıyız? Yani client’ın yaptığı request neticesinde server’da bir hata varsa bu duruma karşı davranışımızı belirleyebilmek için nasıl aksiyon almalıyız?
Yapılan bir işlemin herhangi bir sebepten dolayı askıya uğraması durumunda, o işlemi tekrar edebilmek için Retry Pattern’i kullanabiliriz. Haliyle client’ın gönderdiği request’in server’da ki herhangi bir sebepten dolayı kaynaklanan hatayla karşılaşması neticesinde Retry Pattern uygulanarak belirli periyotlarda bu istek tekrar edilebilir ve meydana gelen hatanın getirdiği zaafiyet mümkün mertebe ortadan kaldırılmaya çalışılabilir.
Retry Pattern Nedir?
Retry pattern, yandaki görüntüde görüldüğü üzere client’ın yapmış olduğu isteğin başarılı olmasına odaklı tekrar edilmesini sağlayan bir tasarım desenidir. Yapılan request neticesinde hata alındıysa eğer bu hatanın kısa süreliğine olma ihtimaline istinaden kullanıcıya fark ettirmeksizin request’i arkaplanda tekrar yenileme üzerine kurulu bir mantığa sahiptir.
Yukarıdaki satırlarda da ifade edilmeye çalışıldığı gibi server’da türlü sebeplerden dolayı meydana gelen aksamalar neticesinde ilgili isteği tarafımızca bildirilen periyotlar kadar tekrar edebilmekte ve böylece isteğin mahiyetindeki önem arz eden operasyona gösterilen hassasiyeti arttırmayı sağlamaktadır.
Retry Pattern, bir tasarım deseni olduğu için OOP nimetlerinden faydalanarak inşa edilmesi gereken stratejik bir matematiğe sahiptir. İstendiği taktirde bu matematiğe uygun çalışma gerçekleştirilerek uygulanabilir. Lakin bu ve bunun gibi senaryolara uygun tasarlanmış ve içerisinde hali hazırda efektif işlevsel fonksiyonellikler barındıran Polly Kütüphanesini kullanmayı tercih edebilirsiniz.
Polly Kütüphanesi Nedir?
Servisler arası haberleşme süreçlerinde bir çok pattern’ı uygulamamızı sağlayan operasyonellikler barındıran, Retry, Circuit Breaker, Timeout, Bulkhead Isolation ve Fallback gibi senaryolarda kullanılabilir niteliğe sahip open source bir kütüphanedir.
Bizler bu içeriğimizde, Retry Pattern’ı Polly kütüphanesi ile uygulamayı ele alıyor olacağız.
Polly Kütüphanesi İle Retry Pattern Operasyonları
Polly kütüphanesini kullanabilmek için her şeyden önce ilgili kütüphanenin aşağıdaki komut eşliğinde projeye yüklenmesi gerekmektedir.
Install-Package Polly
Yükleme işlemi yapıldıktan sonra artık Polly kütüphanesinin server’da alınan olası hatalara karşı, hata kontrol politikalarını oluşturmamız gerekecektir. Bunlar Fault-Handling Reactive Policies(Hata İşleme Politikaları) olarak nitelendirilmektedir.
- Yakalanması İstenilen Hataların Tanımlanması
- Tek bir hata tipine göre çalışılacağını belirtebilmekteyiz.
Policy .Handle<HttpRequestException>()
- Koşullu hata tipine göre çalışılacağını belirtebilmekteyiz.
Policy .Handle<SqlException>(ex => ex.Number == 1205)
- Birden fazla hata tipine göre çalışılacağını belirtebilmekteyiz.
Policy .Handle<HttpRequestException>() .Or<OperationCanceledException>()
- Tek bir hata tipine göre çalışılacağını belirtebilmekteyiz.
- İşlenmesi İstenilen Return Type’larının Tanımlanması
- Dönüş Değerini Koşulla İşleme
Policy .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound)
- Birden Çok Dönüş Değeri İşleme
Policy .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.InternalServerError) .OrResult(r => r.StatusCode == HttpStatusCode.BadGateway)
- Dönüş Değerlerini Equals Şeklinde Direkt Şart Olarak Verilebildiği Durum
Policy .HandleResult<HttpStatusCode>(HttpStatusCode.InternalServerError) .OrResult(HttpStatusCode.BadGateway)
- Hem Hataların Hem de Dönüş Değerlerinin Tek Bir Politikada Ele Alınması
HttpStatusCode[] httpStatusCodesWorthRetrying = { HttpStatusCode.RequestTimeout, // 408 HttpStatusCode.InternalServerError, // 500 HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout // 504 }; HttpResponseMessage result = await Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode)) .RetryAsync(...) .ExecuteAsync( /* some Func<Task<HttpResponseMessage>> */ )
- Dönüş Değerini Koşulla İşleme
- Tek Bir Kez Yeniden İstek Gönderme
Policy .Handle<SomeExceptionType>() .Retry()
- Bir Çok Kez Yeniden İstek Gönderme
Policy .Handle<SomeExceptionType>() .Retry(3)
- Bir Çok Kez Yeniden İstek Gönderme Durumunda Fonksiyon Tetikleme
Policy .Handle<SomeExceptionType>() .Retry(3, onRetry: (exception, retryCount) => { //Her yeniden denemeden önce gerekli loglama vs. operasyonları burada gerçekleştirilir. });
- Sonsuza Kadar İstek Gönderme
Policy .Handle<SomeExceptionType>() .RetryForever()
- Sonsuza Kadar İstek Gönderirken, Alınan Hatayı Yakalama
Policy .Handle<SomeExceptionType>() .RetryForever(onRetry: exception => { //Her yeniden denemeden önce gerekli loglama vs. operasyonları burada gerçekleştirilir. });
Örnek Çalışma
Polly kütüphanesi ile örnek bir Retry operasyonu için aşağıdaki kod bloğunu inceleyebilirsiniz;
await Policy.Handle<Exception>().RetryAsync(5, (e, r) => { Console.WriteLine("Tekrar deneniyor..."); }).ExecuteAsync(async () => { HttpClient httpClient = new HttpClient(); var message = await httpClient.GetAsync("https://localhost:5001/api/products"); var content = message.Content.ReadAsStringAsync(); Console.WriteLine(content.Result); });
Timeout Operasyonları
Polly’de Timeout; Optimistic ve Pessimistic olmak üzere ikiye ayrılmaktadır.
- Optimistic Timeout
Optimistic Timeout, CancellationToken aracılığıyla çalışmaktadır.var policy1 = Policy.Handle<Exception>().RetryAsync(5, (e, i) => Console.WriteLine("Tekrar deneniyor...")); var policy2 = Policy.TimeoutAsync(125); var combinePolicy = policy1.WrapAsync(policy2); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); await combinePolicy.ExecuteAsync(async (cancellationToken) => { HttpClient httpClient = new HttpClient(); var message = await httpClient.GetAsync("https://localhost:5001/api/products"); var content = message.Content.ReadAsStringAsync(); Console.WriteLine(content.Result); }, cancellationTokenSource.Token);
Optimistic Timeout, kesinlikle bir CancellationToken bekleyecektir.
- Pessimistic Timeout
Pessimistic Timeout, CancellationToken olmasa dahi bildirilen zamanın dolması neticesinde net hata fırlatmaktadır.var policy1 = Policy.Handle<Exception>().RetryAsync(5, (e, i) => Console.WriteLine("Tekrar deneniyor...")); var policy2 = Policy.TimeoutAsync(125, Polly.Timeout.TimeoutStrategy.Pessimistic); var combinePolicy = policy1.WrapAsync(policy2); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); await combinePolicy.ExecuteAsync(async (cancellationToken) => { HttpClient httpClient = new HttpClient(); var message = await httpClient.GetAsync("https://localhost:5001/api/products"); var content = message.Content.ReadAsStringAsync(); Console.WriteLine(content.Result); });
Circuit Breaker
Circuit Breaker, servisler arasındaki çağrıları düzenleyen bir pattern’dır. Aşağıdaki diyagramda olduğu gibi ‘Service A’, ‘Service B’ye bir istek gönderdiğinde ‘Service B’de istek gecikir yahut bir hata meydana gelirse ‘Service A’ tarafında da bu istek yukarıda ele aldığımız gibi Retry Pattern ile tekrar gerçekleştirilir. Ancak yeniden denenen isteklerin sayısı aşırıya kaçarsa eğer kullanıcı deneyimi açısından olumlu olan bu eylem grafiği olumsuza seyredebilir ve fazladan yük ve gecikme neticesinden maliyetli olabilir.
İşte böyle bir durumda, yapılan istek neticesinde cevap beklenenden uzun sürdüğünde yahut hata meydana geldiğinde yapılan yeniden isteklerin yanında Circuit Breaker ile belirlenen deneme sınırı aşıldığı taktirde bu istekler son bulacaktır. Böylece süreçteki olumsuz durum son kullanıcıya uygun bir dille izah edilerek, lüzumsuz beklemenin önüne geçilmiş olunacaktır.
Belirlenen sayıda istekten sonra Circuit Breaker ile devreyi kesebilmek için aşağıdaki politikanın kullanılması yeterlidir;
Policy await Policy.Handle<Exception>().CircuitBreakerAsync(2, TimeSpan.FromMinutes(10), (e, t) => { Console.WriteLine("Tekrar deneniyor..."); }, () => { Console.WriteLine("Reset"); }).ExecuteAsync(async () => { HttpClient httpClient = new HttpClient(); var message = await httpClient.GetAsync("https://localhost:5001/api/products"); var content = message.Content.ReadAsStringAsync(); Console.WriteLine(content.Result); });
Asp.NET Core Retry Pattern & Timeout
Asp.NET Core uygulamalarında Polly kütüphanesini kullanabilmek için Microsoft.Extensions.Http.Polly paketinin uygulamaya yüklenmesi gerekmektedir.
Ardından ‘Startup.cs’ dosyasındaki aşağıdaki servis konfigürasyonlarının yapılması gerekmektedir.
. . . public void ConfigureServices(IServiceCollection services) { . . . services.AddControllers(); services.AddHttpClient("myClient") .AddPolicyHandler(Policy.TimeoutAsync(20, async (context, timeSpan, task) => { Console.WriteLine("Zaman aşımı!"); }).AsAsyncPolicy<HttpResponseMessage>()) .AddTransientHttpErrorPolicy(policy => policy.RetryAsync(3, (d, r) => { Console.WriteLine("Tekrar deneniyor."); })); . . . } . . .
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…