Implementing Circuit Breaker

Migo·2023년 12월 16일
0

Fluent Rust

목록 보기
17/18
post-thumbnail

Circuit breaker is what enables preventing your application from hammering down the already-downed system.

Abstraction

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);
}

Implementation

The state of circuit breaker is usually presnted with the following:

  • Closed : indicates that the circuit's closed and messages can flow
  • Open : 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.

Notes

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(())
    }
}

improvement point

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.

profile
Dude with existential crisis

0개의 댓글