我的应用程序中的所有服务调用都是作为任务实现的。当一个任务出现故障时,我需要向用户显示一个对话框来重试上次失败的操作。如果用户选择重试,程序应该重试任务,否则记录异常后应继续执行程序。任何人对如何实现此功能有一个高层次的想法?
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 构造。不幸的是,您至少不能在 Async CTP 中使用 async/await 结构,因为编译器生成的代码不喜欢多个等待或可能在 catch 块中重新抛出。
递归版本可能是在 C# 中构建 Retry 的最简单方法。以下版本不使用 Unwrap 并在 retries 之前添加可选延迟:
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 CTP 的 async/await 编写类似的东西,因为编译器不喜欢 catch 块中的 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--;
}
}
}
至于询问用户,可以修改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);
}
这是我已经测试并在生产中使用的Panagiotis Kanavos 出色答案的重复版本。
它解决了一些对我很重要的事情:
- 希望能够根据先前尝试的次数和当前尝试的异常来决定是否重试
- 不想依赖
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 次重试。为了解决这个问题,您需要为函数提供一种调用自身的方法。使用 lambdas 执行此操作的通常方法是这样的:
//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;