Повторить задачу несколько раз на основе пользовательского ввода в случае исключения в задаче

Все служебные вызовы в моем приложении реализованы как задачи. Когда когда-либо проблема возникает, мне нужно представить пользователю диалоговое окно, чтобы повторить попытку последней операции. Если пользователь выбирает повторную попытку, программа должна повторить задачу, иначе выполнение программы должно продолжаться после регистрации исключения. У кого-то есть идея высокого уровня о том, как реализовать эту функциональность?

ОБНОВЛЕНИЕ 5/2017

Фильтры исключений C # 6 делают предложение catch намного проще:

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

и рекурсивная версия:

  private static async Task Retry(Func 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. К сожалению, вы не можете использовать конструкции async / wait, по крайней мере, в Async CTP, потому что код, сгенерированный компилятором, не любит множественных ожиданий или возможных повторов в блоках catch.

Рекурсивная версия – это, пожалуй, самый простой способ создания Retry в C #. Следующая версия не использует Unwrap и добавляет дополнительную задержку перед повторными попытками:

 private static Task Retry(Func func, int retryCount, int delay, TaskCompletionSource tcs = null) { if (tcs == null) tcs = new TaskCompletionSource(); 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, невозможно написать что-то подобное в C #, используя async / await из Async CTP, потому что компилятор не любит ожидания операторов внутри блока catch. Следующая попытка также терпит неудачу, потому что среда выполнения не любит встречаться с ожиданием после исключения:

 private static async Task Retry(Func 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 AskUser() { var tcs = new TaskCompletionSource(); 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 RetryAsk(Func func, int retryCount, TaskCompletionSource tcs = null) { if (tcs == null) tcs = new TaskCompletionSource(); 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 Retry(Func func, int retryCount) { while (true) { try { var result = await Task.Run(func); return result; } catch { if (retryCount == 0) throw; retryCount--; } } } 

и рекурсивная версия:

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

Вот ребристая версия замечательного ответа Panagiotis Kanavos, который я тестировал и использую в производстве.

В нем рассматриваются некоторые важные для меня вещи:

  • Хотите, чтобы иметь возможность решить, следует ли повторять попытку на основе количества предыдущих попыток и исключений из текущей попытки
  • Не хотите полагаться на async (меньше ограничений среды)
  • Хотите, чтобы получившееся Exception в случае неудачи включало данные из каждой попытки

 static Task RetryWhile( Func> func, Func shouldRetry ) { return RetryWhile( func, shouldRetry, new TaskCompletionSource(), 0, Enumerable.Empty() ); } static Task RetryWhile( Func> func, Func shouldRetry, TaskCompletionSource tcs, int previousAttempts, IEnumerable 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 ). Мы будем использовать эту функцию, потому что сами задачи не повторяются в целом.
  • Функция, которая определяет, завершена ли общая задача или должна быть повторена ( Func )

Вы хотите:

  • Общая задача

Таким образом, у вас будет такая функция, как:

 Task Retry(Func action, Func shouldRetry); 

Расширяя практику внутри функции, задачи в значительной степени выполняют 2 операции с ними, читают их состояние и ContinueWith . Для создания собственных задач TaskCompletionSource является хорошей отправной точкой. Первая попытка может выглядеть примерно так:

 //error checking var result = new TaskCompletionSource(); 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(); Func 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; 
  • Действительное использование goto для управления ошибками в C?
  • Давайте будем гением компьютера.