Реализация паттерна предохранитель на C#

Posted by VladymyrL on Saturday, January 5, 2019

TOC

предохранитель

Что такое паттерн предохранитель?

При разработке приложений enterprise уровня нам часто приходится вызывать внешние сервисы и ресурсы. Этими службами могут быть сетевые папки, сервера баз данных или веб-службы. Всякий раз, когда мы обращаемся к этим сервисам, существует вероятность того, что проблема с сетью или самой службой может вызвать сбой нашей системы. Один из способов работы со сбоями вызываемой службы состоит в том, чтобы ставить запросы в очередь и периодически повторять попытки. Это позволяет нам продолжать обработку запросов, пока сервис снова не станет доступным. Тем не менее, если в сети или сервисе возникают проблемы, то такие попытки повторной отправки запроса не помогут восстановить службу, особенно если она находится под повышенной нагрузкой. Такие запросы могут нанести еще больший ущерб и помешать работе служб. Если мы знаем, что потенциально могут быть проблемы со службой, мы можем помочь решить некоторые проблемы, внедрив шаблон предохранитель в клиентском приложении.

Предохранители в нашем доме предотвращают выброс тока от повреждения приборов или перегрева проводки. Они работают, позволяя определенному уровню тока войти в систему. Если ток превышает пороговое значение, цепь размыкается, останавливая ток и предотвращая дальнейшее повреждение. Как только проблема будет устранена, предохранитель может быть сброшен, что замыкает цепь и позволяет электричеству снова течь. Паттерн автоматического выключателя использует ту же концепцию, останавливая запросы к ресурсу, если число отказов превышает определенный порог.

Схема автоматического выключателя была детально описана в книге Майкла Т. Найгарда «Release it!».

Принцип действия

Шаблон имеет три рабочих состояния: включён (закрыт), выключен (открыт) и полувключён (полуоткрыт).

Во «включённом» состоянии операции выполняются как обычно. Если операция выдает исключение, счетчик ошибок увеличивается и генерируется исключение OperationFailedException. Если количество отказов превышает пороговое значение, предохранитель отключается. Если вызов успешен до достижения порогового значения, счетчик ошибок сбрасывается.

В состоянии «выключён» все вызовы операции немедленно завершатся сбоем и вызовут исключение все вызовы. Тайм-аут начинается, когда предохранитель срабатывает. По истечении времени ожидания предохранитель переходит в состояние «полувключённое».

В состоянии «полувключённое» предохранитель позволяет выполнить одну операцию. Если эта операция заканчивается неудачей, предохранитель снова переходит в состояние «выключенное», и время ожидания сбрасывается. Если операция завершается успешно, предохранитель переходит в «включённое» состояние, и процесс начинается заново.

graph TD * --> A(:Включён:
Выполняем вызовы
к удалённому сервису) B(:Выключен:
Все попытки обращения к сервису
в течении заданного времени
заменяются на проброс исключения
OperationFailedException) A --Сработало
условие
предохранителя--> B B --Истечение
таймаута--> C(:Полувключён:
Выполняем одну
тестовую операцию) C --Операция
выполнилась
успешно--> A C --Сработало
условие предохранителя--> B

Наивная реализация и её минусы

Давайте рассмотрим самую простую реализацию

/// <summary>

/// Базовая имплементация паттерна предохранитель

/// </summary>

public class CircuitBreaker
{
    /// <summary>
    
    /// Выполняет операцию
    
    /// </summary>
    
    /// <param name="operation">Операция для выполения</param>
    
    /// <param name="args">Аргументы операции</param>
    
    /// <returns>Возвращаем результат операции</returns>
    
    /// <exception cref="OpenCircuitException"></exception>
    
    public object Execute(Delegate operation, params object[] args)
    {
        if (this.state == CircuitBreakerState.Open)
        {
            throw new OpenCircuitException("Circuit breaker is currently open");
        }

        object result = null;
        try
        {
            // Выполняем операцию
            
            result = operation.DynamicInvoke(args);
        }
        catch (Exception ex)
        {
            if (this.state == CircuitBreakerState.HalfOpen)
            {
                // Операция не выполнилась в полуоткрытом состоянии выключаем предохранитель
                
                Trip();
            }
            else if (this.failureCount < this.threshold)
            {
                // Операция завершилась неудачей увеличиваем счётчик неудачь
                
                this.failureCount++;
            }
            else if (this.failureCount >= this.threshold)
            {
                // Счётчик неудачь превысил порог - выключаем предохранитель
                
                Trip();
            }

            throw new OperationFailedException("Operation failed", ex.InnerException);
        }

        if (this.state == CircuitBreakerState.HalfOpen)
        {
            // Операция завершилась успешно в полуоткрытом состоянии
            
            // включаем предохранитель
            
            Reset();
        }

        if (this.failureCount > 0)
        {
            // уменьшаем счётчик неудачь 
            
            this.failureCount--;
        }

        return result;
    }
}

Для простоты здесь были опущены некоторые нюансы, такие как:

  • перевод из выключенного состояния в полувключённое (Background Worker)
  • подписка на интересующие нас события (OnError, OnSwitch и т.д.)
  • whitelist список исключений которые мы должны пробрасывать в наше приложение

При необходимости более детально ознакомится с этой реализацией можете скачать этот проект из статьи Тима Росса. Даже в таком неоконченном состоянии можно видеть, что у этого кода есть проблемы. Первое - его не так и удобно использовать, нужно каким-то образом хранить саму сущьность класса синглтоном в приложении, ведь пересоздавая объект CircuitBreaker мы будем терять его состояние. Второе - наше приложение не потокобезопасно - фрагменты изменения состояния приложения стоит заменить на System.Threading.Interlocked.Increment и System.Threading.Interlocked.Decrement.

Библиотека Polly и её предохранитель

Одним из надёжных и проверенных вариантов избавится от необходимости поддерживать свою реализацию - использовать библиотеку Polly и её реализацию Circuit Breaker.

// Определённая ниже политика прерывает обращение к ресурсу 

// после определённого количества ошибок

// и держит предохранитель отключенным на протяжении определённого промежутка времени

// вызывая onBreak в случае выключения предохранителя и  

// onReset при включении его

Action<Exception, TimeSpan, Context> onBreak = (exception, timespan, context) => { ... };
Action<Context> onReset = context => { ... };
CircuitBreakerPolicy breaker = Policy
    .Handle<SomeExceptionType>()
    .CircuitBreaker(2, TimeSpan.FromMinutes(1), onBreak, onReset);

CircuitState state = breaker.CircuitState;

// вот таким образом можно вызвать обращение к ресурсу с заданной политикой.

Action<Task<T>> someAction = ...
T data = await breaker.ExecuteAsync(async ct => await someAction);

У реализаци Polly есть особенность - её выключатель содержит 4-е состояние Isolated - при котором предохранитель выключен и никогда не будет переведён в полувключенное состояние если его не включить явно руками. Для этих манипуляций у Polly есть специальное апи breaker.Isolate() и breaker.Reset() пара методов соответственно для выключения и включения его в ручном режиме.

P.S.: Ребята, если Вам интересна статья и вы хотите продолжения по Polly, пожалуйста прокоментируйте или проголосуйте в Disqus внизу статьи.


comments powered by Disqus