use std::sync::{Mutex, PoisonError};
use crate::core::platform::{Duration, SystemTime};
#[derive(Default)]
pub(super) struct Breaker {
state: Mutex<BreakerState>,
}
#[derive(Default)]
struct BreakerState {
consecutive: u32,
open_until: Option<SystemTime>,
}
impl Breaker {
pub(super) fn try_acquire(&self, threshold: u32) -> bool {
if threshold == 0 {
return true;
}
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(open_until) = state.open_until {
if SystemTime::now() < open_until {
return false;
}
state.open_until = None;
}
true
}
pub(super) fn record_failure(&self, threshold: u32, cooldown: Duration) {
if threshold == 0 {
return;
}
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state.consecutive = state.consecutive.saturating_add(1);
if state.consecutive >= threshold {
state.open_until = Some(SystemTime::now() + cooldown);
}
}
pub(super) fn reset(&self) {
*self.state.lock().unwrap_or_else(PoisonError::into_inner) = BreakerState::default();
}
}
#[cfg(test)]
mod breaker_tests {
use super::Breaker;
use crate::core::platform::Duration;
const COOLDOWN: Duration = Duration::from_mins(1);
#[test]
fn opens_after_threshold_consecutive_failures() {
let breaker = Breaker::default();
assert!(breaker.try_acquire(3));
breaker.record_failure(3, COOLDOWN);
breaker.record_failure(3, COOLDOWN);
assert!(breaker.try_acquire(3));
breaker.record_failure(3, COOLDOWN);
assert!(!breaker.try_acquire(3));
}
#[test]
fn reset_closes_an_open_breaker() {
let breaker = Breaker::default();
breaker.record_failure(1, COOLDOWN);
assert!(!breaker.try_acquire(1));
breaker.reset();
assert!(breaker.try_acquire(1));
}
#[test]
fn zero_threshold_never_opens() {
let breaker = Breaker::default();
for _ in 0..10 {
breaker.record_failure(0, COOLDOWN);
}
assert!(breaker.try_acquire(0));
}
#[tokio::test]
async fn half_opens_one_trial_after_cooldown() {
let breaker = Breaker::default();
breaker.record_failure(1, Duration::from_millis(10));
assert!(!breaker.try_acquire(1));
crate::core::platform::sleep(std::time::Duration::from_millis(25)).await;
assert!(breaker.try_acquire(1));
breaker.record_failure(1, Duration::from_millis(10));
assert!(!breaker.try_acquire(1));
}
}