app_frame/
backoff.rs

1use std::collections::hash_map::Entry;
2use std::collections::HashMap;
3use std::hash::Hash;
4use std::sync::Arc;
5
6use crate::time::Clock;
7use crate::time::SystemClock;
8
9/// Tracks events that you don't want to repeat very often, and lets you know
10/// when it's safe to take the action that may cause those events.
11pub struct BackoffTracker<T> {
12    field: HashMap<T, BackoffEntry>,
13    clock: Arc<dyn Clock>,
14    config: BackoffConfig,
15}
16
17impl<T> Default for BackoffTracker<T> {
18    fn default() -> Self {
19        Self::new(Arc::new(SystemClock::default()), BackoffConfig::default())
20    }
21}
22
23impl<T> BackoffTracker<T> {
24    pub fn new(clock: Arc<dyn Clock>, config: BackoffConfig) -> Self {
25        Self {
26            field: HashMap::new(),
27            clock,
28            config,
29        }
30    }
31}
32
33/// High level management of code execution using backoff tracking primitives.
34impl<'t, T: Eq + Hash + 't> BackoffTracker<T> {
35    /// Use this if you'd like to rate limit some code regardless of its outcome.
36    ///
37    /// Triggers an event on any execution. see `backoff_generic`.
38    pub fn backoff<F: Fn() -> X, X>(&mut self, id: T, f: F) -> Option<X> {
39        self.backoff_generic(id, f, false, |_| true)
40    }
41
42    /// Use this if you'd like to rate limit some code regardless of its
43    /// outcome, but not if other events occur in between.
44    ///
45    /// Triggers an event on any execution. see `backoff_generic` and
46    /// `singular_event`
47    pub fn backoff_singular<F: Fn() -> X, X>(&mut self, id: T, f: F) -> Option<X> {
48        self.backoff_generic(id, f, true, |_| true)
49    }
50
51    /// Use this if you'd like to rate limit some code when it fails.
52    /// Triggers an event on Err. see `backoff_generic`.
53    pub fn backoff_errors<F, O, E>(&mut self, id: T, f: F) -> Option<Result<O, E>>
54    where
55        F: Fn() -> Result<O, E>,
56    {
57        self.backoff_generic(id, f, false, Result::is_err)
58    }
59
60    /// Use this if you'd like to rate limit some code when it succeeds.
61    /// Triggers an event on Ok. see `backoff_generic`.
62    pub fn backoff_oks<F, O, E>(&mut self, id: T, f: F) -> Option<Result<O, E>>
63    where
64        F: Fn() -> Result<O, E>,
65    {
66        self.backoff_generic(id, f, false, Result::is_ok)
67    }
68
69    /// Generic function used by other `backoff_` functions.
70    ///
71    /// Run the code if ready. Trigger an event when the predicate is true
72    /// Returns None if the execution was skipped due to not being ready.
73    pub fn backoff_generic<F, X, P>(
74        &mut self,
75        id: T,
76        f: F,
77        singular: bool,
78        predicate: P,
79    ) -> Option<X>
80    where
81        F: Fn() -> X,
82        P: Fn(&X) -> bool,
83    {
84        if self.is_ready(&id) {
85            let result = f();
86            if predicate(&result) {
87                if singular && !self.field.contains_key(&id) {
88                    self.field = HashMap::new();
89                }
90                self.event(id);
91            }
92            Some(result)
93        } else {
94            None
95        }
96    }
97}
98
99/// Primitives to track the backoff
100impl<'t, T: Eq + Hash + 't> BackoffTracker<T> {
101    pub fn event(&mut self, item: T) {
102        let now = self.clock.current_timestamp();
103        match self.field.entry(item) {
104            Entry::Vacant(entry) => {
105                entry.insert(BackoffEntry {
106                    count: 1,
107                    last: now,
108                    delay: self.config.initial_delay(),
109                });
110            }
111            Entry::Occupied(mut entry) => {
112                let BackoffEntry { count, last, delay } = entry.get();
113                if last + delay < now {
114                    let delay = match self.config.strategy {
115                        BackoffStrategy::Exponential(b) => {
116                            std::cmp::min(delay * b, self.config.max_delay)
117                        }
118                        BackoffStrategy::Cliff(c) => {
119                            if count > &c {
120                                self.config.max_delay
121                            } else {
122                                self.config.min_delay
123                            }
124                        }
125                    };
126                    entry.insert(BackoffEntry {
127                        count: count + 1,
128                        last: now,
129                        delay,
130                    });
131                }
132            }
133        }
134    }
135
136    /// Like `event`, but it clears all history of other events when a new event
137    /// is received.
138    ///
139    /// Use this when you only need to backoff events that are consecutive
140    /// duplicates, but don't need to backoff events if different events occur
141    /// in between.
142    pub fn singular_event(&mut self, item: T) {
143        if !self.field.contains_key(&item) {
144            self.field = HashMap::new();
145        }
146        self.event(item);
147    }
148
149    pub fn clear(&mut self, item: &T) {
150        if self.field.contains_key(item) {
151            self.field.remove(item);
152        }
153    }
154
155    pub fn clear_many<'a>(&mut self, items: impl IntoIterator<Item = &'a T>)
156    where
157        't: 'a,
158    {
159        for item in items {
160            self.field.remove(item);
161        }
162    }
163
164    pub fn is_ready(&self, item: &T) -> bool {
165        match self.field.get(item) {
166            Some(x) => self.clock.current_timestamp() > x.last + x.delay,
167            None => true,
168        }
169    }
170}
171
172/// Events must not be recorded here if they occurred...
173/// - within a delay window
174/// - before the last `clear`
175struct BackoffEntry {
176    /// Total quantity of registered events that occurred outside a delay window.
177    count: u64,
178    /// The time that the last event occurred outside a delay window.
179    last: u64,
180    /// The time window to wait since last_event before being "ready"
181    delay: u64,
182}
183
184pub struct BackoffConfig {
185    /// The minimum amount of time to wait before trying again.
186    pub min_delay: u64,
187    /// The maximum amount of time to wait before trying again.
188    pub max_delay: u64,
189    /// How to progress from min_delay to max_delay.
190    pub strategy: BackoffStrategy,
191}
192
193impl BackoffConfig {
194    /// Simple rate limiter with a constant rate, doesn't backoff progressively.
195    pub fn constant(period: u64) -> Self {
196        Self {
197            min_delay: period,
198            max_delay: period,
199            strategy: BackoffStrategy::Cliff(0),
200        }
201    }
202
203    /// Once an event occurs, it will never be ready again unless cleared.
204    pub fn once() -> Self {
205        Self {
206            min_delay: u64::MAX,
207            max_delay: u64::MAX,
208            strategy: BackoffStrategy::Cliff(0),
209        }
210    }
211
212    pub fn initial_delay(&self) -> u64 {
213        match self.strategy {
214            BackoffStrategy::Cliff(0) => self.max_delay,
215            _ => self.min_delay,
216        }
217    }
218}
219
220impl Default for BackoffConfig {
221    fn default() -> Self {
222        Self {
223            min_delay: 30,
224            max_delay: 3600,
225            strategy: Default::default(),
226        }
227    }
228}
229
230pub enum BackoffStrategy {
231    /// The wrapped integer is the base b of the exponential b^n where n is the
232    /// number of events. Each event will trigger a delay b times as large as
233    /// the prior delay.
234    Exponential(u64),
235
236    /// Cliff means you wait exactly the minimum delay for some number of
237    /// events, then you suddenly switch to waiting the maximum interval for
238    /// each event. The wrapped integer is the number of times to delay with the
239    /// minimum interval before switching to the max interval.
240    Cliff(u64),
241}
242
243impl Default for BackoffStrategy {
244    fn default() -> Self {
245        // 3 is the closest integer to Euler's constant, which makes it the
246        // obvious choice as a standard base for an exponential. It also
247        // subjectively feels to me like a generally satisfying rate of cooldown
248        // in most cases, where 2 feels overly aggressive.
249        BackoffStrategy::Exponential(3)
250    }
251}