use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
pub const DEFAULT_MAX_SPEND_PER_TX: Decimal = dec!(1000);
pub const DEFAULT_DAILY_SPEND_LIMIT: Decimal = dec!(10000);
pub const DEFAULT_MONTHLY_SPEND_LIMIT: Decimal = dec!(100000);
pub const DEFAULT_MAX_FAILURE_RATE: Decimal = dec!(0.3);
pub const DEFAULT_FAILURE_WINDOW_MS: u64 = 300_000;
pub const DEFAULT_COOLDOWN_MS: u64 = 60_000;
pub const DEFAULT_HALF_OPEN_MAX_TXNS: u32 = 3;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircuitBreakerConfig {
pub max_spend_per_tx: Decimal,
pub daily_spend_limit: Decimal,
pub monthly_spend_limit: Decimal,
pub max_failure_rate: Decimal,
pub failure_window_ms: u64,
pub cooldown_ms: u64,
pub half_open_max_txns: u32,
pub global_kill_switch: bool,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
max_spend_per_tx: DEFAULT_MAX_SPEND_PER_TX,
daily_spend_limit: DEFAULT_DAILY_SPEND_LIMIT,
monthly_spend_limit: DEFAULT_MONTHLY_SPEND_LIMIT,
max_failure_rate: DEFAULT_MAX_FAILURE_RATE,
failure_window_ms: DEFAULT_FAILURE_WINDOW_MS,
cooldown_ms: DEFAULT_COOLDOWN_MS,
half_open_max_txns: DEFAULT_HALF_OPEN_MAX_TXNS,
global_kill_switch: false,
}
}
}
impl CircuitBreakerConfig {
#[must_use]
pub const fn with_max_spend_per_tx(mut self, limit: Decimal) -> Self {
self.max_spend_per_tx = limit;
self
}
#[must_use]
pub const fn with_daily_limit(mut self, limit: Decimal) -> Self {
self.daily_spend_limit = limit;
self
}
#[must_use]
pub const fn with_monthly_limit(mut self, limit: Decimal) -> Self {
self.monthly_spend_limit = limit;
self
}
#[must_use]
pub const fn with_max_failure_rate(mut self, rate: Decimal) -> Self {
self.max_failure_rate = rate;
self
}
#[must_use]
pub const fn with_kill_switch(mut self) -> Self {
self.global_kill_switch = true;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let cfg = CircuitBreakerConfig::default();
assert_eq!(cfg.max_spend_per_tx, dec!(1000));
assert_eq!(cfg.daily_spend_limit, dec!(10000));
assert_eq!(cfg.monthly_spend_limit, dec!(100000));
assert_eq!(cfg.max_failure_rate, dec!(0.3));
assert_eq!(cfg.failure_window_ms, 300_000);
assert_eq!(cfg.cooldown_ms, 60_000);
assert_eq!(cfg.half_open_max_txns, 3);
assert!(!cfg.global_kill_switch);
}
#[test]
fn config_builder_pattern() {
let cfg = CircuitBreakerConfig::default()
.with_max_spend_per_tx(dec!(500))
.with_daily_limit(dec!(5000))
.with_monthly_limit(dec!(50000))
.with_max_failure_rate(dec!(0.2));
assert_eq!(cfg.max_spend_per_tx, dec!(500));
assert_eq!(cfg.daily_spend_limit, dec!(5000));
assert_eq!(cfg.monthly_spend_limit, dec!(50000));
assert_eq!(cfg.max_failure_rate, dec!(0.2));
}
#[test]
fn kill_switch() {
let cfg = CircuitBreakerConfig::default().with_kill_switch();
assert!(cfg.global_kill_switch);
}
#[test]
fn config_serde_roundtrip() {
let cfg = CircuitBreakerConfig::default();
let json = serde_json::to_string(&cfg).unwrap();
let parsed: CircuitBreakerConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.max_spend_per_tx, cfg.max_spend_per_tx);
assert_eq!(parsed.cooldown_ms, cfg.cooldown_ms);
}
}