1

I want to achieve the same behavior as the policy below with the built-in RateLimit policy, i.e. the logger message and to read the Retry-After header and wait the exact seconds that were needed to wait for but using the built-in RateLimit policy.

Attempt

// TODO: No logger message and not sure if it waits the time taken from the Retry-After header.
public static AsyncRateLimitPolicy Limit<T>(ILogger<T> logger)
{
    return Policy.RateLimitAsync(RateLimitRetryCount, TimeSpan.FromSeconds(5));
}

Works

public static AsyncRetryPolicy<RestResponse> AsyncRateLimit<T>(ILogger<T> logger)
{
    return Policy.HandleResult<RestResponse>(response => response.StatusCode == HttpStatusCode.TooManyRequests)
        .WaitAndRetryAsync(RateLimitRetryCount,
            (attemptCount, restResponse, _) =>
            {
                var retryAfterHeader = restResponse?.Result?.Headers?.SingleOrDefault(h => h.Name == "Retry-After");
                double secondsInterval = 0;

                if (retryAfterHeader != null)
                {
                    var value = retryAfterHeader.Value?.ToString();
                    if (!double.TryParse(value, out secondsInterval))
                    {
                        secondsInterval = Math.Pow(2, attemptCount);
                    }
                }

                return TimeSpan.FromSeconds(secondsInterval);
            },
            (response, timeSpan, retryCount, _) =>
            {
                logger.LogTrace(
                    "The API request has been rate limited. HttpStatusCode={StatusCode}. Waiting {Seconds} seconds before retry. Number attempt {RetryCount}. Uri={Url}; RequestResponse={Content}",
                    response.Result.StatusCode, timeSpan.TotalSeconds, retryCount, response.Result.ResponseUri, response.Result.Content);

                return Task.CompletedTask;
            });
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
nop
  • 4,711
  • 6
  • 32
  • 93
  • What is your question? – Peter Csala May 27 '22 at 13:02
  • @PeterCsala, hey, the question is how do I add the same logger.LogTrace message to the `RateLimitAsync` one and does it actually wait for the time given in Retry-After header? For ex. if the Retry-After says 5 seconds until the limit is gone, then it should wait exactly 5 seconds and retry. – nop May 27 '22 at 13:33
  • You can pass the logger to the policy via `Context`. There was recently an [SO topic](https://stackoverflow.com/questions/72370383/using-an-ilogger-in-a-polly-policy-attached-to-a-refit-client) about it. – Peter Csala May 27 '22 at 14:21
  • @PeterCsala, thanks. I don't quite understand how to pass the logger through `Context`. I'm not using ASP.NET Core btw. If it's possible, could you write an example for it too? – nop May 27 '22 at 14:36
  • 1
    https://github.com/App-vNext/Polly/wiki/Keys-and-Context-Data#example-varying-the-ilogger-used-within-an-onretry-delegate – Peter Csala May 27 '22 at 14:41
  • *if the Retry-After says 5 seconds until the limit is gone, then it should wait exactly 5 seconds and retry* >> `restResponse?.Result?.Headers?.RetryAfter.Delta ?? TimeSpan.FromSeconds(0)`?? – Peter Csala May 27 '22 at 14:45
  • 1
    @PeterCsala, I think I got the context part working: https://pastebin.com/7YMY7XTN – nop May 27 '22 at 14:50
  • Did my proposed solution for retrieving RetryAfter work for you? – Peter Csala May 28 '22 at 07:34
  • @PeterCsala, the second example (`AsyncRateLimit`) works fine. Btw, `restResponse` is `RestResponse` from RestSharp. I wondered if there was a way to achieve the same logic but using the first example with the built-in `Policy.RateLimitAsync` – nop May 28 '22 at 07:51
  • Are you asking how to set the RetryAfter header if the ratelimiter exceeds the limit? – Peter Csala May 28 '22 at 07:59
  • @PeterCsala, I'm asking if `Policy.RateLimitAsync(RateLimitRetryCount, TimeSpan.FromSeconds(5))` has same behavior – nop May 28 '22 at 11:16
  • 1
    No, it does not. It limits the number of executions and throws an exception if the limit is exceeded and that's all what it does. – Peter Csala May 28 '22 at 11:33
  • @PeterCsala, thank you, you can answer that, so I can accept :) – nop May 28 '22 at 11:42
  • 1
    I'll do that on Monday. But I think you misunderstand the concept of rate limiter. Rate limiter is a proactive policy to prevent resource abuse. In case of REST Api it can return 429 with a RetryAfter header. So, it is a server-side policy, whereas your retry is the client-side part of the story. – Peter Csala May 28 '22 at 12:17

1 Answers1

3

There were multiple questions so let me answer all of them.

1) How to inject logger to a Policy?

You need to use Polly's context for that.

The context is created outside of the policy. It is used as a container to store any arbitrary information

var context = new Context().WithLogger(logger);

Then it is passed through the Execute/ExecuteAsync call

await policy.ExecuteAsync(ctx => FooAsync(), context);

Finally you can use the context in any user delegate (like onRetry/onRetryAsync) to retrieve the passed object

(exception, timeSpan, retryCount, context) =>
{
  var logger = context.GetLogger();
  logger?.LogWarning(...);
  ...
}

The WithLogger and GetLogger extension methods

public static class ContextExtensions
{
    private static readonly string LoggerKey = "LoggerKey";

    public static Context WithLogger(this Context context, ILogger logger)
    {
        context[LoggerKey] = logger;
        return context;
    }

    public static ILogger GetLogger(this Context context)
    {
        if (context.TryGetValue(LoggerKey, out object logger))
        {
            return logger as ILogger;
        }
        return null;
    }
}

2) Does the above rate limiter work in the same way as the retry?

No. The rate limiter is a proactive policy which can be useful to prevent resource abuse. That means it will throw an RateLimitRejectedException if the predefined limit is exceeded.

Whenever we are talking about resilience strategy we are referring to a predefined protocol between the two parties to overcome on transient failures. So the rate limiter is the server-side of this story whereas the retry (reactive policy) is the client-side.

If you want to set the RetryAfter header in your rate limiter then you can do that like this

IAsyncPolicy<HttpResponseMessage> limit = Policy
    .RateLimitAsync(RateLimitRetryCount, TimeSpan.FromSeconds(5), RateLimitRetryCount,
        (retryAfter, context) => {
            var response = new HttpResponseMessage(System.Net.HttpStatusCode.TooManyRequests);
            response.Headers.Add("Retry-After", retryAfter.TotalSeconds.ToString());
            return response;
        });

Then on the client-side inside your retry's sleepDurationProvider delegate you can retrieve that value like this if the response is a DelegateResult<HttpResponseMessage>

response.Result.Headers.RetryAfter.Delta ?? TimeSpan.FromSeconds(0)
Peter Csala
  • 17,736
  • 16
  • 35
  • 75