Flurl を HttpClient としてサーバーにリクエストする際、ネットワークやその他の理由によりリクエストが失敗することがあります(例:
HttpStatusCode.NotFound、HttpStatusCode.ServiceUnavailable、HttpStatusCode.RequestTimeoutなど)。ネット上には HttpClientFactory を使用して Polly でリトライを実装する例が多くありますが、すでに Flurl に慣れている人がすべてを IHttpClient に戻すのは不便です。そこで、本記事では Flurl の Polly を使用したリトライ機構を整理します。
Polly を使用しないテスト
- リクエストテスト用のインターフェースを提供する
[Route("api/[controller]")]
[ApiController]
public class PollyController : ControllerBase
{
private readonly ILogger<PollyController> _logger;
public PollyController(ILogger<PollyController> logger)
{
_logger = logger;
}
// GET: api/<PollyController>
[HttpGet]
public IActionResult Get()
{
var random = new Random().Next(0, 8);
switch (random)
{
case 0:
_logger.LogInformation("About to serve a 404");
return StatusCode(StatusCodes.Status404NotFound);
case 1:
_logger.LogInformation("About to serve a 503");
return StatusCode(StatusCodes.Status503ServiceUnavailable);
case 2:
_logger.LogInformation("Sleeping for 10 seconds then serving a 504");
Thread.Sleep(10000);
_logger.LogInformation("About to serve a 504");
return StatusCode(StatusCodes.Status504GatewayTimeout);
default:
_logger.LogInformation("About to correctly serve a 200 response");
return Ok(new {time = DateTime.Now.ToLocalTime()});
}
}
}
- リクエストクライアントを作成する
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public async Task<IActionResult> Index()
{
try
{
var time = await "http://127.0.0.1:5000/api/polly"
.GetJsonAsync();
_logger.LogInformation($"App: success - {time.time}");
return View(time.time);
}
catch (Exception e)
{
_logger.LogWarning($"App: failed - {e.Message}");
throw;
}
}
}
- リクエストを試すと、多くのリクエストが失敗していることがわかります。この状況は理想的ではなく、サーバーが正常に応答できない確率が高いです。
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 404
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 503
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 503
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to correctly serve a 200 response
info: SuppertRcsInterfaceTest.Controllers.PollyController[0]
About to serve a 404
この状況に対する解決策はあるでしょうか?答えは「ある」です。単純な考え方としては、失敗したらもう一度リクエストをやり直すことです。しかし、Flurl の戻り値でこのロジックを直接処理するのは面倒で、一貫した管理も難しいため、Polly に出会いました。
Polly を使用したテスト
まず Polly をインストールします。
Install-Package Polly以下に、Polly の簡単な紹介と、
Policyのコードスニペットを示します。
Polly の7つのポリシー:リトライ、サーキットブレーカー、タイムアウト、バルクヘッド、フォールバック、キャッシュ。本記事ではリトライとタイムアウトポリシーを使用します。
リトライ(Retry):障害が発生した場合に自動的にリトライします。これは一般的なシナリオです。
サーキットブレーカー(Circuit-breaker):システムに深刻な問題が発生した場合、ユーザー/呼び出し元を待たせるよりも、迅速に失敗を返す方が良いです。システムが障害のコストを制限し、回復を助けます。例えば、サードパーティの API を呼び出す際に、長時間応答がない場合、相手のサーバーがダウンしている可能性があります。そのような場合にシステムがリトライを続けると、負荷が増加し、他のタスクに影響を与える可能性があります。そのため、システムのエラー回数が指定されたしきい値を超えた場合、現在のスレッドを中断し、しばらく待ってから再開します。例:
Policy.Handle<SomeException>().CircuitBreaker(2, TimeSpan.FromMinutes(1));は、システムが特定の異常を2回発生させた場合に停止し、1分後に再開することを意味します。また、サーキットブレーカーが作動したときのコールバックや再開時のコールバックを定義することもできます。タイムアウト(Timeout):システムが一定時間待機しても成功の結果が得られない場合、タイムアウトと判断できます。例えば、通常は瞬間的に完了するネットワークリクエストが30秒以上かかる場合、成功した結果が返されることはないと判断できます。そのため、システムが無駄に待機しないようにタイムアウト時間を設定する必要があります。例:
Policy.Timeout(30, (context, span, task) => {// do something});は、タイムアウト時間を30秒に設定し、それを超えるとエラーとみなしてコールバックを実行します。バルクヘッド隔離(Bulkhead Isolation):システムのある部分で障害が発生すると、複数の失敗した呼び出しがトリガーされ、リソースを大量に消費する可能性があります。ダウンストリームシステムの障害がアップストリームの障害を引き起こし、システム全体のクラッシュにまで及ぶ可能性があります。そのため、制御可能な操作を固定サイズのリソースプールに制限し、潜在的に相互に影響を与える可能性のある操作を隔離します。例:
Policy.Bulkhead(12, context => {// do something});は、最大12スレッドの同時実行を許可し、実行が拒否された場合はコールバックを実行します。フォールバック(Fallback):避けられないエラーに対しては、代替手段を用意する必要があります。避けられないエラーが発生した場合、失敗の代わりに合理的な戻り値を提供します。例:
Policy.Handle<Whatever>().Fallback<UserAvatar>(() => UserAvatar.GetRandomAvatar());は、ユーザーがアバターをアップロードしていない場合にデフォルトのアバターを提供します。キャッシュ(Cache):頻繁に使用され、ほとんど変更されないリソースをキャッシュして、システムの応答速度を向上させます。キャッシュリソースの呼び出しをカプセル化しない場合、呼び出しのたびにキャッシュにリソースが存在するかどうかを確認し、存在する場合はキャッシュから返し、存在しない場合はリソースストアから取得してキャッシュしてから返す必要があります。また、キャッシュの有効期限や更新方法も考慮する必要があります。Polly はキャッシュポリシーをサポートしており、問題を簡素化します。
ポリシーラップ(Policy Wrap):1つの操作に複数の異なる障害が発生する可能性があり、それぞれの障害処理には異なるポリシーが必要です。これらの異なるポリシーをまとめて1つのポリシーラップとして、同じ操作に適用できるようにするのが Polly の弾力性の特徴であり、さまざまなポリシーを柔軟に組み合わせることができます。
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http.Configuration;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
using Polly.Timeout;
using Polly.Wrap;
namespace WithPollyClient.Services
{
public class Policies
{
private readonly ILogger<Policies> _logger;
public Policies(ILogger<Policies> logger)
{
_logger = logger;
}
private AsyncTimeoutPolicy<HttpResponseMessage> TimeoutPolicy
{
get
{
return Policy.TimeoutAsync<HttpResponseMessage>(3, (context, span, task) =>
{
_logger.LogInformation($"Policy: Timeout delegate fired after {span.Seconds} seconds");
return Task.CompletedTask;
});
}
}
private AsyncRetryPolicy<HttpResponseMessage> RetryPolicy
{
get
{
HttpStatusCode[] retryStatus =
{
HttpStatusCode.NotFound,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.RequestTimeout
};
return Policy
.HandleResult<HttpResponseMessage>(r => retryStatus.Contains(r.StatusCode))
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(new[]
{
// 1秒後、2秒後、4秒後にそれぞれリトライすることを意味します
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4)
}, (result, span, count, context) =>
{
_logger.LogInformation($"Policy: Retry delegate fired, attempt {count}");
});
}
}
public AsyncPolicyWrap<HttpResponseMessage> PolicyStrategy =>
Policy.WrapAsync(RetryPolicy, TimeoutPolicy);
}
public class PolicyHandler : DelegatingHandler
{
private readonly Policies _policies;
public PolicyHandler(Policies policies)
{
_policies = policies;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _policies.PolicyStrategy.ExecuteAsync(ct => base.SendAsync(request, ct), cancellationToken);
}
}
public class PollyHttpClientFactory : DefaultHttpClientFactory
{
private readonly Policies _policies;
public PollyHttpClientFactory(Policies policies)
{
_policies = policies;
}
public override HttpMessageHandler CreateMessageHandler()
{
return new PolicyHandler(_policies)
{
InnerHandler = base.CreateMessageHandler()
};
}
}
}
- 次に
Startupで Flurl の設定を行います。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton<Policies>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var policies = app.ApplicationServices.GetService<Policies>();
FlurlHttp.Configure(setting =>
setting.HttpClientFactory = new PollyHttpClientFactory(policies));
......
- 再度リクエストを試すと、結果が非常に理想的であることがわかります。
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:14
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:17
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:22
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:23
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:25
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:31
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:34
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:39
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 1
WithPollyClient.Services.Policies: Information: Policy: Timeout delegate fired after 3 seconds
WithPollyClient.Services.Policies: Information: Policy: Retry delegate fired, attempt 2
WithPollyClient.Controllers.HomeController: Information: App: success - 2021/3/14 16:50:46
リッチクライアントでの使用例
場合によっては、例えば WPF やその他のリッチクライアントでも Flurl がよく使用されます。以下にその例を示します。
var time = await Policy
.Handle<FlurlHttpException>()
.OrResult<IFlurlResponse>(r => !r.ResponseMessage.IsSuccessStatusCode)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4)
}, (result, span, count, context) =>
{
_logger.LogInformation(count.ToString());
})
.ExecuteAsync(() => "http://127.0.0.1:5000/api/polly".WithTimeout(3).GetAsync())
.ReceiveJson();
_logger.LogInformation($"App: success - {time.time}");
return View(time.time);