#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::application::Application;
use crate::middleware::Middleware;
use crate::redis_protocol::{RespConn, RespReply};
use crate::request::Request;
use crate::response::Response;
use crate::server::ConnectionInfo;
#[derive(Debug, Clone, PartialEq)]
pub enum BreakerState {
Closed,
Open,
HalfOpen,
}
struct BackendEntry {
state: BreakerState,
failures: u32,
opened_at: Option<Instant>,
}
impl BackendEntry {
fn new() -> Self {
Self { state: BreakerState::Closed, failures: 0, opened_at: None }
}
}
pub struct CircuitBreaker {
backends: HashMap<String, BackendEntry>,
failure_threshold: u32,
recovery: Duration,
}
impl CircuitBreaker {
pub fn new(failure_threshold: u32, recovery_secs: u64) -> Self {
Self {
backends: HashMap::new(),
failure_threshold,
recovery: Duration::from_secs(recovery_secs),
}
}
pub fn is_available(&mut self, backend: &str) -> bool {
let entry = self.backends.entry(backend.to_string()).or_insert_with(BackendEntry::new);
match entry.state {
BreakerState::Closed => true,
BreakerState::HalfOpen => true,
BreakerState::Open => {
if let Some(opened_at) = entry.opened_at {
if opened_at.elapsed() >= self.recovery {
entry.state = BreakerState::HalfOpen;
entry.opened_at = None;
return true;
}
}
false
}
}
}
pub fn record_success(&mut self, backend: &str) {
let entry = self.backends.entry(backend.to_string()).or_insert_with(BackendEntry::new);
entry.state = BreakerState::Closed;
entry.failures = 0;
entry.opened_at = None;
}
pub fn record_failure(&mut self, backend: &str) {
let threshold = self.failure_threshold;
let entry = self.backends.entry(backend.to_string()).or_insert_with(BackendEntry::new);
match entry.state {
BreakerState::Closed => {
entry.failures += 1;
if entry.failures >= threshold {
entry.state = BreakerState::Open;
entry.opened_at = Some(Instant::now());
}
}
BreakerState::HalfOpen => {
entry.state = BreakerState::Open;
entry.opened_at = Some(Instant::now());
}
BreakerState::Open => {
entry.opened_at = Some(Instant::now());
}
}
}
pub fn reset(&mut self, backend: &str) {
let entry = self.backends.entry(backend.to_string()).or_insert_with(BackendEntry::new);
entry.state = BreakerState::Closed;
entry.failures = 0;
entry.opened_at = None;
}
pub fn state(&self, backend: &str) -> BreakerState {
self.backends
.get(backend)
.map(|e| e.state.clone())
.unwrap_or(BreakerState::Closed)
}
}
static GLOBAL_BREAKER: OnceLock<Mutex<CircuitBreaker>> = OnceLock::new();
pub fn global() -> &'static Mutex<CircuitBreaker> {
GLOBAL_BREAKER.get_or_init(|| Mutex::new(CircuitBreaker::new(5, 30)))
}
pub struct RedisCircuitBreaker {
conn: Arc<RespConn>,
failure_threshold: AtomicU32,
recovery_secs: AtomicU64,
}
impl Clone for RedisCircuitBreaker {
fn clone(&self) -> Self {
RedisCircuitBreaker {
conn: Arc::clone(&self.conn),
failure_threshold: AtomicU32::new(self.failure_threshold.load(Ordering::Relaxed)),
recovery_secs: AtomicU64::new(self.recovery_secs.load(Ordering::Relaxed)),
}
}
}
impl RedisCircuitBreaker {
pub fn new(addr: impl Into<String>, password: Option<String>, failure_threshold: u32, recovery_secs: u64) -> Self {
RedisCircuitBreaker {
conn: Arc::new(RespConn::new(addr, password)),
failure_threshold: AtomicU32::new(failure_threshold),
recovery_secs: AtomicU64::new(recovery_secs),
}
}
pub fn from_env() -> Self {
let host = std::env::var("RWS_REDIS_HOST").unwrap_or_else(|_| "127.0.0.1".into());
let port = std::env::var("RWS_REDIS_PORT").unwrap_or_else(|_| "6379".into());
let addr = format!("{}:{}", host, port);
let password = std::env::var("RWS_REDIS_PASSWORD").ok();
let failure_threshold = std::env::var("RWS_CONFIG_CIRCUIT_BREAKER_FAILURE_THRESHOLD")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5);
let recovery_secs = std::env::var("RWS_CONFIG_CIRCUIT_BREAKER_RECOVERY_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(30);
Self::new(addr, password, failure_threshold, recovery_secs)
}
pub fn set_limits(&self, failure_threshold: u32, recovery_secs: u64) {
self.failure_threshold.store(failure_threshold, Ordering::Relaxed);
self.recovery_secs.store(recovery_secs, Ordering::Relaxed);
}
fn redis_key(backend: &str) -> Vec<u8> {
format!("rws:cb:{}", backend).into_bytes()
}
fn load(&self, backend: &str) -> std::io::Result<(BreakerState, u32, u64)> {
match self.conn.cmd(&[b"GET", &Self::redis_key(backend)])? {
RespReply::Bulk(Some(bytes)) => Ok(decode_entry(&bytes)),
_ => Ok((BreakerState::Closed, 0, 0)),
}
}
fn store(&self, backend: &str, state: &BreakerState, failures: u32, opened_at: u64) -> std::io::Result<()> {
let encoded = encode_entry(state, failures, opened_at);
self.conn.cmd(&[b"SET", &Self::redis_key(backend), encoded.as_bytes()])?;
Ok(())
}
pub fn is_available(&self, backend: &str) -> std::io::Result<bool> {
let (state, failures, opened_at) = self.load(backend)?;
match state {
BreakerState::Closed | BreakerState::HalfOpen => Ok(true),
BreakerState::Open => {
let recovery = self.recovery_secs.load(Ordering::Relaxed);
if now_unix().saturating_sub(opened_at) >= recovery {
self.store(backend, &BreakerState::HalfOpen, failures, 0)?;
Ok(true)
} else {
Ok(false)
}
}
}
}
pub fn record_success(&self, backend: &str) -> std::io::Result<()> {
self.store(backend, &BreakerState::Closed, 0, 0)
}
pub fn record_failure(&self, backend: &str) -> std::io::Result<()> {
let (state, failures, _) = self.load(backend)?;
match state {
BreakerState::Closed => {
let failures = failures + 1;
if failures >= self.failure_threshold.load(Ordering::Relaxed) {
self.store(backend, &BreakerState::Open, failures, now_unix())
} else {
self.store(backend, &BreakerState::Closed, failures, 0)
}
}
BreakerState::HalfOpen | BreakerState::Open => {
self.store(backend, &BreakerState::Open, failures, now_unix())
}
}
}
pub fn reset(&self, backend: &str) -> std::io::Result<()> {
self.conn.cmd(&[b"DEL", &Self::redis_key(backend)])?;
Ok(())
}
pub fn state(&self, backend: &str) -> std::io::Result<BreakerState> {
Ok(self.load(backend)?.0)
}
}
fn now_unix() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
fn encode_entry(state: &BreakerState, failures: u32, opened_at: u64) -> String {
let state_str = match state {
BreakerState::Closed => "closed",
BreakerState::Open => "open",
BreakerState::HalfOpen => "half_open",
};
format!("{}|{}|{}", state_str, failures, opened_at)
}
fn decode_entry(raw: &[u8]) -> (BreakerState, u32, u64) {
let text = String::from_utf8_lossy(raw);
let mut parts = text.splitn(3, '|');
let state = match parts.next() {
Some("open") => BreakerState::Open,
Some("half_open") => BreakerState::HalfOpen,
_ => BreakerState::Closed,
};
let failures = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let opened_at = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
(state, failures, opened_at)
}
pub struct RetryLayer {
max_retries: u32,
retry_on: Vec<i16>,
}
impl RetryLayer {
pub fn new() -> Self {
Self { max_retries: 3, retry_on: vec![502, 503, 504] }
}
pub fn max_retries(mut self, n: u32) -> Self {
self.max_retries = n;
self
}
pub fn retry_on(mut self, codes: Vec<i16>) -> Self {
self.retry_on = codes;
self
}
}
impl Default for RetryLayer {
fn default() -> Self {
Self::new()
}
}
impl Middleware for RetryLayer {
fn handle(
&self,
request: &Request,
connection: &ConnectionInfo,
next: &dyn Application,
) -> Result<Response, String> {
let mut response = next.execute(request, connection)?;
let mut attempts = 0u32;
while attempts < self.max_retries && self.retry_on.contains(&response.status_code) {
response = next.execute(request, connection)?;
attempts += 1;
}
Ok(response)
}
}