pub mod adaptive_rate_limiter;
pub mod circuit_breaker;
pub mod rate_limiter;
use std::sync::Arc;
use std::time::Duration;
use once_cell::sync::Lazy;
use vtcode_commons::ErrorCategory;
use self::adaptive_rate_limiter::{AdaptiveRateLimiter, Priority};
use self::circuit_breaker::CircuitBreaker;
#[derive(Debug, Clone, Copy)]
pub enum CallOutcome {
Success,
InvalidArgument,
ExecutionError,
Cancelled,
}
impl CallOutcome {
fn to_error_category(self) -> Option<ErrorCategory> {
match self {
CallOutcome::Success => None,
CallOutcome::InvalidArgument => Some(ErrorCategory::InvalidParameters),
CallOutcome::ExecutionError | CallOutcome::Cancelled => {
Some(ErrorCategory::ExecutionError)
}
}
}
}
pub struct ToolResilience {
rate_limiter: AdaptiveRateLimiter,
circuit_breaker: CircuitBreaker,
}
impl ToolResilience {
pub fn new(rate_limiter: AdaptiveRateLimiter, circuit_breaker: CircuitBreaker) -> Self {
Self {
rate_limiter,
circuit_breaker,
}
}
pub fn try_acquire(&self, tool_name: &str, priority: Priority) -> Result<(), Duration> {
if !self.circuit_breaker.allow_request_for_tool(tool_name) {
let backoff = self
.circuit_breaker
.remaining_backoff(tool_name)
.unwrap_or_else(|| Duration::from_millis(100));
return Err(backoff);
}
self.rate_limiter.set_priority(tool_name, priority);
self.rate_limiter.try_acquire(tool_name)
}
pub fn record_success(&self, tool_name: &str) {
self.circuit_breaker.record_success_for_tool(tool_name);
}
pub fn record_outcome(&self, tool_name: &str, outcome: CallOutcome) {
match outcome.to_error_category() {
None => self.circuit_breaker.record_success_for_tool(tool_name),
Some(category) => self
.circuit_breaker
.record_failure_category_for_tool(tool_name, category),
}
}
pub fn circuit_snapshot(&self) -> circuit_breaker::CircuitBreakerSnapshot {
self.circuit_breaker.snapshot()
}
}
pub static GLOBAL_TOOL_RESILIENCE: Lazy<Arc<ToolResilience>> = Lazy::new(|| {
Arc::new(ToolResilience::new(
AdaptiveRateLimiter::default(),
CircuitBreaker::default(),
))
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn facade_records_success_and_failures() {
let resilience = ToolResilience::new(
AdaptiveRateLimiter::new(8.0, 4.0),
CircuitBreaker::new(circuit_breaker::CircuitBreakerConfig {
failure_threshold: 2,
..Default::default()
}),
);
resilience
.try_acquire("alpha", Priority::Normal)
.expect("first call allowed");
resilience.record_outcome("alpha", CallOutcome::ExecutionError);
resilience
.try_acquire("alpha", Priority::Normal)
.expect("second call allowed");
resilience.record_outcome("alpha", CallOutcome::ExecutionError);
let third = resilience.try_acquire("alpha", Priority::Normal);
assert!(third.is_err(), "circuit should be open after 2 failures");
}
#[test]
fn invalid_argument_does_not_trip_breaker() {
let resilience = ToolResilience::new(
AdaptiveRateLimiter::new(8.0, 4.0),
CircuitBreaker::new(circuit_breaker::CircuitBreakerConfig {
failure_threshold: 1,
..Default::default()
}),
);
for _ in 0..3 {
resilience
.try_acquire("beta", Priority::Normal)
.expect("call allowed");
resilience.record_outcome("beta", CallOutcome::InvalidArgument);
}
assert!(resilience.try_acquire("beta", Priority::Normal).is_ok());
}
}