chj_unix_util/
backoff.rs

1//! Run a job in an interval that is increased if there are errors,
2//! and lowered again if there aren't.
3
4//! Todo: measure time and subtract the job running time from the
5//! sleep time? Or at least, when gettng an error, take the passed
6//! time as the basis, not the `min_sleep_seconds` time (since that
7//! might be 0, even).
8
9use std::{
10    borrow::Cow,
11    fmt::Display,
12    thread::sleep,
13    time::{Duration, SystemTime},
14};
15
16/// Whether the loop should give additional messaging about its own
17/// working (this excludes messages about handling errors, and is just
18/// about reporting on normal working)
19pub enum LoopVerbosity {
20    Silent,
21    LogEveryIteration,
22    /// NOTE: the interval is at *least* as long; it is never shorter
23    /// than the loop sleep time.
24    LogActivityInterval {
25        every_n_seconds: u64,
26    },
27}
28
29/// Configuration data; implements `Default` so you can initialize an
30/// instance setting only the fields that you want to change (most
31/// likely only the `*_sleep_seconds` values).
32pub struct LoopWithBackoff {
33    /// Prefix for the "loop iteration" and other "loop" messages;
34    /// e.g. say what is being looped.
35    pub prefix: Cow<'static, str>,
36    /// Whether to enable additional diagnostic messages to stderr
37    /// (default: `LoopVerbosity::LogSleepTimeEveryIteration`).
38    pub verbosity: LoopVerbosity,
39    /// Whether to silence diagnostic messages to stderr about errors
40    /// (default: false).
41    pub quiet: bool,
42    /// The number to multiply the sleep time with in case of error (should be > 1)
43    pub error_sleep_factor: f64,
44    /// The number to multiply the sleep time with in case of success
45    /// (should be between 0 and 1)
46    pub success_sleep_factor: f64,
47    /// The number of seconds to sleep at minimum (do not use 0 since
48    /// then it will never back off!)
49    pub min_sleep_seconds: f64,
50    /// The number of seconds to sleep at maximum (should be >
51    /// `min_sleep_seconds`).
52    pub max_sleep_seconds: f64,
53}
54
55impl Default for LoopWithBackoff {
56    fn default() -> Self {
57        Self {
58            prefix: "".into(),
59            verbosity: LoopVerbosity::LogEveryIteration,
60            quiet: false,
61            error_sleep_factor: 1.05,
62            success_sleep_factor: 0.99,
63            min_sleep_seconds: 1.,
64            max_sleep_seconds: 1000.,
65        }
66    }
67}
68
69impl LoopWithBackoff {
70    /// Loop running `job` then sleeping at least `min_seconds`, if
71    /// `job` returns an `Err`, increases the sleep time. Runs `until`
72    /// after every run, and returns if it returns true.
73    pub fn run<E: Display>(
74        &self,
75        mut job: impl FnMut() -> Result<(), E>,
76        until: impl Fn() -> bool,
77    ) {
78        let prefix = self.prefix.as_ref();
79        let mut sleep_seconds = self.min_sleep_seconds;
80        let mut iteration_count: u64 = 0;
81        let mut last_lai_time: Option<SystemTime> = None;
82        loop {
83            let result = job();
84            if let Err(e) = result {
85                // XX e:# ? but we only have Display! Can't require
86                // std::error::Error since anyhow::Error (in the
87                // version that I'm using) is not implementing that.
88                if !self.quiet {
89                    eprintln!("{prefix}loop: got error: {e:#}");
90                }
91                sleep_seconds =
92                    (sleep_seconds * self.error_sleep_factor).min(self.max_sleep_seconds);
93            } else {
94                sleep_seconds = (sleep_seconds * 0.99).max(self.min_sleep_seconds);
95            }
96            if until() {
97                return;
98            }
99            iteration_count += 1;
100            let verbose_print = || {
101                eprintln!(
102                    "{prefix}loop iteration {iteration_count}, \
103                     sleeping {sleep_seconds} seconds"
104                )
105            };
106            match self.verbosity {
107                LoopVerbosity::Silent => (),
108                LoopVerbosity::LogEveryIteration => {
109                    verbose_print();
110                }
111                LoopVerbosity::LogActivityInterval { every_n_seconds } => {
112                    let now = SystemTime::now();
113                    if let Some(last) = last_lai_time {
114                        match now.duration_since(last) {
115                            Ok(passed) => {
116                                if passed.as_secs() >= every_n_seconds {
117                                    verbose_print();
118                                    last_lai_time = Some(now);
119                                }
120                            }
121                            Err(_) => {
122                                eprintln!(
123                                    "{prefix}loop: error calculating duration, erroneous clock? \
124                                     last: {last:?} vs. now {now:?}"
125                                );
126                                // Set it in hopes of it recovering
127                                last_lai_time = Some(now);
128                            }
129                        }
130                    } else {
131                        // Have to set it if it was never set or it
132                        // will remain off forever.
133                        last_lai_time = Some(now);
134                    }
135                }
136            }
137            sleep(Duration::from_secs_f64(sleep_seconds));
138        }
139    }
140}