【问题标题】:Set a timeout on a redirect chain在重定向链上设置超时
【发布时间】:2018-06-27 22:00:25
【问题描述】:

我正在尝试以编程方式跟踪重定向链(对于在线广告像素),但超时时间为 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 文章,可在here 找到。在我的服务中,我有一个类似于这样的方法:

    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);
    }

它似乎等待了整整三秒钟,然后返回具有非空 ResponseUriTestWebResponse

我不知道我的实现是否存在根本问题,或者我的测试是否存在问题,但显然我正在以一种我不希望的方式阻止异步调用。

谁能帮我找出我做错了什么?

【问题讨论】:

    标签: c# asynchronous async-await timeout


    【解决方案1】:
    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;
    }
    

    【讨论】:

    • 我不能一直使用异步;我必须更改一半的代码库才能做到这一点。有其他选择吗?
    • 如果你不能一直使用异步,你希望如何一直执行你的代码非阻塞/异步?
    • 我希望有一个“中断”效果,因为它是......即,最多花两秒钟尝试然后保释。换言之,从流程流外部中止。我知道异步是可能的,所以我试图解决这个问题。
    • 作为另一个问题,抛出的异常不会“解除阻塞”线程吗?我想当这种情况发生时它应该立即返回,但我承认我的理解可能存在缺陷。
    【解决方案2】:

    好吧,看来我想多了。 @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);
    }
    

    测试通过了。世界上一切都很好。谢谢!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-05-25
      • 2010-09-28
      • 2014-08-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多