[英]Polly CircuitBreaker change HttpClient baseaddress while circuit broken to continue execution of requests
目前,我有一個配置了RetryAsync
策略的客戶端,該策略使用主地址並在故障時切換到故障轉移地址。 連接詳細信息從機密管理器中讀取。
services
.AddHttpClient ("MyClient", client => client.BaseAddress = PlaceholderUri)
.ConfigureHttpMessageHandlerBuilder (builder => {
// loads settings from secret manager
var settings = configLoader.LoadSettings().Result;
builder.PrimaryHandler = new HttpClientHandler {
Credentials = new NetworkCredential (settings.Username, settings.Password),
AutomaticDecompression = DecompressionMethods.GZip
};
var primaryBaseAddress = new Uri (settings.Host);
var failoverBaseAddress = new Uri (settings.DrHost);
builder.AdditionalHandlers.Add (new PolicyHttpMessageHandler (requestMessage => {
var relativeAddress = PlaceholderUri.MakeRelativeUri (requestMessage.RequestUri);
requestMessage.RequestUri = new Uri (primaryBaseAddress, relativeAddress);
return HttpPolicyExtensions.HandleTransientHttpError ()
.RetryAsync ((result, retryCount) =>
requestMessage.RequestUri = new Uri (failoverBaseAddress, relativeAddress));
}));
});
我的客戶可以使用主服務或故障轉移服務。 當主服務器關閉時,使用故障轉移直到主服務器備份。 當兩者都關閉時,我們會收到警報,並且可以通過秘密管理器動態更改服務地址。
現在我還想介紹一個CircuitBreakerPolicy
並將這兩個策略鏈接在一起。 我正在尋找一種封裝的配置,並且在客戶端級別而不是在使用該客戶端的 class 上處理故障。
讓我們假設有一個斷路器策略包裝在具有單個客戶端的重試策略中。
斷路器配置為在主基地址上的瞬態錯誤嘗試失敗 3次后斷開電路60 秒。 OnBreak
- 地址從主地址更改為故障轉移。
重試策略配置為處理BrokenCircuitException
,並在地址從主地址更改為故障轉移時重試一次以繼續。
BrokenCircuitException
,調用故障轉移BrokenCircuitException
,調用故障轉移BrokenCircuitException
,調用故障轉移BrokenCircuitException
,調用故障轉移如本文所述,有一個解決方案是使用包裝在回退中的斷路器,但正如您所見,默認和回退的邏輯是在class中實現的,而不是在客戶端級別。
我想
public class OpenExchangeRatesClient
{
private readonly HttpClient _client;
private readonly Policy _policy;
public OpenExchangeRatesClient(string apiUrl)
{
_client = new HttpClient
{
BaseAddress = new Uri(apiUrl),
};
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 2,
durationOfBreak: TimeSpan.FromMinutes(1)
);
_policy = Policy
.Handle<Exception>()
.FallbackAsync(() => GetFallbackRates())
.Wrap(circuitBreaker);
}
public Task<ExchangeRates> GetLatestRates()
{
return _policy
.ExecuteAsync(() => CallRatesApi());
}
public Task<ExchangeRates> CallRatesApi()
{
//call the API, parse the results
}
public Task<ExchangeRates> GetFallbackRates()
{
// load the rates from the embedded file and parse them
}
}
改寫為
public class OpenExchangeRatesClient
{
private readonly HttpClient _client;
public OpenExchangeRatesClient (IHttpClientFactory clientFactory) {
_client = clientFactory.CreateClient ("MyClient");
}
public Task<ExchangeRates> GetLatestRates () {
return _client.GetAsync ("/rates-gbp-usd");
}
}
我嘗試了幾種不同的場景來鏈接斷路器策略並將其與重試策略相結合,以在啟動文件中的客戶端杠桿上實現預期目標。 最后的 state 如下。 這些策略按照重試能夠捕獲BrokenCircuitException
的順序進行包裝,但情況並非如此。 在消費者 class 上拋出異常,這不是預期的結果。 雖然觸發了RetryPolicy
,但是仍然拋出了消費者 class 上的異常。
var retryPolicy = GetRetryPolicy();
var circuitBreaker = GetCircuitBreakerPolicy();
var policyWraper = Policy.WrapAsync(retryPolicy, circuitBreaker);
services
.AddHttpClient("TestClient", client => client.BaseAddress = GetPrimaryUri())
.AddPolicyHandler(policyWraper);
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
3,
TimeSpan.FromSeconds(45),
OnBreak,
OnReset,
OnHalfOpen);
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy<HttpResponseMessage>
.Handle<Exception>()
.RetryAsync(1, (response, retryCount) =>
{
Debug.WriteLine("Retries on broken circuit");
});
}
我省略了方法OnBreak
、 OnReset
和OnHalfOpen
,因為它們只是打印一些消息。
更新:從控制台添加了日志。
Circuit broken (after 3 attempts)
Retries on broken
Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll
Retries on broken circuit
Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll
'CircuitBreakerPolicy.exe'(CoreCLR:clrhost):加載'C:\ Program Retries on break circuit 異常拋出:System.Private.CoreLib.dll中的'System.AggregateException'
更新 2:使用配置了策略的客戶端向 class 添加了參考 URL
更新 3:該項目已更新,以便WeatherService2.Get
的實現以所需的方式工作:當主要服務不可用時,電路中斷,使用 falover 服務直到電路關閉。 這將是這個問題的答案,但是我想探索一個解決方案,使用WeatherService.Get
在Startup
上使用適當的策略和客戶端設置來實現相同的結果。
使用客戶端參考class 。 參考使用 class 的項目。
在上面的日志中可以看到Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll
由斷路器拋出 - 這是不期望的,因為有重試包裝斷路器。
我已經下載了你的項目並使用它,所以這是我的觀察:
.Result
),所以您會看到AggregateException
public IEnumerable<WeatherForecast> Get()
{
HttpResponseMessage response = null;
try
{
response = _client.GetAsync(string.Empty).Result; //AggregateException
}
catch (Exception e)
{
Debug.WriteLine($"{e.Message}");
}
...
}
AggregateException
的InnerException
,您需要使用await
public async Task<IEnumerable<WeatherForecast>> Get()
{
HttpResponseMessage response = null;
try
{
response = await _client.GetAsync(string.Empty); //BrokenCircuitException
}
catch (Exception e)
{
Debug.WriteLine($"{e.Message}");
}
...
}
每當您將策略包裝到另一個策略中時,都可能會發生升級。 這意味着如果內部無法處理問題,那么它會將相同的問題傳播到外部,外部可能會也可能無法處理。 如果最外層沒有處理問題,那么(大部分時間)原始異常將被拋出給彈性策略的消費者(這是策略的組合)。
在這里您可以找到有關升級的更多詳細信息。
讓我們在您的案例中回顧一下這個概念:
var policyWrapper = Policy.WrapAsync(retryPolicy, circuitBreaker);
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(45), ...);
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy<HttpResponseMessage>
.Handle<Exception>()
.RetryAsync(1, ...);
}
https://httpstat.us/500
發出初始請求(1. 嘗試)InternalServerError
狀態代碼的HttpResponseMessage
。讓我們修改重試策略以處理瞬態 http 錯誤:
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(45), ...);
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<Exception>()
.RetryAsync(1, ...);
}
https://httpstat.us/500
發出初始請求(1. 嘗試)https://httpstat.us/500
發出第一次重試請求(2.嘗試)InternalServerError
StatusCode 的HttpResponseMessage
。 現在,讓我們將連續失敗計數從 3 降低到 1 並顯式處理BrokenCircuitException
:
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(1, TimeSpan.FromSeconds(45), ...);
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<BrokenCircuitException>()
.RetryAsync(1, ...);
}
https://httpstat.us/500
發出初始請求(1. 嘗試)https://httpstat.us/500
發出第一次重試請求(2.嘗試)BrokenCircuitException
BrokenCircuitException
它也不會觸發,因為它達到了它的重試計數 (1)BrokenCircuitException
),因此 httpClient 的GetAsync
會拋出該異常。最后讓我們將 retryCount 從 1 增加到 2:
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(1, TimeSpan.FromSeconds(45), ...);
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<BrokenCircuitException>()
.RetryAsync(2, ...);
}
https://httpstat.us/500
發出初始請求(1. 嘗試)https://httpstat.us/500
發出第一次重試請求(2.嘗試)BrokenCircuitException
BrokenCircuitException
並且它沒有超過它的 retryCount 所以它立即發出另一個嘗試https://httpstat.us/500
發出第二次重試請求(3.嘗試)BrokenCircuitException
BrokenCircuitException
它也不會觸發,因為它達到了它的重試計數 (2)BrokenCircuitException
),因此 httpClient 的GetAsync
將拋出該異常。我希望這個練習可以幫助您更好地理解如何創建彈性策略,通過升級問題來組合多個策略。
我已經查看了您的替代解決方案,該解決方案與我在上一篇文章中討論的設計問題相同。
public WeatherService2(IHttpClientFactory clientFactory, IEnumerable<IAsyncPolicy<HttpResponseMessage>> policies)
{
_primaryClient = clientFactory.CreateClient("PrimaryClient");
_failoverClient = clientFactory.CreateClient("FailoverClient");
_circuitBreaker = policies.First(p => p.PolicyKey == "CircuitBreaker");
_policy = Policy<HttpResponseMessage>
.Handle<Exception>()
.FallbackAsync(_ => CallFallbackForecastApi())
.WrapAsync(_circuitBreaker);
}
public async Task<string> Get()
{
var response = await _policy.ExecuteAsync(async () => await CallForecastApi());
if (response.IsSuccessStatusCode)
return response.StatusCode.ToString();
response = await CallFallbackForecastApi();
return response.StatusCode.ToString();
}
您的后備策略永遠不會被觸發。
HttpResponseMessage
傳播到外部策略Handle<Exception>()
HttpResponseMessage
如果您將政策更改為:
_policy = Policy
.HandleResult<HttpResponseMessage>(response => response != null && !response.IsSuccessStatusCode)
.Or<Exception>()
.FallbackAsync(_ => CallFallbackForecastApi())
.WrapAsync(_circuitBreaker);
那么就不需要手動回退了。
HttpResponseMessage
傳播到外部策略HttpResponseMessage
您還需要了解一件更重要的事情。 前面的代碼之所以有效,是因為您在沒有斷路器策略的情況下注冊了 HttpClients。
這意味着 CB未附加到 HttpClient。 因此,如果您像這樣更改代碼:
public async Task<HttpResponseMessage> CallForecastApi()
=> await _primaryClient.GetAsync("https://httpstat.us/500/");
public async Task<HttpResponseMessage> CallFallbackForecastApi()
=> await _primaryClient.GetAsync("https://httpstat.us/200/");
那么即使 CircuitBreaker 在第一次嘗試后打開, CallFallbackForecastApi
也不會拋出BrokenCircuitException
。
但是,如果您像這樣將 CB 附加到 HttpClient:
services
.AddHttpClient("PrimaryClient", client => client.BaseAddress = PlaceholderUri)
...
.AddPolicyHandler(GetCircuitBreakerPolicy());
然后像這樣簡化WeatherService2
:
private readonly HttpClient _primaryClient;
private readonly IAsyncPolicy<HttpResponseMessage> _policy;
public WeatherService2(IHttpClientFactory clientFactory)
{
_primaryClient = clientFactory.CreateClient("PrimaryClient");
_policy = Policy
.HandleResult<HttpResponseMessage>(response => response != null && !response.IsSuccessStatusCode)
.Or<Exception>()
.FallbackAsync(_ => CallFallbackForecastApi());
}
那么它將BrokenCircuitException
而慘遭失敗。
如果您的WeatherService2
看起來像這樣:
public class WeatherService2 : IWeatherService2
{
private readonly HttpClient _primaryClient;
private readonly HttpClient _secondaryClient;
private readonly IAsyncPolicy<HttpResponseMessage> _policy;
public WeatherService2(IHttpClientFactory clientFactory)
{
_primaryClient = clientFactory.CreateClient("PrimaryClient");
_secondaryClient = clientFactory.CreateClient("FailoverClient");
_policy = Policy
.HandleResult<HttpResponseMessage>(response => response != null && !response.IsSuccessStatusCode)
.Or<Exception>()
.FallbackAsync(_ => CallFallbackForecastApi());
}
public async Task<string> Get()
{
var response = await _policy.ExecuteAsync(async () => await CallForecastApi());
return response.StatusCode.ToString();
}
public async Task<HttpResponseMessage> CallForecastApi()
=> await _primaryClient.GetAsync("https://httpstat.us/500/");
public async Task<HttpResponseMessage> CallFallbackForecastApi()
=> await _secondaryClient.GetAsync("https://httpstat.us/200/");
}
那么只有在PrimaryClient
和FailoverClient
有不同的斷路器時它才能正常工作。
services
.AddHttpClient("PrimaryClient", client => client.BaseAddress = PlaceholderUri)
...
.AddPolicyHandler(GetCircuitBreakerPolicy());
services
.AddHttpClient("FailoverClient", client => client.BaseAddress = PlaceholderUri)
...
.AddPolicyHandler(GetCircuitBreakerPolicy());
如果他們將共享同一個斷路器,那么第二次調用將再次失敗並出現BrokenCircuitException
。
var cbPolicy = GetCircuitBreakerPolicy();
services
.AddHttpClient("PrimaryClient", client => client.BaseAddress = PlaceholderUri)
...
.AddPolicyHandler(cbPolicy);
services
.AddHttpClient("FailoverClient", client => client.BaseAddress = PlaceholderUri)
...
.AddPolicyHandler(cbPolicy);
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.