Retry a task multiple times based on user input in case of an exception in task
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;