dimanche 24 septembre 2017

C#: Retry Actions and Func

I was in a need of a Retry logic for void Actions and boolean Func, and I wanted to avoid to carry on a full framework or nuget package for it.

For Func<bool>, Retry when return is false or exception raised during execution:

 /// <summary>  
 /// Retry execution of given Function if return is false or exception raised.  
 /// All exceptions will be always swallowed, it never will reach the caller.  
 /// </summary>  
 /// <param name="action"></param>  
 /// <param name="retryInterval"></param>  
 /// <param name="maxRetry"></param>  
 /// <returns></returns>  
 public static bool Retry(Func<bool> action, int retryInterval, int maxRetry)  
 {  
     bool result = false;  
     for (int retry = 0; retry < maxRetry; retry++)  
     {  
         Exception ex = null;  
         try  
         {  
             if (retry > 0)  
             {  
                 Thread.Sleep(1000 * retryInterval);  
             }  
             result = action();       
             if (result)  
                 break; 
         }  
         catch (Exception e)  
         {  
             ex = e;  
         }  
 
         if (retry + 1 == maxRetry)  
         {  
           Logger.Warn($"Max attempt reached retrying: {action.GetMethodInfo().DeclaringType}!");  
           break;  
         }  
         Logger.Warn($"Attempt {retry + 1}/{maxRetry} failed. Next try in {retryInterval} seconds...");  
       }    
     }  
     return result;  
 }  


Retry for Action (void methods), in case you want to execute custom code if failure after maxRetry, use "onFailure" action. The latest exception is pass as param:

 /// <summary>  
 /// Retry the specified action if exception raised.  
 /// </summary>  
 /// <returns>The retry.</returns>  
 /// <param name="action">Target action.</param>  
 /// <param name="retryInterval">Retry interval.</param>  
 /// <param name="maxRetry">Max retry attempts.</param>  
 /// <param name="onFailure">Action to execute when maxRetry reached.</param>  
 public static void Retry(Action action, int retryInterval, int maxRetry, Action<Exception> onFailure)  
 {  
     for (int retry = 0; retry < maxRetry; retry++)  
     {  
         try  
         {  
             if (retry > 0)  
             {  
                 Thread.Sleep(1000 * retryInterval);  
             }  
             action();  
             return;  
         }  
         catch (Exception e)  
         {  
             if (retry + 1 == maxRetry)  
             {  
                 if (onFailure != null)  
                 {  
                     onFailure.Invoke(e);  
                 }  
                 else  
                 {  
                     Logger.Warn($"Max attempt reached retrying: {action.GetMethodInfo().DeclaringType}!");  
                     throw; // throw latest exception  
                 }  
             }  
             else  
             {  
                 Logger.Warn($"Attempt {retry + 1}/{maxRetry} failed. Next try in {retryInterval} seconds...");  
             }  
         }  
     }  
 }  

You can accommodate code above according your own needs.
If you want to improve exception handling use AggregateExceptions including all exceptions raised during execution.
Also, if you want some custom code on each failure attempt, add another Action and execute it instead of logging "Attempt retry/maxRetry failed" message.

Tests I used to validate the behavior:

   
using NUnit.Framework;
using System;
using Moq;

[TestFixture()]  
   public class Test  
   {  
         public interface IRetryTester  
         {  
             bool TargetFunction();  
             void TargetMethod(string text);  
         }  
         [Test]  
         public void Should_Retry_Only_Failed_Times()  
         {  
             var moq = new Mock<IRetryTester>();  
             int calls = 0;  
             moq.Setup(m => m.TargetMethod("Testing"))  
                 .Callback(() =>  
                 {  
                     if (calls++ < 3)  
                     {  
                         // force failure on 3calls  
                         throw new Exception();  
                     }  
                 }); ;  
             RetryHelper.Retry(() => { moq.Object.TargetMethod("Testing"); }, 10, 5, null);  
             moq.Verify(m => m.TargetMethod("Testing"), Times.Exactly(4)); // 3failed + 1succesfull  
         }  
         [Test]  
         public void Should_Retry_All_Times_And_Execute_Custom_Handler()  
         {  
             var moq = new Mock<IRetryTester>();  
             moq.Setup(m => m.TargetMethod("Testing")).Throws<Exception>();  
             Exception ex = null;  
             RetryHelper.Retry(() => { moq.Object.TargetMethod("Testing"); }, 10, 5,  
                 (e) =>  
                 {  
                     ex = e;  
                 });  
             Assert.IsNotNull(ex);  
             moq.Verify(m => m.TargetMethod("Testing"), Times.Exactly(5));  
         }  
         [Test]  
         public void Should_Retry_All_Times_And_Throw_Last_Exception()  
         {  
             var moq = new Mock<IRetryTester>();  
             moq.Setup(m => m.TargetMethod("Testing")).Throws<Exception>();  
             Assert.Throws<Exception>(() => RetryHelper.Retry(() => { moq.Object.TargetMethod("Testing"); }, 10, 5, null));  
             moq.Verify(m => m.TargetMethod("Testing"), Times.Exactly(5));  
         }  
         [Test]  
         public void Should_Retry_Only_On_False_Return()  
         {  
             var moq = new Mock<IRetryTester>();  
             moq.SetupSequence(m => m.TargetFunction())  
                 .Returns(false)  
                 .Returns(false)  
                 .Returns(true);  
             var result = RetryHelper.Retry(moq.Object.TargetFunction, 10, 5);  
             Assert.IsTrue(result);  
             moq.Verify(m => m.TargetFunction(), Times.Exactly(3)); // 2false + 1true  
         }  
         [Test]  
         public void Should_Retry_Only_Defined_Times()  
         {  
             var moq = new Mock<IRetryTester>();  
             moq.SetupSequence(m => m.TargetFunction())  
                 .Returns(false)  
                 .Returns(false)  
         .Returns(false)  
         .Returns(false)  
                 .Returns(true);  
             var result = RetryHelper.Retry(moq.Object.TargetFunction, 10, 3);  
             Assert.IsFalse(result);  
             moq.Verify(m => m.TargetFunction(), Times.Exactly(3)); // first 3 are false  
         }  
   }  

If you want a full framework to deal with several Retry patterns such as Circuit-braker, timeout, etc then check Poly

Aucun commentaire:

Enregistrer un commentaire