Retry a task multiple times based on user input in case of an exception in task Retry a task multiple times based on user input in case of an exception in task wpf wpf

Retry a task multiple times based on user input in case of an exception in task


UPDATE 5/2017

C# 6 exception filters make the catch clause a lot simpler :

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)    {        while (true)        {            try            {                var result = await Task.Run(func);                return result;            }            catch when (retryCount-- > 0){}        }    }

and a recursive version:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)    {        try        {            var result = await Task.Run(func);            return result;        }        catch when (retryCount-- > 0){}        return await Retry(func, retryCount);    }

ORIGINAL

There are many ways to code a Retry function: you can use recursion or task iteration. There was a discussion in the Greek .NET User group a while back on the different ways to do exactly this.
If you are using F# you can also use Async constructs. Unfortunately, you can't use the async/await constructs at least in the Async CTP, because the code generated by the compiler doesn't like multiple awaits or possible rethrows in catch blocks.

The recursive version is perhaps the simplest way to build a Retry in C#. The following version doesn't use Unwrap and adds an optional delay before retries :

private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null)    {        if (tcs == null)            tcs = new TaskCompletionSource<T>();        Task.Factory.StartNew(func).ContinueWith(_original =>        {            if (_original.IsFaulted)            {                if (retryCount == 0)                    tcs.SetException(_original.Exception.InnerExceptions);                else                    Task.Factory.StartNewDelayed(delay).ContinueWith(t =>                    {                        Retry(func, retryCount - 1, delay,tcs);                    });            }            else                tcs.SetResult(_original.Result);        });        return tcs.Task;    } 

The StartNewDelayed function comes from the ParallelExtensionsExtras samples and uses a timer to trigger a TaskCompletionSource when the timeout occurs.

The F# version is a lot simpler:

let retry (asyncComputation : Async<'T>) (retryCount : int) : Async<'T> = let rec retry' retryCount =     async {        try            let! result = asyncComputation              return result        with exn ->            if retryCount = 0 then                return raise exn            else                return! retry' (retryCount - 1)    }retry' retryCount

Unfortunatley, it isn't possible to write something similar in C# using async/await from the Async CTP because the compiler doesn't like await statements inside a catch block. The following attempt also fails silenty, because the runtime doesn't like encountering an await after an exception:

private static async Task<T> Retry<T>(Func<T> func, int retryCount)    {        while (true)        {            try            {                var result = await TaskEx.Run(func);                return result;            }            catch             {                if (retryCount == 0)                    throw;                retryCount--;            }        }    }

As for asking the user, you can modify Retry to call a function that asks the user and returns a task through a TaskCompletionSource to trigger the next step when the user answers, eg:

 private static Task<bool> AskUser()    {        var tcs = new TaskCompletionSource<bool>();        Task.Factory.StartNew(() =>        {            Console.WriteLine(@"Error Occured, continue? Y\N");            var response = Console.ReadKey();            tcs.SetResult(response.KeyChar=='y');        });        return tcs.Task;    }    private static Task<T> RetryAsk<T>(Func<T> func, int retryCount,  TaskCompletionSource<T> tcs = null)    {        if (tcs == null)            tcs = new TaskCompletionSource<T>();        Task.Factory.StartNew(func).ContinueWith(_original =>        {            if (_original.IsFaulted)            {                if (retryCount == 0)                    tcs.SetException(_original.Exception.InnerExceptions);                else                    AskUser().ContinueWith(t =>                    {                        if (t.Result)                            RetryAsk(func, retryCount - 1, tcs);                    });            }            else                tcs.SetResult(_original.Result);        });        return tcs.Task;    } 

With all the continuations, you can see why an async version of Retry is so desirable.

UPDATE:

In Visual Studio 2012 Beta the following two versions work:

A version with a while loop:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)    {        while (true)        {            try            {                var result = await Task.Run(func);                return result;            }            catch            {                if (retryCount == 0)                    throw;                retryCount--;            }        }    }

and a recursive version:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)    {        try        {            var result = await Task.Run(func);            return result;        }        catch        {            if (retryCount == 0)                throw;        }        return await Retry(func, --retryCount);    }


Here's a riffed version of Panagiotis Kanavos's excellent answer which I've tested and am using in production.

It addresses some things that were important to me:

  • Want to be able to decide whether to retry based on number of preceding attempts and exception from current attempt
  • Don't want to rely on async (less environment constraints)
  • Want to have the resulting Exception in the case of failure include details from each attempt


static Task<T> RetryWhile<T>(    Func<int, Task<T>> func,     Func<Exception, int, bool> shouldRetry ){    return RetryWhile<T>( func, shouldRetry, new TaskCompletionSource<T>(), 0, Enumerable.Empty<Exception>() );}static Task<T> RetryWhile<T>(     Func<int, Task<T>> func,     Func<Exception, int, bool> shouldRetry,     TaskCompletionSource<T> tcs,     int previousAttempts, IEnumerable<Exception> previousExceptions ){    func( previousAttempts ).ContinueWith( antecedent =>    {        if ( antecedent.IsFaulted )        {            var antecedentException = antecedent.Exception;            var allSoFar = previousExceptions                .Concat( antecedentException.Flatten().InnerExceptions );            if ( shouldRetry( antecedentException, previousAttempts ) )                RetryWhile( func,shouldRetry,previousAttempts+1, tcs, allSoFar);            else                tcs.SetException( allLoggedExceptions );        }        else            tcs.SetResult( antecedent.Result );    }, TaskContinuationOptions.ExecuteSynchronously );    return tcs.Task;}


When at the high level, I find it helps to make a function signature from what you have and what you want.

You have:

  • A function that gives you a task (Func<Task>). We'll use the function because tasks themselves are not retryable in general.
  • A function that determines if the overall task is completed or should be retried (Func<Task, bool>)

You want:

  • An overall Task

So you'll have a function like:

Task Retry(Func<Task> action, Func<Task, bool> shouldRetry);

Extending the practice inside the function, tasks pretty much have 2 operations to do with them, read their state and ContinueWith. To make your own tasks, TaskCompletionSource is a good starting point. A first try might look something like:

//error checkingvar result = new TaskCompletionSource<object>();action().ContinueWith((t) =>   {    if (shouldRetry(t))        action();    else    {        if (t.IsFaulted)            result.TrySetException(t.Exception);        //and similar for Canceled and RunToCompletion    }  });

The obvious problem here is that only 1 retry will ever happen. To get around that, you need to make a way for the function to call itself. The usual way to do this with lambdas is something like this:

//error checkingvar result = new TaskCompletionSource<object>();Func<Task, Task> retryRec = null; //declare, then assignretryRec = (t) => { if (shouldRetry(t))                        return action().ContinueWith(retryRec).Unwrap();                    else                    {                        if (t.IsFaulted)                             result.TrySetException(t.Exception);                        //and so on                        return result.Task; //need to return something                     }                  }; action().ContinueWith(retryRec); return result.Task;