3
\$\begingroup\$

Support for retry for action methods. One class RetryHelper that supports retry for four scenarios of action/function signature:

  1. Actions/functions with zero argument, and void return type.
  2. Actions/functions with zero argument, and generic type output
  3. Actions/functions with one argument, and generic type output.
public static class RetryHelper
{
 public static void InvokeActionWithRetry(IActionRetryOptions options)
 {
 uint retries = 0;
 do
 {
 try
 {
 options.Action();
 break;
 }
 catch
 {
 //log
 }
 retries++;
 Thread.Sleep(options.BackOffInterval);
 } while (retries < options.RetryAttempts);
 }
 public static void InvokeActionWithRetry<TResult>(IActionRetryOptions<TResult> options)
 {
 uint retries = 0;
 do
 {
 try
 {
 options.Result = options.InvokeAction();
 if (options.Success)
 break;
 }
 catch
 {
 //log
 }
 retries++;
 Thread.Sleep(options.BackOffInterval);
 } while (retries < options.RetryAttempts);
 }
 public static void InvokeActionWithRetry<TIn, TResult> (IActionRetryOptions<TIn, TResult> options)
 {
 uint retries = 0;
 do
 {
 try
 {
 options.Result = options.InvokeAction();
 if (options.Success)
 break;
 }
 catch
 {
 //log
 }
 retries++;
 Thread.Sleep(options.BackOffInterval);
 } while (retries < options.RetryAttempts);
 }
}
public interface IActionRetryOptions
{
 Action Action { get; set; }
 uint RetryAttempts { get; set; }
 TimeSpan BackOffInterval { get; set; }
}
public interface IActionRetryOptions<TResult>
{
 TResult Result { get; set; }
 Func<TResult> Action { get; set; }
 uint RetryAttempts { get; set; }
 bool Success { get; }
 Func<TResult, bool> RetryAction { get; set; }
 TimeSpan BackOffInterval { get; set; }
 TResult InvokeAction();
}
public interface IActionRetryOptions<TIn, TResult>
{
 TIn Args { get; set; }
 TResult Result { get; set; }
 Func<TIn, TResult> Action { get; set; }
 uint RetryAttempts { get; set; }
 bool Success { get; }
 Func<TResult, bool> RetryAction { get; set; }
 TimeSpan BackOffInterval { get; set; }
 TResult InvokeAction();
}
public class ActionRetryOptions: IActionRetryOptions
{
 public Action Action { get; set; }
 public uint RetryAttempts { get; set; }
 public TimeSpan BackOffInterval { get; set; } = TimeSpan.FromSeconds(3);
}
public class ActionRetryOptions<TResult>: IActionRetryOptions<TResult>
{
 public TResult Result { get; set; }
 public Func<TResult> Action { get; set; }
 public uint RetryAttempts { get; set; }
 public bool Success
 {
 get
 {
 return EvaluateResult();
 }
 }
 public Func<TResult, bool> RetryAction { get; set; }
 public TimeSpan BackOffInterval { get; set; } = TimeSpan.FromSeconds(3);
 public TResult InvokeAction()
 {
 return Action.Invoke();
 }
 bool EvaluateResult()
 {
 if (Result == null)
 throw new InvalidOperationException("Cannot evaluate retry without the action result. Invoke the Action to determine if successfull.");
 else
 return RetryAction.Invoke(this.Result);
 }
}
public class ActionRetryOptions<TIn, TResult>: IActionRetryOptions<TIn, TResult>
{
 public TIn Args { get; set; }
 public TResult Result { get; set; }
 public Func<TIn, TResult> Action { get; set; }
 public uint RetryAttempts { get; set; }
 public TResult InvokeAction()
 {
 return Action.Invoke(Args);
 }
 public bool Success
 {
 get
 {
 return EvaluateResult();
 }
 }
 public Func<TResult, bool> RetryAction { get; set; }
 public TimeSpan BackOffInterval { get; set; } = TimeSpan.FromSeconds(3);
 bool EvaluateResult()
 {
 if (Result == null)
 throw new InvalidOperationException("Cannot evaluate retry without the action result. Invoke the Action to determine if successfull.");
 else
 return RetryAction.Invoke(this.Result);
 }
}

Issues:

  1. How do we support functions with at least one generic parameters and a generic output?
  2. What can we do better?
Vogel612
25.5k7 gold badges59 silver badges141 bronze badges
asked Aug 18, 2017 at 21:35
\$\endgroup\$
5
  • 1
    \$\begingroup\$ For point 1 C++ has variadic templates, but in C# you'll probably have to make do with typename overloading for ActionRetryOptions<TIn, TResult> with multiple TIns and change the properties to match. You just need to support as many Funcs as the BCL provides. \$\endgroup\$ Commented Aug 19, 2017 at 6:24
  • 1
    \$\begingroup\$ Also, your ActionRetryOptions classes really feel like they should descend from an interface, and the builder pattern might come in helpful here, and is the non-caching the result of EvaluateResult in Success intentional? \$\endgroup\$ Commented Aug 19, 2017 at 6:26
  • \$\begingroup\$ Hi @TamoghnaChowdhury, Thanks for your feedback. 1. Edited to make ActionRetryOptions classes inherit from and Interface. 2. For EvaluateResult i dint see a need to cache it, but your feedback will be important. Why do you think we should cache it? 3. I will definitely include support for multiple TIn. Thanks :) \$\endgroup\$ Commented Aug 19, 2017 at 8:57
  • \$\begingroup\$ Not that way - the interface is supposed to be a nongeneric interface which all of the different options classes will implement - you can define the properties/methods common to all the options classes in there, e.g., Success. This is such that you can store all types of option classes in a container structure. \$\endgroup\$ Commented Aug 19, 2017 at 10:15
  • 1
    \$\begingroup\$ On further thought, you shouldn't cache the result - if you're retrying an action, it most probably has side effects and caching would violate the principle of least surprise. However, you probably make Success a field-backed property whose value is set by EvaluateResult - I would not expect that enquiring about the success status of an action would cause it to fire. Make firing the action explicit by calling EvaluateResult and then accessing Success. \$\endgroup\$ Commented Aug 19, 2017 at 10:18

2 Answers 2

4
\$\begingroup\$
  1. You should have an exponential back-off policy i.e. 5 sec, 10 sec, 15 sec.
  2. Why do you need the interface for the options classes? You won't be needing multiple implementations of the options classes so you don't need the interface
  3. The catch all part catch { } is overly generic. You are catching all exceptions including those you shouldn't retry! i.e. imagine you calling a http endpoint and you get a 401/403 repeatedly calling the service won't help you out.
    1. In evaluate result, null could be a valid return value. You need a boolean flag to know if it the function was invoked.
answered Aug 19, 2017 at 9:36
\$\endgroup\$
2
  • 1
    \$\begingroup\$ The interface is there so that a container can be made to hold multiple types of options classes; the OP misunderstood my comment, see clarifying comment below question. \$\endgroup\$ Commented Aug 19, 2017 at 10:13
  • 1
    \$\begingroup\$ 5,10,15 seems pretty linear to me. .. exponential would be something like 1, 2, 4, 8 \$\endgroup\$ Commented Aug 20, 2017 at 10:23
3
\$\begingroup\$

I think a loop would be cleaner here

 uint retries = 0;
 do
 {
 try
 {
 options.Action();
 break;
 }
 catch
 {
 //log
 }
 retries++;
 Thread.Sleep(options.BackOffInterval);
 } while (retries < options.RetryAttempts); 
  • for (uint retries = 0; retries < options.RetryAttempts; retries++)
    {
     try
     {
     options.Action();
     break;
     }
     catch
     {
     //log
     }
     Thread.Sleep(options.BackOffInterval);
    } 
    
answered Aug 19, 2017 at 13:39
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.