I need to retry a certain method until it returns a non-empty Guid.
There's an awesome answer that retries based on whether there is an exception; however, I would like to generalize this class to be able to handle any specified condition.
The current usage will perform an action a specific number of times until there are no exceptions:
Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));
or:
Retry.Do(SomeFunctionThatCanFail, TimeSpan.FromSeconds(1));
or:
int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);
How can I modify this class such that it retries based on the return value of the function that I pass in?
For example, If I wanted to retry until my function returned 3:
Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1)).Until(3);
Which would mean execute SomeFunctionThatCanFail(), every 1 second, until SomeFunctionThatCanFail() = 3?
How would I generalize the usage of Retry.Do until a condition is met?
public static class Retry
{
public static void Do(
Action action,
TimeSpan retryInterval,
int retryCount = 3)
{
Do<object>(() =>
{
action();
return null;
}, retryInterval, retryCount);
}
public static T Do<T>(
Func<T> action,
TimeSpan retryInterval,
int retryCount = 3)
{
var exceptions = new List<Exception>();
for (int retry = 0; retry < retryCount; retry++) //I would like to change this logic so that it will retry not based on whether there is an exception but based on the return value of Action
{
try
{
if (retry > 0)
Thread.Sleep(retryInterval);
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
throw new AggregateException(exceptions);
}
}
How about creating the following interface:
public interface IRetryCondition<TResult>
{
TResult Until(Func<TResult, bool> condition);
}
public class RetryCondition<TResult> : IRetryCondition<TResult>
{
private TResult _value;
private Func<IRetryCondition<TResult>> _retry;
public RetryCondition(TResult value, Func<IRetryCondition<TResult>> retry)
{
_value = value;
_retry = retry;
}
public TResult Until(Func<TResult, bool> condition)
{
return condition(_value) ? _value : _retry().Until(condition);
}
}
And then, you'll update your Retry
static class:
public static class Retry
{
// This method stays the same
// Returning an IRetryCondition does not make sense in a "void" action
public static void Do(
Action action,
TimeSpan retryInterval,
int retryCount = 3)
{
Do<object>(() =>
{
action();
return null;
}, retryInterval, retryCount);
}
// Return an IRetryCondition<T> instance
public static IRetryCondition<T> Do<T>(
Func<T> action,
TimeSpan retryInterval,
int retryCount = 3)
{
var exceptions = new List<Exception>();
for (int retry = 0; retry < retryCount; retry++)
{
try
{
if (retry > 0)
Thread.Sleep(retryInterval);
// We return a retry condition loaded with the return value of action() and telling it to execute this same method again if condition is not met.
return new RetryCondition<T>(action(), () => Do(action, retryInterval, retryCount));
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
throw new AggregateException(exceptions);
}
}
You'll be able to achieve something like the following:
int result = Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1)).Until(r => r == 3);
I tried to come up with a more "functional oriented" solution (somewhat similar to LINQ):
First, we would have two interfaces for executing the action:
public interface IRetryResult
{
void Execute();
}
public interface IRetryResult<out TResult>
{
TResult Execute();
}
Then, we'll need two interfaces for configuring the retry operation:
public interface IRetryConfiguration : IRetryResult
{
IRetryConfiguration Times(int times);
IRetryConfiguration Interval(TimeSpan interval);
}
public interface IRetryConfiguration<out TResult> : IRetryResult<TResult>
{
IRetryConfiguration<TResult> Times(int times);
IRetryConfiguration<TResult> Interval(TimeSpan interval);
IRetryConfiguration<TResult> Until(Function<TResult, bool> condition);
}
Finally, we'll need two implementations for both interfaces:
public class ActionRetryConfiguration : IRetryConfiguration
{
private readonly Action _action;
private readonly int? _times;
private readonly TimeSpan? _interval;
public ActionRetryConfiguration(Action action, int? times, TimeSpan? interval)
{
_action = action;
_times = times;
_interval = interval;
}
public void Execute()
{
Execute(_action, _times, _interval);
}
private void Execute(Action action, int? times, TimeSpan? interval)
{
action();
if (times.HasValue && times.Value <= 1) return;
if (times.HasValue && interval.HasValue) Thread.Sleep(interval.Value);
Execute(action, times - 1, interval);
}
public IRetryConfiguration Times(int times)
{
return new ActionRetryConfiguration(_action, times, _interval);
}
public IRetryConfiguration Interval(TimeSpan interval)
{
return new ActionRetryConfiguration(_action, _times, interval);
}
}
public class FunctionRetryConfiguration<TResult> : IRetryConfiguration<TResult>
{
private readonly Func<TResult> _function;
private readonly int? _times;
private readonly TimeSpan? _interval;
private readonly Func<TResult, bool> _condition;
public FunctionRetryConfiguration(Func<TResult> function, int? times, TimeSpan? interval, Func<TResult, bool> condition)
{
_function = function;
_times = times;
_interval = interval;
_condition = condition;
}
public TResult Execute()
{
return Execute(_function, _times, _interval, _condition);
}
private TResult Execute(Func<TResult> function, int? times, TimeSpan? interval, Func<TResult, bool> condition)
{
TResult result = function();
if (condition != null && condition(result)) return result;
if (times.HasValue && times.Value <= 1) return result;
if ((times.HasValue || condition != null) && interval.HasValue) Thread.Sleep(interval.Value);
return Execute(function, times - 1, interval, condition);
}
public IRetryConfiguration<TResult> Times(int times)
{
return new FunctionRetryConfiguration<TResult>(_function, times, _interval, _condition);
}
public IRetryConfiguration<TResult> Interval(TimeSpan interval)
{
return new FunctionRetryConfiguration<TResult>(_function, _times, interval, _condition);
}
public IRetryConfiguration<TResult> Until(Func<TResult, bool> condition)
{
return new FunctionRetryConfiguration<TResult>(_function, _times, _interval, condition);
}
}
And, lastly, the Retry
static class, the entry point:
public static class Retry
{
public static IRetryConfiguration Do(Action action)
{
return new ActionRetryConfiguration(action, 1, null);
}
public static IRetryConfiguration<TResult> Do<TResult>(Func<TResult> action)
{
return new FunctionRetryConfiguration<TResult>(action, 1, null, null);
}
}
I think this approach is less buggy, and cleaner.
Also, it let you do things like these:
int result = Retry.Do(SomeIntMethod).Interval(TimeSpan.FromSeconds(1)).Until(n => n > 20).Execute();
Retry.Do(SomeVoidMethod).Times(4).Execute();
Microsoft's Reactive Framework (NuGet "Rx-Main") has all of the operators already built to do this kind of thing out of the box.
Try this:
IObservable<int> query =
Observable
.Defer(() =>
Observable.Start(() => GetSomeValue()))
.Where(x => x == 1)
.Timeout(TimeSpan.FromSeconds(0.1))
.Retry()
.Take(1);
query
.Subscribe(x =>
{
// Can only be a `1` if produced in less than 0.1 seconds
Console.WriteLine(x);
});
Well, if I understood everything correctly, something like this should solve your problem:
public static T Do<T>(Func<T> action, TimeSpan retryInterval, Predicate<T> predicate)
{
var exceptions = new List<Exception>();
try
{
bool succeeded;
T result;
do
{
result = action();
succeeded = predicate(result);
} while (!succeeded);
return result;
}
catch (Exception ex)
{
exceptions.Add(ex);
}
throw new AggregateException(exceptions);
}
Add this method to your retry class.
I've tried it with a sample ConsoleApplication, with this code:
class Program
{
static void Main(string[] args)
{
var _random = new Random();
Func<int> func = () =>
{
var result = _random.Next(10);
Console.WriteLine(result);
return result;
};
Retry.Do(func, TimeSpan.FromSeconds(1), i => i == 5);
Console.ReadLine();
}
}
And indeed, it stops when it randoms 5
.
It seems like you're overthinking this:
int returnValue = -1;
while (returnValue != 3)
{
returnValue = DoStuff();
// DoStuff should include a step to avoid maxing out cpu
}
return returnValue;
Of course, "3" could be a variable that you pass into the function.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.