Skip to main content

gosh_dl/http/
connection.rs

1//! Connection Pool Management
2//!
3//! This module provides HTTP connection pooling with health checks,
4//! retry logic, and speed limiting capabilities.
5
6use crate::config::HttpConfig;
7use crate::error::{EngineError, NetworkErrorKind, Result};
8use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
9use parking_lot::RwLock as ParkingRwLock;
10use reqwest::Client;
11use std::num::NonZeroU32;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15use tokio::sync::RwLock;
16
17/// Connection pool with rate limiting and health monitoring
18pub struct ConnectionPool {
19    /// HTTP client (reqwest handles its own connection pool)
20    client: Client,
21    /// Global rate limiter for download speed
22    download_limiter: ParkingRwLock<Option<Arc<DefaultDirectRateLimiter>>>,
23    /// Global rate limiter for upload speed
24    upload_limiter: ParkingRwLock<Option<Arc<DefaultDirectRateLimiter>>>,
25    /// Total bytes downloaded
26    total_downloaded: AtomicU64,
27    /// Total bytes uploaded
28    total_uploaded: AtomicU64,
29    /// Active connection count
30    active_connections: AtomicU64,
31    /// Connection statistics
32    stats: RwLock<ConnectionStats>,
33}
34
35/// Connection statistics
36#[derive(Debug, Clone, Default)]
37pub struct ConnectionStats {
38    /// Total connections created
39    pub connections_created: u64,
40    /// Total successful requests
41    pub successful_requests: u64,
42    /// Total failed requests
43    pub failed_requests: u64,
44    /// Total retried requests
45    pub retried_requests: u64,
46    /// Average response time in milliseconds
47    pub avg_response_time_ms: f64,
48    /// Last error message
49    pub last_error: Option<String>,
50}
51
52impl ConnectionPool {
53    /// Create a new connection pool
54    pub fn new(config: &HttpConfig) -> Result<Self> {
55        let mut builder = Client::builder()
56            .connect_timeout(Duration::from_secs(config.connect_timeout))
57            .read_timeout(Duration::from_secs(config.read_timeout))
58            .redirect(reqwest::redirect::Policy::limited(config.max_redirects))
59            .danger_accept_invalid_certs(config.accept_invalid_certs)
60            .pool_max_idle_per_host(32)
61            .pool_idle_timeout(Duration::from_secs(90))
62            // This is a download engine: preserve the exact bytes on the wire.
63            // Transparent decompression breaks progress accounting, checksums,
64            // range semantics, and on-disk fidelity.
65            .gzip(false)
66            .brotli(false);
67
68        // Add proxy if configured
69        if let Some(ref proxy_url) = config.proxy_url {
70            let proxy = reqwest::Proxy::all(proxy_url)
71                .map_err(|e| EngineError::Internal(format!("Invalid proxy URL: {}", e)))?;
72            builder = builder.proxy(proxy);
73        }
74
75        let client = builder
76            .build()
77            .map_err(|e| EngineError::Internal(format!("Failed to create HTTP client: {}", e)))?;
78
79        Ok(Self {
80            client,
81            download_limiter: ParkingRwLock::new(None),
82            upload_limiter: ParkingRwLock::new(None),
83            total_downloaded: AtomicU64::new(0),
84            total_uploaded: AtomicU64::new(0),
85            active_connections: AtomicU64::new(0),
86            stats: RwLock::new(ConnectionStats::default()),
87        })
88    }
89
90    /// Create a connection pool with rate limiting
91    pub fn with_limits(
92        config: &HttpConfig,
93        download_limit: Option<u64>,
94        upload_limit: Option<u64>,
95    ) -> Result<Self> {
96        let pool = Self::new(config)?;
97        pool.set_download_limit(download_limit);
98        pool.set_upload_limit(upload_limit);
99
100        Ok(pool)
101    }
102
103    /// Get the underlying HTTP client
104    pub fn client(&self) -> &Client {
105        &self.client
106    }
107
108    /// Update download speed limit
109    pub fn set_download_limit(&self, limit: Option<u64>) {
110        *self.download_limiter.write() = limit.and_then(build_rate_limiter);
111    }
112
113    /// Update upload speed limit
114    pub fn set_upload_limit(&self, limit: Option<u64>) {
115        *self.upload_limiter.write() = limit.and_then(build_rate_limiter);
116    }
117
118    /// Wait for rate limiter permission to download bytes
119    pub async fn acquire_download(&self, bytes: u64) {
120        let limiter = self.download_limiter.read().clone();
121        if let Some(limiter) = limiter {
122            for chunk in limiter_chunks(bytes) {
123                let _ = limiter.until_n_ready(chunk).await;
124            }
125        }
126    }
127
128    /// Wait for rate limiter permission to upload bytes
129    pub async fn acquire_upload(&self, bytes: u64) {
130        let limiter = self.upload_limiter.read().clone();
131        if let Some(limiter) = limiter {
132            for chunk in limiter_chunks(bytes) {
133                let _ = limiter.until_n_ready(chunk).await;
134            }
135        }
136    }
137
138    /// Record downloaded bytes
139    pub fn record_download(&self, bytes: u64) {
140        self.total_downloaded.fetch_add(bytes, Ordering::Relaxed);
141    }
142
143    /// Record uploaded bytes
144    pub fn record_upload(&self, bytes: u64) {
145        self.total_uploaded.fetch_add(bytes, Ordering::Relaxed);
146    }
147
148    /// Get total downloaded bytes
149    pub fn total_downloaded(&self) -> u64 {
150        self.total_downloaded.load(Ordering::Relaxed)
151    }
152
153    /// Get total uploaded bytes
154    pub fn total_uploaded(&self) -> u64 {
155        self.total_uploaded.load(Ordering::Relaxed)
156    }
157
158    /// Increment active connection count
159    pub fn connection_started(&self) {
160        self.active_connections.fetch_add(1, Ordering::Relaxed);
161    }
162
163    /// Decrement active connection count
164    pub fn connection_finished(&self) {
165        self.active_connections.fetch_sub(1, Ordering::Relaxed);
166    }
167
168    /// Get active connection count
169    pub fn active_connections(&self) -> u64 {
170        self.active_connections.load(Ordering::Relaxed)
171    }
172
173    /// Record a successful request
174    pub async fn record_success(&self, response_time_ms: f64) {
175        let mut stats = self.stats.write().await;
176        stats.successful_requests += 1;
177
178        // Update average response time (exponential moving average)
179        let alpha = 0.2;
180        stats.avg_response_time_ms =
181            alpha * response_time_ms + (1.0 - alpha) * stats.avg_response_time_ms;
182    }
183
184    /// Record a failed request
185    pub async fn record_failure(&self, error: &str) {
186        let mut stats = self.stats.write().await;
187        stats.failed_requests += 1;
188        stats.last_error = Some(error.to_string());
189    }
190
191    /// Record a retried request
192    pub async fn record_retry(&self) {
193        let mut stats = self.stats.write().await;
194        stats.retried_requests += 1;
195    }
196
197    /// Get connection statistics
198    pub async fn stats(&self) -> ConnectionStats {
199        self.stats.read().await.clone()
200    }
201}
202
203fn build_rate_limiter(limit: u64) -> Option<Arc<DefaultDirectRateLimiter>> {
204    let clamped = limit.min(u32::MAX as u64) as u32;
205    NonZeroU32::new(clamped).map(|n| Arc::new(RateLimiter::direct(Quota::per_second(n))))
206}
207
208fn limiter_chunks(bytes: u64) -> Vec<NonZeroU32> {
209    const CHUNK_SIZE: u64 = 16 * 1024;
210
211    if bytes == 0 {
212        return Vec::new();
213    }
214
215    let full_chunks = bytes / CHUNK_SIZE;
216    let remainder = bytes % CHUNK_SIZE;
217    let mut chunks = Vec::with_capacity(full_chunks as usize + usize::from(remainder > 0));
218
219    for _ in 0..full_chunks {
220        chunks.push(NonZeroU32::new(CHUNK_SIZE as u32).expect("chunk size is non-zero"));
221    }
222
223    if remainder > 0 {
224        chunks.push(NonZeroU32::new(remainder as u32).expect("remainder is non-zero"));
225    }
226
227    chunks
228}
229
230#[cfg(test)]
231mod limiter_tests {
232    use super::limiter_chunks;
233
234    #[test]
235    fn limiter_chunks_is_empty_for_zero_bytes() {
236        assert!(limiter_chunks(0).is_empty());
237    }
238
239    #[test]
240    fn limiter_chunks_preserves_exact_byte_count() {
241        let chunks = limiter_chunks(16 * 1024 + 17);
242        let total: u64 = chunks.into_iter().map(|chunk| chunk.get() as u64).sum();
243        assert_eq!(total, 16 * 1024 + 17);
244    }
245
246    #[test]
247    fn limiter_chunks_does_not_over_throttle_small_reads() {
248        let chunks = limiter_chunks(1);
249        assert_eq!(chunks.len(), 1);
250        assert_eq!(chunks[0].get(), 1);
251    }
252}
253
254/// Retry policy with exponential backoff and jitter
255#[derive(Debug, Clone)]
256pub struct RetryPolicy {
257    /// Maximum number of retry attempts
258    pub max_attempts: u32,
259    /// Initial delay in milliseconds
260    pub initial_delay_ms: u64,
261    /// Maximum delay in milliseconds
262    pub max_delay_ms: u64,
263    /// Jitter factor (0.0 to 1.0)
264    pub jitter_factor: f64,
265}
266
267impl Default for RetryPolicy {
268    fn default() -> Self {
269        Self {
270            max_attempts: 3,
271            initial_delay_ms: 1000,
272            max_delay_ms: 30000,
273            jitter_factor: 0.25,
274        }
275    }
276}
277
278impl RetryPolicy {
279    /// Create a new retry policy
280    pub fn new(max_attempts: u32, initial_delay_ms: u64, max_delay_ms: u64) -> Self {
281        Self {
282            max_attempts,
283            initial_delay_ms,
284            max_delay_ms,
285            jitter_factor: 0.25,
286        }
287    }
288
289    /// Calculate delay for a given attempt (0-indexed)
290    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
291        // Exponential backoff
292        let base = self.initial_delay_ms * 2u64.pow(attempt.min(10));
293        let capped = base.min(self.max_delay_ms);
294
295        // Add jitter: ±jitter_factor randomness
296        let jitter = (rand::random::<f64>() - 0.5) * 2.0 * self.jitter_factor;
297        let with_jitter = (capped as f64 * (1.0 + jitter)) as u64;
298
299        Duration::from_millis(with_jitter)
300    }
301
302    /// Check if we should retry based on error type
303    pub fn should_retry(&self, attempt: u32, error: &EngineError) -> bool {
304        if attempt >= self.max_attempts {
305            return false;
306        }
307
308        error.is_retryable()
309    }
310}
311
312/// Execute a request with retry logic
313pub async fn with_retry<F, T, Fut>(
314    pool: &ConnectionPool,
315    policy: &RetryPolicy,
316    operation: F,
317) -> Result<T>
318where
319    F: Fn() -> Fut,
320    Fut: std::future::Future<Output = Result<T>>,
321{
322    let mut last_error = None;
323
324    for attempt in 0..policy.max_attempts {
325        let start = Instant::now();
326
327        match operation().await {
328            Ok(result) => {
329                let elapsed = start.elapsed().as_millis() as f64;
330                pool.record_success(elapsed).await;
331                return Ok(result);
332            }
333            Err(e) => {
334                let _elapsed = start.elapsed().as_millis() as f64;
335                pool.record_failure(&e.to_string()).await;
336
337                if policy.should_retry(attempt, &e) {
338                    pool.record_retry().await;
339                    let delay = policy.delay_for_attempt(attempt);
340                    tracing::debug!(
341                        "Request failed (attempt {}), retrying in {:?}: {}",
342                        attempt + 1,
343                        delay,
344                        e
345                    );
346                    tokio::time::sleep(delay).await;
347                    last_error = Some(e);
348                } else {
349                    return Err(e);
350                }
351            }
352        }
353    }
354
355    Err(last_error
356        .unwrap_or_else(|| EngineError::network(NetworkErrorKind::Other, "Max retries exceeded")))
357}
358
359/// Speed calculator for tracking download/upload rates
360#[derive(Debug)]
361pub struct SpeedCalculator {
362    /// Window size for averaging
363    window_size: usize,
364    /// Recent measurements (bytes, timestamp)
365    measurements: Vec<(u64, Instant)>,
366    /// Total bytes tracked
367    total_bytes: u64,
368}
369
370impl SpeedCalculator {
371    /// Create a new speed calculator
372    pub fn new(window_size: usize) -> Self {
373        Self {
374            window_size,
375            measurements: Vec::with_capacity(window_size),
376            total_bytes: 0,
377        }
378    }
379
380    /// Add a measurement
381    pub fn add_bytes(&mut self, bytes: u64) {
382        let now = Instant::now();
383        self.total_bytes += bytes;
384
385        if self.measurements.len() >= self.window_size {
386            self.measurements.remove(0);
387        }
388        self.measurements.push((bytes, now));
389    }
390
391    /// Calculate current speed in bytes/second
392    pub fn speed(&self) -> u64 {
393        if self.measurements.len() < 2 {
394            return 0;
395        }
396
397        let first = &self.measurements[0];
398        let last = &self.measurements[self.measurements.len() - 1];
399
400        let elapsed = last.1.duration_since(first.1).as_secs_f64();
401        if elapsed <= 0.0 {
402            return 0;
403        }
404
405        let bytes: u64 = self.measurements.iter().map(|(b, _)| *b).sum();
406        (bytes as f64 / elapsed) as u64
407    }
408
409    /// Get total bytes tracked
410    pub fn total(&self) -> u64 {
411        self.total_bytes
412    }
413
414    /// Reset the calculator
415    pub fn reset(&mut self) {
416        self.measurements.clear();
417        self.total_bytes = 0;
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_retry_delay() {
427        let policy = RetryPolicy::new(3, 1000, 30000);
428
429        // First attempt: ~1000ms
430        let delay0 = policy.delay_for_attempt(0);
431        assert!(delay0.as_millis() >= 750 && delay0.as_millis() <= 1250);
432
433        // Second attempt: ~2000ms
434        let delay1 = policy.delay_for_attempt(1);
435        assert!(delay1.as_millis() >= 1500 && delay1.as_millis() <= 2500);
436
437        // Third attempt: ~4000ms
438        let delay2 = policy.delay_for_attempt(2);
439        assert!(delay2.as_millis() >= 3000 && delay2.as_millis() <= 5000);
440    }
441
442    #[test]
443    fn test_speed_calculator() {
444        let mut calc = SpeedCalculator::new(10);
445
446        // Add measurements
447        calc.add_bytes(1000);
448        std::thread::sleep(Duration::from_millis(100));
449        calc.add_bytes(1000);
450        std::thread::sleep(Duration::from_millis(100));
451        calc.add_bytes(1000);
452
453        // Speed should be roughly 10000 bytes/sec (3000 bytes in 0.2 sec)
454        // But due to timing variations, we just check it's non-zero
455        let speed = calc.speed();
456        assert!(speed > 0);
457
458        assert_eq!(calc.total(), 3000);
459    }
460
461    #[test]
462    fn test_retry_policy_defaults() {
463        let policy = RetryPolicy::default();
464        assert_eq!(policy.max_attempts, 3);
465        assert_eq!(policy.initial_delay_ms, 1000);
466        assert_eq!(policy.max_delay_ms, 30000);
467    }
468}