use std::fmt;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum CircuitState {
Closed = 0,
Open = 1,
HalfOpen = 2,
}
impl CircuitState {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(CircuitState::Closed),
1 => Some(CircuitState::Open),
2 => Some(CircuitState::HalfOpen),
_ => None,
}
}
pub fn allows_requests(&self) -> bool {
match self {
CircuitState::Closed => true,
CircuitState::Open => false,
CircuitState::HalfOpen => true, }
}
pub fn is_unhealthy(&self) -> bool {
match self {
CircuitState::Closed => false,
CircuitState::Open | CircuitState::HalfOpen => true,
}
}
}
impl fmt::Display for CircuitState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CircuitState::Closed => write!(f, "closed"),
CircuitState::Open => write!(f, "open"),
CircuitState::HalfOpen => write!(f, "half_open"),
}
}
}
impl std::str::FromStr for CircuitState {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"closed" => Ok(CircuitState::Closed),
"open" => Ok(CircuitState::Open),
"half_open" | "halfopen" | "half-open" => Ok(CircuitState::HalfOpen),
_ => Err(format!("Unknown circuit state: {}", s)),
}
}
}
#[derive(Debug, Clone)]
pub struct StateTransition {
pub from: CircuitState,
pub to: CircuitState,
pub timestamp: Instant,
pub reason: TransitionReason,
}
impl StateTransition {
pub fn new(from: CircuitState, to: CircuitState, reason: TransitionReason) -> Self {
Self {
from,
to,
timestamp: Instant::now(),
reason,
}
}
pub fn elapsed(&self) -> Duration {
self.timestamp.elapsed()
}
}
impl fmt::Display for StateTransition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} -> {} ({})", self.from, self.to, self.reason)
}
}
#[derive(Debug, Clone)]
pub enum TransitionReason {
FailureThresholdExceeded {
failure_count: u32,
threshold: u32,
},
CooldownElapsed {
cooldown: Duration,
},
ProbeSucceeded {
success_count: u32,
threshold: u32,
},
ProbeFailed {
error: String,
},
ManualForce {
admin: Option<String>,
},
Reset,
AdaptiveAdjustment {
old_threshold: u32,
new_threshold: u32,
},
ReplicationLagExceeded {
lag: Duration,
threshold: Duration,
},
}
impl fmt::Display for TransitionReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransitionReason::FailureThresholdExceeded {
failure_count,
threshold,
} => write!(f, "{} failures (threshold: {})", failure_count, threshold),
TransitionReason::CooldownElapsed { cooldown } => {
write!(f, "cooldown elapsed ({:?})", cooldown)
}
TransitionReason::ProbeSucceeded {
success_count,
threshold,
} => write!(
f,
"{} successful probes (threshold: {})",
success_count, threshold
),
TransitionReason::ProbeFailed { error } => write!(f, "probe failed: {}", error),
TransitionReason::ManualForce { admin } => {
if let Some(admin) = admin {
write!(f, "manual force by {}", admin)
} else {
write!(f, "manual force")
}
}
TransitionReason::Reset => write!(f, "reset"),
TransitionReason::AdaptiveAdjustment {
old_threshold,
new_threshold,
} => write!(
f,
"adaptive adjustment {} -> {}",
old_threshold, new_threshold
),
TransitionReason::ReplicationLagExceeded { lag, threshold } => {
write!(f, "replication lag {:?} > {:?}", lag, threshold)
}
}
}
}
#[derive(Debug, Clone)]
pub enum CircuitEvent {
Opened {
node_id: String,
reason: TransitionReason,
failure_count: u32,
},
HalfOpened {
node_id: String,
cooldown_elapsed: Duration,
},
Closed {
node_id: String,
reason: TransitionReason,
recovery_time: Duration,
},
FailureRecorded {
node_id: String,
error: String,
failure_count: u32,
},
ProbeAttempt {
node_id: String,
attempt: u32,
},
ProbeResult {
node_id: String,
success: bool,
duration: Duration,
},
}
impl CircuitEvent {
pub fn node_id(&self) -> &str {
match self {
CircuitEvent::Opened { node_id, .. }
| CircuitEvent::HalfOpened { node_id, .. }
| CircuitEvent::Closed { node_id, .. }
| CircuitEvent::FailureRecorded { node_id, .. }
| CircuitEvent::ProbeAttempt { node_id, .. }
| CircuitEvent::ProbeResult { node_id, .. } => node_id,
}
}
pub fn event_type(&self) -> &'static str {
match self {
CircuitEvent::Opened { .. } => "opened",
CircuitEvent::HalfOpened { .. } => "half_opened",
CircuitEvent::Closed { .. } => "closed",
CircuitEvent::FailureRecorded { .. } => "failure_recorded",
CircuitEvent::ProbeAttempt { .. } => "probe_attempt",
CircuitEvent::ProbeResult { .. } => "probe_result",
}
}
}
pub trait CircuitBreakerListener: Send + Sync {
fn on_event(&self, event: CircuitEvent);
}
#[derive(Debug, Default)]
pub struct NoOpListener;
impl CircuitBreakerListener for NoOpListener {
fn on_event(&self, _event: CircuitEvent) {}
}
#[derive(Debug, Default)]
pub struct LoggingListener;
impl CircuitBreakerListener for LoggingListener {
fn on_event(&self, event: CircuitEvent) {
match &event {
CircuitEvent::Opened {
node_id,
reason,
failure_count,
} => {
tracing::warn!(
node_id = %node_id,
reason = %reason,
failure_count = failure_count,
"Circuit breaker opened"
);
}
CircuitEvent::HalfOpened {
node_id,
cooldown_elapsed,
} => {
tracing::info!(
node_id = %node_id,
cooldown_elapsed = ?cooldown_elapsed,
"Circuit breaker half-opened"
);
}
CircuitEvent::Closed {
node_id,
reason,
recovery_time,
} => {
tracing::info!(
node_id = %node_id,
reason = %reason,
recovery_time = ?recovery_time,
"Circuit breaker closed"
);
}
CircuitEvent::FailureRecorded {
node_id,
error,
failure_count,
} => {
tracing::debug!(
node_id = %node_id,
error = %error,
failure_count = failure_count,
"Failure recorded"
);
}
CircuitEvent::ProbeAttempt { node_id, attempt } => {
tracing::debug!(
node_id = %node_id,
attempt = attempt,
"Probe attempt"
);
}
CircuitEvent::ProbeResult {
node_id,
success,
duration,
} => {
tracing::debug!(
node_id = %node_id,
success = success,
duration = ?duration,
"Probe result"
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circuit_state_from_u8() {
assert_eq!(CircuitState::from_u8(0), Some(CircuitState::Closed));
assert_eq!(CircuitState::from_u8(1), Some(CircuitState::Open));
assert_eq!(CircuitState::from_u8(2), Some(CircuitState::HalfOpen));
assert_eq!(CircuitState::from_u8(3), None);
}
#[test]
fn test_circuit_state_allows_requests() {
assert!(CircuitState::Closed.allows_requests());
assert!(!CircuitState::Open.allows_requests());
assert!(CircuitState::HalfOpen.allows_requests());
}
#[test]
fn test_circuit_state_display() {
assert_eq!(CircuitState::Closed.to_string(), "closed");
assert_eq!(CircuitState::Open.to_string(), "open");
assert_eq!(CircuitState::HalfOpen.to_string(), "half_open");
}
#[test]
fn test_circuit_state_parse() {
assert_eq!("closed".parse::<CircuitState>().unwrap(), CircuitState::Closed);
assert_eq!("OPEN".parse::<CircuitState>().unwrap(), CircuitState::Open);
assert_eq!(
"half_open".parse::<CircuitState>().unwrap(),
CircuitState::HalfOpen
);
assert_eq!(
"half-open".parse::<CircuitState>().unwrap(),
CircuitState::HalfOpen
);
}
#[test]
fn test_state_transition() {
let transition = StateTransition::new(
CircuitState::Closed,
CircuitState::Open,
TransitionReason::FailureThresholdExceeded {
failure_count: 5,
threshold: 5,
},
);
assert_eq!(transition.from, CircuitState::Closed);
assert_eq!(transition.to, CircuitState::Open);
assert!(transition.elapsed().as_nanos() > 0);
}
#[test]
fn test_transition_reason_display() {
let reason = TransitionReason::FailureThresholdExceeded {
failure_count: 5,
threshold: 5,
};
assert_eq!(reason.to_string(), "5 failures (threshold: 5)");
let reason = TransitionReason::CooldownElapsed {
cooldown: Duration::from_secs(10),
};
assert_eq!(reason.to_string(), "cooldown elapsed (10s)");
}
#[test]
fn test_circuit_event_node_id() {
let event = CircuitEvent::Opened {
node_id: "test-node".to_string(),
reason: TransitionReason::Reset,
failure_count: 0,
};
assert_eq!(event.node_id(), "test-node");
assert_eq!(event.event_type(), "opened");
}
}