reth_cli_commands/download/
archive.rs1use super::{
2 extract::{extract_archive_raw, streaming_download_and_extract, CompressionFormat},
3 fetch::ArchiveFetcher,
4 manifest::SnapshotArchive,
5 planning::{PlannedArchive, PlannedDownloads},
6 progress::{
7 spawn_progress_display, ArchiveDownloadProgress, ArchiveExtractionProgress,
8 ArchiveVerificationProgress, DownloadRequestLimiter, SharedProgress,
9 },
10 session::{ArchiveProcessContext, DownloadSession},
11 verify::OutputVerifier,
12 MAX_DOWNLOAD_RETRIES, RETRY_BACKOFF_SECS,
13};
14use eyre::Result;
15use futures::stream::{self, StreamExt};
16use reth_cli_util::cancellation::CancellationToken;
17use reth_fs_util as fs;
18use std::{
19 path::Path,
20 sync::{atomic::Ordering, Arc},
21 time::Duration,
22};
23use tokio::task;
24use tracing::{debug, info, warn};
25
26const DOWNLOAD_CACHE_DIR: &str = ".download-cache";
27
28pub(crate) async fn run_modular_downloads(
30 planned_downloads: PlannedDownloads,
31 target_dir: &Path,
32 download_concurrency: usize,
33 cancel_token: CancellationToken,
34 retry_backoff: Option<Duration>,
35) -> Result<()> {
36 let download_cache_dir = target_dir.join(DOWNLOAD_CACHE_DIR);
37 fs::create_dir_all(&download_cache_dir)?;
38
39 let shared = SharedProgress::new(
40 planned_downloads.total_download_size,
41 planned_downloads.total_output_size,
42 planned_downloads.total_archives() as u64,
43 cancel_token.clone(),
44 );
45 let session = DownloadSession::new(
46 Some(Arc::clone(&shared)),
47 Some(DownloadRequestLimiter::new(download_concurrency)),
48 cancel_token,
49 )
50 .with_retry_backoff(retry_backoff);
51 let ctx =
52 ArchiveProcessContext::new(target_dir.to_path_buf(), Some(download_cache_dir), session);
53
54 ModularDownloadJob::new(ctx, download_concurrency).run(planned_downloads).await
55}
56
57struct ModularDownloadJob {
59 ctx: ArchiveProcessContext,
61 archive_concurrency: usize,
63}
64
65impl ModularDownloadJob {
66 const fn new(ctx: ArchiveProcessContext, archive_concurrency: usize) -> Self {
68 Self { ctx, archive_concurrency }
69 }
70
71 async fn run(self, planned_downloads: PlannedDownloads) -> Result<()> {
73 let shared = Arc::clone(
74 self.ctx.session().progress().expect("modular downloads always use shared progress"),
75 );
76 let progress_handle = spawn_progress_display(Arc::clone(&shared));
77 let ctx = self.ctx.clone();
78 let results: Vec<Result<()>> = stream::iter(planned_downloads.archives)
79 .map(move |archive| {
80 let ctx = ctx.clone();
81 async move { Self::process_archive(ctx, archive).await }
82 })
83 .buffer_unordered(self.archive_concurrency)
84 .collect()
85 .await;
86
87 shared.done.store(true, Ordering::Relaxed);
88 let _ = progress_handle.await;
89
90 for result in results {
91 result?;
92 }
93
94 Ok(())
95 }
96
97 async fn process_archive(ctx: ArchiveProcessContext, archive: PlannedArchive) -> Result<()> {
99 task::spawn_blocking(move || ArchiveProcessor::new(archive, ctx).run()).await??;
100 Ok(())
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106enum ArchiveAttemptState {
107 RunAttempt,
109 VerifyOutputs,
111 RetryAttempt,
113 Complete,
115 Fail,
117}
118
119struct ArchiveProcessor {
121 archive: PlannedArchive,
123 ctx: ArchiveProcessContext,
125}
126
127impl ArchiveProcessor {
128 fn new(archive: PlannedArchive, ctx: ArchiveProcessContext) -> Self {
130 Self { archive, ctx }
131 }
132
133 fn run(self) -> Result<()> {
135 let archive = self.archive();
136 if self.try_reuse_outputs()? {
137 info!(target: "reth::cli", file = %archive.file_name, component = %self.archive.component, "Skipping already verified plain files");
138 return Ok(());
139 }
140
141 let mode = ArchiveMode::new(&self.ctx)?;
142 let format = CompressionFormat::from_url(&archive.file_name)?;
143 let mut attempt = 1;
144 let mut last_error: Option<eyre::Error> = None;
145 let mut state = ArchiveAttemptState::RunAttempt;
146
147 loop {
148 match state {
149 ArchiveAttemptState::RunAttempt => {
150 self.cleanup_outputs();
151
152 if attempt > 1 {
153 info!(target: "reth::cli",
154 file = %archive.file_name,
155 component = %self.archive.component,
156 attempt,
157 max = MAX_DOWNLOAD_RETRIES,
158 "Retrying archive from scratch"
159 );
160 }
161
162 match self.run_attempt(mode, format) {
163 Ok(()) => state = ArchiveAttemptState::VerifyOutputs,
164 Err(error) if mode.retries_fetch_errors() => {
165 warn!(target: "reth::cli",
166 file = %archive.file_name,
167 component = %self.archive.component,
168 attempt,
169 err = %format_args!("{error:#}"),
170 "Archive attempt failed, retrying from scratch"
171 );
172 last_error = Some(error);
173 state = ArchiveAttemptState::RetryAttempt;
174 }
175 Err(error) => return Err(error),
176 }
177 }
178 ArchiveAttemptState::VerifyOutputs => {
179 if self.verify_outputs_with_progress()? {
180 state = ArchiveAttemptState::Complete;
181 } else {
182 warn!(target: "reth::cli", file = %archive.file_name, component = %self.archive.component, attempt, "Archive extracted, but output verification failed, retrying");
183 state = ArchiveAttemptState::RetryAttempt;
184 }
185 }
186 ArchiveAttemptState::RetryAttempt => {
187 if attempt >= MAX_DOWNLOAD_RETRIES {
188 state = ArchiveAttemptState::Fail;
189 } else {
190 std::thread::sleep(
191 self.ctx.session().retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
192 );
193 attempt += 1;
194 state = ArchiveAttemptState::RunAttempt;
195 }
196 }
197 ArchiveAttemptState::Complete => return Ok(()),
198 ArchiveAttemptState::Fail => {
199 if let Some(error) = last_error {
200 return Err(error.wrap_err(format!(
201 "Failed after {} attempts for {}",
202 MAX_DOWNLOAD_RETRIES, archive.file_name
203 )));
204 }
205
206 eyre::bail!(
207 "Failed integrity validation after {} attempts for {}",
208 MAX_DOWNLOAD_RETRIES,
209 archive.file_name
210 );
211 }
212 }
213 }
214 }
215
216 fn archive(&self) -> &SnapshotArchive {
218 &self.archive.archive
219 }
220
221 fn output_verifier(&self) -> OutputVerifier<'_> {
223 OutputVerifier::new(self.ctx.target_dir())
224 }
225
226 fn try_reuse_outputs(&self) -> Result<bool> {
229 if self.verify_outputs()? {
230 self.mark_complete();
231 return Ok(true);
232 }
233
234 Ok(false)
235 }
236
237 fn cleanup_outputs(&self) {
239 self.output_verifier().cleanup(&self.archive().output_files);
240 }
241
242 fn verify_outputs(&self) -> Result<bool> {
245 self.output_verifier().verify(&self.archive().output_files)
246 }
247
248 fn mark_complete(&self) {
250 self.ctx.session().record_reused_archive(self.archive().size, self.archive().output_size());
251 }
252
253 fn run_attempt(&self, mode: ArchiveMode, format: CompressionFormat) -> Result<()> {
255 mode.execute(self, format)
256 }
257
258 fn run_cached_attempt(&self, format: CompressionFormat) -> Result<()> {
260 let cache_dir =
261 self.ctx.cache_dir().ok_or_else(|| eyre::eyre!("Missing download cache directory"))?;
262 let fetcher =
263 ArchiveFetcher::new(self.archive().url.clone(), cache_dir, self.ctx.session().clone());
264
265 if self.archive.ty == super::manifest::SnapshotComponentType::State {
266 debug!(target: "reth::cli", url = %self.archive().url, "Downloading state snapshot archive");
267 }
268
269 let download_result = {
270 let mut download_progress = ArchiveDownloadProgress::new(self.ctx.session().progress());
271 let result = fetcher.download(Some(&mut download_progress));
272 if let Ok(ref downloaded) = result &&
273 download_progress.has_tracked_bytes()
274 {
275 download_progress.complete(downloaded.size);
276 }
277 result
278 };
279
280 let downloaded = match download_result {
281 Ok(downloaded) => downloaded,
282 Err(error) => {
283 fetcher.cleanup_downloaded_files();
284 return Err(error);
285 }
286 };
287
288 info!(target: "reth::cli",
289 file = %self.archive().file_name,
290 component = %self.archive.component,
291 size = %super::progress::DownloadProgress::format_size(downloaded.size),
292 "Archive download complete"
293 );
294
295 let extract_result = self.extract_cached_archive(&downloaded.path, format);
296 fetcher.cleanup_downloaded_files();
297 extract_result
298 }
299
300 fn run_streaming_attempt(&self, format: CompressionFormat) -> Result<()> {
302 let _download_progress = ArchiveDownloadProgress::new(self.ctx.session().progress());
303 streaming_download_and_extract(
304 &self.archive().url,
305 format,
306 self.ctx.target_dir(),
307 self.ctx.session(),
308 )
309 }
310
311 fn extract_cached_archive(&self, archive_path: &Path, format: CompressionFormat) -> Result<()> {
313 let mut extraction_progress = ArchiveExtractionProgress::new(self.ctx.session().progress());
314 let file = fs::open(archive_path)?;
315 let result = extract_archive_raw(
316 file,
317 format,
318 self.ctx.target_dir(),
319 Some(&mut extraction_progress),
320 );
321 extraction_progress.finish();
322 result
323 }
324
325 fn verify_outputs_with_progress(&self) -> Result<bool> {
328 let mut verification_progress =
329 ArchiveVerificationProgress::new(self.ctx.session().progress());
330 let verified = self
331 .output_verifier()
332 .verify_with_progress(&self.archive().output_files, Some(&mut verification_progress))?;
333 if verified {
334 verification_progress.complete(self.archive().output_size());
335 }
336 Ok(verified)
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342enum ArchiveMode {
343 Cached,
345 Streaming,
347}
348
349impl ArchiveMode {
350 fn new(ctx: &ArchiveProcessContext) -> Result<Self> {
352 if ctx.cache_dir().is_some() {
353 ctx.session().require_request_limiter()?;
354 return Ok(Self::Cached)
355 }
356
357 Ok(Self::Streaming)
358 }
359
360 const fn retries_fetch_errors(&self) -> bool {
362 matches!(self, Self::Cached)
363 }
364
365 fn execute(&self, processor: &ArchiveProcessor, format: CompressionFormat) -> Result<()> {
367 match self {
368 Self::Cached => processor.run_cached_attempt(format),
369 Self::Streaming => processor.run_streaming_attempt(format),
370 }
371 }
372}