start_command/
recovery_delay.rs1use std::time::Duration;
14
15use serde_json::Value;
16
17pub const CANCEL_CHECK_INTERVAL_MS: u64 = 1000;
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct RecoveryDelayRange {
23 pub min_seconds: f64,
24 pub max_seconds: f64,
25}
26
27fn parse_seconds(text: &str) -> Option<f64> {
28 let (whole, fraction) = match text.split_once('.') {
29 Some((whole, fraction)) => (whole, Some(fraction)),
30 None => (text, None),
31 };
32 let digits = |part: &str| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit());
33 if !digits(whole) || fraction.is_some_and(|fraction| !digits(fraction)) {
34 return None;
35 }
36 text.parse::<f64>().ok().filter(|value| value.is_finite())
37}
38
39pub fn parse_recovery_delay_range(value: &str) -> Option<RecoveryDelayRange> {
41 let text = value.trim();
42 let (min, max) = match text.split_once('-') {
43 Some((min, max)) => (parse_seconds(min)?, parse_seconds(max)?),
44 None => {
45 let seconds = parse_seconds(text)?;
46 (seconds, seconds)
47 }
48 };
49 (max >= min).then_some(RecoveryDelayRange {
50 min_seconds: min,
51 max_seconds: max,
52 })
53}
54
55fn format_seconds(seconds: f64) -> String {
56 format!("{}", seconds)
58}
59
60pub fn format_recovery_delay_range(range: &RecoveryDelayRange) -> String {
62 if range.min_seconds == range.max_seconds {
63 format_seconds(range.min_seconds)
64 } else {
65 format!(
66 "{}-{}",
67 format_seconds(range.min_seconds),
68 format_seconds(range.max_seconds)
69 )
70 }
71}
72
73pub fn parse_on_kill_resume_delay_value(value: &str) -> Result<String, String> {
75 parse_recovery_delay_range(value)
76 .map(|range| format_recovery_delay_range(&range))
77 .ok_or_else(|| {
78 format!(
79 "Invalid --on-kill-resume-delay value: \"{}\". Expected seconds as <min>[-<max>] with max >= min, e.g. 30-90.",
80 value
81 )
82 })
83}
84
85pub fn effective_on_kill_resume_delay(value: Option<&str>) -> Option<String> {
87 let range = parse_recovery_delay_range(value?)?;
88 (range.max_seconds > 0.0).then(|| format_recovery_delay_range(&range))
89}
90
91pub fn record_on_kill_resume_delay(value: Option<&Value>) -> Option<String> {
94 match value? {
95 Value::String(text) => effective_on_kill_resume_delay(Some(text)),
96 Value::Number(number) => effective_on_kill_resume_delay(Some(&number.to_string())),
97 _ => None,
98 }
99}
100
101pub fn system_random() -> f64 {
104 let bytes = uuid::Uuid::new_v4().into_bytes();
105 let mut word = [0u8; 8];
106 word.copy_from_slice(&bytes[..8]);
107 (u64::from_le_bytes(word) >> 11) as f64 / (1u64 << 53) as f64
109}
110
111pub fn pick_recovery_delay_ms(value: Option<&str>, random: &dyn Fn() -> f64) -> u64 {
113 let Some(range) = value.and_then(parse_recovery_delay_range) else {
114 return 0;
115 };
116 let min_ms = range.min_seconds * 1000.0;
117 let max_ms = range.max_seconds * 1000.0;
118 (min_ms + (max_ms - min_ms) * random()).round() as u64
119}
120
121pub fn format_recovery_delay(delay_ms: u64) -> String {
123 format!("{}s", (delay_ms as f64 / 100.0).round() / 10.0)
124}
125
126pub fn system_sleep(ms: u64) {
128 std::thread::sleep(Duration::from_millis(ms));
129}
130
131pub fn wait_for_recovery_delay(
134 delay_ms: u64,
135 sleep: &dyn Fn(u64),
136 should_cancel: &dyn Fn() -> bool,
137) -> bool {
138 let mut remaining = delay_ms;
139 while remaining > 0 {
140 if should_cancel() {
141 return true;
142 }
143 let step = remaining.min(CANCEL_CHECK_INTERVAL_MS);
144 sleep(step);
145 remaining -= step;
146 }
147 should_cancel()
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn system_random_stays_in_the_unit_interval() {
156 for _ in 0..1000 {
157 let value = system_random();
158 assert!((0.0..1.0).contains(&value));
159 }
160 }
161}