Circuit breaker (шаблон проєктування)Circuit breaker — патерн проєктування, який використовується для виявлення відмов та надає логіку запобіганню постійних повторів. ПроблемаНехай, дано два сервіси, які взаємодіють між собою. Результат роботи одного сервісу напряму залежить від іншого. Основний сервіс дізнається про несправність допоміжного із певною затримкою. Однак на момент очікування, основний сервіс витрачає свої ресурси, такі як пам'ять, процесорний час, кількість доступних потоків виконання, тощо. Якщо кількість запитів на основний сервіс перевищує затримку очікування відповіді про несправність допоміжного сервісу, із часом основний сервіс вичерпає свої ресурси й також вийде із ладу. ВирішенняНеобхідно додати проміжний сервіс, який заздалегідь перериватиме невдалі запити, а також слідкуватиме за відновленням роботи допоміжного сервісу. АлгоритмClosed (з'єднаний зв'язок) Система перебуває у з'єднаному стані тоді коли допоміжний сервіс успішно відповідає на запити основного сервісу.
Open (зв'язок із розривами) Система перебуває у стані розриву. Задача цього стану не навантажувати допоміжний сервіс запитами та дати йому час на відновлення. При цьому основний сервіс отримує відповідь про несправність без жодних затримок.
Half Open (відновлення зв'язку) На цьому етапі ми не знаємо напевно чи допоміжний сервіс відновився, чи досі перебуває в аварійному стані.
Опис мовою C#Нехай, дано сервіси які взаємодіють між собою. public interface IService
{
// виконання операції
string GetValue();
// виконання операції базуючись на результаті від іншого сервісу
string GetModifiedValue(IService service)
{
try
{
return $"From another service: {service.GetValue()}";
}
catch
{
return "Call to another service failed";
}
}
}
У той час, як один із сервісів успішно виконує свою операцію. Інший має шанс аварійного завершення. При цьому присутня затримка, що впливає на роботу основного сервісу. public class ServiceA : IService
{
public string GetValue()
{
return "Service A";
}
}
public class ServiceB : IService
{
public string GetValue()
{
// 50% шанс аварійного завершення
if (new Random().NextDouble() > 0.5)
{
// імітуємо довготривалу роботу
Thread.Sleep(5000);
throw new InvalidOperationException();
}
return "Service B";
}
}
Таким чином несправності в допоміжному сервісі блокують основний. static void Main(string[] args)
{
IService serviceA = new ServiceA();
IService serviceB = new ServiceB();
// несправності в допоміжному сервісі блокують основний
for (int i = 0; i < 20; ++i)
{
Console.WriteLine($"Request {i}");
Console.WriteLine(serviceA.GetValue());
Console.WriteLine(serviceA.GetModifiedValue(serviceB));
Console.WriteLine();
}
}
Додамо сервіс, який корегуватиме стан систему. // опишемо стани системи
public enum CircuitBreakerStateEnum
{
Closed = 0,
Open = 1,
HalfOpen = 2,
}
interface ICircuitBreaker
{
CircuitBreakerStateEnum State { get; }
int FailThreshold { get; set; } // максимальна кількість невдалих запитів потрібних для переходу в Open стан
int RetriesThreshold { get; set; } // максимальна кількість запитів потрібних для переходу в HalfOpen стан
}
Проміжний сервіс матиме наступний вигляд: public class ServiceBProxy : IService, ICircuitBreaker
{
private readonly IService _service;
public ServiceBProxy(IService service)
{
_service = service;
}
public string GetValue()
{
if (State == CircuitBreakerStateEnum.Closed)
{
try
{
// делегуємо запит допоміжному сервісу
return _service.GetValue();
}
catch (Exception)
{
// підраховуємо невдалі запити та при потребі переходимо в Open стан
++_currentFailAttempt;
if (_currentFailAttempt >= FailThreshold)
{
_currentFailAttempt = 0;
State = CircuitBreakerStateEnum.Open;
}
throw;
}
}
else if (State == CircuitBreakerStateEnum.Open)
{
// рахуємо запити і при потребі переходимо в HalfOpen стан
++_retriesAttempt;
if (_retriesAttempt > RetriesThreshold)
{
_retriesAttempt = 0;
State = CircuitBreakerStateEnum.HalfOpen;
}
// без затримки повідомляємо про помилку в системі
throw new InvalidOperationException();
}
else // if (State == CircuitBreakerStateEnum.HalfOpen)
{
try
{
// робимо запит
// у разі успішного виконання переходимо в стан Closed
var value = _service.GetValue();
State = CircuitBreakerStateEnum.Closed;
return value;
}
catch (Exception)
{
// при помилці переходимо в стан Open
State = CircuitBreakerStateEnum.Open;
throw;
}
}
}
// поля необхідні для роботи CircuitBreaker
private int _currentFailAttempt;
private int _retriesAttempt; // можливо таймер
public CircuitBreakerStateEnum State { get; private set; } = CircuitBreakerStateEnum.Closed;
public int FailThreshold { get; set; } = 2;
public int RetriesThreshold { get; set; } = 3;
}
РеалізаціяC#Приклад реалізації на мові С#
using System;
using System.Threading;
namespace CircuitBreakerPattern
{
public interface IService
{
// виконання операції
string GetValue();
// виконання операції базуючись на результаті від іншого сервісу
string GetModifiedValue(IService service)
{
try
{
return $"From another service: {service.GetValue()}";
}
catch
{
return "Call another service failed";
}
}
}
public class ServiceA : IService
{
public string GetValue()
{
return "Service A";
}
}
public class ServiceB : IService
{
public string GetValue()
{
// 50% шанс аварійного завершення
if (new Random().NextDouble() > 0.5)
{
// імітуємо довготривалу роботу
Thread.Sleep(5000);
throw new InvalidOperationException();
}
return "Service B";
}
}
public enum CircuitBreakerStateEnum
{
Closed = 0,
Open = 1,
HalfOpen = 2,
}
interface ICircuitBreaker
{
CircuitBreakerStateEnum State { get; }
int FailThreshold { get; set; } // максимальна кількість невдалих запитів потрібних для переходу в Open стан
// оригінальний алгоритм передбачає використання таймеру
int RetriesThreshold { get; set; } // максимальна кількість запитів потрібних для переходу в HalfOpen стан
}
public class ServiceBProxy : IService, ICircuitBreaker
{
private readonly IService _service;
public ServiceBProxy(IService service)
{
_service = service;
}
private int _currentFailAttempt;
private int _retriesAttempt;
public CircuitBreakerStateEnum State { get; private set; } = CircuitBreakerStateEnum.Closed;
public int FailThreshold { get; set; } = 2;
public int RetriesThreshold { get; set; } = 3;
public string GetValue()
{
if (State == CircuitBreakerStateEnum.Closed)
{
return GetValueInClosedState();
}
else if (State == CircuitBreakerStateEnum.Open)
{
return GetValueInOpenState();
}
else // if (State == CircuitBreakerStateEnum.HalfOpen)
{
return GetValueInHalfOpenState();
}
}
private string GetValueInClosedState()
{
try
{
// делегуємо запит допоміжному сервісу
return _service.GetValue();
}
catch
{
// підраховуємо невдалі запити
++_currentFailAttempt;
// при перевищені ліміту переходимо в Open стан
if (_currentFailAttempt >= FailThreshold) Open();
throw;
}
}
private string GetValueInOpenState()
{
// рахуємо запити
++_retriesAttempt;
// при перевищені ліміту переходимо в HalfOpen стан
if (_retriesAttempt > RetriesThreshold) HalfOpen();
// без затримки повідомляємо про помилку в системі
throw new InvalidOperationException();
}
private string GetValueInHalfOpenState()
{
try
{
// робимо запит
var value = _service.GetValue();
// у разі успішного виконання переходимо в стан Closed
Close();
return value;
}
catch
{
// при помилці переходимо в стан Open
Open();
throw;
}
}
private void Open()
{
ResetCounter();
State = CircuitBreakerStateEnum.Open;
}
private void HalfOpen()
{
ResetCounter();
State = CircuitBreakerStateEnum.HalfOpen;
}
private void Close()
{
ResetCounter();
State = CircuitBreakerStateEnum.Closed;
}
private void ResetCounter()
{
_currentFailAttempt = 0;
_retriesAttempt = 0;
}
}
class Program
{
static void Main(string[] args)
{
IService serviceA = new ServiceA();
IService serviceB = new ServiceB();
// обгортаємо допоміжний сервіс, proxy-сервісом, який реалізовує логіку Circuit Breaker
serviceB = new ServiceBProxy(serviceB);
// несправності в допоміжному сервісі блокують основний
for (int i = 0; i < 20; ++i)
{
Console.WriteLine($"Request {i}");
Console.WriteLine(serviceA.GetValue());
Console.WriteLine(serviceA.GetModifiedValue(serviceB));
Console.WriteLine();
}
}
}
}
Див. такожДжерела
|