1use super::{
2 progress::{
3 ArchiveDownloadProgress, DownloadProgress, DownloadRequestLimiter, SharedProgress,
4 SharedProgressWriter,
5 },
6 session::DownloadSession,
7 RETRY_BACKOFF_SECS,
8};
9use eyre::Result;
10use reqwest::{blocking::Client as BlockingClient, header::RANGE, StatusCode};
11use reth_cli_util::cancellation::CancellationToken;
12use reth_fs_util as fs;
13use std::{
14 any::Any,
15 collections::VecDeque,
16 fs::OpenOptions,
17 io::{self, BufWriter, Read, Write},
18 os::unix::fs::FileExt,
19 path::{Path, PathBuf},
20 sync::{
21 atomic::{AtomicBool, AtomicU64, Ordering},
22 Arc, Mutex,
23 },
24 time::Duration,
25};
26use tracing::{info, warn};
27use url::Url;
28
29const SEGMENT_RETRY_ATTEMPTS: u32 = 3;
31
32const SEGMENTED_DOWNLOAD_MIN_FILE_SIZE: u64 = 128 * 1024 * 1024;
34
35const SEGMENTED_DOWNLOAD_SMALL_PIECE_SIZE: u64 = 32 * 1024 * 1024;
38const SEGMENTED_DOWNLOAD_LARGE_PIECE_SIZE: u64 = 64 * 1024 * 1024;
39
40const SEGMENTED_DOWNLOAD_MAX_BACKOFF_SECS: u64 = 30;
42
43const SEGMENTED_DOWNLOAD_REQUEST_TIMEOUT_SECS: u64 = 120;
46
47#[derive(Debug, Clone)]
49struct DownloadPaths {
50 file_name: String,
52 final_path: PathBuf,
54 part_path: PathBuf,
56}
57
58impl DownloadPaths {
59 fn from_url(url: &str, target_dir: &Path) -> Self {
61 let file_name = Url::parse(url)
62 .ok()
63 .and_then(|u| u.path_segments()?.next_back().map(|s| s.to_string()))
64 .unwrap_or_else(|| "snapshot.tar".to_string());
65
66 Self {
67 final_path: target_dir.join(&file_name),
68 part_path: target_dir.join(format!("{file_name}.part")),
69 file_name,
70 }
71 }
72
73 fn file_name(&self) -> &str {
75 &self.file_name
76 }
77
78 fn final_path(&self) -> &Path {
80 &self.final_path
81 }
82
83 fn part_path(&self) -> &Path {
85 &self.part_path
86 }
87
88 fn finalize(&self) -> Result<()> {
90 fs::rename(&self.part_path, &self.final_path)?;
91 Ok(())
92 }
93
94 fn cleanup_partial(&self) {
96 let _ = fs::remove_file(&self.part_path);
97 }
98
99 fn cleanup_all(&self) {
101 let _ = fs::remove_file(&self.final_path);
102 self.cleanup_partial();
103 }
104}
105
106pub(crate) struct ArchiveFetcher {
108 url: String,
110 paths: DownloadPaths,
112 session: DownloadSession,
114}
115
116impl ArchiveFetcher {
117 pub(crate) fn new(url: impl Into<String>, target_dir: &Path, session: DownloadSession) -> Self {
119 let url = url.into();
120 let paths = DownloadPaths::from_url(&url, target_dir);
121 Self { url, paths, session }
122 }
123
124 pub(crate) fn download(
126 &self,
127 download_progress: Option<&mut ArchiveDownloadProgress<'_>>,
128 ) -> Result<DownloadedArchive> {
129 if let Some(path) = archive_file_url_path(&self.url)? {
130 let size = fs::metadata(&path)?.len();
131 if !self.quiet() {
132 info!(target: "reth::cli",
133 file = %path.display(),
134 size = %DownloadProgress::format_size(size),
135 "Using local archive"
136 );
137 }
138 return Ok(DownloadedArchive { path, size })
139 }
140
141 let Some(request_limiter) = self.session.request_limiter() else {
142 return self.download_sequential(super::MAX_DOWNLOAD_RETRIES, download_progress)
143 };
144
145 let client = BlockingClient::builder().connect_timeout(Duration::from_secs(30)).build()?;
146 let probe = self.probe(&client)?;
147
148 match choose_fetch_strategy(probe, request_limiter.max_concurrency()) {
149 FetchStrategy::Sequential(reason) => {
150 self.log_sequential_fallback(reason, probe.total_size);
151 self.download_sequential(super::MAX_DOWNLOAD_RETRIES, download_progress)
152 }
153 FetchStrategy::Segmented(plan) => {
154 self.download_segmented(probe.total_size, plan, download_progress)
155 }
156 }
157 }
158
159 pub(crate) fn cleanup_downloaded_files(&self) {
161 self.paths.cleanup_all();
162 }
163
164 fn probe(&self, client: &BlockingClient) -> Result<RemoteArchiveProbe> {
166 let mut attempt = 1;
167 let probe = loop {
168 let probe = client
169 .get(&self.url)
170 .header(RANGE, "bytes=0-0")
171 .send()
172 .and_then(|response| response.error_for_status());
173 if probe.is_ok() || attempt == SEGMENT_RETRY_ATTEMPTS {
175 break probe
176 }
177 std::thread::sleep(self.session.retry_delay(piece_retry_backoff(attempt, false)));
178 attempt += 1;
179 };
180
181 let (supports_ranges, total_size) = match probe {
182 Ok(response) if response.status() == StatusCode::PARTIAL_CONTENT => {
183 let total = response
184 .headers()
185 .get("Content-Range")
186 .and_then(|value| value.to_str().ok())
187 .and_then(|value| value.split('/').next_back())
188 .and_then(|value| value.parse::<u64>().ok());
189 (true, total)
190 }
191 _ => {
192 let response = client.head(&self.url).send()?.error_for_status()?;
193 (false, response.content_length())
194 }
195 };
196
197 Ok(RemoteArchiveProbe {
198 total_size: total_size.ok_or_else(|| eyre::eyre!("Server did not return file size"))?,
199 supports_ranges,
200 })
201 }
202
203 fn download_sequential(
205 &self,
206 max_download_retries: u32,
207 mut download_progress: Option<&mut ArchiveDownloadProgress<'_>>,
208 ) -> Result<DownloadedArchive> {
209 let quiet = self.quiet();
210
211 if !quiet {
212 info!(target: "reth::cli", file = %self.paths.file_name(), "Connecting to download server");
213 }
214
215 let client = BlockingClient::builder().timeout(Duration::from_secs(30)).build()?;
216 let mut total_size: Option<u64> = None;
217 let mut last_error: Option<eyre::Error> = None;
218
219 for attempt in 1..=max_download_retries {
220 let existing_size =
221 fs::metadata(self.paths.part_path()).map(|meta| meta.len()).unwrap_or(0);
222
223 if let Some(total) = total_size &&
224 existing_size >= total
225 {
226 return self.finalize_download(total)
227 }
228
229 if attempt > 1 {
230 info!(target: "reth::cli",
231 file = %self.paths.file_name(),
232 "Retry attempt {}/{} - resuming from {} bytes",
233 attempt, max_download_retries, existing_size
234 );
235 }
236
237 let mut request = client.get(&self.url);
238 if existing_size > 0 {
239 request = request.header(RANGE, format!("bytes={existing_size}-"));
240 if !quiet && attempt == 1 {
241 info!(target: "reth::cli", file = %self.paths.file_name(), "Resuming from {} bytes", existing_size);
242 }
243 }
244
245 let _request_permit = self
246 .session
247 .request_limiter()
248 .map(|limiter| {
249 limiter.acquire(self.session.progress(), self.session.cancel_token())
250 })
251 .transpose()?;
252
253 let response = match request.send().and_then(|response| response.error_for_status()) {
254 Ok(response) => response,
255 Err(error) => {
256 last_error = Some(error.into());
257 if attempt < max_download_retries {
258 info!(target: "reth::cli",
259 file = %self.paths.file_name(),
260 retry_delay = ?self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
261 "Download failed, retrying"
262 );
263 std::thread::sleep(
264 self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
265 );
266 }
267 continue;
268 }
269 };
270
271 let is_partial = response.status() == StatusCode::PARTIAL_CONTENT;
272 let size = if is_partial {
273 response
274 .headers()
275 .get("Content-Range")
276 .and_then(|value| value.to_str().ok())
277 .and_then(|value| value.split('/').next_back())
278 .and_then(|value| value.parse().ok())
279 } else {
280 response.content_length()
281 };
282
283 if total_size.is_none() {
284 total_size = size;
285 if !quiet && let Some(size) = size {
286 info!(target: "reth::cli",
287 file = %self.paths.file_name(),
288 size = %DownloadProgress::format_size(size),
289 "Downloading"
290 );
291 }
292 }
293
294 let current_total = total_size.ok_or_else(|| {
295 eyre::eyre!("Server did not provide Content-Length or Content-Range header")
296 })?;
297
298 let file = if is_partial && existing_size > 0 {
299 OpenOptions::new()
300 .append(true)
301 .open(self.paths.part_path())
302 .map_err(|error| fs::FsPathError::open(error, self.paths.part_path()))?
303 } else {
304 fs::create_file(self.paths.part_path())?
305 };
306
307 let start_offset = if is_partial { existing_size } else { 0 };
308 let mut reader = response;
309
310 let copy_result;
311 let flush_result;
312
313 if let Some(progress) = self.session.progress() {
314 let mut on_written = |bytes| {
315 if let Some(download_progress) = download_progress.as_deref_mut() {
316 download_progress.record_downloaded(bytes);
317 }
318 };
319 let mut writer = SharedProgressWriter {
320 inner: BufWriter::new(file),
321 progress: Arc::clone(progress),
322 on_written: Some(&mut on_written),
323 };
324 copy_result = io::copy(&mut reader, &mut writer);
325 flush_result = writer.inner.flush();
326 } else {
327 let mut progress = DownloadProgress::new(current_total);
328 progress.downloaded = start_offset;
329 let mut writer = ProgressWriter {
330 inner: BufWriter::new(file),
331 progress,
332 cancel_token: self.session.cancel_token().clone(),
333 };
334 copy_result = io::copy(&mut reader, &mut writer);
335 flush_result = writer.inner.flush();
336 println!();
337 }
338
339 if let Err(error) = copy_result.and(flush_result) {
340 last_error = Some(error.into());
341 if attempt < max_download_retries {
342 info!(target: "reth::cli",
343 file = %self.paths.file_name(),
344 retry_delay = ?self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
345 "Download interrupted, retrying"
346 );
347 std::thread::sleep(
348 self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
349 );
350 }
351 continue;
352 }
353
354 return self.finalize_download(current_total)
355 }
356
357 Err(last_error.unwrap_or_else(|| {
358 eyre::eyre!("Download failed after {} attempts", max_download_retries)
359 }))
360 }
361
362 fn download_segmented(
364 &self,
365 total_size: u64,
366 plan: SegmentedDownloadPlan,
367 download_progress: Option<&mut ArchiveDownloadProgress<'_>>,
368 ) -> Result<DownloadedArchive> {
369 let request_limiter = self.session.require_request_limiter()?;
370 info!(target: "reth::cli",
371 total_size = %DownloadProgress::format_size(total_size),
372 piece_size = %DownloadProgress::format_size(plan.piece_size),
373 pieces = plan.piece_count,
374 workers = plan.worker_count,
375 max_concurrent_requests = request_limiter.max_concurrency(),
376 "Starting queued segmented download"
377 );
378
379 SegmentedDownload::new(
380 self.url.clone(),
381 self.paths.clone(),
382 total_size,
383 plan,
384 self.session.clone(),
385 download_progress,
386 )
387 .run()
388 }
389
390 fn log_sequential_fallback(&self, reason: SequentialDownloadFallback, total_size: u64) {
392 match reason {
393 SequentialDownloadFallback::NoRangeSupport => {
394 info!(target: "reth::cli",
395 file = %self.paths.file_name(),
396 "Server does not support Range requests, falling back to sequential download"
397 );
398 }
399 SequentialDownloadFallback::EmptyFile => {
400 info!(target: "reth::cli",
401 file = %self.paths.file_name(),
402 "Remote archive is empty, falling back to sequential download"
403 );
404 }
405 SequentialDownloadFallback::TooSmall => {
406 let _ = total_size;
407 }
408 }
409 }
410
411 fn finalize_download(&self, size: u64) -> Result<DownloadedArchive> {
413 self.paths.finalize()?;
414 if !self.quiet() {
415 info!(target: "reth::cli", file = %self.paths.file_name(), "Download complete");
416 }
417 Ok(DownloadedArchive { path: self.paths.final_path().to_path_buf(), size })
418 }
419
420 fn quiet(&self) -> bool {
422 self.session.progress().is_some()
423 }
424}
425
426fn archive_file_url_path(url: &str) -> Result<Option<PathBuf>> {
428 let Ok(parsed) = Url::parse(url) else { return Ok(None) };
429 if parsed.scheme() != "file" {
430 return Ok(None)
431 }
432
433 parsed
434 .to_file_path()
435 .map(Some)
436 .map_err(|_| eyre::eyre!("Invalid file:// archive URL path: {url}"))
437}
438
439#[derive(Debug, Clone)]
441pub(crate) struct DownloadedArchive {
442 pub(crate) path: PathBuf,
444 pub(crate) size: u64,
446}
447
448#[derive(Debug, Clone, Copy)]
450struct RemoteArchiveProbe {
451 total_size: u64,
453 supports_ranges: bool,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459enum SequentialDownloadFallback {
460 NoRangeSupport,
462 EmptyFile,
464 TooSmall,
466}
467
468#[derive(Debug)]
470enum FetchStrategy {
471 Sequential(SequentialDownloadFallback),
473 Segmented(SegmentedDownloadPlan),
475}
476
477fn choose_fetch_strategy(probe: RemoteArchiveProbe, max_workers: usize) -> FetchStrategy {
479 if !probe.supports_ranges {
480 return FetchStrategy::Sequential(SequentialDownloadFallback::NoRangeSupport)
481 }
482
483 if probe.total_size == 0 {
484 return FetchStrategy::Sequential(SequentialDownloadFallback::EmptyFile)
485 }
486
487 plan_segmented_download(probe.total_size, max_workers)
488 .map(FetchStrategy::Segmented)
489 .unwrap_or(FetchStrategy::Sequential(SequentialDownloadFallback::TooSmall))
490}
491
492struct ProgressWriter<W> {
495 inner: W,
497 progress: DownloadProgress,
499 cancel_token: CancellationToken,
501}
502
503impl<W: Write> Write for ProgressWriter<W> {
504 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
506 if self.cancel_token.is_cancelled() {
507 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
508 }
509 let n = self.inner.write(buf)?;
510 let _ = self.progress.update(n as u64);
511 Ok(n)
512 }
513
514 fn flush(&mut self) -> io::Result<()> {
516 self.inner.flush()
517 }
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
522struct DownloadPiece {
523 start: u64,
525 end: u64,
527}
528
529#[derive(Debug)]
531struct SegmentedDownloadPlan {
532 piece_size: u64,
534 piece_count: usize,
536 worker_count: usize,
538 pieces: VecDeque<DownloadPiece>,
540}
541
542struct SegmentedDownload {
544 url: String,
546 paths: DownloadPaths,
548 total_size: u64,
550 plan: SegmentedDownloadPlan,
552 session: DownloadSession,
554}
555
556#[derive(Clone, Copy)]
558struct SegmentedWorkerContext<'a> {
559 url: &'a str,
561 part_path: &'a Path,
563 shared: Option<&'a Arc<SharedProgress>>,
565 request_limiter: &'a DownloadRequestLimiter,
567 cancel_token: &'a CancellationToken,
569 session: &'a DownloadSession,
571}
572
573impl SegmentedDownload {
574 fn new(
576 url: String,
577 paths: DownloadPaths,
578 total_size: u64,
579 plan: SegmentedDownloadPlan,
580 session: DownloadSession,
581 _download_progress: Option<&mut ArchiveDownloadProgress<'_>>,
582 ) -> Self {
583 Self { url, paths, total_size, plan, session }
584 }
585
586 fn run(self) -> Result<DownloadedArchive> {
588 let Self { url, paths, total_size, plan, session } = self;
589 {
590 let file = fs::create_file(paths.part_path())?;
591 file.set_len(total_size)?;
592 }
593
594 let worker_count = plan.worker_count;
595 let state = Arc::new(SegmentedDownloadState::new(plan.pieces));
596 let terminal_failure = Arc::new(TerminalFailure::default());
597 let piece_progress_bytes = Arc::new(AtomicU64::new(0));
598 let worker_client = BlockingClient::builder()
599 .connect_timeout(Duration::from_secs(30))
600 .timeout(Duration::from_secs(SEGMENTED_DOWNLOAD_REQUEST_TIMEOUT_SECS))
601 .build()?;
602 let request_limiter = Arc::clone(session.require_request_limiter()?);
603 let shared = session.progress();
604 let cancel_token = session.cancel_token();
605 let url = url.as_str();
606 let worker_context = SegmentedWorkerContext {
607 url,
608 part_path: paths.part_path(),
609 session: &session,
610 shared,
611 request_limiter: request_limiter.as_ref(),
612 cancel_token,
613 };
614
615 std::thread::scope(|scope| {
616 let mut handles = Vec::with_capacity(worker_count);
617
618 for _ in 0..worker_count {
619 let state = Arc::clone(&state);
620 let terminal_failure = Arc::clone(&terminal_failure);
621 let piece_progress_bytes = Arc::clone(&piece_progress_bytes);
622 let client = worker_client.clone();
623
624 handles.push(scope.spawn(move || {
625 Self::worker_loop(
626 &client,
627 worker_context,
628 state,
629 terminal_failure,
630 piece_progress_bytes,
631 );
632 }));
633 }
634
635 for handle in handles {
636 if let Err(payload) = handle.join() {
637 state.note_terminal_failure();
638 terminal_failure.record(eyre::eyre!(
639 "Segmented download worker panicked: {}",
640 panic_payload_message(payload)
641 ));
642 }
643 }
644 });
645
646 if let Some(error) = terminal_failure.take() {
647 if let Some(shared) = shared {
648 shared.sub_active_download_bytes(piece_progress_bytes.load(Ordering::Relaxed));
649 }
650 paths.cleanup_partial();
651 return Err(error.wrap_err("Parallel download failed"))
652 }
653
654 if let Some(shared) = shared {
655 shared.sub_active_download_bytes(piece_progress_bytes.load(Ordering::Relaxed));
656 shared.record_archive_download_complete(total_size);
657 }
658
659 paths.finalize()?;
660 info!(target: "reth::cli", file = %paths.file_name(), "Download complete");
661 Ok(DownloadedArchive { path: paths.final_path().to_path_buf(), size: total_size })
662 }
663
664 fn worker_loop(
666 client: &BlockingClient,
667 context: SegmentedWorkerContext<'_>,
668 state: Arc<SegmentedDownloadState>,
669 terminal_failure: Arc<TerminalFailure>,
670 piece_progress_bytes: Arc<AtomicU64>,
671 ) {
672 let file = match OpenOptions::new().write(true).open(context.part_path) {
673 Ok(file) => file,
674 Err(error) => {
675 state.note_terminal_failure();
676 terminal_failure.record(error.into());
677 return;
678 }
679 };
680
681 while let Some(piece) = state.next_piece(context.cancel_token) {
682 match Self::download_piece_with_retries(
683 client,
684 context.url,
685 &file,
686 piece,
687 context.shared,
688 &piece_progress_bytes,
689 context.request_limiter,
690 context.cancel_token,
691 context.session,
692 ) {
693 Ok(()) => {}
694 Err(PieceAttemptFailure::Retryable { error, .. }) if state.requeue(piece) => {
695 warn!(target: "reth::cli",
696 start = piece.start,
697 end = piece.end,
698 %error,
699 "Piece download failed, requeued"
700 );
701 }
702 Err(
703 PieceAttemptFailure::Retryable { error, .. } |
704 PieceAttemptFailure::Terminal(error),
705 ) => {
706 state.note_terminal_failure();
707 terminal_failure.record(error);
708 return;
709 }
710 }
711 }
712 }
713
714 #[expect(clippy::too_many_arguments)]
719 fn download_piece_with_retries(
720 client: &BlockingClient,
721 url: &str,
722 file: &std::fs::File,
723 piece: DownloadPiece,
724 shared: Option<&Arc<SharedProgress>>,
725 piece_progress_bytes: &AtomicU64,
726 request_limiter: &DownloadRequestLimiter,
727 cancel_token: &CancellationToken,
728 session: &DownloadSession,
729 ) -> std::result::Result<(), PieceAttemptFailure> {
730 for attempt in 1..=SEGMENT_RETRY_ATTEMPTS {
731 if cancel_token.is_cancelled() {
732 return Err(PieceAttemptFailure::Terminal(eyre::eyre!("Download cancelled")))
733 }
734
735 let _request_permit = request_limiter
736 .acquire(shared, cancel_token)
737 .map_err(PieceAttemptFailure::Terminal)?;
738 match Self::download_piece_once(
739 client,
740 url,
741 file,
742 piece,
743 shared,
744 piece_progress_bytes,
745 cancel_token,
746 ) {
747 Ok(()) => return Ok(()),
748 Err(PieceAttemptFailure::Retryable { error: _, throttled })
749 if attempt < SEGMENT_RETRY_ATTEMPTS =>
750 {
751 std::thread::sleep(
752 session.retry_delay(piece_retry_backoff(attempt, throttled)),
753 );
754 }
755 Err(failure) => return Err(failure),
756 }
757 }
758
759 Err(PieceAttemptFailure::Terminal(eyre::eyre!(
760 "Piece download failed after {SEGMENT_RETRY_ATTEMPTS} attempts"
761 )))
762 }
763
764 fn download_piece_once(
766 client: &BlockingClient,
767 url: &str,
768 file: &std::fs::File,
769 piece: DownloadPiece,
770 shared: Option<&Arc<SharedProgress>>,
771 piece_progress_bytes: &AtomicU64,
772 cancel_token: &CancellationToken,
773 ) -> std::result::Result<(), PieceAttemptFailure> {
774 let expected_len = piece.end - piece.start + 1;
775
776 let response = match client
777 .get(url)
778 .header(RANGE, format!("bytes={}-{}", piece.start, piece.end))
779 .send()
780 {
781 Ok(response) if response.status() == StatusCode::PARTIAL_CONTENT => response,
782 Ok(response) if should_retry_piece_status(response.status()) => {
783 return Err(PieceAttemptFailure::Retryable {
784 error: eyre::eyre!(
785 "Server returned {} for piece {}-{}",
786 response.status(),
787 piece.start,
788 piece.end
789 ),
790 throttled: is_throttle_piece_status(response.status()),
791 });
792 }
793 Ok(response) => {
794 return Err(PieceAttemptFailure::Terminal(eyre::eyre!(
795 "Server returned {} instead of 206 for Range request",
796 response.status()
797 )));
798 }
799 Err(error) => {
800 return Err(PieceAttemptFailure::Retryable {
801 throttled: is_throttle_piece_error(&error),
802 error: error.into(),
803 });
804 }
805 };
806
807 let mut buf = [0u8; 64 * 1024];
808 let mut reader = response.take(expected_len);
809 let mut offset = piece.start;
810
811 loop {
812 if cancel_token.is_cancelled() {
813 return Err(PieceAttemptFailure::Terminal(eyre::eyre!("Download cancelled")));
814 }
815
816 match reader.read(&mut buf) {
817 Ok(0) => break,
818 Ok(n) => {
819 file.write_all_at(&buf[..n], offset)
820 .map_err(|error| PieceAttemptFailure::Terminal(error.into()))?;
821 offset += n as u64;
822 if let Some(progress) = shared {
823 progress.record_session_fetched_bytes(n as u64);
824 }
825 }
826 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
827 Err(error) => {
828 return Err(PieceAttemptFailure::Retryable {
829 throttled: error.kind() == io::ErrorKind::TimedOut,
830 error: error.into(),
831 });
832 }
833 }
834 }
835
836 let downloaded_len = offset - piece.start;
837 if downloaded_len == expected_len {
838 if let Some(progress) = shared {
839 progress.add_active_download_bytes(expected_len);
840 }
841 piece_progress_bytes.fetch_add(expected_len, Ordering::Relaxed);
842 return Ok(())
843 }
844
845 Err(PieceAttemptFailure::Retryable {
846 error: eyre::eyre!(
847 "Piece {}-{} ended early: expected {} bytes, downloaded {}",
848 piece.start,
849 piece.end,
850 expected_len,
851 downloaded_len
852 ),
853 throttled: false,
854 })
855 }
856}
857
858struct SegmentedDownloadState {
862 pieces: Mutex<VecDeque<DownloadPiece>>,
864 failed: AtomicBool,
866 requeues_left: AtomicU64,
868}
869
870impl SegmentedDownloadState {
871 fn new(pieces: VecDeque<DownloadPiece>) -> Self {
873 let requeues_left = AtomicU64::new(pieces.len() as u64);
874 Self { pieces: Mutex::new(pieces), failed: AtomicBool::new(false), requeues_left }
875 }
876
877 fn requeue(&self, piece: DownloadPiece) -> bool {
879 if self
880 .requeues_left
881 .try_update(Ordering::Relaxed, Ordering::Relaxed, |left| left.checked_sub(1))
882 .is_err()
883 {
884 return false
885 }
886 self.pieces.lock().unwrap().push_back(piece);
887 true
888 }
889
890 fn next_piece(&self, cancel_token: &CancellationToken) -> Option<DownloadPiece> {
892 if cancel_token.is_cancelled() || self.failed.load(Ordering::Relaxed) {
893 return None;
894 }
895
896 self.pieces.lock().unwrap().pop_front()
897 }
898
899 fn note_terminal_failure(&self) {
901 self.failed.store(true, Ordering::Relaxed);
902 }
903}
904
905#[derive(Default)]
907struct TerminalFailure {
908 error: Mutex<Option<eyre::Error>>,
910}
911
912impl TerminalFailure {
913 fn record(&self, error: eyre::Error) {
915 let mut slot = self.error.lock().unwrap();
916 if slot.is_none() {
917 *slot = Some(error);
918 }
919 }
920
921 fn take(&self) -> Option<eyre::Error> {
923 self.error.lock().unwrap().take()
924 }
925}
926
927fn build_download_pieces(total_size: u64, piece_size: u64) -> VecDeque<DownloadPiece> {
929 let mut pieces = VecDeque::new();
930 let mut start = 0;
931
932 while start < total_size {
933 let end = (start + piece_size).min(total_size) - 1;
934 pieces.push_back(DownloadPiece { start, end });
935 start = end + 1;
936 }
937
938 pieces
939}
940
941fn segmented_piece_size(total_size: u64) -> u64 {
946 if total_size < 2 * 1024 * 1024 * 1024 {
947 SEGMENTED_DOWNLOAD_SMALL_PIECE_SIZE
948 } else {
949 SEGMENTED_DOWNLOAD_LARGE_PIECE_SIZE
950 }
951}
952
953fn plan_segmented_download(total_size: u64, max_workers: usize) -> Option<SegmentedDownloadPlan> {
958 if max_workers == 0 || total_size < SEGMENTED_DOWNLOAD_MIN_FILE_SIZE {
959 return None;
960 }
961
962 let piece_size = segmented_piece_size(total_size);
963 if total_size <= piece_size {
964 return None;
965 }
966
967 let pieces = build_download_pieces(total_size, piece_size);
968 let piece_count = pieces.len();
969 let worker_count = max_workers.min(piece_count).max(1);
970
971 Some(SegmentedDownloadPlan { piece_size, piece_count, worker_count, pieces })
972}
973
974fn piece_retry_backoff(attempt: u32, throttled: bool) -> Duration {
976 let base = if throttled { 2 } else { RETRY_BACKOFF_SECS };
977 let multiplier = 1u64 << attempt.saturating_sub(1).min(3);
978 Duration::from_secs(base.saturating_mul(multiplier).min(SEGMENTED_DOWNLOAD_MAX_BACKOFF_SECS))
979}
980
981fn is_retryable_piece_status(status: StatusCode) -> bool {
983 matches!(status, StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_MANY_REQUESTS) ||
984 status.is_server_error()
985}
986
987fn should_retry_piece_status(status: StatusCode) -> bool {
989 status == StatusCode::OK || is_retryable_piece_status(status)
990}
991
992fn is_throttle_piece_status(status: StatusCode) -> bool {
994 matches!(
995 status,
996 StatusCode::REQUEST_TIMEOUT |
997 StatusCode::TOO_MANY_REQUESTS |
998 StatusCode::SERVICE_UNAVAILABLE |
999 StatusCode::GATEWAY_TIMEOUT
1000 )
1001}
1002
1003fn is_throttle_piece_error(error: &reqwest::Error) -> bool {
1005 error.is_timeout() || matches!(error.status(), Some(status) if is_throttle_piece_status(status))
1006}
1007
1008enum PieceAttemptFailure {
1010 Retryable { error: eyre::Error, throttled: bool },
1012 Terminal(eyre::Error),
1014}
1015
1016fn panic_payload_message(payload: Box<dyn Any + Send + 'static>) -> String {
1018 if let Some(message) = payload.downcast_ref::<&'static str>() {
1019 (*message).to_string()
1020 } else if let Some(message) = payload.downcast_ref::<String>() {
1021 message.clone()
1022 } else {
1023 "unknown panic payload".to_string()
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030 use reqwest::StatusCode;
1031 use reth_cli_util::cancellation::CancellationToken;
1032 use std::io::Write;
1033
1034 #[test]
1035 fn segmented_plan_skips_small_files() {
1036 assert!(plan_segmented_download(SEGMENTED_DOWNLOAD_MIN_FILE_SIZE - 1, 16).is_none());
1037 }
1038
1039 #[test]
1040 fn segmented_plan_uses_large_pieces_and_adaptive_workers() {
1041 let total_size = 512 * 1024 * 1024;
1042 let plan = plan_segmented_download(total_size, 32).unwrap();
1043
1044 assert_eq!(plan.piece_size, SEGMENTED_DOWNLOAD_SMALL_PIECE_SIZE);
1045 assert_eq!(plan.piece_count, 16);
1046 assert_eq!(plan.worker_count, 16);
1047 }
1048
1049 #[test]
1050 fn build_download_pieces_covers_entire_file() {
1051 let pieces = build_download_pieces(10, 4).into_iter().collect::<Vec<_>>();
1052
1053 assert_eq!(
1054 pieces,
1055 vec![
1056 DownloadPiece { start: 0, end: 3 },
1057 DownloadPiece { start: 4, end: 7 },
1058 DownloadPiece { start: 8, end: 9 },
1059 ]
1060 );
1061 }
1062
1063 #[test]
1064 fn piece_status_retry_policy_retries_200_ok() {
1065 assert!(should_retry_piece_status(StatusCode::OK));
1066 assert!(should_retry_piece_status(StatusCode::TOO_MANY_REQUESTS));
1067 assert!(should_retry_piece_status(StatusCode::from_u16(520).unwrap()));
1068 assert!(!should_retry_piece_status(StatusCode::NOT_FOUND));
1069 }
1070
1071 #[test]
1072 fn choose_fetch_strategy_uses_segmented_when_ranges_are_supported() {
1073 let strategy = choose_fetch_strategy(
1074 RemoteArchiveProbe { total_size: 512 * 1024 * 1024, supports_ranges: true },
1075 16,
1076 );
1077
1078 assert!(matches!(strategy, FetchStrategy::Segmented(_)));
1079 }
1080
1081 #[test]
1082 fn choose_fetch_strategy_falls_back_without_ranges() {
1083 let strategy = choose_fetch_strategy(
1084 RemoteArchiveProbe { total_size: 512 * 1024 * 1024, supports_ranges: false },
1085 16,
1086 );
1087
1088 assert!(matches!(
1089 strategy,
1090 FetchStrategy::Sequential(SequentialDownloadFallback::NoRangeSupport)
1091 ));
1092 }
1093
1094 #[test]
1095 fn archive_fetcher_uses_file_url_archive_directly() {
1096 let dir = tempfile::tempdir().unwrap();
1097 let archive_path = dir.path().join("state.tar.zst");
1098 {
1099 let mut archive = std::fs::File::create(&archive_path).unwrap();
1100 archive.write_all(b"local archive bytes").unwrap();
1101 }
1102
1103 let cache_dir = dir.path().join("cache");
1104 std::fs::create_dir(&cache_dir).unwrap();
1105 let url = Url::from_file_path(&archive_path).unwrap().to_string();
1106 let session = DownloadSession::new(None, None, CancellationToken::new());
1107 let fetcher = ArchiveFetcher::new(url, &cache_dir, session);
1108
1109 let downloaded = fetcher.download(None).unwrap();
1110
1111 assert_eq!(downloaded.path, archive_path);
1112 assert_eq!(downloaded.size, b"local archive bytes".len() as u64);
1113 assert!(!cache_dir.join("state.tar.zst").exists());
1114 assert!(!cache_dir.join("state.tar.zst.part").exists());
1115 }
1116
1117 #[test]
1118 fn segmented_state_requeues_failed_pieces_within_budget() {
1119 let state = SegmentedDownloadState::new(build_download_pieces(8, 4));
1120 let token = CancellationToken::new();
1121 let first = state.next_piece(&token).unwrap();
1122
1123 assert!(state.requeue(first));
1124 assert!(state.requeue(first));
1125 assert!(!state.requeue(first));
1126 assert_eq!(state.pieces.lock().unwrap().back(), Some(&first));
1127 }
1128}