use anyhow::Result;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::{minilm::MiniLMEmbedder, Embedder};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
impl std::fmt::Display for CircuitState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CircuitState::Closed => write!(f, "closed"),
CircuitState::Open => write!(f, "open"),
CircuitState::HalfOpen => write!(f, "half_open"),
}
}
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerConfig {
pub failure_threshold: u32,
pub success_threshold: u32,
pub open_duration: Duration,
pub call_timeout: Duration,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
success_threshold: 2,
open_duration: Duration::from_secs(30),
call_timeout: Duration::from_secs(10),
}
}
}
struct CircuitBreakerState {
state: CircuitState,
consecutive_failures: u32,
consecutive_successes: u32,
last_failure_time: Option<Instant>,
last_state_change: Instant,
}
impl CircuitBreakerState {
fn new() -> Self {
Self {
state: CircuitState::Closed,
consecutive_failures: 0,
consecutive_successes: 0,
last_failure_time: None,
last_state_change: Instant::now(),
}
}
}
pub struct ResilientEmbedder {
inner: Arc<MiniLMEmbedder>,
config: CircuitBreakerConfig,
state: Mutex<CircuitBreakerState>,
total_calls: AtomicU64,
total_rejections: AtomicU64,
total_fallbacks: AtomicU64,
}
impl ResilientEmbedder {
pub fn new(embedder: Arc<MiniLMEmbedder>, config: CircuitBreakerConfig) -> Self {
Self {
inner: embedder,
config,
state: Mutex::new(CircuitBreakerState::new()),
total_calls: AtomicU64::new(0),
total_rejections: AtomicU64::new(0),
total_fallbacks: AtomicU64::new(0),
}
}
pub fn with_defaults(embedder: Arc<MiniLMEmbedder>) -> Self {
Self::new(embedder, CircuitBreakerConfig::default())
}
pub fn state(&self) -> CircuitState {
self.state.lock().state
}
pub fn metrics(&self) -> CircuitBreakerMetrics {
let state = self.state.lock();
CircuitBreakerMetrics {
state: state.state,
consecutive_failures: state.consecutive_failures,
consecutive_successes: state.consecutive_successes,
total_calls: self.total_calls.load(Ordering::Relaxed),
total_rejections: self.total_rejections.load(Ordering::Relaxed),
total_fallbacks: self.total_fallbacks.load(Ordering::Relaxed),
time_in_current_state: state.last_state_change.elapsed(),
}
}
fn should_allow_request(&self) -> bool {
let mut state = self.state.lock();
match state.state {
CircuitState::Closed => true,
CircuitState::Open => {
if state.last_state_change.elapsed() >= self.config.open_duration {
tracing::info!(
"Circuit breaker transitioning from Open to HalfOpen after {:?}",
self.config.open_duration
);
state.state = CircuitState::HalfOpen;
state.consecutive_successes = 0;
state.last_state_change = Instant::now();
self.record_state_change(CircuitState::HalfOpen);
true
} else {
false
}
}
CircuitState::HalfOpen => true,
}
}
fn record_success(&self) {
let mut state = self.state.lock();
state.consecutive_failures = 0;
state.consecutive_successes += 1;
if state.state == CircuitState::HalfOpen
&& state.consecutive_successes >= self.config.success_threshold
{
tracing::info!(
"Circuit breaker closing after {} consecutive successes",
state.consecutive_successes
);
state.state = CircuitState::Closed;
state.last_state_change = Instant::now();
self.record_state_change(CircuitState::Closed);
}
}
fn record_failure(&self) {
let mut state = self.state.lock();
state.consecutive_successes = 0;
state.consecutive_failures += 1;
state.last_failure_time = Some(Instant::now());
match state.state {
CircuitState::Closed => {
if state.consecutive_failures >= self.config.failure_threshold {
tracing::warn!(
"Circuit breaker opening after {} consecutive failures",
state.consecutive_failures
);
state.state = CircuitState::Open;
state.last_state_change = Instant::now();
self.record_state_change(CircuitState::Open);
}
}
CircuitState::HalfOpen => {
tracing::warn!("Circuit breaker returning to Open after failure in HalfOpen state");
state.state = CircuitState::Open;
state.last_state_change = Instant::now();
self.record_state_change(CircuitState::Open);
}
CircuitState::Open => {
}
}
}
fn record_state_change(&self, new_state: CircuitState) {
let label1 = format!("circuit_breaker_{new_state}");
let label2 = String::from("embedding");
crate::metrics::ERRORS_TOTAL
.with_label_values(&[&label1, &label2])
.inc();
}
fn generate_fallback(&self, text: &str) -> Vec<f32> {
self.total_fallbacks.fetch_add(1, Ordering::Relaxed);
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let dimension = self.inner.dimension();
let mut embedding = vec![0.0; dimension];
let words: Vec<&str> = text.split_whitespace().collect();
for (i, word) in words.iter().enumerate() {
let mut hasher = DefaultHasher::new();
word.hash(&mut hasher);
let hash = hasher.finish();
for j in 0..dimension {
let index = (i.wrapping_mul(7) + j) % dimension;
if j < 64 {
embedding[index] += ((hash >> j) & 1) as f32 * 0.1;
} else {
embedding[index] += ((hash >> (j % 64)) & 1) as f32 * 0.1;
}
}
}
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for val in &mut embedding {
*val /= norm;
}
}
embedding
}
}
impl Embedder for ResilientEmbedder {
fn encode(&self, text: &str) -> Result<Vec<f32>> {
self.total_calls.fetch_add(1, Ordering::Relaxed);
if text.is_empty() {
return Ok(vec![0.0; self.inner.dimension()]);
}
if !self.should_allow_request() {
self.total_rejections.fetch_add(1, Ordering::Relaxed);
tracing::debug!("Circuit breaker open, using fallback embedding");
return Ok(self.generate_fallback(text));
}
match self.inner.encode(text) {
Ok(embedding) => {
self.record_success();
Ok(embedding)
}
Err(e) => {
self.record_failure();
tracing::warn!("Embedding failed (circuit breaker tracking): {}", e);
Ok(self.generate_fallback(text))
}
}
}
fn dimension(&self) -> usize {
self.inner.dimension()
}
fn encode_batch(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
if !self.should_allow_request() {
self.total_rejections
.fetch_add(texts.len() as u64, Ordering::Relaxed);
return Ok(texts.iter().map(|t| self.generate_fallback(t)).collect());
}
let mut results = Vec::with_capacity(texts.len());
let mut any_success = false;
let mut any_failure = false;
for text in texts {
self.total_calls.fetch_add(1, Ordering::Relaxed);
match self.inner.encode(text) {
Ok(embedding) => {
any_success = true;
results.push(embedding);
}
Err(_) => {
any_failure = true;
results.push(self.generate_fallback(text));
}
}
}
if any_failure && !any_success {
self.record_failure();
} else if any_success {
self.record_success();
}
Ok(results)
}
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerMetrics {
pub state: CircuitState,
pub consecutive_failures: u32,
pub consecutive_successes: u32,
pub total_calls: u64,
pub total_rejections: u64,
pub total_fallbacks: u64,
pub time_in_current_state: Duration,
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_embedder() -> Arc<MiniLMEmbedder> {
let config = super::super::minilm::EmbeddingConfig {
model_path: PathBuf::from("dummy.onnx"),
tokenizer_path: PathBuf::from("dummy.json"),
max_length: 256,
use_quantized: true,
embed_timeout_ms: 5000,
};
Arc::new(MiniLMEmbedder::new_simplified(config).unwrap())
}
#[test]
fn test_circuit_breaker_starts_closed() {
let embedder = create_test_embedder();
let resilient = ResilientEmbedder::with_defaults(embedder);
assert_eq!(resilient.state(), CircuitState::Closed);
}
#[test]
fn test_circuit_breaker_metrics() {
let embedder = create_test_embedder();
let resilient = ResilientEmbedder::with_defaults(embedder);
let _ = resilient.encode("test");
let metrics = resilient.metrics();
assert!(metrics.total_calls >= 1);
assert_eq!(metrics.state, CircuitState::Closed);
}
#[test]
fn test_fallback_generates_valid_embedding() {
let embedder = create_test_embedder();
let resilient = ResilientEmbedder::with_defaults(embedder);
let fallback = resilient.generate_fallback("test input");
assert_eq!(fallback.len(), 384);
let norm: f32 = fallback.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 1e-5 || norm == 0.0);
}
}