Generic Retry Logic Based on Action Result

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++;
} 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++;
} 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++;
} 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?
• 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. – Tamoghna Chowdhury Aug 19 '17 at 6:24
• 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? – Tamoghna Chowdhury Aug 19 '17 at 6:26
• 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 :) – georgeouma Aug 19 '17 at 8:57
• 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. – Tamoghna Chowdhury Aug 19 '17 at 10:15
• 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. – Tamoghna Chowdhury Aug 19 '17 at 10:18

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.
• 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. – Tamoghna Chowdhury Aug 19 '17 at 10:13
• 5,10,15 seems pretty linear to me. .. exponential would be something like 1, 2, 4, 8 – Vogel612 Aug 20 '17 at 10:23

I think a loop would be cleaner here

    uint retries = 0;
do
{
try
{
options.Action();
break;
}
catch
{
//log
}
retries++;
} while (retries < options.RetryAttempts);

• for (uint retries = 0; retries < options.RetryAttempts; retries++)
{
try
{
options.Action();
break;
}
catch
{
//log
}