Я создал следующий минимальный, полный, проверяемый пример , чтобы помочь исследовать проблему:
Примечание: не обязательно готовый продукт;просто несколько незначительных модов к опубликованному коду и дополнительная аннотация, чтобы помочь исследовать вопрос.
using Polly;
using Polly.CircuitBreaker;
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class Program
{
public static void Main()
{
int maxRetryCount = 6;
double circuitBreakDurationSeconds = 0.2 /* experiment with effect of shorter or longer here, eg: change to = 1, and the fallbackForCircuitBreaker is correctly invoked */ ;
int maxExceptionsBeforeBreaking = 4; /* experiment with effect of fewer here, eg change to = 1, and the fallbackForCircuitBreaker is correctly invoked */
int maxParallelizations = 2;
int maxQueuingActions = 2;
var retryPolicy = Policy.Handle<Exception>(e => (e is HttpRequestException || (/*!(e is BrokenCircuitException) &&*/ e.InnerException is HttpRequestException))) // experiment with introducing the extra (!(e is BrokenCircuitException) && ) clause here, if necessary/desired, depending on goal
.WaitAndRetry(
retryCount: maxRetryCount,
sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (ex, calculatedWaitDuration, retryCount, context) =>
{
Console.WriteLine(String.Format("Retry => Count: {0}, Wait duration: {1}, Policy Wrap: {2}, Policy: {3}, Endpoint: {4}, Exception: {5}", retryCount, calculatedWaitDuration, context.PolicyWrapKey, context.PolicyKey, context.OperationKey, ex.Message));
});
var circuitBreaker = Policy.Handle<Exception>(e => (e is HttpRequestException || e.InnerException is HttpRequestException))
.CircuitBreaker(maxExceptionsBeforeBreaking,
TimeSpan.FromSeconds(circuitBreakDurationSeconds),
onBreak: (ex, breakDuration) => {
Console.WriteLine(String.Format("Circuit breaking for {0} ms due to {1}", breakDuration.TotalMilliseconds, ex.Message));
},
onReset: () => {
Console.WriteLine("Circuit closed again.");
},
onHalfOpen: () => { Console.WriteLine("Half open."); });
var sharedBulkhead = Policy.Bulkhead(maxParallelizations, maxQueuingActions);
var fallbackForCircuitBreaker = Policy<bool>
.Handle<BrokenCircuitException>()
/* .OrInner<BrokenCircuitException>() */ // Consider this if necessary.
/* .Or<Exception>(e => circuitBreaker.State != CircuitState.Closed) */ // This check will also detect the circuit in anything but healthy state, regardless of the final exception thrown.
.Fallback(
fallbackValue: false,
onFallback: (b, context) =>
{
Console.WriteLine(String.Format("Operation attempted on broken circuit => Policy Wrap: {0}, Policy: {1}, Endpoint: {2}", context.PolicyWrapKey, context.PolicyKey, context.OperationKey));
}
);
var fallbackForAnyException = Policy<bool>
.Handle<Exception>()
.Fallback<bool>(
fallbackAction: (context) => { return false; },
onFallback: (e, context) =>
{
Console.WriteLine(String.Format("An unexpected error occured => Policy Wrap: {0}, Policy: {1}, Endpoint: {2}, Exception: {3}", context.PolicyWrapKey, context.PolicyKey, context.OperationKey, e.Exception.Message));
}
);
var resilienceStrategy = Policy.Wrap(retryPolicy, circuitBreaker, sharedBulkhead);
var policyWrap = fallbackForAnyException.Wrap(fallbackForCircuitBreaker.Wrap(resilienceStrategy));
bool outcome = policyWrap.Execute((context) => CallApi("http://www.doesnotexistattimeofwriting.com/"), new Context("some endpoint info"));
}
public static bool CallApi(string uri)
{
using (var httpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(1) }) // Consider HttpClient lifetimes and disposal; this pattern is for minimum change from original posted code, not a recommendation.
{
Task<HttpResponseMessage> res = httpClient.GetAsync(uri);
var response = res.Result; // Consider async/await rather than blocking on the returned Task.
response.EnsureSuccessStatusCode();
return true;
}
}
}
Более чем один фактор может привести к тому, что fallbackForCircuitBreaker
не будет вызываться:
-
circuitBreakDurationSeconds
может быть установлен короче, чем общее время, затрачиваемое различными попытками и ожиданиями между попытками.
В этом случае цепьможет вернуться в полуоткрытое состояние.В полуоткрытом состоянии или закрытом состоянии , исключение, которое вызывает разрыв цепи, перерабатывается как есть.BrokenCircuitException
генерируется только в том случае, если попытка вызова (полностью) разомкнутой цепью препятствует выполнению вызова.
Таким образом, если ваша схема вернулась к полуоткрытому к моменту истечения времени повторных попыток, исключение возвращаетсяк резервным политикам упаковки будет HttpRequestException
, а не BrokenCircuitException
.
.Handle<Exception>(e => (e is HttpRequestException || e.InnerException is HttpRequestException))
предложения могут поймать CircuitBreakerException
с InnerException is HttpRequestException
A CircuitBreakerException
содержит исключение, которое вызвало разрыв цепи какего InnerException
.Таким образом, чересчур жадная / более слабая проверка e.InnerException is HttpRequestException
может также поймать CircuitBreakerException
с InnerException is HttpRequestException
.Это может или не может быть желательным в зависимости от вашей цели.
Я полагаю, что это не происходит для исходного размещенного кода из-за особого способа его построения.Блокировка Task
, возвращаемая HttpClient.DoSomethingAsync(...)
, уже вызывает AggregateException->HttpRequestException
, что означает, что результирующее CircuitBreakerException
вложит HttpRequestException
в две глубины:
CircuitBreakerException -> AggregateException -> HttpRequestException
Так что это не относится к one - глубокая проверка в размещенном коде.Однако имейте в виду , что CircuitBreakerException
содержит исключение, которое вызвало разрыв цепи как InnerException
.Это может привести к тому, что дескриптор предложения проверяет только e.InnerException is HttpRequestException
до нежелательно (неожиданно, если это не ваша цель), повторяет попытку для CircuitBreakerException
, если либо:
(a) код изменен на async
/ await
, что приведет к удалению AggregateException
и, таким образом, вложение будет иметь только одну глубину
(b) код будет изменен на Polly's.HandleInner<HttpRequestException>()
синтаксис , который является рекурсивно-жадным и поэтому может перехватывать вложенные две глубины CircuitBreakerException->AggregateException->HttpRequestException
.
Предложения /* commented out */ // with additional explanation
в приведенном выше коде показывают, каккод после публикации может быть скорректирован так, чтобы fallbackForCircuitBreaker
вызывался как ожидалось.
Две другие мысли:
Если возможно, рассмотрите возможность изменения на
async
/
await
.
Блокировка на HttpClient.DoSomethingAsync()
с помощью вызова .Result
может повлиять на производительность или привести к возникновению взаимоблокировок при смешивании с другим асинхронным кодом,и приносит всю AggregateException
-с- InnerException
боль.
Рассмотрите возможность удаления и срок службы
HttpClient
экземпляров.
(Держите эти пункты 3 и 4 намеренно краткими, как это широко обсуждается в других местах.)