我正在尝试以编程方式跟踪重定向链(针对在线广告像素),但超时为 2 秒(换句话说,如果重定向链需要超过 2 秒才能解决,我希望它中止并返回 null)。
我的代码(或多或少)同步运行,所以我不得不做一些杂技来做我想做的事情,但从功能上讲,它似乎有效......超时部分除外。
我有一些异步助手,如下所示:
public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
{
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
if (completedTask != task)
{
throw new TimeoutException();
}
timeoutCancellationTokenSource.Cancel();
return await task;
}
}
public static T ToSynchronousResult<T>(this Task<T> task)
{
return Task.Run(async () => await task).Result;
}
TimeoutAfter()
帮助程序方法改编自可在此处找到的 SO 文章。 在我的服务中,我有一个类似于这样的方法:
public string GetFinalUrl(string url)
{
string finalUrl;
try
{
finalUrl = FollowDestinationUrl(url).TimeoutAfter(TimeSpan.FromSeconds(2)).ToSynchronousResult();
}
catch (TimeoutException)
{
finalUrl = null;
}
return finalUrl;
}
private async Task<string> FollowDestinationUrl(string url)
{
var request = _webRequestFactory.CreateGet(url);
var payload = await request.GetResponseAsync();
return payload.ResponseUri.ToString();
}
此处的_webRequestFactory
返回一个编写为IHttpRequest
的 HttpWebRequest 抽象。
在我的成功案例单元测试(响应不到 2 秒)中,我得到了我期望的结果:
private class TestWebResponse : WebResponse
{
public override Uri ResponseUri => new Uri("https://www.mytest.com/responseIsGood");
}
[TestMethod]
public void RedirectUriUnderTimeout()
{
//arrange
var service = GetService();
A.CallTo(() => _httpRequest.GetResponseAsync()).ReturnsLazily(() => new TestWebResponse());
A.CallTo(() => _httpRequest.GetResponseString())
.ReturnsLazily(() => VALID_REQUEST_PAYLOAD);
//act
var url = service.GetFinalUrl("https://someplace.com/testurl");
//assert
Assert.IsNotNull(url);
}
。但是,当我尝试实现延迟以验证超时是否正常工作时,它不会像我预期的那样中止:
[TestMethod]
public void RedirectUriUnderTimeout()
{
//arrange
var service = GetService();
A.CallTo(() => _httpRequest.GetResponseAsync()).ReturnsLazily(() => {
Thread.Sleep(TimeSpan.FromSeconds(3));
return new TestWebResponse();
});
A.CallTo(() => _httpRequest.GetResponseString())
.ReturnsLazily(() => VALID_REQUEST_PAYLOAD);
//act
var url = service.GetFinalUrl("https://someplace.com/testurl");
//assert
Assert.IsNull(url);
}
似乎它等待了整整三秒钟,然后返回具有非空ResponseUri
的TestWebResponse
。
我不知道我的实现是否有根本性的问题,或者我的测试是否有问题,但显然我以一种我意想不到的方式阻止了异步调用。
有人可以帮助我确定我做错了什么吗?
public static T ToSynchronousResult<T>(this Task<T> task)
{
return Task.Run(async () => await task).Result;
}
这部分会导致线程阻塞。正如您提到的方法ToSynchronousResult
,它将阻塞线程,直到返回任务结果。您应该遵循"一路异步">规则,并且应该使用await
。这是有效应用async
的唯一方法。
public async Task<string> GetFinalUrl(string url)
{
string finalUrl;
try
{
finalUrl = await FollowDestinationUrl(url).TimeoutAfter(TimeSpan.FromSeconds(2));
}
catch (TimeoutException)
{
finalUrl = null;
}
return finalUrl;
}
好吧,看起来我想多了。 @Stormcloak告诉我,我正在做的事情是行不通的,所以我开始寻找替代方案,我意识到虽然 async/await 模式在这里不合适,但 TPL 库仍然派上用场。
我将FinalDestinationUrl
方法更改为同步,如下所示:
private string FollowDestinationUrl(string url)
{
var request = _webRequestFactory.CreateGet(url);
var payload = request.GetResponse();
return payload.ResponseUri.ToString();
}
然后我这样称呼它:
var task = Task.Run(() => FollowDestinationUrl(destinationUrl));
finalUrl = task.Wait(TimeSpan.FromSeconds(2)) ? task.Result : null;
然后我更改了我的单元测试,类似于:
[TestMethod]
public void RedirectUriUnderTimeout()
{
//arrange
var service = GetService();
A.CallTo(() => _httpRequest.GetResponse()).ReturnsLazily(() => {
Thread.Sleep(TimeSpan.FromSeconds(3));
return new TestWebResponse();
});
A.CallTo(() => _httpRequest.GetResponseString())
.ReturnsLazily(() => VALID_REQUEST_PAYLOAD);
//act
var url = service.GetFinalUrl("https://someplace.com/testurl");
//assert
Assert.IsNull(url);
}
测试通过。 世界上一切都很好。 谢谢!