Все служебные вызовы в моем приложении реализованы как задачи. Если когда-либо проблема сбой, мне нужно представить пользователю диалоговое окно для повторной попытки последней операции. Если пользователь выбирает повторную попытку, программа должна повторить задачу, иначе выполнение программы должно продолжаться после регистрации исключения. Любой имеет идею высокого уровня о том, как реализовать эту функциональность?
Повторить задачу несколько раз на основе пользовательского ввода в случае исключения в задаче
Ответ 1
ОБНОВЛЕНИЕ 5/2017
Фильтры исключений С# 6 делают предложение catch
намного проще:
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){}
}
}
и рекурсивная версия:
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);
}
ОРИГИНАЛ
Существует множество способов кодирования функции Retry: вы можете использовать рекурсию или итерацию задачи. В греческой группе .NET User было обсуждение, а затем повторить разные способы сделать именно это.
Если вы используете F #, вы также можете использовать конструкции Async. К сожалению, вы не можете использовать конструкции async/wait, по крайней мере, в Async CTP, потому что код, сгенерированный компилятором, не любит множественных ожиданий или возможных повторов в блоках catch.
Рекурсивная версия - это, пожалуй, самый простой способ создания Retry в С#. Следующая версия не использует Unwrap и добавляет дополнительную задержку перед повторением:
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;
}
Функция StartNewDelayed исходит из ParallelExtensionsExtras образцов и использует таймер для запуска источника TaskCompletion, когда происходит таймаут.
Версия F # намного проще:
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, невозможно написать что-то подобное в С#, используя async/await из Async CTP, потому что компилятор не любит ожидания операторов внутри блока catch. Следующая попытка также терпит неудачу, поскольку среда выполнения не любит встречаться с ожиданием после исключения:
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--;
}
}
}
Что касается запроса пользователя, вы можете изменить Retry для вызова функции, которая запрашивает пользователя, и возвращает задачу через TaskCompletionSource для запуска следующего шага, когда пользователь отвечает, например:
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;
}
Со всеми продолжениями вы можете понять, почему асинхронная версия Retry настолько желательна.
UPDATE:
В Visual Studio 2012 Beta работают следующие две версии:
Версия с циклом while:
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--;
}
}
}
и рекурсивная версия:
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);
}
Ответ 2
Здесь приведенная версия Panagiotis Kanavos отличного ответа, который я тестировал и использую на производстве.
В нем рассматриваются некоторые важные для меня вещи:
- Хотите иметь возможность решить, следует ли повторять попытку на основе количества предыдущих попыток и исключений из текущей попытки.
- Не стоит полагаться на
async
(меньше ограничений среды) - Хотите, чтобы получившийся
Exception
в случае сбоя включал в себя детали из каждой попытки.
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;
}
Ответ 3
Когда на высоком уровне я нахожу, что это помогает сделать подпись функции из того, что у вас есть и чего вы хотите.
У вас есть:
- Функция, которая дает вам задачу (
Func<Task>
). Мы будем использовать эту функцию, потому что сами задачи вообще не повторяются. - Функция, которая определяет, завершена ли общая задача или ее следует повторить (
Func<Task, bool>
)
Вы хотите:
- Общая задача
Итак, у вас будет такая функция, как:
Task Retry(Func<Task> action, Func<Task, bool> shouldRetry);
Расширение практики внутри функции, задачи в значительной степени выполняют 2 операции с ними, считывают их состояние и ContinueWith
. Для выполнения ваших собственных задач TaskCompletionSource
является хорошей отправной точкой. Первая попытка может выглядеть примерно так:
//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
}
});
Очевидная проблема здесь заключается в том, что только 1 повторение произойдет. Чтобы обойти это, вам нужно создать способ для вызова функции. Обычный способ сделать это с помощью lambdas - это примерно так:
//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;