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 retry_delay = ?self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
251 "Download failed, retrying"
252 );
253 std::thread::sleep(
254 self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
255 );
256 }
257 continue;
258 }
259 };
260
261 let is_partial = response.status() == StatusCode::PARTIAL_CONTENT;
262 let size = if is_partial {
263 response
264 .headers()
265 .get("Content-Range")
266 .and_then(|value| value.to_str().ok())
267 .and_then(|value| value.split('/').next_back())
268 .and_then(|value| value.parse().ok())
269 } else {
270 response.content_length()
271 };
272
273 if total_size.is_none() {
274 total_size = size;
275 if !quiet && let Some(size) = size {
276 info!(target: "reth::cli",
277 file = %self.paths.file_name(),
278 size = %DownloadProgress::format_size(size),
279 "Downloading"
280 );
281 }
282 }
283
284 let current_total = total_size.ok_or_else(|| {
285 eyre::eyre!("Server did not provide Content-Length or Content-Range header")
286 })?;
287
288 let file = if is_partial && existing_size > 0 {
289 OpenOptions::new()
290 .append(true)
291 .open(self.paths.part_path())
292 .map_err(|error| fs::FsPathError::open(error, self.paths.part_path()))?
293 } else {
294 fs::create_file(self.paths.part_path())?
295 };
296
297 let start_offset = if is_partial { existing_size } else { 0 };
298 let mut reader = response;
299
300 let copy_result;
301 let flush_result;
302
303 if let Some(progress) = self.session.progress() {
304 let mut on_written = |bytes| {
305 if let Some(download_progress) = download_progress.as_deref_mut() {
306 download_progress.record_downloaded(bytes);
307 }
308 };
309 let mut writer = SharedProgressWriter {
310 inner: BufWriter::new(file),
311 progress: Arc::clone(progress),
312 on_written: Some(&mut on_written),
313 };
314 copy_result = io::copy(&mut reader, &mut writer);
315 flush_result = writer.inner.flush();
316 } else {
317 let mut progress = DownloadProgress::new(current_total);
318 progress.downloaded = start_offset;
319 let mut writer = ProgressWriter {
320 inner: BufWriter::new(file),
321 progress,
322 cancel_token: self.session.cancel_token().clone(),
323 };
324 copy_result = io::copy(&mut reader, &mut writer);
325 flush_result = writer.inner.flush();
326 println!();
327 }
328
329 if let Err(error) = copy_result.and(flush_result) {
330 last_error = Some(error.into());
331 if attempt < max_download_retries {
332 info!(target: "reth::cli",
333 file = %self.paths.file_name(),
334 retry_delay = ?self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
335 "Download interrupted, retrying"
336 );
337 std::thread::sleep(
338 self.session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
339 );
340 }
341 continue;
342 }
343
344 return self.finalize_download(current_total)
345 }
346
347 Err(last_error.unwrap_or_else(|| {
348 eyre::eyre!("Download failed after {} attempts", max_download_retries)
349 }))
350 }
351
352 fn download_segmented(
354 &self,
355 total_size: u64,
356 plan: SegmentedDownloadPlan,
357 download_progress: Option<&mut ArchiveDownloadProgress<'_>>,
358 ) -> Result<DownloadedArchive> {
359 let request_limiter = self.session.require_request_limiter()?;
360 info!(target: "reth::cli",
361 total_size = %DownloadProgress::format_size(total_size),
362 piece_size = %DownloadProgress::format_size(plan.piece_size),
363 pieces = plan.piece_count,
364 workers = plan.worker_count,
365 max_concurrent_requests = request_limiter.max_concurrency(),
366 "Starting queued segmented download"
367 );
368
369 SegmentedDownload::new(
370 self.url.clone(),
371 self.paths.clone(),
372 total_size,
373 plan,
374 self.session.clone(),
375 download_progress,
376 )
377 .run()
378 }
379
380 fn log_sequential_fallback(&self, reason: SequentialDownloadFallback, total_size: u64) {
382 match reason {
383 SequentialDownloadFallback::NoRangeSupport => {
384 info!(target: "reth::cli",
385 file = %self.paths.file_name(),
386 "Server does not support Range requests, falling back to sequential download"
387 );
388 }
389 SequentialDownloadFallback::EmptyFile => {
390 info!(target: "reth::cli",
391 file = %self.paths.file_name(),
392 "Remote archive is empty, falling back to sequential download"
393 );
394 }
395 SequentialDownloadFallback::TooSmall => {
396 let _ = total_size;
397 }
398 }
399 }
400
401 fn finalize_download(&self, size: u64) -> Result<DownloadedArchive> {
403 self.paths.finalize()?;
404 if !self.quiet() {
405 info!(target: "reth::cli", file = %self.paths.file_name(), "Download complete");
406 }
407 Ok(DownloadedArchive { path: self.paths.final_path().to_path_buf(), size })
408 }
409
410 fn quiet(&self) -> bool {
412 self.session.progress().is_some()
413 }
414}
415
416fn archive_file_url_path(url: &str) -> Result<Option<PathBuf>> {
418 let Ok(parsed) = Url::parse(url) else { return Ok(None) };
419 if parsed.scheme() != "file" {
420 return Ok(None)
421 }
422
423 parsed
424 .to_file_path()
425 .map(Some)
426 .map_err(|_| eyre::eyre!("Invalid file:// archive URL path: {url}"))
427}
428
429#[derive(Debug, Clone)]
431pub(crate) struct DownloadedArchive {
432 pub(crate) path: PathBuf,
434 pub(crate) size: u64,
436}
437
438#[derive(Debug, Clone, Copy)]
440struct RemoteArchiveProbe {
441 total_size: u64,
443 supports_ranges: bool,
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449enum SequentialDownloadFallback {
450 NoRangeSupport,
452 EmptyFile,
454 TooSmall,
456}
457
458#[derive(Debug)]
460enum FetchStrategy {
461 Sequential(SequentialDownloadFallback),
463 Segmented(SegmentedDownloadPlan),
465}
466
467fn choose_fetch_strategy(probe: RemoteArchiveProbe, max_workers: usize) -> FetchStrategy {
469 if !probe.supports_ranges {
470 return FetchStrategy::Sequential(SequentialDownloadFallback::NoRangeSupport)
471 }
472
473 if probe.total_size == 0 {
474 return FetchStrategy::Sequential(SequentialDownloadFallback::EmptyFile)
475 }
476
477 plan_segmented_download(probe.total_size, max_workers)
478 .map(FetchStrategy::Segmented)
479 .unwrap_or(FetchStrategy::Sequential(SequentialDownloadFallback::TooSmall))
480}
481
482struct ProgressWriter<W> {
485 inner: W,
487 progress: DownloadProgress,
489 cancel_token: CancellationToken,
491}
492
493impl<W: Write> Write for ProgressWriter<W> {
494 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
496 if self.cancel_token.is_cancelled() {
497 return Err(io::Error::new(io::ErrorKind::Interrupted, "download cancelled"));
498 }
499 let n = self.inner.write(buf)?;
500 let _ = self.progress.update(n as u64);
501 Ok(n)
502 }
503
504 fn flush(&mut self) -> io::Result<()> {
506 self.inner.flush()
507 }
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512struct DownloadPiece {
513 start: u64,
515 end: u64,
517}
518
519#[derive(Debug)]
521struct SegmentedDownloadPlan {
522 piece_size: u64,
524 piece_count: usize,
526 worker_count: usize,
528 pieces: VecDeque<DownloadPiece>,
530}
531
532struct SegmentedDownload {
534 url: String,
536 paths: DownloadPaths,
538 total_size: u64,
540 plan: SegmentedDownloadPlan,
542 session: DownloadSession,
544}
545
546#[derive(Clone, Copy)]
548struct SegmentedWorkerContext<'a> {
549 url: &'a str,
551 part_path: &'a Path,
553 shared: Option<&'a Arc<SharedProgress>>,
555 request_limiter: &'a DownloadRequestLimiter,
557 cancel_token: &'a CancellationToken,
559 session: &'a DownloadSession,
561}
562
563impl SegmentedDownload {
564 fn new(
566 url: String,
567 paths: DownloadPaths,
568 total_size: u64,
569 plan: SegmentedDownloadPlan,
570 session: DownloadSession,
571 _download_progress: Option<&mut ArchiveDownloadProgress<'_>>,
572 ) -> Self {
573 Self { url, paths, total_size, plan, session }
574 }
575
576 fn run(self) -> Result<DownloadedArchive> {
578 let Self { url, paths, total_size, plan, session } = self;
579 {
580 let file = fs::create_file(paths.part_path())?;
581 file.set_len(total_size)?;
582 }
583
584 let worker_count = plan.worker_count;
585 let state = Arc::new(SegmentedDownloadState::new(plan.pieces));
586 let terminal_failure = Arc::new(TerminalFailure::default());
587 let piece_progress_bytes = Arc::new(AtomicU64::new(0));
588 let worker_client = BlockingClient::builder()
589 .connect_timeout(Duration::from_secs(30))
590 .timeout(Duration::from_secs(SEGMENTED_DOWNLOAD_REQUEST_TIMEOUT_SECS))
591 .build()?;
592 let request_limiter = Arc::clone(session.require_request_limiter()?);
593 let shared = session.progress();
594 let cancel_token = session.cancel_token();
595 let url = url.as_str();
596 let worker_context = SegmentedWorkerContext {
597 url,
598 part_path: paths.part_path(),
599 session: &session,
600 shared,
601 request_limiter: request_limiter.as_ref(),
602 cancel_token,
603 };
604
605 std::thread::scope(|scope| {
606 let mut handles = Vec::with_capacity(worker_count);
607
608 for _ in 0..worker_count {
609 let state = Arc::clone(&state);
610 let terminal_failure = Arc::clone(&terminal_failure);
611 let piece_progress_bytes = Arc::clone(&piece_progress_bytes);
612 let client = worker_client.clone();
613
614 handles.push(scope.spawn(move || {
615 Self::worker_loop(
616 &client,
617 worker_context,
618 state,
619 terminal_failure,
620 piece_progress_bytes,
621 );
622 }));
623 }
624
625 for handle in handles {
626 if let Err(payload) = handle.join() {
627 state.note_terminal_failure();
628 terminal_failure.record(eyre::eyre!(
629 "Segmented download worker panicked: {}",
630 panic_payload_message(payload)
631 ));
632 }
633 }
634 });
635
636 if let Some(error) = terminal_failure.take() {
637 if let Some(shared) = shared {
638 shared.sub_active_download_bytes(piece_progress_bytes.load(Ordering::Relaxed));
639 }
640 paths.cleanup_partial();
641 return Err(error.wrap_err("Parallel download failed"))
642 }
643
644 if let Some(shared) = shared {
645 shared.sub_active_download_bytes(piece_progress_bytes.load(Ordering::Relaxed));
646 shared.record_archive_download_complete(total_size);
647 }
648
649 paths.finalize()?;
650 info!(target: "reth::cli", file = %paths.file_name(), "Download complete");
651 Ok(DownloadedArchive { path: paths.final_path().to_path_buf(), size: total_size })
652 }
653
654 fn worker_loop(
656 client: &BlockingClient,
657 context: SegmentedWorkerContext<'_>,
658 state: Arc<SegmentedDownloadState>,
659 terminal_failure: Arc<TerminalFailure>,
660 piece_progress_bytes: Arc<AtomicU64>,
661 ) {
662 let file = match OpenOptions::new().write(true).open(context.part_path) {
663 Ok(file) => file,
664 Err(error) => {
665 state.note_terminal_failure();
666 terminal_failure.record(error.into());
667 return;
668 }
669 };
670
671 while let Some(piece) = state.next_piece(context.cancel_token) {
672 if let Err(error) = Self::download_piece_with_retries(
673 client,
674 context.url,
675 &file,
676 piece,
677 context.shared,
678 &piece_progress_bytes,
679 context.request_limiter,
680 context.cancel_token,
681 context.session,
682 ) {
683 state.note_terminal_failure();
684 terminal_failure.record(error);
685 return;
686 }
687 }
688 }
689
690 #[expect(clippy::too_many_arguments)]
695 fn download_piece_with_retries(
696 client: &BlockingClient,
697 url: &str,
698 file: &std::fs::File,
699 piece: DownloadPiece,
700 shared: Option<&Arc<SharedProgress>>,
701 piece_progress_bytes: &AtomicU64,
702 request_limiter: &DownloadRequestLimiter,
703 cancel_token: &CancellationToken,
704 session: &DownloadSession,
705 ) -> Result<()> {
706 for attempt in 1..=SEGMENT_RETRY_ATTEMPTS {
707 if cancel_token.is_cancelled() {
708 return Err(eyre::eyre!("Download cancelled"))
709 }
710
711 let _request_permit = request_limiter.acquire(shared, cancel_token)?;
712 match Self::download_piece_once(
713 client,
714 url,
715 file,
716 piece,
717 shared,
718 piece_progress_bytes,
719 cancel_token,
720 ) {
721 Ok(()) => return Ok(()),
722 Err(PieceAttemptFailure::Retryable { error: _, throttled })
723 if attempt < SEGMENT_RETRY_ATTEMPTS =>
724 {
725 std::thread::sleep(
726 session.retry_delay(piece_retry_backoff(attempt, throttled)),
727 );
728 }
729 Err(PieceAttemptFailure::Retryable { error, .. }) => return Err(error),
730 Err(PieceAttemptFailure::Terminal(error)) => return Err(error),
731 }
732 }
733
734 Err(eyre::eyre!("Piece download failed after {SEGMENT_RETRY_ATTEMPTS} attempts"))
735 }
736
737 fn download_piece_once(
739 client: &BlockingClient,
740 url: &str,
741 file: &std::fs::File,
742 piece: DownloadPiece,
743 shared: Option<&Arc<SharedProgress>>,
744 piece_progress_bytes: &AtomicU64,
745 cancel_token: &CancellationToken,
746 ) -> std::result::Result<(), PieceAttemptFailure> {
747 use std::os::unix::fs::FileExt;
748
749 let expected_len = piece.end - piece.start + 1;
750
751 let response = match client
752 .get(url)
753 .header(RANGE, format!("bytes={}-{}", piece.start, piece.end))
754 .send()
755 {
756 Ok(response) if response.status() == StatusCode::PARTIAL_CONTENT => response,
757 Ok(response) if should_retry_piece_status(response.status()) => {
758 return Err(PieceAttemptFailure::Retryable {
759 error: eyre::eyre!(
760 "Server returned {} for piece {}-{}",
761 response.status(),
762 piece.start,
763 piece.end
764 ),
765 throttled: is_throttle_piece_status(response.status()),
766 });
767 }
768 Ok(response) => {
769 return Err(PieceAttemptFailure::Terminal(eyre::eyre!(
770 "Server returned {} instead of 206 for Range request",
771 response.status()
772 )));
773 }
774 Err(error) => {
775 return Err(PieceAttemptFailure::Retryable {
776 throttled: is_throttle_piece_error(&error),
777 error: error.into(),
778 });
779 }
780 };
781
782 let mut buf = [0u8; 64 * 1024];
783 let mut reader = response.take(expected_len);
784 let mut offset = piece.start;
785
786 loop {
787 if cancel_token.is_cancelled() {
788 return Err(PieceAttemptFailure::Terminal(eyre::eyre!("Download cancelled")));
789 }
790
791 match reader.read(&mut buf) {
792 Ok(0) => break,
793 Ok(n) => {
794 file.write_all_at(&buf[..n], offset)
795 .map_err(|error| PieceAttemptFailure::Terminal(error.into()))?;
796 offset += n as u64;
797 if let Some(progress) = shared {
798 progress.record_session_fetched_bytes(n as u64);
799 }
800 }
801 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
802 Err(error) => {
803 return Err(PieceAttemptFailure::Retryable {
804 throttled: error.kind() == io::ErrorKind::TimedOut,
805 error: error.into(),
806 });
807 }
808 }
809 }
810
811 let downloaded_len = offset - piece.start;
812 if downloaded_len == expected_len {
813 if let Some(progress) = shared {
814 progress.add_active_download_bytes(expected_len);
815 }
816 piece_progress_bytes.fetch_add(expected_len, Ordering::Relaxed);
817 return Ok(())
818 }
819
820 Err(PieceAttemptFailure::Retryable {
821 error: eyre::eyre!(
822 "Piece {}-{} ended early: expected {} bytes, downloaded {}",
823 piece.start,
824 piece.end,
825 expected_len,
826 downloaded_len
827 ),
828 throttled: false,
829 })
830 }
831}
832
833struct SegmentedDownloadState {
837 pieces: Mutex<VecDeque<DownloadPiece>>,
839 failed: AtomicBool,
841}
842
843impl SegmentedDownloadState {
844 fn new(pieces: VecDeque<DownloadPiece>) -> Self {
846 Self { pieces: Mutex::new(pieces), failed: AtomicBool::new(false) }
847 }
848
849 fn next_piece(&self, cancel_token: &CancellationToken) -> Option<DownloadPiece> {
851 if cancel_token.is_cancelled() || self.failed.load(Ordering::Relaxed) {
852 return None;
853 }
854
855 self.pieces.lock().unwrap().pop_front()
856 }
857
858 fn note_terminal_failure(&self) {
860 self.failed.store(true, Ordering::Relaxed);
861 }
862}
863
864#[derive(Default)]
866struct TerminalFailure {
867 error: Mutex<Option<eyre::Error>>,
869}
870
871impl TerminalFailure {
872 fn record(&self, error: eyre::Error) {
874 let mut slot = self.error.lock().unwrap();
875 if slot.is_none() {
876 *slot = Some(error);
877 }
878 }
879
880 fn take(&self) -> Option<eyre::Error> {
882 self.error.lock().unwrap().take()
883 }
884}
885
886fn build_download_pieces(total_size: u64, piece_size: u64) -> VecDeque<DownloadPiece> {
888 let mut pieces = VecDeque::new();
889 let mut start = 0;
890
891 while start < total_size {
892 let end = (start + piece_size).min(total_size) - 1;
893 pieces.push_back(DownloadPiece { start, end });
894 start = end + 1;
895 }
896
897 pieces
898}
899
900fn segmented_piece_size(total_size: u64) -> u64 {
905 if total_size < 2 * 1024 * 1024 * 1024 {
906 SEGMENTED_DOWNLOAD_SMALL_PIECE_SIZE
907 } else {
908 SEGMENTED_DOWNLOAD_LARGE_PIECE_SIZE
909 }
910}
911
912fn plan_segmented_download(total_size: u64, max_workers: usize) -> Option<SegmentedDownloadPlan> {
917 if max_workers == 0 || total_size < SEGMENTED_DOWNLOAD_MIN_FILE_SIZE {
918 return None;
919 }
920
921 let piece_size = segmented_piece_size(total_size);
922 if total_size <= piece_size {
923 return None;
924 }
925
926 let pieces = build_download_pieces(total_size, piece_size);
927 let piece_count = pieces.len();
928 let worker_count = max_workers.min(piece_count).max(1);
929
930 Some(SegmentedDownloadPlan { piece_size, piece_count, worker_count, pieces })
931}
932
933fn piece_retry_backoff(attempt: u32, throttled: bool) -> Duration {
935 let base = if throttled { 2 } else { RETRY_BACKOFF_SECS };
936 let multiplier = 1u64 << attempt.saturating_sub(1).min(3);
937 Duration::from_secs(base.saturating_mul(multiplier).min(SEGMENTED_DOWNLOAD_MAX_BACKOFF_SECS))
938}
939
940fn is_retryable_piece_status(status: StatusCode) -> bool {
942 matches!(
943 status,
944 StatusCode::REQUEST_TIMEOUT |
945 StatusCode::TOO_MANY_REQUESTS |
946 StatusCode::INTERNAL_SERVER_ERROR |
947 StatusCode::BAD_GATEWAY |
948 StatusCode::SERVICE_UNAVAILABLE |
949 StatusCode::GATEWAY_TIMEOUT
950 )
951}
952
953fn should_retry_piece_status(status: StatusCode) -> bool {
955 status == StatusCode::OK || is_retryable_piece_status(status)
956}
957
958fn is_throttle_piece_status(status: StatusCode) -> bool {
960 matches!(
961 status,
962 StatusCode::REQUEST_TIMEOUT |
963 StatusCode::TOO_MANY_REQUESTS |
964 StatusCode::SERVICE_UNAVAILABLE |
965 StatusCode::GATEWAY_TIMEOUT
966 )
967}
968
969fn is_throttle_piece_error(error: &reqwest::Error) -> bool {
971 error.is_timeout() || matches!(error.status(), Some(status) if is_throttle_piece_status(status))
972}
973
974enum PieceAttemptFailure {
976 Retryable { error: eyre::Error, throttled: bool },
978 Terminal(eyre::Error),
980}
981
982fn panic_payload_message(payload: Box<dyn Any + Send + 'static>) -> String {
984 if let Some(message) = payload.downcast_ref::<&'static str>() {
985 (*message).to_string()
986 } else if let Some(message) = payload.downcast_ref::<String>() {
987 message.clone()
988 } else {
989 "unknown panic payload".to_string()
990 }
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996 use reqwest::StatusCode;
997 use reth_cli_util::cancellation::CancellationToken;
998 use std::io::Write;
999
1000 #[test]
1001 fn segmented_plan_skips_small_files() {
1002 assert!(plan_segmented_download(SEGMENTED_DOWNLOAD_MIN_FILE_SIZE - 1, 16).is_none());
1003 }
1004
1005 #[test]
1006 fn segmented_plan_uses_large_pieces_and_adaptive_workers() {
1007 let total_size = 512 * 1024 * 1024;
1008 let plan = plan_segmented_download(total_size, 32).unwrap();
1009
1010 assert_eq!(plan.piece_size, SEGMENTED_DOWNLOAD_SMALL_PIECE_SIZE);
1011 assert_eq!(plan.piece_count, 16);
1012 assert_eq!(plan.worker_count, 16);
1013 }
1014
1015 #[test]
1016 fn build_download_pieces_covers_entire_file() {
1017 let pieces = build_download_pieces(10, 4).into_iter().collect::<Vec<_>>();
1018
1019 assert_eq!(
1020 pieces,
1021 vec![
1022 DownloadPiece { start: 0, end: 3 },
1023 DownloadPiece { start: 4, end: 7 },
1024 DownloadPiece { start: 8, end: 9 },
1025 ]
1026 );
1027 }
1028
1029 #[test]
1030 fn piece_status_retry_policy_retries_200_ok() {
1031 assert!(should_retry_piece_status(StatusCode::OK));
1032 assert!(should_retry_piece_status(StatusCode::TOO_MANY_REQUESTS));
1033 assert!(!should_retry_piece_status(StatusCode::NOT_FOUND));
1034 }
1035
1036 #[test]
1037 fn choose_fetch_strategy_uses_segmented_when_ranges_are_supported() {
1038 let strategy = choose_fetch_strategy(
1039 RemoteArchiveProbe { total_size: 512 * 1024 * 1024, supports_ranges: true },
1040 16,
1041 );
1042
1043 assert!(matches!(strategy, FetchStrategy::Segmented(_)));
1044 }
1045
1046 #[test]
1047 fn choose_fetch_strategy_falls_back_without_ranges() {
1048 let strategy = choose_fetch_strategy(
1049 RemoteArchiveProbe { total_size: 512 * 1024 * 1024, supports_ranges: false },
1050 16,
1051 );
1052
1053 assert!(matches!(
1054 strategy,
1055 FetchStrategy::Sequential(SequentialDownloadFallback::NoRangeSupport)
1056 ));
1057 }
1058
1059 #[test]
1060 fn archive_fetcher_uses_file_url_archive_directly() {
1061 let dir = tempfile::tempdir().unwrap();
1062 let archive_path = dir.path().join("state.tar.zst");
1063 {
1064 let mut archive = std::fs::File::create(&archive_path).unwrap();
1065 archive.write_all(b"local archive bytes").unwrap();
1066 }
1067
1068 let cache_dir = dir.path().join("cache");
1069 std::fs::create_dir(&cache_dir).unwrap();
1070 let url = Url::from_file_path(&archive_path).unwrap().to_string();
1071 let session = DownloadSession::new(None, None, CancellationToken::new());
1072 let fetcher = ArchiveFetcher::new(url, &cache_dir, session);
1073
1074 let downloaded = fetcher.download(None).unwrap();
1075
1076 assert_eq!(downloaded.path, archive_path);
1077 assert_eq!(downloaded.size, b"local archive bytes".len() as u64);
1078 assert!(!cache_dir.join("state.tar.zst").exists());
1079 assert!(!cache_dir.join("state.tar.zst.part").exists());
1080 }
1081}