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

14,760

Solution 1

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

Solution 2

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

Solution 3

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 checking
var 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 checking
var result = new TaskCompletionSource<object>();

Func<Task, Task> retryRec = null; //declare, then assign
retryRec = (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;
Share:
14,760
Vasudevan Kannan
Author by

Vasudevan Kannan

Updated on June 05, 2022

Comments

  • Vasudevan Kannan
    Vasudevan Kannan over 1 year

    All the service calls in my application are implemented as tasks.When ever a task is faulted ,I need to present the user with a dialog box to retry the last operation failed.If the user chooses retry the program should retry the task ,else the execution of the program should continue after logging the exception.Any one has got a high level idea on how to implement this functionality ?

  • Vasudevan Kannan
    Vasudevan Kannan over 11 years
    Thanks for a detailed explanation , will try this out and let you know
  • Vasudevan Kannan
    Vasudevan Kannan over 11 years
    Thanks , will try and let you know