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::{CheckedDownloads, PlannedArchive},
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::{path::Path, sync::Arc, time::Duration};
19use tracing::{debug, info, warn};
20
21const DOWNLOAD_CACHE_DIR: &str = ".download-cache";
22
23/// Runs all planned modular archive downloads for one command invocation.
24pub(crate) async fn run_modular_downloads(
25    planned_downloads: CheckedDownloads,
26    target_dir: &Path,
27    static_files_dir: Option<&Path>,
28    download_concurrency: usize,
29    cancel_token: CancellationToken,
30    retry_backoff: Option<Duration>,
31) -> Result<()> {
32    let download_cache_dir = target_dir.join(DOWNLOAD_CACHE_DIR);
33    fs::create_dir_all(&download_cache_dir)?;
34
35    let shared = SharedProgress::new(
36        planned_downloads.total_download_size,
37        planned_downloads.total_output_size,
38        planned_downloads.total_archives() as u64,
39        cancel_token.clone(),
40    );
41    let session = DownloadSession::new(
42        Some(Arc::clone(&shared)),
43        Some(DownloadRequestLimiter::new(download_concurrency)),
44        cancel_token,
45    )
46    .with_retry_backoff(retry_backoff);
47    let ctx = ArchiveProcessContext::new(
48        target_dir.to_path_buf(),
49        static_files_dir.map(Path::to_path_buf),
50        Some(download_cache_dir),
51        session,
52    );
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: CheckedDownloads) -> 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        for reused in &planned_downloads.reused {
78            info!(target: "reth::cli", file = %reused.archive.file_name, component = %reused.component, "Skipping already verified plain files");
79            shared.record_reused_archive(reused.archive.size, reused.archive.output_size());
80        }
81        let ctx = self.ctx.clone();
82        let results: Vec<Result<()>> = stream::iter(planned_downloads.pending)
83            .map(move |archive| {
84                let ctx = ctx.clone();
85                async move { Self::process_archive(ctx, archive).await }
86            })
87            .buffer_unordered(self.archive_concurrency)
88            .collect()
89            .await;
90
91        shared.done.notify_one();
92        let _ = progress_handle.await;
93
94        for result in results {
95            result?;
96        }
97
98        Ok(())
99    }
100
101    /// Runs one archive on the blocking pool so fetch and extraction stay off the async executor.
102    async fn process_archive(ctx: ArchiveProcessContext, archive: PlannedArchive) -> Result<()> {
103        reth_tasks::spawn_blocking(move || ArchiveProcessor::new(archive, ctx).run()).await??;
104        Ok(())
105    }
106}
107
108/// Explicit retry states for one modular archive.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110enum ArchiveAttemptState {
111    /// Start or restart one full archive attempt.
112    RunAttempt,
113    /// Check whether the extracted outputs verify.
114    VerifyOutputs,
115    /// Wait and decide whether another full attempt should run.
116    RetryAttempt,
117    /// Finish successfully.
118    Complete,
119    /// Stop with an error after retries are exhausted.
120    Fail,
121}
122
123/// Processes one modular archive through fetch, extraction, and verification.
124struct ArchiveProcessor {
125    /// The concrete archive and component being processed.
126    archive: PlannedArchive,
127    /// Shared paths and session state for this archive attempt.
128    ctx: ArchiveProcessContext,
129}
130
131impl ArchiveProcessor {
132    /// Creates a processor for one archive and the shared download context.
133    fn new(archive: PlannedArchive, ctx: ArchiveProcessContext) -> Self {
134        Self { archive, ctx }
135    }
136
137    /// Runs the archive retry state machine until outputs are verified or retries are exhausted.
138    ///
139    /// Callers only pass archives that failed the startup reuse check.
140    fn run(self) -> Result<()> {
141        let archive = self.archive();
142        let mode = ArchiveMode::new(&self.ctx)?;
143        let format = CompressionFormat::from_url(&archive.file_name)?;
144        let mut attempt = 1;
145        let mut last_error: Option<eyre::Error> = None;
146        let mut state = ArchiveAttemptState::RunAttempt;
147
148        loop {
149            match state {
150                ArchiveAttemptState::RunAttempt => {
151                    self.cleanup_outputs();
152
153                    if attempt > 1 {
154                        info!(target: "reth::cli",
155                            file = %archive.file_name,
156                            component = %self.archive.component,
157                            attempt,
158                            max = MAX_DOWNLOAD_RETRIES,
159                            "Retrying archive from scratch"
160                        );
161                    }
162
163                    match self.run_attempt(mode, format) {
164                        Ok(()) => state = ArchiveAttemptState::VerifyOutputs,
165                        Err(error) if mode.retries_fetch_errors() => {
166                            warn!(target: "reth::cli",
167                                file = %archive.file_name,
168                                component = %self.archive.component,
169                                attempt,
170                                err = %format_args!("{error:#}"),
171                                "Archive attempt failed, retrying from scratch"
172                            );
173                            last_error = Some(error);
174                            state = ArchiveAttemptState::RetryAttempt;
175                        }
176                        Err(error) => return Err(error),
177                    }
178                }
179                ArchiveAttemptState::VerifyOutputs => {
180                    if self.verify_outputs_with_progress()? {
181                        state = ArchiveAttemptState::Complete;
182                    } else {
183                        warn!(target: "reth::cli", file = %archive.file_name, component = %self.archive.component, attempt, "Archive extracted, but output verification failed, retrying");
184                        state = ArchiveAttemptState::RetryAttempt;
185                    }
186                }
187                ArchiveAttemptState::RetryAttempt => {
188                    if attempt >= MAX_DOWNLOAD_RETRIES {
189                        state = ArchiveAttemptState::Fail;
190                    } else {
191                        std::thread::sleep(
192                            self.ctx.session().retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
193                        );
194                        attempt += 1;
195                        state = ArchiveAttemptState::RunAttempt;
196                    }
197                }
198                ArchiveAttemptState::Complete => return Ok(()),
199                ArchiveAttemptState::Fail => {
200                    if let Some(error) = last_error {
201                        return Err(error.wrap_err(format!(
202                            "Failed after {} attempts for {}",
203                            MAX_DOWNLOAD_RETRIES, archive.file_name
204                        )));
205                    }
206
207                    eyre::bail!(
208                        "Failed integrity validation after {} attempts for {}",
209                        MAX_DOWNLOAD_RETRIES,
210                        archive.file_name
211                    );
212                }
213            }
214        }
215    }
216
217    /// Returns the concrete archive being fetched or verified.
218    fn archive(&self) -> &SnapshotArchive {
219        &self.archive.archive
220    }
221
222    /// Returns the verifier for this archive's output files.
223    fn output_verifier(&self) -> OutputVerifier<'_> {
224        OutputVerifier::new(self.ctx.target_dir(), self.ctx.static_files_dir())
225    }
226
227    /// Removes any partial outputs before a fresh archive attempt.
228    fn cleanup_outputs(&self) {
229        self.output_verifier().cleanup(&self.archive().output_files);
230    }
231
232    /// Executes one archive attempt according to the selected cache-vs-stream mode.
233    fn run_attempt(&self, mode: ArchiveMode, format: CompressionFormat) -> Result<()> {
234        mode.execute(self, format)
235    }
236
237    /// Downloads the archive into the cache, then extracts from the cached file.
238    fn run_cached_attempt(&self, format: CompressionFormat) -> Result<()> {
239        let cache_dir =
240            self.ctx.cache_dir().ok_or_else(|| eyre::eyre!("Missing download cache directory"))?;
241        let fetcher =
242            ArchiveFetcher::new(self.archive().url.clone(), cache_dir, self.ctx.session().clone());
243
244        if self.archive.ty == super::manifest::SnapshotComponentType::State {
245            debug!(target: "reth::cli", url = %self.archive().url, "Downloading state snapshot archive");
246        }
247
248        let download_result = {
249            let mut download_progress = ArchiveDownloadProgress::new(self.ctx.session().progress());
250            let result = fetcher.download(Some(&mut download_progress));
251            if let Ok(ref downloaded) = result &&
252                download_progress.has_tracked_bytes()
253            {
254                download_progress.complete(downloaded.size);
255            }
256            result
257        };
258
259        let downloaded = match download_result {
260            Ok(downloaded) => downloaded,
261            Err(error) => {
262                fetcher.cleanup_downloaded_files();
263                return Err(error);
264            }
265        };
266
267        info!(target: "reth::cli",
268            file = %self.archive().file_name,
269            component = %self.archive.component,
270            size = %super::progress::DownloadProgress::format_size(downloaded.size),
271            "Archive download complete"
272        );
273
274        let extract_result = self.extract_cached_archive(&downloaded.path, format);
275        fetcher.cleanup_downloaded_files();
276        extract_result
277    }
278
279    /// Streams the archive directly into extraction without keeping a cached copy.
280    fn run_streaming_attempt(&self, format: CompressionFormat) -> Result<()> {
281        let _download_progress = ArchiveDownloadProgress::new(self.ctx.session().progress());
282        streaming_download_and_extract(
283            &self.archive().url,
284            format,
285            self.ctx.target_dir(),
286            self.ctx.static_files_dir(),
287            self.ctx.session(),
288        )
289    }
290
291    /// Extracts a cached archive file while updating shared extraction activity.
292    fn extract_cached_archive(&self, archive_path: &Path, format: CompressionFormat) -> Result<()> {
293        let mut extraction_progress = ArchiveExtractionProgress::new(self.ctx.session().progress());
294        let file = fs::open(archive_path)?;
295        let result = extract_archive_raw(
296            file,
297            format,
298            self.ctx.target_dir(),
299            self.ctx.static_files_dir(),
300            Some(&mut extraction_progress),
301        );
302        extraction_progress.finish();
303        result
304    }
305
306    /// Returns `true` if all declared plain outputs verify while updating shared verification
307    /// progress.
308    fn verify_outputs_with_progress(&self) -> Result<bool> {
309        let mut verification_progress =
310            ArchiveVerificationProgress::new(self.ctx.session().progress());
311        let verified = self
312            .output_verifier()
313            .verify_with_progress(&self.archive().output_files, Some(&verification_progress))?;
314        if verified {
315            verification_progress.complete(self.archive().output_size());
316        }
317        Ok(verified)
318    }
319}
320
321/// Chooses whether an archive attempt uses the cache or streams directly.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323enum ArchiveMode {
324    /// Download the archive to the cache, then extract it.
325    Cached,
326    /// Stream the archive directly into extraction.
327    Streaming,
328}
329
330impl ArchiveMode {
331    /// Picks the archive mode from the process context.
332    fn new(ctx: &ArchiveProcessContext) -> Result<Self> {
333        if ctx.cache_dir().is_some() {
334            ctx.session().require_request_limiter()?;
335            return Ok(Self::Cached)
336        }
337
338        Ok(Self::Streaming)
339    }
340
341    /// Returns `true` when fetch failures should retry the whole archive attempt.
342    const fn retries_fetch_errors(&self) -> bool {
343        matches!(self, Self::Cached)
344    }
345
346    /// Runs the selected archive mode for a single attempt.
347    fn execute(&self, processor: &ArchiveProcessor, format: CompressionFormat) -> Result<()> {
348        match self {
349            Self::Cached => processor.run_cached_attempt(format),
350            Self::Streaming => processor.run_streaming_attempt(format),
351        }
352    }
353}