Circuit breaker
is what enables preventing your application from hammering down the already-downed system.
In Rust, the simplest abstraction possible will be:
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub trait TCircuitBreaker {
// guard will let you through when the state is either closed or half-open
fn guard(&mut self) -> Result<()>;
fn succeed(&mut self);
fn trip(&mut self);
}
The state of circuit breaker is usually presnted with the following:
Closed
: indicates that the circuit's closed and messages can flowOpen
: the breaker lets no calls through to the remote system.Half-open
: allows single remote call, and if it succeeds, the state goes back to Closed
#[derive(Default, PartialEq, Eq)]
pub enum CircuitBreakerState {
#[default]
Closed,
Open,
HalfOpen,
}
Along with the state, we have a struct that will implement the trait above.
use chrono::{DateTime, Duration, Utc};
pub struct CircuitBreaker {
timeout: DateTime<Utc>,
state: CircuitBreakerState,
}
impl CircuitBreaker {
pub fn new(timeout: i64) -> Self {
Self {
timeout: Utc::now() + Duration::seconds(timeout),
state: Default::default(),
}
}
}
Now, let's implement TCircuitBreaker
;
impl TCircuitBreaker for CircuitBreaker {
fn guard(&mut self) -> crate::interfaces::Result<()> {
match self.state {
CircuitBreakerState::Closed => Ok(()),
CircuitBreakerState::HalfOpen => {
self.state = CircuitBreakerState::Closed;
Ok(())
}
CircuitBreakerState::Open => {
if self.timeout < Utc::now() {
self.state = CircuitBreakerState::HalfOpen;
Ok(())
} else {
eprintln!("operation interrupted by circuit breaker!");
Err(Box::new(CircuitBreakerError))
}
}
}
}
fn succeed(&mut self) {
self.state = CircuitBreakerState::Closed;
}
fn trip(&mut self) {
self.state = CircuitBreakerState::Open;
}
}
You may want to move state-related logic into CircuitBreakerState
in a form of state pattern
, one of design patterns suggested by Gang of four.
As a circuit breaker is one of these cross-cutting concerns, you can imagine that the use case of this implementation will be something along the lines of:
pub struct CircuitBreakerRepositoryDecorator<C: TCircuitBreaker, R: TRepository> {
circuit_breaker: C,
repo: R,
}
impl<C: TCircuitBreaker, R: TRepository> TRepository for CircuitBreakerRepositoryDecorator<C, R> {
// say, you don't want to use circuit breaker on `get` method
fn get(&self, id: i64) -> DomainStruct {
self.repo.get(id)
}
// circuit breaker is used only for `add` method of repository
fn add(&mut self, aggregate: DomainStruct) -> Result<()> {
self.circuit_breaker.guard()?;
if let Err(err) = self.repo.add(aggregate) {
self.circuit_breaker.trip();
return Err(err);
};
self.circuit_breaker.succeed();
Ok(())
}
}
Due to the nature of circuit breaker, the implementation above may require some retouches so it could turn more into singleton lifetime, necessitating thread-safe implemenation. In Rust, thread-safe implementation is quite easy so I will leave it at that.