2012-05-07 39 views
10

我的应用程序中的所有服务调用均作为任务实现。当有任务出现故障时,我需要向用户提供一个对话框以重试最后的操作失败了。如果用户选择重试,程序应该重试任务,否则在记录异常之后程序的执行应该继续。任何人都已经对如何实现这个功能有了高层次的想法?任务中出现异常时,根据用户输入多次重试任务

+0

@svick我没试过来实现这个功能它是一个未来的任务来了,看起来我们得到了有趣的想法已经,已经通过他们去详细,并给它一个尝试 –

回答

31

UPDATE二千零十七分之五

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

ORIGINAL

有很多方法来编写一个重试功能:您可以使用递归或任务迭代。在希腊.NET用户组中有一个discussion,而回到完成这个的不同方式。
如果你使用F#,你也可以使用Async结构。不幸的是,至少在Async CTP中不能使用异步/等待结构,因为编译器生成的代码不像多个等待或catch块中可能的重新抛出。

递归版本可能是在C#中构建重试的最简单方法。下面的版本不使用展开和重试前增加了一个可选的延迟:

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#中类似的东西从异步CTP /等待,因为编译器不喜欢里面等待语句一个catch块。下面的尝试也失败silenty,因为运行时不喜欢异常后遇到的await:

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

至于询问用户,你可以修改重试打电话,询问用户,并返回任务的功能通过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; 
    } 

与所有的延续,你可以看到为什么重试的异步版本是这么希望的。

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

感谢您的详细解释,将尝试这一点,让你知道 –

+1

+1好的工作。 [在下面添加了一个段落](http://stackoverflow.com/a/16354355/11635) –

2

当处于高层次时,我发现它有助于根据您拥有的和想要的来制作函数签名。

您有:

  • ,给你一个任务(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重试将不会发生。为了解决这个问题,你需要为函数自己调用一个方法。通常的方式与lambda表达式来做到这一点是这样的:

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

感谢,将试着让你知道 –

4

这里是一个riffed版本Panagiotis Kanavos's excellent answer我已经测试过,并在生产中使用。

它解决一些事情,是很重要的对我说:

  • 希望能够决定基于目前从前面的尝试和努力例外数量是否重试
  • 不想依靠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; 
}