1use std::collections::VecDeque;
2use std::sync::Mutex;
3use std::time::{Duration, Instant};
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Default)]
12pub struct PoolMetrics {
13 pub total_proxies: usize,
15 pub healthy_proxies: usize,
16 pub cooldown_proxies: usize,
17 pub dead_proxies: usize,
18 pub retired_proxies: usize,
20
21 pub pending_tasks: usize,
23 pub delayed_tasks: usize,
24 pub completed_tasks: u64,
25 pub failed_tasks: u64,
26
27 pub throughput_1s: f64,
29 pub throughput_10s: f64,
30 pub throughput_60s: f64,
31
32 pub success_rate_1m: f64,
34 pub avg_latency_ms: f64,
35
36 pub inflight: usize,
38 pub requeued_tasks: u64,
39 pub zero_available_events: u64,
40 pub skipped_no_permit: u64,
41 pub skipped_rate_limit: u64,
42 pub skipped_cooldown: u64,
43 pub dispatch_count: u64,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ProxyHostStats {
49 pub success: u32,
50 pub fail: u32,
51 pub success_rate: f64,
52 pub avg_latency_ms: f64,
53 pub consecutive_fails: u32,
54}
55
56impl Default for ProxyHostStats {
57 fn default() -> Self {
58 Self {
59 success: 0,
60 fail: 0,
61 success_rate: 0.0,
62 avg_latency_ms: 0.0,
63 consecutive_fails: 0,
64 }
65 }
66}
67
68pub(crate) struct ThroughputTracker {
77 timestamps: Mutex<VecDeque<Instant>>,
78 max_window: Duration,
79}
80
81impl ThroughputTracker {
82 pub fn new() -> Self {
84 Self::with_max_window(Duration::from_secs(60))
85 }
86
87 pub fn with_max_window(max_window: Duration) -> Self {
89 Self {
90 timestamps: Mutex::new(VecDeque::new()),
91 max_window,
92 }
93 }
94
95 pub fn record(&self) {
97 self.record_at(Instant::now());
98 }
99
100 pub(crate) fn record_at(&self, now: Instant) {
102 let mut ts = self.timestamps.lock().unwrap();
103 ts.push_back(now);
104 Self::prune(&mut ts, now, self.max_window);
105 }
106
107 pub fn throughput(&self, window: Duration) -> f64 {
111 self.throughput_at(Instant::now(), window)
112 }
113
114 pub(crate) fn throughput_at(&self, now: Instant, window: Duration) -> f64 {
116 if window.is_zero() {
117 return 0.0;
118 }
119 let mut ts = self.timestamps.lock().unwrap();
120 Self::prune(&mut ts, now, self.max_window);
121
122 let cutoff = now.checked_sub(window).unwrap_or(now);
123 let count = ts.iter().filter(|&&t| t >= cutoff).count();
124 count as f64 / window.as_secs_f64()
125 }
126
127 fn prune(ts: &mut VecDeque<Instant>, now: Instant, max_window: Duration) {
129 let cutoff = now.checked_sub(max_window).unwrap_or(now);
130 while let Some(&front) = ts.front() {
131 if front < cutoff {
132 ts.pop_front();
133 } else {
134 break;
135 }
136 }
137 }
138
139 #[cfg(test)]
141 fn len(&self) -> usize {
142 let ts = self.timestamps.lock().unwrap();
143 ts.len()
144 }
145}
146
147#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::time::Duration;
153
154 #[test]
157 fn pool_metrics_default_is_zeroed() {
158 let m = PoolMetrics::default();
159 assert_eq!(m.total_proxies, 0);
160 assert_eq!(m.healthy_proxies, 0);
161 assert_eq!(m.cooldown_proxies, 0);
162 assert_eq!(m.dead_proxies, 0);
163 assert_eq!(m.retired_proxies, 0);
164 assert_eq!(m.pending_tasks, 0);
165 assert_eq!(m.delayed_tasks, 0);
166 assert_eq!(m.completed_tasks, 0);
167 assert_eq!(m.failed_tasks, 0);
168 assert!((m.throughput_1s).abs() < f64::EPSILON);
169 assert!((m.throughput_10s).abs() < f64::EPSILON);
170 assert!((m.throughput_60s).abs() < f64::EPSILON);
171 assert!((m.success_rate_1m).abs() < f64::EPSILON);
172 assert!((m.avg_latency_ms).abs() < f64::EPSILON);
173 assert_eq!(m.inflight, 0);
174 assert_eq!(m.requeued_tasks, 0);
175 assert_eq!(m.zero_available_events, 0);
176 assert_eq!(m.skipped_no_permit, 0);
177 assert_eq!(m.skipped_rate_limit, 0);
178 assert_eq!(m.skipped_cooldown, 0);
179 assert_eq!(m.dispatch_count, 0);
180 }
181
182 #[test]
183 fn pool_metrics_is_clone() {
184 let m = PoolMetrics {
185 total_proxies: 42,
186 ..Default::default()
187 };
188 let m2 = m.clone();
189 assert_eq!(m2.total_proxies, 42);
190 }
191
192 #[test]
195 fn proxy_host_stats_default() {
196 let s = ProxyHostStats::default();
197 assert_eq!(s.success, 0);
198 assert_eq!(s.fail, 0);
199 assert!((s.success_rate).abs() < f64::EPSILON);
200 assert!((s.avg_latency_ms).abs() < f64::EPSILON);
201 assert_eq!(s.consecutive_fails, 0);
202 }
203
204 #[test]
205 fn proxy_host_stats_serde_round_trip() {
206 let stats = ProxyHostStats {
207 success: 10,
208 fail: 2,
209 success_rate: 0.833,
210 avg_latency_ms: 120.5,
211 consecutive_fails: 0,
212 };
213 let json = serde_json::to_string(&stats).unwrap();
214 let deser: ProxyHostStats = serde_json::from_str(&json).unwrap();
215 assert_eq!(deser.success, 10);
216 assert_eq!(deser.fail, 2);
217 assert!((deser.success_rate - 0.833).abs() < 1e-6);
218 assert!((deser.avg_latency_ms - 120.5).abs() < 1e-6);
219 assert_eq!(deser.consecutive_fails, 0);
220 }
221
222 #[test]
225 fn new_tracker_is_empty() {
226 let t = ThroughputTracker::new();
227 assert_eq!(t.len(), 0);
228 assert!((t.throughput(Duration::from_secs(1))).abs() < f64::EPSILON);
229 }
230
231 #[test]
232 fn record_increases_count() {
233 let t = ThroughputTracker::new();
234 t.record();
235 assert_eq!(t.len(), 1);
236 t.record();
237 assert_eq!(t.len(), 2);
238 }
239
240 #[test]
241 fn throughput_with_zero_window_is_zero() {
242 let t = ThroughputTracker::new();
243 t.record();
244 assert!((t.throughput(Duration::ZERO)).abs() < f64::EPSILON);
245 }
246
247 #[test]
248 fn throughput_over_1s_window() {
249 let t = ThroughputTracker::new();
250 let now = Instant::now();
251
252 for i in 0..5 {
254 t.record_at(now - Duration::from_millis(100 * i));
255 }
256
257 let tp = t.throughput_at(now, Duration::from_secs(1));
258 assert!((tp - 5.0).abs() < 0.01);
260 }
261
262 #[test]
263 fn throughput_over_10s_window() {
264 let t = ThroughputTracker::new();
265 let now = Instant::now();
266
267 for i in 0..20 {
269 t.record_at(now - Duration::from_millis(500 * i));
270 }
271
272 let tp = t.throughput_at(now, Duration::from_secs(10));
273 assert!((tp - 2.0).abs() < 0.01);
275 }
276
277 #[test]
278 fn old_entries_are_pruned() {
279 let t = ThroughputTracker::with_max_window(Duration::from_secs(5));
280 let now = Instant::now();
281
282 t.record_at(now - Duration::from_secs(10));
284 t.record_at(now - Duration::from_secs(1));
286
287 let tp = t.throughput_at(now, Duration::from_secs(5));
289 assert!((tp - 0.2).abs() < 0.01); assert_eq!(t.len(), 1);
292 }
293
294 #[test]
295 fn window_larger_than_max_window_still_works() {
296 let t = ThroughputTracker::with_max_window(Duration::from_secs(5));
297 let now = Instant::now();
298
299 t.record_at(now - Duration::from_secs(3));
300 t.record_at(now - Duration::from_secs(1));
301
302 let tp = t.throughput_at(now, Duration::from_secs(60));
304 assert!((tp - 2.0 / 60.0).abs() < 0.01);
306 }
307
308 #[test]
309 fn concurrent_access_does_not_panic() {
310 use std::sync::Arc;
311 use std::thread;
312
313 let tracker = Arc::new(ThroughputTracker::new());
314 let mut handles = Vec::new();
315
316 for _ in 0..8 {
317 let t = Arc::clone(&tracker);
318 handles.push(thread::spawn(move || {
319 for _ in 0..100 {
320 t.record();
321 let _ = t.throughput(Duration::from_secs(1));
322 }
323 }));
324 }
325
326 for h in handles {
327 h.join().unwrap();
328 }
329
330 assert_eq!(tracker.len(), 800);
332 }
333
334 #[test]
335 fn throughput_only_counts_events_within_requested_window() {
336 let t = ThroughputTracker::new();
337 let now = Instant::now();
338
339 for _ in 0..3 {
341 t.record_at(now - Duration::from_millis(500));
342 }
343 for _ in 0..2 {
345 t.record_at(now - Duration::from_secs(5));
346 }
347
348 let tp_1s = t.throughput_at(now, Duration::from_secs(1));
350 assert!((tp_1s - 3.0).abs() < 0.01);
351
352 let tp_10s = t.throughput_at(now, Duration::from_secs(10));
354 assert!((tp_10s - 0.5).abs() < 0.01);
355 }
356
357 #[test]
358 fn custom_max_window() {
359 let t = ThroughputTracker::with_max_window(Duration::from_secs(2));
360 assert_eq!(t.max_window, Duration::from_secs(2));
361 }
362
363 #[test]
364 fn default_max_window_is_60s() {
365 let t = ThroughputTracker::new();
366 assert_eq!(t.max_window, Duration::from_secs(60));
367 }
368}