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