Skip to main content

reth_cli_commands/download/
archive.rs

1use 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
28/// Runs all planned modular archive downloads for one command invocation.
29pub(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
57/// Schedules modular archive work for one run of `reth download`.
58struct ModularDownloadJob {
59    /// Shared paths and session state for each archive in this job.
60    ctx: ArchiveProcessContext,
61    /// Maximum number of archives processed at once.
62    archive_concurrency: usize,
63}
64
65impl ModularDownloadJob {
66    /// Creates the modular download job for one command run.
67    const fn new(ctx: ArchiveProcessContext, archive_concurrency: usize) -> Self {
68        Self { ctx, archive_concurrency }
69    }
70
71    /// Runs all planned archives and waits for the shared progress task to finish.
72    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    /// Runs one archive on the blocking pool so fetch and extraction stay off the async executor.
98    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/// Explicit retry states for one modular archive.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106enum ArchiveAttemptState {
107    /// Start or restart one full archive attempt.
108    RunAttempt,
109    /// Check whether the extracted outputs verify.
110    VerifyOutputs,
111    /// Wait and decide whether another full attempt should run.
112    RetryAttempt,
113    /// Finish successfully.
114    Complete,
115    /// Stop with an error after retries are exhausted.
116    Fail,
117}
118
119/// Processes one modular archive from reuse check through extraction and verification.
120struct ArchiveProcessor {
121    /// The concrete archive and component being processed.
122    archive: PlannedArchive,
123    /// Shared paths and session state for this archive attempt.
124    ctx: ArchiveProcessContext,
125}
126
127impl ArchiveProcessor {
128    /// Creates a processor for one archive and the shared download context.
129    fn new(archive: PlannedArchive, ctx: ArchiveProcessContext) -> Self {
130        Self { archive, ctx }
131    }
132
133    /// Runs the archive retry state machine until outputs are verified or retries are exhausted.
134    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    /// Returns the concrete archive being fetched or verified.
217    fn archive(&self) -> &SnapshotArchive {
218        &self.archive.archive
219    }
220
221    /// Returns the verifier for this archive's output files.
222    fn output_verifier(&self) -> OutputVerifier<'_> {
223        OutputVerifier::new(self.ctx.target_dir())
224    }
225
226    /// Returns `true` if this archive can be reused from existing verified outputs.
227    /// Returns `false` if a fresh archive attempt is still needed.
228    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    /// Removes any partial outputs before a fresh archive attempt.
238    fn cleanup_outputs(&self) {
239        self.output_verifier().cleanup(&self.archive().output_files);
240    }
241
242    /// Returns `true` if all declared plain outputs verify.
243    /// Returns `false` if any output is missing or does not match.
244    fn verify_outputs(&self) -> Result<bool> {
245        self.output_verifier().verify(&self.archive().output_files)
246    }
247
248    /// Records archive completion in shared progress once outputs verify.
249    fn mark_complete(&self) {
250        self.ctx.session().record_reused_archive(self.archive().size, self.archive().output_size());
251    }
252
253    /// Executes one archive attempt according to the selected cache-vs-stream mode.
254    fn run_attempt(&self, mode: ArchiveMode, format: CompressionFormat) -> Result<()> {
255        mode.execute(self, format)
256    }
257
258    /// Downloads the archive into the cache, then extracts from the cached file.
259    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    /// Streams the archive directly into extraction without keeping a cached copy.
301    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    /// Extracts a cached archive file while updating shared extraction activity.
312    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    /// Returns `true` if all declared plain outputs verify while updating shared verification
326    /// progress.
327    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/// Chooses whether an archive attempt uses the cache or streams directly.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342enum ArchiveMode {
343    /// Download the archive to the cache, then extract it.
344    Cached,
345    /// Stream the archive directly into extraction.
346    Streaming,
347}
348
349impl ArchiveMode {
350    /// Picks the archive mode from the process context.
351    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    /// Returns `true` when fetch failures should retry the whole archive attempt.
361    const fn retries_fetch_errors(&self) -> bool {
362        matches!(self, Self::Cached)
363    }
364
365    /// Runs the selected archive mode for a single attempt.
366    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}