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}