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 tokio::task;
20use tracing::{debug, info, warn};
21
22const DOWNLOAD_CACHE_DIR: &str = ".download-cache";
23
24pub(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
58struct ModularDownloadJob {
60 ctx: ArchiveProcessContext,
62 archive_concurrency: usize,
64}
65
66impl ModularDownloadJob {
67 const fn new(ctx: ArchiveProcessContext, archive_concurrency: usize) -> Self {
69 Self { ctx, archive_concurrency }
70 }
71
72 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum ArchiveAttemptState {
112 RunAttempt,
114 VerifyOutputs,
116 RetryAttempt,
118 Complete,
120 Fail,
122}
123
124struct ArchiveProcessor {
126 archive: PlannedArchive,
128 ctx: ArchiveProcessContext,
130}
131
132impl ArchiveProcessor {
133 fn new(archive: PlannedArchive, ctx: ArchiveProcessContext) -> Self {
135 Self { archive, ctx }
136 }
137
138 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 fn archive(&self) -> &SnapshotArchive {
220 &self.archive.archive
221 }
222
223 fn output_verifier(&self) -> OutputVerifier<'_> {
225 OutputVerifier::new(self.ctx.target_dir(), self.ctx.static_files_dir())
226 }
227
228 fn cleanup_outputs(&self) {
230 self.output_verifier().cleanup(&self.archive().output_files);
231 }
232
233 fn run_attempt(&self, mode: ArchiveMode, format: CompressionFormat) -> Result<()> {
235 mode.execute(self, format)
236 }
237
238 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324enum ArchiveMode {
325 Cached,
327 Streaming,
329}
330
331impl ArchiveMode {
332 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 const fn retries_fetch_errors(&self) -> bool {
344 matches!(self, Self::Cached)
345 }
346
347 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}