Повторите задачу несколько раз на основе пользовательского ввода в случае исключения в задаче
Все сервисные вызовы в моем приложении реализованы в виде задач. При возникновении сбоя задачи мне нужно предоставить пользователю диалоговое окно, чтобы повторить попытку последней неудачной операции. Если пользователь выбирает повторить, программа должна повторить задачу, иначе выполнение программы должно продолжаться после регистрации исключения. У кого-нибудь есть общее представление о том, как реализовать эту функциональность?
3 ответа
ОБНОВЛЕНИЕ 5/2017
Фильтры исключенийC# 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 было обсуждение различных способов сделать именно это.
Если вы используете F#, вы также можете использовать асинхронные конструкции. К сожалению, вы не можете использовать конструкции async/await, по крайней мере, в Async CTP, потому что код, сгенерированный компилятором, не любит множественное ожидание или возможные повторные выбросы в блоках catch.
Рекурсивная версия, пожалуй, самый простой способ построить Retry в C#. Следующая версия не использует 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 и использует таймер для запуска TaskCompletionSource при наступлении тайм-аута.
Версия 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
К сожалению, невозможно написать что-то подобное в C#, используя async/await из Async CTP, потому что компилятору не нравятся операторы await внутри блока 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 так желательна.
ОБНОВИТЬ:
В 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);
}
Вот отличная версия превосходного ответа Панагиотиса Канавоса, который я протестировал и использую в производстве.
В нем рассматриваются некоторые вещи, которые были важны для меня:
- Хотите иметь возможность решить, следует ли повторить попытку, основываясь на количестве предыдущих попыток и исключении из текущей попытки
- Не хочу полагаться на
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;
}
Находясь на высоком уровне, я считаю, что это помогает сделать сигнатуру функции из того, что у вас есть и что вы хотите.
У тебя есть:
- Функция, которая дает вам задачу (
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 повтор. Чтобы обойти это, вам нужно сделать так, чтобы функция вызывала сама себя. Обычный способ сделать это с лямбдами это примерно так:
//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;