このバックオフ再試行の実装の鍵はdeferred observablesです。遅延オブザーバブルは、誰かがサブスクライブするまでファクトリを実行しません。また、サブスクリプションごとにファクトリが呼び出されるため、再試行のシナリオに最適です。
ネットワーク要求をトリガーするメソッドがあるとします。
public IObservable<WebResponse> SomeApiMethod() { ... }
この小さなスニペットの目的のために、遅延オブジェクトを次のように定義しましょう。source
var source = Observable.Defer(() => SomeApiMethod());
誰かがソースをサブスクライブするたびに、SomeApiMethod が呼び出され、新しい Web リクエストが開始されます。失敗したときに再試行する単純な方法は、組み込みの Retry 演算子を使用することです。
source.Retry(4)
しかし、それは API にとってあまり良いことではなく、あなたが求めているものでもありません。各試行の間にリクエストの開始を遅らせる必要があります。これを行う 1 つの方法は、遅延サブスクリプションを使用することです。
Observable.Defer(() => source.DelaySubscription(TimeSpan.FromSeconds(1))).Retry(4)
最初のリクエストでも遅延が追加されるため、これは理想的ではありません。修正しましょう。
int attempt = 0;
Observable.Defer(() => {
return ((++attempt == 1) ? source : source.DelaySubscription(TimeSpan.FromSeconds(1)))
})
.Retry(4)
.Select(response => ...)
ただし、1 秒間一時停止するだけでは、再試行の方法としてはあまり適切ではありません。この定数を、再試行カウントを受け取って適切な遅延を返す関数に変更しましょう。指数バックオフは実装が簡単です。
Func<int, TimeSpan> strategy = n => TimeSpan.FromSeconds(Math.Pow(n, 2));
((++attempt == 1) ? source : source.DelaySubscription(strategy(attempt - 1)))
再試行する例外を指定する方法を追加する必要があるだけです。再試行する意味があるかどうかにかかわらず、例外が返される関数を追加しましょう。これを retryOnError と呼びます。
ここで、恐ろしく見えるコードを書く必要がありますが、我慢してください。
Observable.Defer(() => {
return ((++attempt == 1) ? source : source.DelaySubscription(strategy(attempt - 1)))
.Select(item => new Tuple<bool, WebResponse, Exception>(true, item, null))
.Catch<Tuple<bool, WebResponse, Exception>, Exception>(e => retryOnError(e)
? Observable.Throw<Tuple<bool, WebResponse, Exception>>(e)
: Observable.Return(new Tuple<bool, WebResponse, Exception>(false, null, e)));
})
.Retry(retryCount)
.SelectMany(t => t.Item1
? Observable.Return(t.Item2)
: Observable.Throw<T>(t.Item3))
これらの山括弧はすべて、 を超えて再試行してはならない例外をマーシャリングするためにあり.Retry()
ます。内側のオブザーバブルをIObservable<Tuple<bool, WebResponse, Exception>>
、最初の bool が応答または例外があるかどうかを示す場所にしました。retryOnError が特定の例外を再試行する必要があることを示している場合、内部のオブザーバブルがスローされ、再試行によって取得されます。SelectMany はタプルをアンラップし、結果のオブザーバブルをIObservable<WebRequest>
再度作成します。
最終バージョンの完全なソースとテストを含む私の要点を参照してください。この演算子を使用すると、再試行コードを非常に簡潔に記述できます
Observable.Defer(() => SomApiMethod())
.RetryWithBackoffStrategy(
retryCount: 4,
retryOnError: e => e is ApiRetryWebException
)