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

Posted by VladymyrL on Saturday, January 5, 2019
Last Modified on Monday, February 11, 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