簡體   English   中英

Polly.Contrib.WaitAndRetry 在達到速率限制時“匯集”所有請求

[英]Polly.Contrib.WaitAndRetry to "funnel" all requests when hitting rate limit

我們使用 Polly 中的 Dropbox API 來處理重試。
我們將其設置為指數回退,就像這里解釋的那樣。

我們遇到的問題是我們進行了大量的並發調用。
當 API 開始拋出速率限制異常時,每個單獨的調用者都會退出,但新的調用者仍會調用 API 並“竊取”正在等待的調用者的重試。
這意味着在高負載時,我們會遇到失敗的 API 調用和錯誤。

我們想要實現的是,在速率限制錯誤時,對 API 的所有調用(包括新調用者)都會同步並等待速率限制到期。
然后可以恢復調用(理想情況下按順序確保調用不再返回速率限制異常)。

是否有 Polly 支持的方法來實現這一目標?

根據我的理解,您希望擁有以下內容:

  1. 下游系統可以限制傳入的請求
    1.1 系統足夠智能,可以提供一個RetryAfter時間跨度
  2. 如果您已經知道自己受到限制,您希望避免淹沒下游系統
  3. 但是您不想丟失任何傳入的請求,而是希望最終處理所有請求

讓我們整理一個工作示例

#1 - 下游系統

在這里,我們將實現一個超級簡單的模擬,可以模擬節流。

讓我們從異常開始

public class DownstreamServiceException: Exception
{
    public TimeSpan RetryAfter { get; set; }
}

現在,讓我們看看服務代碼

public class DownstreamService
{
    private readonly CancellationTokenSource initCompletionSignal;
    private readonly TimeSpan initDuration;
    private bool isAvailable = false;
    private DateTime initEstimatedEnd;

    public DownstreamService()
    {
        initDuration = TimeSpan.FromSeconds(10);
        initCompletionSignal = new CancellationTokenSource(initDuration);
        initCompletionSignal.Token.Register(() => isAvailable = true);
        initEstimatedEnd = DateTime.UtcNow.Add(initDuration);
    }

    public Task<string> GetAsync()
    {
        if (!isAvailable) throw new DownstreamServiceException { RetryAfter = initEstimatedEnd - DateTime.UtcNow };
        return Task.FromResult("Available");
    }
}
  • 為了簡單起見,我使用了在前 10 秒內使服務不可用
  • 我使用CancellationTokenSource作為計時器來使服務可用
  • 如果GetAsync在不可用時被調用(我們被限制),它會返回異常,否則返回"Available"字符串

#2 - 避免洪水下游不可用

在這里,我們將定義一個斷路器來在下游不可用時縮短請求(我們被限制了)

var throttledPolicy = Policy<string>
    .Handle<DownstreamServiceException>()
    .CircuitBreakerAsync(1, TimeSpan.FromSeconds(0),
        onBreak: (result, state, _, __) => {
            if (state == CircuitState.Open) return;
            Console.WriteLine("onBreak");
            throw result.Exception;
        },
        onReset: (_) => Console.WriteLine("onReset"),
        onHalfOpen: () => { });
  • 當我們收到第一個DownstreamServiceException時,Circuit Breaker 將從Closed轉換為Open
  • 休息的持續時間( TimeSpan.FromSeconds(0) )在這里無關緊要
    • 我們將從重試邏輯控制斷路器的狀態
  • if (state == CircuitState.Open) :這將在重試部分解釋
  • 最后重新拋出原始異常(我知道,我知道......應該避免,但它使我們的示例應用程序保持簡單)

#3 - 重試直到最終處理

這是解決方案中最復雜的部分,因為此重試策略以不同的方式處理多個異常( DownstreamServiceExceptionIsolatedCircuitException

CancellationTokenSource throttlingEndSignal;
var retryPolicy = Policy<string>
    .Handle<DownstreamServiceException>()
    .Or<IsolatedCircuitException>()
    .WaitAndRetryForeverAsync(_ => TimeSpan.FromSeconds(3),
        onRetry: (dr, __) =>
        {
            Console.WriteLine($"onRetry caused by {dr.Exception.GetType().Name}");
            if (dr.Exception is DownstreamServiceException dse)
            {
                throttledPolicy.Isolate();
                throttlingEndSignal = new(dse.RetryAfter);
                throttlingEndSignal.Token.Register(() => throttledPolicy.Reset());
            }
        });
  • 讓我們從DownstreamServiceException開始
    • 我們將收到此異常,因為我們要將兩個策略鏈接在一起,並且 Circuit Breaker 的onBreak委托重新拋出收到的異常
    • onRetry內部,我們有一個DownstreamServiceException的保護表達式
    • 這里我們調用了斷路器上的Isolate ,它試圖從打開狀態轉換到隔離狀態 >> 調用onBreak委托
    • 為了避免無限循環,這就是為什么我們有這個if (state == CircuitState.Open) return; 那里的代碼
    • 我們在這里使用CancellationTokenSource執行相同的計時器技巧,當節流結束時,我們將斷路器推回關閉狀態( Reset
  • IsolatedCircuitException的情況要簡單得多
    • 每當我們嘗試執行重試嘗試但斷路器處於隔離狀態時,我們都會收到此異常
    • 所以,CB 縮短了執行,因為 WaitAndRetry Forever調用我們最終會成功

把東西放在一起

var combinedPolicy = Policy.WrapAsync(retryPolicy, throttledPolicy);

var result = await combinedPolicy.ExecuteAsync(async () => await service.GetAsync());

請注意以下事項:

  • 該解決方案也適用於多個請求,因為斷路器是共享的
  • 這個解決方案是一種變通方法,因為我們不能動態設置中斷的持續時間

我希望您發現這個小示例應用程序很有用 :)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM