use std::time::Duration;
use serde_json::Value;
pub const CANCEL_CHECK_INTERVAL_MS: u64 = 1000;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RecoveryDelayRange {
pub min_seconds: f64,
pub max_seconds: f64,
}
fn parse_seconds(text: &str) -> Option<f64> {
let (whole, fraction) = match text.split_once('.') {
Some((whole, fraction)) => (whole, Some(fraction)),
None => (text, None),
};
let digits = |part: &str| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit());
if !digits(whole) || fraction.is_some_and(|fraction| !digits(fraction)) {
return None;
}
text.parse::<f64>().ok().filter(|value| value.is_finite())
}
pub fn parse_recovery_delay_range(value: &str) -> Option<RecoveryDelayRange> {
let text = value.trim();
let (min, max) = match text.split_once('-') {
Some((min, max)) => (parse_seconds(min)?, parse_seconds(max)?),
None => {
let seconds = parse_seconds(text)?;
(seconds, seconds)
}
};
(max >= min).then_some(RecoveryDelayRange {
min_seconds: min,
max_seconds: max,
})
}
fn format_seconds(seconds: f64) -> String {
format!("{}", seconds)
}
pub fn format_recovery_delay_range(range: &RecoveryDelayRange) -> String {
if range.min_seconds == range.max_seconds {
format_seconds(range.min_seconds)
} else {
format!(
"{}-{}",
format_seconds(range.min_seconds),
format_seconds(range.max_seconds)
)
}
}
pub fn parse_on_kill_resume_delay_value(value: &str) -> Result<String, String> {
parse_recovery_delay_range(value)
.map(|range| format_recovery_delay_range(&range))
.ok_or_else(|| {
format!(
"Invalid --on-kill-resume-delay value: \"{}\". Expected seconds as <min>[-<max>] with max >= min, e.g. 30-90.",
value
)
})
}
pub fn effective_on_kill_resume_delay(value: Option<&str>) -> Option<String> {
let range = parse_recovery_delay_range(value?)?;
(range.max_seconds > 0.0).then(|| format_recovery_delay_range(&range))
}
pub fn record_on_kill_resume_delay(value: Option<&Value>) -> Option<String> {
match value? {
Value::String(text) => effective_on_kill_resume_delay(Some(text)),
Value::Number(number) => effective_on_kill_resume_delay(Some(&number.to_string())),
_ => None,
}
}
pub fn system_random() -> f64 {
let bytes = uuid::Uuid::new_v4().into_bytes();
let mut word = [0u8; 8];
word.copy_from_slice(&bytes[..8]);
(u64::from_le_bytes(word) >> 11) as f64 / (1u64 << 53) as f64
}
pub fn pick_recovery_delay_ms(value: Option<&str>, random: &dyn Fn() -> f64) -> u64 {
let Some(range) = value.and_then(parse_recovery_delay_range) else {
return 0;
};
let min_ms = range.min_seconds * 1000.0;
let max_ms = range.max_seconds * 1000.0;
(min_ms + (max_ms - min_ms) * random()).round() as u64
}
pub fn format_recovery_delay(delay_ms: u64) -> String {
format!("{}s", (delay_ms as f64 / 100.0).round() / 10.0)
}
pub fn system_sleep(ms: u64) {
std::thread::sleep(Duration::from_millis(ms));
}
pub fn wait_for_recovery_delay(
delay_ms: u64,
sleep: &dyn Fn(u64),
should_cancel: &dyn Fn() -> bool,
) -> bool {
let mut remaining = delay_ms;
while remaining > 0 {
if should_cancel() {
return true;
}
let step = remaining.min(CANCEL_CHECK_INTERVAL_MS);
sleep(step);
remaining -= step;
}
should_cancel()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_random_stays_in_the_unit_interval() {
for _ in 0..1000 {
let value = system_random();
assert!((0.0..1.0).contains(&value));
}
}
}