Flurl Pollyで再試行ポリシーを実装する

Flurl Pollyで再試行ポリシーを実装する

FlurlをHttpClientからサーバーへのリクエストとして使用すると、ネットワークやその他の理由でリクエストが失敗する場合があります。

最后更新 2021/03/15 11:57
非法关键字
预计阅读 8 分钟
分类
.NET
标签
.NET C# Flurl Policy

在使用 Flurl 作为 HttpClient 向 Server 请求时,由于网络或者其他一些原因导致请求会有失败的情况,比如HttpStatusCode.NotFoundHttpStatusCode.ServiceUnavailable、HttpStatusCode.RequestTimeout 等;网络上有比较多的 HttpClientFactory 使用 Polly 来实现重试的内容,奈何已经习惯使用 Flurl 的人,要全部换回到 IHttpClient 的确有不方便的地方,因为本文使用 Flurl 的 Polly 来实现重试机制做一个整理;

Pollyを使用しないでください。

  1. リクエストテストのためのインターフェイスを提供する
[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()});
       }
   }
}
  1. リクエストクライアントを作成する
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;
       }
   }
}
  1. リクエストを試してみると、リクエストが失敗した場所がたくさんあることがわかります。この状況は理想的ではなく、サーバーが正常に応答できない可能性が高いです。
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を使ってテスト

  1. 首先安装 Polly, Install-Package Polly

  2. 下面先给出 Polly 的简单介绍后接着给出Policy的代码片段

Pollyの7つのポリシー:再試行、ブレークアウト、タイムアウト、分離、ロールバック、キャッシュポリシー。この記事では再試行、タイムアウトポリシーを使用します。

“再試行”Retry:障害が発生して自動的に再試行します。これは一般的なシナリオです。

断路(Circuit-breaker):当系统遇到严重的问题时,快速回馈失败比让用户/调用者等待要好,限制系统出错的消耗,有助于系统恢复,比如,当我们去调用一个第三方的 API,有很长一段时间 API 都没有响应,可能对方服务器瘫痪了,如果我们的系统还不停地重试,不仅会加重系统的负担,还有可能导致系统其他任务受影响,因此,当系统出错的次数超过了指定的阈值,就得中断当前线程,等待一段时间后再继续;比如: Policy.Handle<SomeException>().CircuitBreaker(2, TimeSpan.FromMinutes(1));表示当系统出现两次某个异常时就停下来,等待 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[]
                   {
                       // 表示重试3次,第一次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()
           };
       }
   }
}
  1. 接下来在Starup中对 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));
   ......
  1. リクエストをもう一度試してみると、結果は非常に良いです。
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);
Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/04/22

バージョン別の. NETサポート状況(250 7 0 7更新)

仮想マシンとテストマシンを使用して、各バージョンのオペレーティングシステムの. NETサポートをテストします。オペレーティングシステムのインストール後、対応するランタイムを測定し、スターダストエージェントをパスとして実行できます。

继续阅读