use std::io;
use std::time::Duration;
pub const ETXTBSY_MAX_ATTEMPTS: u32 = 3;
pub const ETXTBSY_BACKOFF_MS: u64 = 5;
enum Step<T> {
Done(io::Result<T>),
Backoff(Duration),
}
fn step<T>(result: io::Result<T>, attempt: u32) -> Step<T> {
match result {
Err(e)
if e.kind() == io::ErrorKind::ExecutableFileBusy
&& attempt + 1 < ETXTBSY_MAX_ATTEMPTS =>
{
Step::Backoff(Duration::from_millis(
ETXTBSY_BACKOFF_MS.saturating_mul(1u64 << attempt),
))
}
other => Step::Done(other),
}
}
pub fn retry_on_etxtbsy<T>(mut attempt: impl FnMut() -> io::Result<T>) -> io::Result<T> {
let mut n = 0;
loop {
match step(attempt(), n) {
Step::Done(result) => return result,
Step::Backoff(delay) => {
std::thread::sleep(delay);
n += 1;
}
}
}
}
pub async fn retry_on_etxtbsy_async<T>(
mut attempt: impl FnMut() -> io::Result<T>,
) -> io::Result<T> {
let mut n = 0;
loop {
match step(attempt(), n) {
Step::Done(result) => return result,
Step::Backoff(delay) => {
tokio::time::sleep(delay).await;
n += 1;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
fn busy() -> io::Error {
io::Error::from_raw_os_error(26)
}
type Outcome = Option<io::ErrorKind>;
const BUSY: Outcome = Some(io::ErrorKind::ExecutableFileBusy);
const OK: Outcome = None;
fn scripted(outcomes: &[Outcome]) -> impl FnMut() -> io::Result<()> + '_ {
let mut n = 0;
move || {
assert!(
n < outcomes.len(),
"driver invoked the attempt more times than the case scripts"
);
let outcome = outcomes[n];
n += 1;
match outcome {
None => Ok(()),
Some(io::ErrorKind::ExecutableFileBusy) => Err(busy()),
Some(kind) => Err(io::Error::from(kind)),
}
}
}
const CASES: &[(&str, &[Outcome], usize)] = &[
("success on the first attempt", &[OK], 1),
("one busy, then success", &[BUSY, OK], 2),
("two busy, then success", &[BUSY, BUSY, OK], 3),
(
"permanently busy stops at the budget",
&[BUSY, BUSY, BUSY],
3,
),
(
"a non-busy error is not retried",
&[Some(io::ErrorKind::NotFound)],
1,
),
(
"a wait-phase error after a busy spawn ends the loop",
&[BUSY, Some(io::ErrorKind::Interrupted)],
2,
),
("success ends the loop even before a busy", &[OK, BUSY], 1),
];
#[test]
fn raw_os_error_26_is_executable_file_busy() {
assert_eq!(busy().kind(), io::ErrorKind::ExecutableFileBusy);
}
#[test]
fn contract_retry_invokes_attempt_at_most_max_attempts_times() {
let calls = Cell::new(0u32);
let err = retry_on_etxtbsy(|| {
calls.set(calls.get() + 1);
Err::<(), _>(busy())
})
.unwrap_err();
assert_eq!(calls.get(), ETXTBSY_MAX_ATTEMPTS);
assert_eq!(err.kind(), io::ErrorKind::ExecutableFileBusy);
let calls = Cell::new(0u32);
let ok = retry_on_etxtbsy(|| {
calls.set(calls.get() + 1);
Ok(7)
})
.unwrap();
assert_eq!((calls.get(), ok), (1, 7));
let calls = Cell::new(0u32);
let err = retry_on_etxtbsy(|| {
calls.set(calls.get() + 1);
Err::<(), _>(io::Error::from(io::ErrorKind::PermissionDenied))
})
.unwrap_err();
assert_eq!(calls.get(), 1, "a non-ETXTBSY error must never be retried");
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
}
#[tokio::test]
async fn contract_async_retry_matches_the_blocking_policy() {
let calls = Cell::new(0u32);
let err = retry_on_etxtbsy_async(|| {
calls.set(calls.get() + 1);
Err::<(), _>(busy())
})
.await
.unwrap_err();
assert_eq!(calls.get(), ETXTBSY_MAX_ATTEMPTS);
assert_eq!(err.kind(), io::ErrorKind::ExecutableFileBusy);
let calls = Cell::new(0u32);
let ok = retry_on_etxtbsy_async(|| {
calls.set(calls.get() + 1);
Ok(7)
})
.await
.unwrap();
assert_eq!((calls.get(), ok), (1, 7));
let calls = Cell::new(0u32);
let err = retry_on_etxtbsy_async(|| {
calls.set(calls.get() + 1);
Err::<(), _>(io::Error::from(io::ErrorKind::PermissionDenied))
})
.await
.unwrap_err();
assert_eq!(calls.get(), 1);
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn returns_first_success() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy(|| {
calls.set(calls.get() + 1);
Ok::<_, io::Error>(7)
});
assert_eq!(got.unwrap(), 7);
assert_eq!(calls.get(), 1, "a success must not be retried");
}
#[test]
fn recovers_after_two_busy_attempts() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy(|| {
calls.set(calls.get() + 1);
if calls.get() < 3 { Err(busy()) } else { Ok(7) }
});
assert_eq!(got.unwrap(), 7);
assert_eq!(calls.get(), 3);
}
#[test]
fn gives_up_after_max_attempts() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy(|| {
calls.set(calls.get() + 1);
Err::<(), _>(busy())
});
assert_eq!(got.unwrap_err().kind(), io::ErrorKind::ExecutableFileBusy);
assert_eq!(calls.get(), ETXTBSY_MAX_ATTEMPTS as i32);
}
#[test]
fn does_not_retry_other_errors() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy(|| {
calls.set(calls.get() + 1);
Err::<(), _>(io::Error::from(io::ErrorKind::NotFound))
});
assert_eq!(got.unwrap_err().kind(), io::ErrorKind::NotFound);
assert_eq!(calls.get(), 1, "a missing binary must fail on attempt 1");
}
#[test]
fn retries_only_after_an_etxtbsy_outcome() {
for (name, outcomes, want_invocations) in CASES {
let mut attempt = scripted(outcomes);
let mut invocations = 0;
let _ = retry_on_etxtbsy(|| {
invocations += 1;
attempt()
});
assert_eq!(invocations, *want_invocations, "case: {name}");
}
}
#[tokio::test]
async fn async_returns_first_success() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy_async(|| {
calls.set(calls.get() + 1);
Ok::<_, io::Error>(7)
})
.await;
assert_eq!(got.unwrap(), 7);
assert_eq!(calls.get(), 1, "a success must not be retried");
}
#[tokio::test]
async fn async_recovers_after_two_busy_attempts() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy_async(|| {
calls.set(calls.get() + 1);
if calls.get() < 3 { Err(busy()) } else { Ok(7) }
})
.await;
assert_eq!(got.unwrap(), 7);
assert_eq!(calls.get(), 3);
}
#[tokio::test]
async fn async_gives_up_after_max_attempts() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy_async(|| {
calls.set(calls.get() + 1);
Err::<(), _>(busy())
})
.await;
assert_eq!(got.unwrap_err().kind(), io::ErrorKind::ExecutableFileBusy);
assert_eq!(calls.get(), ETXTBSY_MAX_ATTEMPTS as i32);
}
#[tokio::test]
async fn async_does_not_retry_other_errors() {
let calls = Cell::new(0);
let got = retry_on_etxtbsy_async(|| {
calls.set(calls.get() + 1);
Err::<(), _>(io::Error::from(io::ErrorKind::PermissionDenied))
})
.await;
assert_eq!(got.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
assert_eq!(calls.get(), 1);
}
#[tokio::test]
async fn async_retries_only_after_an_etxtbsy_outcome() {
for (name, outcomes, want_invocations) in CASES {
let mut attempt = scripted(outcomes);
let mut invocations = 0;
let _ = retry_on_etxtbsy_async(|| {
invocations += 1;
attempt()
})
.await;
assert_eq!(invocations, *want_invocations, "case: {name}");
}
}
}