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