aoc_runtime/aoc/
throttle.rs1use std::{
18 cell::Cell,
19 fs,
20 path::PathBuf,
21 thread,
22 time::{Duration, SystemTime, UNIX_EPOCH},
23};
24
25pub const MIN_INTERVAL: Duration = Duration::from_secs(5);
32
33const LAST_REQUEST_FILE: &str = "last-request";
35
36pub trait Timer {
42 fn now(&self) -> SystemTime;
44
45 fn wait(&self, duration: Duration);
47}
48
49#[derive(Debug, Default, Clone, Copy)]
51pub struct SystemTimer;
52
53impl Timer for SystemTimer {
54 fn now(&self) -> SystemTime {
55 SystemTime::now()
56 }
57
58 fn wait(&self, duration: Duration) {
59 thread::sleep(duration);
60 }
61}
62
63#[derive(Debug)]
65pub struct Throttle<T = SystemTimer> {
66 path: PathBuf,
67 timer: T,
68 interval: Duration,
69 last: Cell<Option<SystemTime>>,
70}
71
72impl Throttle {
73 #[must_use]
76 pub fn new(state_dir: impl Into<PathBuf>) -> Self {
77 Self::with_timer(state_dir, SystemTimer, MIN_INTERVAL)
78 }
79}
80
81impl<T: Timer> Throttle<T> {
82 #[must_use]
84 pub fn with_timer(state_dir: impl Into<PathBuf>, timer: T, interval: Duration) -> Self {
85 Self {
86 path: state_dir.into().join(LAST_REQUEST_FILE),
87 timer,
88 interval,
89 last: Cell::new(None),
90 }
91 }
92
93 pub fn acquire(&self) {
95 let now = self.timer.now();
96 let wait = self.remaining(now);
97
98 if !wait.is_zero() {
99 self.timer.wait(wait);
100 }
101
102 self.record(now.checked_add(wait).unwrap_or(now));
103 }
104
105 fn remaining(&self, now: SystemTime) -> Duration {
107 self.last_request()
108 .and_then(|last| last.checked_add(self.interval))
109 .and_then(|earliest| earliest.duration_since(now).ok())
110 .map_or(Duration::ZERO, |wait| wait.min(self.interval))
113 }
114
115 fn last_request(&self) -> Option<SystemTime> {
118 let persisted = fs::read_to_string(&self.path)
119 .ok()
120 .and_then(|text| text.trim().parse::<u64>().ok())
121 .and_then(|millis| UNIX_EPOCH.checked_add(Duration::from_millis(millis)));
122
123 match (self.last.get(), persisted) {
124 (Some(memory), Some(disk)) => Some(memory.max(disk)),
125 (memory, disk) => memory.or(disk),
126 }
127 }
128
129 fn record(&self, at: SystemTime) {
135 self.last.set(Some(at));
136
137 let Ok(since_epoch) = at.duration_since(UNIX_EPOCH) else {
138 return;
139 };
140 let Ok(millis) = u64::try_from(since_epoch.as_millis()) else {
141 return;
142 };
143 let Some(parent) = self.path.parent() else {
144 return;
145 };
146
147 if fs::create_dir_all(parent).is_ok() {
148 let _ = fs::write(&self.path, millis.to_string());
149 }
150 }
151}
152
153#[cfg(test)]
154pub(crate) mod fake {
155 use super::{Duration, SystemTime, Timer};
156 use std::cell::{Cell, RefCell};
157
158 #[derive(Debug)]
160 pub(crate) struct FakeTimer {
161 now: Cell<SystemTime>,
162 waits: RefCell<Vec<Duration>>,
163 }
164
165 impl FakeTimer {
166 pub(crate) fn new(now: SystemTime) -> Self {
167 Self {
168 now: Cell::new(now),
169 waits: RefCell::new(Vec::new()),
170 }
171 }
172
173 pub(crate) fn advance(&self, by: Duration) {
174 self.now.set(self.now.get() + by);
175 }
176
177 pub(crate) fn waits(&self) -> Vec<Duration> {
179 self.waits.borrow().clone()
180 }
181 }
182
183 impl Timer for FakeTimer {
184 fn now(&self) -> SystemTime {
185 self.now.get()
186 }
187
188 fn wait(&self, duration: Duration) {
189 self.waits.borrow_mut().push(duration);
190 self.advance(duration);
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::{fake::FakeTimer, *};
198
199 const INTERVAL: Duration = Duration::from_secs(5);
200
201 fn at(seconds: u64) -> SystemTime {
202 UNIX_EPOCH + Duration::from_secs(seconds)
203 }
204
205 fn throttle(state_dir: impl Into<PathBuf>, now: u64) -> Throttle<FakeTimer> {
206 Throttle::with_timer(state_dir, FakeTimer::new(at(now)), INTERVAL)
207 }
208
209 #[test]
210 fn the_first_request_is_not_delayed() {
211 let dir = tempfile::tempdir().expect("temp dir");
212 let throttle = throttle(dir.path(), 1000);
213
214 throttle.acquire();
215
216 assert!(throttle.timer.waits().is_empty());
217 }
218
219 #[test]
220 fn a_burst_is_spaced_out() {
221 let dir = tempfile::tempdir().expect("temp dir");
222 let throttle = throttle(dir.path(), 1000);
223
224 throttle.acquire();
225 throttle.acquire();
226 throttle.acquire();
227
228 assert_eq!(throttle.timer.waits(), [INTERVAL, INTERVAL]);
229 }
230
231 #[test]
232 fn waiting_out_the_interval_costs_nothing() {
233 let dir = tempfile::tempdir().expect("temp dir");
234 let throttle = throttle(dir.path(), 1000);
235
236 throttle.acquire();
237 throttle.timer.advance(INTERVAL);
238 throttle.acquire();
239
240 assert!(throttle.timer.waits().is_empty());
241 }
242
243 #[test]
244 fn the_gap_is_honoured_across_invocations() {
245 let dir = tempfile::tempdir().expect("temp dir");
246 throttle(dir.path(), 1000).acquire();
247
248 let next = throttle(dir.path(), 1002);
251 next.acquire();
252
253 assert_eq!(next.timer.waits(), [Duration::from_secs(3)]);
254 }
255
256 #[test]
257 fn an_unwritable_state_directory_still_throttles_in_process() {
258 let throttle = throttle("/proc/definitely-not-writable", 1000);
259
260 throttle.acquire();
261 throttle.acquire();
262
263 assert_eq!(throttle.timer.waits(), [INTERVAL]);
264 }
265
266 #[test]
267 fn a_timestamp_from_the_future_waits_at_most_one_interval() {
268 let dir = tempfile::tempdir().expect("temp dir");
269 fs::write(dir.path().join(LAST_REQUEST_FILE), "99000000000000").expect("seed timestamp");
270
271 let throttle = throttle(dir.path(), 1000);
272 throttle.acquire();
273
274 assert_eq!(throttle.timer.waits(), [INTERVAL]);
275 }
276
277 #[test]
278 fn an_unreadable_record_does_not_block_the_first_request() {
279 let dir = tempfile::tempdir().expect("temp dir");
280 fs::write(dir.path().join(LAST_REQUEST_FILE), "not a timestamp").expect("seed timestamp");
281
282 let throttle = throttle(dir.path(), 1000);
283 throttle.acquire();
284
285 assert!(throttle.timer.waits().is_empty());
286 }
287}