调整代码以使用 built-in RateLimit 政策

Adjusting code to use the built-in RateLimit policy

我想通过 built-in RateLimit policy 实现与以下策略相同的行为,即记录器消息并读取 Retry-After header 并等待需要等待但使用 built-in RateLimit 策略的确切秒数。

尝试

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

有效

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

有多个问题,让我一一回答。

1) 如何将记录器注入策略?

为此你需要使用 Polly's context

上下文是在策略之外创建的。它被用作存储任意信息的容器

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

然后通过Execute/ExecuteAsync调用传递

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

最后,您可以在任何用户委托(如 onRetry/onRetryAsync)中使用上下文来检索传递的 object

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

WithLoggerGetLogger 扩展方法

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) 上述限速器的工作方式是否与重试相同?

没有。速率限制器是一种 主动 策略,可用于防止资源滥用。这意味着如果超过预定义的限制,它将抛出 RateLimitRejectedException

每当我们谈论弹性策略时,我们指的是双方之间的预定义协议,以克服瞬态故障。所以限速器是这个故事的 server-side 而重试(反应策略)是 client-side.

如果你想在你的速率限制器中设置 RetryAfter header 那么你可以这样做

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

然后在重试的 sleepDurationProvider 委托中的 client-side 上,如果 responseDelegateResult<HttpResponseMessage>

,您可以像这样检索该值
response.Result.Headers.RetryAfter.Delta ?? TimeSpan.FromSeconds(0)