Полли CircuitBreaker запасной вариант не работает - PullRequest
0 голосов
/ 19 декабря 2018

У меня есть следующие политики:

var retryPolicy = Policy.Handle<Exception>(e => (e is HttpRequestException || e.InnerException is HttpRequestException)).WaitAndRetry(
                retryCount: maxRetryCount,
                sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
                onRetry: (exception, calculatedWaitDuration, retryCount, context) =>
                {
                    Log.Error($"Retry => Count: {retryCount}, Wait duration: {calculatedWaitDuration}, Policy Wrap: {context.PolicyWrapKey}, Policy: {context.PolicyKey}, Endpoint: {context.OperationKey}, Exception: {exception}.");
                });

var circuitBreaker = Policy.Handle<Exception>(e => (e is HttpRequestException || e.InnerException is HttpRequestException)).CircuitBreaker(maxExceptionsBeforeBreaking, TimeSpan.FromSeconds(circuitBreakDurationSeconds), onBreak, onReset);

var sharedBulkhead = Policy.Bulkhead(maxParallelizations, maxQueuingActions, onBulkheadRejected);

var fallbackForCircuitBreaker = Policy<bool>
             .Handle<BrokenCircuitException>()
             .Fallback(
                 fallbackValue: false,
                 onFallback: (b, context) =>
                 {
                     Log.Error($"Operation attempted on broken circuit => Policy Wrap: {context.PolicyWrapKey}, Policy: {context.PolicyKey}, Endpoint: {context.OperationKey}");
                 }
             );

            var fallbackForAnyException = Policy<bool>
                .Handle<Exception>()
                .Fallback(
                    fallbackAction: (context) => { return false; },
                    onFallback: (e, context) =>
                    {
                        Log.Error($"An unexpected error occured => Policy Wrap: {context.PolicyWrapKey}, Policy: {context.PolicyKey}, Endpoint: {context.OperationKey}");
                    }
                );

var resilienceStrategy = Policy.Wrap(retryPolicy, circuitBreaker, sharedBulkhead);
            var policyWrap = fallbackForAnyException.Wrap(fallbackForCircuitBreaker.Wrap(resilienceStrategy));

public bool CallApi(ChangeMapModel changeMessage)
    {
        var httpClient = new HttpClient();
        var endPoint = changeMessage.EndPoint;
        var headers = endPoint.Headers;
        if (headers != null)
        {
            foreach (var header in headers)
            {
                if (header.Contains(':'))
                {
                    var splitHeader = header.Split(':');
                    httpClient.DefaultRequestHeaders.Add(splitHeader[0], splitHeader[1]); 
                }
            } 
        }

        var res = httpClient.PostAsync(endPoint.Uri, null);
        var response = res.Result;
        response.EnsureSuccessStatusCode();
        return true;
    }

Я выполняю политику следующим образом:

policyWrap.Execute((context) => CallApi(changeMessage), new Context(endPoint));

Проблема в том, что я не получаю попадание в обратном вызове CircuitBreaker, когдадействие выполняется в разомкнутом контуре.

Я хочу, чтобы через политику политики выполнялся вызов API с типом исключительной ситуации HttpRequestException.Что-то не так с определениями политики?Почему не вызывается резервный выключатель?

1 Ответ

0 голосов
/ 20 декабря 2018

Я создал следующий минимальный, полный, проверяемый пример , чтобы помочь исследовать проблему:

Примечание: не обязательно готовый продукт;просто несколько незначительных модов к опубликованному коду и дополнительная аннотация, чтобы помочь исследовать вопрос.

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 не будет вызываться:

  1. 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 намеренно краткими, как это широко обсуждается в других местах.)

...