简体   繁体   中英

How to retry until some condition is met

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);

A more functional approach

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM