Image
Mayıs 08 2016 02:28

Retry Pattern - Yeniden dene kalıbı

Uygulama:  Kötü bir şeyler oldu, şu anda her ne yapmak istiyorsanız, size yardımcı olamam lütfen daha sonra deneyiniz.

Müşteri / Kullanıcı:   Eee ama 15 dakikadır bu formu/siparişi/çok önemli belgeyi/üyeliğimi vs çok önemli yada bir daha uğraşamayacağım kadar önemsiz veya can sıkıcı bir işle uğraşıyordum. Ne kadar büyük bir yazılım olduğunun benim için anlamı yok ki bu saatten sonra! 

Satıcı, hizmet sağlayıcı, işin ucundaki : Bu anlık bir şeydi, yani bizim makinelerde çalışıyor : ) O butona 3-5 saniye sonra bassaydınız sorun olmayacaktı. Sistemde toplu güncelleme yapılıyor, IIS lerin biri çalışmıyor o yüzden diğerine çok yük biniyor bazı işlemlere cevap veremiyor...  

Tahmin edersiniz ki artık bu uygulama kullanıcıdan düşük not almıştır. İşin ucundaki kurumun / Servisin  söyleyeceği pek bir şey yoktur.

Belki biraz sert yada tam anlamı ile konuyu anlatan bir senaryo olmadı ise de konuşacağımız tasarımın dokunduğu kurgulardan biridir.

Şu durumda hizmet sağlayıcı / uygulamanın istenilen işlemi yapamamasının pek çok nedeni olabilir. İlk akla gelenleri saymak gerekirse
1-    Gerçekten bir Exception oluştu ve sistem bu durumda ne yapacağını bilemedi. Yüksek ihtimalle bir yerlere log atıp exception-ı olduğu gibi kullanıcıya geri döndürdü yada o exception-u sarıp sarmaladı ve başka bir exceptiona donüştürüp fırlattı.(Exception ucuz bir işlem değildir, düzeltilemeyecek bir hata bulmanın kimseye faydası yoktur. )

2-    İşlem, logic bir takım süreçlere takıldı ve aslında exception olmayan bir durum iken exception muamalesi görüp yine exception olarak belki log-landı ya da başka bir exceptiona dönüştürülerek istemciye geri gönderildi.(Merak etmeyin bu senaryo sadece bizde değil yabancılarda aynı şeyi yapıyorlar : ) 

3-    Üçüncü ve asıl ilgilendiğimiz mesele 1nci ve 2nci maddelere Kesinlikle uymayan, çağırdığımızda mutlaka çalışacağını düşündüğümüz yapıların, herhangi bir sebepten dolayı o an için bizim talebimize cevap verememesi durumu; nasıl mı?

Servisin üzerinde bulunduğu vm, datacenter vs o anda bir network sorunu yaşıyor olabilir.
Dabase aşırı yüklenmiştir o an connection açamıyordur ve timeout-a düşmüştür talebiniz.
IIS restart ediliyordur,
Service Bus instance-si o an meşkuldur. 
Queue/ storage/blob tableler, Azure Sql service-ler... vs benzeri onlarca sebepten dolayı talebiniz o an karşılanmıyor olabilir.
 
Bu kısa süreli, belki anlık gecikme sebebiyle kullanıcıya “İstenmeyen bir hata oluştu lütfen daha sonra tekrar deneyiniz” gibi sevimsiz bildirimler yapmak yerine, çaktırmadan aynı işlemi birkaç kez yapmayı denemek sorunun çözümü olabilir. Belki de bu servislere erişmeye çalışan yine bir worker role, web jobs, jobs,scheduler vs yapılarda olabilir. Bu durumda retry pattern kullanmak faydalı olacaktır.

Elbette burada işi inada bindirip sürekli başarısız denemeler yapmamak gerekir. Kullanıcımız (eğer bir frontend Applicationumuz varsa) için 10-15 saniye beklemek yerine basit bir uyarı almayı tercih edebilir. Bunu da göz önünde bulundurarak yapılan işleme göre 3 kere ya da 5 kere denemek gibi makul sayılarda denemeler yapılabilir.
Not : bu denemeler arasında ne kadar bekleneceği yapılan işe göre değişir. Örnek: Bir worker role yada job çalıştırıyorsam 1-5 dakika söz konusu olabilir ancak web app ise başka bir aralık vermek gerekiyor.

Özellikle bulutta çalışan sistemler/servisler ile çalışılacaksa, yapımızın bulut ortamında meydana gelebilecek bu türden geçici arızalara ve bağlantı kayıplarına karşı duyarlı olması gerekiyor. Bu yetenek uygulamanızın “transient fault”  direncinin olduğunu gösterir.
Aşağıda transient fault ile ilgili msdn kaynaklarını bulabilirsiniz

The Transient Fault Handling Application Block
Perseverance, Secret of All Triumphs: Using the Transient Fault Handling Application Block
Using the Transient Fault Handling Application Block with SQL Azure

 

Pattern örnek 

sql

Şekilde gördüğümüz gibi bir uygulama var, bir servis noktasına bağlanmaya çalışıyor,
İlk requesti başarısız olmuş ve response code 500 yani “Internal server error” hatası dönmüş.
İkinci requestin de reesponsu failed olmuş yine 500 dönmüş 
Üçüncü denemesinde requesti başarı ile karşılanmış ve response olarak 200 OK kodu dönülmüş.

Peki hangi durumlarda bu yapıyı kullanmak gerekiyor?
Genel kural:  Geçici bir hatanın olduğu veya olabileceği düşünüldüğü zaman bu patterni kullanmak faydalı olacaktır. Aksi halde sadece yavaşlık ve kaynakların boşuna kullanılması dışında bir getirisi olmayacaktır
Birkaç örnek vermek gerekirse;
     Veri tabanları ile ilgili bağlantı hataları zaman aşımı vb durumlarda
     http: Too Many Requests - try after a longer period(429), Request Timeout(408), Gateway Timeout(504), Service Unavailable(503), belki Internal server error(500)

Kod örneğimize geçmeden önce Son Olarak 2 tavsiyede bulunmak istiyorum
1- Retry kodlarını mutlaka test edin, sonsuz döngülerden kaçının,
2- Retry-ları mutlaka loglayın ve bu logları daha sonra yorumlayıp olası başka hataları giderin yada olabilecek hataları engelleyin.

Pattern-in uygulanışı:

 public class RetrySample
    {
        // hangi hatalar Transient Exceptions olarak kabul edilecek 
        private readonly List<Type> _exceptionTypes;

        // başarısız olursa Kaç defa denecek
        private readonly int _retryCount;

        // test için kaç defa denediğimizi saydıracağız
        private int _testCounter;

        public RetrySample(List<Type> exceptionTypes, int retryCount)
        {
            _exceptionTypes = exceptionTypes;
            _retryCount = retryCount;
            _testCounter = 0;
        }

        public async Task Run()
        {
            var currentRetry = 0;

            for (;;)
            {
                try
                {
                    await TransientOperationAsync();
                    break;
                }
                catch (Exception ex)
                {
                    currentRetry++;
                    if (currentRetry > _retryCount || !IsTransient(ex))
                    {
                        // Transient Exceptions dışında bir Exception alındıysa artık devam etmemeli
                        Console.WriteLine("Başka bir hata oldu");
                        throw;
                    }
                     // başarısız olan her denemeden sonra ne kadar bekleyip yeniden deneyecek
                     Task.Delay(100);
                }
            }
        }
        private async Task TransientOperationAsync()
        {
            // testimizde 3 kez deneyip 4 ncüde başarılı olduğunu kabul edeceğiz
            if (_testCounter < 3)
            {
                _testCounter += 1;
                Console.WriteLine("TimeOut hatası");
                throw new TimeoutException();
            }
            Console.WriteLine("File Upload yapıldı");
        }

        private bool IsTransient(Exception ex)
        {
            // Alınan hata Transient Exceptions listemizde ki bir Exception mı?
            return _exceptionTypes.Any(t => t == ex.GetType());
        }

    }

Dışardan hata tipleri ve hata olması durumunda kaç kez - deneme yapılacağı bilgisini Constructor da alıp, işlem yapan. Yaptığı işlemde bir hata olması durumunda bu hatanın tekrar deneme yapılacak hatalar içinde olup olmadığını kontrol eden bir metodumuz var.
Eğer Transient Exceptions dışında bir hata alınırsa "Başka bir hata oldu" oldu deyip hayatı yukarı atacak birde kontrol mekanizmamız oluşmuş oldu.
Aslında Microsoft.Practices te özellikle Azure için RetryPolicy ler ve ilgili yapıları eklemiş.
Örneğimize dönecek olursak, yapımız tamam artık bu RetrySample-i çalıştıracak bir metod yazmamız gerekiyor.

static void Main(string[] args)
        {
            // test için hata TimeoutException ı ekliyorum
            var retry = new RetrySample(new List<Type> { typeof(TimeoutException) }, 3);
            retry.Run();
        }

kodları çalıştırdığımızda aşağıdaki gibi bir ekranın çıkmış olması gerekiyor. 1-2-3ncü denemelerde TimeoutException hatası alınacak. Bu hata Transient Exceptions listemizde olduğu için bir miktar bekleyip denemeye devam edecek. Dördüncü denemesinde ise "File Upload yapıldı" mesajını yazdırarak uygulamayı sonlandıracak.

sql

bir sonraki yazıda görüşmek üzere : )

2016 : memet tayanç