llm_optimizer_processor/decision/
rate_limiting.rs

1//! Rate Limiting Strategy
2//!
3//! Implements adaptive rate limiting to control costs and prevent overload.
4//! Uses a token bucket algorithm with adaptive adjustment based on error rates,
5//! costs, and traffic patterns.
6
7use async_trait::async_trait;
8use chrono::{DateTime, Duration as ChronoDuration, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::VecDeque;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::{debug, info, warn};
14use uuid::Uuid;
15
16use super::config::RateLimitingConfig;
17use super::error::{DecisionError, DecisionResult};
18use super::traits::OptimizationStrategy;
19use super::types::{
20    ComparisonOperator, ConfigChange, ConfigType, Decision, DecisionCriteria, DecisionInput,
21    DecisionOutcome, DecisionType, ExpectedImpact, RollbackCondition, RollbackPlan, SafetyCheck,
22};
23
24/// Rate limiting strategy that adjusts rate limits based on costs and error rates
25pub struct RateLimitingStrategy {
26    config: RateLimitingConfig,
27    state: Arc<RwLock<RateLimitState>>,
28}
29
30/// Internal state for rate limiting strategy
31#[derive(Debug, Clone, Serialize, Deserialize)]
32struct RateLimitState {
33    /// Current rate limit (requests per second)
34    current_rps_limit: f64,
35
36    /// Current token limit (tokens per minute)
37    current_token_limit: u64,
38
39    /// Current cost limit (USD per hour)
40    current_cost_limit: f64,
41
42    /// Token bucket state
43    token_bucket: TokenBucket,
44
45    /// Recent rate limit adjustments
46    adjustment_history: VecDeque<RateLimitAdjustment>,
47
48    /// Recent error rate samples
49    error_rate_history: VecDeque<ErrorRateSample>,
50
51    /// Recent cost rate samples
52    cost_rate_history: VecDeque<CostRateSample>,
53
54    /// Traffic pattern analysis
55    traffic_pattern: TrafficPattern,
56
57    /// Learning metrics
58    learning_metrics: LearningMetrics,
59
60    /// Last adjustment time
61    last_adjustment: DateTime<Utc>,
62
63    /// Number of decisions made
64    decisions_made: u64,
65}
66
67/// Token bucket for rate limiting
68#[derive(Debug, Clone, Serialize, Deserialize)]
69struct TokenBucket {
70    /// Current token count
71    tokens: f64,
72
73    /// Maximum tokens (bucket capacity)
74    capacity: f64,
75
76    /// Token refill rate (tokens per second)
77    refill_rate: f64,
78
79    /// Last refill timestamp
80    last_refill: DateTime<Utc>,
81
82    /// Burst allowance multiplier
83    burst_multiplier: f64,
84}
85
86impl TokenBucket {
87    fn new(capacity: f64, refill_rate: f64, burst_multiplier: f64) -> Self {
88        Self {
89            tokens: capacity,
90            capacity,
91            refill_rate,
92            last_refill: Utc::now(),
93            burst_multiplier,
94        }
95    }
96
97    /// Refill tokens based on elapsed time
98    fn refill(&mut self) {
99        let now = Utc::now();
100        let elapsed = (now - self.last_refill).num_milliseconds() as f64 / 1000.0;
101
102        let new_tokens = elapsed * self.refill_rate;
103        let max_capacity = self.capacity * self.burst_multiplier;
104
105        self.tokens = (self.tokens + new_tokens).min(max_capacity);
106        self.last_refill = now;
107    }
108
109    /// Try to consume tokens
110    fn try_consume(&mut self, tokens: f64) -> bool {
111        self.refill();
112
113        if self.tokens >= tokens {
114            self.tokens -= tokens;
115            true
116        } else {
117            false
118        }
119    }
120
121    /// Update capacity and refill rate
122    fn update(&mut self, new_capacity: f64, new_refill_rate: f64) {
123        self.refill(); // Refill before changing rates
124        self.capacity = new_capacity;
125        self.refill_rate = new_refill_rate;
126
127        // If capacity decreased, adjust current tokens
128        if self.tokens > self.capacity * self.burst_multiplier {
129            self.tokens = self.capacity * self.burst_multiplier;
130        }
131    }
132
133    /// Get utilization (0-1)
134    fn utilization(&self) -> f64 {
135        1.0 - (self.tokens / self.capacity).clamp(0.0, 1.0)
136    }
137}
138
139/// Record of a rate limit adjustment
140#[derive(Debug, Clone, Serialize, Deserialize)]
141struct RateLimitAdjustment {
142    timestamp: DateTime<Utc>,
143    old_rps_limit: f64,
144    new_rps_limit: f64,
145    old_token_limit: u64,
146    new_token_limit: u64,
147    old_cost_limit: f64,
148    new_cost_limit: f64,
149    reason: String,
150    effectiveness: Option<f64>,
151}
152
153/// Error rate sample
154#[derive(Debug, Clone, Serialize, Deserialize)]
155struct ErrorRateSample {
156    timestamp: DateTime<Utc>,
157    error_rate_pct: f64,
158    request_count: u64,
159}
160
161/// Cost rate sample
162#[derive(Debug, Clone, Serialize, Deserialize)]
163struct CostRateSample {
164    timestamp: DateTime<Utc>,
165    cost_per_hour: f64,
166    tokens_per_minute: f64,
167}
168
169/// Traffic pattern analysis
170#[derive(Debug, Clone, Serialize, Deserialize)]
171struct TrafficPattern {
172    /// Average requests per second
173    avg_rps: f64,
174
175    /// Peak requests per second
176    peak_rps: f64,
177
178    /// Average burst size
179    avg_burst_size: f64,
180
181    /// Burst frequency (bursts per hour)
182    burst_frequency: f64,
183
184    /// Traffic volatility (0-1)
185    volatility: f64,
186
187    /// Last update time
188    last_update: DateTime<Utc>,
189}
190
191impl TrafficPattern {
192    fn new() -> Self {
193        Self {
194            avg_rps: 0.0,
195            peak_rps: 0.0,
196            avg_burst_size: 0.0,
197            burst_frequency: 0.0,
198            volatility: 0.0,
199            last_update: Utc::now(),
200        }
201    }
202
203    fn update(&mut self, current_rps: f64, peak_rps: f64) {
204        // Exponential moving average
205        let alpha = 0.1;
206        self.avg_rps = alpha * current_rps + (1.0 - alpha) * self.avg_rps;
207        self.peak_rps = self.peak_rps.max(peak_rps);
208
209        // Calculate volatility
210        let deviation = (current_rps - self.avg_rps).abs();
211        self.volatility = alpha * (deviation / self.avg_rps.max(1.0)) + (1.0 - alpha) * self.volatility;
212
213        self.last_update = Utc::now();
214    }
215}
216
217/// Learning metrics for adaptive adjustment
218#[derive(Debug, Clone, Serialize, Deserialize)]
219struct LearningMetrics {
220    /// Total adjustments made
221    total_adjustments: u64,
222
223    /// Successful adjustments (improved metrics)
224    successful_adjustments: u64,
225
226    /// Failed adjustments (degraded metrics)
227    failed_adjustments: u64,
228
229    /// Average effectiveness score
230    avg_effectiveness: f64,
231
232    /// Confidence in current limits (0-1)
233    confidence: f64,
234
235    /// Learning rate
236    learning_rate: f64,
237}
238
239impl LearningMetrics {
240    fn new() -> Self {
241        Self {
242            total_adjustments: 0,
243            successful_adjustments: 0,
244            failed_adjustments: 0,
245            avg_effectiveness: 0.5,
246            confidence: 0.5,
247            learning_rate: 0.1,
248        }
249    }
250
251    fn record_outcome(&mut self, effectiveness: f64) {
252        self.total_adjustments += 1;
253
254        if effectiveness > 0.7 {
255            self.successful_adjustments += 1;
256        } else if effectiveness < 0.3 {
257            self.failed_adjustments += 1;
258        }
259
260        // Update average effectiveness
261        let alpha = self.learning_rate;
262        self.avg_effectiveness = alpha * effectiveness + (1.0 - alpha) * self.avg_effectiveness;
263
264        // Update confidence based on success rate
265        let success_rate = self.successful_adjustments as f64 / self.total_adjustments.max(1) as f64;
266        self.confidence = (0.5 * success_rate + 0.3 * self.avg_effectiveness + 0.2 * (1.0 - self.learning_rate)).clamp(0.0, 1.0);
267    }
268}
269
270impl RateLimitState {
271    fn new(config: &RateLimitingConfig) -> Self {
272        let token_bucket = TokenBucket::new(
273            config.max_requests_per_second * 10.0, // 10-second capacity
274            config.max_requests_per_second,
275            config.burst_multiplier,
276        );
277
278        Self {
279            current_rps_limit: config.max_requests_per_second,
280            current_token_limit: config.max_tokens_per_minute,
281            current_cost_limit: config.max_cost_per_hour,
282            token_bucket,
283            adjustment_history: VecDeque::with_capacity(100),
284            error_rate_history: VecDeque::with_capacity(100),
285            cost_rate_history: VecDeque::with_capacity(100),
286            traffic_pattern: TrafficPattern::new(),
287            learning_metrics: LearningMetrics::new(),
288            last_adjustment: Utc::now(),
289            decisions_made: 0,
290        }
291    }
292
293    /// Add error rate sample
294    fn add_error_rate_sample(&mut self, error_rate_pct: f64, request_count: u64) {
295        let sample = ErrorRateSample {
296            timestamp: Utc::now(),
297            error_rate_pct,
298            request_count,
299        };
300
301        self.error_rate_history.push_back(sample);
302
303        // Keep last 100 samples
304        if self.error_rate_history.len() > 100 {
305            self.error_rate_history.pop_front();
306        }
307    }
308
309    /// Add cost rate sample
310    fn add_cost_rate_sample(&mut self, cost_per_hour: f64, tokens_per_minute: f64) {
311        let sample = CostRateSample {
312            timestamp: Utc::now(),
313            cost_per_hour,
314            tokens_per_minute,
315        };
316
317        self.cost_rate_history.push_back(sample);
318
319        // Keep last 100 samples
320        if self.cost_rate_history.len() > 100 {
321            self.cost_rate_history.pop_front();
322        }
323    }
324
325    /// Get recent average error rate
326    fn recent_avg_error_rate(&self) -> f64 {
327        if self.error_rate_history.is_empty() {
328            return 0.0;
329        }
330
331        let sum: f64 = self.error_rate_history.iter().map(|s| s.error_rate_pct).sum();
332        sum / self.error_rate_history.len() as f64
333    }
334
335    /// Get recent average cost rate
336    fn recent_avg_cost_rate(&self) -> f64 {
337        if self.cost_rate_history.is_empty() {
338            return 0.0;
339        }
340
341        let sum: f64 = self.cost_rate_history.iter().map(|s| s.cost_per_hour).sum();
342        sum / self.cost_rate_history.len() as f64
343    }
344
345    /// Check if adjustment is needed
346    fn needs_adjustment(&self, config: &RateLimitingConfig) -> bool {
347        // Don't adjust too frequently
348        let min_interval = ChronoDuration::minutes(5);
349        if Utc::now() - self.last_adjustment < min_interval {
350            return false;
351        }
352
353        // Check error rate
354        let avg_error_rate = self.recent_avg_error_rate();
355        if config.adaptive_enabled && avg_error_rate > config.target_error_rate_pct * 1.5 {
356            return true;
357        }
358
359        // Check cost overrun
360        let avg_cost = self.recent_avg_cost_rate();
361        if avg_cost > config.max_cost_per_hour * 0.9 {
362            return true;
363        }
364
365        // Check if we're significantly under limits (opportunity to increase)
366        if avg_error_rate < config.target_error_rate_pct * 0.5
367            && avg_cost < config.max_cost_per_hour * 0.5 {
368            return true;
369        }
370
371        false
372    }
373}
374
375impl RateLimitingStrategy {
376    /// Create a new rate limiting strategy
377    pub fn new(config: RateLimitingConfig) -> Self {
378        let state = RateLimitState::new(&config);
379
380        Self {
381            config,
382            state: Arc::new(RwLock::new(state)),
383        }
384    }
385
386    /// Calculate optimal rate limits based on current metrics
387    async fn calculate_optimal_limits(
388        &self,
389        input: &DecisionInput,
390        criteria: &DecisionCriteria,
391    ) -> (f64, u64, f64, f64) {
392        let state = self.state.read().await;
393        let metrics = &input.current_metrics;
394
395        // Calculate current rates
396        let current_error_rate = metrics.error_rate_pct;
397        let current_cost_rate = metrics.daily_cost_usd / 24.0; // Convert to hourly
398        let current_rps = metrics.throughput_rps;
399
400        // Calculate adjustment factor based on error rate
401        let error_adjustment = if self.config.adaptive_enabled {
402            if current_error_rate > self.config.target_error_rate_pct * 2.0 {
403                0.7 // Significant reduction
404            } else if current_error_rate > self.config.target_error_rate_pct * 1.5 {
405                0.85 // Moderate reduction
406            } else if current_error_rate > self.config.target_error_rate_pct {
407                0.95 // Small reduction
408            } else if current_error_rate < self.config.target_error_rate_pct * 0.5 {
409                1.1 // Small increase
410            } else {
411                1.0 // No change
412            }
413        } else {
414            1.0
415        };
416
417        // Calculate adjustment factor based on cost
418        let cost_adjustment = if current_cost_rate > self.config.max_cost_per_hour * 0.9 {
419            0.8 // Reduce to stay within budget
420        } else if current_cost_rate > self.config.max_cost_per_hour * 0.8 {
421            0.9 // Small reduction
422        } else if current_cost_rate < self.config.max_cost_per_hour * 0.5 {
423            1.15 // Increase if we have budget headroom
424        } else {
425            1.0
426        };
427
428        // Calculate adjustment factor based on traffic pattern
429        let traffic_adjustment = if state.traffic_pattern.volatility > 0.5 {
430            1.05 // Slightly increase for volatile traffic
431        } else {
432            1.0
433        };
434
435        // Combine adjustment factors with learning
436        let combined_adjustment = error_adjustment * cost_adjustment * traffic_adjustment;
437        let learned_adjustment = state.learning_metrics.confidence * combined_adjustment
438            + (1.0 - state.learning_metrics.confidence);
439
440        // Apply gradual changes (max 20% per adjustment)
441        let safe_adjustment = if learned_adjustment > 1.0 {
442            learned_adjustment.min(1.2)
443        } else {
444            learned_adjustment.max(0.8)
445        };
446
447        // Calculate new limits
448        let new_rps_limit = (state.current_rps_limit * safe_adjustment)
449            .max(1.0) // Minimum 1 RPS
450            .min(self.config.max_requests_per_second * 2.0); // Don't exceed 2x configured max
451
452        let new_token_limit = (state.current_token_limit as f64 * safe_adjustment) as u64;
453        let new_cost_limit = (state.current_cost_limit * safe_adjustment)
454            .max(1.0)
455            .min(self.config.max_cost_per_hour * 1.5);
456
457        // Calculate confidence based on learning metrics and data quality
458        let confidence = state.learning_metrics.confidence *
459            (1.0 - state.traffic_pattern.volatility * 0.3);
460
461        (new_rps_limit, new_token_limit, new_cost_limit, confidence)
462    }
463
464    /// Perform safety checks for rate limit adjustment
465    async fn perform_safety_checks(
466        &self,
467        new_rps: f64,
468        new_tokens: u64,
469        new_cost: f64,
470        criteria: &DecisionCriteria,
471    ) -> Vec<SafetyCheck> {
472        let state = self.state.read().await;
473        let mut checks = Vec::new();
474
475        // Check 1: Minimum availability (don't reduce too much)
476        let min_rps = self.config.max_requests_per_second * 0.1;
477        let availability_check = SafetyCheck {
478            name: "availability_check".to_string(),
479            passed: new_rps >= min_rps,
480            details: format!(
481                "New RPS limit {:.2} vs minimum {:.2}",
482                new_rps, min_rps
483            ),
484            timestamp: Utc::now(),
485        };
486        checks.push(availability_check);
487
488        // Check 2: Gradual change (max 20% change)
489        let change_pct = ((new_rps - state.current_rps_limit) / state.current_rps_limit).abs();
490        let gradual_check = SafetyCheck {
491            name: "gradual_change_check".to_string(),
492            passed: change_pct <= 0.2,
493            details: format!("Change {:.1}% (max 20%)", change_pct * 100.0),
494            timestamp: Utc::now(),
495        };
496        checks.push(gradual_check);
497
498        // Check 3: Cost constraint
499        let cost_check = SafetyCheck {
500            name: "cost_check".to_string(),
501            passed: new_cost <= self.config.max_cost_per_hour * 1.5,
502            details: format!(
503                "New cost limit ${:.2}/hr vs max ${:.2}/hr",
504                new_cost,
505                self.config.max_cost_per_hour * 1.5
506            ),
507            timestamp: Utc::now(),
508        };
509        checks.push(cost_check);
510
511        // Check 4: Token budget
512        let token_check = SafetyCheck {
513            name: "token_budget_check".to_string(),
514            passed: new_tokens <= self.config.max_tokens_per_minute * 2,
515            details: format!(
516                "New token limit {} vs max {}",
517                new_tokens,
518                self.config.max_tokens_per_minute * 2
519            ),
520            timestamp: Utc::now(),
521        };
522        checks.push(token_check);
523
524        // Check 5: Learning confidence
525        let confidence_check = SafetyCheck {
526            name: "learning_confidence_check".to_string(),
527            passed: state.learning_metrics.confidence >= 0.3,
528            details: format!(
529                "Learning confidence {:.2}",
530                state.learning_metrics.confidence
531            ),
532            timestamp: Utc::now(),
533        };
534        checks.push(confidence_check);
535
536        checks
537    }
538
539    /// Create rollback plan
540    async fn create_rollback_plan(&self) -> RollbackPlan {
541        let state = self.state.read().await;
542
543        let revert_changes = vec![
544            ConfigChange {
545                config_type: ConfigType::RateLimit,
546                path: "rate_limit.requests_per_second".to_string(),
547                old_value: None,
548                new_value: serde_json::json!(state.current_rps_limit),
549                description: "Revert RPS limit".to_string(),
550            },
551            ConfigChange {
552                config_type: ConfigType::RateLimit,
553                path: "rate_limit.tokens_per_minute".to_string(),
554                old_value: None,
555                new_value: serde_json::json!(state.current_token_limit),
556                description: "Revert token limit".to_string(),
557            },
558            ConfigChange {
559                config_type: ConfigType::RateLimit,
560                path: "rate_limit.cost_per_hour".to_string(),
561                old_value: None,
562                new_value: serde_json::json!(state.current_cost_limit),
563                description: "Revert cost limit".to_string(),
564            },
565        ];
566
567        let trigger_conditions = vec![
568            RollbackCondition {
569                metric: "error_rate_pct".to_string(),
570                operator: ComparisonOperator::GreaterThan,
571                threshold: self.config.target_error_rate_pct * 3.0,
572                description: "Error rate exceeds 3x target".to_string(),
573            },
574            RollbackCondition {
575                metric: "cost_per_hour".to_string(),
576                operator: ComparisonOperator::GreaterThan,
577                threshold: self.config.max_cost_per_hour * 1.5,
578                description: "Cost exceeds 1.5x budget".to_string(),
579            },
580            RollbackCondition {
581                metric: "success_rate_pct".to_string(),
582                operator: ComparisonOperator::LessThan,
583                threshold: 90.0,
584                description: "Success rate drops below 90%".to_string(),
585            },
586        ];
587
588        RollbackPlan {
589            description: "Revert rate limit changes if metrics degrade".to_string(),
590            revert_changes,
591            trigger_conditions,
592            max_duration: std::time::Duration::from_secs(600), // 10 minutes
593        }
594    }
595
596    /// Update internal state with new metrics
597    async fn update_state(&self, input: &DecisionInput) {
598        let mut state = self.state.write().await;
599        let metrics = &input.current_metrics;
600
601        // Update error rate history
602        state.add_error_rate_sample(metrics.error_rate_pct, metrics.request_count);
603
604        // Update cost rate history
605        let cost_per_hour = metrics.daily_cost_usd / 24.0;
606        let tokens_per_minute = metrics.throughput_rps * 60.0; // Rough estimate
607        state.add_cost_rate_sample(cost_per_hour, tokens_per_minute);
608
609        // Update traffic pattern
610        state.traffic_pattern.update(metrics.throughput_rps, metrics.throughput_rps);
611    }
612}
613
614#[async_trait]
615impl OptimizationStrategy for RateLimitingStrategy {
616    fn name(&self) -> &str {
617        "rate_limiting"
618    }
619
620    fn priority(&self) -> u32 {
621        self.config.priority
622    }
623
624    fn is_applicable(&self, input: &DecisionInput) -> bool {
625        if !self.config.enabled {
626            return false;
627        }
628
629        let metrics = &input.current_metrics;
630
631        // Applicable if:
632        // 1. Error rate is high
633        // 2. Cost is approaching or exceeding budget
634        // 3. There's opportunity to increase limits safely
635
636        let error_rate_high = metrics.error_rate_pct > self.config.target_error_rate_pct;
637        let cost_high = metrics.daily_cost_usd / 24.0 > self.config.max_cost_per_hour * 0.8;
638        let has_headroom = metrics.error_rate_pct < self.config.target_error_rate_pct * 0.5
639            && metrics.daily_cost_usd / 24.0 < self.config.max_cost_per_hour * 0.6;
640
641        error_rate_high || cost_high || has_headroom
642    }
643
644    async fn evaluate(
645        &self,
646        input: &DecisionInput,
647        criteria: &DecisionCriteria,
648    ) -> DecisionResult<Vec<Decision>> {
649        // Update internal state
650        self.update_state(input).await;
651
652        // Check if adjustment is needed
653        let state = self.state.read().await;
654        if !state.needs_adjustment(&self.config) {
655            debug!("Rate limit adjustment not needed at this time");
656            return Ok(vec![]);
657        }
658        drop(state);
659
660        // Calculate optimal limits
661        let (new_rps, new_tokens, new_cost, confidence) =
662            self.calculate_optimal_limits(input, criteria).await;
663
664        // Check if limits actually changed
665        let state = self.state.read().await;
666        let rps_change = (new_rps - state.current_rps_limit).abs();
667        let token_change = (new_tokens as i64 - state.current_token_limit as i64).abs();
668        let cost_change = (new_cost - state.current_cost_limit).abs();
669        drop(state);
670
671        if rps_change < 0.1 && token_change < 100 && cost_change < 0.1 {
672            debug!("Rate limits unchanged, no decision needed");
673            return Ok(vec![]);
674        }
675
676        // Perform safety checks
677        let safety_checks = self.perform_safety_checks(new_rps, new_tokens, new_cost, criteria).await;
678
679        // Check if all critical safety checks passed
680        let all_passed = safety_checks.iter().all(|check| check.passed);
681        if !all_passed {
682            warn!("Rate limit adjustment failed safety checks");
683            return Ok(vec![]);
684        }
685
686        // Create configuration changes
687        let state = self.state.read().await;
688        let config_changes = vec![
689            ConfigChange {
690                config_type: ConfigType::RateLimit,
691                path: "rate_limit.requests_per_second".to_string(),
692                old_value: Some(serde_json::json!(state.current_rps_limit)),
693                new_value: serde_json::json!(new_rps),
694                description: format!(
695                    "Adjust RPS limit from {:.2} to {:.2}",
696                    state.current_rps_limit, new_rps
697                ),
698            },
699            ConfigChange {
700                config_type: ConfigType::RateLimit,
701                path: "rate_limit.tokens_per_minute".to_string(),
702                old_value: Some(serde_json::json!(state.current_token_limit)),
703                new_value: serde_json::json!(new_tokens),
704                description: format!(
705                    "Adjust token limit from {} to {}",
706                    state.current_token_limit, new_tokens
707                ),
708            },
709            ConfigChange {
710                config_type: ConfigType::RateLimit,
711                path: "rate_limit.cost_per_hour".to_string(),
712                old_value: Some(serde_json::json!(state.current_cost_limit)),
713                new_value: serde_json::json!(new_cost),
714                description: format!(
715                    "Adjust cost limit from ${:.2} to ${:.2}",
716                    state.current_cost_limit, new_cost
717                ),
718            },
719        ];
720
721        // Calculate expected impact
722        let rps_change_pct = ((new_rps - state.current_rps_limit) / state.current_rps_limit) * 100.0;
723        let expected_cost_change = state.current_cost_limit - new_cost;
724        let expected_throughput_change = new_rps - state.current_rps_limit;
725
726        let expected_impact = ExpectedImpact {
727            cost_change_usd: Some(expected_cost_change),
728            latency_change_ms: Some(if rps_change_pct < 0.0 { -10.0 } else { 5.0 }),
729            quality_change: None,
730            throughput_change_rps: Some(expected_throughput_change),
731            success_rate_change_pct: Some(if rps_change_pct < 0.0 { 1.0 } else { -0.5 }),
732            time_to_impact: std::time::Duration::from_secs(60),
733            confidence,
734        };
735
736        // Generate justification
737        let avg_error_rate = state.recent_avg_error_rate();
738        let avg_cost_rate = state.recent_avg_cost_rate();
739
740        let justification = if rps_change_pct < 0.0 {
741            format!(
742                "Reducing rate limits by {:.1}% due to: error rate {:.2}% (target {:.2}%), \
743                 cost ${:.2}/hr (budget ${:.2}/hr). This will improve stability and control costs.",
744                rps_change_pct.abs(), avg_error_rate, self.config.target_error_rate_pct,
745                avg_cost_rate, self.config.max_cost_per_hour
746            )
747        } else {
748            format!(
749                "Increasing rate limits by {:.1}% due to: low error rate {:.2}% (target {:.2}%), \
750                 cost headroom ${:.2}/hr available. This will improve throughput.",
751                rps_change_pct, avg_error_rate, self.config.target_error_rate_pct,
752                self.config.max_cost_per_hour - avg_cost_rate
753            )
754        };
755
756        drop(state);
757
758        // Create rollback plan
759        let rollback_plan = self.create_rollback_plan().await;
760
761        // Create decision
762        let decision = Decision {
763            id: Uuid::new_v4().to_string(),
764            timestamp: Utc::now(),
765            strategy: self.name().to_string(),
766            decision_type: DecisionType::AdjustRateLimits,
767            confidence,
768            expected_impact,
769            config_changes,
770            justification,
771            related_insights: input
772                .insights
773                .iter()
774                .filter(|i| {
775                    i.title.to_lowercase().contains("rate")
776                        || i.title.to_lowercase().contains("error")
777                        || i.title.to_lowercase().contains("cost")
778                })
779                .map(|i| i.id.clone())
780                .collect(),
781            related_recommendations: input
782                .recommendations
783                .iter()
784                .filter(|r| {
785                    r.title.to_lowercase().contains("rate")
786                        || r.title.to_lowercase().contains("limit")
787                })
788                .map(|r| r.id.clone())
789                .collect(),
790            priority: if avg_error_rate > self.config.target_error_rate_pct * 2.0 {
791                90 // High priority if error rate is very high
792            } else if avg_cost_rate > self.config.max_cost_per_hour {
793                85 // High priority if over budget
794            } else {
795                self.priority()
796            },
797            requires_approval: rps_change_pct.abs() > 15.0, // Require approval for large changes
798            safety_checks,
799            rollback_plan: Some(rollback_plan),
800            metadata: {
801                let mut meta = std::collections::HashMap::new();
802                meta.insert("strategy_version".to_string(), serde_json::json!("1.0"));
803                meta.insert("adjustment_type".to_string(),
804                    serde_json::json!(if rps_change_pct < 0.0 { "decrease" } else { "increase" }));
805                meta.insert("rps_change_pct".to_string(), serde_json::json!(rps_change_pct));
806                meta.insert("token_change".to_string(), serde_json::json!(token_change));
807                meta.insert("cost_change".to_string(), serde_json::json!(cost_change));
808                meta
809            },
810        };
811
812        info!(
813            "Generated rate limiting decision: RPS {:.2} -> {:.2} ({:+.1}%)",
814            self.state.read().await.current_rps_limit, new_rps, rps_change_pct
815        );
816
817        Ok(vec![decision])
818    }
819
820    async fn validate(
821        &self,
822        decision: &Decision,
823        criteria: &DecisionCriteria,
824    ) -> DecisionResult<()> {
825        // Verify decision is for rate limiting
826        if decision.decision_type != DecisionType::AdjustRateLimits {
827            return Err(DecisionError::ValidationFailed(
828                "Decision is not a rate limit adjustment".to_string(),
829            ));
830        }
831
832        // Verify strategy matches
833        if decision.strategy != self.name() {
834            return Err(DecisionError::ValidationFailed(
835                "Decision strategy does not match".to_string(),
836            ));
837        }
838
839        // Verify confidence threshold
840        if decision.confidence < criteria.min_confidence {
841            return Err(DecisionError::ValidationFailed(format!(
842                "Confidence {:.2} below minimum {:.2}",
843                decision.confidence, criteria.min_confidence
844            )));
845        }
846
847        // Verify all safety checks passed
848        let all_passed = decision.safety_checks.iter().all(|check| check.passed);
849        if !all_passed {
850            return Err(DecisionError::ValidationFailed(
851                "Not all safety checks passed".to_string(),
852            ));
853        }
854
855        // Verify config changes are present
856        if decision.config_changes.is_empty() {
857            return Err(DecisionError::ValidationFailed(
858                "No configuration changes specified".to_string(),
859            ));
860        }
861
862        // Verify rollback plan exists
863        if decision.rollback_plan.is_none() {
864            return Err(DecisionError::ValidationFailed(
865                "No rollback plan specified".to_string(),
866            ));
867        }
868
869        Ok(())
870    }
871
872    async fn learn(
873        &mut self,
874        decision: &Decision,
875        outcome: &DecisionOutcome,
876    ) -> DecisionResult<()> {
877        let mut state = self.state.write().await;
878
879        // Calculate effectiveness
880        let effectiveness = if outcome.success {
881            if let Some(actual_impact) = &outcome.actual_impact {
882                // Compare actual vs expected impact
883                let expected_cost_change = decision
884                    .expected_impact
885                    .cost_change_usd
886                    .unwrap_or(0.0);
887                let actual_cost_change = actual_impact.cost_change_usd;
888
889                // Calculate how close we were to expectations
890                let cost_accuracy = if expected_cost_change.abs() > 0.01 {
891                    1.0 - ((actual_cost_change - expected_cost_change).abs() / expected_cost_change.abs()).min(1.0)
892                } else {
893                    0.8
894                };
895
896                // Factor in whether outcome was positive
897                let outcome_quality = if outcome.rolled_back {
898                    0.2 // Poor outcome if we had to rollback
899                } else if actual_cost_change < 0.0 {
900                    0.9 // Good if we saved money
901                } else {
902                    0.6 // Neutral if cost increased but manageable
903                };
904
905                (cost_accuracy * 0.4 + outcome_quality * 0.6).clamp(0.0, 1.0)
906            } else {
907                0.7 // Success but no impact data yet
908            }
909        } else {
910            0.1 // Failed execution
911        };
912
913        // Update learning metrics
914        state.learning_metrics.record_outcome(effectiveness);
915
916        // If this was a successful decision, update our limits
917        if outcome.success && !outcome.rolled_back {
918            if let Some(actual_impact) = &outcome.actual_impact {
919                // Extract new limits from the decision
920                for change in &decision.config_changes {
921                    match change.path.as_str() {
922                        "rate_limit.requests_per_second" => {
923                            if let Some(new_rps) = change.new_value.as_f64() {
924                                state.current_rps_limit = new_rps;
925                                state.token_bucket.update(
926                                    new_rps * 10.0,
927                                    new_rps,
928                                );
929                            }
930                        }
931                        "rate_limit.tokens_per_minute" => {
932                            if let Some(new_tokens) = change.new_value.as_u64() {
933                                state.current_token_limit = new_tokens;
934                            }
935                        }
936                        "rate_limit.cost_per_hour" => {
937                            if let Some(new_cost) = change.new_value.as_f64() {
938                                state.current_cost_limit = new_cost;
939                            }
940                        }
941                        _ => {}
942                    }
943                }
944            }
945
946            // Record the adjustment
947            let adjustment = RateLimitAdjustment {
948                timestamp: outcome.execution_time,
949                old_rps_limit: decision
950                    .config_changes
951                    .iter()
952                    .find(|c| c.path == "rate_limit.requests_per_second")
953                    .and_then(|c| c.old_value.as_ref())
954                    .and_then(|v| v.as_f64())
955                    .unwrap_or(state.current_rps_limit),
956                new_rps_limit: state.current_rps_limit,
957                old_token_limit: decision
958                    .config_changes
959                    .iter()
960                    .find(|c| c.path == "rate_limit.tokens_per_minute")
961                    .and_then(|c| c.old_value.as_ref())
962                    .and_then(|v| v.as_u64())
963                    .unwrap_or(state.current_token_limit),
964                new_token_limit: state.current_token_limit,
965                old_cost_limit: decision
966                    .config_changes
967                    .iter()
968                    .find(|c| c.path == "rate_limit.cost_per_hour")
969                    .and_then(|c| c.old_value.as_ref())
970                    .and_then(|v| v.as_f64())
971                    .unwrap_or(state.current_cost_limit),
972                new_cost_limit: state.current_cost_limit,
973                reason: decision.justification.clone(),
974                effectiveness: Some(effectiveness),
975            };
976
977            state.adjustment_history.push_back(adjustment);
978
979            // Keep last 100 adjustments
980            if state.adjustment_history.len() > 100 {
981                state.adjustment_history.pop_front();
982            }
983        }
984
985        state.last_adjustment = Utc::now();
986        state.decisions_made += 1;
987
988        info!(
989            "Learned from rate limiting decision: effectiveness={:.2}, confidence={:.2}",
990            effectiveness, state.learning_metrics.confidence
991        );
992
993        Ok(())
994    }
995
996    fn get_stats(&self) -> serde_json::Value {
997        // Use try_read to avoid blocking
998        if let Ok(state) = self.state.try_read() {
999            serde_json::json!({
1000                "current_rps_limit": state.current_rps_limit,
1001                "current_token_limit": state.current_token_limit,
1002                "current_cost_limit": state.current_cost_limit,
1003                "token_bucket_utilization": state.token_bucket.utilization(),
1004                "total_adjustments": state.adjustment_history.len(),
1005                "learning_metrics": {
1006                    "total_adjustments": state.learning_metrics.total_adjustments,
1007                    "successful_adjustments": state.learning_metrics.successful_adjustments,
1008                    "failed_adjustments": state.learning_metrics.failed_adjustments,
1009                    "avg_effectiveness": state.learning_metrics.avg_effectiveness,
1010                    "confidence": state.learning_metrics.confidence,
1011                },
1012                "traffic_pattern": {
1013                    "avg_rps": state.traffic_pattern.avg_rps,
1014                    "peak_rps": state.traffic_pattern.peak_rps,
1015                    "volatility": state.traffic_pattern.volatility,
1016                },
1017                "recent_error_rate": state.recent_avg_error_rate(),
1018                "recent_cost_rate": state.recent_avg_cost_rate(),
1019                "decisions_made": state.decisions_made,
1020            })
1021        } else {
1022            serde_json::json!({
1023                "error": "Unable to acquire state lock"
1024            })
1025        }
1026    }
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::*;
1032    use crate::analyzer::{AnalysisReport, Insight, InsightCategory, Recommendation, Severity, Confidence};
1033    use std::collections::HashMap;
1034
1035    fn create_test_config() -> RateLimitingConfig {
1036        RateLimitingConfig {
1037            enabled: true,
1038            priority: 80,
1039            min_confidence: 0.6,
1040            max_requests_per_second: 100.0,
1041            max_tokens_per_minute: 100000,
1042            max_cost_per_hour: 10.0,
1043            burst_multiplier: 1.5,
1044            adaptive_enabled: true,
1045            target_error_rate_pct: 1.0,
1046        }
1047    }
1048
1049    fn create_test_input(error_rate: f64, cost_per_day: f64, throughput: f64) -> DecisionInput {
1050        DecisionInput {
1051            timestamp: Utc::now(),
1052            analysis_reports: vec![],
1053            insights: vec![],
1054            recommendations: vec![],
1055            current_metrics: super::super::types::SystemMetrics {
1056                avg_latency_ms: 100.0,
1057                p95_latency_ms: 200.0,
1058                p99_latency_ms: 300.0,
1059                success_rate_pct: 99.0,
1060                error_rate_pct: error_rate,
1061                throughput_rps: throughput,
1062                total_cost_usd: cost_per_day * 7.0,
1063                daily_cost_usd: cost_per_day,
1064                avg_rating: Some(4.5),
1065                cache_hit_rate_pct: Some(30.0),
1066                request_count: 10000,
1067                timestamp: Utc::now(),
1068            },
1069            context: HashMap::new(),
1070        }
1071    }
1072
1073    #[tokio::test]
1074    async fn test_strategy_creation() {
1075        let config = create_test_config();
1076        let strategy = RateLimitingStrategy::new(config);
1077
1078        assert_eq!(strategy.name(), "rate_limiting");
1079        assert_eq!(strategy.priority(), 80);
1080    }
1081
1082    #[tokio::test]
1083    async fn test_token_bucket_basic() {
1084        let mut bucket = TokenBucket::new(100.0, 10.0, 1.5);
1085
1086        // Should allow consuming tokens up to capacity
1087        assert!(bucket.try_consume(50.0));
1088        assert!(bucket.try_consume(50.0));
1089        assert!(!bucket.try_consume(1.0)); // Exceeds capacity
1090    }
1091
1092    #[tokio::test]
1093    async fn test_token_bucket_refill() {
1094        let mut bucket = TokenBucket::new(100.0, 100.0, 1.5); // 100 tokens/sec
1095
1096        // Consume all tokens
1097        assert!(bucket.try_consume(100.0));
1098        assert!(!bucket.try_consume(1.0));
1099
1100        // Wait and refill
1101        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1102        bucket.refill();
1103
1104        // Should have ~50 tokens after 0.5 seconds at 100/sec
1105        assert!(bucket.try_consume(40.0));
1106    }
1107
1108    #[tokio::test]
1109    async fn test_token_bucket_burst() {
1110        let mut bucket = TokenBucket::new(100.0, 10.0, 2.0); // 2x burst
1111
1112        // Can burst up to 200 tokens
1113        tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1114        bucket.refill();
1115
1116        assert!(bucket.try_consume(150.0)); // Within burst
1117    }
1118
1119    #[tokio::test]
1120    async fn test_is_applicable_high_error_rate() {
1121        let config = create_test_config();
1122        let strategy = RateLimitingStrategy::new(config);
1123
1124        // High error rate should trigger applicability
1125        let input = create_test_input(3.0, 100.0, 50.0);
1126        assert!(strategy.is_applicable(&input));
1127    }
1128
1129    #[tokio::test]
1130    async fn test_is_applicable_high_cost() {
1131        let config = create_test_config();
1132        let strategy = RateLimitingStrategy::new(config);
1133
1134        // High cost should trigger applicability (>80% of budget)
1135        let input = create_test_input(0.5, 200.0, 50.0); // $200/day = $8.33/hr
1136        assert!(strategy.is_applicable(&input));
1137    }
1138
1139    #[tokio::test]
1140    async fn test_is_applicable_headroom() {
1141        let config = create_test_config();
1142        let strategy = RateLimitingStrategy::new(config);
1143
1144        // Low error and cost should trigger increase opportunity
1145        let input = create_test_input(0.2, 50.0, 50.0);
1146        assert!(strategy.is_applicable(&input));
1147    }
1148
1149    #[tokio::test]
1150    async fn test_not_applicable_disabled() {
1151        let mut config = create_test_config();
1152        config.enabled = false;
1153        let strategy = RateLimitingStrategy::new(config);
1154
1155        let input = create_test_input(3.0, 200.0, 50.0);
1156        assert!(!strategy.is_applicable(&input));
1157    }
1158
1159    #[tokio::test]
1160    async fn test_evaluate_reduce_on_high_error_rate() {
1161        let config = create_test_config();
1162        let mut strategy = RateLimitingStrategy::new(config);
1163
1164        // Create input with high error rate
1165        let input = create_test_input(5.0, 100.0, 80.0);
1166
1167        // Add some samples to trigger adjustment
1168        {
1169            let mut state = strategy.state.write().await;
1170            state.last_adjustment = Utc::now() - ChronoDuration::minutes(10);
1171        }
1172
1173        let criteria = DecisionCriteria::default();
1174        let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1175
1176        assert!(!decisions.is_empty());
1177        let decision = &decisions[0];
1178        assert_eq!(decision.decision_type, DecisionType::AdjustRateLimits);
1179
1180        // Should reduce limits
1181        let rps_change = decision.config_changes
1182            .iter()
1183            .find(|c| c.path == "rate_limit.requests_per_second")
1184            .unwrap();
1185        let new_rps: f64 = rps_change.new_value.as_f64().unwrap();
1186        let old_rps: f64 = rps_change.old_value.as_ref().unwrap().as_f64().unwrap();
1187        assert!(new_rps < old_rps, "RPS should decrease");
1188    }
1189
1190    #[tokio::test]
1191    async fn test_evaluate_increase_with_headroom() {
1192        let config = create_test_config();
1193        let mut strategy = RateLimitingStrategy::new(config);
1194
1195        // Create input with low error rate and cost
1196        let input = create_test_input(0.2, 50.0, 30.0);
1197
1198        // Add some samples and set last adjustment time
1199        {
1200            let mut state = strategy.state.write().await;
1201            state.last_adjustment = Utc::now() - ChronoDuration::minutes(10);
1202            state.add_error_rate_sample(0.2, 1000);
1203            state.add_cost_rate_sample(2.0, 5000.0);
1204        }
1205
1206        let criteria = DecisionCriteria::default();
1207        let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1208
1209        if !decisions.is_empty() {
1210            let decision = &decisions[0];
1211
1212            // Should increase limits
1213            let rps_change = decision.config_changes
1214                .iter()
1215                .find(|c| c.path == "rate_limit.requests_per_second")
1216                .unwrap();
1217            let new_rps: f64 = rps_change.new_value.as_f64().unwrap();
1218            let old_rps: f64 = rps_change.old_value.as_ref().unwrap().as_f64().unwrap();
1219            assert!(new_rps >= old_rps, "RPS should increase or stay same");
1220        }
1221    }
1222
1223    #[tokio::test]
1224    async fn test_safety_checks() {
1225        let config = create_test_config();
1226        let strategy = RateLimitingStrategy::new(config);
1227
1228        let criteria = DecisionCriteria::default();
1229        let checks = strategy.perform_safety_checks(90.0, 90000, 9.0, &criteria).await;
1230
1231        assert!(!checks.is_empty());
1232        assert!(checks.iter().any(|c| c.name == "availability_check"));
1233        assert!(checks.iter().any(|c| c.name == "gradual_change_check"));
1234        assert!(checks.iter().any(|c| c.name == "cost_check"));
1235    }
1236
1237    #[tokio::test]
1238    async fn test_safety_check_minimum_availability() {
1239        let config = create_test_config();
1240        let strategy = RateLimitingStrategy::new(config);
1241
1242        let criteria = DecisionCriteria::default();
1243        let checks = strategy.perform_safety_checks(5.0, 5000, 1.0, &criteria).await;
1244
1245        // Should fail availability check (too low)
1246        let availability = checks.iter().find(|c| c.name == "availability_check").unwrap();
1247        assert!(!availability.passed);
1248    }
1249
1250    #[tokio::test]
1251    async fn test_rollback_plan_creation() {
1252        let config = create_test_config();
1253        let strategy = RateLimitingStrategy::new(config);
1254
1255        let plan = strategy.create_rollback_plan().await;
1256
1257        assert!(!plan.revert_changes.is_empty());
1258        assert!(!plan.trigger_conditions.is_empty());
1259        assert!(plan.trigger_conditions.iter().any(|c| c.metric == "error_rate_pct"));
1260        assert!(plan.trigger_conditions.iter().any(|c| c.metric == "cost_per_hour"));
1261    }
1262
1263    #[tokio::test]
1264    async fn test_validate_decision() {
1265        let config = create_test_config();
1266        let strategy = RateLimitingStrategy::new(config);
1267
1268        let decision = Decision {
1269            id: Uuid::new_v4().to_string(),
1270            timestamp: Utc::now(),
1271            strategy: "rate_limiting".to_string(),
1272            decision_type: DecisionType::AdjustRateLimits,
1273            confidence: 0.8,
1274            expected_impact: ExpectedImpact {
1275                cost_change_usd: Some(-1.0),
1276                latency_change_ms: Some(-5.0),
1277                quality_change: None,
1278                throughput_change_rps: Some(-10.0),
1279                success_rate_change_pct: Some(0.5),
1280                time_to_impact: std::time::Duration::from_secs(60),
1281                confidence: 0.8,
1282            },
1283            config_changes: vec![ConfigChange {
1284                config_type: ConfigType::RateLimit,
1285                path: "rate_limit.requests_per_second".to_string(),
1286                old_value: Some(serde_json::json!(100.0)),
1287                new_value: serde_json::json!(90.0),
1288                description: "Reduce RPS".to_string(),
1289            }],
1290            justification: "Test".to_string(),
1291            related_insights: vec![],
1292            related_recommendations: vec![],
1293            priority: 80,
1294            requires_approval: false,
1295            safety_checks: vec![SafetyCheck {
1296                name: "test_check".to_string(),
1297                passed: true,
1298                details: "OK".to_string(),
1299                timestamp: Utc::now(),
1300            }],
1301            rollback_plan: Some(RollbackPlan {
1302                description: "Test rollback".to_string(),
1303                revert_changes: vec![],
1304                trigger_conditions: vec![],
1305                max_duration: std::time::Duration::from_secs(300),
1306            }),
1307            metadata: HashMap::new(),
1308        };
1309
1310        let criteria = DecisionCriteria::default();
1311        assert!(strategy.validate(&decision, &criteria).await.is_ok());
1312    }
1313
1314    #[tokio::test]
1315    async fn test_validate_wrong_decision_type() {
1316        let config = create_test_config();
1317        let strategy = RateLimitingStrategy::new(config);
1318
1319        let decision = Decision {
1320            id: Uuid::new_v4().to_string(),
1321            timestamp: Utc::now(),
1322            strategy: "rate_limiting".to_string(),
1323            decision_type: DecisionType::ModelSwitch, // Wrong type
1324            confidence: 0.8,
1325            expected_impact: ExpectedImpact {
1326                cost_change_usd: None,
1327                latency_change_ms: None,
1328                quality_change: None,
1329                throughput_change_rps: None,
1330                success_rate_change_pct: None,
1331                time_to_impact: std::time::Duration::from_secs(60),
1332                confidence: 0.8,
1333            },
1334            config_changes: vec![],
1335            justification: "Test".to_string(),
1336            related_insights: vec![],
1337            related_recommendations: vec![],
1338            priority: 80,
1339            requires_approval: false,
1340            safety_checks: vec![],
1341            rollback_plan: None,
1342            metadata: HashMap::new(),
1343        };
1344
1345        let criteria = DecisionCriteria::default();
1346        assert!(strategy.validate(&decision, &criteria).await.is_err());
1347    }
1348
1349    #[tokio::test]
1350    async fn test_learn_successful_outcome() {
1351        let config = create_test_config();
1352        let mut strategy = RateLimitingStrategy::new(config);
1353
1354        let decision = Decision {
1355            id: Uuid::new_v4().to_string(),
1356            timestamp: Utc::now(),
1357            strategy: "rate_limiting".to_string(),
1358            decision_type: DecisionType::AdjustRateLimits,
1359            confidence: 0.8,
1360            expected_impact: ExpectedImpact {
1361                cost_change_usd: Some(-1.0),
1362                latency_change_ms: Some(-5.0),
1363                quality_change: None,
1364                throughput_change_rps: Some(-10.0),
1365                success_rate_change_pct: Some(0.5),
1366                time_to_impact: std::time::Duration::from_secs(60),
1367                confidence: 0.8,
1368            },
1369            config_changes: vec![
1370                ConfigChange {
1371                    config_type: ConfigType::RateLimit,
1372                    path: "rate_limit.requests_per_second".to_string(),
1373                    old_value: Some(serde_json::json!(100.0)),
1374                    new_value: serde_json::json!(90.0),
1375                    description: "Reduce RPS".to_string(),
1376                },
1377                ConfigChange {
1378                    config_type: ConfigType::RateLimit,
1379                    path: "rate_limit.tokens_per_minute".to_string(),
1380                    old_value: Some(serde_json::json!(100000)),
1381                    new_value: serde_json::json!(90000),
1382                    description: "Reduce tokens".to_string(),
1383                },
1384            ],
1385            justification: "Test".to_string(),
1386            related_insights: vec![],
1387            related_recommendations: vec![],
1388            priority: 80,
1389            requires_approval: false,
1390            safety_checks: vec![],
1391            rollback_plan: None,
1392            metadata: HashMap::new(),
1393        };
1394
1395        let outcome = DecisionOutcome {
1396            decision_id: decision.id.clone(),
1397            execution_time: Utc::now(),
1398            success: true,
1399            error: None,
1400            actual_impact: Some(super::super::types::ActualImpact {
1401                cost_change_usd: -1.2,
1402                latency_change_ms: -4.0,
1403                quality_change: 0.0,
1404                throughput_change_rps: -10.0,
1405                success_rate_change_pct: 0.5,
1406                time_to_impact: std::time::Duration::from_secs(60),
1407                variance_from_expected: 0.2,
1408            }),
1409            rolled_back: false,
1410            rollback_reason: None,
1411            metrics_before: create_test_input(2.0, 100.0, 100.0).current_metrics,
1412            metrics_after: Some(create_test_input(1.5, 95.0, 90.0).current_metrics),
1413            duration: std::time::Duration::from_secs(300),
1414        };
1415
1416        let result = strategy.learn(&decision, &outcome).await;
1417        assert!(result.is_ok());
1418
1419        // Check that learning metrics were updated
1420        let state = strategy.state.read().await;
1421        assert_eq!(state.learning_metrics.total_adjustments, 1);
1422        assert!(state.learning_metrics.avg_effectiveness > 0.5);
1423        assert_eq!(state.current_rps_limit, 90.0);
1424    }
1425
1426    #[tokio::test]
1427    async fn test_learn_failed_outcome() {
1428        let config = create_test_config();
1429        let mut strategy = RateLimitingStrategy::new(config);
1430
1431        let decision = Decision {
1432            id: Uuid::new_v4().to_string(),
1433            timestamp: Utc::now(),
1434            strategy: "rate_limiting".to_string(),
1435            decision_type: DecisionType::AdjustRateLimits,
1436            confidence: 0.8,
1437            expected_impact: ExpectedImpact {
1438                cost_change_usd: Some(-1.0),
1439                latency_change_ms: None,
1440                quality_change: None,
1441                throughput_change_rps: None,
1442                success_rate_change_pct: None,
1443                time_to_impact: std::time::Duration::from_secs(60),
1444                confidence: 0.8,
1445            },
1446            config_changes: vec![],
1447            justification: "Test".to_string(),
1448            related_insights: vec![],
1449            related_recommendations: vec![],
1450            priority: 80,
1451            requires_approval: false,
1452            safety_checks: vec![],
1453            rollback_plan: None,
1454            metadata: HashMap::new(),
1455        };
1456
1457        let outcome = DecisionOutcome {
1458            decision_id: decision.id.clone(),
1459            execution_time: Utc::now(),
1460            success: false,
1461            error: Some("Execution failed".to_string()),
1462            actual_impact: None,
1463            rolled_back: false,
1464            rollback_reason: None,
1465            metrics_before: create_test_input(2.0, 100.0, 100.0).current_metrics,
1466            metrics_after: None,
1467            duration: std::time::Duration::from_secs(10),
1468        };
1469
1470        let result = strategy.learn(&decision, &outcome).await;
1471        assert!(result.is_ok());
1472
1473        // Check that learning metrics were updated with low effectiveness
1474        let state = strategy.state.read().await;
1475        assert_eq!(state.learning_metrics.total_adjustments, 1);
1476        assert!(state.learning_metrics.avg_effectiveness < 0.5);
1477    }
1478
1479    #[tokio::test]
1480    async fn test_get_stats() {
1481        let config = create_test_config();
1482        let strategy = RateLimitingStrategy::new(config);
1483
1484        let stats = strategy.get_stats();
1485        assert!(stats.get("current_rps_limit").is_some());
1486        assert!(stats.get("current_token_limit").is_some());
1487        assert!(stats.get("learning_metrics").is_some());
1488    }
1489
1490    #[tokio::test]
1491    async fn test_gradual_adjustment() {
1492        let config = create_test_config();
1493        let strategy = RateLimitingStrategy::new(config);
1494
1495        // Simulate needing a large adjustment
1496        {
1497            let mut state = strategy.state.write().await;
1498            state.current_rps_limit = 100.0;
1499            // Set very high error rate to trigger large reduction
1500            state.add_error_rate_sample(10.0, 1000);
1501            state.add_error_rate_sample(10.0, 1000);
1502        }
1503
1504        let input = create_test_input(10.0, 100.0, 100.0);
1505        let criteria = DecisionCriteria::default();
1506
1507        let (new_rps, _, _, _) = strategy.calculate_optimal_limits(&input, &criteria).await;
1508
1509        // Should not reduce by more than 20%
1510        let state = strategy.state.read().await;
1511        let change_pct = (state.current_rps_limit - new_rps) / state.current_rps_limit;
1512        assert!(change_pct <= 0.2, "Change should be gradual (<= 20%)");
1513    }
1514}