use crate::diagnostic_log::warn;
use std::io::{Error, ErrorKind, Result};
use std::process::{Child, Command, ExitStatus, Output};
use std::time::Duration;
pub const START_RETRY_BUDGET: Duration = Duration::from_secs(5);
const FIRST_PAUSE: Duration = Duration::from_millis(10);
const LONGEST_PAUSE: Duration = Duration::from_millis(500);
const TAG: &str = "startRetry";
pub trait RefusableStart<T> {
fn start(&mut self) -> Result<T>;
}
pub fn retry_refused_start_with<T>(
what: &str,
budget: Duration,
pause: &mut dyn FnMut(Duration),
start: &mut dyn RefusableStart<T>,
) -> Result<T> {
let mut refusals: u32 = 0;
let mut paused: Duration = Duration::ZERO;
let mut delay: Duration = FIRST_PAUSE;
loop {
let error: Error = match start.start() {
Ok(started) => return Ok(started),
Err(error) if error.kind() == ErrorKind::WouldBlock => error,
Err(error) => return Err(error),
};
refusals += 1;
if paused + delay > budget {
return Err(Error::new(
ErrorKind::WouldBlock,
format!("{error}; the start was refused {refusals} times over {} ms", paused.as_millis()),
));
}
warn(
TAG,
format!(
"starting {what} was refused for lack of processes ({error}); trying again in {} ms",
delay.as_millis()
)
.as_str(),
);
pause(delay);
paused += delay;
delay = (delay * 2).min(LONGEST_PAUSE);
}
}
pub fn retry_refused_start<T>(what: &str, start: &mut dyn RefusableStart<T>) -> Result<T> {
return retry_refused_start_with(what, START_RETRY_BUDGET, &mut sleep_for, start);
}
fn sleep_for(delay: Duration) {
std::thread::sleep(delay);
}
pub trait RetryStart {
fn spawn_retrying(&mut self) -> Result<Child>;
fn output_retrying(&mut self) -> Result<Output>;
fn status_retrying(&mut self) -> Result<ExitStatus>;
}
struct SpawnStart<'a> {
command: &'a mut Command,
}
impl RefusableStart<Child> for SpawnStart<'_> {
fn start(&mut self) -> Result<Child> {
return self.command.spawn();
}
}
struct OutputStart<'a> {
command: &'a mut Command,
}
impl RefusableStart<Output> for OutputStart<'_> {
fn start(&mut self) -> Result<Output> {
return self.command.output();
}
}
struct StatusStart<'a> {
command: &'a mut Command,
}
impl RefusableStart<ExitStatus> for StatusStart<'_> {
fn start(&mut self) -> Result<ExitStatus> {
return self.command.status();
}
}
impl RetryStart for Command {
fn spawn_retrying(&mut self) -> Result<Child> {
let what: String = self.get_program().to_string_lossy().into_owned();
return retry_refused_start(what.as_str(), &mut SpawnStart { command: self });
}
fn output_retrying(&mut self) -> Result<Output> {
let what: String = self.get_program().to_string_lossy().into_owned();
return retry_refused_start(what.as_str(), &mut OutputStart { command: self });
}
fn status_retrying(&mut self) -> Result<ExitStatus> {
let what: String = self.get_program().to_string_lossy().into_owned();
return retry_refused_start(what.as_str(), &mut StatusStart { command: self });
}
}
#[cfg(unix)]
struct ExecStart<'a> {
command: &'a mut Command,
}
#[cfg(unix)]
impl RefusableStart<()> for ExecStart<'_> {
fn start(&mut self) -> Result<()> {
use std::os::unix::process::CommandExt;
return Err(self.command.exec());
}
}
#[cfg(unix)]
pub fn exec_retrying(command: &mut Command) -> Error {
let what: String = command.get_program().to_string_lossy().into_owned();
let refused: Result<()> = retry_refused_start(what.as_str(), &mut ExecStart { command });
match refused {
Err(error) => return error,
Ok(()) => return Error::other("exec returned without an error"),
}
}
#[cfg(test)]
#[path = "start_retry_tests.rs"]
mod tests;