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}