我试图为任意代码编写一个包装器,它会在给定的超时时间后取消(或至少停止等待)代码。
我有以下测试和实施
[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
Assert.Throws<TimeoutException>(async () => await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token));
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
和&#34; DoStuff&#34;方法
private static async Task DoStuff(int sleepTime, IFakeService fakeService)
{
await Task.Delay(sleepTime).ConfigureAwait(false);
var result = await Task.FromResult("bob");
var test = result + "test";
fakeService.DoStuff();
}
并执行IExecutionPolicy.ExecuteAsync
public async Task ExecuteAsync(Action action, CancellationToken token)
{
var cts = new CancellationTokenSource();//TODO: resolve ignoring the token we were given!
var task = _decoratedPolicy.ExecuteAsync(action, cts.Token);
cts.CancelAfter(_timeout);
try
{
await task.ConfigureAwait(false);
}
catch(OperationCanceledException err)
{
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms", err);
}
}
应该发生的是测试方法尝试采用> 3000ms并且超时应该在20ms发生,但这不会发生。为什么我的代码没有按预期超时?
编辑:
根据要求 - decoratedPolicy如下
public async Task ExecuteAsync(Action action, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.Factory.StartNew(action.Invoke, token);
}
答案 0 :(得分:4)
如果我理解正确,那么您正尝试为不支持超时/取消的方法支持超时。
通常,这是通过启动具有所需超时值的计时器来完成的。如果计时器首先触发,那么您可以抛出异常。使用TPL,您可以使用Task.Delay(_timeout)
而不是计时器。
public async Task ExecuteAsync(Action action, CancellationToken token)
{
var task = _decoratedPolicy.ExecuteAsync(action, token);
var completed = await Task.WhenAny(task, Task.Delay(_timeout));
if (completed != task)
{
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
}
}
注意:这不会停止执行_decoratedPolicy.ExecuteAsync
方法,而是忽略它。
如果您的方法支持取消(但不及时),则最好在超时后取消任务。您可以通过创建链接令牌来完成。
public async Task ExecuteAsync(Action action, CancellationToken token)
{
using(var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token))
{
var task = _decoratedPolicy.ExecuteAsync(action, linkedTokenSource.Token);
var completed = await Task.WhenAny(task, Task.Delay(_timeout));
if (completed != task)
{
linkedTokenSource.Cancel();//Try to cancel the method
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
}
}
}
答案 1 :(得分:2)
使用CancellationToken
表示您正在进行合作取消。设置CancellationTokenSource.CancelAfter
会在指定的时间后将基础标记转换为已取消状态,但如果该标记未被调用异步方法监视,则不会发生任何事情。
为了实际生成OperationCanceledException
,您需要在cts.Token.ThrowIfCancellationRequested
内拨打_decoratedPolicy.ExecuteAsync
。
例如:
// Assuming this is _decoratedPolicy.ExecuteAsync
public async Task ExecuteAsync(Action action, CancellationToken token)
{
// This is what creates and throws the OperationCanceledException
token.ThrowIfCancellationRequested();
// Simulate some work
await Task.Delay(20);
}
修改强>
为了实际取消令牌,您需要在执行工作的所有点监视它,并且执行可能会超时。如果你不能做出这种保证,那么请按照@SriramSakthivel回答实际Task
被丢弃的地方,而不是实际取消。
答案 2 :(得分:1)
您正在调用Assert.Throws(Action操作),并且您的匿名异步方法将转换为async void。该方法将与Fire&amp; Forget语义异步调用,而不会抛出异常。
然而,由于异步void方法中的未捕获异常,该过程很快就会崩溃。
您应该同步调用ExecuteAsync:
[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
Assert.Throws<AggregateException>(() => policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token).Wait());
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
或使用异步测试方法:
[Test]
public async Task Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
try
{
await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token);
Assert.Fail("Method did not timeout.");
}
catch (TimeoutException)
{ }
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
答案 3 :(得分:0)
我决定在这里回答我自己的问题,因为虽然列出的每个答案都解决了我需要做的事情,但他们没有确定这个问题的根本原因。很多,非常感谢:Scott Chamberlain,Yuval Itzchakov,Sriram Sakthivel,Jeff Cyr。感谢所有的建议。
根本原因/解决方案:
await Task.Factory.StartNew(action.Invoke, token);
你在我的“装饰策略”中看到的返回一个Task,并且await只等待外部任务。用
替换它await Task.Run(async () => await action.Invoke());
获得正确的结果。
我的代码受到C# async gotchas
上一篇优秀文章的 Gotcha#4 和 Gotcha#5 的影响。整篇文章(以及发给这个问题的答案)确实提高了我的整体理解。