Search code examples
c#rate-limitingpollyretry-logic

How to retry a Polly rate limit when the rate limit has exceeded?


I've got the following policy setup:

// Slack api for postMessage is generally 1 per channel per second.
// Some workspace specific limits may apply.
// We just limit everything to 1 second.
var slackApiRateLimitPerChannelPerSecond = 1;

var rateLimit = Policy.RateLimitAsync(slackApiRateLimitPerChannelPerSecond, TimeSpan.FromSeconds(slackApiRateLimitPerChannelPerSecond),
    (retryAfter, _) => retryAfter.Add(TimeSpan.FromSeconds(slackApiRateLimitPerChannelPerSecond)));

This should:

  • Rate limit requests till 1 req/s
  • Retry when rate limited

I can't wrap my head around wrapping this into a second policy that would retry...

I could retry this like so:

try
{
   _policy.Execute(...)
}
catch(RateLimitedException ex)
{
   // Policy.Retry with ex.RetryAfter
}

But that does not seem right.

I'd like to retry this a couple (3?) times so the method is abit more resilient - how would i do that?


Solution

  • You can omit the factory and wrap the rate-limiting policy into another one:

    var ts = TimeSpan.FromSeconds(1);
    var rateLimit = Policy.RateLimit(1, ts);
    var policyWrap = Policy.Handle<RateLimitRejectedException>()
        .WaitAndRetry(3, _ => ts) // note that you might want to use more advanced back off policy here 
        .Wrap(rateLimit);
    policyWrap.Execute(...);
    

    If you want to respect the returned RetryAfter then try-catch approach is way to go, based on the documentation example:

    public async Task SearchAsync(string query, HttpContext httpContext)
    {
        var rateLimit = Policy.RateLimitAsync(20, TimeSpan.FromSeconds(1), 10);
    
        try
        {
            var result = await rateLimit.ExecuteAsync(() => TextSearchAsync(query));
    
            var json = JsonConvert.SerializeObject(result);
    
            httpContext.Response.ContentType = "application/json";
            await httpContext.Response.WriteAsync(json);
        }
        catch (RateLimitRejectedException ex)
        {
            string retryAfter = DateTimeOffset.UtcNow
                .Add(ex.RetryAfter)
                .ToUnixTimeSeconds()
                .ToString(CultureInfo.InvariantCulture);
    
            httpContext.Response.StatusCode = 429;
            httpContext.Response.Headers["Retry-After"] = retryAfter;
        }
    }
    

    UPD

    There is WaitAndRetry overload with sleepDurationProvider which also passes the exception, so it can be used for the Wrap approach:

    var policyWrap = Policy.Handle<RateLimitRejectedException>()
        .WaitAndRetry(5, 
            sleepDurationProvider: (_, ex, _) => (ex as RateLimitRejectedException)?.RetryAfter.Add(TimeSpan.From....) ?? TimeSpan.From...,
            onRetry:(ex, _, i, _) => { Console.WriteLine($"retry: {i}"); }) 
        .Wrap(rateLimit);