Flurl使用Polly實現重試Policy

Flurl使用Polly實現重試Policy

在使用Flurl作為HttpClient向Server請求時,由於網路或者其他一些原因導致請求會有失敗的情況

最後更新 2021/3/15 上午11:57
非法关键字
預計閱讀 9 分鐘
分類
.NET
標籤
.NET C# Flurl Policy

在使用 Flurl 作為 HttpClient 向 Server 請求時,由於網路或其他一些原因導致請求可能會有失敗的情況,例如 HttpStatusCode.NotFoundHttpStatusCode.ServiceUnavailableHttpStatusCode.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 的七種策略:重試、斷路、超時、隔離、回退和快取策略,本文使用到了重試、超時策略

重試(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):一種操作會有多種不同的故障,而不同的故障處理需要不同的策略,這些不同的策略必須包在一起,作為一個策略包,才能應用在同一種操作上,這就是 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. 接下來在 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));
   ......
  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);
繼續探索

延伸閱讀

更多文章