-1

I am trying to trace a chain of redirects (for an online ad pixel) programmatically, but with a timeout of 2 seconds (in other words, if the redirect chain takes more than 2 seconds to resolve, I want it to abort and return null).

My code is (more or less) running synchronously, so I had to do some acrobatics to do what I wanted, but functionally speaking, it seems to work... except for the timeout part.

I have some asynchronous helpers like so:

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

The TimeoutAfter() helper method was adapted from the SO article that can be found here. In my service I have a method that resembles this:

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

The _webRequestFactory here returns an HttpWebRequest abstraction that was written as an IHttpRequest.

In my success case unit test (response under 2 seconds), I get back the result I expect:

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

...however, when I try to implement a delay to verify the timeout is working correctly, it's not aborting as I would expect:

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

It seems like it waits for the full three seconds, before returning the TestWebResponse that has a non-null ResponseUri.

I don't know if there's something fundamentally wrong with my implementation, or wrong with my test, but obviously I'm blocking an async call in a way I'm not expecting to.

Can someone help me identify what I've done wrong?

Jeremy Holovacs
  • 22,480
  • 33
  • 117
  • 254

2 Answers2

1
public static T ToSynchronousResult<T>(this Task<T> task)
{
    return Task.Run(async () => await task).Result;
}

This part causes to get thread blocked.As you mentioned the method ToSynchronousResult, it will block the thread until task result returned. You should follow "async all the way" rule and you should use await. It is only way to apply async efficiently.

public async Task<string> GetFinalUrl(string url)
{
    string finalUrl;

    try
    {
        finalUrl = await FollowDestinationUrl(url).TimeoutAfter(TimeSpan.FromSeconds(2));
    }
    catch (TimeoutException)
    {
        finalUrl = null;
    }

    return finalUrl;
}
lucky
  • 12,734
  • 4
  • 24
  • 46
  • I can't use async all the way; I'd have to change half the code base to do it. Is there an alternative? – Jeremy Holovacs Jan 18 '18 at 18:46
  • If you can't use async all the way, how would you expect to be executed your code non-blocked/async for all the way? – lucky Jan 18 '18 at 18:47
  • I was hoping for an "interrupt" effect, as it were... i.e., spend a maximum of two seconds trying then bail. In other words, abort from outside the process flow. I know it's possible to do with async, so I tried to get around the problem. – Jeremy Holovacs Jan 18 '18 at 18:48
  • As a further question, wouldn't the thrown exception "unblock" the thread? I'd imagine it should return immediately when that happens, but I acknowledge that my understanding could be flawed. – Jeremy Holovacs Jan 18 '18 at 18:52
0

OK, it looks like I was way overthinking it. @Stormcloak clued me in that what I was doing wasn't going to work, so I started looking at alternatives, and I realized that while the async/ await pattern weren't appropriate here, the TPL library still came in handy.

I changed my FinalDestinationUrl method to synchronous like so:

private string FollowDestinationUrl(string url)
    {
        var request = _webRequestFactory.CreateGet(url);
        var payload = request.GetResponse();
        return payload.ResponseUri.ToString();
    }

then I called it like so:

var task = Task.Run(() => FollowDestinationUrl(destinationUrl));
finalUrl = task.Wait(TimeSpan.FromSeconds(2)) ? task.Result : null;

Then I changed my unit test to resemble:

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

The test passed. All is well in the world. Thanks!

Jeremy Holovacs
  • 22,480
  • 33
  • 117
  • 254