refactor: comprehensive audit — fix bugs, harden security, deduplicate providers, add CI/Docker
Phase 1: Fix compilation (config_path Option<PathBuf>, streaming test, stale test cleanup) Phase 2: Fix critical bugs (remove block_on deadlocks in 4 providers, fix broken SQL query builder) Phase 3: Security hardening (session manager, real auth, token masking, Gemini key to header, password policy) Phase 4: Implement stubs (real provider test, /proc health metrics, client/provider/backup endpoints, has_images) Phase 5: Code quality (shared provider helpers, explicit re-exports, all Clippy warnings fixed, unwrap removal, 6 unused deps removed, dashboard split into 7 sub-modules) Phase 6: Infrastructure (GitHub Actions CI, multi-stage Dockerfile, rustfmt.toml, clippy.toml, script fixes)
This commit is contained in:
@@ -5,12 +5,12 @@
|
||||
//! 2. Provider circuit breaking to handle API failures
|
||||
//! 3. Global rate limiting for overall system protection
|
||||
|
||||
use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, warn};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Rate limiter configuration
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -26,8 +26,8 @@ pub struct RateLimiterConfig {
|
||||
impl Default for RateLimiterConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
requests_per_minute: 60, // 1 request per second per client
|
||||
burst_size: 10, // Allow bursts of up to 10 requests
|
||||
requests_per_minute: 60, // 1 request per second per client
|
||||
burst_size: 10, // Allow bursts of up to 10 requests
|
||||
global_requests_per_minute: 600, // 10 requests per second globally
|
||||
}
|
||||
}
|
||||
@@ -36,9 +36,9 @@ impl Default for RateLimiterConfig {
|
||||
/// Circuit breaker state
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CircuitState {
|
||||
Closed, // Normal operation
|
||||
Open, // Circuit is open, requests fail fast
|
||||
HalfOpen, // Testing if service has recovered
|
||||
Closed, // Normal operation
|
||||
Open, // Circuit is open, requests fail fast
|
||||
HalfOpen, // Testing if service has recovered
|
||||
}
|
||||
|
||||
/// Circuit breaker configuration
|
||||
@@ -57,10 +57,10 @@ pub struct CircuitBreakerConfig {
|
||||
impl Default for CircuitBreakerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
failure_threshold: 5, // 5 failures
|
||||
failure_window_secs: 60, // within 60 seconds
|
||||
reset_timeout_secs: 30, // wait 30 seconds before half-open
|
||||
success_threshold: 3, // 3 successes to close circuit
|
||||
failure_threshold: 5, // 5 failures
|
||||
failure_window_secs: 60, // within 60 seconds
|
||||
reset_timeout_secs: 30, // wait 30 seconds before half-open
|
||||
success_threshold: 3, // 3 successes to close circuit
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,14 +88,14 @@ impl TokenBucket {
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(self.last_refill).as_secs_f64();
|
||||
let new_tokens = elapsed * self.refill_rate;
|
||||
|
||||
|
||||
self.tokens = (self.tokens + new_tokens).min(self.capacity);
|
||||
self.last_refill = now;
|
||||
}
|
||||
|
||||
fn try_acquire(&mut self, tokens: f64) -> bool {
|
||||
self.refill();
|
||||
|
||||
|
||||
if self.tokens >= tokens {
|
||||
self.tokens -= tokens;
|
||||
true
|
||||
@@ -175,18 +175,18 @@ impl ProviderCircuitBreaker {
|
||||
/// Record a failed request
|
||||
pub fn record_failure(&mut self) {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
|
||||
// Check if failure window has expired
|
||||
if let Some(last_failure) = self.last_failure_time {
|
||||
if now.duration_since(last_failure).as_secs() > self.config.failure_window_secs {
|
||||
// Reset failure count if window expired
|
||||
self.failure_count = 0;
|
||||
}
|
||||
if let Some(last_failure) = self.last_failure_time
|
||||
&& now.duration_since(last_failure).as_secs() > self.config.failure_window_secs
|
||||
{
|
||||
// Reset failure count if window expired
|
||||
self.failure_count = 0;
|
||||
}
|
||||
|
||||
|
||||
self.failure_count += 1;
|
||||
self.last_failure_time = Some(now);
|
||||
|
||||
|
||||
if self.failure_count >= self.config.failure_threshold && self.state == CircuitState::Closed {
|
||||
self.state = CircuitState::Open;
|
||||
self.last_state_change = now;
|
||||
@@ -220,7 +220,7 @@ impl RateLimitManager {
|
||||
pub fn new(config: RateLimiterConfig, circuit_config: CircuitBreakerConfig) -> Self {
|
||||
// Convert requests per minute to tokens per second
|
||||
let global_refill_rate = config.global_requests_per_minute as f64 / 60.0;
|
||||
|
||||
|
||||
Self {
|
||||
client_buckets: Arc::new(RwLock::new(HashMap::new())),
|
||||
global_bucket: Arc::new(RwLock::new(TokenBucket::new(
|
||||
@@ -243,18 +243,16 @@ impl RateLimitManager {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check client-specific rate limit
|
||||
let mut buckets = self.client_buckets.write().await;
|
||||
let bucket = buckets
|
||||
.entry(client_id.to_string())
|
||||
.or_insert_with(|| {
|
||||
TokenBucket::new(
|
||||
self.config.burst_size as f64,
|
||||
self.config.requests_per_minute as f64 / 60.0,
|
||||
)
|
||||
});
|
||||
|
||||
let bucket = buckets.entry(client_id.to_string()).or_insert_with(|| {
|
||||
TokenBucket::new(
|
||||
self.config.burst_size as f64,
|
||||
self.config.requests_per_minute as f64 / 60.0,
|
||||
)
|
||||
});
|
||||
|
||||
Ok(bucket.try_acquire(1.0))
|
||||
}
|
||||
|
||||
@@ -264,7 +262,7 @@ impl RateLimitManager {
|
||||
let breaker = breakers
|
||||
.entry(provider_name.to_string())
|
||||
.or_insert_with(|| ProviderCircuitBreaker::new(self.circuit_config.clone()));
|
||||
|
||||
|
||||
Ok(breaker.allow_request())
|
||||
}
|
||||
|
||||
@@ -282,7 +280,7 @@ impl RateLimitManager {
|
||||
let breaker = breakers
|
||||
.entry(provider_name.to_string())
|
||||
.or_insert_with(|| ProviderCircuitBreaker::new(self.circuit_config.clone()));
|
||||
|
||||
|
||||
breaker.record_failure();
|
||||
}
|
||||
|
||||
@@ -299,14 +297,13 @@ impl RateLimitManager {
|
||||
/// Axum middleware for rate limiting
|
||||
pub mod middleware {
|
||||
use super::*;
|
||||
use crate::errors::AppError;
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use crate::errors::AppError;
|
||||
use crate::state::AppState;
|
||||
|
||||
|
||||
/// Rate limiting middleware
|
||||
pub async fn rate_limit_middleware(
|
||||
@@ -319,41 +316,35 @@ pub mod middleware {
|
||||
|
||||
// Check rate limits
|
||||
if !state.rate_limit_manager.check_client_request(&client_id).await? {
|
||||
return Err(AppError::RateLimitError(
|
||||
"Rate limit exceeded".to_string()
|
||||
));
|
||||
return Err(AppError::RateLimitError("Rate limit exceeded".to_string()));
|
||||
}
|
||||
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
|
||||
/// Extract client ID from request (helper function)
|
||||
fn extract_client_id_from_request(request: &Request) -> String {
|
||||
// Try to extract from Authorization header
|
||||
if let Some(auth_header) = request.headers().get("Authorization") {
|
||||
if let Ok(auth_str) = auth_header.to_str() {
|
||||
if auth_str.starts_with("Bearer ") {
|
||||
let token = &auth_str[7..];
|
||||
// Use token hash as client ID (same logic as auth module)
|
||||
return format!("client_{}", &token[..8.min(token.len())]);
|
||||
}
|
||||
}
|
||||
if let Some(auth_header) = request.headers().get("Authorization")
|
||||
&& let Ok(auth_str) = auth_header.to_str()
|
||||
&& let Some(token) = auth_str.strip_prefix("Bearer ")
|
||||
{
|
||||
// Use token hash as client ID (same logic as auth module)
|
||||
return format!("client_{}", &token[..8.min(token.len())]);
|
||||
}
|
||||
|
||||
|
||||
// Fallback to anonymous
|
||||
"anonymous".to_string()
|
||||
}
|
||||
|
||||
/// Circuit breaker middleware for provider requests
|
||||
pub async fn circuit_breaker_middleware(
|
||||
provider_name: &str,
|
||||
state: &AppState,
|
||||
) -> Result<(), AppError> {
|
||||
pub async fn circuit_breaker_middleware(provider_name: &str, state: &AppState) -> Result<(), AppError> {
|
||||
if !state.rate_limit_manager.check_provider_request(provider_name).await? {
|
||||
return Err(AppError::ProviderError(
|
||||
format!("Provider {} is currently unavailable (circuit breaker open)", provider_name)
|
||||
));
|
||||
return Err(AppError::ProviderError(format!(
|
||||
"Provider {} is currently unavailable (circuit breaker open)",
|
||||
provider_name
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user