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