【C#】Pollyを使用してリトライ処理を実装する

C#

はじめに

データベースや外部サービスへのアクセス中に通信エラーやタイムアウトが発生することはよくあります。そうした障害に対処するためには、リトライ処理が有効です。本記事では、C#でリトライ処理を実装するためにPollyというライブラリを使用する方法について解説します。

Pollyとは

Pollyは、.NET向けのポリシーベースのリソース制御ライブラリです。再試行、タイムアウト、断続的なバックオフなど、様々なポリシーを定義し、簡単にリトライ処理を組み込むことができます。

Pollyの導入

まずは、Pollyをプロジェクトに導入します。
NuGet パッケージ マネージャーコンソールで以下のコマンドを実行します。

リトライ処理の基本

Pollyを使用してリトライ処理を実装するには、以下の基本的な手順を踏みます。

  1. リトライポリシーの定義
  2. ポリシーでラップした処理の実行

この例では、RetryPolicy を使用して3回までリトライするポリシーを設定しています。例外が発生した場合には指数バックオフを適用しています。データベースアクセスのメソッドは AccessDatabaseAsync メソッド内に実装され、このメソッドが retryPolicy.ExecuteAsync でラップされています。

ポリシーの基本的な構成

ポリシーは例外をハンドリングして実行されるため、どの例外をハンドリングするかを指定する必要があります。
Handle の基本的な構文(<ExceptionType>: この部分には処理したい特定の例外の型が入ります。)

特定の例外に対するリトライポリシーの設定

Handle メソッドは、リトライポリシーを特定の例外に対して有効にするために使用されます。以下は、特定の例外に対して3回までリトライするポリシーの設定例です。

複数の例外のハンドリング

Handle メソッドは複数の例外型を指定できます。これにより、異なる種類の例外に対して同じポリシーを適用できます。

条件に基づいた例外のハンドリング

Handle メソッドには条件を指定して、特定の条件に合致する例外のみをハンドルすることもできます。

特定の例外をハンドルせず、残りをスルーする

Or<ExceptionType>() を使用して、特定の例外をハンドルした後、残りの例外をスルーすることもできます。

例外の型に基づいた制御フロー

ハンドルされる例外に応じて異なる処理を実行することができます。

ポリシーのカスタマイズ

Pollyでは、異なるタイプのポリシーが提供されており、それぞれ異なる用途に特化しています。以下に、いくつか代表的なポリシーの種類について説明します。

1. Retry Policy (リトライポリシー)

RetryPolicy は、特定の例外が発生した場合に処理を再試行するためのポリシーです。リトライのタイミングや条件、最大リトライ回数などを指定することができます。例外が発生したら指定された回数だけ処理を再試行します。

2. Wait and Retry Policy (待機とリトライポリシー)

WaitAndRetryPolicy は、リトライの間隔を設定できるポリシーです。リトライが発生すると、指数バックオフや固定の待機時間を指定してリトライを試行します。

3. Circuit Breaker Policy (サーキットブレーカーポリシー)

CircuitBreakerPolicy は、一時的な障害からアプリケーションを保護するためのポリシーです。指定した回数のリトライが連続して失敗した場合、サーキットがオープンし、一定の時間が経過するまでリトライが中止されます。

4. Fallback Policy (フォールバックポリシー)

FallbackPolicy は、指定されたデリゲートが例外をスローした場合に、代替の処理を実行するポリシーです。例外が発生した場合でも処理を続行するための柔軟性を提供します。

5. Bulkhead Isolation Policy (ブルクヘッドアイソレーションポリシー)

BulkheadIsolationPolicy は、アプリケーションの異なる部分を分離し、一部の障害が他の部分に影響を与えないようにするためのポリシーです。これにより、特定の部分での障害が全体に波及するのを防ぎます。

6. Timeout Policy (タイムアウトポリシー)

TimeoutPolicy は、処理が指定された時間内に完了しなかった場合に例外をスローするポリシーです。外部リソースに対する要求など、処理に時間制約がある場合に使用されます。

7. Cache Policy (キャッシュポリシー)

CachePolicy は、処理の結果をキャッシュし、同じ要求が再度行われた際にキャッシュから結果を取得するポリシーです。これにより、冪等性がある操作の再実行を避けることができます。

8. NoOp Policy (No Operationポリシー)

NoOpPolicy は、何も処理を行わないポリシーで、デバッグやテスト時に特定の条件でポリシーを無効にする際に使用されます。

9. Advanced Circuit Breaker Policy (高度なサーキットブレーカーポリシー)

AdvancedCircuitBreakerPolicy は、通常のサーキットブレーカーポリシーよりも高度な設定が可能なポリシーです。失敗率、サーキットの閉じる条件、リセット条件などを詳細に設定できます。

ポリシーの組み合わせ

ポリシーはアプリケーションの動作や要求に応じて、組み合わせて使用することができます。
以下に、いくつか代表的なポリシーの組み合わせについて説明します。

リトライとタイムアウトの組み合わせ

リトライとタイムアウトの組み合わせは、外部サービスやネットワーク通信などの操作に対して耐障害性を向上させる際によく使用されます。以下に、リトライとタイムアウトを組み合わせたポリシーの例を示します。

上記の例では、まず RetryPolicy が定義され、HttpRequestException が発生した場合に最大3回までリトライします。その後、 TimeoutPolicy が30秒以内に処理が完了しなかった場合に例外をスローします。PolicyWrap を使用してこれらのポリシーを組み合わせ、一つの実行単位として使用します。

ここで気を付けなければならない点として、 Policy.Wrap の指定順序があります。

PolicyWrapは指定されたデリゲートをレイヤーまたはラップを通じて実行します。

  • 最も外側 (読み取り順で最も左) のポリシーが次の内側を実行し、さらに次の内側が実行されます。最も内側のポリシーがユーザーデリゲートを実行するまで。
  • 例外は、レイヤーを介して (処理されるまで) 外側に戻ってきます。
https://github.com/App-vNext/Polly/wiki/PolicyWrap

公式のGitHubからの引用ですが、つまり Policy.Wrap では第1引数から順に適用される(優先される)ということです。

例えばですが、1回の処理のタイムアウトは30秒で、3回までリトライしたい。(つまり、最大で30秒の処理×3回で90秒かかる可能性がある)というユースケースの場合は下記の様にリトライ→タイムアウトの順で指定します。

これを下記の様にタイムアウト→リトライの順で指定した場合、タイムアウトが優先され、リトライも含めた処理時間全てに対してのタイムアウトという設定になります。

仮にタイムアウトが30秒、リトライが3回の場合、Executeで実行した処理が30秒で終わらなかった場合はタイムアウトで終了となりリトライされません。

リトライ毎にタイムアウトを設定したいのか、リトライ含めた処理全体にタイムアウトを設定したいのかを意識して設定しましょう。

タイムアウトとフォールバックの組み合わせ

タイムアウトに加えてフォールバックポリシーを組み合わせることも検討されます。タイムアウトが発生した場合、代替の処理を実行することで、ユーザーエクスペリエンスを向上させることができます。

Circuit BreakerとFallbackの組み合わせ

サードパーティのAPIへのリクエストがサービスの不安定性を引き起こす場合、Circuit Breaker で回路をオープンし、一時的にリクエストを中止し、Fallback で代替のデータを提供します。

BulkheadとTimeoutの組み合わせ

多くの並行リクエストがサービスを過負荷にする場合、Bulkhead で同時実行数を制限し、Timeout で処理が長時間かかる場合に中断します。

Retry、Circuit Breaker、Fallback、Timeoutの組み合わせ

複雑なシナリオに対応するために、複数のポリシーを組み合わせます。

TimeoutStrategy(タイムアウト戦略)

TimeoutPolicyにはOptimistic(楽観的タイムアウト)Pessimistic(悲観的タイムアウト)の2種類のタイムアウト戦略があります。

楽観的タイムアウト(Optimistic)

TimeoutPolicyに指定した時間を経過しても処理が完了しない場合、処理に渡したCancellationToken.IsCancellationRequestedがTrueになります。
そのため、実装者はCancellationToken.IsCancellationRequestedをチェックして処理の続行/中断を制御する必要があります。

悲観的タイムアウト(Pessimistic)

TimeoutPolicyに指定した時間を経過しても処理が完了しない場合、TimeoutRejectedExceptionがスローされます。
つまり、タイムアウトした時点で強制的に処理が中断されるということです。

ここで気を付ける点として、非同期実行時の制御があります。
下記のように非同期処理を悲観的タイムアウトで実行した場合、非同期処理が30秒経過しても終わらなかった場合はTimeoutRejectedExceptionがスローされて処理が強制終了しますが、httpClientの非同期処理自体がキャンセルされるわけではないです。

非同期処理でDBアクセス等適切に処理の終了を制御しなければならない場合は楽観的タイムアウトを使用します。

まとめ

Pollyを使用することで、C#プロジェクトで簡単かつ柔軟にリトライ処理を実装することができます。データベースアクセスや外部サービスへの通信など、信頼性の要求される処理において、Pollyは頼りになるツールとなります。是非、プロジェクトに導入してみてください。

C#プログラミング
凡人プログラマーのブログ

コメント