有时,我需要在放弃之前将一个操作重试几次。我的代码是:
int retries = 3;
while(true) {
try {
DoSomething();
break; // success!
} catch {
if(--retries == 0) throw;
else Thread.Sleep(1000);
}
}
我想在一个通用的重试函数中重写这个:
TryThreeTimes(DoSomething);
这在c#中可行吗?TryThreeTimes()方法的代码是什么?
此方法允许对某些异常类型进行重试(立即抛出其他异常类型)。
public static void DoRetry(
List<Type> retryOnExceptionTypes,
Action actionToTry,
int retryCount = 5,
int msWaitBeforeEachRety = 300)
{
for (var i = 0; i < retryCount; ++i)
{
try
{
actionToTry();
break;
}
catch (Exception ex)
{
// Retries exceeded
// Throws on last iteration of loop
if (i == retryCount - 1) throw;
// Is type retryable?
var exceptionType = ex.GetType();
if (!retryOnExceptionTypes.Contains(exceptionType))
{
throw;
}
// Wait before retry
Thread.Sleep(msWaitBeforeEachRety);
}
}
}
public static void DoRetry(
Type retryOnExceptionType,
Action actionToTry,
int retryCount = 5,
int msWaitBeforeEachRety = 300)
=> DoRetry(new List<Type> {retryOnExceptionType}, actionToTry, retryCount, msWaitBeforeEachRety);
使用示例:
DoRetry(typeof(IOException), () => {
using (var fs = new FileStream(requestedFilePath, FileMode.Create, FileAccess.Write))
{
fs.Write(entryBytes, 0, entryBytes.Length);
}
});
在之前工作的基础上,我考虑用三种方式增强重试逻辑:
Specifying what exception type to catch/retry. This is the primary enhacement as retrying for any exception is just plain wrong.
Not nesting the last try in a try/catch, achieving slightly better performance
Making it an Action extension method
static class ActionExtensions
{
public static void InvokeAndRetryOnException<T> (this Action action, int retries, TimeSpan retryDelay) where T : Exception
{
if (action == null)
throw new ArgumentNullException("action");
while( retries-- > 0 )
{
try
{
action( );
return;
}
catch (T)
{
Thread.Sleep( retryDelay );
}
}
action( );
}
}
然后可以像这样调用方法(当然也可以使用匿名方法):
new Action( AMethodThatMightThrowIntermittentException )
.InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );
您还可以考虑添加要重试的异常类型。例如,这是您想要重试的超时异常吗?数据库异常?
RetryForExcpetionType(DoSomething, typeof(TimeoutException), 5, 1000);
public static void RetryForExcpetionType(Action action, Type retryOnExceptionType, int numRetries, int retryTimeout)
{
if (action == null)
throw new ArgumentNullException("action");
if (retryOnExceptionType == null)
throw new ArgumentNullException("retryOnExceptionType");
while (true)
{
try
{
action();
return;
}
catch(Exception e)
{
if (--numRetries <= 0 || !retryOnExceptionType.IsAssignableFrom(e.GetType()))
throw;
if (retryTimeout > 0)
System.Threading.Thread.Sleep(retryTimeout);
}
}
}
您可能还注意到,所有其他示例在测试retries == 0时都存在类似的问题,要么重试无穷大,要么在给定负值时无法引发异常。Sleep(-1000)在上面的catch块中也会失败。这取决于你期望人们有多“愚蠢”,但防御性编程永远不会伤害到你。
我使用Polly实现了该模式的两个实现。其一是异步。
我的同步方法是基于Erik Bergstedt的回答
public static T Retry<T>(Func<T> action, TimeSpan retryWait, int retryCount = 0)
{
PolicyResult<T> policyResult = Policy
.Handle<ApiException>(ex => ex.ResponseCode == (int)HttpStatusCode.TooManyRequests)
.WaitAndRetry(retryCount, retryAttempt => retryWait)
.ExecuteAndCapture(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
异步:
public static async Task<T> RetryAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0)
{
PolicyResult<T> policyResult = await Policy
.Handle<ApiException>(ex => ex.ResponseCode == (int)HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
.ExecuteAndCaptureAsync(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
允许传入异常类型以及异常类型的lambda也很容易。
我将在接受的答案中添加以下代码
public static class Retry<TException> where TException : Exception //ability to pass the exception type
{
//same code as the accepted answer ....
public static T Do<T>(Func<T> action, TimeSpan retryInterval, int retryCount = 3)
{
var exceptions = new List<Exception>();
for (int retry = 0; retry < retryCount; retry++)
{
try
{
return action();
}
catch (TException ex) //Usage of the exception type
{
exceptions.Add(ex);
Thread.Sleep(retryInterval);
}
}
throw new AggregateException(String.Format("Failed to excecute after {0} attempt(s)", retryCount), exceptions);
}
}
基本上,上面的代码使Retry类成为泛型,这样您就可以传递想要捕获的异常类型进行重试。
现在,以几乎相同的方式使用它,但指定异常类型
Retry<EndpointNotFoundException>.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));
我已经实现了一个异步版本的接受的答案,就像这样-它似乎工作得很好-有评论吗?
public static async Task DoAsync(
Action action,
TimeSpan retryInterval,
int maxAttemptCount = 3)
{
DoAsync<object>(() =>
{
action();
return null;
}, retryInterval, maxAttemptCount);
}
public static async Task<T> DoAsync<T>(
Func<Task<T>> action,
TimeSpan retryInterval,
int maxAttemptCount = 3)
{
var exceptions = new List<Exception>();
for (int attempted = 0; attempted < maxAttemptCount; attempted++)
{
try
{
if (attempted > 0)
{
Thread.Sleep(retryInterval);
}
return await action();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
throw new AggregateException(exceptions);
}
并且,简单地像这样称呼它:
var result = await Retry.DoAsync(() => MyAsyncMethod(), TimeSpan.FromSeconds(5), 4);