6

So, I've been working on an app that consumes REST API requests, however, for some reason, the API gets unresponsive randomly (sometimes it gives a response within 3 seconds and sometimes the request will take so long that it throws a TimeoutException) so Whenever I consume a call I use this code to restart the call if no response is retrieved during a certain amount of time:

bool taskCompletion = false;
while(taskCompletion == false)
{
    try
    {
        using (CancellationTokenSource cts = new CancellationTokenSource())
        {
            cts.CancelAfter(timeSpan);
            await task(cts.Token);
            taskCompletion = true;
        }
    }
    catch (OperationCanceledException)
    {
        taskCompletion = false;
    }
}

and one of my API requests is the following:

public static async Task<Result> task(CancellationToken ct)
{
    string Url = baseurl
    ApiHelper instance = new ApiHelper();

    using (HttpResponseMessage response = await instance.ApiClient.GetAsync(Url, ct))
    {
        if (response.IsSuccessStatusCode)
        {
            var x = await response.Content.ReadAsStringAsync();
            var result = JsonConvert.DeserializeObject<ResultL>(x);
            if (result.result.Count() != 0)
                return result.result[0];
            else
                return null;
        }
        return null;
    }
}

I don't think however that using the try-catch every time for each of the different API requests the code consumes is the best solution, any help on how to improve my code will be highly appreciated!

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
callencx
  • 63
  • 3

2 Answers2

3

Have you considered using a fault-resilience library? One example for .net is Polly. https://github.com/App-vNext/Polly

This is helpful because you can easily configure the retry count or the timeout as well as fallback logic for certain type of exceptions.

There is also a very helpful article by Scott Hanselman on this: https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx

I have used it before and it made my code super clean and easy to manage, since all policies are in one place, and not part of the http response handler. You can also have a separate policy for each different http requestor or client if needed.

mehtarit
  • 101
  • 1
  • 4
  • Great! after a brief look at the article it seems to be exactly what I need, I'll implement it shortly, thank you! – callencx May 28 '20 at 15:05
1

The whole premise of this problem is that an asynchronous operation that has become unresponsive, will still respond to a request for cancellation via the supplied CancellationToken. I am a bit skeptic about the real life applicability of this assumption, but in any case here is a AwaitCancelRetry method that automatically cancels and repeats an asynchronous operation, if it takes too long to complete:

public static async Task<T> AwaitCancelRetry<T>(
    Func<CancellationToken, Task<T>> function,
    TimeSpan timeout, int maxAttempts,
    CancellationToken externalToken = default)
{
    for (int i = 0; i < maxAttempts; i++)
    {
        using var linkedCts = CancellationTokenSource
            .CreateLinkedTokenSource(externalToken);
        linkedCts.CancelAfter(timeout);
        try
        {
            return await function(linkedCts.Token); // Continue on captured context
        }
        catch (OperationCanceledException)
            when (linkedCts.IsCancellationRequested
                && !externalToken.IsCancellationRequested)
        {
            continue; // Retry
        }
    }
    throw new TimeoutException();
}

// Non generic version
public static Task AwaitCancelRetry(
    Func<CancellationToken, Task> function,
    TimeSpan timeout, int maxAttempts,
    CancellationToken externalToken = default)
{
    return AwaitCancelRetry<object>(
        async ct => { await function(ct).ConfigureAwait(false); return null; },
        timeout, maxAttempts, externalToken);
}

Usage example:

private static HttpClient _httpClient;

public static Task<string> GetDataAsync(string url, CancellationToken token)
{
    return AwaitCancelRetry(async ct =>
    {
        using (HttpResponseMessage response = await _httpClient.GetAsync(url, ct))
        {
            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            return null;
        }
    }, TimeSpan.FromSeconds(10), maxAttempts: 3, token);
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104