有时,我需要在放弃之前将一个操作重试几次。我的代码是:
int retries = 3;
while(true) {
try {
DoSomething();
break; // success!
} catch {
if(--retries == 0) throw;
else Thread.Sleep(1000);
}
}
我想在一个通用的重试函数中重写这个:
TryThreeTimes(DoSomething);
这在c#中可行吗?TryThreeTimes()方法的代码是什么?
我已经实现了一个异步版本的接受的答案,就像这样-它似乎工作得很好-有评论吗?
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);
下面是一个async/await版本,它可以聚合异常并支持取消。
/// <seealso href="https://learn.microsoft.com/en-us/azure/architecture/patterns/retry"/>
protected static async Task<T> DoWithRetry<T>( Func<Task<T>> action, CancellationToken cancelToken, int maxRetries = 3 )
{
var exceptions = new List<Exception>();
for ( int retries = 0; !cancelToken.IsCancellationRequested; retries++ )
try {
return await action().ConfigureAwait( false );
} catch ( Exception ex ) {
exceptions.Add( ex );
if ( retries < maxRetries )
await Task.Delay( 500, cancelToken ).ConfigureAwait( false ); //ease up a bit
else
throw new AggregateException( "Retry limit reached", exceptions );
}
exceptions.Add( new OperationCanceledException( cancelToken ) );
throw new AggregateException( "Retry loop was canceled", exceptions );
}
用c#、Java或其他语言简单地完成:
internal class ShouldRetryHandler {
private static int RETRIES_MAX_NUMBER = 3;
private static int numberTryes;
public static bool shouldRetry() {
var statusRetry = false;
if (numberTryes< RETRIES_MAX_NUMBER) {
numberTryes++;
statusRetry = true;
//log msg -> 'retry number' + numberTryes
}
else {
statusRetry = false;
//log msg -> 'reached retry number limit'
}
return statusRetry;
}
}
并在你的代码中简单地使用它:
void simpleMethod(){
//some code
if(ShouldRetryHandler.shouldRetry()){
//do some repetitive work
}
//some code
}
或者你可以在递归方法中使用它:
void recursiveMethod(){
//some code
if(ShouldRetryHandler.shouldRetry()){
recursiveMethod();
}
//some code
}
public void TryThreeTimes(Action action)
{
var tries = 3;
while (true) {
try {
action();
break; // success!
} catch {
if (--tries == 0)
throw;
Thread.Sleep(1000);
}
}
}
然后你会呼叫:
TryThreeTimes(DoSomething);
...或者……
TryThreeTimes(() => DoSomethingElse(withLocalVariable));
一个更灵活的选择:
public void DoWithRetry(Action action, TimeSpan sleepPeriod, int tryCount = 3)
{
if (tryCount <= 0)
throw new ArgumentOutOfRangeException(nameof(tryCount));
while (true) {
try {
action();
break; // success!
} catch {
if (--tryCount == 0)
throw;
Thread.Sleep(sleepPeriod);
}
}
}
用作:
DoWithRetry(DoSomething, TimeSpan.FromSeconds(2), tryCount: 10);
支持async/await的更现代的版本:
public async Task DoWithRetryAsync(Func<Task> action, TimeSpan sleepPeriod, int tryCount = 3)
{
if (tryCount <= 0)
throw new ArgumentOutOfRangeException(nameof(tryCount));
while (true) {
try {
await action();
return; // success!
} catch {
if (--tryCount == 0)
throw;
await Task.Delay(sleepPeriod);
}
}
}
用作:
await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);
这可能是个坏主意。首先,这是一句格言的象征:“疯狂的定义是做同一件事两次,每次都期待不同的结果”。其次,这种编码模式本身不能很好地组合。例如:
假设您的网络硬件层在失败时重发一个数据包三次,在两次失败之间等待一秒钟。
现在假设软件层在包失败时重发关于失败的通知三次。
现在假设通知层在通知传递失败时重新激活通知三次。
现在假设错误报告层在通知失败时重新激活通知层三次。
现在假设web服务器在错误失败时重新激活错误报告三次。
现在假设web客户端在从服务器得到错误后重新发送请求三次。
现在假设网络交换机上用来将通知路由到管理员的线路被拔掉。web客户端的用户什么时候最终得到错误消息?我大约12分钟后到。
以免您认为这只是一个愚蠢的例子:我们已经在客户代码中看到了这个错误,尽管比我在这里描述的要严重得多。在特定的客户代码中,错误条件发生和最终报告给用户之间的间隔是几个星期,因为有很多层自动重试并等待。想象一下,如果是十次而不是三次,会发生什么。
通常正确的处理错误的方法是立即报告它,让用户决定怎么做。如果用户想要创建自动重试的策略,让他们在软件抽象的适当级别上创建该策略。