1use dashmap::DashMap;
4use std::sync::atomic::AtomicUsize;
5use std::time::SystemTime;
6
7#[derive(Debug, Clone, Default)]
9pub struct ProviderStats {
10 pub requests: u64,
11 pub errors: u64,
12 pub total_tokens: u64,
13 pub total_cost: f64,
14 pub avg_latency_ms: f64,
15 pub last_used: Option<SystemTime>,
16 pub health_score: f64,
17}
18
19#[derive(Debug)]
21pub struct LoadBalancer {
22 pub(crate) strategy: LoadBalancingStrategy,
23 pub(crate) round_robin_counters: DashMap<String, AtomicUsize>,
24}
25
26#[derive(Debug, Clone)]
28pub enum LoadBalancingStrategy {
29 RoundRobin,
30 LeastLatency,
31 WeightedRandom,
32 HealthBased,
33}
34
35impl LoadBalancer {
36 pub(crate) fn new(strategy: LoadBalancingStrategy) -> Self {
37 Self {
38 strategy,
39 round_robin_counters: DashMap::new(),
40 }
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
51 fn test_provider_stats_default() {
52 let stats = ProviderStats::default();
53
54 assert_eq!(stats.requests, 0);
55 assert_eq!(stats.errors, 0);
56 assert_eq!(stats.total_tokens, 0);
57 assert!((stats.total_cost - 0.0).abs() < f64::EPSILON);
58 assert!((stats.avg_latency_ms - 0.0).abs() < f64::EPSILON);
59 assert!(stats.last_used.is_none());
60 assert!((stats.health_score - 0.0).abs() < f64::EPSILON);
61 }
62
63 #[test]
64 fn test_provider_stats_with_values() {
65 let stats = ProviderStats {
66 requests: 1000,
67 errors: 10,
68 total_tokens: 500000,
69 total_cost: 25.50,
70 avg_latency_ms: 150.0,
71 last_used: Some(SystemTime::now()),
72 health_score: 0.95,
73 };
74
75 assert_eq!(stats.requests, 1000);
76 assert_eq!(stats.errors, 10);
77 assert_eq!(stats.total_tokens, 500000);
78 assert!((stats.total_cost - 25.50).abs() < f64::EPSILON);
79 }
80
81 #[test]
82 fn test_provider_stats_clone() {
83 let stats = ProviderStats {
84 requests: 500,
85 errors: 5,
86 total_tokens: 250000,
87 total_cost: 12.75,
88 avg_latency_ms: 100.0,
89 last_used: None,
90 health_score: 0.90,
91 };
92
93 let cloned = stats.clone();
94 assert_eq!(cloned.requests, stats.requests);
95 assert_eq!(cloned.errors, stats.errors);
96 assert_eq!(cloned.health_score, stats.health_score);
97 }
98
99 #[test]
100 fn test_provider_stats_debug() {
101 let stats = ProviderStats::default();
102 let debug_str = format!("{:?}", stats);
103
104 assert!(debug_str.contains("ProviderStats"));
105 assert!(debug_str.contains("requests"));
106 }
107
108 #[test]
109 fn test_provider_stats_error_rate() {
110 let stats = ProviderStats {
111 requests: 100,
112 errors: 5,
113 total_tokens: 0,
114 total_cost: 0.0,
115 avg_latency_ms: 0.0,
116 last_used: None,
117 health_score: 0.0,
118 };
119
120 let error_rate = if stats.requests > 0 {
121 stats.errors as f64 / stats.requests as f64
122 } else {
123 0.0
124 };
125
126 assert!((error_rate - 0.05).abs() < f64::EPSILON);
127 }
128
129 #[test]
130 fn test_provider_stats_tokens_per_request() {
131 let stats = ProviderStats {
132 requests: 100,
133 errors: 0,
134 total_tokens: 50000,
135 total_cost: 0.0,
136 avg_latency_ms: 0.0,
137 last_used: None,
138 health_score: 0.0,
139 };
140
141 let avg_tokens = if stats.requests > 0 {
142 stats.total_tokens as f64 / stats.requests as f64
143 } else {
144 0.0
145 };
146
147 assert!((avg_tokens - 500.0).abs() < f64::EPSILON);
148 }
149
150 #[test]
151 fn test_provider_stats_cost_per_request() {
152 let stats = ProviderStats {
153 requests: 1000,
154 errors: 0,
155 total_tokens: 0,
156 total_cost: 10.0,
157 avg_latency_ms: 0.0,
158 last_used: None,
159 health_score: 0.0,
160 };
161
162 let cost_per_request = if stats.requests > 0 {
163 stats.total_cost / stats.requests as f64
164 } else {
165 0.0
166 };
167
168 assert!((cost_per_request - 0.01).abs() < f64::EPSILON);
169 }
170
171 #[test]
172 fn test_provider_stats_high_load() {
173 let stats = ProviderStats {
174 requests: 1_000_000,
175 errors: 1000,
176 total_tokens: 500_000_000,
177 total_cost: 5000.0,
178 avg_latency_ms: 200.0,
179 last_used: Some(SystemTime::now()),
180 health_score: 0.85,
181 };
182
183 assert!(stats.requests > 0);
184 assert!(stats.health_score > 0.8);
185 }
186
187 #[test]
188 fn test_provider_stats_unhealthy() {
189 let stats = ProviderStats {
190 requests: 100,
191 errors: 50, total_tokens: 10000,
193 total_cost: 1.0,
194 avg_latency_ms: 5000.0, last_used: Some(SystemTime::now()),
196 health_score: 0.2,
197 };
198
199 let error_rate = stats.errors as f64 / stats.requests as f64;
200 assert!(error_rate > 0.3);
201 assert!(stats.health_score < 0.5);
202 }
203
204 #[test]
207 fn test_load_balancing_strategy_round_robin() {
208 let strategy = LoadBalancingStrategy::RoundRobin;
209 let debug_str = format!("{:?}", strategy);
210 assert!(debug_str.contains("RoundRobin"));
211 }
212
213 #[test]
214 fn test_load_balancing_strategy_least_latency() {
215 let strategy = LoadBalancingStrategy::LeastLatency;
216 let debug_str = format!("{:?}", strategy);
217 assert!(debug_str.contains("LeastLatency"));
218 }
219
220 #[test]
221 fn test_load_balancing_strategy_weighted_random() {
222 let strategy = LoadBalancingStrategy::WeightedRandom;
223 let debug_str = format!("{:?}", strategy);
224 assert!(debug_str.contains("WeightedRandom"));
225 }
226
227 #[test]
228 fn test_load_balancing_strategy_health_based() {
229 let strategy = LoadBalancingStrategy::HealthBased;
230 let debug_str = format!("{:?}", strategy);
231 assert!(debug_str.contains("HealthBased"));
232 }
233
234 #[test]
235 fn test_load_balancing_strategy_clone() {
236 let strategy = LoadBalancingStrategy::LeastLatency;
237 let cloned = strategy.clone();
238
239 let original_str = format!("{:?}", strategy);
240 let cloned_str = format!("{:?}", cloned);
241 assert_eq!(original_str, cloned_str);
242 }
243
244 #[test]
247 fn test_load_balancer_new_round_robin() {
248 let balancer = LoadBalancer::new(LoadBalancingStrategy::RoundRobin);
249 let debug_str = format!("{:?}", balancer);
250 assert!(debug_str.contains("RoundRobin"));
251 }
252
253 #[test]
254 fn test_load_balancer_new_least_latency() {
255 let balancer = LoadBalancer::new(LoadBalancingStrategy::LeastLatency);
256 let debug_str = format!("{:?}", balancer);
257 assert!(debug_str.contains("LeastLatency"));
258 }
259
260 #[test]
261 fn test_load_balancer_new_weighted_random() {
262 let balancer = LoadBalancer::new(LoadBalancingStrategy::WeightedRandom);
263 let debug_str = format!("{:?}", balancer);
264 assert!(debug_str.contains("WeightedRandom"));
265 }
266
267 #[test]
268 fn test_load_balancer_new_health_based() {
269 let balancer = LoadBalancer::new(LoadBalancingStrategy::HealthBased);
270 let debug_str = format!("{:?}", balancer);
271 assert!(debug_str.contains("HealthBased"));
272 }
273
274 #[test]
275 fn test_load_balancer_debug() {
276 let balancer = LoadBalancer::new(LoadBalancingStrategy::RoundRobin);
277 let debug_str = format!("{:?}", balancer);
278 assert!(debug_str.contains("LoadBalancer"));
279 }
280
281 #[test]
284 fn test_provider_selection_simulation() {
285 let providers = [
286 ProviderStats {
287 requests: 100,
288 errors: 1,
289 health_score: 0.99,
290 avg_latency_ms: 50.0,
291 ..ProviderStats::default()
292 },
293 ProviderStats {
294 requests: 200,
295 errors: 20,
296 health_score: 0.80,
297 avg_latency_ms: 100.0,
298 ..ProviderStats::default()
299 },
300 ProviderStats {
301 requests: 50,
302 errors: 0,
303 health_score: 1.0,
304 avg_latency_ms: 30.0,
305 ..ProviderStats::default()
306 },
307 ];
308
309 let best_provider = providers
311 .iter()
312 .enumerate()
313 .max_by(|(_, a), (_, b)| {
314 a.health_score
315 .partial_cmp(&b.health_score)
316 .unwrap_or(std::cmp::Ordering::Equal)
317 })
318 .map(|(idx, _)| idx);
319
320 assert_eq!(best_provider, Some(2)); }
322
323 #[test]
324 fn test_latency_based_selection_simulation() {
325 let providers = [
326 ProviderStats {
327 avg_latency_ms: 150.0,
328 ..ProviderStats::default()
329 },
330 ProviderStats {
331 avg_latency_ms: 50.0,
332 ..ProviderStats::default()
333 },
334 ProviderStats {
335 avg_latency_ms: 100.0,
336 ..ProviderStats::default()
337 },
338 ];
339
340 let fastest = providers
342 .iter()
343 .enumerate()
344 .min_by(|(_, a), (_, b)| {
345 a.avg_latency_ms
346 .partial_cmp(&b.avg_latency_ms)
347 .unwrap_or(std::cmp::Ordering::Equal)
348 })
349 .map(|(idx, _)| idx);
350
351 assert_eq!(fastest, Some(1)); }
353
354 #[test]
355 fn test_stats_aggregation() {
356 let providers = [
357 ProviderStats {
358 requests: 100,
359 errors: 5,
360 total_tokens: 50000,
361 total_cost: 5.0,
362 ..ProviderStats::default()
363 },
364 ProviderStats {
365 requests: 200,
366 errors: 10,
367 total_tokens: 100000,
368 total_cost: 10.0,
369 ..ProviderStats::default()
370 },
371 ];
372
373 let total_requests: u64 = providers.iter().map(|p| p.requests).sum();
374 let total_errors: u64 = providers.iter().map(|p| p.errors).sum();
375 let total_tokens: u64 = providers.iter().map(|p| p.total_tokens).sum();
376 let total_cost: f64 = providers.iter().map(|p| p.total_cost).sum();
377
378 assert_eq!(total_requests, 300);
379 assert_eq!(total_errors, 15);
380 assert_eq!(total_tokens, 150000);
381 assert!((total_cost - 15.0).abs() < f64::EPSILON);
382 }
383}