2012-05-07 12 views
10

Todas las llamadas de servicio en mi aplicación se implementan como tasks.When cada vez se abre una tarea, necesito presentar al usuario un cuadro de diálogo para volver a intentar la última operación failed.If el usuario elige volver a intentar el programa debe volver a intentar la tarea, de lo contrario la ejecución del programa debe continuar después de entrar el exception.Any tiene una idea de alto nivel sobre cómo implementar esta funcionalidad?reintentar una tarea varias veces sobre la base de la entrada del usuario en caso de una excepción en tarea

+0

@svick no he tratado de implementar esta funcionalidad está llegando a una tarea futura, Parece que tenemos ideas interesantes ya, tiene que ir a través de ellos en detalle y darle una oportunidad –

Respuesta

31

ACTUALIZACIÓN 5/2017

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

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

y una versión recursiva:

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

Hay muchas maneras de codificar una función de reintento: se puede usar recursión o iteración tarea. Hubo un discussion en el grupo de usuarios griego .NET hace un tiempo sobre las diferentes formas de hacer exactamente esto.
Si está utilizando F # También se puede usar construcciones asíncronas. Desafortunadamente, no se pueden usar las construcciones async/await al menos en el Async CTP, porque al código generado por el compilador 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 reintento en C#.La siguiente versión no utiliza Separar y añade un retardo opcional antes reintentos:

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

La función StartNewDelayed proviene de los ParallelExtensionsExtras muestras y utiliza un temporizador para activar 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 asíncrono/espera de la asíncrono CTP porque el compilador no le gusta declaraciones aguardan en el interior un bloque de captura. El siguiente intento también falla Silenty, debido a que el tiempo de ejecución no le gusta encontrarse con un descansar tras una excepción:

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

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

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

con todas las continuaciones, se puede ver por qué una versión asíncrona de reintento es tan deseable.

ACTUALIZACIÓN:

En Visual Studio 2012 Beta las dos versiones siguientes trabajos:

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

y una versión recursiva:

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

Gracias por una explicación detallada, probaré esto y le dejaré saber –

+1

+1 Buen trabajo. [Agregó un riff debajo] (http://stackoverflow.com/a/16354355/11635) –

2

Cuando en el nivel alto, me parece que ayuda a hacer una firma de función de lo que tienes y lo que quiere.

tiene:

  • Una función que le da una tarea (Func<Task>). Usaremos la función porque las tareas en sí no son recuperables en general.
  • Una función que determina si la tarea se ha completado o total debe ser juzgado (Func<Task, bool>)

que desee:

  • Una tarea general

por lo que tendrá una funcionan como:

Task Retry(Func<Task> action, Func<Task, bool> shouldRetry); 

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

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

El problema obvio aquí es que sólo 1 de reintento volverá a suceder. 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<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; 
+0

Gracias, lo hará intente y le deje saber –

4

Aquí hay una versión riffed de Panagiotis Kanavos's excellent answer que he probado y estoy usando en producción.

trata de resolver algunas cosas que eran importantes para mí:

  • ¿Desea poder decidir si vuelve a intentar función del número de intentos anteriores y excepción del intento actual
  • no quiero depender de async (menos restricciones medio ambiente)
  • quiere tener la Exception lo que resulta en el caso de fallo incluyen detalles de cada intento


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