1use 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
24pub struct RateLimitingStrategy {
26 config: RateLimitingConfig,
27 state: Arc<RwLock<RateLimitState>>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32struct RateLimitState {
33 current_rps_limit: f64,
35
36 current_token_limit: u64,
38
39 current_cost_limit: f64,
41
42 token_bucket: TokenBucket,
44
45 adjustment_history: VecDeque<RateLimitAdjustment>,
47
48 error_rate_history: VecDeque<ErrorRateSample>,
50
51 cost_rate_history: VecDeque<CostRateSample>,
53
54 traffic_pattern: TrafficPattern,
56
57 learning_metrics: LearningMetrics,
59
60 last_adjustment: DateTime<Utc>,
62
63 decisions_made: u64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69struct TokenBucket {
70 tokens: f64,
72
73 capacity: f64,
75
76 refill_rate: f64,
78
79 last_refill: DateTime<Utc>,
81
82 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 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 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 fn update(&mut self, new_capacity: f64, new_refill_rate: f64) {
123 self.refill(); self.capacity = new_capacity;
125 self.refill_rate = new_refill_rate;
126
127 if self.tokens > self.capacity * self.burst_multiplier {
129 self.tokens = self.capacity * self.burst_multiplier;
130 }
131 }
132
133 fn utilization(&self) -> f64 {
135 1.0 - (self.tokens / self.capacity).clamp(0.0, 1.0)
136 }
137}
138
139#[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#[derive(Debug, Clone, Serialize, Deserialize)]
155struct ErrorRateSample {
156 timestamp: DateTime<Utc>,
157 error_rate_pct: f64,
158 request_count: u64,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163struct CostRateSample {
164 timestamp: DateTime<Utc>,
165 cost_per_hour: f64,
166 tokens_per_minute: f64,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171struct TrafficPattern {
172 avg_rps: f64,
174
175 peak_rps: f64,
177
178 avg_burst_size: f64,
180
181 burst_frequency: f64,
183
184 volatility: f64,
186
187 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
219struct LearningMetrics {
220 total_adjustments: u64,
222
223 successful_adjustments: u64,
225
226 failed_adjustments: u64,
228
229 avg_effectiveness: f64,
231
232 confidence: f64,
234
235 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 let alpha = self.learning_rate;
262 self.avg_effectiveness = alpha * effectiveness + (1.0 - alpha) * self.avg_effectiveness;
263
264 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, 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 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 if self.error_rate_history.len() > 100 {
305 self.error_rate_history.pop_front();
306 }
307 }
308
309 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 if self.cost_rate_history.len() > 100 {
321 self.cost_rate_history.pop_front();
322 }
323 }
324
325 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 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 fn needs_adjustment(&self, config: &RateLimitingConfig) -> bool {
347 let min_interval = ChronoDuration::minutes(5);
349 if Utc::now() - self.last_adjustment < min_interval {
350 return false;
351 }
352
353 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 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 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 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 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 let current_error_rate = metrics.error_rate_pct;
397 let current_cost_rate = metrics.daily_cost_usd / 24.0; let current_rps = metrics.throughput_rps;
399
400 let error_adjustment = if self.config.adaptive_enabled {
402 if current_error_rate > self.config.target_error_rate_pct * 2.0 {
403 0.7 } else if current_error_rate > self.config.target_error_rate_pct * 1.5 {
405 0.85 } else if current_error_rate > self.config.target_error_rate_pct {
407 0.95 } else if current_error_rate < self.config.target_error_rate_pct * 0.5 {
409 1.1 } else {
411 1.0 }
413 } else {
414 1.0
415 };
416
417 let cost_adjustment = if current_cost_rate > self.config.max_cost_per_hour * 0.9 {
419 0.8 } else if current_cost_rate > self.config.max_cost_per_hour * 0.8 {
421 0.9 } else if current_cost_rate < self.config.max_cost_per_hour * 0.5 {
423 1.15 } else {
425 1.0
426 };
427
428 let traffic_adjustment = if state.traffic_pattern.volatility > 0.5 {
430 1.05 } else {
432 1.0
433 };
434
435 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 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 let new_rps_limit = (state.current_rps_limit * safe_adjustment)
449 .max(1.0) .min(self.config.max_requests_per_second * 2.0); 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 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 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 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 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 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 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 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 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), }
594 }
595
596 async fn update_state(&self, input: &DecisionInput) {
598 let mut state = self.state.write().await;
599 let metrics = &input.current_metrics;
600
601 state.add_error_rate_sample(metrics.error_rate_pct, metrics.request_count);
603
604 let cost_per_hour = metrics.daily_cost_usd / 24.0;
606 let tokens_per_minute = metrics.throughput_rps * 60.0; state.add_cost_rate_sample(cost_per_hour, tokens_per_minute);
608
609 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 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 self.update_state(input).await;
651
652 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 let (new_rps, new_tokens, new_cost, confidence) =
662 self.calculate_optimal_limits(input, criteria).await;
663
664 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 let safety_checks = self.perform_safety_checks(new_rps, new_tokens, new_cost, criteria).await;
678
679 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 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 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 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 let rollback_plan = self.create_rollback_plan().await;
760
761 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 } else if avg_cost_rate > self.config.max_cost_per_hour {
793 85 } else {
795 self.priority()
796 },
797 requires_approval: rps_change_pct.abs() > 15.0, 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 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 if decision.strategy != self.name() {
834 return Err(DecisionError::ValidationFailed(
835 "Decision strategy does not match".to_string(),
836 ));
837 }
838
839 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 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 if decision.config_changes.is_empty() {
857 return Err(DecisionError::ValidationFailed(
858 "No configuration changes specified".to_string(),
859 ));
860 }
861
862 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 let effectiveness = if outcome.success {
881 if let Some(actual_impact) = &outcome.actual_impact {
882 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 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 let outcome_quality = if outcome.rolled_back {
898 0.2 } else if actual_cost_change < 0.0 {
900 0.9 } else {
902 0.6 };
904
905 (cost_accuracy * 0.4 + outcome_quality * 0.6).clamp(0.0, 1.0)
906 } else {
907 0.7 }
909 } else {
910 0.1 };
912
913 state.learning_metrics.record_outcome(effectiveness);
915
916 if outcome.success && !outcome.rolled_back {
918 if let Some(actual_impact) = &outcome.actual_impact {
919 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 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 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 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 assert!(bucket.try_consume(50.0));
1088 assert!(bucket.try_consume(50.0));
1089 assert!(!bucket.try_consume(1.0)); }
1091
1092 #[tokio::test]
1093 async fn test_token_bucket_refill() {
1094 let mut bucket = TokenBucket::new(100.0, 100.0, 1.5); assert!(bucket.try_consume(100.0));
1098 assert!(!bucket.try_consume(1.0));
1099
1100 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1102 bucket.refill();
1103
1104 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); tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1114 bucket.refill();
1115
1116 assert!(bucket.try_consume(150.0)); }
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 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 let input = create_test_input(0.5, 200.0, 50.0); 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 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 let input = create_test_input(5.0, 100.0, 80.0);
1166
1167 {
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 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 let input = create_test_input(0.2, 50.0, 30.0);
1197
1198 {
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 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 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, 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 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 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 {
1497 let mut state = strategy.state.write().await;
1498 state.current_rps_limit = 100.0;
1499 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 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}