我在.Net 4上,并有以下Async“Retry”包装器:
public static Task<T> Retry<T, TException>(Func<T> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount = 3) where TException : Exception
{
for (var i = 0; i < maxExecutionCount; i++)
{
try
{
return Task.Factory.StartNew(work);
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is TException)
{
// allow program to continue in this case
// do necessary logging or whatever
if (onException != null) { onException((TException)e); }
Thread.Sleep(retryInterval);
return true;
}
throw new RetryWrapperException("Unexpected exception occurred", ae);
});
}
}
var msg = "Retry unsuccessful after: {0} attempt(s)".FormatWith(maxExecutionCount);
throw new RetryWrapperException(msg);
}
我正在尝试使用:
[TestFixture]
public class RetryWrapperTest
{
private static int _counter;
private Func<string> _work;
private Action<InvalidOperationException> _onException;
private TimeSpan _retryInterval;
private int _maxRetryCount;
[TestFixtureSetUp]
public void SetUp()
{
_counter = 0;
_work = GetSampleResult;
_onException = e => Console.WriteLine("Caught the exception: {0}", e);
_retryInterval = TimeSpan.FromSeconds(5);
_maxRetryCount = 4;
}
[Test]
public void Run()
{
var resultTask = RetryWrapper.Retry(_work, _onException, _retryInterval, _maxRetryCount);
Console.WriteLine("This wrapper doesn't block!");
// Why is this line throwing? I expect the exception to be handled
// by the wrapper
var result = resultTask.Result;
result.Should().NotBeNullOrWhiteSpace();
result.Should().Be("Sample result!");
}
private static string GetSampleResult()
{
if (_counter < 3)
{
_counter++;
throw new InvalidOperationException("Baaah!");
}
return "Sample result!";
}
}
然而,抛出 AggregateException 而不是被捕获。使用这个包装器的全部意义是不必在var result = resultTask.Result;
周围放置try-catch我做错了什么?
请注意我不能使用async-await或Bcl.Async,因为我在.Net 4,VS 2010
答案 0 :(得分:4)
你想做的事并不容易。实际上,为了解决这类问题(Rackspace Threading Library,Apache 2.0许可证),我写了一个特别的库来“恰到好处”是非常具有挑战性的。
尽管代码的最终版本不会使用此功能,但使用async
/ await
最容易描述代码的所需行为。我对此Retry
方法进行了一些更改。
work
参数从Func<T>
更改为Func<Task<T>>
。Task.Delay
而不是Thread.Sleep
。onException
永远不会null
。虽然对于使用async
/ await
的代码而言,这种假设并不容易省略,但如果必要,可以为下面的实现解决。work
返回的任务进入Faulted
状态,它将具有InnerExceptions
属性,只包含1个异常。如果它包含多个异常,则将忽略除第一个异常之外的所有异常。虽然对于使用async
/ await
的代码而言,这种假设并不容易省略,但如果必要,可以为下面的实现解决。以下是最终的实现。请注意,我可以使用for
循环而不是while
循环,但正如您将看到的那样会使后续步骤复杂化。
public async Task<T> Retry<T, TException>(Func<Task<T>> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount)
where TException : Exception
{
int count = 0;
while (count < maxExecutionCount)
{
if (count > 0)
await Task.Delay(retryInterval);
count++;
try
{
return await work();
}
catch (TException ex)
{
onException(ex);
}
catch (Exception ex)
{
throw new RetryWrapperException("Unexpected exception occurred", ex);
}
}
string message = string.Format("Retry unsuccessful after: {0} attempt(s)", maxExecutionCount);
throw new RetryWrapperException(message);
}
将此代码转换为.NET 4.0(甚至是.NET 3.5)使用Rackspace线程库的以下功能:
TaskBlocks.While
:转换while
循环。CoreTaskExtensions.Select
:用于在先前任务成功完成后执行同步操作。CoreTaskExtensions.Then
:用于在先前任务成功完成后执行异步操作。CoreTaskExtensions.Catch
(V1.1的新内容):用于异常处理。DelayedTask.Delay
(V1.1的新内容):针对Task.Delay
。此实现与上述实现之间存在一些行为差异。特别是:
work
返回null
,此方法返回的Task
将转换为已取消的状态(如this test所示),由于NullReferenceException
,上述方法将转换为故障状态。ConfigureAwait(false)
之前调用await
一样。至少在我看来,这实际上并不是一件坏事。如果onException
方法抛出异常,则此实现会将该异常包装在RetryWrapperException
中。换句话说,此实现实际上是对此代码进行建模,而不是在期望行为部分中编写的块:
try
{
try
{
return await work();
}
catch (TException ex)
{
onException(ex);
}
}
catch (Exception ex)
{
throw new RetryWrapperException("Unexpected exception occurred", ex);
}
以下是最终的实施:
public static Task<T> Retry<T, TException>(Func<Task<T>> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount)
where TException : Exception
{
int count = 0;
bool haveResult = false;
T result = default(T);
Func<bool> condition = () => count < maxExecutionCount;
Func<Task> body =
() =>
{
Task t1 = count > 0 ? DelayedTask.Delay(retryInterval) : CompletedTask.Default;
Task t2 =
t1.Then(
_ =>
{
count++;
return work();
})
.Select(
task =>
{
result = task.Result;
haveResult = true;
});
Task t3 =
t2.Catch<TException>(
(_, ex) =>
{
onException(ex);
})
.Catch<Exception>((_, ex) =>
{
throw new RetryWrapperException("Unexpected exception occurred", ex);
});
return t3;
};
Func<Task, T> selector =
_ =>
{
if (haveResult)
return result;
string message = string.Format("Retry unsuccessful after: {0} attempt(s)", maxExecutionCount);
throw new RetryWrapperException(message);
};
return
TaskBlocks.While(condition, body)
.Select(selector);
}
以下测试方法演示了上述代码的上述代码。
[TestMethod]
public void Run()
{
Func<Task<string>> work = GetSampleResultAsync;
Action<InvalidOperationException> onException = e => Console.WriteLine("Caught the exception: {0}", e);
TimeSpan retryInterval = TimeSpan.FromSeconds(5);
int maxRetryCount = 4;
Task<string> resultTask = Retry(work, onException, retryInterval, maxRetryCount);
Console.WriteLine("This wrapper doesn't block");
var result = resultTask.Result;
Assert.IsFalse(string.IsNullOrWhiteSpace(result));
Assert.AreEqual("Sample result!", result);
}
private static int _counter;
private static Task<string> GetSampleResultAsync()
{
if (_counter < 3)
{
_counter++;
throw new InvalidOperationException("Baaah!");
}
return CompletedTask.FromResult("Sample result!");
}
如果您真的想要坚如磐石的实现,我建议您通过以下方式进一步修改代码。
支持取消。
CancellationToken cancellationToken
参数作为Retry
方法的最后一个参数。work
的类型更改为Func<CancellationToken, Task<T>>
。cancellationToken
参数传递给work
,然后传递给DelayedTask.Delay
。支持退避政策。您可以删除retryInterval
和maxExecutionCount
参数,然后使用IEnumerable<TimeSpan>
,或者可以合并IBackoffPolicy
之类的界面以及BackoffPolicy
等默认实现(麻省理工学院获得许可)。
答案 1 :(得分:1)
如果希望此包装器工作,则需要使用async / await或使用continuation编写catch块。这是因为在您尝试获取结果之前不会抛出异常。
要在.NET 4.0上使用async / await执行此操作,请下载Microsoft.Bcl.Async,它为您提供async / await支持4.0
public static async Task<T> Retry<T, TException>(Func<T> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount = 3) where TException : Exception
{
for (var i = 0; i < maxExecutionCount; i++)
{
try
{
return await Task.Factory.StartNew(work);
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is TException)
{
// allow program to continue in this case
// do necessary logging or whatever
if (onException != null) { onException((TException)e); }
Thread.Sleep(retryInterval);
return true;
}
throw new RetryWrapperException("Unexpected exception occurred", ae);
});
}
}
var msg = "Retry unsuccessful after: {0} attempt(s)".FormatWith(maxExecutionCount);
throw new RetryWrapperException(msg);
}
我不确定没有async / await的确切方法是什么。我知道.ContunueWith(
与TaskContinuationOptions.OnlyOnFaulted
有关,但我不确定具体细节。
答案 2 :(得分:0)
您的代码存在一些问题:
如前所述,您没有await
执行Task
,没有机会
完成并捕获异常,因为返回的Task
即时传播给调用者。如果await
不可用,则必须使用延续样式ContinueWith
方法而不是此实现。
您在代码中使用Thread.Sleep
。如你所知,有人可能会在他们的主线程中使用它,而不是理解为什么线程会突然卡住,这非常不可靠。
如果onException
委托是假的,则使用throw new
重新抛出异常,这将失去你方法的StackTrace,这是你真正追求的吗?一个更好的建议可能是保存它们并在重试结束时使用AggregationException
。
This post有你需要的东西:
public static Task StartNewDelayed(int millisecondsDelay, Action action)
{
// Validate arguments
if (millisecondsDelay < 0)
throw new ArgumentOutOfRangeException("millisecondsDelay");
if (action == null) throw new ArgumentNullException("action");
// Create the task
var t = new Task(action);
// Start a timer that will trigger it
var timer = new Timer(
_ => t.Start(), null, millisecondsDelay, Timeout.Infinite);
t.ContinueWith(_ => timer.Dispose());
return t;
}
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;
}