如何在Polly速率限制超过时重试?

3
我已经设置了以下策略:
// 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)));

这应该:

  • 将请求速率限制为1 req/s
  • 在速率受限时重试

我无法理解如何将其包装成第二个策略以进行重试...

我可以像这样重试:

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

但这似乎不正确。

我想再试几次(3次?),以使该方法更具弹性 - 我该怎么做?


你想让重试遵循速率限制器的retryAfter吗? - Peter Csala
@PeterCsala 是的 - sommmen
2个回答

5

我可能来晚了,但让我发表一下我的意见。

速率限制器

该策略引擎以无锁方式实现了令牌桶算法。这有一个含义,因此它的工作方式并不像您可能直觉地认为的那样

例如,从该策略的角度来看,每秒1个请求与每分钟60个请求是相同的。
实际上,后者不应该强制平均分配(但它确实这样做)! 因此,您不能像这样使用它:

  • 在前10秒内发出50个请求
  • 45秒没有任何请求
  • 在最后5秒内可以发出9个请求而不达到限制

共享策略的速率限制器

在Polly的情况下,大多数策略都是无状态的。这意味着两个执行不需要共享任何内容。

但是在断路器的情况下,控制器中有一个状态。因此,您应该在多个执行之间使用同一个实例。
在分隔器和速率限制器策略的情况下,状态并不那么明显。它们被隐藏在实现内部。但是同样的规则也适用于这里,您应该在多个线程之间共享相同的策略实例以达到期望的结果。
速率限制器本身可以同时在客户端和服务器端使用。服务器端可以主动拒绝过多的请求以减轻过载。而客户端可以主动自我限制发送的请求,以遵守服务器和客户端之间的协议。
这个策略更适合服务器端(请参阅RetryAfter属性)。在客户端,使用速率门实现可能更合适,它通过利用队列和定时器来延迟发送请求。
带重试的速率限制器

如果重试和速率限制器都在客户端上运行

var retryPolicy = Policy
    .Handle<RateLimitRejectedException>()
    .WaitAndRetry(
        3,
        (int _, Exception ex, Context __) => ((RateLimitRejectedException)ex).RetryAfter,
        (_, __, ___, ____) => { });

如果重试在客户端,而速率限制器在服务器端

var retryPolicy = Policy<HttpResponseMessage>
    .HandleResult(res => res.StatusCode == HttpStatusCode.TooManyRequests)
    .WaitAndRetry(
        3,
        (int _, DelegateResult<HttpResponseMessage> res, Context __)
            => res.Result.Headers.RetryAfter.Delta ?? TimeSpan.FromSeconds(0));

1
太棒了的回答 - undefined

2

您可以省略工厂,并将速率限制策略包装到另一个策略中:

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

如果您想要尊重返回的RetryAfter,那么根据文档示例,使用try-catch方法是可行的:

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

更新

有一个带有sleepDurationProviderWaitAndRetry重载,它还传递异常,因此可以用于Wrap方法:

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

我对 Polly 不太熟悉,.wrap() 是什么意思?它是说先限制速率,然后重试吗?这样它就会遵守速率限制吗? - sommmen
@sommen 是的,它会使用速率限制器并在失败时重试。 - Guru Stron
@sommmen 还要注意,retryAfterFactory 重载接受的是 Func<TimeSpan, Context, TResult>,即它应该与操作的结果相同。 - Guru Stron

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接