1use dashmap::DashMap;
2use futures::StreamExt;
3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::time::Duration;
5use tracing::{debug, warn};
6
7use crate::constants;
8use crate::error::{Aria2Error, RecoverableError, Result};
9use crate::http::hyper_client::HyperDirectClient;
10
11pub use crate::selector::adaptive_uri_selector::{
13 score_source_raw as score_source, score_source_raw,
14};
15
16pub struct HttpSegmentDownloader {
17 client: reqwest::Client,
18 hyper_client: Option<HyperDirectClient>,
24}
25
26pub fn calculate_dynamic_segment_size(
29 total_remaining: u64,
30 num_connections: usize,
31 avg_speed_bps: f64,
32 elapsed_secs: u64,
33) -> u64 {
34 const MIN_SEGMENT: u64 = 1024 * 256; const MAX_SEGMENT: u64 = 1024 * 1024 * 16; if elapsed_secs < 2 || avg_speed_bps < 1024.0 {
38 return (total_remaining / num_connections.max(1) as u64).clamp(MIN_SEGMENT, MAX_SEGMENT);
40 }
41
42 let target_size = (avg_speed_bps * 10.0) as u64;
44 target_size.clamp(MIN_SEGMENT, MAX_SEGMENT)
45}
46
47pub struct ConnectionLimiter {
57 per_host: DashMap<String, AtomicUsize>,
58 global_count: AtomicUsize,
59 global_limit: usize,
60 per_host_limit: usize,
61}
62
63impl ConnectionLimiter {
64 pub fn new(global: usize, per_host: usize) -> Self {
66 Self {
67 per_host: DashMap::new(),
68 global_count: AtomicUsize::new(0),
69 global_limit: global,
70 per_host_limit: per_host,
71 }
72 }
73
74 pub fn try_acquire(&self, host: &str) -> bool {
92 loop {
96 let current = self.global_count.load(Ordering::Relaxed);
97 if current >= self.global_limit {
98 return false;
99 }
100 match self.global_count.compare_exchange_weak(
101 current,
102 current + 1,
103 Ordering::AcqRel,
104 Ordering::Relaxed,
105 ) {
106 Ok(_) => break,
107 Err(_) => continue,
108 }
109 }
110
111 let entry = self
119 .per_host
120 .entry(host.to_string())
121 .or_insert_with(|| AtomicUsize::new(0));
122 loop {
123 let current = entry.load(Ordering::Relaxed);
124 if current >= self.per_host_limit {
125 self.global_count.fetch_sub(1, Ordering::Relaxed);
127 return false;
128 }
129 match entry.compare_exchange_weak(
130 current,
131 current + 1,
132 Ordering::AcqRel,
133 Ordering::Relaxed,
134 ) {
135 Ok(_) => return true,
136 Err(_) => continue,
137 }
138 }
139 }
140
141 pub fn release(&self, host: &str) {
147 if let Some(entry) = self.per_host.get(host) {
148 let prev = entry.fetch_sub(1, Ordering::AcqRel);
149 debug_assert!(prev > 0, "release called more times than acquire for host");
150 }
151 let prev = self.global_count.fetch_sub(1, Ordering::AcqRel);
152 debug_assert!(prev > 0, "global release underflow");
153 }
154
155 pub fn host_count(&self, host: &str) -> usize {
157 self.per_host
158 .get(host)
159 .map(|e| e.load(Ordering::Relaxed))
160 .unwrap_or(0)
161 }
162
163 pub fn global_count(&self) -> usize {
165 self.global_count.load(Ordering::Relaxed)
166 }
167
168 pub fn global_limit(&self) -> usize {
170 self.global_limit
171 }
172
173 pub fn per_host_limit(&self) -> usize {
175 self.per_host_limit
176 }
177
178 pub fn available_for(&self, host: &str) -> usize {
182 let current = self.host_count(host);
183 if current >= self.per_host_limit {
184 return 0;
185 }
186 self.per_host_limit - current
187 }
188}
189
190impl HttpSegmentDownloader {
191 pub fn new(client: &reqwest::Client, use_hyper: bool) -> Self {
199 Self {
200 client: client.clone(),
201 hyper_client: if use_hyper {
202 Some(HyperDirectClient::new())
203 } else {
204 None
205 },
206 }
207 }
208
209 #[cfg(test)]
213 pub(crate) fn has_hyper_client(&self) -> bool {
214 self.hyper_client.is_some()
215 }
216
217 pub async fn supports_range(
218 &self,
219 url: &str,
220 cookie_header: Option<&str>,
221 headers: &[(String, String)],
222 ) -> Result<bool> {
223 let mut req = self.client.head(url);
228 if let Some(ch) = cookie_header {
229 req = req.header("Cookie", ch);
230 }
231 for (name, value) in headers {
232 req = req.header(name, value);
233 }
234 let resp = req.send().await.map_err(|e| {
235 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
236 message: format!("HEAD request failed: {}", e),
237 })
238 })?;
239
240 if let Some(accept_ranges) = resp.headers().get("Accept-Ranges")
241 && let Ok(value) = accept_ranges.to_str()
242 {
243 return Ok(value.to_lowercase().contains("bytes"));
244 }
245
246 let status = resp.status();
247 if status.as_u16() >= 400 {
248 return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
249 code: status.as_u16(),
250 }));
251 }
252
253 Ok(false)
254 }
255
256 pub async fn download_range(
257 &self,
258 url: &str,
259 offset: u64,
260 length: u64,
261 cookie_header: Option<&str>,
262 headers: &[(String, String)],
263 ) -> Result<bytes::Bytes> {
264 if length == 0 {
265 return Ok(bytes::Bytes::new());
266 }
267
268 if let Some(ref hyper) = self.hyper_client
274 && !url.starts_with("https://")
275 {
276 match hyper.download_range(url, offset, Some(length)).await {
277 Ok(data) => {
278 debug!(
279 "HyperDirectClient served range {}-{} ({} bytes) from {}",
280 offset,
281 offset + length,
282 data.len(),
283 url
284 );
285 return Ok(data);
286 }
287 Err(e) => {
288 warn!(
289 "HyperDirectClient failed for {} (offset={}, len={}), falling back to reqwest: {}",
290 url, offset, length, e
291 );
292 }
294 }
295 }
296
297 let range_header = format!("bytes={}-{}", offset, offset + length.saturating_sub(1));
298 debug!("HTTP Range request: {} ({})", range_header, url);
299
300 let mut req =
301 self.client
302 .get(url)
303 .header("Range", &range_header)
304 .timeout(Duration::from_secs(
305 constants::HTTP_DEFAULT_OVERALL_TIMEOUT_SECS,
306 ));
307 if let Some(ch) = cookie_header {
308 req = req.header("Cookie", ch);
309 }
310 for (name, value) in headers {
311 req = req.header(name, value);
312 }
313 let response = req.send().await.map_err(|e| {
314 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
315 message: format!("HTTP Range request failed: {}", e),
316 })
317 })?;
318
319 let status = response.status();
320 match status.as_u16() {
321 206 => {}
322 200 => {
323 warn!(
324 "Server returned 200 instead of 206 for Range request (offset={}, len={}), reading full body",
325 offset, length
326 );
327 }
328 416 => {
329 return Err(Aria2Error::Recoverable(
330 RecoverableError::TemporaryNetworkFailure {
331 message: format!(
332 "Range not satisfiable: bytes={}-{}",
333 offset,
334 offset + length.saturating_sub(1)
335 ),
336 },
337 ));
338 }
339 code if (400..500).contains(&code) => {
340 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
341 format!("HTTP client error {}: {}", code, url),
342 )));
343 }
344 code if code >= 500 => {
345 return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
346 code,
347 }));
348 }
349 _ => {}
350 }
351
352 let mut data = bytes::BytesMut::with_capacity(length as usize);
354 let mut stream = response.bytes_stream();
355
356 while let Some(chunk_result) = stream.next().await {
357 match chunk_result {
358 Ok(bytes) => data.extend_from_slice(&bytes),
359 Err(e) => {
360 return Err(Aria2Error::Recoverable(
361 RecoverableError::TemporaryNetworkFailure {
362 message: format!("Stream read error: {}", e),
363 },
364 ));
365 }
366 }
367 }
368
369 if data.is_empty() && length > 0 {
370 return Err(Aria2Error::Recoverable(
371 RecoverableError::TemporaryNetworkFailure {
372 message: format!(
373 "Empty response for range {}-{} from {}",
374 offset,
375 offset + length.saturating_sub(1),
376 url
377 ),
378 },
379 ));
380 }
381
382 Ok(data.freeze())
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[tokio::test]
392 async fn test_supports_range_no_server() {
393 let client = reqwest::Client::builder()
394 .connect_timeout(Duration::from_millis(100))
395 .build()
396 .unwrap();
397 let dl = HttpSegmentDownloader::new(&client, false);
400 let result = dl
401 .supports_range("http://127.0.0.1:1/nonexistent", None, &[])
402 .await;
403 assert!(result.is_err(), "should fail for unreachable host");
404 }
405
406 #[tokio::test]
407 async fn test_download_range_zero_length() {
408 let client = reqwest::Client::new();
409 let dl = HttpSegmentDownloader::new(&client, false);
410 let result = dl
411 .download_range("http://example.com", 0, 0, None, &[])
412 .await;
413 assert!(result.is_ok(), "zero-length range should return empty vec");
414 assert!(result.unwrap().is_empty());
415 }
416
417 #[tokio::test]
418 async fn test_downloader_creation() {
419 let client = reqwest::Client::new();
420 let dl = HttpSegmentDownloader::new(&client, false);
421 let _dl2 = HttpSegmentDownloader::new(&dl.client, false);
422 }
423
424 #[tokio::test]
425 async fn test_download_range_with_mock_http_416() {
426 use tokio::io::{AsyncReadExt, AsyncWriteExt};
427
428 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
429 let addr = listener.local_addr().unwrap();
430
431 let server_handle = tokio::spawn(async move {
432 let (mut stream, _) = listener.accept().await.unwrap();
433 let mut buf = [0u8; 2048];
434 let _n = stream.read(&mut buf).await.unwrap();
436 stream.write_all(b"HTTP/1.1 416 Range Not Satisfiable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").await.unwrap();
437 });
438
439 tokio::time::sleep(Duration::from_millis(50)).await;
440
441 let url = format!("http://{}", addr);
442 let client = reqwest::Client::builder()
443 .redirect(reqwest::redirect::Policy::none())
444 .timeout(Duration::from_secs(5))
445 .build()
446 .unwrap();
447 let dl = HttpSegmentDownloader::new(&client, false);
451
452 let result = dl.download_range(&url, 99999, 100, None, &[]).await;
453 assert!(result.is_err(), "416 should be an error");
454
455 let _ = tokio::time::timeout(Duration::from_secs(2), server_handle).await;
457 }
458
459 #[tokio::test]
460 async fn test_supports_range_header_parsing() {
461 let client = reqwest::Client::builder()
462 .timeout(std::time::Duration::from_secs(3))
463 .build()
464 .unwrap();
465 let dl = HttpSegmentDownloader::new(&client, false);
466
467 match dl
468 .supports_range(
469 "http://invalid-host-name-that-does-not-exist-12345.com/",
470 None,
471 &[],
472 )
473 .await
474 {
475 Ok(supports) => {
476 eprintln!(
477 "[WARN] Unexpected success for invalid host, supports={:?}",
478 supports
479 );
480 }
481 Err(e) => {
482 println!("Expected network error for invalid host: {:?}", e);
483 }
484 }
485 }
486
487 #[tokio::test]
488 async fn test_download_range_status_code_handling() {
489 let client = reqwest::Client::new();
490 let dl = HttpSegmentDownloader::new(&client, false);
491
492 let result_404 = dl
493 .download_range("http://httpbin.org/status/404", 0, 100, None, &[])
494 .await;
495 assert!(result_404.is_err(), "404 should be fatal error");
496 }
497
498 #[test]
502 fn test_http_segment_downloader_uses_hyper_by_default() {
503 let client = reqwest::Client::new();
504 let dl = HttpSegmentDownloader::new(&client, true);
505 assert!(
506 dl.has_hyper_client(),
507 "use_hyper=true must wire the HyperDirectClient"
508 );
509 }
510
511 #[test]
515 fn test_http_segment_downloader_no_hyper_with_proxy() {
516 let client = reqwest::Client::new();
517 let dl = HttpSegmentDownloader::new(&client, false);
518 assert!(
519 !dl.has_hyper_client(),
520 "use_hyper=false must NOT wire the HyperDirectClient (proxy path)"
521 );
522 }
523
524 #[test]
525 fn test_dynamic_segment_size_slow_start() {
526 let size = calculate_dynamic_segment_size(10_000_000, 4, 50000.0, 1);
528 assert!(size >= 1024 * 256, "Should be at least MIN_SEGMENT");
531 assert!(size <= 1024 * 1024 * 16, "Should be at most MAX_SEGMENT");
532
533 let size_slow = calculate_dynamic_segment_size(10_000_000, 4, 100.0, 5);
535 assert!(
536 size_slow >= 1024 * 256,
537 "Slow speed should use conservative default"
538 );
539 }
540
541 #[test]
542 fn test_dynamic_segment_size_fast_download() {
543 let size = calculate_dynamic_segment_size(100_000_000, 8, 1_048_576.0, 10);
545 assert_eq!(
548 size, 10_485_760,
549 "Fast download should produce large segments"
550 );
551
552 let size_very_fast = calculate_dynamic_segment_size(1_000_000_000, 16, 10_485_760.0, 30);
554 assert_eq!(
556 size_very_fast, 16_777_216,
557 "Very fast download should be capped at MAX_SEGMENT"
558 );
559 }
560
561 #[test]
562 fn test_connection_limiter_per_host() {
563 let limiter = ConnectionLimiter::new(10, 2); assert!(
567 limiter.try_acquire("example.com"),
568 "First acquisition should succeed"
569 );
570 assert!(
571 limiter.try_acquire("example.com"),
572 "Second acquisition should succeed"
573 );
574 assert!(
575 !limiter.try_acquire("example.com"),
576 "Third acquisition should fail (per-host limit)"
577 );
578
579 assert!(
581 limiter.try_acquire("other.com"),
582 "Different host should work"
583 );
584 assert!(
585 limiter.try_acquire("other.com"),
586 "Second slot for other host"
587 );
588 assert!(
589 !limiter.try_acquire("other.com"),
590 "Third slot for other host should fail"
591 );
592
593 limiter.release("example.com");
595 assert!(
596 limiter.try_acquire("example.com"),
597 "After release, should acquire again"
598 );
599
600 assert_eq!(
602 limiter.available_for("example.com"),
603 0,
604 "No slots available after acquiring limit"
605 );
606 limiter.release("example.com");
607 assert_eq!(
608 limiter.available_for("example.com"),
609 1,
610 "One slot available after release"
611 );
612 }
613
614 #[tokio::test]
618 async fn test_connection_limiter_concurrent_no_deadlock() {
619 use std::sync::Arc;
620 use std::sync::atomic::{AtomicUsize, Ordering};
621
622 let limiter = Arc::new(ConnectionLimiter::new(100, 10));
623 let success_count = Arc::new(AtomicUsize::new(0));
624
625 let mut handles = Vec::new();
626 for _ in 0..20 {
627 let l = limiter.clone();
628 let s = success_count.clone();
629 handles.push(tokio::spawn(async move {
630 if l.try_acquire("example.com") {
633 s.fetch_add(1, Ordering::Relaxed);
634 }
635 }));
636 }
637
638 for h in handles {
639 h.await.unwrap();
640 }
641
642 assert_eq!(
645 success_count.load(Ordering::Relaxed),
646 10,
647 "per-host limit must cap successful acquires"
648 );
649 assert_eq!(
650 limiter.host_count("example.com"),
651 10,
652 "host_count must reflect the 10 successful acquires"
653 );
654 assert_eq!(
655 limiter.global_count(),
656 10,
657 "global_count must equal the 10 successful acquires"
658 );
659
660 for _ in 0..5 {
662 limiter.release("example.com");
663 }
664 assert_eq!(
665 limiter.host_count("example.com"),
666 5,
667 "host_count must drop to 5 after 5 releases"
668 );
669 assert_eq!(
670 limiter.global_count(),
671 5,
672 "global_count must drop to 5 after 5 releases"
673 );
674 }
675
676 #[test]
677 fn test_source_scoring_slow_penalized() {
678 let fast_score = score_source(1_048_576.0, 0, 0);
680
681 let slow_score = score_source(1024.0, 0, 0);
683
684 let dead_score = score_source(0.0, 3, 0);
686
687 assert!(
689 slow_score > fast_score,
690 "Slow source should have worse score than fast source"
691 );
692
693 assert_eq!(dead_score, f64::MAX, "Dead source should have MAX score");
695
696 let failed_score = score_source(1_048_576.0, 2, 0);
698 assert!(
699 failed_score > fast_score,
700 "Failed source should have worse score than successful one"
701 );
702
703 let recent_score = score_source(1_048_576.0, 0, 10); let old_score = score_source(1_048_576.0, 0, 300); assert!(
710 old_score < recent_score,
711 "Old success should give better (lower) score due to larger age bonus"
712 );
713
714 let very_slow = score_source(1024.0, 0, 0);
716 assert!(
717 recent_score < very_slow,
718 "Even recent fast source beats slow source"
719 );
720 }
721}