- はじめに
- Pollyとは
- Pollyの導入
- リトライ処理の基本
- ポリシーの基本的な構成
- ポリシーのカスタマイズ
- 1. Retry Policy (リトライポリシー)
- 2. Wait and Retry Policy (待機とリトライポリシー)
- 3. Circuit Breaker Policy (サーキットブレーカーポリシー)
- 4. Fallback Policy (フォールバックポリシー)
- 5. Bulkhead Isolation Policy (ブルクヘッドアイソレーションポリシー)
- 6. Timeout Policy (タイムアウトポリシー)
- 7. Cache Policy (キャッシュポリシー)
- 8. NoOp Policy (No Operationポリシー)
- 9. Advanced Circuit Breaker Policy (高度なサーキットブレーカーポリシー)
- ポリシーの組み合わせ
- TimeoutStrategy(タイムアウト戦略)
- まとめ
はじめに
データベースや外部サービスへのアクセス中に通信エラーやタイムアウトが発生することはよくあります。そうした障害に対処するためには、リトライ処理が有効です。本記事では、C#でリトライ処理を実装するためにPollyというライブラリを使用する方法について解説します。
Pollyとは
Pollyは、.NET向けのポリシーベースのリソース制御ライブラリです。再試行、タイムアウト、断続的なバックオフなど、様々なポリシーを定義し、簡単にリトライ処理を組み込むことができます。
Pollyの導入
まずは、Pollyをプロジェクトに導入します。
NuGet パッケージ マネージャーコンソールで以下のコマンドを実行します。
1 |
Install-Package Polly |
リトライ処理の基本
Pollyを使用してリトライ処理を実装するには、以下の基本的な手順を踏みます。
- リトライポリシーの定義
- ポリシーでラップした処理の実行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using System; using System.Threading.Tasks; using Polly; using Polly.Retry; class Program { static async Task Main(string[] args) { // データベース接続文字列 string connectionString = "your_database_connection_string"; // Pollyのリトライポリシーを設定 RetryPolicy retryPolicy = Policy .Handle<Exception>() // 例外が発生したらリトライ対象とする .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); // Pollyでラップされたデータベースアクセスメソッドを呼び出し await retryPolicy.ExecuteAsync(async () => { await AccessDatabaseAsync(connectionString); }); } static async Task AccessDatabaseAsync(string connectionString) { // ここにデータベースアクセスのロジックを実装 // 例: Entity Framework Core を使用したコード // using var context = new YourDbContext(connectionString); // var result = await context.YourTable.FirstOrDefaultAsync(); // 何かしらのデータベースアクセスコードを実装 Console.WriteLine("データベースアクセス成功"); } } |
この例では、RetryPolicy
を使用して3回までリトライするポリシーを設定しています。例外が発生した場合には指数バックオフを適用しています。データベースアクセスのメソッドは AccessDatabaseAsync
メソッド内に実装され、このメソッドが retryPolicy.ExecuteAsync
でラップされています。
ポリシーの基本的な構成
ポリシーは例外をハンドリングして実行されるため、どの例外をハンドリングするかを指定する必要があります。Handle
の基本的な構文(<ExceptionType>
: この部分には処理したい特定の例外の型が入ります。)
1 |
Policy.Handle<ExceptionType>() |
特定の例外に対するリトライポリシーの設定
Handle
メソッドは、リトライポリシーを特定の例外に対して有効にするために使用されます。以下は、特定の例外に対して3回までリトライするポリシーの設定例です。
1 2 |
Policy.Handle<HttpRequestException>() .Retry(3); |
複数の例外のハンドリング
Handle
メソッドは複数の例外型を指定できます。これにより、異なる種類の例外に対して同じポリシーを適用できます。
1 2 3 4 |
Policy.Handle<HttpRequestException>() .Or<TimeoutException>() .Retry(3); |
条件に基づいた例外のハンドリング
Handle
メソッドには条件を指定して、特定の条件に合致する例外のみをハンドルすることもできます。
1 2 3 |
Policy.Handle<Exception>(ex => ex.Message.Contains("specific message")) .Retry(3); |
特定の例外をハンドルせず、残りをスルーする
Or<ExceptionType>()
を使用して、特定の例外をハンドルした後、残りの例外をスルーすることもできます。
1 2 3 4 5 6 7 8 9 |
Policy.Handle<HttpRequestException>() .Or<TimeoutException>() .Retry(3) .Or<Exception>() // この行以降の例外はリトライされずにスルーされる .Execute(() => { // リトライされるか、もしくはスルーされる処理 }); |
例外の型に基づいた制御フロー
ハンドルされる例外に応じて異なる処理を実行することができます。
1 2 3 4 5 6 7 8 9 10 |
Policy.Handle<HttpRequestException>() .Retry(3, (exception, retryCount) => { // 特定の例外が発生した場合の処理 }) .Execute(() => { // リトライされるか、もしくはスルーされる処理 }); |
ポリシーのカスタマイズ
Pollyでは、異なるタイプのポリシーが提供されており、それぞれ異なる用途に特化しています。以下に、いくつか代表的なポリシーの種類について説明します。
1. Retry Policy (リトライポリシー)
RetryPolicy
は、特定の例外が発生した場合に処理を再試行するためのポリシーです。リトライのタイミングや条件、最大リトライ回数などを指定することができます。例外が発生したら指定された回数だけ処理を再試行します。
1 2 3 |
RetryPolicy retryPolicy = Policy .Handle<SqlException>() .Retry(3); // 最大3回までリトライ |
2. Wait and Retry Policy (待機とリトライポリシー)
WaitAndRetryPolicy
は、リトライの間隔を設定できるポリシーです。リトライが発生すると、指数バックオフや固定の待機時間を指定してリトライを試行します。
1 2 3 4 |
RetryPolicy retryPolicy = Policy .Handle<SqlException>() .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); |
3. Circuit Breaker Policy (サーキットブレーカーポリシー)
CircuitBreakerPolicy
は、一時的な障害からアプリケーションを保護するためのポリシーです。指定した回数のリトライが連続して失敗した場合、サーキットがオープンし、一定の時間が経過するまでリトライが中止されます。
1 2 3 4 |
CircuitBreakerPolicy circuitBreakerPolicy = Policy .Handle<SqlException>() .CircuitBreaker(3, TimeSpan.FromSeconds(30)); // 失敗が3回連続で発生した場合、30秒間回復を試みない |
4. Fallback Policy (フォールバックポリシー)
FallbackPolicy
は、指定されたデリゲートが例外をスローした場合に、代替の処理を実行するポリシーです。例外が発生した場合でも処理を続行するための柔軟性を提供します。
1 2 3 4 |
FallbackPolicy fallbackPolicy = Policy .Handle<SqlException>() .Fallback(() => Console.WriteLine("代替の処理を実行")); |
5. Bulkhead Isolation Policy (ブルクヘッドアイソレーションポリシー)
BulkheadIsolationPolicy
は、アプリケーションの異なる部分を分離し、一部の障害が他の部分に影響を与えないようにするためのポリシーです。これにより、特定の部分での障害が全体に波及するのを防ぎます。
1 2 3 |
BulkheadPolicy bulkheadPolicy = Policy .Bulkhead(10); // 同時に実行できる操作の最大数を10に制限 |
6. Timeout Policy (タイムアウトポリシー)
TimeoutPolicy
は、処理が指定された時間内に完了しなかった場合に例外をスローするポリシーです。外部リソースに対する要求など、処理に時間制約がある場合に使用されます。
1 2 |
TimeoutPolicy timeoutPolicy = Policy.Timeout(30); // 30秒以内に処理が完了しなければ例外をスロー |
7. Cache Policy (キャッシュポリシー)
CachePolicy
は、処理の結果をキャッシュし、同じ要求が再度行われた際にキャッシュから結果を取得するポリシーです。これにより、冪等性がある操作の再実行を避けることができます。
1 2 |
CachePolicy cachePolicy = Policy.Cache(30); // 30秒間キャッシュされる |
8. NoOp Policy (No Operationポリシー)
NoOpPolicy
は、何も処理を行わないポリシーで、デバッグやテスト時に特定の条件でポリシーを無効にする際に使用されます。
1 2 |
NoOpPolicy noOpPolicy = Policy.NoOp(); |
9. Advanced Circuit Breaker Policy (高度なサーキットブレーカーポリシー)
AdvancedCircuitBreakerPolicy
は、通常のサーキットブレーカーポリシーよりも高度な設定が可能なポリシーです。失敗率、サーキットの閉じる条件、リセット条件などを詳細に設定できます。
1 2 3 4 5 6 7 8 9 |
AdvancedCircuitBreakerPolicy circuitBreakerPolicy = Policy .Handle<SqlException>() .AdvancedCircuitBreaker( failureThreshold: 0.5, // 失敗率が50%を超えたらサーキットをオープン samplingDuration: TimeSpan.FromSeconds(30), // 検査期間 minimumThroughput: 10, // 最低10回以上の呼び出しがある場合のみ考慮 durationOfBreak: TimeSpan.FromSeconds(30) // サーキットがオープン状態からクローズ状態に戻るまでの期間 ); |
ポリシーの組み合わせ
ポリシーはアプリケーションの動作や要求に応じて、組み合わせて使用することができます。
以下に、いくつか代表的なポリシーの組み合わせについて説明します。
リトライとタイムアウトの組み合わせ
リトライとタイムアウトの組み合わせは、外部サービスやネットワーク通信などの操作に対して耐障害性を向上させる際によく使用されます。以下に、リトライとタイムアウトを組み合わせたポリシーの例を示します。
1 2 3 4 5 6 7 8 |
RetryPolicy retryPolicy = Policy .Handle<HttpRequestException>() // リトライ対象の例外を指定 .Retry(3); // 最大3回までリトライ TimeoutPolicy timeoutPolicy = Policy.Timeout(30); // 30秒以内に処理が完了しなければ例外をスロー PolicyWrap policyWrap = Policy.Wrap(retryPolicy, timeoutPolicy); |
上記の例では、まず 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秒かかる可能性がある)というユースケースの場合は下記の様にリトライ→タイムアウトの順で指定します。
1 |
PolicyWrap policyWrap = Policy.Wrap(retryPolicy, timeoutPolicy); |
これを下記の様にタイムアウト→リトライの順で指定した場合、タイムアウトが優先され、リトライも含めた処理時間全てに対してのタイムアウトという設定になります。
1 |
PolicyWrap policyWrap = Policy.Wrap(timeoutPolicy,retryPolicy); |
仮にタイムアウトが30秒、リトライが3回の場合、Executeで実行した処理が30秒で終わらなかった場合はタイムアウトで終了となりリトライされません。
リトライ毎にタイムアウトを設定したいのか、リトライ含めた処理全体にタイムアウトを設定したいのかを意識して設定しましょう。
タイムアウトとフォールバックの組み合わせ
タイムアウトに加えてフォールバックポリシーを組み合わせることも検討されます。タイムアウトが発生した場合、代替の処理を実行することで、ユーザーエクスペリエンスを向上させることができます。
1 2 3 4 5 6 7 8 |
TimeoutPolicy timeoutPolicy = Policy.Timeout(30); FallbackPolicy fallbackPolicy = Policy .Handle<TimeoutRejectedException>() // タイムアウト例外のハンドリング .Fallback(() => Console.WriteLine("処理がタイムアウトしました")); PolicyWrap policyWrap = Policy.Wrap(timeoutPolicy, fallbackPolicy); |
Circuit BreakerとFallbackの組み合わせ
サードパーティのAPIへのリクエストがサービスの不安定性を引き起こす場合、Circuit Breaker
で回路をオープンし、一時的にリクエストを中止し、Fallback
で代替のデータを提供します。
1 2 3 4 |
CircuitBreakerPolicy circuitBreakerPolicy = Policy.Handle<Exception>().CircuitBreaker(3, TimeSpan.FromSeconds(30)); FallbackPolicy fallbackPolicy = Policy.Handle<BrokenCircuitException>().Fallback(() => ProvideFallbackData()); PolicyWrap policyWrap = Policy.Wrap(circuitBreakerPolicy, fallbackPolicy); |
BulkheadとTimeoutの組み合わせ
多くの並行リクエストがサービスを過負荷にする場合、Bulkhead
で同時実行数を制限し、Timeout
で処理が長時間かかる場合に中断します。
1 2 3 4 |
BulkheadPolicy bulkheadPolicy = Policy.Bulkhead(10); TimeoutPolicy timeoutPolicy = Policy.Timeout(20); PolicyWrap policyWrap = Policy.Wrap(bulkheadPolicy, timeoutPolicy); |
Retry、Circuit Breaker、Fallback、Timeoutの組み合わせ
複雑なシナリオに対応するために、複数のポリシーを組み合わせます。
1 2 3 4 5 |
RetryPolicy retryPolicy = Policy.Handle<Exception>().Retry(3); CircuitBreakerPolicy circuitBreakerPolicy = Policy.Handle<Exception>().CircuitBreaker(3, TimeSpan.FromSeconds(30)); FallbackPolicy fallbackPolicy = Policy.Handle<BrokenCircuitException>().Fallback(() => ProvideFallbackData()); TimeoutPolicy timeoutPolicy = Policy.Timeout(30); PolicyWrap policyWrap = Policy.Wrap(retryPolicy, circuitBreakerPolicy, fallbackPolicy, timeoutPolicy); |
TimeoutStrategy(タイムアウト戦略)
TimeoutPolicy
にはOptimistic(楽観的タイムアウト)
とPessimistic(悲観的タイムアウト)
の2種類のタイムアウト戦略があります。
楽観的タイムアウト(Optimistic)
TimeoutPolicy
に指定した時間を経過しても処理が完了しない場合、処理に渡したCancellationToken.IsCancellationRequested
がTrueになります。
そのため、実装者はCancellationToken.IsCancellationRequested
をチェックして処理の続行/中断を制御する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// CancellationTokenを介してタイムアウトが発生したか判定する CancellationTokenSource userCancellationSource = new CancellationTokenSource(); IAsyncPolicy timeoutPolicy = Policy.TimeoutAsync(30, TimeoutStrategy.Optimistic); HttpResponseMessage httpResponse = await timeoutPolicy .ExecuteAsync( async ct => { ~~~ // タイムアウトしている場合は処理を終了する ct.ThrowIfCancellationRequested(); ~~~ }, userCancellationSource.Token ); |
1 2 3 4 5 6 |
// CancellationTokenを介して非同期処理をキャンセルする IAsyncPolicy timeoutPolicy = Policy.TimeoutAsync(30, TimeoutStrategy.Optimistic); HttpResponseMessage httpResponse = await timeoutPolicy .ExecuteAsync( async ct => await httpClient.GetAsync(requestEndpoint, ct), CancellationToken.None); |
悲観的タイムアウト(Pessimistic)
TimeoutPolicy
に指定した時間を経過しても処理が完了しない場合、TimeoutRejectedException
がスローされます。
つまり、タイムアウトした時点で強制的に処理が中断されるということです。
ここで気を付ける点として、非同期実行時の制御があります。
下記のように非同期処理を悲観的タイムアウトで実行した場合、非同期処理が30秒経過しても終わらなかった場合はTimeoutRejectedException
がスローされて処理が強制終了しますが、httpClientの非同期処理自体がキャンセルされるわけではないです。
1 2 3 4 |
IAsyncPolicy timeoutPolicy = Policy.TimeoutAsync(30, TimeoutStrategy.Pessimistic); HttpResponseMessage httpResponse = await timeoutPolicy .ExecuteAsync( async ct => await httpClient.GetAsync(requestEndpoint)); |
非同期処理でDBアクセス等適切に処理の終了を制御しなければならない場合は楽観的タイムアウトを使用します。
まとめ
Pollyを使用することで、C#プロジェクトで簡単かつ柔軟にリトライ処理を実装することができます。データベースアクセスや外部サービスへの通信など、信頼性の要求される処理において、Pollyは頼りになるツールとなります。是非、プロジェクトに導入してみてください。
コメント