Reintentar una tarea varias veces en función de la entrada del usuario en caso de una excepción en la tarea

Todas las llamadas de servicio en mi aplicación se implementan como tareas. Cuando una tarea tiene una falla, necesito presentarle al usuario un cuadro de diálogo para intentar de nuevo la última operación fallida. Si el usuario elige reintentar el progtwig, debe volver a intentar la tarea; la ejecución del progtwig debe continuar después de registrar la excepción. ¿Alguno tiene una idea de alto nivel sobre cómo implementar esta funcionalidad?

ACTUALIZACIÓN 5/2017

Los filtros de excepción C # 6 hacen que la cláusula catch mucho más simple:

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

y una versión recursiva:

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

ORIGINAL

Hay muchas formas de codificar una función Reintentar: puede usar recursión o iteración de tareas. Hubo un debate en el grupo de usuarios de .NET griego hace un tiempo sobre las diferentes formas de hacer exactamente esto.
Si usa F #, también puede usar construcciones Async. Desafortunadamente, no se pueden usar las construcciones async / await al menos en el Async CTP, porque al código generado por el comstackdor no le gustan los múltiples tiempos de espera o los posibles retiros en los bloques catch.

La versión recursiva es quizás la forma más sencilla de construir un Reintentar en C #. La siguiente versión no utiliza Desenvolver y agrega un retraso opcional antes de volver a intentar:

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

La función StartNewDelayed proviene de los ejemplos ParallelExtensionsExtras y utiliza un temporizador para desencadenar un TaskCompletionSource cuando se produce el tiempo de espera.

La versión F # es mucho más simple:

 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 

Desafortunadamente, no es posible escribir algo similar en C # usando async / await del CTP Async porque al comstackdor no le gusta las instrucciones de espera dentro de un bloque catch. El siguiente bash también falla silencioso, porque al tiempo de ejecución no le gusta encontrar un estado de espera después de una excepción:

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

En cuanto a preguntar al usuario, puede modificar Reintentar para llamar a una función que le pregunta al usuario y devuelve una tarea a través de TaskCompletionSource para activar el próximo paso cuando el usuario responde, por ejemplo:

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

Con todas las continuación, puede ver por qué una versión asíncrona de Retry es tan deseable.

ACTUALIZAR:

En Visual Studio 2012 Beta, las siguientes dos versiones funcionan:

Una versión con un ciclo 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--; } } } 

y una versión recursiva:

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

Aquí hay una versión rimada de la excelente respuesta de Panagiotis Kanavos que he probado y estoy usando en producción.

Aborda algunas cosas que eran importantes para mí:

  • Desea poder decidir si volver a intentar en función del número de bashs anteriores y la excepción del bash actual
  • No quiero confiar en async (menos restricciones de entorno)
  • Desea que la Exception resultante en caso de falla incluya detalles de cada bash

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

Cuando estoy en el nivel superior, me parece útil hacer una firma de función a partir de lo que tienes y lo que quieres.

Tienes:

  • Una función que le proporciona una tarea ( Func ). Usaremos la función porque las tareas en sí no son recuperables en general.
  • Una función que determina si la tarea global se completa o se debe volver a intentar ( Func )

Usted quiere:

  • Una tarea general

Entonces tendrás una función como:

 Task Retry(Func action, Func shouldRetry); 

Extendiendo la práctica dentro de la función, las tareas tienen prácticamente 2 operaciones que hacer con ellas, leer su estado y ContinueWith . Para realizar sus propias tareas, TaskCompletionSource es un buen punto de partida. Un primer bash podría ser algo así como:

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

El problema obvio aquí es que solo 1 rebash ocurrirá alguna vez. Para evitarlo, debe hacer que la función se llame a sí misma. La forma habitual de hacer esto con lambdas es algo como esto:

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