Skip to content

Commit 0a60116

Browse files
committed
add retries to OCI network hiccups
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com> Assisted-by: claude-opus-4.6
1 parent 60e5a9b commit 0a60116

13 files changed

Lines changed: 752 additions & 232 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ nix = { version = "0.29", features = ["ioctl"] }
2121
# This ensures OpenSSL builds from source with musl compatibility
2222
openssl = { version = "0.10", features = ["vendored"] }
2323
xz2 = "0.1"
24+
http-body = "1.0.1"
2425

2526
[dev-dependencies]
2627
wiremock = "0.6"

src/fls/decompress.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,43 @@ pub(crate) async fn start_decompressor_process(
3535
) -> Result<(Child, &'static str), Box<dyn std::error::Error>> {
3636
let cmd = get_decompressor_command(url);
3737

38-
// Check if binary is available before attempting to spawn
3938
check_binary_available(cmd)?;
39+
println!("Using decompressor: {}", cmd);
40+
spawn_decompressor(cmd)
41+
}
42+
43+
/// Maps a Compression enum to the corresponding decompressor command
44+
pub(crate) fn decompressor_for_compression(
45+
compression: crate::fls::compression::Compression,
46+
) -> &'static str {
47+
use crate::fls::compression::Compression;
48+
match compression {
49+
Compression::Gzip => "zcat",
50+
Compression::Xz => "xzcat",
51+
Compression::Zstd => "zstdcat",
52+
Compression::None => "cat",
53+
}
54+
}
4055

56+
/// Starts a decompressor process based on detected compression type
57+
pub(crate) fn start_decompressor_for_compression(
58+
compression: crate::fls::compression::Compression,
59+
) -> Result<(Child, &'static str), Box<dyn std::error::Error>> {
60+
let cmd = decompressor_for_compression(compression);
61+
check_binary_available(cmd)?;
4162
println!("Using decompressor: {}", cmd);
63+
spawn_decompressor(cmd)
64+
}
4265

66+
/// Spawns a decompressor subprocess with piped stdin/stdout/stderr
67+
fn spawn_decompressor(
68+
cmd: &'static str,
69+
) -> Result<(Child, &'static str), Box<dyn std::error::Error>> {
4370
let process = Command::new(cmd)
4471
.stdin(std::process::Stdio::piped())
4572
.stdout(std::process::Stdio::piped())
4673
.stderr(std::process::Stdio::piped())
4774
.spawn()?;
48-
4975
Ok((process, cmd))
5076
}
5177

src/fls/download_error.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,42 @@ impl fmt::Display for DownloadError {
231231

232232
impl std::error::Error for DownloadError {}
233233

234+
/// Shared retry handler for download errors.
235+
///
236+
/// Returns `Some(Duration)` with the delay to wait before retrying, or `None` if
237+
/// the error is non-retryable or max retries have been exceeded.
238+
pub fn handle_download_retry(
239+
error: &DownloadError,
240+
retry_count: &mut usize,
241+
max_retries: usize,
242+
default_retry_delay_secs: u64,
243+
) -> Option<Duration> {
244+
if !error.is_retryable() {
245+
eprintln!(
246+
"\nDownload failed with non-retryable error: {}",
247+
error.format_error()
248+
);
249+
return None;
250+
}
251+
if *retry_count >= max_retries {
252+
eprintln!("\nMax retries ({}) reached, giving up", max_retries);
253+
eprintln!("Last error: {}", error.format_error());
254+
return None;
255+
}
256+
let retry_delay = error
257+
.suggested_retry_delay()
258+
.unwrap_or_else(|| Duration::from_secs(default_retry_delay_secs));
259+
eprintln!("\nDownload failed: {}", error.format_error());
260+
eprintln!(
261+
"Retrying in {} seconds... (attempt {}/{})",
262+
retry_delay.as_secs(),
263+
*retry_count + 1,
264+
max_retries
265+
);
266+
*retry_count += 1;
267+
Some(retry_delay)
268+
}
269+
234270
#[cfg(test)]
235271
mod tests {
236272
use super::*;

src/fls/fastboot.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ fn build_oci_options(options: &FastbootOptions) -> crate::fls::options::OciOptio
8686
username: options.username.clone(),
8787
password: options.password.clone(),
8888
file_pattern: None,
89+
max_retries: crate::fls::options::DEFAULT_MAX_RETRIES,
90+
retry_delay_secs: crate::fls::options::DEFAULT_RETRY_DELAY_SECS,
8991
}
9092
}
9193

src/fls/from_url.rs

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -49,44 +49,7 @@ async fn get_decompressor_error(
4949
}
5050
}
5151

52-
/// Handles retry logic for download errors.
53-
///
54-
/// Returns `Some(Duration)` with the delay to wait before retrying, or `None` if
55-
/// the error is non-retryable or max retries have been exceeded.
56-
fn handle_retry_error(
57-
error: &DownloadError,
58-
retry_count: &mut usize,
59-
max_retries: usize,
60-
default_retry_delay_secs: u64,
61-
) -> Option<Duration> {
62-
if !error.is_retryable() {
63-
eprintln!(
64-
"\nDownload failed with non-retryable error: {}",
65-
error.format_error()
66-
);
67-
return None;
68-
}
69-
70-
if *retry_count >= max_retries {
71-
eprintln!("\nMax retries ({}) reached, giving up", max_retries);
72-
eprintln!("Last error: {}", error.format_error());
73-
return None;
74-
}
75-
76-
let retry_delay = error
77-
.suggested_retry_delay()
78-
.unwrap_or_else(|| Duration::from_secs(default_retry_delay_secs));
79-
80-
eprintln!("\nDownload failed: {}", error.format_error());
81-
eprintln!(
82-
"Retrying in {} seconds... (attempt {}/{})",
83-
retry_delay.as_secs(),
84-
*retry_count + 1,
85-
max_retries
86-
);
87-
*retry_count += 1;
88-
Some(retry_delay)
89-
}
52+
use crate::fls::download_error::handle_download_retry;
9053

9154
/// Execute a sequence of write commands on the block writer
9255
async fn execute_write_commands(
@@ -407,7 +370,7 @@ pub async fn flash_from_url(
407370
match start_download(url, &client, resume_from, &options.headers, debug).await {
408371
Ok(r) => r,
409372
Err(e) => {
410-
match handle_retry_error(
373+
match handle_download_retry(
411374
&e,
412375
&mut retry_count,
413376
options.max_retries,
@@ -551,7 +514,7 @@ pub async fn flash_from_url(
551514

552515
if let Some(e) = connection_error {
553516
eprintln!("\nConnection interrupted: {}", e.format_error());
554-
match handle_retry_error(
517+
match handle_download_retry(
555518
&e,
556519
&mut retry_count,
557520
options.max_retries,

src/fls/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub use from_url::flash_from_url;
2424
pub use oci::flash_from_oci;
2525
pub use options::{
2626
BlockFlashOptions, FastbootOptions, FlashOptions, HttpClientOptions, OciOptions,
27+
DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_SECS,
2728
};
2829

2930
#[cfg(test)]

0 commit comments

Comments
 (0)