use std::process::Command;
const RETRY_ATTEMPTS: &str = "3";
const RETRY_DELAY_SECS: &str = "2";
const CONNECT_TIMEOUT_SECS: &str = "30";
const fn retry_args() -> [&'static str; 8] {
[
"--retry",
RETRY_ATTEMPTS,
"--retry-delay",
RETRY_DELAY_SECS,
"--retry-connrefused",
"--retry-all-errors",
"--connect-timeout",
CONNECT_TIMEOUT_SECS,
]
}
pub fn curl_argv(url: &str, dest: &str) -> Vec<String> {
let mut argv = vec!["curl".to_string(), "-fsSL".to_string()];
argv.extend(retry_args().iter().map(|s| (*s).to_string()));
argv.extend(["-o".to_string(), dest.to_string(), url.to_string()]);
argv
}
pub fn apply_retry_args(cmd: &mut Command) {
cmd.args(retry_args());
}
pub fn with_retry<T, E, F>(mut attempt: F) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
{
let attempts: u32 = RETRY_ATTEMPTS.parse().unwrap_or(3);
let delay = std::time::Duration::from_secs(RETRY_DELAY_SECS.parse().unwrap_or(2));
let mut last = None;
for i in 0..attempts.max(1) {
if i > 0 {
std::thread::sleep(delay);
}
match attempt() {
Ok(value) => return Ok(value),
Err(err) => last = Some(err),
}
}
Err(last.expect("at least one attempt ran"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn curl_argv_includes_retry_flags() {
let argv = curl_argv("https://example.com/x.gz", "/tmp/x.dl");
assert!(argv.contains(&"--retry".to_string()));
assert!(argv.contains(&"--retry-connrefused".to_string()));
assert!(argv.contains(&"--retry-all-errors".to_string()));
assert!(argv.contains(&"--connect-timeout".to_string()));
}
#[test]
fn curl_argv_puts_url_last_and_dest_after_o() {
let argv = curl_argv("https://example.com/x.gz", "/tmp/x.dl");
assert_eq!(argv[0], "curl");
assert_eq!(argv.last().unwrap(), "https://example.com/x.gz");
let o = argv.iter().position(|a| a == "-o").expect("-o present");
assert_eq!(argv[o + 1], "/tmp/x.dl");
}
#[test]
fn no_total_transfer_timeout() {
let argv = curl_argv("https://example.com/x.gz", "/tmp/x.dl");
assert!(!argv.contains(&"--max-time".to_string()));
}
#[test]
fn with_retry_returns_first_success_without_retrying() {
let mut calls = 0;
let out: Result<u8, ()> = with_retry(|| {
calls += 1;
Ok(7)
});
assert_eq!(out, Ok(7));
assert_eq!(calls, 1);
}
#[test]
fn with_retry_succeeds_after_a_transient_failure() {
let mut calls = 0;
let out: Result<u8, &str> = with_retry(|| {
calls += 1;
if calls < 2 { Err("reset") } else { Ok(9) }
});
assert_eq!(out, Ok(9));
assert_eq!(calls, 2);
}
#[test]
fn with_retry_gives_up_and_returns_last_error() {
let mut calls = 0;
let out: Result<u8, &str> = with_retry(|| {
calls += 1;
Err("down")
});
assert_eq!(out, Err("down"));
assert_eq!(calls, 3);
}
}