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};
9pub use crate::selector::adaptive_uri_selector::{
11 score_source_raw as score_source, score_source_raw,
12};
13
14pub struct HttpSegmentDownloader {
15 client: reqwest::Client,
16}
17
18pub fn calculate_dynamic_segment_size(
21 total_remaining: u64,
22 num_connections: usize,
23 avg_speed_bps: f64,
24 elapsed_secs: u64,
25) -> u64 {
26 const MIN_SEGMENT: u64 = 1024 * 256; const MAX_SEGMENT: u64 = 1024 * 1024 * 16; if elapsed_secs < 2 || avg_speed_bps < 1024.0 {
30 return (total_remaining / num_connections.max(1) as u64).clamp(MIN_SEGMENT, MAX_SEGMENT);
32 }
33
34 let target_size = (avg_speed_bps * 10.0) as u64;
36 target_size.clamp(MIN_SEGMENT, MAX_SEGMENT)
37}
38
39pub struct ConnectionLimiter {
49 per_host: DashMap<String, AtomicUsize>,
50 global_count: AtomicUsize,
51 global_limit: usize,
52 per_host_limit: usize,
53}
54
55impl ConnectionLimiter {
56 pub fn new(global: usize, per_host: usize) -> Self {
58 Self {
59 per_host: DashMap::new(),
60 global_count: AtomicUsize::new(0),
61 global_limit: global,
62 per_host_limit: per_host,
63 }
64 }
65
66 pub fn try_acquire(&self, host: &str) -> bool {
84 loop {
88 let current = self.global_count.load(Ordering::Relaxed);
89 if current >= self.global_limit {
90 return false;
91 }
92 match self.global_count.compare_exchange_weak(
93 current,
94 current + 1,
95 Ordering::AcqRel,
96 Ordering::Relaxed,
97 ) {
98 Ok(_) => break,
99 Err(_) => continue,
100 }
101 }
102
103 let entry = self
111 .per_host
112 .entry(host.to_string())
113 .or_insert_with(|| AtomicUsize::new(0));
114 loop {
115 let current = entry.load(Ordering::Relaxed);
116 if current >= self.per_host_limit {
117 self.global_count.fetch_sub(1, Ordering::Relaxed);
119 return false;
120 }
121 match entry.compare_exchange_weak(
122 current,
123 current + 1,
124 Ordering::AcqRel,
125 Ordering::Relaxed,
126 ) {
127 Ok(_) => return true,
128 Err(_) => continue,
129 }
130 }
131 }
132
133 pub fn release(&self, host: &str) {
139 if let Some(entry) = self.per_host.get(host) {
140 let prev = entry.fetch_sub(1, Ordering::AcqRel);
141 debug_assert!(prev > 0, "release called more times than acquire for host");
142 }
143 let prev = self.global_count.fetch_sub(1, Ordering::AcqRel);
144 debug_assert!(prev > 0, "global release underflow");
145 }
146
147 pub fn host_count(&self, host: &str) -> usize {
149 self.per_host
150 .get(host)
151 .map(|e| e.load(Ordering::Relaxed))
152 .unwrap_or(0)
153 }
154
155 pub fn global_count(&self) -> usize {
157 self.global_count.load(Ordering::Relaxed)
158 }
159
160 pub fn global_limit(&self) -> usize {
162 self.global_limit
163 }
164
165 pub fn per_host_limit(&self) -> usize {
167 self.per_host_limit
168 }
169
170 pub fn available_for(&self, host: &str) -> usize {
174 let current = self.host_count(host);
175 if current >= self.per_host_limit {
176 return 0;
177 }
178 self.per_host_limit - current
179 }
180}
181
182impl HttpSegmentDownloader {
183 pub fn new(client: &reqwest::Client) -> Self {
185 Self {
186 client: client.clone(),
187 }
188 }
189
190 pub async fn supports_range(
191 &self,
192 url: &str,
193 cookie_header: Option<&str>,
194 headers: &[(String, String)],
195 ) -> Result<bool> {
196 let mut req = self.client.head(url);
197 if let Some(ch) = cookie_header {
198 req = req.header("Cookie", ch);
199 }
200 for (name, value) in headers {
201 req = req.header(name, value);
202 }
203 let resp = req.send().await.map_err(|e| {
204 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
205 message: format!("HEAD request failed: {}", e),
206 })
207 })?;
208
209 if let Some(accept_ranges) = resp.headers().get("Accept-Ranges")
210 && let Ok(value) = accept_ranges.to_str()
211 {
212 return Ok(value.to_lowercase().contains("bytes"));
213 }
214
215 let status = resp.status();
216 if status.as_u16() >= 400 {
217 return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
218 code: status.as_u16(),
219 }));
220 }
221
222 Ok(false)
223 }
224
225 pub async fn download_range(
226 &self,
227 url: &str,
228 offset: u64,
229 length: u64,
230 cookie_header: Option<&str>,
231 headers: &[(String, String)],
232 ) -> Result<bytes::Bytes> {
233 if length == 0 {
234 return Ok(bytes::Bytes::new());
235 }
236
237 let range_header = format!("bytes={}-{}", offset, offset + length.saturating_sub(1));
238 debug!("HTTP Range request: {} ({})", range_header, url);
239
240 let mut req =
241 self.client
242 .get(url)
243 .header("Range", &range_header)
244 .timeout(Duration::from_secs(
245 constants::HTTP_DEFAULT_OVERALL_TIMEOUT_SECS,
246 ));
247 if let Some(ch) = cookie_header {
248 req = req.header("Cookie", ch);
249 }
250 for (name, value) in headers {
251 req = req.header(name, value);
252 }
253 let response = req.send().await.map_err(|e| {
254 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
255 message: format!("HTTP Range request failed: {}", e),
256 })
257 })?;
258
259 let status = response.status();
260 match status.as_u16() {
261 206 => {}
262 200 => {
263 warn!(
264 "Server returned 200 instead of 206 for Range request (offset={}, len={}), reading full body",
265 offset, length
266 );
267 }
268 416 => {
269 return Err(Aria2Error::Recoverable(
270 RecoverableError::TemporaryNetworkFailure {
271 message: format!(
272 "Range not satisfiable: bytes={}-{}",
273 offset,
274 offset + length.saturating_sub(1)
275 ),
276 },
277 ));
278 }
279 code if (400..500).contains(&code) => {
280 return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
281 format!("HTTP client error {}: {}", code, url),
282 )));
283 }
284 code if code >= 500 => {
285 return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
286 code,
287 }));
288 }
289 _ => {}
290 }
291
292 let mut data = bytes::BytesMut::with_capacity(length as usize);
294 let mut stream = response.bytes_stream();
295
296 while let Some(chunk_result) = stream.next().await {
297 match chunk_result {
298 Ok(bytes) => data.extend_from_slice(&bytes),
299 Err(e) => {
300 return Err(Aria2Error::Recoverable(
301 RecoverableError::TemporaryNetworkFailure {
302 message: format!("Stream read error: {}", e),
303 },
304 ));
305 }
306 }
307 }
308
309 if data.is_empty() && length > 0 {
310 return Err(Aria2Error::Recoverable(
311 RecoverableError::TemporaryNetworkFailure {
312 message: format!(
313 "Empty response for range {}-{} from {}",
314 offset,
315 offset + length.saturating_sub(1),
316 url
317 ),
318 },
319 ));
320 }
321
322 Ok(data.freeze())
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[tokio::test]
332 async fn test_supports_range_no_server() {
333 let client = reqwest::Client::builder()
334 .connect_timeout(Duration::from_millis(100))
335 .build()
336 .unwrap();
337 let dl = HttpSegmentDownloader::new(&client);
338 let result = dl
339 .supports_range("http://127.0.0.1:1/nonexistent", None, &[])
340 .await;
341 assert!(result.is_err(), "should fail for unreachable host");
342 }
343
344 #[tokio::test]
345 async fn test_download_range_zero_length() {
346 let client = reqwest::Client::new();
347 let dl = HttpSegmentDownloader::new(&client);
348 let result = dl
349 .download_range("http://example.com", 0, 0, None, &[])
350 .await;
351 assert!(result.is_ok(), "zero-length range should return empty vec");
352 assert!(result.unwrap().is_empty());
353 }
354
355 #[tokio::test]
356 async fn test_downloader_creation() {
357 let client = reqwest::Client::new();
358 let dl = HttpSegmentDownloader::new(&client);
359 let _dl2 = HttpSegmentDownloader::new(&dl.client);
360 }
361
362 #[tokio::test]
363 async fn test_download_range_with_mock_http_416() {
364 use tokio::io::{AsyncReadExt, AsyncWriteExt};
365
366 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
367 let addr = listener.local_addr().unwrap();
368
369 let server_handle = tokio::spawn(async move {
370 let (mut stream, _) = listener.accept().await.unwrap();
371 let mut buf = [0u8; 2048];
372 let _n = stream.read(&mut buf).await.unwrap();
374 stream.write_all(b"HTTP/1.1 416 Range Not Satisfiable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").await.unwrap();
375 });
376
377 tokio::time::sleep(Duration::from_millis(50)).await;
378
379 let url = format!("http://{}", addr);
380 let client = reqwest::Client::builder()
381 .redirect(reqwest::redirect::Policy::none())
382 .timeout(Duration::from_secs(5))
383 .build()
384 .unwrap();
385 let dl = HttpSegmentDownloader::new(&client);
386
387 let result = dl.download_range(&url, 99999, 100, None, &[]).await;
388 assert!(result.is_err(), "416 should be an error");
389
390 let _ = tokio::time::timeout(Duration::from_secs(2), server_handle).await;
392 }
393
394 #[tokio::test]
395 async fn test_supports_range_header_parsing() {
396 let client = reqwest::Client::builder()
397 .timeout(std::time::Duration::from_secs(3))
398 .build()
399 .unwrap();
400 let dl = HttpSegmentDownloader::new(&client);
401
402 match dl
403 .supports_range(
404 "http://invalid-host-name-that-does-not-exist-12345.com/",
405 None,
406 &[],
407 )
408 .await
409 {
410 Ok(supports) => {
411 eprintln!(
412 "[WARN] Unexpected success for invalid host, supports={:?}",
413 supports
414 );
415 }
416 Err(e) => {
417 println!("Expected network error for invalid host: {:?}", e);
418 }
419 }
420 }
421
422 #[tokio::test]
423 async fn test_download_range_status_code_handling() {
424 let client = reqwest::Client::new();
425 let dl = HttpSegmentDownloader::new(&client);
426
427 let result_404 = dl
428 .download_range("http://httpbin.org/status/404", 0, 100, None, &[])
429 .await;
430 assert!(result_404.is_err(), "404 should be fatal error");
431 }
432
433 #[test]
434 fn test_dynamic_segment_size_slow_start() {
435 let size = calculate_dynamic_segment_size(10_000_000, 4, 50000.0, 1);
437 assert!(size >= 1024 * 256, "Should be at least MIN_SEGMENT");
440 assert!(size <= 1024 * 1024 * 16, "Should be at most MAX_SEGMENT");
441
442 let size_slow = calculate_dynamic_segment_size(10_000_000, 4, 100.0, 5);
444 assert!(
445 size_slow >= 1024 * 256,
446 "Slow speed should use conservative default"
447 );
448 }
449
450 #[test]
451 fn test_dynamic_segment_size_fast_download() {
452 let size = calculate_dynamic_segment_size(100_000_000, 8, 1_048_576.0, 10);
454 assert_eq!(
457 size, 10_485_760,
458 "Fast download should produce large segments"
459 );
460
461 let size_very_fast = calculate_dynamic_segment_size(1_000_000_000, 16, 10_485_760.0, 30);
463 assert_eq!(
465 size_very_fast, 16_777_216,
466 "Very fast download should be capped at MAX_SEGMENT"
467 );
468 }
469
470 #[test]
471 fn test_connection_limiter_per_host() {
472 let limiter = ConnectionLimiter::new(10, 2); assert!(
476 limiter.try_acquire("example.com"),
477 "First acquisition should succeed"
478 );
479 assert!(
480 limiter.try_acquire("example.com"),
481 "Second acquisition should succeed"
482 );
483 assert!(
484 !limiter.try_acquire("example.com"),
485 "Third acquisition should fail (per-host limit)"
486 );
487
488 assert!(
490 limiter.try_acquire("other.com"),
491 "Different host should work"
492 );
493 assert!(
494 limiter.try_acquire("other.com"),
495 "Second slot for other host"
496 );
497 assert!(
498 !limiter.try_acquire("other.com"),
499 "Third slot for other host should fail"
500 );
501
502 limiter.release("example.com");
504 assert!(
505 limiter.try_acquire("example.com"),
506 "After release, should acquire again"
507 );
508
509 assert_eq!(
511 limiter.available_for("example.com"),
512 0,
513 "No slots available after acquiring limit"
514 );
515 limiter.release("example.com");
516 assert_eq!(
517 limiter.available_for("example.com"),
518 1,
519 "One slot available after release"
520 );
521 }
522
523 #[tokio::test]
527 async fn test_connection_limiter_concurrent_no_deadlock() {
528 use std::sync::Arc;
529 use std::sync::atomic::{AtomicUsize, Ordering};
530
531 let limiter = Arc::new(ConnectionLimiter::new(100, 10));
532 let success_count = Arc::new(AtomicUsize::new(0));
533
534 let mut handles = Vec::new();
535 for _ in 0..20 {
536 let l = limiter.clone();
537 let s = success_count.clone();
538 handles.push(tokio::spawn(async move {
539 if l.try_acquire("example.com") {
542 s.fetch_add(1, Ordering::Relaxed);
543 }
544 }));
545 }
546
547 for h in handles {
548 h.await.unwrap();
549 }
550
551 assert_eq!(
554 success_count.load(Ordering::Relaxed),
555 10,
556 "per-host limit must cap successful acquires"
557 );
558 assert_eq!(
559 limiter.host_count("example.com"),
560 10,
561 "host_count must reflect the 10 successful acquires"
562 );
563 assert_eq!(
564 limiter.global_count(),
565 10,
566 "global_count must equal the 10 successful acquires"
567 );
568
569 for _ in 0..5 {
571 limiter.release("example.com");
572 }
573 assert_eq!(
574 limiter.host_count("example.com"),
575 5,
576 "host_count must drop to 5 after 5 releases"
577 );
578 assert_eq!(
579 limiter.global_count(),
580 5,
581 "global_count must drop to 5 after 5 releases"
582 );
583 }
584
585 #[test]
586 fn test_source_scoring_slow_penalized() {
587 let fast_score = score_source(1_048_576.0, 0, 0);
589
590 let slow_score = score_source(1024.0, 0, 0);
592
593 let dead_score = score_source(0.0, 3, 0);
595
596 assert!(
598 slow_score > fast_score,
599 "Slow source should have worse score than fast source"
600 );
601
602 assert_eq!(dead_score, f64::MAX, "Dead source should have MAX score");
604
605 let failed_score = score_source(1_048_576.0, 2, 0);
607 assert!(
608 failed_score > fast_score,
609 "Failed source should have worse score than successful one"
610 );
611
612 let recent_score = score_source(1_048_576.0, 0, 10); let old_score = score_source(1_048_576.0, 0, 300); assert!(
619 old_score < recent_score,
620 "Old success should give better (lower) score due to larger age bonus"
621 );
622
623 let very_slow = score_source(1024.0, 0, 0);
625 assert!(
626 recent_score < very_slow,
627 "Even recent fast source beats slow source"
628 );
629 }
630}