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::{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
23pub(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
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: 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110enum ArchiveAttemptState {
111 RunAttempt,
113 VerifyOutputs,
115 RetryAttempt,
117 Complete,
119 Fail,
121}
122
123struct ArchiveProcessor {
125 archive: PlannedArchive,
127 ctx: ArchiveProcessContext,
129}
130
131impl ArchiveProcessor {
132 fn new(archive: PlannedArchive, ctx: ArchiveProcessContext) -> Self {
134 Self { archive, ctx }
135 }
136
137 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 fn archive(&self) -> &SnapshotArchive {
219 &self.archive.archive
220 }
221
222 fn output_verifier(&self) -> OutputVerifier<'_> {
224 OutputVerifier::new(self.ctx.target_dir(), self.ctx.static_files_dir())
225 }
226
227 fn cleanup_outputs(&self) {
229 self.output_verifier().cleanup(&self.archive().output_files);
230 }
231
232 fn run_attempt(&self, mode: ArchiveMode, format: CompressionFormat) -> Result<()> {
234 mode.execute(self, format)
235 }
236
237 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323enum ArchiveMode {
324 Cached,
326 Streaming,
328}
329
330impl ArchiveMode {
331 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 const fn retries_fetch_errors(&self) -> bool {
343 matches!(self, Self::Cached)
344 }
345
346 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}