TOC
Что такое паттерн предохранитель?
При разработке приложений enterprise уровня нам часто приходится вызывать внешние сервисы и ресурсы. Этими службами могут быть сетевые папки, сервера баз данных или веб-службы. Всякий раз, когда мы обращаемся к этим сервисам, существует вероятность того, что проблема с сетью или самой службой может вызвать сбой нашей системы. Один из способов работы со сбоями вызываемой службы состоит в том, чтобы ставить запросы в очередь и периодически повторять попытки. Это позволяет нам продолжать обработку запросов, пока сервис снова не станет доступным. Тем не менее, если в сети или сервисе возникают проблемы, то такие попытки повторной отправки запроса не помогут восстановить службу, особенно если она находится под повышенной нагрузкой. Такие запросы могут нанести еще больший ущерб и помешать работе служб. Если мы знаем, что потенциально могут быть проблемы со службой, мы можем помочь решить некоторые проблемы, внедрив шаблон предохранитель в клиентском приложении.
Предохранители в нашем доме предотвращают выброс тока от повреждения приборов или перегрева проводки. Они работают, позволяя определенному уровню тока войти в систему. Если ток превышает пороговое значение, цепь размыкается, останавливая ток и предотвращая дальнейшее повреждение. Как только проблема будет устранена, предохранитель может быть сброшен, что замыкает цепь и позволяет электричеству снова течь. Паттерн автоматического выключателя использует ту же концепцию, останавливая запросы к ресурсу, если число отказов превышает определенный порог.
Схема автоматического выключателя была детально описана в книге Майкла Т. Найгарда «Release it!».
Принцип действия
Шаблон имеет три рабочих состояния: включён (закрыт), выключен (открыт) и полувключён (полуоткрыт).
Во «включённом» состоянии операции выполняются как обычно. Если операция выдает исключение, счетчик ошибок увеличивается и генерируется исключение OperationFailedException. Если количество отказов превышает пороговое значение, предохранитель отключается. Если вызов успешен до достижения порогового значения, счетчик ошибок сбрасывается.
В состоянии «выключён» все вызовы операции немедленно завершатся сбоем и вызовут исключение все вызовы. Тайм-аут начинается, когда предохранитель срабатывает. По истечении времени ожидания предохранитель переходит в состояние «полувключённое».
В состоянии «полувключённое» предохранитель позволяет выполнить одну операцию. Если эта операция заканчивается неудачей, предохранитель снова переходит в состояние «выключенное», и время ожидания сбрасывается. Если операция завершается успешно, предохранитель переходит в «включённое» состояние, и процесс начинается заново.
Выполняем вызовы
к удалённому сервису) 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